@spacelr/sdk 0.1.9 → 0.2.0
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 +272 -18
- package/dist/index.d.ts +272 -18
- package/dist/index.js +403 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +400 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -33,7 +33,9 @@ __export(index_exports, {
|
|
|
33
33
|
SpacelrTimeoutError: () => SpacelrTimeoutError,
|
|
34
34
|
SpacelrTwoFactorRequiredError: () => SpacelrTwoFactorRequiredError,
|
|
35
35
|
createClient: () => createClient,
|
|
36
|
-
generatePKCEChallenge: () => generatePKCEChallenge
|
|
36
|
+
generatePKCEChallenge: () => generatePKCEChallenge,
|
|
37
|
+
localStorageCursorStorage: () => localStorageCursorStorage,
|
|
38
|
+
memoryCursorStorage: () => memoryCursorStorage
|
|
37
39
|
});
|
|
38
40
|
module.exports = __toCommonJS(index_exports);
|
|
39
41
|
|
|
@@ -575,6 +577,13 @@ async function generatePKCEChallenge() {
|
|
|
575
577
|
// libs/sdk/src/core/realtime.ts
|
|
576
578
|
var import_socket = require("socket.io-client");
|
|
577
579
|
var REBUILD_RETRY_DELAY_MS = 5e3;
|
|
580
|
+
var PERMANENT_STREAM_ACK_ERRORS = /* @__PURE__ */ new Set([
|
|
581
|
+
"not-stream-collection",
|
|
582
|
+
"Collection not found",
|
|
583
|
+
"Invalid sinceId format",
|
|
584
|
+
"Not a member of this project",
|
|
585
|
+
"Subscribe denied"
|
|
586
|
+
]);
|
|
578
587
|
var RealtimeClient = class {
|
|
579
588
|
constructor(config) {
|
|
580
589
|
this.socket = null;
|
|
@@ -595,6 +604,7 @@ var RealtimeClient = class {
|
|
|
595
604
|
this.rebuildRetryTimer = null;
|
|
596
605
|
this.connectionState = "disconnected";
|
|
597
606
|
this.connectionStateListeners = /* @__PURE__ */ new Set();
|
|
607
|
+
this.streamSubscriptions = /* @__PURE__ */ new Map();
|
|
598
608
|
this.config = config;
|
|
599
609
|
}
|
|
600
610
|
async subscribe(projectId, collectionName, callback, onError, where) {
|
|
@@ -643,6 +653,53 @@ var RealtimeClient = class {
|
|
|
643
653
|
}
|
|
644
654
|
};
|
|
645
655
|
}
|
|
656
|
+
/**
|
|
657
|
+
* Subscribe to a stream-mode collection using Redis Streams replay +
|
|
658
|
+
* cursor-based delivery. Parallel to `subscribe()` (which targets
|
|
659
|
+
* pubsub-mode collections). The server validates that the collection is
|
|
660
|
+
* configured in stream mode and rejects the handshake otherwise.
|
|
661
|
+
*
|
|
662
|
+
* The per-subscription cursor (`lastDeliveredId`) advances after each
|
|
663
|
+
* `onEvent` promise resolves, so reconnects resume from the last delivered
|
|
664
|
+
* event rather than the originally-subscribed `sinceId`.
|
|
665
|
+
*/
|
|
666
|
+
async subscribeWithCursor(options) {
|
|
667
|
+
this.ensureWakeListeners();
|
|
668
|
+
if (this.connectionState === "disconnected") {
|
|
669
|
+
this.setConnectionState("reconnecting");
|
|
670
|
+
}
|
|
671
|
+
await this.ensureConnected();
|
|
672
|
+
const streamKey = `stream:${options.projectId}:${options.collectionName}`;
|
|
673
|
+
const state = {
|
|
674
|
+
options,
|
|
675
|
+
lastDeliveredId: options.sinceId ?? null,
|
|
676
|
+
streamKey,
|
|
677
|
+
dispatchQueue: Promise.resolve()
|
|
678
|
+
};
|
|
679
|
+
const existing = this.streamSubscriptions.get(streamKey);
|
|
680
|
+
if (existing && existing.size > 0) {
|
|
681
|
+
existing.add(state);
|
|
682
|
+
return () => this.unsubscribeStream(state);
|
|
683
|
+
}
|
|
684
|
+
const set = /* @__PURE__ */ new Set();
|
|
685
|
+
set.add(state);
|
|
686
|
+
this.streamSubscriptions.set(streamKey, set);
|
|
687
|
+
try {
|
|
688
|
+
const ack = await this.emitSubscribeEvents(state);
|
|
689
|
+
if (ack.error) {
|
|
690
|
+
const message = ack.message ?? ack.error;
|
|
691
|
+
const err = new Error(message);
|
|
692
|
+
this.evictStreamKey(streamKey, err, state);
|
|
693
|
+
options.onError?.(err);
|
|
694
|
+
throw err;
|
|
695
|
+
}
|
|
696
|
+
return () => this.unsubscribeStream(state);
|
|
697
|
+
} catch (err) {
|
|
698
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
699
|
+
this.evictStreamKey(streamKey, error, state);
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
646
703
|
/**
|
|
647
704
|
* Tear down the realtime client permanently. After this call the instance
|
|
648
705
|
* is disposed — subsequent `subscribe()` calls will not re-establish a
|
|
@@ -667,6 +724,7 @@ var RealtimeClient = class {
|
|
|
667
724
|
}
|
|
668
725
|
this.subscriptions.clear();
|
|
669
726
|
this.roomWhereMap.clear();
|
|
727
|
+
this.streamSubscriptions.clear();
|
|
670
728
|
this.connecting = null;
|
|
671
729
|
}
|
|
672
730
|
getConnectionState() {
|
|
@@ -748,6 +806,7 @@ var RealtimeClient = class {
|
|
|
748
806
|
resolve();
|
|
749
807
|
} else {
|
|
750
808
|
this.resubscribeAll();
|
|
809
|
+
void this.resubscribeAllStreams();
|
|
751
810
|
}
|
|
752
811
|
});
|
|
753
812
|
this.socket.on("connect_error", (err) => {
|
|
@@ -780,6 +839,17 @@ var RealtimeClient = class {
|
|
|
780
839
|
}
|
|
781
840
|
}
|
|
782
841
|
});
|
|
842
|
+
this.socket.on("event", (payload) => {
|
|
843
|
+
this.dispatchStreamEvent(payload).catch(() => void 0);
|
|
844
|
+
});
|
|
845
|
+
this.socket.on("event-gap", (info) => {
|
|
846
|
+
const streamKey = `stream:${info.projectId}:${info.collectionName}`;
|
|
847
|
+
const set = this.streamSubscriptions.get(streamKey);
|
|
848
|
+
if (!set) return;
|
|
849
|
+
for (const state of set) {
|
|
850
|
+
state.options.onGap?.(info);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
783
853
|
this.socket.on("disconnect", () => {
|
|
784
854
|
if (!this.disposed) {
|
|
785
855
|
this.setConnectionState("reconnecting");
|
|
@@ -837,6 +907,9 @@ var RealtimeClient = class {
|
|
|
837
907
|
if (this.subscriptions.size > 0) {
|
|
838
908
|
this.resubscribeAll();
|
|
839
909
|
}
|
|
910
|
+
if (this.streamSubscriptions.size > 0) {
|
|
911
|
+
void this.resubscribeAllStreams();
|
|
912
|
+
}
|
|
840
913
|
}
|
|
841
914
|
scheduleRebuildRetry() {
|
|
842
915
|
if (this.disposed || this.rebuildRetryTimer) return;
|
|
@@ -896,8 +969,186 @@ var RealtimeClient = class {
|
|
|
896
969
|
}
|
|
897
970
|
}
|
|
898
971
|
}
|
|
972
|
+
emitSubscribeEvents(state) {
|
|
973
|
+
return new Promise((resolve) => {
|
|
974
|
+
if (!this.socket) {
|
|
975
|
+
resolve({ error: "disconnected" });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const socket = this.socket;
|
|
979
|
+
const onDisconnect = () => {
|
|
980
|
+
socket.off("disconnect", onDisconnect);
|
|
981
|
+
resolve({ error: "disconnected" });
|
|
982
|
+
};
|
|
983
|
+
socket.once("disconnect", onDisconnect);
|
|
984
|
+
socket.emit(
|
|
985
|
+
"subscribe-events",
|
|
986
|
+
this.buildStreamPayload(state),
|
|
987
|
+
(ack) => {
|
|
988
|
+
socket.off("disconnect", onDisconnect);
|
|
989
|
+
resolve(ack);
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
buildStreamPayload(state) {
|
|
995
|
+
const { projectId, collectionName, where } = state.options;
|
|
996
|
+
const payload = { projectId, collectionName };
|
|
997
|
+
if (state.lastDeliveredId) payload["sinceId"] = state.lastDeliveredId;
|
|
998
|
+
if (where && Object.keys(where).length > 0) payload["where"] = where;
|
|
999
|
+
return payload;
|
|
1000
|
+
}
|
|
1001
|
+
unsubscribeStream(state) {
|
|
1002
|
+
const set = this.streamSubscriptions.get(state.streamKey);
|
|
1003
|
+
if (!set) return;
|
|
1004
|
+
set.delete(state);
|
|
1005
|
+
if (set.size === 0) {
|
|
1006
|
+
this.streamSubscriptions.delete(state.streamKey);
|
|
1007
|
+
this.socket?.emit("unsubscribe-events", {
|
|
1008
|
+
projectId: state.options.projectId,
|
|
1009
|
+
collectionName: state.options.collectionName
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
dispatchStreamEvent(payload) {
|
|
1014
|
+
if (!payload.eventId) return Promise.resolve();
|
|
1015
|
+
const streamKey = `stream:${payload.projectId}:${payload.collectionName}`;
|
|
1016
|
+
const set = this.streamSubscriptions.get(streamKey);
|
|
1017
|
+
if (!set) return Promise.resolve();
|
|
1018
|
+
const entryId = payload.eventId;
|
|
1019
|
+
for (const state of set) {
|
|
1020
|
+
state.dispatchQueue = state.dispatchQueue.then(async () => {
|
|
1021
|
+
const liveSet = this.streamSubscriptions.get(state.streamKey);
|
|
1022
|
+
if (!liveSet?.has(state)) return;
|
|
1023
|
+
try {
|
|
1024
|
+
await state.options.onEvent({ eventId: entryId, event: payload });
|
|
1025
|
+
state.lastDeliveredId = entryId;
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1028
|
+
try {
|
|
1029
|
+
state.options.onError?.(error);
|
|
1030
|
+
} catch {
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
return Promise.resolve();
|
|
1036
|
+
}
|
|
1037
|
+
async resubscribeAllStreams() {
|
|
1038
|
+
const work = [];
|
|
1039
|
+
for (const set of this.streamSubscriptions.values()) {
|
|
1040
|
+
const states = Array.from(set);
|
|
1041
|
+
if (states.length === 0) continue;
|
|
1042
|
+
const primary = this.pickEarliestCursorState(states);
|
|
1043
|
+
work.push(this.resubscribeOne(primary, set));
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
await Promise.all(work);
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async resubscribeOne(primary, set) {
|
|
1051
|
+
try {
|
|
1052
|
+
const ack = await this.emitSubscribeEvents(primary);
|
|
1053
|
+
if (ack.error) {
|
|
1054
|
+
const err = new Error(ack.message ?? ack.error);
|
|
1055
|
+
for (const state of [...set]) {
|
|
1056
|
+
state.options.onError?.(err);
|
|
1057
|
+
}
|
|
1058
|
+
if (PERMANENT_STREAM_ACK_ERRORS.has(ack.error)) {
|
|
1059
|
+
this.streamSubscriptions.delete(primary.streamKey);
|
|
1060
|
+
}
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (!this.streamSubscriptions.has(primary.streamKey)) {
|
|
1064
|
+
this.socket?.emit("unsubscribe-events", {
|
|
1065
|
+
projectId: primary.options.projectId,
|
|
1066
|
+
collectionName: primary.options.collectionName
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1071
|
+
for (const state of [...set]) {
|
|
1072
|
+
state.options.onError?.(error);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
pickEarliestCursorState(states) {
|
|
1077
|
+
const parse = (id) => {
|
|
1078
|
+
if (!id) return [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
|
|
1079
|
+
const [ms, seq = "0"] = id.split("-");
|
|
1080
|
+
return [Number(ms), Number(seq)];
|
|
1081
|
+
};
|
|
1082
|
+
let earliest = states[0];
|
|
1083
|
+
let [eMs, eSeq] = parse(earliest.lastDeliveredId);
|
|
1084
|
+
for (let i = 1; i < states.length; i++) {
|
|
1085
|
+
const [ms, seq] = parse(states[i].lastDeliveredId);
|
|
1086
|
+
if (ms < eMs || ms === eMs && seq < eSeq) {
|
|
1087
|
+
earliest = states[i];
|
|
1088
|
+
eMs = ms;
|
|
1089
|
+
eSeq = seq;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return earliest;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Evict the primary subscriber AND every sibling that joined the same
|
|
1096
|
+
* streamKey via the dedup path. Fires `onError` on all siblings, then
|
|
1097
|
+
* removes the entire streamKey entry from `streamSubscriptions`.
|
|
1098
|
+
*/
|
|
1099
|
+
evictStreamKey(streamKey, err, primary) {
|
|
1100
|
+
const set = this.streamSubscriptions.get(streamKey);
|
|
1101
|
+
if (!set) return;
|
|
1102
|
+
for (const sibling of [...set]) {
|
|
1103
|
+
if (sibling !== primary) {
|
|
1104
|
+
sibling.options.onError?.(err);
|
|
1105
|
+
}
|
|
1106
|
+
set.delete(sibling);
|
|
1107
|
+
}
|
|
1108
|
+
this.streamSubscriptions.delete(streamKey);
|
|
1109
|
+
}
|
|
899
1110
|
};
|
|
900
1111
|
|
|
1112
|
+
// libs/sdk/src/core/cursor-storage.ts
|
|
1113
|
+
function memoryCursorStorage() {
|
|
1114
|
+
const store = /* @__PURE__ */ new Map();
|
|
1115
|
+
return {
|
|
1116
|
+
load(key) {
|
|
1117
|
+
return store.get(key) ?? null;
|
|
1118
|
+
},
|
|
1119
|
+
save(key, cursor) {
|
|
1120
|
+
store.set(key, cursor);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function localStorageCursorStorage(prefix = "spacelr:cursor:") {
|
|
1125
|
+
const storage = (() => {
|
|
1126
|
+
try {
|
|
1127
|
+
const candidate = globalThis.localStorage;
|
|
1128
|
+
return typeof candidate === "undefined" ? null : candidate;
|
|
1129
|
+
} catch {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
})();
|
|
1133
|
+
return {
|
|
1134
|
+
load(key) {
|
|
1135
|
+
if (!storage) return null;
|
|
1136
|
+
try {
|
|
1137
|
+
return storage.getItem(prefix + key);
|
|
1138
|
+
} catch {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
save(key, cursor) {
|
|
1143
|
+
if (!storage) return;
|
|
1144
|
+
try {
|
|
1145
|
+
storage.setItem(prefix + key, cursor);
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
901
1152
|
// libs/sdk/src/modules/auth.module.ts
|
|
902
1153
|
var AuthModule = class {
|
|
903
1154
|
constructor(http, tokenManager, config) {
|
|
@@ -1442,6 +1693,46 @@ var QueryBuilder = class {
|
|
|
1442
1693
|
this._offset = offset;
|
|
1443
1694
|
return this;
|
|
1444
1695
|
}
|
|
1696
|
+
/**
|
|
1697
|
+
* **Constraint:** the cursor value must be a 24-hex ObjectId string.
|
|
1698
|
+
* Collections using custom non-ObjectId `_id` strings will not work
|
|
1699
|
+
* correctly with cursor pagination — the server's `$lt`/`$gt` comparison
|
|
1700
|
+
* uses BSON type ordering, mixing string `_id`s with ObjectId comparison
|
|
1701
|
+
* produces undefined behaviour. Documented limitation; future work may
|
|
1702
|
+
* add opaque cursor tokens that abstract over `_id` types.
|
|
1703
|
+
*
|
|
1704
|
+
* Switch to cursor-pagination mode: return documents with `_id < id`
|
|
1705
|
+
* (in the sort-defined order). The cursor refers to the cursor *value*,
|
|
1706
|
+
* not visual UI direction. Requires `.sort()` to be `{ _id: 1 }` or
|
|
1707
|
+
* `{ _id: -1 }` (or omitted — server defaults to `{ _id: 1 }`).
|
|
1708
|
+
*
|
|
1709
|
+
* Narrows the builder's mode parameter so subsequent `.execute()` returns
|
|
1710
|
+
* a `CursorResult<T>` instead of `OffsetResult<T>`.
|
|
1711
|
+
*
|
|
1712
|
+
* **For paginating further:** the `nextCursor` field returned by
|
|
1713
|
+
* `execute()` is the `_id` of the last document on the page. To load the
|
|
1714
|
+
* next older page, pass it again to `.before()`. (Do NOT pass it to
|
|
1715
|
+
* `.after()` — that would request docs newer than this page.)
|
|
1716
|
+
*
|
|
1717
|
+
* **Cannot be combined with `.offset()`.** Type system allows the chain
|
|
1718
|
+
* for ergonomics, but the server rejects it with HTTP 400.
|
|
1719
|
+
*/
|
|
1720
|
+
before(id) {
|
|
1721
|
+
this._before = id;
|
|
1722
|
+
return this;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Switch to cursor-pagination mode: return documents with `_id > id`.
|
|
1726
|
+
* See `.before()` for full semantics — including the ObjectId-only cursor
|
|
1727
|
+
* constraint and the `.offset()` incompatibility (server-enforced 400).
|
|
1728
|
+
*
|
|
1729
|
+
* **For paginating further:** pass the returned `nextCursor` to
|
|
1730
|
+
* `.after()` again to load the next newer page.
|
|
1731
|
+
*/
|
|
1732
|
+
after(id) {
|
|
1733
|
+
this._after = id;
|
|
1734
|
+
return this;
|
|
1735
|
+
}
|
|
1445
1736
|
select(fields) {
|
|
1446
1737
|
this._fields = fields;
|
|
1447
1738
|
return this;
|
|
@@ -1456,6 +1747,8 @@ var QueryBuilder = class {
|
|
|
1456
1747
|
if (this._sort) query["sort"] = JSON.stringify(this._sort);
|
|
1457
1748
|
if (this._limit !== void 0) query["limit"] = this._limit;
|
|
1458
1749
|
if (this._offset !== void 0) query["offset"] = this._offset;
|
|
1750
|
+
if (this._before !== void 0) query["before"] = this._before;
|
|
1751
|
+
if (this._after !== void 0) query["after"] = this._after;
|
|
1459
1752
|
if (this._fields) query["fields"] = this._fields.join(",");
|
|
1460
1753
|
if (this._populate.length) {
|
|
1461
1754
|
query["populate"] = this._populate.map(
|
|
@@ -1598,6 +1891,97 @@ var CollectionRef = class {
|
|
|
1598
1891
|
}
|
|
1599
1892
|
};
|
|
1600
1893
|
}
|
|
1894
|
+
subscribeEvents(handlers) {
|
|
1895
|
+
if (!this.realtime) {
|
|
1896
|
+
throw new Error("Realtime not available: no RealtimeClient configured");
|
|
1897
|
+
}
|
|
1898
|
+
let lastCursor;
|
|
1899
|
+
let unsub = null;
|
|
1900
|
+
let unsubscribed = false;
|
|
1901
|
+
const cursorStorage = handlers.cursorStorage;
|
|
1902
|
+
const cursorKey = handlers.cursorKey ?? `${this.projectId}:${this.collectionName}`;
|
|
1903
|
+
if (cursorStorage && handlers.where && Object.keys(handlers.where).length > 0 && handlers.cursorKey === void 0) {
|
|
1904
|
+
console.warn(
|
|
1905
|
+
`[spacelr] subscribeEvents on ${this.projectId}:${this.collectionName} uses a 'where' filter with cursorStorage but no explicit cursorKey \u2014 filtered subscriptions sharing the default key will silently skip events across reconnects. Pass a unique cursorKey (e.g. include a hash of the filter).`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
const realtime = this.realtime;
|
|
1909
|
+
let lastSavePromise = Promise.resolve();
|
|
1910
|
+
const onEvent = async ({
|
|
1911
|
+
eventId,
|
|
1912
|
+
event
|
|
1913
|
+
}) => {
|
|
1914
|
+
try {
|
|
1915
|
+
if (event.type === "insert" && handlers.onInsert && event.document) {
|
|
1916
|
+
await handlers.onInsert({
|
|
1917
|
+
...event.document,
|
|
1918
|
+
_eventId: eventId
|
|
1919
|
+
});
|
|
1920
|
+
} else if (event.type === "update" && handlers.onUpdate && event.document) {
|
|
1921
|
+
await handlers.onUpdate({
|
|
1922
|
+
...event.document,
|
|
1923
|
+
_eventId: eventId
|
|
1924
|
+
});
|
|
1925
|
+
} else if (event.type === "delete" && handlers.onDelete) {
|
|
1926
|
+
await handlers.onDelete(event.documentId, eventId);
|
|
1927
|
+
}
|
|
1928
|
+
lastCursor = eventId;
|
|
1929
|
+
if (cursorStorage) {
|
|
1930
|
+
lastSavePromise = lastSavePromise.then(
|
|
1931
|
+
() => cursorStorage.save(cursorKey, eventId),
|
|
1932
|
+
() => cursorStorage.save(cursorKey, eventId)
|
|
1933
|
+
).catch((err) => {
|
|
1934
|
+
handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
} catch (err) {
|
|
1938
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
const callSubscribe = (sinceId) => realtime.subscribeWithCursor({
|
|
1942
|
+
projectId: this.projectId,
|
|
1943
|
+
collectionName: this.collectionName,
|
|
1944
|
+
sinceId,
|
|
1945
|
+
where: handlers.where,
|
|
1946
|
+
onEvent,
|
|
1947
|
+
onGap: handlers.onGap,
|
|
1948
|
+
onError: handlers.onError
|
|
1949
|
+
});
|
|
1950
|
+
let promise;
|
|
1951
|
+
if (handlers.sinceId !== void 0 || !cursorStorage) {
|
|
1952
|
+
promise = callSubscribe(handlers.sinceId);
|
|
1953
|
+
} else {
|
|
1954
|
+
promise = (async () => {
|
|
1955
|
+
let resumeFrom;
|
|
1956
|
+
try {
|
|
1957
|
+
const loaded = await Promise.resolve(cursorStorage.load(cursorKey));
|
|
1958
|
+
if (loaded) resumeFrom = loaded;
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1961
|
+
}
|
|
1962
|
+
return callSubscribe(resumeFrom);
|
|
1963
|
+
})();
|
|
1964
|
+
}
|
|
1965
|
+
promise.then((u) => {
|
|
1966
|
+
if (unsubscribed) {
|
|
1967
|
+
u();
|
|
1968
|
+
} else {
|
|
1969
|
+
unsub = u;
|
|
1970
|
+
}
|
|
1971
|
+
}).catch(() => void 0);
|
|
1972
|
+
return {
|
|
1973
|
+
unsubscribe() {
|
|
1974
|
+
unsubscribed = true;
|
|
1975
|
+
if (unsub) {
|
|
1976
|
+
unsub();
|
|
1977
|
+
unsub = null;
|
|
1978
|
+
}
|
|
1979
|
+
},
|
|
1980
|
+
getCursor() {
|
|
1981
|
+
return lastCursor;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1601
1985
|
};
|
|
1602
1986
|
var DatabaseModule = class {
|
|
1603
1987
|
constructor(http, projectId, realtime) {
|
|
@@ -1773,24 +2157,31 @@ var FunctionsModule = class {
|
|
|
1773
2157
|
}
|
|
1774
2158
|
/**
|
|
1775
2159
|
* Invoke a function.
|
|
1776
|
-
* Calls POST
|
|
2160
|
+
* Calls POST `/functions/:projectId/:functionId/invoke`, resolved against
|
|
2161
|
+
* `config.apiUrl` (which already carries the `/api/v1` prefix).
|
|
2162
|
+
*
|
|
2163
|
+
* Auth defaults, based on `invokeMode` semantics:
|
|
2164
|
+
* - webhook: pass `secret` → Authorization is NOT attached
|
|
2165
|
+
* - authenticated: pass nothing → Authorization IS attached (from token manager)
|
|
2166
|
+
* - public: pass nothing → Authorization is attached if logged in, else omitted
|
|
2167
|
+
* - hybrid: pass both `secret` and `authenticated: true`
|
|
1777
2168
|
*
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
* neither, though `authenticated: true` still populates `event.auth` inside
|
|
1781
|
-
* the function.
|
|
2169
|
+
* To force a specific behaviour, set `authenticated` explicitly — it wins
|
|
2170
|
+
* over the `secret`-based default.
|
|
1782
2171
|
*/
|
|
1783
2172
|
async invoke(projectId, functionId, options = {}) {
|
|
2173
|
+
const hasSecret = (options.secret?.length ?? 0) > 0;
|
|
1784
2174
|
const headers = {};
|
|
1785
|
-
if (
|
|
2175
|
+
if (hasSecret) {
|
|
1786
2176
|
headers["X-Webhook-Secret"] = options.secret;
|
|
1787
2177
|
}
|
|
2178
|
+
const authenticated = options.authenticated ?? !hasSecret;
|
|
1788
2179
|
return this.http.request({
|
|
1789
2180
|
method: "POST",
|
|
1790
|
-
path: `/
|
|
2181
|
+
path: `/functions/${encodeURIComponent(projectId)}/${encodeURIComponent(functionId)}/invoke`,
|
|
1791
2182
|
headers,
|
|
1792
2183
|
body: options.payload ?? {},
|
|
1793
|
-
authenticated
|
|
2184
|
+
authenticated
|
|
1794
2185
|
});
|
|
1795
2186
|
}
|
|
1796
2187
|
};
|
|
@@ -1857,6 +2248,8 @@ function createClient(config) {
|
|
|
1857
2248
|
SpacelrTimeoutError,
|
|
1858
2249
|
SpacelrTwoFactorRequiredError,
|
|
1859
2250
|
createClient,
|
|
1860
|
-
generatePKCEChallenge
|
|
2251
|
+
generatePKCEChallenge,
|
|
2252
|
+
localStorageCursorStorage,
|
|
2253
|
+
memoryCursorStorage
|
|
1861
2254
|
});
|
|
1862
2255
|
//# sourceMappingURL=index.js.map
|