@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.d.mts
CHANGED
|
@@ -62,6 +62,12 @@ interface RegisterResponse {
|
|
|
62
62
|
access_token?: string;
|
|
63
63
|
/** Omitted by the server when `emailVerificationRequired` is true. */
|
|
64
64
|
refresh_token?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Access-token lifetime in seconds. Present whenever tokens are issued
|
|
67
|
+
* (i.e. not when email verification is required). Lets the SDK store an
|
|
68
|
+
* expiry and refresh proactively instead of on the first request.
|
|
69
|
+
*/
|
|
70
|
+
expires_in?: number;
|
|
65
71
|
emailVerificationRequired?: boolean;
|
|
66
72
|
}
|
|
67
73
|
interface TokenResponse {
|
|
@@ -687,6 +693,32 @@ declare class RealtimeClient {
|
|
|
687
693
|
onConnectionStateChanged(listener: (state: ConnectionState) => void): () => void;
|
|
688
694
|
private setConnectionState;
|
|
689
695
|
private buildRoomKey;
|
|
696
|
+
/**
|
|
697
|
+
* Stream subscription identity (#643): `streamKey` + canonical(`where`), so
|
|
698
|
+
* two `subscribeEvents` on the same collection with different filters get
|
|
699
|
+
* distinct handshakes/readers instead of one colliding on the bare streamKey.
|
|
700
|
+
* Empty/absent `where` keeps the bare streamKey. Same type-preserving sorted
|
|
701
|
+
* encoding as `buildRoomKey` / the gateway's `streamSubscriptionKey`; it is a
|
|
702
|
+
* client-local Map key only (never sent — the gateway derives its own).
|
|
703
|
+
*/
|
|
704
|
+
private streamSubscriptionKey;
|
|
705
|
+
/**
|
|
706
|
+
* All per-filter subscription sets registered for one collection's stream
|
|
707
|
+
* (#643). The socket delivers events keyed only by `streamKey` (no `where`),
|
|
708
|
+
* so a dispatch / gap for `streamKey` must reach every filter variant:
|
|
709
|
+
* the bare `streamKey` entry plus any `${streamKey}?…` filtered entries.
|
|
710
|
+
*/
|
|
711
|
+
private streamSetsForStreamKey;
|
|
712
|
+
/**
|
|
713
|
+
* Whether a stream event should reach a subscriber with this `where`. The
|
|
714
|
+
* server already filters doc-bearing events per per-filter reader, but events
|
|
715
|
+
* arrive on one socket without a `where`, so we re-filter to route each to the
|
|
716
|
+
* right subscriber (and as defense against desync/over-delivery). An event
|
|
717
|
+
* with no document (tombstone delete) can't be filtered — it is delivered to
|
|
718
|
+
* every subscriber on the collection, as the server forwards it to every
|
|
719
|
+
* reader. Empty `where` matches everything.
|
|
720
|
+
*/
|
|
721
|
+
private eventMatchesStreamWhere;
|
|
690
722
|
private ensureConnected;
|
|
691
723
|
private connect;
|
|
692
724
|
private eventMatchesRoom;
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,12 @@ interface RegisterResponse {
|
|
|
62
62
|
access_token?: string;
|
|
63
63
|
/** Omitted by the server when `emailVerificationRequired` is true. */
|
|
64
64
|
refresh_token?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Access-token lifetime in seconds. Present whenever tokens are issued
|
|
67
|
+
* (i.e. not when email verification is required). Lets the SDK store an
|
|
68
|
+
* expiry and refresh proactively instead of on the first request.
|
|
69
|
+
*/
|
|
70
|
+
expires_in?: number;
|
|
65
71
|
emailVerificationRequired?: boolean;
|
|
66
72
|
}
|
|
67
73
|
interface TokenResponse {
|
|
@@ -687,6 +693,32 @@ declare class RealtimeClient {
|
|
|
687
693
|
onConnectionStateChanged(listener: (state: ConnectionState) => void): () => void;
|
|
688
694
|
private setConnectionState;
|
|
689
695
|
private buildRoomKey;
|
|
696
|
+
/**
|
|
697
|
+
* Stream subscription identity (#643): `streamKey` + canonical(`where`), so
|
|
698
|
+
* two `subscribeEvents` on the same collection with different filters get
|
|
699
|
+
* distinct handshakes/readers instead of one colliding on the bare streamKey.
|
|
700
|
+
* Empty/absent `where` keeps the bare streamKey. Same type-preserving sorted
|
|
701
|
+
* encoding as `buildRoomKey` / the gateway's `streamSubscriptionKey`; it is a
|
|
702
|
+
* client-local Map key only (never sent — the gateway derives its own).
|
|
703
|
+
*/
|
|
704
|
+
private streamSubscriptionKey;
|
|
705
|
+
/**
|
|
706
|
+
* All per-filter subscription sets registered for one collection's stream
|
|
707
|
+
* (#643). The socket delivers events keyed only by `streamKey` (no `where`),
|
|
708
|
+
* so a dispatch / gap for `streamKey` must reach every filter variant:
|
|
709
|
+
* the bare `streamKey` entry plus any `${streamKey}?…` filtered entries.
|
|
710
|
+
*/
|
|
711
|
+
private streamSetsForStreamKey;
|
|
712
|
+
/**
|
|
713
|
+
* Whether a stream event should reach a subscriber with this `where`. The
|
|
714
|
+
* server already filters doc-bearing events per per-filter reader, but events
|
|
715
|
+
* arrive on one socket without a `where`, so we re-filter to route each to the
|
|
716
|
+
* right subscriber (and as defense against desync/over-delivery). An event
|
|
717
|
+
* with no document (tombstone delete) can't be filtered — it is delivered to
|
|
718
|
+
* every subscriber on the collection, as the server forwards it to every
|
|
719
|
+
* reader. Empty `where` matches everything.
|
|
720
|
+
*/
|
|
721
|
+
private eventMatchesStreamWhere;
|
|
690
722
|
private ensureConnected;
|
|
691
723
|
private connect;
|
|
692
724
|
private eventMatchesRoom;
|
package/dist/index.js
CHANGED
|
@@ -883,13 +883,15 @@ var RealtimeClient = class {
|
|
|
883
883
|
}
|
|
884
884
|
await this.ensureConnected();
|
|
885
885
|
const streamKey = `stream:${options.projectId}:${options.collectionName}`;
|
|
886
|
+
const subscriptionKey = this.streamSubscriptionKey(streamKey, options.where);
|
|
886
887
|
const state = {
|
|
887
888
|
options,
|
|
888
889
|
lastDeliveredId: options.sinceId ?? null,
|
|
889
890
|
streamKey,
|
|
891
|
+
subscriptionKey,
|
|
890
892
|
dispatchQueue: Promise.resolve()
|
|
891
893
|
};
|
|
892
|
-
const existing = this.streamSubscriptions.get(
|
|
894
|
+
const existing = this.streamSubscriptions.get(subscriptionKey);
|
|
893
895
|
if (existing && existing.size > 0) {
|
|
894
896
|
existing.add(state);
|
|
895
897
|
const syntheticAck = {
|
|
@@ -904,13 +906,13 @@ var RealtimeClient = class {
|
|
|
904
906
|
}
|
|
905
907
|
const set = /* @__PURE__ */ new Set();
|
|
906
908
|
set.add(state);
|
|
907
|
-
this.streamSubscriptions.set(
|
|
909
|
+
this.streamSubscriptions.set(subscriptionKey, set);
|
|
908
910
|
try {
|
|
909
911
|
const ack = await this.emitSubscribeEvents(state);
|
|
910
912
|
if (ack.error) {
|
|
911
913
|
const message = ack.message ?? ack.error;
|
|
912
914
|
const err = new SpacelrError(message, ack.error);
|
|
913
|
-
this.evictStreamKey(
|
|
915
|
+
this.evictStreamKey(subscriptionKey, err, state);
|
|
914
916
|
options.onError?.(err);
|
|
915
917
|
throw err;
|
|
916
918
|
}
|
|
@@ -920,7 +922,7 @@ var RealtimeClient = class {
|
|
|
920
922
|
};
|
|
921
923
|
} catch (err) {
|
|
922
924
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
923
|
-
this.evictStreamKey(
|
|
925
|
+
this.evictStreamKey(subscriptionKey, error, state);
|
|
924
926
|
throw error;
|
|
925
927
|
}
|
|
926
928
|
}
|
|
@@ -979,6 +981,52 @@ var RealtimeClient = class {
|
|
|
979
981
|
const filterStr = Object.keys(where).sort().map((k) => `${k}=${JSON.stringify(where[k])}`).join("&");
|
|
980
982
|
return `${base}?${filterStr}`;
|
|
981
983
|
}
|
|
984
|
+
/**
|
|
985
|
+
* Stream subscription identity (#643): `streamKey` + canonical(`where`), so
|
|
986
|
+
* two `subscribeEvents` on the same collection with different filters get
|
|
987
|
+
* distinct handshakes/readers instead of one colliding on the bare streamKey.
|
|
988
|
+
* Empty/absent `where` keeps the bare streamKey. Same type-preserving sorted
|
|
989
|
+
* encoding as `buildRoomKey` / the gateway's `streamSubscriptionKey`; it is a
|
|
990
|
+
* client-local Map key only (never sent — the gateway derives its own).
|
|
991
|
+
*/
|
|
992
|
+
streamSubscriptionKey(streamKey, where) {
|
|
993
|
+
if (!where || Object.keys(where).length === 0) return streamKey;
|
|
994
|
+
const filterStr = Object.keys(where).sort().map((k) => `${k}=${JSON.stringify(where[k])}`).join("&");
|
|
995
|
+
return `${streamKey}?${filterStr}`;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* All per-filter subscription sets registered for one collection's stream
|
|
999
|
+
* (#643). The socket delivers events keyed only by `streamKey` (no `where`),
|
|
1000
|
+
* so a dispatch / gap for `streamKey` must reach every filter variant:
|
|
1001
|
+
* the bare `streamKey` entry plus any `${streamKey}?…` filtered entries.
|
|
1002
|
+
*/
|
|
1003
|
+
streamSetsForStreamKey(streamKey) {
|
|
1004
|
+
const sets = [];
|
|
1005
|
+
const prefix = `${streamKey}?`;
|
|
1006
|
+
for (const [key, set] of this.streamSubscriptions) {
|
|
1007
|
+
if (key === streamKey || key.startsWith(prefix)) sets.push(set);
|
|
1008
|
+
}
|
|
1009
|
+
return sets;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Whether a stream event should reach a subscriber with this `where`. The
|
|
1013
|
+
* server already filters doc-bearing events per per-filter reader, but events
|
|
1014
|
+
* arrive on one socket without a `where`, so we re-filter to route each to the
|
|
1015
|
+
* right subscriber (and as defense against desync/over-delivery). An event
|
|
1016
|
+
* with no document (tombstone delete) can't be filtered — it is delivered to
|
|
1017
|
+
* every subscriber on the collection, as the server forwards it to every
|
|
1018
|
+
* reader. Empty `where` matches everything.
|
|
1019
|
+
*/
|
|
1020
|
+
eventMatchesStreamWhere(event, where) {
|
|
1021
|
+
if (!where || Object.keys(where).length === 0) return true;
|
|
1022
|
+
if (!event.document) return true;
|
|
1023
|
+
for (const [key, value] of Object.entries(where)) {
|
|
1024
|
+
if (!whereValueMatches(resolveWherePath(event.document, key), value)) {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
982
1030
|
async ensureConnected() {
|
|
983
1031
|
if (this.socket?.connected) return;
|
|
984
1032
|
if (this.connecting) {
|
|
@@ -1117,10 +1165,10 @@ var RealtimeClient = class {
|
|
|
1117
1165
|
});
|
|
1118
1166
|
this.socket.on("event-gap", (info) => {
|
|
1119
1167
|
const streamKey = `stream:${info.projectId}:${info.collectionName}`;
|
|
1120
|
-
const set
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1168
|
+
for (const set of this.streamSetsForStreamKey(streamKey)) {
|
|
1169
|
+
for (const state of set) {
|
|
1170
|
+
state.options.onGap?.(info);
|
|
1171
|
+
}
|
|
1124
1172
|
}
|
|
1125
1173
|
});
|
|
1126
1174
|
this.socket.on("disconnect", () => {
|
|
@@ -1325,38 +1373,46 @@ var RealtimeClient = class {
|
|
|
1325
1373
|
return payload;
|
|
1326
1374
|
}
|
|
1327
1375
|
unsubscribeStream(state) {
|
|
1328
|
-
const set = this.streamSubscriptions.get(state.
|
|
1376
|
+
const set = this.streamSubscriptions.get(state.subscriptionKey);
|
|
1329
1377
|
if (!set) return;
|
|
1330
1378
|
set.delete(state);
|
|
1331
1379
|
if (set.size === 0) {
|
|
1332
|
-
this.streamSubscriptions.delete(state.
|
|
1333
|
-
|
|
1380
|
+
this.streamSubscriptions.delete(state.subscriptionKey);
|
|
1381
|
+
const payload = {
|
|
1334
1382
|
projectId: state.options.projectId,
|
|
1335
1383
|
collectionName: state.options.collectionName
|
|
1336
|
-
}
|
|
1384
|
+
};
|
|
1385
|
+
if (state.options.where && Object.keys(state.options.where).length > 0) {
|
|
1386
|
+
payload["where"] = state.options.where;
|
|
1387
|
+
}
|
|
1388
|
+
this.socket?.emit("unsubscribe-events", payload);
|
|
1337
1389
|
}
|
|
1338
1390
|
}
|
|
1339
1391
|
dispatchStreamEvent(payload) {
|
|
1340
1392
|
if (!payload.eventId) return Promise.resolve();
|
|
1341
1393
|
const streamKey = `stream:${payload.projectId}:${payload.collectionName}`;
|
|
1342
|
-
const
|
|
1343
|
-
if (
|
|
1394
|
+
const sets = this.streamSetsForStreamKey(streamKey);
|
|
1395
|
+
if (sets.length === 0) return Promise.resolve();
|
|
1344
1396
|
const entryId = payload.eventId;
|
|
1345
|
-
for (const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
state.lastDeliveredId
|
|
1352
|
-
} catch (err) {
|
|
1353
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1397
|
+
for (const set of sets) {
|
|
1398
|
+
for (const state of set) {
|
|
1399
|
+
if (!this.eventMatchesStreamWhere(payload, state.options.where)) continue;
|
|
1400
|
+
state.dispatchQueue = state.dispatchQueue.then(async () => {
|
|
1401
|
+
const liveSet = this.streamSubscriptions.get(state.subscriptionKey);
|
|
1402
|
+
if (!liveSet?.has(state)) return;
|
|
1403
|
+
if (state.lastDeliveredId === entryId) return;
|
|
1354
1404
|
try {
|
|
1355
|
-
state.options.
|
|
1356
|
-
|
|
1405
|
+
await state.options.onEvent({ eventId: entryId, event: payload });
|
|
1406
|
+
state.lastDeliveredId = entryId;
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1409
|
+
try {
|
|
1410
|
+
state.options.onError?.(error);
|
|
1411
|
+
} catch {
|
|
1412
|
+
}
|
|
1357
1413
|
}
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1360
1416
|
}
|
|
1361
1417
|
return Promise.resolve();
|
|
1362
1418
|
}
|
|
@@ -1382,15 +1438,19 @@ var RealtimeClient = class {
|
|
|
1382
1438
|
state.options.onError?.(err);
|
|
1383
1439
|
}
|
|
1384
1440
|
if (PERMANENT_STREAM_ACK_ERRORS.has(ack.error)) {
|
|
1385
|
-
this.streamSubscriptions.delete(primary.
|
|
1441
|
+
this.streamSubscriptions.delete(primary.subscriptionKey);
|
|
1386
1442
|
}
|
|
1387
1443
|
return;
|
|
1388
1444
|
}
|
|
1389
|
-
if (!this.streamSubscriptions.has(primary.
|
|
1390
|
-
|
|
1445
|
+
if (!this.streamSubscriptions.has(primary.subscriptionKey)) {
|
|
1446
|
+
const payload = {
|
|
1391
1447
|
projectId: primary.options.projectId,
|
|
1392
1448
|
collectionName: primary.options.collectionName
|
|
1393
|
-
}
|
|
1449
|
+
};
|
|
1450
|
+
if (primary.options.where && Object.keys(primary.options.where).length > 0) {
|
|
1451
|
+
payload["where"] = primary.options.where;
|
|
1452
|
+
}
|
|
1453
|
+
this.socket?.emit("unsubscribe-events", payload);
|
|
1394
1454
|
}
|
|
1395
1455
|
} catch (err) {
|
|
1396
1456
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1422,8 +1482,8 @@ var RealtimeClient = class {
|
|
|
1422
1482
|
* streamKey via the dedup path. Fires `onError` on all siblings, then
|
|
1423
1483
|
* removes the entire streamKey entry from `streamSubscriptions`.
|
|
1424
1484
|
*/
|
|
1425
|
-
evictStreamKey(
|
|
1426
|
-
const set = this.streamSubscriptions.get(
|
|
1485
|
+
evictStreamKey(subscriptionKey, err, primary) {
|
|
1486
|
+
const set = this.streamSubscriptions.get(subscriptionKey);
|
|
1427
1487
|
if (!set) return;
|
|
1428
1488
|
for (const sibling of [...set]) {
|
|
1429
1489
|
if (sibling !== primary) {
|
|
@@ -1431,7 +1491,7 @@ var RealtimeClient = class {
|
|
|
1431
1491
|
}
|
|
1432
1492
|
set.delete(sibling);
|
|
1433
1493
|
}
|
|
1434
|
-
this.streamSubscriptions.delete(
|
|
1494
|
+
this.streamSubscriptions.delete(subscriptionKey);
|
|
1435
1495
|
}
|
|
1436
1496
|
};
|
|
1437
1497
|
|
|
@@ -1870,9 +1930,11 @@ var AuthModule = class {
|
|
|
1870
1930
|
}
|
|
1871
1931
|
async storeTokensFromRegister(response) {
|
|
1872
1932
|
if (!response.access_token) return;
|
|
1933
|
+
const expiresAt = response.expires_in ? Math.floor(Date.now() / 1e3) + response.expires_in : void 0;
|
|
1873
1934
|
await this.tokenManager.setTokens({
|
|
1874
1935
|
accessToken: response.access_token,
|
|
1875
|
-
refreshToken: response.refresh_token
|
|
1936
|
+
refreshToken: response.refresh_token,
|
|
1937
|
+
expiresAt
|
|
1876
1938
|
});
|
|
1877
1939
|
this.emitState("authenticated");
|
|
1878
1940
|
}
|