@mega-yfue/eufy-sdk 0.5.0-beta.45 → 0.5.0-beta.46

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.
@@ -711,6 +711,11 @@ export declare class EufyMega extends EventEmitter {
711
711
  * — an already-open session resolves immediately.
712
712
  */
713
713
  connectStation(deviceSn: string, signal?: AbortSignal): Promise<void>;
714
+ /**
715
+ * Keep a T9000 station's RTC command session open and warm, so a command fires without a cold reconnect.
716
+ * Answers the hold's disposer; throws for any other station, and while logged out.
717
+ */
718
+ keepStationWarm(stationSn: string): () => void;
714
719
  /** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
715
720
  * stations stay closed until an on-demand open. Best-effort per station. */
716
721
  private warmWiredP2P;
package/dist/index.js CHANGED
@@ -22724,6 +22724,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
22724
22724
  // dist/transport/rtc/live.js
22725
22725
  import { EventEmitter as EventEmitter7 } from "node:events";
22726
22726
 
22727
+ // dist/transport/rtc/portal-keepalive.js
22728
+ var PORTAL_KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a5948730400000000000000000002", "hex");
22729
+ var PORTAL_KEEPALIVE_MS = 29e3;
22730
+
22727
22731
  // dist/transport/rtc/portal-packet.js
22728
22732
  var MAGIC2 = Buffer.from("XZYH", "ascii");
22729
22733
  var PORTAL_HEADER_LENGTH = 16;
@@ -22833,8 +22837,6 @@ var LIVE_STOP = 1004;
22833
22837
  var LIVE_MEDIA = 1300;
22834
22838
  var LIVE_MEDIA_CHANNEL = 101;
22835
22839
  var LIVE_STREAM_ID = 1;
22836
- var KEEPALIVE_MS = 29e3;
22837
- var KEEPALIVE = Buffer.from("0009000010000000000000006300000000000000585a5948730400000000000000000002", "hex");
22838
22840
  var MEDIA_HEADER_LENGTH = 22;
22839
22841
  var STREAM_TYPE_HEVC = 1;
22840
22842
  function parseMediaBody(body) {
@@ -22904,9 +22906,9 @@ var RtcLiveStream = class extends EventEmitter7 {
22904
22906
  return this;
22905
22907
  }
22906
22908
  this.keepalive = setInterval(() => {
22907
- if (!session.sendRaw(KEEPALIVE))
22909
+ if (!session.sendRaw(PORTAL_KEEPALIVE))
22908
22910
  this.opts.logger?.warn?.(`[rtc] ${this.tag} keepalive not sent`);
22909
- }, KEEPALIVE_MS);
22911
+ }, PORTAL_KEEPALIVE_MS);
22910
22912
  this.keepalive.unref?.();
22911
22913
  this.opts.logger?.debug?.(`[rtc] ${this.tag} live start sent`);
22912
22914
  return this;
@@ -23440,7 +23442,8 @@ var RtcPeer = class extends EventEmitter8 {
23440
23442
  }
23441
23443
  /**
23442
23444
  * Wire one data channel. Every channel feeds the framer once the command channel has opened it; a
23443
- * message before that has nothing to reassemble into and is dropped.
23445
+ * message before that has nothing to reassemble into and is dropped. The hub's echo of the portal
23446
+ * keepalive on the command channel is unframed and surfaces as `keepaliveEcho` instead.
23444
23447
  */
23445
23448
  wireChannel(label, dc) {
23446
23449
  if (this.channels.has(label))
@@ -23478,6 +23481,10 @@ var RtcPeer = class extends EventEmitter8 {
23478
23481
  if (n <= 3 || n % 100 === 0) {
23479
23482
  this.logger.debug(`[rtc] wire ${label} #${n} ${buf.length}B ${buf.subarray(0, 8).toString("hex")}`);
23480
23483
  }
23484
+ if (label === COMMAND_CHANNEL && buf.equals(PORTAL_KEEPALIVE)) {
23485
+ this.emit("keepaliveEcho");
23486
+ return;
23487
+ }
23481
23488
  this.framer?.recvPacket(buf);
23482
23489
  });
23483
23490
  }
@@ -23789,6 +23796,7 @@ var RtcSession = class extends EventEmitter10 {
23789
23796
  }
23790
23797
  });
23791
23798
  this.peer.on("error", (e) => this.emit("error", e));
