@spacelr/sdk 0.10.0 → 0.10.2
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.d.mts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +126 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +126 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -672,6 +672,21 @@ var RealtimeClient = class {
|
|
|
672
672
|
this.connecting = null;
|
|
673
673
|
// Store original where objects per room for reconnect resubscription
|
|
674
674
|
this.roomWhereMap = /* @__PURE__ */ new Map();
|
|
675
|
+
// Per-room, per-callback error handlers so EVERY subscriber to a room (not
|
|
676
|
+
// just the first) is notified on a denial, and a departed subscriber's handler
|
|
677
|
+
// is never left dangling for its still-active siblings (#661).
|
|
678
|
+
this.roomErrorMap = /* @__PURE__ */ new Map();
|
|
679
|
+
// Per-room generation stamp, taken from a global monotonic counter each time a
|
|
680
|
+
// room becomes active. A re-subscribe snapshots the room's stamp and acts on
|
|
681
|
+
// the ack only if it is still current — so a late ack cannot notify or clean
|
|
682
|
+
// up after the app has unsubscribed/re-subscribed (#661). Entries are deleted
|
|
683
|
+
// on last-unsubscribe (bounded); the global counter guarantees a re-subscribe
|
|
684
|
+
// always gets a strictly higher stamp, so there is no ABA hole.
|
|
685
|
+
this.roomGeneration = /* @__PURE__ */ new Map();
|
|
686
|
+
this.subEpoch = 0;
|
|
687
|
+
// Rooms with a re-subscribe handshake in flight — dedupes the duplicate
|
|
688
|
+
// `subscription-invalidated` events the gateway emits per evicted room (#661).
|
|
689
|
+
this.resubscribingRooms = /* @__PURE__ */ new Set();
|
|
675
690
|
this.unsubscribeFromTokenRefreshed = null;
|
|
676
691
|
// Wake-up listeners (browser only) — recover from long OS suspends where
|
|
677
692
|
// socket.io's internal reconnect loop may have already given up.
|
|
@@ -700,7 +715,17 @@ var RealtimeClient = class {
|
|
|
700
715
|
}
|
|
701
716
|
const callbacks = this.subscriptions.get(room);
|
|
702
717
|
callbacks?.add(callback);
|
|
718
|
+
if (onError) {
|
|
719
|
+
let handlers = this.roomErrorMap.get(room);
|
|
720
|
+
if (!handlers) {
|
|
721
|
+
handlers = /* @__PURE__ */ new Map();
|
|
722
|
+
this.roomErrorMap.set(room, handlers);
|
|
723
|
+
}
|
|
724
|
+
handlers.set(callback, onError);
|
|
725
|
+
}
|
|
703
726
|
if (callbacks?.size === 1) {
|
|
727
|
+
const generation = ++this.subEpoch;
|
|
728
|
+
this.roomGeneration.set(room, generation);
|
|
704
729
|
if (where && Object.keys(where).length > 0) {
|
|
705
730
|
this.roomWhereMap.set(room, where);
|
|
706
731
|
}
|
|
@@ -712,8 +737,8 @@ var RealtimeClient = class {
|
|
|
712
737
|
"subscribe",
|
|
713
738
|
payload,
|
|
714
739
|
(response) => {
|
|
715
|
-
if (response.error &&
|
|
716
|
-
|
|
740
|
+
if (response.error && this.roomGeneration.get(room) === generation) {
|
|
741
|
+
this.notifyRoomError(room, this.toSubscribeError(response));
|
|
717
742
|
}
|
|
718
743
|
}
|
|
719
744
|
);
|
|
@@ -722,9 +747,12 @@ var RealtimeClient = class {
|
|
|
722
747
|
const callbacks2 = this.subscriptions.get(room);
|
|
723
748
|
if (callbacks2) {
|
|
724
749
|
callbacks2.delete(callback);
|
|
750
|
+
this.roomErrorMap.get(room)?.delete(callback);
|
|
725
751
|
if (callbacks2.size === 0) {
|
|
726
752
|
this.subscriptions.delete(room);
|
|
727
753
|
this.roomWhereMap.delete(room);
|
|
754
|
+
this.roomErrorMap.delete(room);
|
|
755
|
+
this.roomGeneration.delete(room);
|
|
728
756
|
const payload = { projectId, collectionName };
|
|
729
757
|
if (where && Object.keys(where).length > 0) {
|
|
730
758
|
payload["where"] = where;
|
|
@@ -877,6 +905,9 @@ var RealtimeClient = class {
|
|
|
877
905
|
}
|
|
878
906
|
this.subscriptions.clear();
|
|
879
907
|
this.roomWhereMap.clear();
|
|
908
|
+
this.roomErrorMap.clear();
|
|
909
|
+
this.roomGeneration.clear();
|
|
910
|
+
this.resubscribingRooms.clear();
|
|
880
911
|
this.streamSubscriptions.clear();
|
|
881
912
|
this.connecting = null;
|
|
882
913
|
}
|
|
@@ -1026,6 +1057,18 @@ var RealtimeClient = class {
|
|
|
1026
1057
|
}
|
|
1027
1058
|
}
|
|
1028
1059
|
});
|
|
1060
|
+
this.socket.on(
|
|
1061
|
+
"subscription-invalidated",
|
|
1062
|
+
(payload) => {
|
|
1063
|
+
if (!payload?.projectId || !payload?.collectionName) return;
|
|
1064
|
+
const base = `db:${payload.projectId}:${payload.collectionName}`;
|
|
1065
|
+
for (const room of this.subscriptions.keys()) {
|
|
1066
|
+
if (room === base || room.startsWith(`${base}?`)) {
|
|
1067
|
+
this.resubscribeRoom(room);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1029
1072
|
this.socket.on("event", (payload) => {
|
|
1030
1073
|
this.dispatchStreamEvent(payload).catch(() => void 0);
|
|
1031
1074
|
});
|
|
@@ -1127,31 +1170,88 @@ var RealtimeClient = class {
|
|
|
1127
1170
|
this.onOnline = null;
|
|
1128
1171
|
}
|
|
1129
1172
|
resubscribeAll() {
|
|
1173
|
+
this.resubscribingRooms.clear();
|
|
1130
1174
|
for (const [room] of this.subscriptions) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1175
|
+
this.resubscribeRoom(room);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Parse a pubsub room key back into its projectId + collectionName.
|
|
1180
|
+
* Room format: `db:{projectId}:{collectionName}` or that base plus `?filter`.
|
|
1181
|
+
* Returns null for anything that isn't a well-formed `db:` room.
|
|
1182
|
+
*/
|
|
1183
|
+
parseRoom(room) {
|
|
1184
|
+
const queryIdx = room.indexOf("?");
|
|
1185
|
+
const basePart = queryIdx >= 0 ? room.substring(0, queryIdx) : room;
|
|
1186
|
+
const parts = basePart.split(":");
|
|
1187
|
+
if (parts.length < 3 || parts[0] !== "db") return null;
|
|
1188
|
+
return { projectId: parts[1], collectionName: parts.slice(2).join(":") };
|
|
1189
|
+
}
|
|
1190
|
+
/** Build an Error carrying the gateway's typed `errorCode` (if any) as `.code`. */
|
|
1191
|
+
toSubscribeError(response) {
|
|
1192
|
+
const err = new Error(response.error ?? "Subscribe denied");
|
|
1193
|
+
if (response.errorCode) err.code = response.errorCode;
|
|
1194
|
+
return err;
|
|
1195
|
+
}
|
|
1196
|
+
/** Call every subscriber's error handler registered for `room` (#661). */
|
|
1197
|
+
notifyRoomError(room, error) {
|
|
1198
|
+
const handlers = this.roomErrorMap.get(room);
|
|
1199
|
+
if (!handlers) return;
|
|
1200
|
+
for (const handler of handlers.values()) {
|
|
1201
|
+
try {
|
|
1202
|
+
handler(error);
|
|
1203
|
+
} catch {
|
|
1152
1204
|
}
|
|
1153
1205
|
}
|
|
1154
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Re-run the subscribe handshake for one already-tracked room. Shared by
|
|
1209
|
+
* reconnect resubscription (`resubscribeAll`) and the #661
|
|
1210
|
+
* `subscription-invalidated` handler.
|
|
1211
|
+
*
|
|
1212
|
+
* - Deduped per room via `resubscribingRooms` so the duplicate invalidation
|
|
1213
|
+
* events the gateway emits (one per evicted room) collapse to one handshake.
|
|
1214
|
+
* - Snapshots the room's generation; the ack acts only if it is still current,
|
|
1215
|
+
* so a late ack cannot clobber an unsubscribe/re-subscribe that happened in
|
|
1216
|
+
* the meantime — and a success that arrives after the room was removed emits
|
|
1217
|
+
* a compensating `unsubscribe` so no ghost server room is left behind.
|
|
1218
|
+
* - On a genuine denial the subscription is evicted and its stored `onError`
|
|
1219
|
+
* is called (with the typed `.code`), instead of silently going quiet.
|
|
1220
|
+
*/
|
|
1221
|
+
resubscribeRoom(room) {
|
|
1222
|
+
if (!this.socket) return;
|
|
1223
|
+
if (this.resubscribingRooms.has(room)) return;
|
|
1224
|
+
if (!this.subscriptions.has(room)) return;
|
|
1225
|
+
const parsed = this.parseRoom(room);
|
|
1226
|
+
if (!parsed) return;
|
|
1227
|
+
const { projectId, collectionName } = parsed;
|
|
1228
|
+
const where = this.roomWhereMap.get(room);
|
|
1229
|
+
const generation = this.roomGeneration.get(room) ?? 0;
|
|
1230
|
+
const payload = { projectId, collectionName };
|
|
1231
|
+
if (where) payload["where"] = where;
|
|
1232
|
+
this.resubscribingRooms.add(room);
|
|
1233
|
+
this.socket.emit(
|
|
1234
|
+
"subscribe",
|
|
1235
|
+
payload,
|
|
1236
|
+
(response) => {
|
|
1237
|
+
this.resubscribingRooms.delete(room);
|
|
1238
|
+
if ((this.roomGeneration.get(room) ?? 0) !== generation) {
|
|
1239
|
+
if (!this.subscriptions.has(room) && !response?.error) {
|
|
1240
|
+
this.socket?.emit("unsubscribe", payload);
|
|
1241
|
+
}
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
if (response?.error) {
|
|
1245
|
+
const error = this.toSubscribeError(response);
|
|
1246
|
+
this.notifyRoomError(room, error);
|
|
1247
|
+
this.subscriptions.delete(room);
|
|
1248
|
+
this.roomWhereMap.delete(room);
|
|
1249
|
+
this.roomErrorMap.delete(room);
|
|
1250
|
+
this.roomGeneration.delete(room);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1155
1255
|
emitSubscribeEvents(state) {
|
|
1156
1256
|
return new Promise((resolve) => {
|
|
1157
1257
|
if (!this.socket) {
|
|
@@ -1727,9 +1827,11 @@ var AuthModule = class {
|
|
|
1727
1827
|
}
|
|
1728
1828
|
async storeTokensFromRegister(response) {
|
|
1729
1829
|
if (!response.access_token) return;
|
|
1830
|
+
const expiresAt = response.expires_in ? Math.floor(Date.now() / 1e3) + response.expires_in : void 0;
|
|
1730
1831
|
await this.tokenManager.setTokens({
|
|
1731
1832
|
accessToken: response.access_token,
|
|
1732
|
-
refreshToken: response.refresh_token
|
|
1833
|
+
refreshToken: response.refresh_token,
|
|
1834
|
+
expiresAt
|
|
1733
1835
|
});
|
|
1734
1836
|
this.emitState("authenticated");
|
|
1735
1837
|
}
|