@spacelr/sdk 0.10.0 → 0.10.1
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 +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +123 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -597,6 +597,10 @@ declare class RealtimeClient {
|
|
|
597
597
|
private subscriptions;
|
|
598
598
|
private connecting;
|
|
599
599
|
private roomWhereMap;
|
|
600
|
+
private roomErrorMap;
|
|
601
|
+
private roomGeneration;
|
|
602
|
+
private subEpoch;
|
|
603
|
+
private resubscribingRooms;
|
|
600
604
|
private unsubscribeFromTokenRefreshed;
|
|
601
605
|
private onVisibilityChange;
|
|
602
606
|
private onOnline;
|
|
@@ -606,7 +610,9 @@ declare class RealtimeClient {
|
|
|
606
610
|
private connectionStateListeners;
|
|
607
611
|
private streamSubscriptions;
|
|
608
612
|
constructor(config: RealtimeConfig);
|
|
609
|
-
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error
|
|
613
|
+
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error & {
|
|
614
|
+
code?: string;
|
|
615
|
+
}) => void, where?: WhereFilter): Promise<() => void>;
|
|
610
616
|
/**
|
|
611
617
|
* Subscribe to a stream-mode collection using Redis Streams replay +
|
|
612
618
|
* cursor-based delivery. Parallel to `subscribe()` (which targets
|
|
@@ -698,6 +704,31 @@ declare class RealtimeClient {
|
|
|
698
704
|
private ensureWakeListeners;
|
|
699
705
|
private detachWakeListeners;
|
|
700
706
|
private resubscribeAll;
|
|
707
|
+
/**
|
|
708
|
+
* Parse a pubsub room key back into its projectId + collectionName.
|
|
709
|
+
* Room format: `db:{projectId}:{collectionName}` or that base plus `?filter`.
|
|
710
|
+
* Returns null for anything that isn't a well-formed `db:` room.
|
|
711
|
+
*/
|
|
712
|
+
private parseRoom;
|
|
713
|
+
/** Build an Error carrying the gateway's typed `errorCode` (if any) as `.code`. */
|
|
714
|
+
private toSubscribeError;
|
|
715
|
+
/** Call every subscriber's error handler registered for `room` (#661). */
|
|
716
|
+
private notifyRoomError;
|
|
717
|
+
/**
|
|
718
|
+
* Re-run the subscribe handshake for one already-tracked room. Shared by
|
|
719
|
+
* reconnect resubscription (`resubscribeAll`) and the #661
|
|
720
|
+
* `subscription-invalidated` handler.
|
|
721
|
+
*
|
|
722
|
+
* - Deduped per room via `resubscribingRooms` so the duplicate invalidation
|
|
723
|
+
* events the gateway emits (one per evicted room) collapse to one handshake.
|
|
724
|
+
* - Snapshots the room's generation; the ack acts only if it is still current,
|
|
725
|
+
* so a late ack cannot clobber an unsubscribe/re-subscribe that happened in
|
|
726
|
+
* the meantime — and a success that arrives after the room was removed emits
|
|
727
|
+
* a compensating `unsubscribe` so no ghost server room is left behind.
|
|
728
|
+
* - On a genuine denial the subscription is evicted and its stored `onError`
|
|
729
|
+
* is called (with the typed `.code`), instead of silently going quiet.
|
|
730
|
+
*/
|
|
731
|
+
private resubscribeRoom;
|
|
701
732
|
private emitSubscribeEvents;
|
|
702
733
|
private buildStreamPayload;
|
|
703
734
|
private unsubscribeStream;
|
package/dist/index.d.ts
CHANGED
|
@@ -597,6 +597,10 @@ declare class RealtimeClient {
|
|
|
597
597
|
private subscriptions;
|
|
598
598
|
private connecting;
|
|
599
599
|
private roomWhereMap;
|
|
600
|
+
private roomErrorMap;
|
|
601
|
+
private roomGeneration;
|
|
602
|
+
private subEpoch;
|
|
603
|
+
private resubscribingRooms;
|
|
600
604
|
private unsubscribeFromTokenRefreshed;
|
|
601
605
|
private onVisibilityChange;
|
|
602
606
|
private onOnline;
|
|
@@ -606,7 +610,9 @@ declare class RealtimeClient {
|
|
|
606
610
|
private connectionStateListeners;
|
|
607
611
|
private streamSubscriptions;
|
|
608
612
|
constructor(config: RealtimeConfig);
|
|
609
|
-
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error
|
|
613
|
+
subscribe(projectId: string, collectionName: string, callback: (event: DatabaseChangeEvent) => void, onError?: (error: Error & {
|
|
614
|
+
code?: string;
|
|
615
|
+
}) => void, where?: WhereFilter): Promise<() => void>;
|
|
610
616
|
/**
|
|
611
617
|
* Subscribe to a stream-mode collection using Redis Streams replay +
|
|
612
618
|
* cursor-based delivery. Parallel to `subscribe()` (which targets
|
|
@@ -698,6 +704,31 @@ declare class RealtimeClient {
|
|
|
698
704
|
private ensureWakeListeners;
|
|
699
705
|
private detachWakeListeners;
|
|
700
706
|
private resubscribeAll;
|
|
707
|
+
/**
|
|
708
|
+
* Parse a pubsub room key back into its projectId + collectionName.
|
|
709
|
+
* Room format: `db:{projectId}:{collectionName}` or that base plus `?filter`.
|
|
710
|
+
* Returns null for anything that isn't a well-formed `db:` room.
|
|
711
|
+
*/
|
|
712
|
+
private parseRoom;
|
|
713
|
+
/** Build an Error carrying the gateway's typed `errorCode` (if any) as `.code`. */
|
|
714
|
+
private toSubscribeError;
|
|
715
|
+
/** Call every subscriber's error handler registered for `room` (#661). */
|
|
716
|
+
private notifyRoomError;
|
|
717
|
+
/**
|
|
718
|
+
* Re-run the subscribe handshake for one already-tracked room. Shared by
|
|
719
|
+
* reconnect resubscription (`resubscribeAll`) and the #661
|
|
720
|
+
* `subscription-invalidated` handler.
|
|
721
|
+
*
|
|
722
|
+
* - Deduped per room via `resubscribingRooms` so the duplicate invalidation
|
|
723
|
+
* events the gateway emits (one per evicted room) collapse to one handshake.
|
|
724
|
+
* - Snapshots the room's generation; the ack acts only if it is still current,
|
|
725
|
+
* so a late ack cannot clobber an unsubscribe/re-subscribe that happened in
|
|
726
|
+
* the meantime — and a success that arrives after the room was removed emits
|
|
727
|
+
* a compensating `unsubscribe` so no ghost server room is left behind.
|
|
728
|
+
* - On a genuine denial the subscription is evicted and its stored `onError`
|
|
729
|
+
* is called (with the typed `.code`), instead of silently going quiet.
|
|
730
|
+
*/
|
|
731
|
+
private resubscribeRoom;
|
|
701
732
|
private emitSubscribeEvents;
|
|
702
733
|
private buildStreamPayload;
|
|
703
734
|
private unsubscribeStream;
|
package/dist/index.js
CHANGED
|
@@ -715,6 +715,21 @@ var RealtimeClient = class {
|
|
|
715
715
|
this.connecting = null;
|
|
716
716
|
// Store original where objects per room for reconnect resubscription
|
|
717
717
|
this.roomWhereMap = /* @__PURE__ */ new Map();
|
|
718
|
+
// Per-room, per-callback error handlers so EVERY subscriber to a room (not
|
|
719
|
+
// just the first) is notified on a denial, and a departed subscriber's handler
|
|
720
|
+
// is never left dangling for its still-active siblings (#661).
|
|
721
|
+
this.roomErrorMap = /* @__PURE__ */ new Map();
|
|
722
|
+
// Per-room generation stamp, taken from a global monotonic counter each time a
|
|
723
|
+
// room becomes active. A re-subscribe snapshots the room's stamp and acts on
|
|
724
|
+
// the ack only if it is still current — so a late ack cannot notify or clean
|
|
725
|
+
// up after the app has unsubscribed/re-subscribed (#661). Entries are deleted
|
|
726
|
+
// on last-unsubscribe (bounded); the global counter guarantees a re-subscribe
|
|
727
|
+
// always gets a strictly higher stamp, so there is no ABA hole.
|
|
728
|
+
this.roomGeneration = /* @__PURE__ */ new Map();
|
|
729
|
+
this.subEpoch = 0;
|
|
730
|
+
// Rooms with a re-subscribe handshake in flight — dedupes the duplicate
|
|
731
|
+
// `subscription-invalidated` events the gateway emits per evicted room (#661).
|
|
732
|
+
this.resubscribingRooms = /* @__PURE__ */ new Set();
|
|
718
733
|
this.unsubscribeFromTokenRefreshed = null;
|
|
719
734
|
// Wake-up listeners (browser only) — recover from long OS suspends where
|
|
720
735
|
// socket.io's internal reconnect loop may have already given up.
|
|
@@ -743,7 +758,17 @@ var RealtimeClient = class {
|
|
|
743
758
|
}
|
|
744
759
|
const callbacks = this.subscriptions.get(room);
|
|
745
760
|
callbacks?.add(callback);
|
|
761
|
+
if (onError) {
|
|
762
|
+
let handlers = this.roomErrorMap.get(room);
|
|
763
|
+
if (!handlers) {
|
|
764
|
+
handlers = /* @__PURE__ */ new Map();
|
|
765
|
+
this.roomErrorMap.set(room, handlers);
|
|
766
|
+
}
|
|
767
|
+
handlers.set(callback, onError);
|
|
768
|
+
}
|
|
746
769
|
if (callbacks?.size === 1) {
|
|
770
|
+
const generation = ++this.subEpoch;
|
|
771
|
+
this.roomGeneration.set(room, generation);
|
|
747
772
|
if (where && Object.keys(where).length > 0) {
|
|
748
773
|
this.roomWhereMap.set(room, where);
|
|
749
774
|
}
|
|
@@ -755,8 +780,8 @@ var RealtimeClient = class {
|
|
|
755
780
|
"subscribe",
|
|
756
781
|
payload,
|
|
757
782
|
(response) => {
|
|
758
|
-
if (response.error &&
|
|
759
|
-
|
|
783
|
+
if (response.error && this.roomGeneration.get(room) === generation) {
|
|
784
|
+
this.notifyRoomError(room, this.toSubscribeError(response));
|
|
760
785
|
}
|
|
761
786
|
}
|
|
762
787
|
);
|
|
@@ -765,9 +790,12 @@ var RealtimeClient = class {
|
|
|
765
790
|
const callbacks2 = this.subscriptions.get(room);
|
|
766
791
|
if (callbacks2) {
|
|
767
792
|
callbacks2.delete(callback);
|
|
793
|
+
this.roomErrorMap.get(room)?.delete(callback);
|
|
768
794
|
if (callbacks2.size === 0) {
|
|
769
795
|
this.subscriptions.delete(room);
|
|
770
796
|
this.roomWhereMap.delete(room);
|
|
797
|
+
this.roomErrorMap.delete(room);
|
|
798
|
+
this.roomGeneration.delete(room);
|
|
771
799
|
const payload = { projectId, collectionName };
|
|
772
800
|
if (where && Object.keys(where).length > 0) {
|
|
773
801
|
payload["where"] = where;
|
|
@@ -920,6 +948,9 @@ var RealtimeClient = class {
|
|
|
920
948
|
}
|
|
921
949
|
this.subscriptions.clear();
|
|
922
950
|
this.roomWhereMap.clear();
|
|
951
|
+
this.roomErrorMap.clear();
|
|
952
|
+
this.roomGeneration.clear();
|
|
953
|
+
this.resubscribingRooms.clear();
|
|
923
954
|
this.streamSubscriptions.clear();
|
|
924
955
|
this.connecting = null;
|
|
925
956
|
}
|
|
@@ -1069,6 +1100,18 @@ var RealtimeClient = class {
|
|
|
1069
1100
|
}
|
|
1070
1101
|
}
|
|
1071
1102
|
});
|
|
1103
|
+
this.socket.on(
|
|
1104
|
+
"subscription-invalidated",
|
|
1105
|
+
(payload) => {
|
|
1106
|
+
if (!payload?.projectId || !payload?.collectionName) return;
|
|
1107
|
+
const base = `db:${payload.projectId}:${payload.collectionName}`;
|
|
1108
|
+
for (const room of this.subscriptions.keys()) {
|
|
1109
|
+
if (room === base || room.startsWith(`${base}?`)) {
|
|
1110
|
+
this.resubscribeRoom(room);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
);
|
|
1072
1115
|
this.socket.on("event", (payload) => {
|
|
1073
1116
|
this.dispatchStreamEvent(payload).catch(() => void 0);
|
|
1074
1117
|
});
|
|
@@ -1170,31 +1213,88 @@ var RealtimeClient = class {
|
|
|
1170
1213
|
this.onOnline = null;
|
|
1171
1214
|
}
|
|
1172
1215
|
resubscribeAll() {
|
|
1216
|
+
this.resubscribingRooms.clear();
|
|
1173
1217
|
for (const [room] of this.subscriptions) {
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1218
|
+
this.resubscribeRoom(room);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Parse a pubsub room key back into its projectId + collectionName.
|
|
1223
|
+
* Room format: `db:{projectId}:{collectionName}` or that base plus `?filter`.
|
|
1224
|
+
* Returns null for anything that isn't a well-formed `db:` room.
|
|
1225
|
+
*/
|
|
1226
|
+
parseRoom(room) {
|
|
1227
|
+
const queryIdx = room.indexOf("?");
|
|
1228
|
+
const basePart = queryIdx >= 0 ? room.substring(0, queryIdx) : room;
|
|
1229
|
+
const parts = basePart.split(":");
|
|
1230
|
+
if (parts.length < 3 || parts[0] !== "db") return null;
|
|
1231
|
+
return { projectId: parts[1], collectionName: parts.slice(2).join(":") };
|
|
1232
|
+
}
|
|
1233
|
+
/** Build an Error carrying the gateway's typed `errorCode` (if any) as `.code`. */
|
|
1234
|
+
toSubscribeError(response) {
|
|
1235
|
+
const err = new Error(response.error ?? "Subscribe denied");
|
|
1236
|
+
if (response.errorCode) err.code = response.errorCode;
|
|
1237
|
+
return err;
|
|
1238
|
+
}
|
|
1239
|
+
/** Call every subscriber's error handler registered for `room` (#661). */
|
|
1240
|
+
notifyRoomError(room, error) {
|
|
1241
|
+
const handlers = this.roomErrorMap.get(room);
|
|
1242
|
+
if (!handlers) return;
|
|
1243
|
+
for (const handler of handlers.values()) {
|
|
1244
|
+
try {
|
|
1245
|
+
handler(error);
|
|
1246
|
+
} catch {
|
|
1195
1247
|
}
|
|
1196
1248
|
}
|
|
1197
1249
|
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Re-run the subscribe handshake for one already-tracked room. Shared by
|
|
1252
|
+
* reconnect resubscription (`resubscribeAll`) and the #661
|
|
1253
|
+
* `subscription-invalidated` handler.
|
|
1254
|
+
*
|
|
1255
|
+
* - Deduped per room via `resubscribingRooms` so the duplicate invalidation
|
|
1256
|
+
* events the gateway emits (one per evicted room) collapse to one handshake.
|
|
1257
|
+
* - Snapshots the room's generation; the ack acts only if it is still current,
|
|
1258
|
+
* so a late ack cannot clobber an unsubscribe/re-subscribe that happened in
|
|
1259
|
+
* the meantime — and a success that arrives after the room was removed emits
|
|
1260
|
+
* a compensating `unsubscribe` so no ghost server room is left behind.
|
|
1261
|
+
* - On a genuine denial the subscription is evicted and its stored `onError`
|
|
1262
|
+
* is called (with the typed `.code`), instead of silently going quiet.
|
|
1263
|
+
*/
|
|
1264
|
+
resubscribeRoom(room) {
|
|
1265
|
+
if (!this.socket) return;
|
|
1266
|
+
if (this.resubscribingRooms.has(room)) return;
|
|
1267
|
+
if (!this.subscriptions.has(room)) return;
|
|
1268
|
+
const parsed = this.parseRoom(room);
|
|
1269
|
+
if (!parsed) return;
|
|
1270
|
+
const { projectId, collectionName } = parsed;
|
|
1271
|
+
const where = this.roomWhereMap.get(room);
|
|
1272
|
+
const generation = this.roomGeneration.get(room) ?? 0;
|
|
1273
|
+
const payload = { projectId, collectionName };
|
|
1274
|
+
if (where) payload["where"] = where;
|
|
1275
|
+
this.resubscribingRooms.add(room);
|
|
1276
|
+
this.socket.emit(
|
|
1277
|
+
"subscribe",
|
|
1278
|
+
payload,
|
|
1279
|
+
(response) => {
|
|
1280
|
+
this.resubscribingRooms.delete(room);
|
|
1281
|
+
if ((this.roomGeneration.get(room) ?? 0) !== generation) {
|
|
1282
|
+
if (!this.subscriptions.has(room) && !response?.error) {
|
|
1283
|
+
this.socket?.emit("unsubscribe", payload);
|
|
1284
|
+
}
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (response?.error) {
|
|
1288
|
+
const error = this.toSubscribeError(response);
|
|
1289
|
+
this.notifyRoomError(room, error);
|
|
1290
|
+
this.subscriptions.delete(room);
|
|
1291
|
+
this.roomWhereMap.delete(room);
|
|
1292
|
+
this.roomErrorMap.delete(room);
|
|
1293
|
+
this.roomGeneration.delete(room);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1198
1298
|
emitSubscribeEvents(state) {
|
|
1199
1299
|
return new Promise((resolve) => {
|
|
1200
1300
|
if (!this.socket) {
|