23799
+ this.peer.on("keepaliveEcho", () => this.emit("keepaliveEcho"));
23792
23800
  this.peer.on("data", (frame, linkType) => {
23793
23801
  if (linkType === PortalLinkType.LIVE)
23794
23802
  this.emit("mediaData", frame);
@@ -23984,6 +23992,7 @@ var RtcCommandRouter = class {
23984
23992
  deps;
23985
23993
  sessions = /* @__PURE__ */ new Map();
23986
23994
  lives = /* @__PURE__ */ new Map();
23995
+ warm = /* @__PURE__ */ new Map();
23987
23996
  constructor(deps) {
23988
23997
  this.deps = deps;
23989
23998
  }
@@ -24233,6 +24242,96 @@ var RtcCommandRouter = class {
24233
24242
  Promise.resolve().then(() => session.connect()).catch((e) => finish(e instanceof Error ? e : new Error(String(e))));
24234
24243
  });
24235
24244
  }
24245
+ /**
24246
+ * Hold a station's command session open: one hold per station, its idle-close ({@link IDLE_CLOSE_MS})
24247
+ * suppressed, {@link PORTAL_KEEPALIVE} sent every {@link PORTAL_KEEPALIVE_MS}, and the session dropped
24248
+ * on a tick that finds the previous keepalive unechoed. A closed session ends the hold.
24249
+ *
24250
+ * Answers an idempotent disposer that releases the hold. Throws when no account is logged in.
24251
+ */
24252
+ keepWarm(route) {
24253
+ const sn = route.stationSn;
24254
+ const existing = this.warm.get(sn);
24255
+ if (existing)
24256
+ return existing.dispose;
24257
+ const identity = this.deps.identity();
24258
+ if (!identity)
24259
+ throw new Error(`rtc: not logged in, cannot warm ${sn}`);
24260
+ const hold = {
24261
+ awaitingEcho: false,
24262
+ dispose: () => {
24263
+ if (this.warm.get(sn) !== hold)
24264
+ return;
24265
+ this.warm.delete(sn);
24266
+ const st = hold.watch?.st;
24267
+ this.unwatch(hold);
24268
+ if (st) {
24269
+ st.leases = Math.max(0, st.leases - 1);
24270
+ this.touch(sn, st);
24271
+ }
24272
+ }
24273
+ };
24274
+ this.warm.set(sn, hold);
24275
+ void this.warmUp(sn, route, identity, hold);
24276
+ return hold.dispose;
24277
+ }
24278
+ /**
24279
+ * Bring a station session up for a {@link keepWarm} hold and start its keepalive. A bring-up that fails,
24280
+ * or a hold disposed before it completes, clears the hold and arms no timer. Each tick first checks
24281
+ * that the hub echoed the previous keepalive, and drops the session when it did not. The keepalive
24282
+ * stops when the session closes, which also clears the hold so the next command or `keepWarm`
24283
+ * rebuilds cleanly.
24284
+ */
24285
+ async warmUp(sn, route, identity, hold) {
24286
+ const adminUserId = route.adminUserId || identity.userId;
24287
+ let st;
24288
+ try {
24289
+ st = await this.stationSession(route, adminUserId, identity);
24290
+ } catch (e) {
24291
+ if (this.warm.get(sn) === hold)
24292
+ this.warm.delete(sn);
24293
+ this.deps.onError?.(e instanceof Error ? e : new Error(String(e)));
24294
+ return;
24295
+ }
24296
+ if (this.warm.get(sn) !== hold) {
24297
+ this.touch(sn, st);
24298
+ return;
24299
+ }
24300
+ st.leases++;
24301
+ this.touch(sn, st);
24302
+ const onClose = () => {
24303
+ this.unwatch(hold);
24304
+ if (this.warm.get(sn) === hold)
24305
+ this.warm.delete(sn);
24306
+ };
24307
+ const onEcho = () => {
24308
+ hold.awaitingEcho = false;
24309
+ };
24310
+ hold.watch = { st, onClose, onEcho };
24311
+ st.session.once("close", onClose);
24312
+ st.session.on("keepaliveEcho", onEcho);
24313
+ hold.keepalive = setInterval(() => {
24314
+ if (hold.awaitingEcho) {
24315
+ this.deps.logger?.warn?.(`[rtc] ${sn} warm keepalive went unechoed, dropping the session`);
24316
+ this.drop(sn, st);
24317
+ return;
24318
+ }
24319
+ hold.awaitingEcho = true;
24320
+ st.session.sendRaw(PORTAL_KEEPALIVE);
24321
+ }, PORTAL_KEEPALIVE_MS);
24322
+ hold.keepalive.unref?.();
24323
+ }
24324
+ /** Stop a hold's keepalive and remove its listeners from the session it watches. */
24325
+ unwatch(hold) {
24326
+ if (hold.keepalive)
24327
+ clearInterval(hold.keepalive);
24328
+ hold.keepalive = void 0;
24329
+ if (hold.watch) {
24330
+ hold.watch.st.session.off("close", hold.watch.onClose);
24331
+ hold.watch.st.session.off("keepaliveEcho", hold.watch.onEcho);
24332
+ }
24333
+ hold.watch = void 0;
24334
+ }
24236
24335
  touch(sn, st) {
24237
24336
  if (st.idle)
24238
24337
  clearTimeout(st.idle);
@@ -28993,6 +29092,17 @@ var EufyMega = class extends EventEmitter13 {
28993
29092
  await this.getDevices();
28994
29093
  await this.p2p.ensureStation(this.p2p.stationKeyOf(deviceSn), signal);
28995
29094
  }
29095
+ /**
29096
+ * Keep a T9000 station's RTC command session open and warm, so a command fires without a cold reconnect.
29097
+ * Answers the hold's disposer; throws for any other station, and while logged out.
29098
+ */
29099
+ keepStationWarm(stationSn) {
29100
+ const { t9000 } = this.stationOf(stationSn);
29101
+ const route = this.rtcRouteFor(stationSn);
29102
+ if (!t9000 || !route)
29103
+ throw new Error(`rtc: ${stationSn} has no warm command session`);
29104
+ return this.rtc.keepWarm(route);
29105
+ }
28996
29106
  /** Eagerly open P2P sessions for WIRED stations only (persistent — they don't drain). Battery
28997
29107
  * stations stay closed until an on-demand open. Best-effort per station. */
28998
29108
  async warmWiredP2P(readiness = createPlaneReadiness(), signal) {