@spacelr/sdk 0.2.2 → 0.3.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 +191 -2
- package/dist/index.d.ts +191 -2
- package/dist/index.js +459 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +458 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -50,6 +50,14 @@ var SpacelrTwoFactorRequiredError = class extends SpacelrError {
|
|
|
50
50
|
this.twoFactorToken = twoFactorToken;
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
+
var SpacelrSearchFilterRequiredError = class extends SpacelrError {
|
|
54
|
+
constructor(message, details) {
|
|
55
|
+
super(message, "SEARCH_FILTER_REQUIRED", 400, details);
|
|
56
|
+
this.name = "SpacelrSearchFilterRequiredError";
|
|
57
|
+
const fields = details?.["missingFields"];
|
|
58
|
+
this.missingFields = Array.isArray(fields) ? fields.filter((f) => typeof f === "string") : [];
|
|
59
|
+
}
|
|
60
|
+
};
|
|
53
61
|
var SpacelrEmailVerificationRequiredError = class extends SpacelrError {
|
|
54
62
|
constructor(emailSent, details) {
|
|
55
63
|
super(
|
|
@@ -78,7 +86,25 @@ var HttpClient = class {
|
|
|
78
86
|
const timeout = this.config.timeout ?? 3e4;
|
|
79
87
|
const includeCredentials = options.withCredentials ?? options.path.startsWith("/auth/");
|
|
80
88
|
const controller = new AbortController();
|
|
81
|
-
|
|
89
|
+
let timeoutFired = false;
|
|
90
|
+
const timeoutId = setTimeout(() => {
|
|
91
|
+
timeoutFired = true;
|
|
92
|
+
controller.abort();
|
|
93
|
+
}, timeout);
|
|
94
|
+
let externalAbort = false;
|
|
95
|
+
let onExternalAbort;
|
|
96
|
+
if (options.signal) {
|
|
97
|
+
if (options.signal.aborted) {
|
|
98
|
+
externalAbort = true;
|
|
99
|
+
controller.abort();
|
|
100
|
+
} else {
|
|
101
|
+
onExternalAbort = () => {
|
|
102
|
+
externalAbort = true;
|
|
103
|
+
controller.abort();
|
|
104
|
+
};
|
|
105
|
+
options.signal.addEventListener("abort", onExternalAbort);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
82
108
|
try {
|
|
83
109
|
const response = await fetch(url, {
|
|
84
110
|
method: options.method,
|
|
@@ -100,6 +126,7 @@ var HttpClient = class {
|
|
|
100
126
|
} catch (error) {
|
|
101
127
|
if (error instanceof SpacelrError) throw error;
|
|
102
128
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
129
|
+
if (externalAbort && !timeoutFired) throw error;
|
|
103
130
|
throw new SpacelrTimeoutError(timeout);
|
|
104
131
|
}
|
|
105
132
|
throw new SpacelrNetworkError(
|
|
@@ -107,6 +134,9 @@ var HttpClient = class {
|
|
|
107
134
|
);
|
|
108
135
|
} finally {
|
|
109
136
|
clearTimeout(timeoutId);
|
|
137
|
+
if (options.signal && onExternalAbort) {
|
|
138
|
+
options.signal.removeEventListener("abort", onExternalAbort);
|
|
139
|
+
}
|
|
110
140
|
}
|
|
111
141
|
}
|
|
112
142
|
async uploadForm(path, formData, onProgress) {
|
|
@@ -277,6 +307,12 @@ var HttpClient = class {
|
|
|
277
307
|
const message = apiBody.error?.message ?? `HTTP ${statusCode}`;
|
|
278
308
|
const code = apiBody.error?.code ?? `HTTP_${statusCode}`;
|
|
279
309
|
const details = apiBody.error?.details;
|
|
310
|
+
const flatBody = body;
|
|
311
|
+
if (statusCode === 400 && flatBody["errorCode"] === "SEARCH_FILTER_REQUIRED") {
|
|
312
|
+
const flatMessage = typeof flatBody["message"] === "string" ? flatBody["message"] : message;
|
|
313
|
+
const missingFields = Array.isArray(flatBody["missingFields"]) ? flatBody["missingFields"] : [];
|
|
314
|
+
throw new SpacelrSearchFilterRequiredError(flatMessage, { missingFields });
|
|
315
|
+
}
|
|
280
316
|
if (statusCode === 401 || statusCode === 403) {
|
|
281
317
|
throw new SpacelrAuthError(message, statusCode, details);
|
|
282
318
|
}
|
|
@@ -632,6 +668,67 @@ var RealtimeClient = class {
|
|
|
632
668
|
* event rather than the originally-subscribed `sinceId`.
|
|
633
669
|
*/
|
|
634
670
|
async subscribeWithCursor(options) {
|
|
671
|
+
const { unsubscribe } = await this._subscribeInternal(options);
|
|
672
|
+
return unsubscribe;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Metadata-aware variant of `subscribeWithCursor()` — see #99.
|
|
676
|
+
*
|
|
677
|
+
* Identical to `subscribeWithCursor` except it also returns the
|
|
678
|
+
* `realtimeMode` and `latestStreamId` from the server's
|
|
679
|
+
* subscribe-events ack so callers (notably `subscribeWithSnapshot`)
|
|
680
|
+
* can fail-fast on pubsub-mode collections and anchor the snapshot
|
|
681
|
+
* baseline atomically with the reader-attach.
|
|
682
|
+
*
|
|
683
|
+
* Defaults: if the server didn't send `realtimeMode` (older
|
|
684
|
+
* gateway), the SDK treats the collection as pubsub-mode so
|
|
685
|
+
* downstream fail-fasts still fire correctly. `latestStreamId`
|
|
686
|
+
* defaults to `null` when absent.
|
|
687
|
+
*
|
|
688
|
+
* Dedup-path note: when a sibling local subscription already holds
|
|
689
|
+
* the server-side reader slot for this streamKey, no new handshake
|
|
690
|
+
* is emitted — the SDK piggy-backs on the existing slot. In that
|
|
691
|
+
* case we synthesise `{ realtimeMode: 'stream', latestStreamId: null }`
|
|
692
|
+
* because the existing slot's prior ack is no longer available.
|
|
693
|
+
* Callers that need a fresh `latestStreamId` (e.g. snapshot anchor)
|
|
694
|
+
* MUST handle the `null` case.
|
|
695
|
+
*/
|
|
696
|
+
async subscribeWithCursorWithMeta(options) {
|
|
697
|
+
const { unsubscribe, ack } = await this._subscribeInternal(options);
|
|
698
|
+
return {
|
|
699
|
+
unsubscribe,
|
|
700
|
+
// Default to pubsub if the server didn't send the field — older
|
|
701
|
+
// gateways that haven't been updated yet are treated as pubsub-mode
|
|
702
|
+
// so the SDK helper's fail-fast still fires correctly.
|
|
703
|
+
realtimeMode: ack.realtimeMode ?? "pubsub",
|
|
704
|
+
latestStreamId: ack.latestStreamId ?? null
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Shared body for `subscribeWithCursor` and
|
|
709
|
+
* `subscribeWithCursorWithMeta`. Owns the handshake, dedup path,
|
|
710
|
+
* pre-ack registration, and error eviction. Returns the unsubscribe
|
|
711
|
+
* function plus the subscribe-events ack so metadata-aware callers
|
|
712
|
+
* can read `realtimeMode` / `latestStreamId`.
|
|
713
|
+
*
|
|
714
|
+
* Behaviour-preserving contract — DO NOT change without re-running
|
|
715
|
+
* the singleflight tests in `realtime.spec.ts`:
|
|
716
|
+
*
|
|
717
|
+
* 1. Dedup branch BEFORE registration: if a sibling subscriber for
|
|
718
|
+
* this streamKey is already registered, the new caller piggy-backs
|
|
719
|
+
* on its server-side slot. Re-emitting the handshake here would
|
|
720
|
+
* tear the prior subscriber's reader down.
|
|
721
|
+
* 2. Registration BEFORE handshake: `streamSubscriptions.set` must
|
|
722
|
+
* fire before `emitSubscribeEvents` so replay/event-gap frames
|
|
723
|
+
* delivered DURING the handshake fanout find this subscriber.
|
|
724
|
+
* 3. evictStreamKey on ack-error MUST sweep dedup-path siblings:
|
|
725
|
+
* they have no independent server-side state and would silently
|
|
726
|
+
* miss every event until the next full reconnect.
|
|
727
|
+
* 4. The catch block is a safety net for the re-thrown ack-error
|
|
728
|
+
* only — `emitSubscribeEvents` itself always resolves, so this
|
|
729
|
+
* catch should never fire on the success path.
|
|
730
|
+
*/
|
|
731
|
+
async _subscribeInternal(options) {
|
|
635
732
|
this.ensureWakeListeners();
|
|
636
733
|
if (this.connectionState === "disconnected") {
|
|
637
734
|
this.setConnectionState("reconnecting");
|
|
@@ -647,7 +744,15 @@ var RealtimeClient = class {
|
|
|
647
744
|
const existing = this.streamSubscriptions.get(streamKey);
|
|
648
745
|
if (existing && existing.size > 0) {
|
|
649
746
|
existing.add(state);
|
|
650
|
-
|
|
747
|
+
const syntheticAck = {
|
|
748
|
+
subscribed: streamKey,
|
|
749
|
+
realtimeMode: "stream",
|
|
750
|
+
latestStreamId: null
|
|
751
|
+
};
|
|
752
|
+
return {
|
|
753
|
+
unsubscribe: () => this.unsubscribeStream(state),
|
|
754
|
+
ack: syntheticAck
|
|
755
|
+
};
|
|
651
756
|
}
|
|
652
757
|
const set = /* @__PURE__ */ new Set();
|
|
653
758
|
set.add(state);
|
|
@@ -656,12 +761,15 @@ var RealtimeClient = class {
|
|
|
656
761
|
const ack = await this.emitSubscribeEvents(state);
|
|
657
762
|
if (ack.error) {
|
|
658
763
|
const message = ack.message ?? ack.error;
|
|
659
|
-
const err = new
|
|
764
|
+
const err = new SpacelrError(message, ack.error);
|
|
660
765
|
this.evictStreamKey(streamKey, err, state);
|
|
661
766
|
options.onError?.(err);
|
|
662
767
|
throw err;
|
|
663
768
|
}
|
|
664
|
-
return
|
|
769
|
+
return {
|
|
770
|
+
unsubscribe: () => this.unsubscribeStream(state),
|
|
771
|
+
ack
|
|
772
|
+
};
|
|
665
773
|
} catch (err) {
|
|
666
774
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
667
775
|
this.evictStreamKey(streamKey, error, state);
|
|
@@ -740,6 +848,7 @@ var RealtimeClient = class {
|
|
|
740
848
|
const wsUrl = this.config.baseUrl.replace(/\/api\/v\d+\/?$/, "");
|
|
741
849
|
return new Promise((resolve, reject) => {
|
|
742
850
|
let initialConnect = true;
|
|
851
|
+
let onInitialDisconnect = null;
|
|
743
852
|
this.socket = io(`${wsUrl}/database`, {
|
|
744
853
|
auth: async (cb) => {
|
|
745
854
|
try {
|
|
@@ -750,8 +859,11 @@ var RealtimeClient = class {
|
|
|
750
859
|
}
|
|
751
860
|
},
|
|
752
861
|
transports: ["websocket"],
|
|
862
|
+
// Disable auto-reconnect for the INITIAL connect — caller's promise
|
|
863
|
+
// rejects on connect_error and they decide whether to retry. Once the
|
|
864
|
+
// first authentication succeeds we re-enable it via the Manager's
|
|
865
|
+
// setters (see authenticated handler).
|
|
753
866
|
reconnection: false
|
|
754
|
-
// Disabled until first successful connect
|
|
755
867
|
});
|
|
756
868
|
this.socket.on("authenticated", () => {
|
|
757
869
|
if (this.disposed) return;
|
|
@@ -759,10 +871,32 @@ var RealtimeClient = class {
|
|
|
759
871
|
if (initialConnect) {
|
|
760
872
|
initialConnect = false;
|
|
761
873
|
if (this.socket) {
|
|
762
|
-
this.socket.io
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
874
|
+
const manager = this.socket.io;
|
|
875
|
+
try {
|
|
876
|
+
manager.reconnection(true);
|
|
877
|
+
manager.skipReconnect = false;
|
|
878
|
+
manager.reconnectionDelay(1e3);
|
|
879
|
+
manager.reconnectionDelayMax(5e3);
|
|
880
|
+
manager.reconnectionAttempts(50);
|
|
881
|
+
} catch (err) {
|
|
882
|
+
if (onInitialDisconnect && this.socket) {
|
|
883
|
+
this.socket.off("disconnect", onInitialDisconnect);
|
|
884
|
+
onInitialDisconnect = null;
|
|
885
|
+
}
|
|
886
|
+
this.socket?.disconnect();
|
|
887
|
+
this.socket = null;
|
|
888
|
+
this.setConnectionState("disconnected");
|
|
889
|
+
reject(
|
|
890
|
+
new Error(
|
|
891
|
+
`socket.io-client Manager reconnection setters failed \u2014 internal API may have changed; auto-reconnect cannot be enabled. Check socket.io-client version compatibility. Underlying error: ${err instanceof Error ? err.message : String(err)}`
|
|
892
|
+
)
|
|
893
|
+
);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (onInitialDisconnect && this.socket) {
|
|
898
|
+
this.socket.off("disconnect", onInitialDisconnect);
|
|
899
|
+
onInitialDisconnect = null;
|
|
766
900
|
}
|
|
767
901
|
if (this.config.onTokenRefreshed && !this.unsubscribeFromTokenRefreshed) {
|
|
768
902
|
this.unsubscribeFromTokenRefreshed = this.config.onTokenRefreshed(
|
|
@@ -782,6 +916,14 @@ var RealtimeClient = class {
|
|
|
782
916
|
reject(new Error(`WebSocket connection failed: ${err.message}`));
|
|
783
917
|
}
|
|
784
918
|
});
|
|
919
|
+
onInitialDisconnect = (reason) => {
|
|
920
|
+
if (!initialConnect) return;
|
|
921
|
+
this.socket?.disconnect();
|
|
922
|
+
this.socket = null;
|
|
923
|
+
this.setConnectionState("disconnected");
|
|
924
|
+
reject(new Error(`WebSocket disconnected during handshake: ${reason}`));
|
|
925
|
+
};
|
|
926
|
+
this.socket.on("disconnect", onInitialDisconnect);
|
|
785
927
|
this.socket.io.on("reconnect_failed", () => {
|
|
786
928
|
void this.rebuildSocket();
|
|
787
929
|
});
|
|
@@ -819,9 +961,10 @@ var RealtimeClient = class {
|
|
|
819
961
|
}
|
|
820
962
|
});
|
|
821
963
|
this.socket.on("disconnect", () => {
|
|
822
|
-
if (
|
|
823
|
-
|
|
824
|
-
|
|
964
|
+
if (this.disposed) return;
|
|
965
|
+
if (initialConnect) return;
|
|
966
|
+
if (!this.socket) return;
|
|
967
|
+
this.setConnectionState("reconnecting");
|
|
825
968
|
});
|
|
826
969
|
});
|
|
827
970
|
}
|
|
@@ -1019,7 +1162,7 @@ var RealtimeClient = class {
|
|
|
1019
1162
|
try {
|
|
1020
1163
|
const ack = await this.emitSubscribeEvents(primary);
|
|
1021
1164
|
if (ack.error) {
|
|
1022
|
-
const err = new
|
|
1165
|
+
const err = new SpacelrError(ack.message ?? ack.error, ack.error);
|
|
1023
1166
|
for (const state of [...set]) {
|
|
1024
1167
|
state.options.onError?.(err);
|
|
1025
1168
|
}
|
|
@@ -1771,7 +1914,13 @@ var QueryBuilder = class {
|
|
|
1771
1914
|
this._populate.push({ field, collection, foreignField });
|
|
1772
1915
|
return this;
|
|
1773
1916
|
}
|
|
1774
|
-
|
|
1917
|
+
/**
|
|
1918
|
+
* `signal` is an optional external AbortSignal forwarded into the underlying
|
|
1919
|
+
* HTTP request. Used by `subscribeWithSnapshot` so that an unsubscribe()
|
|
1920
|
+
* call cancels the in-flight snapshot find(); regular callers can leave it
|
|
1921
|
+
* undefined.
|
|
1922
|
+
*/
|
|
1923
|
+
async execute(signal) {
|
|
1775
1924
|
const query = {};
|
|
1776
1925
|
if (this._filter) query["filter"] = JSON.stringify(this._filter);
|
|
1777
1926
|
if (this._sort) query["sort"] = JSON.stringify(this._sort);
|
|
@@ -1789,7 +1938,8 @@ var QueryBuilder = class {
|
|
|
1789
1938
|
method: "GET",
|
|
1790
1939
|
path: this.basePath,
|
|
1791
1940
|
query,
|
|
1792
|
-
authenticated: true
|
|
1941
|
+
authenticated: true,
|
|
1942
|
+
signal
|
|
1793
1943
|
});
|
|
1794
1944
|
}
|
|
1795
1945
|
};
|
|
@@ -1842,6 +1992,13 @@ var CollectionRef = class {
|
|
|
1842
1992
|
* cannot use a standard B-tree index — on very large collections consider
|
|
1843
1993
|
* narrowing with `filter` to scope the scan.
|
|
1844
1994
|
*
|
|
1995
|
+
* Required filters: collections may declare `searchConfig.requireFilter`
|
|
1996
|
+
* via the admin API. Calls without those keys at the top level of `filter`
|
|
1997
|
+
* (or inside a top-level `$and`) reject with
|
|
1998
|
+
* `SpacelrSearchFilterRequiredError` carrying `missingFields`. Allowed
|
|
1999
|
+
* shapes: `{ field: value }`, `{ field: { $eq: value } }`,
|
|
2000
|
+
* `{ field: { $in: [...] } }` (non-empty).
|
|
2001
|
+
*
|
|
1845
2002
|
* Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching
|
|
1846
2003
|
* `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.
|
|
1847
2004
|
*/
|
|
@@ -2026,6 +2183,291 @@ var CollectionRef = class {
|
|
|
2026
2183
|
}
|
|
2027
2184
|
};
|
|
2028
2185
|
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Snapshot-aware subscribe — see #99 / SubscribeWithSnapshotOptions.
|
|
2188
|
+
*
|
|
2189
|
+
* Returns a Promise that resolves with the unsubscribe function only
|
|
2190
|
+
* after the first `onSnapshot` has run to completion (initial-load
|
|
2191
|
+
* signal). Calling unsubscribe() while the snapshot find() is in flight
|
|
2192
|
+
* aborts the request and silences the AbortError. Gap-recovery state
|
|
2193
|
+
* machine lands in Task 6.
|
|
2194
|
+
*/
|
|
2195
|
+
async subscribeWithSnapshot(opts) {
|
|
2196
|
+
if (!this.realtime) {
|
|
2197
|
+
throw new Error("Realtime not available: no RealtimeClient configured");
|
|
2198
|
+
}
|
|
2199
|
+
let isActive = true;
|
|
2200
|
+
let currentUnsubscribe = null;
|
|
2201
|
+
let snapshotAbort = new AbortController();
|
|
2202
|
+
let recoveryState = "idle";
|
|
2203
|
+
const realtime = this.realtime;
|
|
2204
|
+
const streamWhere = (() => {
|
|
2205
|
+
if (!opts.where) return void 0;
|
|
2206
|
+
const out = {};
|
|
2207
|
+
for (const [k, v] of Object.entries(opts.where)) {
|
|
2208
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
2209
|
+
out[k] = v;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2213
|
+
})();
|
|
2214
|
+
if (opts.where && streamWhere === void 0 && Object.keys(opts.where).length > 0) {
|
|
2215
|
+
console.warn(
|
|
2216
|
+
`[spacelr] subscribeWithSnapshot on ${this.projectId}:${this.collectionName}: 'where' filter contains only operator-shaped values (e.g. $gt, $in) which are not supported by the live stream filter. onChange will receive ALL collection events; snapshot is still filtered.`
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
const runRecovery = async () => {
|
|
2220
|
+
currentUnsubscribe?.();
|
|
2221
|
+
currentUnsubscribe = null;
|
|
2222
|
+
if (!isActive) return;
|
|
2223
|
+
let recoveryAckResolved = false;
|
|
2224
|
+
let recoverySnapshotDelivered = false;
|
|
2225
|
+
const pendingRecoveryEvents = [];
|
|
2226
|
+
let next;
|
|
2227
|
+
try {
|
|
2228
|
+
next = await realtime.subscribeWithCursorWithMeta({
|
|
2229
|
+
projectId: this.projectId,
|
|
2230
|
+
collectionName: this.collectionName,
|
|
2231
|
+
sinceId: void 0,
|
|
2232
|
+
where: streamWhere,
|
|
2233
|
+
onEvent: async ({ event }) => {
|
|
2234
|
+
if (!isActive) return;
|
|
2235
|
+
if (!recoverySnapshotDelivered) {
|
|
2236
|
+
pendingRecoveryEvents.push(event);
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
await opts.onChange(event);
|
|
2240
|
+
},
|
|
2241
|
+
onError: (err) => {
|
|
2242
|
+
if (!isActive) return;
|
|
2243
|
+
if (!recoveryAckResolved) return;
|
|
2244
|
+
isActive = false;
|
|
2245
|
+
currentUnsubscribe?.();
|
|
2246
|
+
currentUnsubscribe = null;
|
|
2247
|
+
snapshotAbort.abort();
|
|
2248
|
+
opts.onError?.(err);
|
|
2249
|
+
},
|
|
2250
|
+
onGap: handleGap
|
|
2251
|
+
});
|
|
2252
|
+
recoveryAckResolved = true;
|
|
2253
|
+
} catch (err) {
|
|
2254
|
+
if (!isActive) return;
|
|
2255
|
+
opts.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
if (!isActive) {
|
|
2259
|
+
next.unsubscribe();
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
currentUnsubscribe = next.unsubscribe;
|
|
2263
|
+
snapshotAbort = new AbortController();
|
|
2264
|
+
let recoveryDocs;
|
|
2265
|
+
try {
|
|
2266
|
+
const builder = this.find(opts.where);
|
|
2267
|
+
if (opts.sort) builder.sort(opts.sort);
|
|
2268
|
+
if (opts.limit !== void 0) builder.limit(opts.limit);
|
|
2269
|
+
if (opts.select) builder.select(opts.select);
|
|
2270
|
+
const result = await builder.execute(snapshotAbort.signal);
|
|
2271
|
+
recoveryDocs = result.documents;
|
|
2272
|
+
} catch (err) {
|
|
2273
|
+
if (!isActive) return;
|
|
2274
|
+
if (err instanceof DOMException && err.name === "AbortError") return;
|
|
2275
|
+
currentUnsubscribe?.();
|
|
2276
|
+
currentUnsubscribe = null;
|
|
2277
|
+
isActive = false;
|
|
2278
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2279
|
+
if (opts.onSnapshotError) opts.onSnapshotError(error);
|
|
2280
|
+
else opts.onError?.(error);
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (!isActive) return;
|
|
2284
|
+
try {
|
|
2285
|
+
await opts.onSnapshot(recoveryDocs, next.latestStreamId);
|
|
2286
|
+
} catch (err) {
|
|
2287
|
+
currentUnsubscribe?.();
|
|
2288
|
+
currentUnsubscribe = null;
|
|
2289
|
+
isActive = false;
|
|
2290
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2291
|
+
opts.onError?.(error);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
while (true) {
|
|
2295
|
+
if (!isActive) return;
|
|
2296
|
+
if (pendingRecoveryEvents.length === 0) {
|
|
2297
|
+
recoverySnapshotDelivered = true;
|
|
2298
|
+
break;
|
|
2299
|
+
}
|
|
2300
|
+
const ev = pendingRecoveryEvents.shift();
|
|
2301
|
+
if (ev !== void 0) {
|
|
2302
|
+
try {
|
|
2303
|
+
await opts.onChange(ev);
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
currentUnsubscribe?.();
|
|
2306
|
+
currentUnsubscribe = null;
|
|
2307
|
+
isActive = false;
|
|
2308
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2309
|
+
opts.onError?.(error);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
const dispatchRecovery = () => {
|
|
2316
|
+
if (!isActive) return;
|
|
2317
|
+
if (recoveryState === "running") {
|
|
2318
|
+
recoveryState = "pending-rerun";
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (recoveryState === "pending-rerun") {
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
recoveryState = "running";
|
|
2325
|
+
void (async () => {
|
|
2326
|
+
await Promise.resolve();
|
|
2327
|
+
while (true) {
|
|
2328
|
+
try {
|
|
2329
|
+
await runRecovery();
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
recoveryState = "idle";
|
|
2332
|
+
if (!isActive) return;
|
|
2333
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2334
|
+
try {
|
|
2335
|
+
opts.onError?.(error);
|
|
2336
|
+
} catch {
|
|
2337
|
+
}
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
if (!isActive) {
|
|
2341
|
+
recoveryState = "idle";
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
if (recoveryState === "pending-rerun") {
|
|
2345
|
+
recoveryState = "running";
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
recoveryState = "idle";
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
})();
|
|
2352
|
+
};
|
|
2353
|
+
const handleGap = (info) => {
|
|
2354
|
+
if (!isActive) return;
|
|
2355
|
+
if (info.reason !== "outside-retention-window") return;
|
|
2356
|
+
dispatchRecovery();
|
|
2357
|
+
};
|
|
2358
|
+
let subscribeAckResolved = false;
|
|
2359
|
+
let initialSnapshotDelivered = false;
|
|
2360
|
+
const pendingInitialEvents = [];
|
|
2361
|
+
let initial;
|
|
2362
|
+
try {
|
|
2363
|
+
initial = await realtime.subscribeWithCursorWithMeta({
|
|
2364
|
+
projectId: this.projectId,
|
|
2365
|
+
collectionName: this.collectionName,
|
|
2366
|
+
sinceId: void 0,
|
|
2367
|
+
where: streamWhere,
|
|
2368
|
+
onEvent: async ({ event }) => {
|
|
2369
|
+
if (!isActive) return;
|
|
2370
|
+
if (!initialSnapshotDelivered) {
|
|
2371
|
+
pendingInitialEvents.push(event);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
await opts.onChange(event);
|
|
2375
|
+
},
|
|
2376
|
+
onError: (err) => {
|
|
2377
|
+
if (!isActive) return;
|
|
2378
|
+
if (!subscribeAckResolved) return;
|
|
2379
|
+
isActive = false;
|
|
2380
|
+
snapshotAbort.abort();
|
|
2381
|
+
currentUnsubscribe?.();
|
|
2382
|
+
currentUnsubscribe = null;
|
|
2383
|
+
opts.onError?.(err);
|
|
2384
|
+
},
|
|
2385
|
+
onGap: handleGap
|
|
2386
|
+
});
|
|
2387
|
+
subscribeAckResolved = true;
|
|
2388
|
+
} catch (err) {
|
|
2389
|
+
isActive = false;
|
|
2390
|
+
const code = err instanceof SpacelrError ? err.code : void 0;
|
|
2391
|
+
if (code === "not-stream-collection") {
|
|
2392
|
+
throw new SpacelrError(
|
|
2393
|
+
`subscribeWithSnapshot requires a stream-mode collection. Use subscribe() for pubsub collections.`,
|
|
2394
|
+
"SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM",
|
|
2395
|
+
void 0,
|
|
2396
|
+
{ collectionName: this.collectionName }
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
throw err;
|
|
2400
|
+
}
|
|
2401
|
+
if (initial.realtimeMode !== "stream") {
|
|
2402
|
+
isActive = false;
|
|
2403
|
+
initial.unsubscribe();
|
|
2404
|
+
throw new SpacelrError(
|
|
2405
|
+
`subscribeWithSnapshot requires a stream-mode collection. Use subscribe() for pubsub collections.`,
|
|
2406
|
+
"SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM",
|
|
2407
|
+
void 0,
|
|
2408
|
+
{ collectionName: this.collectionName }
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
currentUnsubscribe = initial.unsubscribe;
|
|
2412
|
+
let snapshotDocs;
|
|
2413
|
+
try {
|
|
2414
|
+
const builder = this.find(opts.where);
|
|
2415
|
+
if (opts.sort) builder.sort(opts.sort);
|
|
2416
|
+
if (opts.limit !== void 0) builder.limit(opts.limit);
|
|
2417
|
+
if (opts.select) builder.select(opts.select);
|
|
2418
|
+
const result = await builder.execute(snapshotAbort.signal);
|
|
2419
|
+
snapshotDocs = result.documents;
|
|
2420
|
+
} catch (err) {
|
|
2421
|
+
currentUnsubscribe?.();
|
|
2422
|
+
isActive = false;
|
|
2423
|
+
const isAbort = err instanceof DOMException && err.name === "AbortError";
|
|
2424
|
+
if (isAbort) throw err;
|
|
2425
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2426
|
+
if (opts.onSnapshotError) opts.onSnapshotError(error);
|
|
2427
|
+
else opts.onError?.(error);
|
|
2428
|
+
throw error;
|
|
2429
|
+
}
|
|
2430
|
+
if (!isActive) {
|
|
2431
|
+
currentUnsubscribe?.();
|
|
2432
|
+
return () => void 0;
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
await opts.onSnapshot(snapshotDocs, initial.latestStreamId);
|
|
2436
|
+
} catch (err) {
|
|
2437
|
+
currentUnsubscribe?.();
|
|
2438
|
+
currentUnsubscribe = null;
|
|
2439
|
+
isActive = false;
|
|
2440
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2441
|
+
opts.onError?.(error);
|
|
2442
|
+
throw error;
|
|
2443
|
+
}
|
|
2444
|
+
while (true) {
|
|
2445
|
+
if (!isActive) return () => void 0;
|
|
2446
|
+
if (pendingInitialEvents.length === 0) {
|
|
2447
|
+
initialSnapshotDelivered = true;
|
|
2448
|
+
break;
|
|
2449
|
+
}
|
|
2450
|
+
const ev = pendingInitialEvents.shift();
|
|
2451
|
+
if (ev !== void 0) {
|
|
2452
|
+
try {
|
|
2453
|
+
await opts.onChange(ev);
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
currentUnsubscribe?.();
|
|
2456
|
+
currentUnsubscribe = null;
|
|
2457
|
+
isActive = false;
|
|
2458
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2459
|
+
opts.onError?.(error);
|
|
2460
|
+
throw error;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
return () => {
|
|
2465
|
+
if (!isActive) return;
|
|
2466
|
+
isActive = false;
|
|
2467
|
+
snapshotAbort.abort();
|
|
2468
|
+
currentUnsubscribe?.();
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2029
2471
|
};
|
|
2030
2472
|
var DatabaseModule = class {
|
|
2031
2473
|
constructor(http, projectId, realtime) {
|
|
@@ -2288,6 +2730,7 @@ export {
|
|
|
2288
2730
|
SpacelrEmailVerificationRequiredError,
|
|
2289
2731
|
SpacelrError,
|
|
2290
2732
|
SpacelrNetworkError,
|
|
2733
|
+
SpacelrSearchFilterRequiredError,
|
|
2291
2734
|
SpacelrTimeoutError,
|
|
2292
2735
|
SpacelrTwoFactorRequiredError,
|
|
2293
2736
|
createClient,
|