@camstack/addon-terminal 0.1.156 → 0.1.157

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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.157] - 2026-10-31
11
+
12
+ ### Changed
13
+
14
+ - Deleting a terminal whose node left the cluster now completes instead of hanging
15
+
10
16
  ## [0.1.156] - 2026-10-30
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -97868,6 +97868,8 @@ var SNAPSHOT_SETTLE_MAX_PULLS = 20;
97868
97868
  var CLOSE_RETRY_BASE_MS = 50;
97869
97869
  var CLOSE_RETRY_MAX_MS = 1e3;
97870
97870
  var CLOSE_ATTEMPTS_PER_PASS = 3;
97871
+ /** How long a delete/edit waits for a node to acknowledge a camera close. */
97872
+ var CLOSE_INSTANCE_MAX_WAIT_MS = 5e3;
97871
97873
  function relayKey(instanceId) {
97872
97874
  return instanceId;
97873
97875
  }
@@ -98222,18 +98224,43 @@ var TerminalCameraRelay = class {
98222
98224
  if (state.clients === 0 && state.leases === 0) await this.closeState(state);
98223
98225
  }
98224
98226
  }
98225
- /** Stop a withdrawn/offline camera's relay, including active HTTP readers. */
98226
- async closeInstance(instanceId) {
98227
+ /**
98228
+ * Stop a withdrawn/offline camera's relay, including active HTTP readers.
98229
+ *
98230
+ * The remote close/open is addressed to the instance's node. A node that
98231
+ * left the cluster never answers, so the wait is bounded: the retrying
98232
+ * cleanup keeps running in the background, but a caller (a delete, an edit)
98233
+ * is never held hostage by a node that cannot reply.
98234
+ */
98235
+ async closeInstance(instanceId, maxWaitMs = CLOSE_INSTANCE_MAX_WAIT_MS) {
98227
98236
  const state = this.states.get(relayKey(instanceId));
98228
98237
  if (!state) return;
98229
98238
  for (const response of state.responses) response.destroy();
98230
98239
  state.responses.clear();
98231
98240
  state.clients = 0;
98232
98241
  state.leases = 0;
98233
- await this.closeState(state);
98234
- if (state.openPromise) {
98235
- await state.openPromise.catch(() => {});
98242
+ const closing = (async () => {
98236
98243
  await this.closeState(state);
98244
+ if (state.openPromise) {
98245
+ await state.openPromise.catch(() => {});
98246
+ await this.closeState(state);
98247
+ }
98248
+ })();
98249
+ let timer;
98250
+ const bound = new Promise((resolve) => {
98251
+ timer = setTimeout(() => resolve("timeout"), maxWaitMs);
98252
+ });
98253
+ const outcome = await Promise.race([closing.then(() => "closed"), bound]);
98254
+ if (timer) clearTimeout(timer);
98255
+ if (outcome === "timeout") {
98256
+ closing.catch(() => {});
98257
+ this.logger.warn("terminal camera close still pending — node did not answer; continuing", {
98258
+ ...this.tagsFor(instanceId),
98259
+ meta: {
98260
+ nodeId: state.nodeId,
98261
+ waitedMs: maxWaitMs
98262
+ }
98263
+ });
98237
98264
  }
98238
98265
  }
98239
98266
  async dispose() {
@@ -99547,11 +99574,17 @@ var TerminalAddon = class extends BaseAddon {
99547
99574
  } });
99548
99575
  });
99549
99576
  this.terminalCameraTombstones.add(instance.cameraStableId);
99550
- await this.cameraRelay?.closeInstance(instance.id);
99551
99577
  await this.updateGlobalSettings({
99552
99578
  terminalInstances: this.config.terminalInstances.filter((candidate) => candidate.id !== instanceId),
99553
99579
  terminalCameraTombstones: [...new Set([...this.config.terminalCameraTombstones, instance.cameraStableId])]
99554
99580
  });
99581
+ await this.cameraRelay?.closeInstance(instance.id).catch((error) => {
99582
+ this.ctx.logger.warn("could not close the deleted Terminal instance session", { meta: {
99583
+ instanceId: instance.id,
99584
+ nodeId: instance.nodeId,
99585
+ error: error instanceof Error ? error.message : String(error)
99586
+ } });
99587
+ });
99555
99588
  }
