@spacelr/sdk 0.9.2 → 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 +83 -19
- package/dist/index.d.ts +83 -19
- package/dist/index.js +183 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -614,6 +614,57 @@ var PERMANENT_STREAM_ACK_ERRORS = /* @__PURE__ */ new Set([
|
|
|
614
614
|
"Not a member of this project",
|
|
615
615
|
"Subscribe denied"
|
|
616
616
|
]);
|
|
617
|
+
function isWhereScalar(value) {
|
|
618
|
+
const t = typeof value;
|
|
619
|
+
return t === "string" || t === "number" || t === "boolean";
|
|
620
|
+
}
|
|
621
|
+
function isWhereOperator(value) {
|
|
622
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
const keys = Object.keys(value);
|
|
626
|
+
if (keys.length !== 1) return false;
|
|
627
|
+
const op = keys[0];
|
|
628
|
+
if (op !== "in" && op !== "array-contains-any") return false;
|
|
629
|
+
const candidates = value[op];
|
|
630
|
+
return Array.isArray(candidates) && candidates.every(isWhereScalar);
|
|
631
|
+
}
|
|
632
|
+
var WHERE_PATH_ABSENT = /* @__PURE__ */ Symbol("where-path-absent");
|
|
633
|
+
var WHERE_PATH_UNRESOLVABLE = /* @__PURE__ */ Symbol("where-path-unresolvable");
|
|
634
|
+
var RESERVED_PATH_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
635
|
+
var MAX_WHERE_PATH_SEGMENTS = 16;
|
|
636
|
+
function resolveWherePath(document2, key) {
|
|
637
|
+
const segments = key.split(".");
|
|
638
|
+
if (segments.length > MAX_WHERE_PATH_SEGMENTS) return WHERE_PATH_ABSENT;
|
|
639
|
+
let cur = document2;
|
|
640
|
+
for (const segment of segments) {
|
|
641
|
+
if (RESERVED_PATH_SEGMENTS.has(segment)) return WHERE_PATH_ABSENT;
|
|
642
|
+
if (cur === null || cur === void 0) return WHERE_PATH_ABSENT;
|
|
643
|
+
if (Array.isArray(cur)) return WHERE_PATH_UNRESOLVABLE;
|
|
644
|
+
if (typeof cur !== "object") return WHERE_PATH_ABSENT;
|
|
645
|
+
if (!Object.prototype.hasOwnProperty.call(cur, segment)) {
|
|
646
|
+
return WHERE_PATH_ABSENT;
|
|
647
|
+
}
|
|
648
|
+
cur = cur[segment];
|
|
649
|
+
}
|
|
650
|
+
return cur === void 0 ? WHERE_PATH_ABSENT : cur;
|
|
651
|
+
}
|
|
652
|
+
function whereValueMatches(actual, expected) {
|
|
653
|
+
if (actual === WHERE_PATH_ABSENT || actual === WHERE_PATH_UNRESOLVABLE) {
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
if (isWhereOperator(expected)) {
|
|
657
|
+
if ("in" in expected) {
|
|
658
|
+
if (Array.isArray(actual)) return false;
|
|
659
|
+
return expected.in.includes(actual);
|
|
660
|
+
}
|
|
661
|
+
if (!Array.isArray(actual)) return false;
|
|
662
|
+
const candidates = expected["array-contains-any"];
|
|
663
|
+
return actual.some((el) => candidates.includes(el));
|
|
664
|
+
}
|
|
665
|
+
if (Array.isArray(actual)) return actual.includes(expected);
|
|
666
|
+
return actual === expected;
|
|
667
|
+
}
|
|
617
668
|
var RealtimeClient = class {
|
|
618
669
|
constructor(config) {
|
|
619
670
|
this.socket = null;
|
|
@@ -621,6 +672,21 @@ var RealtimeClient = class {
|
|
|
621
672
|
this.connecting = null;
|
|
622
673
|
// Store original where objects per room for reconnect resubscription
|
|
623
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();
|
|
624
690
|
this.unsubscribeFromTokenRefreshed = null;
|
|
625
691
|
// Wake-up listeners (browser only) — recover from long OS suspends where
|
|
626
692
|
// socket.io's internal reconnect loop may have already given up.
|
|
@@ -649,7 +715,17 @@ var RealtimeClient = class {
|
|
|
649
715
|
}
|
|
650
716
|
const callbacks = this.subscriptions.get(room);
|
|
651
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
|
+
}
|
|
652
726
|
if (callbacks?.size === 1) {
|
|
727
|
+
const generation = ++this.subEpoch;
|
|
728
|
+
this.roomGeneration.set(room, generation);
|
|
653
729
|
if (where && Object.keys(where).length > 0) {
|
|
654
730
|
this.roomWhereMap.set(room, where);
|
|
655
731
|
}
|
|
@@ -661,8 +737,8 @@ var RealtimeClient = class {
|
|
|
661
737
|
"subscribe",
|
|
662
738
|
payload,
|
|
663
739
|
(response) => {
|
|
664
|
-
if (response.error &&
|
|
665
|
-
|
|
740
|
+
if (response.error && this.roomGeneration.get(room) === generation) {
|
|
741
|
+
this.notifyRoomError(room, this.toSubscribeError(response));
|
|
666
742
|
}
|
|
667
743
|
}
|
|
668
744
|
);
|
|
@@ -671,9 +747,12 @@ var RealtimeClient = class {
|
|
|
671
747
|
const callbacks2 = this.subscriptions.get(room);
|
|
672
748
|
if (callbacks2) {
|
|
673
749
|
callbacks2.delete(callback);
|
|
750
|
+
this.roomErrorMap.get(room)?.delete(callback);
|
|
674
751
|
if (callbacks2.size === 0) {
|
|
675
752
|
this.subscriptions.delete(room);
|
|
676
753
|
this.roomWhereMap.delete(room);
|
|
754
|
+
this.roomErrorMap.delete(room);
|
|
755
|
+
this.roomGeneration.delete(room);
|
|
677
756
|
const payload = { projectId, collectionName };
|
|
678
757
|
if (where && Object.keys(where).length > 0) {
|
|
679
758
|
payload["where"] = where;
|
|
@@ -826,6 +905,9 @@ var RealtimeClient = class {
|
|
|
826
905
|
}
|
|
827
906
|
this.subscriptions.clear();
|
|
828
907
|
this.roomWhereMap.clear();
|
|
908
|
+
this.roomErrorMap.clear();
|
|
909
|
+
this.roomGeneration.clear();
|
|
910
|
+
this.resubscribingRooms.clear();
|
|
829
911
|
this.streamSubscriptions.clear();
|
|
830
912
|
this.connecting = null;
|
|
831
913
|
}
|
|
@@ -975,6 +1057,18 @@ var RealtimeClient = class {
|
|
|
975
1057
|
}
|
|
976
1058
|
}
|
|
977
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
|
+
);
|
|
978
1072
|
this.socket.on("event", (payload) => {
|
|
979
1073
|
this.dispatchStreamEvent(payload).catch(() => void 0);
|
|
980
1074
|
});
|
|
@@ -1002,12 +1096,7 @@ var RealtimeClient = class {
|
|
|
1002
1096
|
if (!where) return false;
|
|
1003
1097
|
if (!event.document) return false;
|
|
1004
1098
|
for (const [key, value] of Object.entries(where)) {
|
|
1005
|
-
|
|
1006
|
-
if (Array.isArray(docValue)) {
|
|
1007
|
-
if (!docValue.includes(value)) {
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
|
-
} else if (docValue !== value) {
|
|
1099
|
+
if (!whereValueMatches(resolveWherePath(event.document, key), value)) {
|
|
1011
1100
|
return false;
|
|
1012
1101
|
}
|
|
1013
1102
|
}
|
|
@@ -1081,31 +1170,88 @@ var RealtimeClient = class {
|
|
|
1081
1170
|
this.onOnline = null;
|
|
1082
1171
|
}
|
|
1083
1172
|
resubscribeAll() {
|
|
1173
|
+
this.resubscribingRooms.clear();
|
|
1084
1174
|
for (const [room] of this.subscriptions) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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 {
|
|
1106
1204
|
}
|
|
1107
1205
|
}
|
|
1108
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
|
+
}
|
|
1109
1255
|
emitSubscribeEvents(state) {
|
|
1110
1256
|
return new Promise((resolve) => {
|
|
1111
1257
|
if (!this.socket) {
|
|
@@ -2968,10 +3114,14 @@ var FunctionsModule = class {
|
|
|
2968
3114
|
* `config.apiUrl` (which already carries the `/api/v1` prefix).
|
|
2969
3115
|
*
|
|
2970
3116
|
* Auth defaults, based on `invokeMode` semantics:
|
|
2971
|
-
* - webhook:
|
|
2972
|
-
* - authenticated:
|
|
2973
|
-
*
|
|
2974
|
-
* -
|
|
3117
|
+
* - webhook: pass `secret` → Authorization is NOT attached
|
|
3118
|
+
* - authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
3119
|
+
* caller must be a member of the target project
|
|
3120
|
+
* - public: pass nothing → Authorization is attached if logged in, else omitted
|
|
3121
|
+
* - hybrid: pass both `secret` and `authenticated: true`; JWT path
|
|
3122
|
+
* requires project membership like `authenticated`
|
|
3123
|
+
* - platform-authenticated: pass nothing → Authorization IS attached (from token manager);
|
|
3124
|
+
* any signed-in user is accepted, still just attaches the bearer token
|
|
2975
3125
|
*
|
|
2976
3126
|
* To force a specific behaviour, set `authenticated` explicitly — it wins
|
|
2977
3127
|
* over the `secret`-based default.
|