@camstack/sdk 1.2.8 → 1.2.10

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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `subscribeEvent` fans one WS subscription out to N listeners.
3
+ *
4
+ * Measured on the viewer's boot: ~40 tRPC subscriptions before first paint,
5
+ * of which 24 were twelve camera cards subscribing to the SAME two motion
6
+ * categories and filtering by deviceId inside their own callbacks. Each was a
7
+ * separate handshake on a transport that does not batch.
8
+ *
9
+ * The tests exercise the real `System` with a stubbed tRPC client, so
10
+ * what is asserted is the number of `live.onEvent.subscribe` calls the SDK
11
+ * actually makes — not a mock of the deduping itself.
12
+ */
13
+ export {};
package/dist/index.cjs CHANGED
@@ -683,6 +683,17 @@ var System = class {
683
683
  token;
684
684
  _trpcClient;
685
685
  _wsClient = null;
686
+ /**
687
+ * One underlying `live.onEvent` subscription per DISTINCT category, fanned
688
+ * out to every listener.
689
+ *
690
+ * Measured on the viewer's boot: ~40 subscriptions, of which 24 were twelve
691
+ * cameras subscribing to the SAME two motion categories and filtering by
692
+ * deviceId in their own callbacks. Each was its own WS handshake on a
693
+ * transport that does not batch. Deduping here needs no protocol change and
694
+ * no cooperation from callers — `subscribeEvent` keeps its exact shape.
695
+ */
696
+ _eventSubs = /* @__PURE__ */ new Map();
686
697
  mirror = null;
687
698
  mirrorInit = null;
688
699
  connected = false;
@@ -826,6 +837,7 @@ var System = class {
826
837
  this.emitConnectionEvent("connecting");
827
838
  this._wsClient?.close();
828
839
  this.disposeMirror();
840
+ this.resetEventSubs();
829
841
  this.connected = false;
830
842
  this.connectedPromise = null;
831
843
  this._trpcClient = this.buildTrpcClient();
@@ -1222,19 +1234,59 @@ var System = class {
1222
1234
  * (`@camstack/types`) — passing a raw string works for forward-compat
1223
1235
  * but loses type safety. The SDK forwards the value verbatim to the
1224
1236
  * server's `live.onEvent` subscription.
1237
+ *
1238
+ * **N callers on one category share ONE subscription.** The caller's shape
1239
+ * is unchanged, and it is not asked to coordinate: twelve camera cards each
1240
+ * subscribing to the motion category and filtering by deviceId is the normal
1241
+ * pattern, and it used to open twelve WS subscriptions.
1225
1242
  */
1226
1243
  subscribeEvent(category, cb) {
1227
- const sub = this._trpcClient.live.onEvent.subscribe({ category }, { onData: (event) => {
1228
- try {
1229
- cb(event);
1230
- } catch {}
1231
- } });
1244
+ const listener = cb;
1245
+ let entry = this._eventSubs.get(category);
1246
+ if (entry === void 0) {
1247
+ const listeners = /* @__PURE__ */ new Set();
1248
+ const sub = this._trpcClient.live.onEvent.subscribe({ category }, { onData: (event) => {
1249
+ for (const fn of [...listeners]) try {
1250
+ fn(event);
1251
+ } catch {}
1252
+ } });
1253
+ entry = {
1254
+ listeners,
1255
+ unsubscribe: () => {
1256
+ try {
1257
+ sub.unsubscribe();
1258
+ } catch {}
1259
+ }
1260
+ };
1261
+ this._eventSubs.set(category, entry);
1262
+ }
1263
+ entry.listeners.add(listener);
1264
+ let released = false;
1232
1265
  return () => {
1233
- try {
1234
- sub.unsubscribe();
1235
- } catch {}
1266
+ if (released) return;
1267
+ released = true;
1268
+ const current = this._eventSubs.get(category);
1269
+ if (current === void 0) return;
1270
+ current.listeners.delete(listener);
1271
+ if (current.listeners.size === 0) {
1272
+ current.unsubscribe();
1273
+ this._eventSubs.delete(category);
1274
+ }
1236
1275
  };
1237
1276
  }
1277
+ /**
1278
+ * Drop every shared subscription.
1279
+ *
1280
+ * Called when the tRPC client is rebuilt: the handles above belong to the
1281
+ * OLD client and unsubscribing them is meaningless, while keeping them in
1282
+ * the map would make the next `subscribeEvent` attach a listener to a dead
1283
+ * socket and go silent — the worst outcome, because it looks like "no
1284
+ * events happened".
1285
+ */
1286
+ resetEventSubs() {
1287
+ for (const entry of this._eventSubs.values()) entry.unsubscribe();
1288
+ this._eventSubs.clear();
1289
+ }
1238
1290
  /** Direct tRPC client. Read once per call; rebuilt on `reconnect()`. */
1239
1291
  get trpcClient() {
1240
1292
  return this._trpcClient;
package/dist/index.js CHANGED
@@ -682,6 +682,17 @@ var System = class {
682
682
  token;
683
683
  _trpcClient;
684
684
  _wsClient = null;
685
+ /**
686
+ * One underlying `live.onEvent` subscription per DISTINCT category, fanned
687
+ * out to every listener.
688
+ *
689
+ * Measured on the viewer's boot: ~40 subscriptions, of which 24 were twelve
690
+ * cameras subscribing to the SAME two motion categories and filtering by
691
+ * deviceId in their own callbacks. Each was its own WS handshake on a
692
+ * transport that does not batch. Deduping here needs no protocol change and
693
+ * no cooperation from callers — `subscribeEvent` keeps its exact shape.
694
+ */
695
+ _eventSubs = /* @__PURE__ */ new Map();
685
696
  mirror = null;
686
697
  mirrorInit = null;
687
698
  connected = false;
@@ -825,6 +836,7 @@ var System = class {
825
836
  this.emitConnectionEvent("connecting");
826
837
  this._wsClient?.close();
827
838
  this.disposeMirror();
839
+ this.resetEventSubs();
828
840
  this.connected = false;
829
841
  this.connectedPromise = null;
830
842
  this._trpcClient = this.buildTrpcClient();
@@ -1221,19 +1233,59 @@ var System = class {
1221
1233
  * (`@camstack/types`) — passing a raw string works for forward-compat
1222
1234
  * but loses type safety. The SDK forwards the value verbatim to the
1223
1235
  * server's `live.onEvent` subscription.
1236
+ *
1237
+ * **N callers on one category share ONE subscription.** The caller's shape
1238
+ * is unchanged, and it is not asked to coordinate: twelve camera cards each
1239
+ * subscribing to the motion category and filtering by deviceId is the normal
1240
+ * pattern, and it used to open twelve WS subscriptions.
1224
1241
  */
1225
1242
  subscribeEvent(category, cb) {
1226
- const sub = this._trpcClient.live.onEvent.subscribe({ category }, { onData: (event) => {
1227
- try {
1228
- cb(event);
1229
- } catch {}
1230
- } });
1243
+ const listener = cb;
1244
+ let entry = this._eventSubs.get(category);
1245
+ if (entry === void 0) {
1246
+ const listeners = /* @__PURE__ */ new Set();
1247
+ const sub = this._trpcClient.live.onEvent.subscribe({ category }, { onData: (event) => {
1248
+ for (const fn of [...listeners]) try {
1249
+ fn(event);
1250
+ } catch {}
1251
+ } });
1252
+ entry = {
1253
+ listeners,
1254
+ unsubscribe: () => {
1255
+ try {
1256
+ sub.unsubscribe();
1257
+ } catch {}
1258
+ }
1259
+ };
1260
+ this._eventSubs.set(category, entry);
1261
+ }
1262
+ entry.listeners.add(listener);
1263
+ let released = false;
1231
1264
  return () => {
1232
- try {
1233
- sub.unsubscribe();
1234
- } catch {}
1265
+ if (released) return;
1266
+ released = true;
1267
+ const current = this._eventSubs.get(category);
1268
+ if (current === void 0) return;
1269
+ current.listeners.delete(listener);
1270
+ if (current.listeners.size === 0) {
1271
+ current.unsubscribe();
1272
+ this._eventSubs.delete(category);
1273
+ }
1235
1274
  };
1236
1275
  }
1276
+ /**
1277
+ * Drop every shared subscription.
1278
+ *
1279
+ * Called when the tRPC client is rebuilt: the handles above belong to the
1280
+ * OLD client and unsubscribing them is meaningless, while keeping them in
1281
+ * the map would make the next `subscribeEvent` attach a listener to a dead
1282
+ * socket and go silent — the worst outcome, because it looks like "no
1283
+ * events happened".
1284
+ */
1285
+ resetEventSubs() {
1286
+ for (const entry of this._eventSubs.values()) entry.unsubscribe();
1287
+ this._eventSubs.clear();
1288
+ }
1237
1289
  /** Direct tRPC client. Read once per call; rebuilt on `reconnect()`. */
1238
1290
  get trpcClient() {
1239
1291
  return this._trpcClient;
package/dist/system.d.ts CHANGED
@@ -75,6 +75,17 @@ export declare class System {
75
75
  private token;
76
76
  private _trpcClient;
77
77
  private _wsClient;
78
+ /**
79
+ * One underlying `live.onEvent` subscription per DISTINCT category, fanned
80
+ * out to every listener.
81
+ *
82
+ * Measured on the viewer's boot: ~40 subscriptions, of which 24 were twelve
83
+ * cameras subscribing to the SAME two motion categories and filtering by
84
+ * deviceId in their own callbacks. Each was its own WS handshake on a
85
+ * transport that does not batch. Deduping here needs no protocol change and
86
+ * no cooperation from callers — `subscribeEvent` keeps its exact shape.
87
+ */
88
+ private readonly _eventSubs;
78
89
  private mirror;
79
90
  private mirrorInit;
80
91
  private connected;
@@ -382,8 +393,23 @@ export declare class System {
382
393
  * (`@camstack/types`) — passing a raw string works for forward-compat
383
394
  * but loses type safety. The SDK forwards the value verbatim to the
384
395
  * server's `live.onEvent` subscription.
396
+ *
397
+ * **N callers on one category share ONE subscription.** The caller's shape
398
+ * is unchanged, and it is not asked to coordinate: twelve camera cards each
399
+ * subscribing to the motion category and filtering by deviceId is the normal
400
+ * pattern, and it used to open twelve WS subscriptions.
385
401
  */
386
402
  subscribeEvent<TData = unknown>(category: string, cb: SystemLiveEventListener<TData>): () => void;
403
+ /**
404
+ * Drop every shared subscription.
405
+ *
406
+ * Called when the tRPC client is rebuilt: the handles above belong to the
407
+ * OLD client and unsubscribing them is meaningless, while keeping them in
408
+ * the map would make the next `subscribeEvent` attach a listener to a dead
409
+ * socket and go silent — the worst outcome, because it looks like "no
410
+ * events happened".
411
+ */
412
+ private resetEventSubs;
387
413
  /** Direct tRPC client. Read once per call; rebuilt on `reconnect()`. */
388
414
  get trpcClient(): TrpcClient;
389
415
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/sdk",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",