@taphubhq/sdk-core 0.13.0 → 0.14.0

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/dist/index.cjs CHANGED
@@ -818,9 +818,17 @@ var LocaleModule = class {
818
818
 
819
819
  // src/transport/mqtt/index.ts
820
820
  var import_mqtt = __toESM(require("mqtt"));
821
- var GAME_SCOPED_SUFFIXES = ["candle", "config", "ideal_config"];
821
+
822
+ // src/transport/mqtt/utils.ts
823
+ function subKey(gameId, userId) {
824
+ return `${gameId}::${userId ?? ""}`;
825
+ }
826
+
827
+ // src/transport/mqtt/index.ts
828
+ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
822
829
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
823
830
  var TOPIC_PREFIX = "game";
831
+ var MARKET_TOPIC_PREFIX = "market";
824
832
  function topicFor(gameId, suffix, userId) {
825
833
  if (USER_SCOPED_SUFFIXES.includes(suffix)) {
826
834
  return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
@@ -833,12 +841,10 @@ function normaliseUserId(userId) {
833
841
  function userScopedTopicsFor(gameId, userId) {
834
842
  return USER_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s, userId));
835
843
  }
836
- function subKey(gameId, userId) {
837
- return `${gameId}::${userId ?? ""}`;
838
- }
839
844
  function createMqttTransport(endpoint, opts = {}) {
840
845
  let client = null;
841
846
  const subscriptions = /* @__PURE__ */ new Map();
847
+ const candleSubscriptions = /* @__PURE__ */ new Map();
842
848
  const { onLifecycle } = opts;
843
849
  let connectStartedAt = 0;
844
850
  function fireLifecycle(event) {
@@ -885,6 +891,17 @@ function createMqttTransport(endpoint, opts = {}) {
885
891
  fireLifecycle({ kind: "error", err });
886
892
  });
887
893
  client.on("message", (receivedTopic, message) => {
894
+ const candleSub = [...candleSubscriptions.values()].find((s) => s.topic === receivedTopic);
895
+ if (candleSub) {
896
+ let payload2;
897
+ try {
898
+ payload2 = JSON.parse(message.toString());
899
+ } catch {
900
+ return;
901
+ }
902
+ candleSub.onCandle(receivedTopic, payload2);
903
+ return;
904
+ }
888
905
  let matched;
889
906
  for (const sub of subscriptions.values()) {
890
907
  if (sub.topics.includes(receivedTopic)) {
@@ -929,9 +946,22 @@ function createMqttTransport(endpoint, opts = {}) {
929
946
  onError
930
947
  });
931
948
  for (const t of fullTopics) {
932
- mqttClient.subscribe(t, { qos: t.endsWith("candle") ? 0 : 1 });
949
+ mqttClient.subscribe(t, { qos: 1 });
933
950
  }
934
951
  },
952
+ subscribeCandle(pair, onCandle) {
953
+ const topic = `${MARKET_TOPIC_PREFIX}/${pair.replace(/\//g, "_")}/candle`;
954
+ if (candleSubscriptions.has(pair)) return;
955
+ const mqttClient = ensureConnected();
956
+ candleSubscriptions.set(pair, { topic, onCandle });
957
+ mqttClient.subscribe(topic, { qos: 0 });
958
+ },
959
+ unsubscribeCandle(pair) {
960
+ const entry = candleSubscriptions.get(pair);
961
+ if (!entry) return;
962
+ if (client) client.unsubscribe(entry.topic);
963
+ candleSubscriptions.delete(pair);
964
+ },
935
965
  unsubscribeAll(gameId, userId) {
936
966
  const matches = entriesForGame(gameId);
937
967
  if (matches.length === 0) return;
@@ -963,8 +993,12 @@ function createMqttTransport(endpoint, opts = {}) {
963
993
  client.unsubscribe(t);
964
994
  }
965
995
  }
996
+ for (const entry of candleSubscriptions.values()) {
997
+ client.unsubscribe(entry.topic);
998
+ }
966
999
  }
967
1000
  subscriptions.clear();
1001
+ candleSubscriptions.clear();
968
1002
  if (client) {
969
1003
  client.end(true);
970
1004
  client = null;
@@ -1082,9 +1116,6 @@ function mapWireToEvent(topic, payload) {
1082
1116
  function normaliseUserId2(userId) {
1083
1117
  return userId && userId !== "" ? userId : null;
1084
1118
  }
1085
- function subKey2(gameId, userId) {
1086
- return `${gameId}::${userId ?? ""}`;
1087
- }
1088
1119
  var RealtimeModule = class extends import_eventemitter32.default {
1089
1120
  #transport;
1090
1121
  #entries = /* @__PURE__ */ new Map();
@@ -1104,7 +1135,7 @@ var RealtimeModule = class extends import_eventemitter32.default {
1104
1135
  }
1105
1136
  subscribe(gameId, userId) {
1106
1137
  const cleanUserId = normaliseUserId2(userId);
1107
- const key = subKey2(gameId, cleanUserId);
1138
+ const key = subKey(gameId, cleanUserId);
1108
1139
  const existing = this.#entries.get(key);
1109
1140
  if (existing) {
1110
1141
  existing.refcount += 1;
@@ -1160,11 +1191,22 @@ var RealtimeModule = class extends import_eventemitter32.default {
1160
1191
  }
1161
1192
  target.refcount -= 1;
1162
1193
  if (target.refcount > 0) return;
1163
- const key = subKey2(target.gameId, target.userId);
1194
+ const key = subKey(target.gameId, target.userId);
1164
1195
  this.#entries.delete(key);
1165
1196
  target.channel.removeAllListeners();
1166
1197
  this.#transport.unsubscribeAll(gameId, target.userId);
1167
1198
  }
1199
+ /**
1200
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
1201
+ * Independent of game/agency — candle is market-wide public data.
1202
+ */
1203
+ subscribeCandle(pair, onCandle) {
1204
+ this.#transport.subscribeCandle(pair, onCandle);
1205
+ }
1206
+ /** Unsubscribe from candle data for a pair. */
1207
+ unsubscribeCandle(pair) {
1208
+ this.#transport.unsubscribeCandle(pair);
1209
+ }
1168
1210
  disconnect() {
1169
1211
  for (const entry of this.#entries.values()) {
1170
1212
  entry.channel.removeAllListeners();
package/dist/index.d.mts CHANGED
@@ -452,14 +452,13 @@ type MqttLifecycleEvent = {
452
452
  rttMs: number;
453
453
  };
454
454
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
455
+ type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
455
456
  interface MqttTransport {
456
457
  /**
457
- * Subscribe to a game's MQTT topics keyed by the tuple `(gameId, userId)`.
458
- * When `userId` is nullish (including the empty string `''`), only
459
- * game-scoped topics are subscribed and the entry is stored under the
460
- * anonymous slot for that gameId. Two calls with the same `(gameId, userId)`
461
- * tuple are idempotent; calls with the same `gameId` but a different
462
- * `userId` create an independent entry with its own user-scoped topics.
458
+ * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
459
+ * keyed by the tuple `(gameId, userId)`. Does NOT subscribe to candle —
460
+ * use `subscribeCandle` for public market candle data.
461
+ * Two calls with the same `(gameId, userId)` tuple are idempotent.
463
462
  */
464
463
  subscribe(gameId: string, userId: string | null | undefined, onMessage: MqttMessageHandler, onError: MqttErrorHandler): void;
465
464
  /**
@@ -470,6 +469,14 @@ interface MqttTransport {
470
469
  * `code='AmbiguousUnsubscribe'`.
471
470
  */
472
471
  unsubscribeAll(gameId: string, userId?: string | null): void;
472
+ /**
473
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
474
+ * Independent of game/agency context — candle is market-wide public data.
475
+ * Idempotent: multiple calls for the same pair are deduplicated.
476
+ */
477
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
478
+ /** Tear down the candle subscription for a pair. */
479
+ unsubscribeCandle(pair: string): void;
473
480
  close(): void;
474
481
  }
475
482
 
@@ -580,6 +587,13 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
580
587
  get _transport(): MqttTransport;
581
588
  subscribe(gameId: string, userId?: string | null): GameChannel;
582
589
  unsubscribe(gameId: string, userId?: string | null): void;
590
+ /**
591
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
592
+ * Independent of game/agency — candle is market-wide public data.
593
+ */
594
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
595
+ /** Unsubscribe from candle data for a pair. */
596
+ unsubscribeCandle(pair: string): void;
583
597
  disconnect(): void;
584
598
  }
585
599
 
package/dist/index.d.ts CHANGED
@@ -452,14 +452,13 @@ type MqttLifecycleEvent = {
452
452
  rttMs: number;
453
453
  };
454
454
  type MqttLifecycleHook = (event: MqttLifecycleEvent) => void;
455
+ type MqttCandleHandler = (topic: string, payload: MqttWirePayload) => void;
455
456
  interface MqttTransport {
456
457
  /**
457
- * Subscribe to a game's MQTT topics keyed by the tuple `(gameId, userId)`.
458
- * When `userId` is nullish (including the empty string `''`), only
459
- * game-scoped topics are subscribed and the entry is stored under the
460
- * anonymous slot for that gameId. Two calls with the same `(gameId, userId)`
461
- * tuple are idempotent; calls with the same `gameId` but a different
462
- * `userId` create an independent entry with its own user-scoped topics.
458
+ * Subscribe to a game's MQTT topics (config, ideal_config, bid_result, balance)
459
+ * keyed by the tuple `(gameId, userId)`. Does NOT subscribe to candle —
460
+ * use `subscribeCandle` for public market candle data.
461
+ * Two calls with the same `(gameId, userId)` tuple are idempotent.
463
462
  */
464
463
  subscribe(gameId: string, userId: string | null | undefined, onMessage: MqttMessageHandler, onError: MqttErrorHandler): void;
465
464
  /**
@@ -470,6 +469,14 @@ interface MqttTransport {
470
469
  * `code='AmbiguousUnsubscribe'`.
471
470
  */
472
471
  unsubscribeAll(gameId: string, userId?: string | null): void;
472
+ /**
473
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
474
+ * Independent of game/agency context — candle is market-wide public data.
475
+ * Idempotent: multiple calls for the same pair are deduplicated.
476
+ */
477
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
478
+ /** Tear down the candle subscription for a pair. */
479
+ unsubscribeCandle(pair: string): void;
473
480
  close(): void;
474
481
  }
475
482
 
@@ -580,6 +587,13 @@ declare class RealtimeModule extends EventEmitter<RealtimeModuleEvents> {
580
587
  get _transport(): MqttTransport;
581
588
  subscribe(gameId: string, userId?: string | null): GameChannel;
582
589
  unsubscribe(gameId: string, userId?: string | null): void;
590
+ /**
591
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
592
+ * Independent of game/agency — candle is market-wide public data.
593
+ */
594
+ subscribeCandle(pair: string, onCandle: MqttCandleHandler): void;
595
+ /** Unsubscribe from candle data for a pair. */
596
+ unsubscribeCandle(pair: string): void;
583
597
  disconnect(): void;
584
598
  }
585
599
 
package/dist/index.js CHANGED
@@ -754,9 +754,17 @@ var LocaleModule = class {
754
754
 
755
755
  // src/transport/mqtt/index.ts
756
756
  import mqtt from "mqtt";
757
- var GAME_SCOPED_SUFFIXES = ["candle", "config", "ideal_config"];
757
+
758
+ // src/transport/mqtt/utils.ts
759
+ function subKey(gameId, userId) {
760
+ return `${gameId}::${userId ?? ""}`;
761
+ }
762
+
763
+ // src/transport/mqtt/index.ts
764
+ var GAME_SCOPED_SUFFIXES = ["config", "ideal_config"];
758
765
  var USER_SCOPED_SUFFIXES = ["bid_result", "balance_update"];
759
766
  var TOPIC_PREFIX = "game";
767
+ var MARKET_TOPIC_PREFIX = "market";
760
768
  function topicFor(gameId, suffix, userId) {
761
769
  if (USER_SCOPED_SUFFIXES.includes(suffix)) {
762
770
  return `${TOPIC_PREFIX}/${gameId}/user/${userId}/${suffix}`;
@@ -769,12 +777,10 @@ function normaliseUserId(userId) {
769
777
  function userScopedTopicsFor(gameId, userId) {
770
778
  return USER_SCOPED_SUFFIXES.map((s) => topicFor(gameId, s, userId));
771
779
  }
772
- function subKey(gameId, userId) {
773
- return `${gameId}::${userId ?? ""}`;
774
- }
775
780
  function createMqttTransport(endpoint, opts = {}) {
776
781
  let client = null;
777
782
  const subscriptions = /* @__PURE__ */ new Map();
783
+ const candleSubscriptions = /* @__PURE__ */ new Map();
778
784
  const { onLifecycle } = opts;
779
785
  let connectStartedAt = 0;
780
786
  function fireLifecycle(event) {
@@ -821,6 +827,17 @@ function createMqttTransport(endpoint, opts = {}) {
821
827
  fireLifecycle({ kind: "error", err });
822
828
  });
823
829
  client.on("message", (receivedTopic, message) => {
830
+ const candleSub = [...candleSubscriptions.values()].find((s) => s.topic === receivedTopic);
831
+ if (candleSub) {
832
+ let payload2;
833
+ try {
834
+ payload2 = JSON.parse(message.toString());
835
+ } catch {
836
+ return;
837
+ }
838
+ candleSub.onCandle(receivedTopic, payload2);
839
+ return;
840
+ }
824
841
  let matched;
825
842
  for (const sub of subscriptions.values()) {
826
843
  if (sub.topics.includes(receivedTopic)) {
@@ -865,9 +882,22 @@ function createMqttTransport(endpoint, opts = {}) {
865
882
  onError
866
883
  });
867
884
  for (const t of fullTopics) {
868
- mqttClient.subscribe(t, { qos: t.endsWith("candle") ? 0 : 1 });
885
+ mqttClient.subscribe(t, { qos: 1 });
869
886
  }
870
887
  },
888
+ subscribeCandle(pair, onCandle) {
889
+ const topic = `${MARKET_TOPIC_PREFIX}/${pair.replace(/\//g, "_")}/candle`;
890
+ if (candleSubscriptions.has(pair)) return;
891
+ const mqttClient = ensureConnected();
892
+ candleSubscriptions.set(pair, { topic, onCandle });
893
+ mqttClient.subscribe(topic, { qos: 0 });
894
+ },
895
+ unsubscribeCandle(pair) {
896
+ const entry = candleSubscriptions.get(pair);
897
+ if (!entry) return;
898
+ if (client) client.unsubscribe(entry.topic);
899
+ candleSubscriptions.delete(pair);
900
+ },
871
901
  unsubscribeAll(gameId, userId) {
872
902
  const matches = entriesForGame(gameId);
873
903
  if (matches.length === 0) return;
@@ -899,8 +929,12 @@ function createMqttTransport(endpoint, opts = {}) {
899
929
  client.unsubscribe(t);
900
930
  }
901
931
  }
932
+ for (const entry of candleSubscriptions.values()) {
933
+ client.unsubscribe(entry.topic);
934
+ }
902
935
  }
903
936
  subscriptions.clear();
937
+ candleSubscriptions.clear();
904
938
  if (client) {
905
939
  client.end(true);
906
940
  client = null;
@@ -1018,9 +1052,6 @@ function mapWireToEvent(topic, payload) {
1018
1052
  function normaliseUserId2(userId) {
1019
1053
  return userId && userId !== "" ? userId : null;
1020
1054
  }
1021
- function subKey2(gameId, userId) {
1022
- return `${gameId}::${userId ?? ""}`;
1023
- }
1024
1055
  var RealtimeModule = class extends EventEmitter2 {
1025
1056
  #transport;
1026
1057
  #entries = /* @__PURE__ */ new Map();
@@ -1040,7 +1071,7 @@ var RealtimeModule = class extends EventEmitter2 {
1040
1071
  }
1041
1072
  subscribe(gameId, userId) {
1042
1073
  const cleanUserId = normaliseUserId2(userId);
1043
- const key = subKey2(gameId, cleanUserId);
1074
+ const key = subKey(gameId, cleanUserId);
1044
1075
  const existing = this.#entries.get(key);
1045
1076
  if (existing) {
1046
1077
  existing.refcount += 1;
@@ -1096,11 +1127,22 @@ var RealtimeModule = class extends EventEmitter2 {
1096
1127
  }
1097
1128
  target.refcount -= 1;
1098
1129
  if (target.refcount > 0) return;
1099
- const key = subKey2(target.gameId, target.userId);
1130
+ const key = subKey(target.gameId, target.userId);
1100
1131
  this.#entries.delete(key);
1101
1132
  target.channel.removeAllListeners();
1102
1133
  this.#transport.unsubscribeAll(gameId, target.userId);
1103
1134
  }
1135
+ /**
1136
+ * Subscribe to public market candle data for a pair (e.g. "ETH/USD").
1137
+ * Independent of game/agency — candle is market-wide public data.
1138
+ */
1139
+ subscribeCandle(pair, onCandle) {
1140
+ this.#transport.subscribeCandle(pair, onCandle);
1141
+ }
1142
+ /** Unsubscribe from candle data for a pair. */
1143
+ unsubscribeCandle(pair) {
1144
+ this.#transport.unsubscribeCandle(pair);
1145
+ }
1104
1146
  disconnect() {
1105
1147
  for (const entry of this.#entries.values()) {
1106
1148
  entry.channel.removeAllListeners();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taphubhq/sdk-core",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Core SDK for building on the TabHub platform",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.cjs",