99556
99589
  async setTerminalInstanceEnabled(instanceId, enabled) {
99557
99590
  const instance = await this.instanceMutationQueue.run(() => this.setTerminalInstanceEnabledUnlocked(instanceId, enabled));
package/dist/addon.mjs CHANGED
@@ -97845,6 +97845,8 @@ var SNAPSHOT_SETTLE_MAX_PULLS = 20;
97845
97845
  var CLOSE_RETRY_BASE_MS = 50;
97846
97846
  var CLOSE_RETRY_MAX_MS = 1e3;
97847
97847
  var CLOSE_ATTEMPTS_PER_PASS = 3;
97848
+ /** How long a delete/edit waits for a node to acknowledge a camera close. */
97849
+ var CLOSE_INSTANCE_MAX_WAIT_MS = 5e3;
97848
97850
  function relayKey(instanceId) {
97849
97851
  return instanceId;
97850
97852
  }
@@ -98199,18 +98201,43 @@ var TerminalCameraRelay = class {
98199
98201
  if (state.clients === 0 && state.leases === 0) await this.closeState(state);
98200
98202
  }
98201
98203
  }
98202
- /** Stop a withdrawn/offline camera's relay, including active HTTP readers. */
98203
- async closeInstance(instanceId) {
98204
+ /**
98205
+ * Stop a withdrawn/offline camera's relay, including active HTTP readers.
98206
+ *
98207
+ * The remote close/open is addressed to the instance's node. A node that
98208
+ * left the cluster never answers, so the wait is bounded: the retrying
98209
+ * cleanup keeps running in the background, but a caller (a delete, an edit)
98210
+ * is never held hostage by a node that cannot reply.
98211
+ */
98212
+ async closeInstance(instanceId, maxWaitMs = CLOSE_INSTANCE_MAX_WAIT_MS) {
98204
98213
  const state = this.states.get(relayKey(instanceId));
98205
98214
  if (!state) return;
98206
98215
  for (const response of state.responses) response.destroy();
98207
98216
  state.responses.clear();
98208
98217
  state.clients = 0;
98209
98218
  state.leases = 0;
98210
- await this.closeState(state);
98211
- if (state.openPromise) {
98212
- await state.openPromise.catch(() => {});
98219
+ const closing = (async () => {
98213
98220
  await this.closeState(state);
98221
+ if (state.openPromise) {
98222
+ await state.openPromise.catch(() => {});
98223
+ await this.closeState(state);
98224
+ }
98225
+ })();
98226
+ let timer;
98227
+ const bound = new Promise((resolve) => {
98228
+ timer = setTimeout(() => resolve("timeout"), maxWaitMs);
98229
+ });
98230
+ const outcome = await Promise.race([closing.then(() => "closed"), bound]);
98231
+ if (timer) clearTimeout(timer);
98232
+ if (outcome === "timeout") {
98233
+ closing.catch(() => {});
98234
+ this.logger.warn("terminal camera close still pending — node did not answer; continuing", {
98235
+ ...this.tagsFor(instanceId),
98236
+ meta: {
98237
+ nodeId: state.nodeId,
98238
+ waitedMs: maxWaitMs
98239
+ }
98240
+ });
98214
98241
  }
98215
98242
  }
98216
98243
  async dispose() {
@@ -99524,11 +99551,17 @@ var TerminalAddon = class extends BaseAddon {
99524
99551
  } });
99525
99552
  });
99526
99553
  this.terminalCameraTombstones.add(instance.cameraStableId);
99527
- await this.cameraRelay?.closeInstance(instance.id);
99528
99554
  await this.updateGlobalSettings({
99529
99555
  terminalInstances: this.config.terminalInstances.filter((candidate) => candidate.id !== instanceId),
99530
99556
  terminalCameraTombstones: [...new Set([...this.config.terminalCameraTombstones, instance.cameraStableId])]
99531
99557
  });
99558
+ await this.cameraRelay?.closeInstance(instance.id).catch((error) => {
99559
+ this.ctx.logger.warn("could not close the deleted Terminal instance session", { meta: {
99560
+ instanceId: instance.id,
99561
+ nodeId: instance.nodeId,
99562
+ error: error instanceof Error ? error.message : String(error)
99563
+ } });
99564
+ });
99532
99565
  }
99533
99566
  async setTerminalInstanceEnabled(instanceId, enabled) {
99534
99567
  const instance = await this.instanceMutationQueue.run(() => this.setTerminalInstanceEnabledUnlocked(instanceId, enabled));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-terminal",
3
- "version": "0.1.156",
3
+ "version": "0.1.157",
4
4
  "description": "Interactive terminal sessions (pty + xterm) as a CamStack addon",
5
5
  "keywords": [
6
6
  "camstack",