@spacelr/sdk 0.10.1 → 0.10.3
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 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +97 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -840,13 +840,15 @@ var RealtimeClient = class {
|
|
|
840
840
|
}
|
|
841
841
|
await this.ensureConnected();
|
|
842
842
|
const streamKey = `stream:${options.projectId}:${options.collectionName}`;
|
|
843
|
+
const subscriptionKey = this.streamSubscriptionKey(streamKey, options.where);
|
|
843
844
|
const state = {
|
|
844
845
|
options,
|
|
845
846
|
lastDeliveredId: options.sinceId ?? null,
|
|
846
847
|
streamKey,
|
|
848
|
+
subscriptionKey,
|
|
847
849
|
dispatchQueue: Promise.resolve()
|
|
848
850
|
};
|
|
849
|
-
const existing = this.streamSubscriptions.get(
|
|
851
|
+
const existing = this.streamSubscriptions.get(subscriptionKey);
|
|
850
852
|
if (existing && existing.size > 0) {
|
|
851
853
|
existing.add(state);
|
|
852
854
|
const syntheticAck = {
|
|
@@ -861,13 +863,13 @@ var RealtimeClient = class {
|
|
|
861
863
|
}
|
|
862
864
|
const set = /* @__PURE__ */ new Set();
|
|
863
865
|
set.add(state);
|
|
864
|
-
this.streamSubscriptions.set(
|
|
866
|
+
this.streamSubscriptions.set(subscriptionKey, set);
|
|
865
867
|
try {
|
|
866
868
|
const ack = await this.emitSubscribeEvents(state);
|
|
867
869
|
if (ack.error) {
|
|
868
870
|
const message = ack.message ?? ack.error;
|
|
869
871
|
const err = new SpacelrError(message, ack.error);
|
|
870
|
-
this.evictStreamKey(
|
|
872
|
+
this.evictStreamKey(subscriptionKey, err, state);
|
|
871
873
|
options.onError?.(err);
|
|
872
874
|
throw err;
|
|
873
875
|
}
|
|
@@ -877,7 +879,7 @@ var RealtimeClient = class {
|
|
|
877
879
|
};
|
|
878
880
|
} catch (err) {
|
|
879
881
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
880
|
-
this.evictStreamKey(
|
|
882
|
+
this.evictStreamKey(subscriptionKey, error, state);
|
|
881
883
|
throw error;
|
|
882
884
|
}
|
|
883
885
|
}
|
|
@@ -936,6 +938,52 @@ var RealtimeClient = class {
|
|
|
936
938
|
const filterStr = Object.keys(where).sort().map((k) => `${k}=${JSON.stringify(where[k])}`).join("&");
|
|
937
939
|
return `${base}?${filterStr}`;
|
|
938
940
|
}
|
|
941
|
+
/**
|
|
942
|
+
* Stream subscription identity (#643): `streamKey` + canonical(`where`), so
|
|
943
|
+
* two `subscribeEvents` on the same collection with different filters get
|
|
944
|
+
* distinct handshakes/readers instead of one colliding on the bare streamKey.
|
|
945
|
+
* Empty/absent `where` keeps the bare streamKey. Same type-preserving sorted
|
|
946
|
+
* encoding as `buildRoomKey` / the gateway's `streamSubscriptionKey`; it is a
|
|
947
|
+
* client-local Map key only (never sent — the gateway derives its own).
|
|
948
|
+
*/
|
|
949
|
+
streamSubscriptionKey(streamKey, where) {
|
|
950
|
+
if (!where || Object.keys(where).length === 0) return streamKey;
|
|
951
|
+
const filterStr = Object.keys(where).sort().map((k) => `${k}=${JSON.stringify(where[k])}`).join("&");
|
|
952
|
+
return `${streamKey}?${filterStr}`;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* All per-filter subscription sets registered for one collection's stream
|
|
956
|
+
* (#643). The socket delivers events keyed only by `streamKey` (no `where`),
|
|
957
|
+
* so a dispatch / gap for `streamKey` must reach every filter variant:
|
|
958
|
+
* the bare `streamKey` entry plus any `${streamKey}?…` filtered entries.
|
|
959
|
+
*/
|
|
960
|
+
streamSetsForStreamKey(streamKey) {
|
|
961
|
+
const sets = [];
|
|
962
|
+
const prefix = `${streamKey}?`;
|
|
963
|
+
for (const [key, set] of this.streamSubscriptions) {
|
|
964
|
+
if (key === streamKey || key.startsWith(prefix)) sets.push(set);
|
|
965
|
+
}
|
|
966
|
+
return sets;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Whether a stream event should reach a subscriber with this `where`. The
|
|
970
|
+
* server already filters doc-bearing events per per-filter reader, but events
|
|
971
|
+
* arrive on one socket without a `where`, so we re-filter to route each to the
|
|
972
|
+
* right subscriber (and as defense against desync/over-delivery). An event
|
|
973
|
+
* with no document (tombstone delete) can't be filtered — it is delivered to
|
|
974
|
+
* every subscriber on the collection, as the server forwards it to every
|
|
975
|
+
* reader. Empty `where` matches everything.
|
|
976
|
+
*/
|
|
977
|
+
eventMatchesStreamWhere(event, where) {
|
|
978
|
+
if (!where || Object.keys(where).length === 0) return true;
|
|
979
|
+
if (!event.document) return true;
|
|
980
|
+
for (const [key, value] of Object.entries(where)) {
|
|
981
|
+
if (!whereValueMatches(resolveWherePath(event.document, key), value)) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
939
987
|
async ensureConnected() {
|
|
940
988
|
if (this.socket?.connected) return;
|
|
941
989
|
if (this.connecting) {
|
|
@@ -1074,10 +1122,10 @@ var RealtimeClient = class {
|
|
|
1074
1122
|
});
|
|
1075
1123
|
this.socket.on("event-gap", (info) => {
|
|
1076
1124
|
const streamKey = `stream:${info.projectId}:${info.collectionName}`;
|
|
1077
|
-
const set
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1125
|
+
for (const set of this.streamSetsForStreamKey(streamKey)) {
|
|
1126
|
+
for (const state of set) {
|
|
1127
|
+
state.options.onGap?.(info);
|
|
1128
|
+
}
|
|
1081
1129
|
}
|
|
1082
1130
|
});
|
|
1083
1131
|
this.socket.on("disconnect", () => {
|
|
@@ -1282,38 +1330,46 @@ var RealtimeClient = class {
|
|
|
1282
1330
|
return payload;
|
|
1283
1331
|
}
|
|
1284
1332
|
unsubscribeStream(state) {
|
|
1285
|
-
const set = this.streamSubscriptions.get(state.
|
|
1333
|
+
const set = this.streamSubscriptions.get(state.subscriptionKey);
|
|
1286
1334
|
if (!set) return;
|
|
1287
1335
|
set.delete(state);
|
|
1288
1336
|
if (set.size === 0) {
|
|
1289
|
-
this.streamSubscriptions.delete(state.
|
|
1290
|
-
|
|
1337
|
+
this.streamSubscriptions.delete(state.subscriptionKey);
|
|
1338
|
+
const payload = {
|
|
1291
1339
|
projectId: state.options.projectId,
|
|
1292
1340
|
collectionName: state.options.collectionName
|
|
1293
|
-
}
|
|
1341
|
+
};
|
|
1342
|
+
if (state.options.where && Object.keys(state.options.where).length > 0) {
|
|
1343
|
+
payload["where"] = state.options.where;
|
|
1344
|
+
}
|
|
1345
|
+
this.socket?.emit("unsubscribe-events", payload);
|
|
1294
1346
|
}
|
|
1295
1347
|
}
|
|
1296
1348
|
dispatchStreamEvent(payload) {
|
|
1297
1349
|
if (!payload.eventId) return Promise.resolve();
|
|
1298
1350
|
const streamKey = `stream:${payload.projectId}:${payload.collectionName}`;
|
|
1299
|
-
const
|
|
1300
|
-
if (
|
|
1351
|
+
const sets = this.streamSetsForStreamKey(streamKey);
|
|
1352
|
+
if (sets.length === 0) return Promise.resolve();
|
|
1301
1353
|
const entryId = payload.eventId;
|
|
1302
|
-
for (const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
state.lastDeliveredId
|
|
1309
|
-
} catch (err) {
|
|
1310
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1354
|
+
for (const set of sets) {
|
|
1355
|
+
for (const state of set) {
|
|
1356
|
+
if (!this.eventMatchesStreamWhere(payload, state.options.where)) continue;
|
|
1357
|
+
state.dispatchQueue = state.dispatchQueue.then(async () => {
|
|
1358
|
+
const liveSet = this.streamSubscriptions.get(state.subscriptionKey);
|
|
1359
|
+
if (!liveSet?.has(state)) return;
|
|
1360
|
+
if (state.lastDeliveredId === entryId) return;
|
|
1311
1361
|
try {
|
|
1312
|
-
state.options.
|
|
1313
|
-
|
|
1362
|
+
await state.options.onEvent({ eventId: entryId, event: payload });
|
|
1363
|
+
state.lastDeliveredId = entryId;
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1366
|
+
try {
|
|
1367
|
+
state.options.onError?.(error);
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1314
1370
|
}
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1317
1373
|
}
|
|
1318
1374
|
return Promise.resolve();
|
|
1319
1375
|
}
|
|
@@ -1339,15 +1395,19 @@ var RealtimeClient = class {
|
|
|
1339
1395
|
state.options.onError?.(err);
|
|
1340
1396
|
}
|
|
1341
1397
|
if (PERMANENT_STREAM_ACK_ERRORS.has(ack.error)) {
|
|
1342
|
-
this.streamSubscriptions.delete(primary.
|
|
1398
|
+
this.streamSubscriptions.delete(primary.subscriptionKey);
|
|
1343
1399
|
}
|
|
1344
1400
|
return;
|
|
1345
1401
|
}
|
|
1346
|
-
if (!this.streamSubscriptions.has(primary.
|
|
1347
|
-
|
|
1402
|
+
if (!this.streamSubscriptions.has(primary.subscriptionKey)) {
|
|
1403
|
+
const payload = {
|
|
1348
1404
|
projectId: primary.options.projectId,
|
|
1349
1405
|
collectionName: primary.options.collectionName
|
|
1350
|
-
}
|
|
1406
|
+
};
|
|
1407
|
+
if (primary.options.where && Object.keys(primary.options.where).length > 0) {
|
|
1408
|
+
payload["where"] = primary.options.where;
|
|
1409
|
+
}
|
|
1410
|
+
this.socket?.emit("unsubscribe-events", payload);
|
|
1351
1411
|
}
|
|
1352
1412
|
} catch (err) {
|
|
1353
1413
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1379,8 +1439,8 @@ var RealtimeClient = class {
|
|
|
1379
1439
|
* streamKey via the dedup path. Fires `onError` on all siblings, then
|
|
1380
1440
|
* removes the entire streamKey entry from `streamSubscriptions`.
|
|
1381
1441
|
*/
|
|
1382
|
-
evictStreamKey(
|
|
1383
|
-
const set = this.streamSubscriptions.get(
|
|
1442
|
+
evictStreamKey(subscriptionKey, err, primary) {
|
|
1443
|
+
const set = this.streamSubscriptions.get(subscriptionKey);
|
|
1384
1444
|
if (!set) return;
|
|
1385
1445
|
for (const sibling of [...set]) {
|
|
1386
1446
|
if (sibling !== primary) {
|
|
@@ -1388,7 +1448,7 @@ var RealtimeClient = class {
|
|
|
1388
1448
|
}
|
|
1389
1449
|
set.delete(sibling);
|
|
1390
1450
|
}
|
|
1391
|
-
this.streamSubscriptions.delete(
|
|
1451
|
+
this.streamSubscriptions.delete(subscriptionKey);
|
|
1392
1452
|
}
|
|
1393
1453
|
};
|
|
1394
1454
|
|
|
@@ -1827,9 +1887,11 @@ var AuthModule = class {
|
|
|
1827
1887
|
}
|
|
1828
1888
|
async storeTokensFromRegister(response) {
|
|
1829
1889
|
if (!response.access_token) return;
|
|
1890
|
+
const expiresAt = response.expires_in ? Math.floor(Date.now() / 1e3) + response.expires_in : void 0;
|
|
1830
1891
|
await this.tokenManager.setTokens({
|
|
1831
1892
|
accessToken: response.access_token,
|
|
1832
|
-
refreshToken: response.refresh_token
|
|
1893
|
+
refreshToken: response.refresh_token,
|
|
1894
|
+
expiresAt
|
|
1833
1895
|
});
|
|
1834
1896
|
this.emitState("authenticated");
|
|
1835
1897
|
}
|