@spacelr/sdk 0.2.2 → 0.4.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.d.mts
CHANGED
|
@@ -296,6 +296,15 @@ interface HttpRequestOptions {
|
|
|
296
296
|
query?: Record<string, string | number | undefined>;
|
|
297
297
|
/** Send cookies cross-origin (credentials: 'include'). Defaults to true for /auth/ paths. */
|
|
298
298
|
withCredentials?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* External AbortSignal that lets callers cancel an in-flight request before
|
|
301
|
+
* the configured timeout elapses (e.g. `subscribeWithSnapshot` aborting its
|
|
302
|
+
* snapshot find() when the user calls unsubscribe()). Forwarded into the
|
|
303
|
+
* internal AbortController so cancellation surfaces as a DOMException with
|
|
304
|
+
* name 'AbortError' — the caller is responsible for distinguishing it from
|
|
305
|
+
* the timeout-driven abort if it matters.
|
|
306
|
+
*/
|
|
307
|
+
signal?: AbortSignal;
|
|
299
308
|
}
|
|
300
309
|
declare class HttpClient {
|
|
301
310
|
private config;
|
|
@@ -362,6 +371,19 @@ declare class SpacelrTwoFactorRequiredError extends SpacelrError {
|
|
|
362
371
|
readonly twoFactorToken: string;
|
|
363
372
|
constructor(twoFactorToken: string, details?: Record<string, unknown>);
|
|
364
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Thrown when `collection.search()` is called without the pre-filter the
|
|
376
|
+
* collection requires (issue #165). `details.missingFields` lists the
|
|
377
|
+
* top-level filter keys the server expected.
|
|
378
|
+
*
|
|
379
|
+
* Required filters protect against unindexable full-collection regex
|
|
380
|
+
* scans on large collections — see the collection's `searchConfig` for
|
|
381
|
+
* which fields are required.
|
|
382
|
+
*/
|
|
383
|
+
declare class SpacelrSearchFilterRequiredError extends SpacelrError {
|
|
384
|
+
readonly missingFields: string[];
|
|
385
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
386
|
+
}
|
|
365
387
|
declare class SpacelrEmailVerificationRequiredError extends SpacelrError {
|
|
366
388
|
readonly emailSent: boolean;
|
|
367
389
|
constructor(emailSent: boolean, details?: Record<string, unknown>);
|
|
@@ -448,6 +470,58 @@ declare class RealtimeClient {
|
|
|
448
470
|
* event rather than the originally-subscribed `sinceId`.
|
|
449
471
|
*/
|
|
450
472
|
subscribeWithCursor(options: StreamSubscriptionOptions): Promise<() => void>;
|
|
473
|
+
/**
|
|
474
|
+
* Metadata-aware variant of `subscribeWithCursor()` — see #99.
|
|
475
|
+
*
|
|
476
|
+
* Identical to `subscribeWithCursor` except it also returns the
|
|
477
|
+
* `realtimeMode` and `latestStreamId` from the server's
|
|
478
|
+
* subscribe-events ack so callers (notably `subscribeWithSnapshot`)
|
|
479
|
+
* can fail-fast on pubsub-mode collections and anchor the snapshot
|
|
480
|
+
* baseline atomically with the reader-attach.
|
|
481
|
+
*
|
|
482
|
+
* Defaults: if the server didn't send `realtimeMode` (older
|
|
483
|
+
* gateway), the SDK treats the collection as pubsub-mode so
|
|
484
|
+
* downstream fail-fasts still fire correctly. `latestStreamId`
|
|
485
|
+
* defaults to `null` when absent.
|
|
486
|
+
*
|
|
487
|
+
* Dedup-path note: when a sibling local subscription already holds
|
|
488
|
+
* the server-side reader slot for this streamKey, no new handshake
|
|
489
|
+
* is emitted — the SDK piggy-backs on the existing slot. In that
|
|
490
|
+
* case we synthesise `{ realtimeMode: 'stream', latestStreamId: null }`
|
|
491
|
+
* because the existing slot's prior ack is no longer available.
|
|
492
|
+
* Callers that need a fresh `latestStreamId` (e.g. snapshot anchor)
|
|
493
|
+
* MUST handle the `null` case.
|
|
494
|
+
*/
|
|
495
|
+
subscribeWithCursorWithMeta(options: StreamSubscriptionOptions): Promise<{
|
|
496
|
+
unsubscribe: () => void;
|
|
497
|
+
realtimeMode: 'pubsub' | 'stream';
|
|
498
|
+
latestStreamId: string | null;
|
|
499
|
+
}>;
|
|
500
|
+
/**
|
|
501
|
+
* Shared body for `subscribeWithCursor` and
|
|
502
|
+
* `subscribeWithCursorWithMeta`. Owns the handshake, dedup path,
|
|
503
|
+
* pre-ack registration, and error eviction. Returns the unsubscribe
|
|
504
|
+
* function plus the subscribe-events ack so metadata-aware callers
|
|
505
|
+
* can read `realtimeMode` / `latestStreamId`.
|
|
506
|
+
*
|
|
507
|
+
* Behaviour-preserving contract — DO NOT change without re-running
|
|
508
|
+
* the singleflight tests in `realtime.spec.ts`:
|
|
509
|
+
*
|
|
510
|
+
* 1. Dedup branch BEFORE registration: if a sibling subscriber for
|
|
511
|
+
* this streamKey is already registered, the new caller piggy-backs
|
|
512
|
+
* on its server-side slot. Re-emitting the handshake here would
|
|
513
|
+
* tear the prior subscriber's reader down.
|
|
514
|
+
* 2. Registration BEFORE handshake: `streamSubscriptions.set` must
|
|
515
|
+
* fire before `emitSubscribeEvents` so replay/event-gap frames
|
|
516
|
+
* delivered DURING the handshake fanout find this subscriber.
|
|
517
|
+
* 3. evictStreamKey on ack-error MUST sweep dedup-path siblings:
|
|
518
|
+
* they have no independent server-side state and would silently
|
|
519
|
+
* miss every event until the next full reconnect.
|
|
520
|
+
* 4. The catch block is a safety net for the re-thrown ack-error
|
|
521
|
+
* only — `emitSubscribeEvents` itself always resolves, so this
|
|
522
|
+
* catch should never fire on the success path.
|
|
523
|
+
*/
|
|
524
|
+
private _subscribeInternal;
|
|
451
525
|
/**
|
|
452
526
|
* Tear down the realtime client permanently. After this call the instance
|
|
453
527
|
* is disposed — subsequent `subscribe()` calls will not re-establish a
|
|
@@ -733,6 +807,98 @@ interface SearchOptions {
|
|
|
733
807
|
offset?: number;
|
|
734
808
|
select?: string[];
|
|
735
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Options for `CollectionRef.subscribeWithSnapshot()` — see #99.
|
|
812
|
+
*
|
|
813
|
+
* The helper combines an initial query, a live stream subscription,
|
|
814
|
+
* and outside-retention-window gap recovery into one idempotent
|
|
815
|
+
* contract. Apps must implement `onChange` as state mutation keyed
|
|
816
|
+
* by `_id` (Map/Set semantics): events MAY arrive twice (snapshot
|
|
817
|
+
* vs stream overlap, gap recovery replay window, reconnect replay
|
|
818
|
+
* from persisted cursor).
|
|
819
|
+
*
|
|
820
|
+
* Performance: `onChange` is awaited; a slow handler stalls the
|
|
821
|
+
* stream for THIS subscriber. Keep it fast (synchronous state
|
|
822
|
+
* updates only).
|
|
823
|
+
*
|
|
824
|
+
* Requires a stream-mode collection. Calling on a pubsub collection
|
|
825
|
+
* throws SpacelrError with code SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM
|
|
826
|
+
* BEFORE any find() runs.
|
|
827
|
+
*/
|
|
828
|
+
interface SubscribeWithSnapshotOptions<T> {
|
|
829
|
+
/**
|
|
830
|
+
* Filter for both the initial snapshot AND the live `onChange` stream.
|
|
831
|
+
*
|
|
832
|
+
* **Snapshot semantics** (full MongoDB query): supports operators like
|
|
833
|
+
* `{ status: { $in: [...] } }`, `{ ts: { $gt: ... } }`, etc.
|
|
834
|
+
*
|
|
835
|
+
* **Live-stream semantics** (gateway-side equality only): the stream
|
|
836
|
+
* filter only honours top-level primitive equality
|
|
837
|
+
* (`string | number | boolean` values). Operator-shaped values are
|
|
838
|
+
* silently dropped from the stream filter at handshake time — the
|
|
839
|
+
* remaining primitive entries still apply, so a typical chat-style
|
|
840
|
+
* `{ chatId: 'c1' }` filter works identically on both sides. If you
|
|
841
|
+
* pass a mixed `{ chatId: 'c1', status: { $in: [...] } }`, the
|
|
842
|
+
* snapshot is fully filtered but the live stream filters by `chatId`
|
|
843
|
+
* only; you may receive `onChange` events for documents whose `status`
|
|
844
|
+
* is outside your `$in` set. Re-check in the handler if needed.
|
|
845
|
+
*/
|
|
846
|
+
where?: Record<string, unknown>;
|
|
847
|
+
sort?: Record<string, 1 | -1>;
|
|
848
|
+
limit?: number;
|
|
849
|
+
select?: string[];
|
|
850
|
+
/**
|
|
851
|
+
* Called with the full snapshot:
|
|
852
|
+
* - once after the initial find()
|
|
853
|
+
* - again after every gap-exceeded recovery
|
|
854
|
+
* `cursor` is the stream position at the moment the snapshot was
|
|
855
|
+
* taken — opaque, may be persisted, may be passed to a later
|
|
856
|
+
* subscribeWithCursor as sinceId. Never compare cursor strings.
|
|
857
|
+
*
|
|
858
|
+
* Note on shared readers: when another subscriber on the SAME
|
|
859
|
+
* client+collection is already active, this subscriber piggy-backs
|
|
860
|
+
* on that reader. The returned `cursor` reflects the stream tip at
|
|
861
|
+
* THIS handshake; the shared reader may already have advanced past
|
|
862
|
+
* it, so the duplicate-delivery window between this `cursor` and
|
|
863
|
+
* the next `onChange` event can be wider than a fresh single-
|
|
864
|
+
* subscriber attach. The at-least-once contract still holds —
|
|
865
|
+
* idempotent `onChange` handlers absorb the extra duplicates.
|
|
866
|
+
*/
|
|
867
|
+
onSnapshot: (docs: (T & {
|
|
868
|
+
_id: string;
|
|
869
|
+
})[], cursor: string | null) => void | Promise<void>;
|
|
870
|
+
/**
|
|
871
|
+
* Per-event callback. Awaited; cursor advances after this resolves.
|
|
872
|
+
* State mutations MUST be idempotent by `_id`.
|
|
873
|
+
*/
|
|
874
|
+
onChange: (event: DatabaseChangeEvent) => void | Promise<void>;
|
|
875
|
+
/**
|
|
876
|
+
* Fatal stream/subscription errors AFTER the initial handshake has
|
|
877
|
+
* resolved. Subscription is torn down. Pre-handshake failures (e.g.
|
|
878
|
+
* pubsub-mode collection, ack-error from the gateway) DO NOT fire
|
|
879
|
+
* this callback — they reject the helper's promise instead, with a
|
|
880
|
+
* typed `SpacelrError` carrying a discriminator code.
|
|
881
|
+
*
|
|
882
|
+
* If this callback itself throws, the gap-recovery state machine
|
|
883
|
+
* still resets to `'idle'`, but `onError` may be invoked a second
|
|
884
|
+
* time (with the same error) by the dispatch IIFE's safety-net
|
|
885
|
+
* catch. Keep the callback total: log, set state, return.
|
|
886
|
+
*/
|
|
887
|
+
onError?: (err: Error) => void;
|
|
888
|
+
/**
|
|
889
|
+
* Snapshot read (`find()`) failed. Default: forwards to `onError`.
|
|
890
|
+
* Aborted reads (caused by `unsubscribe()`) NEVER call this — they
|
|
891
|
+
* silently reject the helper's promise with `AbortError`.
|
|
892
|
+
*
|
|
893
|
+
* **Dual-notification on non-abort failures**: a snapshot failure
|
|
894
|
+
* fires this callback AND rejects the helper's outer promise with
|
|
895
|
+
* the same error. Callers that handle both `await ...catch()` AND
|
|
896
|
+
* register `onSnapshotError` will receive the error twice — pick
|
|
897
|
+
* one path. The dual surface is intentional so apps using fire-and-
|
|
898
|
+
* forget callback handlers don't need to also wrap the await.
|
|
899
|
+
*/
|
|
900
|
+
onSnapshotError?: (err: Error) => void;
|
|
901
|
+
}
|
|
736
902
|
interface SubscribeEventsHandlers<T = Record<string, unknown>> {
|
|
737
903
|
/** Cursor to resume from. Undefined = fresh subscription, deliver only new events. */
|
|
738
904
|
sinceId?: string;
|
|
@@ -919,7 +1085,13 @@ declare class QueryBuilder<T = Record<string, unknown>, M extends 'offset' | 'cu
|
|
|
919
1085
|
after(id: string): QueryBuilder<T, 'cursor'>;
|
|
920
1086
|
select(fields: string[]): this;
|
|
921
1087
|
populate(field: string, collection: string, foreignField?: string): this;
|
|
922
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* `signal` is an optional external AbortSignal forwarded into the underlying
|
|
1090
|
+
* HTTP request. Used by `subscribeWithSnapshot` so that an unsubscribe()
|
|
1091
|
+
* call cancels the in-flight snapshot find(); regular callers can leave it
|
|
1092
|
+
* undefined.
|
|
1093
|
+
*/
|
|
1094
|
+
execute(signal?: AbortSignal): Promise<M extends 'cursor' ? CursorResult<T> : OffsetResult<T>>;
|
|
923
1095
|
}
|
|
924
1096
|
declare class CollectionRef<T = Record<string, unknown>> {
|
|
925
1097
|
private http;
|
|
@@ -955,6 +1127,13 @@ declare class CollectionRef<T = Record<string, unknown>> {
|
|
|
955
1127
|
* cannot use a standard B-tree index — on very large collections consider
|
|
956
1128
|
* narrowing with `filter` to scope the scan.
|
|
957
1129
|
*
|
|
1130
|
+
* Required filters: collections may declare `searchConfig.requireFilter`
|
|
1131
|
+
* via the admin API. Calls without those keys at the top level of `filter`
|
|
1132
|
+
* (or inside a top-level `$and`) reject with
|
|
1133
|
+
* `SpacelrSearchFilterRequiredError` carrying `missingFields`. Allowed
|
|
1134
|
+
* shapes: `{ field: value }`, `{ field: { $eq: value } }`,
|
|
1135
|
+
* `{ field: { $in: [...] } }` (non-empty).
|
|
1136
|
+
*
|
|
958
1137
|
* Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching
|
|
959
1138
|
* `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.
|
|
960
1139
|
*/
|
|
@@ -967,6 +1146,16 @@ declare class CollectionRef<T = Record<string, unknown>> {
|
|
|
967
1146
|
count(filter?: Record<string, unknown>): Promise<number>;
|
|
968
1147
|
subscribe(handlers: SubscribeHandlers<T>): () => void;
|
|
969
1148
|
subscribeEvents(handlers: SubscribeEventsHandlers<T>): StreamSubscription;
|
|
1149
|
+
/**
|
|
1150
|
+
* Snapshot-aware subscribe — see #99 / SubscribeWithSnapshotOptions.
|
|
1151
|
+
*
|
|
1152
|
+
* Returns a Promise that resolves with the unsubscribe function only
|
|
1153
|
+
* after the first `onSnapshot` has run to completion (initial-load
|
|
1154
|
+
* signal). Calling unsubscribe() while the snapshot find() is in flight
|
|
1155
|
+
* aborts the request and silences the AbortError. Gap-recovery state
|
|
1156
|
+
* machine lands in Task 6.
|
|
1157
|
+
*/
|
|
1158
|
+
subscribeWithSnapshot(opts: SubscribeWithSnapshotOptions<T>): Promise<() => void>;
|
|
970
1159
|
}
|
|
971
1160
|
declare class DatabaseModule {
|
|
972
1161
|
private http;
|
|
@@ -1137,4 +1326,4 @@ interface SpacelrClient {
|
|
|
1137
1326
|
}
|
|
1138
1327
|
declare function createClient(config: SpacelrClientConfig): SpacelrClient;
|
|
1139
1328
|
|
|
1140
|
-
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type SearchOptions, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|
|
1329
|
+
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type SearchOptions, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrSearchFilterRequiredError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type SubscribeWithSnapshotOptions, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|
package/dist/index.d.ts
CHANGED
|
@@ -296,6 +296,15 @@ interface HttpRequestOptions {
|
|
|
296
296
|
query?: Record<string, string | number | undefined>;
|
|
297
297
|
/** Send cookies cross-origin (credentials: 'include'). Defaults to true for /auth/ paths. */
|
|
298
298
|
withCredentials?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* External AbortSignal that lets callers cancel an in-flight request before
|
|
301
|
+
* the configured timeout elapses (e.g. `subscribeWithSnapshot` aborting its
|
|
302
|
+
* snapshot find() when the user calls unsubscribe()). Forwarded into the
|
|
303
|
+
* internal AbortController so cancellation surfaces as a DOMException with
|
|
304
|
+
* name 'AbortError' — the caller is responsible for distinguishing it from
|
|
305
|
+
* the timeout-driven abort if it matters.
|
|
306
|
+
*/
|
|
307
|
+
signal?: AbortSignal;
|
|
299
308
|
}
|
|
300
309
|
declare class HttpClient {
|
|
301
310
|
private config;
|
|
@@ -362,6 +371,19 @@ declare class SpacelrTwoFactorRequiredError extends SpacelrError {
|
|
|
362
371
|
readonly twoFactorToken: string;
|
|
363
372
|
constructor(twoFactorToken: string, details?: Record<string, unknown>);
|
|
364
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Thrown when `collection.search()` is called without the pre-filter the
|
|
376
|
+
* collection requires (issue #165). `details.missingFields` lists the
|
|
377
|
+
* top-level filter keys the server expected.
|
|
378
|
+
*
|
|
379
|
+
* Required filters protect against unindexable full-collection regex
|
|
380
|
+
* scans on large collections — see the collection's `searchConfig` for
|
|
381
|
+
* which fields are required.
|
|
382
|
+
*/
|
|
383
|
+
declare class SpacelrSearchFilterRequiredError extends SpacelrError {
|
|
384
|
+
readonly missingFields: string[];
|
|
385
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
386
|
+
}
|
|
365
387
|
declare class SpacelrEmailVerificationRequiredError extends SpacelrError {
|
|
366
388
|
readonly emailSent: boolean;
|
|
367
389
|
constructor(emailSent: boolean, details?: Record<string, unknown>);
|
|
@@ -448,6 +470,58 @@ declare class RealtimeClient {
|
|
|
448
470
|
* event rather than the originally-subscribed `sinceId`.
|
|
449
471
|
*/
|
|
450
472
|
subscribeWithCursor(options: StreamSubscriptionOptions): Promise<() => void>;
|
|
473
|
+
/**
|
|
474
|
+
* Metadata-aware variant of `subscribeWithCursor()` — see #99.
|
|
475
|
+
*
|
|
476
|
+
* Identical to `subscribeWithCursor` except it also returns the
|
|
477
|
+
* `realtimeMode` and `latestStreamId` from the server's
|
|
478
|
+
* subscribe-events ack so callers (notably `subscribeWithSnapshot`)
|
|
479
|
+
* can fail-fast on pubsub-mode collections and anchor the snapshot
|
|
480
|
+
* baseline atomically with the reader-attach.
|
|
481
|
+
*
|
|
482
|
+
* Defaults: if the server didn't send `realtimeMode` (older
|
|
483
|
+
* gateway), the SDK treats the collection as pubsub-mode so
|
|
484
|
+
* downstream fail-fasts still fire correctly. `latestStreamId`
|
|
485
|
+
* defaults to `null` when absent.
|
|
486
|
+
*
|
|
487
|
+
* Dedup-path note: when a sibling local subscription already holds
|
|
488
|
+
* the server-side reader slot for this streamKey, no new handshake
|
|
489
|
+
* is emitted — the SDK piggy-backs on the existing slot. In that
|
|
490
|
+
* case we synthesise `{ realtimeMode: 'stream', latestStreamId: null }`
|
|
491
|
+
* because the existing slot's prior ack is no longer available.
|
|
492
|
+
* Callers that need a fresh `latestStreamId` (e.g. snapshot anchor)
|
|
493
|
+
* MUST handle the `null` case.
|
|
494
|
+
*/
|
|
495
|
+
subscribeWithCursorWithMeta(options: StreamSubscriptionOptions): Promise<{
|
|
496
|
+
unsubscribe: () => void;
|
|
497
|
+
realtimeMode: 'pubsub' | 'stream';
|
|
498
|
+
latestStreamId: string | null;
|
|
499
|
+
}>;
|
|
500
|
+
/**
|
|
501
|
+
* Shared body for `subscribeWithCursor` and
|
|
502
|
+
* `subscribeWithCursorWithMeta`. Owns the handshake, dedup path,
|
|
503
|
+
* pre-ack registration, and error eviction. Returns the unsubscribe
|
|
504
|
+
* function plus the subscribe-events ack so metadata-aware callers
|
|
505
|
+
* can read `realtimeMode` / `latestStreamId`.
|
|
506
|
+
*
|
|
507
|
+
* Behaviour-preserving contract — DO NOT change without re-running
|
|
508
|
+
* the singleflight tests in `realtime.spec.ts`:
|
|
509
|
+
*
|
|
510
|
+
* 1. Dedup branch BEFORE registration: if a sibling subscriber for
|
|
511
|
+
* this streamKey is already registered, the new caller piggy-backs
|
|
512
|
+
* on its server-side slot. Re-emitting the handshake here would
|
|
513
|
+
* tear the prior subscriber's reader down.
|
|
514
|
+
* 2. Registration BEFORE handshake: `streamSubscriptions.set` must
|
|
515
|
+
* fire before `emitSubscribeEvents` so replay/event-gap frames
|
|
516
|
+
* delivered DURING the handshake fanout find this subscriber.
|
|
517
|
+
* 3. evictStreamKey on ack-error MUST sweep dedup-path siblings:
|
|
518
|
+
* they have no independent server-side state and would silently
|
|
519
|
+
* miss every event until the next full reconnect.
|
|
520
|
+
* 4. The catch block is a safety net for the re-thrown ack-error
|
|
521
|
+
* only — `emitSubscribeEvents` itself always resolves, so this
|
|
522
|
+
* catch should never fire on the success path.
|
|
523
|
+
*/
|
|
524
|
+
private _subscribeInternal;
|
|
451
525
|
/**
|
|
452
526
|
* Tear down the realtime client permanently. After this call the instance
|
|
453
527
|
* is disposed — subsequent `subscribe()` calls will not re-establish a
|
|
@@ -733,6 +807,98 @@ interface SearchOptions {
|
|
|
733
807
|
offset?: number;
|
|
734
808
|
select?: string[];
|
|
735
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Options for `CollectionRef.subscribeWithSnapshot()` — see #99.
|
|
812
|
+
*
|
|
813
|
+
* The helper combines an initial query, a live stream subscription,
|
|
814
|
+
* and outside-retention-window gap recovery into one idempotent
|
|
815
|
+
* contract. Apps must implement `onChange` as state mutation keyed
|
|
816
|
+
* by `_id` (Map/Set semantics): events MAY arrive twice (snapshot
|
|
817
|
+
* vs stream overlap, gap recovery replay window, reconnect replay
|
|
818
|
+
* from persisted cursor).
|
|
819
|
+
*
|
|
820
|
+
* Performance: `onChange` is awaited; a slow handler stalls the
|
|
821
|
+
* stream for THIS subscriber. Keep it fast (synchronous state
|
|
822
|
+
* updates only).
|
|
823
|
+
*
|
|
824
|
+
* Requires a stream-mode collection. Calling on a pubsub collection
|
|
825
|
+
* throws SpacelrError with code SUBSCRIBE_WITH_SNAPSHOT_REQUIRES_STREAM
|
|
826
|
+
* BEFORE any find() runs.
|
|
827
|
+
*/
|
|
828
|
+
interface SubscribeWithSnapshotOptions<T> {
|
|
829
|
+
/**
|
|
830
|
+
* Filter for both the initial snapshot AND the live `onChange` stream.
|
|
831
|
+
*
|
|
832
|
+
* **Snapshot semantics** (full MongoDB query): supports operators like
|
|
833
|
+
* `{ status: { $in: [...] } }`, `{ ts: { $gt: ... } }`, etc.
|
|
834
|
+
*
|
|
835
|
+
* **Live-stream semantics** (gateway-side equality only): the stream
|
|
836
|
+
* filter only honours top-level primitive equality
|
|
837
|
+
* (`string | number | boolean` values). Operator-shaped values are
|
|
838
|
+
* silently dropped from the stream filter at handshake time — the
|
|
839
|
+
* remaining primitive entries still apply, so a typical chat-style
|
|
840
|
+
* `{ chatId: 'c1' }` filter works identically on both sides. If you
|
|
841
|
+
* pass a mixed `{ chatId: 'c1', status: { $in: [...] } }`, the
|
|
842
|
+
* snapshot is fully filtered but the live stream filters by `chatId`
|
|
843
|
+
* only; you may receive `onChange` events for documents whose `status`
|
|
844
|
+
* is outside your `$in` set. Re-check in the handler if needed.
|
|
845
|
+
*/
|
|
846
|
+
where?: Record<string, unknown>;
|
|
847
|
+
sort?: Record<string, 1 | -1>;
|
|
848
|
+
limit?: number;
|
|
849
|
+
select?: string[];
|
|
850
|
+
/**
|
|
851
|
+
* Called with the full snapshot:
|
|
852
|
+
* - once after the initial find()
|
|
853
|
+
* - again after every gap-exceeded recovery
|
|
854
|
+
* `cursor` is the stream position at the moment the snapshot was
|
|
855
|
+
* taken — opaque, may be persisted, may be passed to a later
|
|
856
|
+
* subscribeWithCursor as sinceId. Never compare cursor strings.
|
|
857
|
+
*
|
|
858
|
+
* Note on shared readers: when another subscriber on the SAME
|
|
859
|
+
* client+collection is already active, this subscriber piggy-backs
|
|
860
|
+
* on that reader. The returned `cursor` reflects the stream tip at
|
|
861
|
+
* THIS handshake; the shared reader may already have advanced past
|
|
862
|
+
* it, so the duplicate-delivery window between this `cursor` and
|
|
863
|
+
* the next `onChange` event can be wider than a fresh single-
|
|
864
|
+
* subscriber attach. The at-least-once contract still holds —
|
|
865
|
+
* idempotent `onChange` handlers absorb the extra duplicates.
|
|
866
|
+
*/
|
|
867
|
+
onSnapshot: (docs: (T & {
|
|
868
|
+
_id: string;
|
|
869
|
+
})[], cursor: string | null) => void | Promise<void>;
|
|
870
|
+
/**
|
|
871
|
+
* Per-event callback. Awaited; cursor advances after this resolves.
|
|
872
|
+
* State mutations MUST be idempotent by `_id`.
|
|
873
|
+
*/
|
|
874
|
+
onChange: (event: DatabaseChangeEvent) => void | Promise<void>;
|
|
875
|
+
/**
|
|
876
|
+
* Fatal stream/subscription errors AFTER the initial handshake has
|
|
877
|
+
* resolved. Subscription is torn down. Pre-handshake failures (e.g.
|
|
878
|
+
* pubsub-mode collection, ack-error from the gateway) DO NOT fire
|
|
879
|
+
* this callback — they reject the helper's promise instead, with a
|
|
880
|
+
* typed `SpacelrError` carrying a discriminator code.
|
|
881
|
+
*
|
|
882
|
+
* If this callback itself throws, the gap-recovery state machine
|
|
883
|
+
* still resets to `'idle'`, but `onError` may be invoked a second
|
|
884
|
+
* time (with the same error) by the dispatch IIFE's safety-net
|
|
885
|
+
* catch. Keep the callback total: log, set state, return.
|
|
886
|
+
*/
|
|
887
|
+
onError?: (err: Error) => void;
|
|
888
|
+
/**
|
|
889
|
+
* Snapshot read (`find()`) failed. Default: forwards to `onError`.
|
|
890
|
+
* Aborted reads (caused by `unsubscribe()`) NEVER call this — they
|
|
891
|
+
* silently reject the helper's promise with `AbortError`.
|
|
892
|
+
*
|
|
893
|
+
* **Dual-notification on non-abort failures**: a snapshot failure
|
|
894
|
+
* fires this callback AND rejects the helper's outer promise with
|
|
895
|
+
* the same error. Callers that handle both `await ...catch()` AND
|
|
896
|
+
* register `onSnapshotError` will receive the error twice — pick
|
|
897
|
+
* one path. The dual surface is intentional so apps using fire-and-
|
|
898
|
+
* forget callback handlers don't need to also wrap the await.
|
|
899
|
+
*/
|
|
900
|
+
onSnapshotError?: (err: Error) => void;
|
|
901
|
+
}
|
|
736
902
|
interface SubscribeEventsHandlers<T = Record<string, unknown>> {
|
|
737
903
|
/** Cursor to resume from. Undefined = fresh subscription, deliver only new events. */
|
|
738
904
|
sinceId?: string;
|
|
@@ -919,7 +1085,13 @@ declare class QueryBuilder<T = Record<string, unknown>, M extends 'offset' | 'cu
|
|
|
919
1085
|
after(id: string): QueryBuilder<T, 'cursor'>;
|
|
920
1086
|
select(fields: string[]): this;
|
|
921
1087
|
populate(field: string, collection: string, foreignField?: string): this;
|
|
922
|
-
|
|
1088
|
+
/**
|
|
1089
|
+
* `signal` is an optional external AbortSignal forwarded into the underlying
|
|
1090
|
+
* HTTP request. Used by `subscribeWithSnapshot` so that an unsubscribe()
|
|
1091
|
+
* call cancels the in-flight snapshot find(); regular callers can leave it
|
|
1092
|
+
* undefined.
|
|
1093
|
+
*/
|
|
1094
|
+
execute(signal?: AbortSignal): Promise<M extends 'cursor' ? CursorResult<T> : OffsetResult<T>>;
|
|
923
1095
|
}
|
|
924
1096
|
declare class CollectionRef<T = Record<string, unknown>> {
|
|
925
1097
|
private http;
|
|
@@ -955,6 +1127,13 @@ declare class CollectionRef<T = Record<string, unknown>> {
|
|
|
955
1127
|
* cannot use a standard B-tree index — on very large collections consider
|
|
956
1128
|
* narrowing with `filter` to scope the scan.
|
|
957
1129
|
*
|
|
1130
|
+
* Required filters: collections may declare `searchConfig.requireFilter`
|
|
1131
|
+
* via the admin API. Calls without those keys at the top level of `filter`
|
|
1132
|
+
* (or inside a top-level `$and`) reject with
|
|
1133
|
+
* `SpacelrSearchFilterRequiredError` carrying `missingFields`. Allowed
|
|
1134
|
+
* shapes: `{ field: value }`, `{ field: { $eq: value } }`,
|
|
1135
|
+
* `{ field: { $in: [...] } }` (non-empty).
|
|
1136
|
+
*
|
|
958
1137
|
* Limits: `query` 1–200 chars, `fields` 1–10 entries (each matching
|
|
959
1138
|
* `/^[a-zA-Z0-9_.]+$/`, max 64 chars), `limit` max 100.
|
|
960
1139
|
*/
|
|
@@ -967,6 +1146,16 @@ declare class CollectionRef<T = Record<string, unknown>> {
|
|
|
967
1146
|
count(filter?: Record<string, unknown>): Promise<number>;
|
|
968
1147
|
subscribe(handlers: SubscribeHandlers<T>): () => void;
|
|
969
1148
|
subscribeEvents(handlers: SubscribeEventsHandlers<T>): StreamSubscription;
|
|
1149
|
+
/**
|
|
1150
|
+
* Snapshot-aware subscribe — see #99 / SubscribeWithSnapshotOptions.
|
|
1151
|
+
*
|
|
1152
|
+
* Returns a Promise that resolves with the unsubscribe function only
|
|
1153
|
+
* after the first `onSnapshot` has run to completion (initial-load
|
|
1154
|
+
* signal). Calling unsubscribe() while the snapshot find() is in flight
|
|
1155
|
+
* aborts the request and silences the AbortError. Gap-recovery state
|
|
1156
|
+
* machine lands in Task 6.
|
|
1157
|
+
*/
|
|
1158
|
+
subscribeWithSnapshot(opts: SubscribeWithSnapshotOptions<T>): Promise<() => void>;
|
|
970
1159
|
}
|
|
971
1160
|
declare class DatabaseModule {
|
|
972
1161
|
private http;
|
|
@@ -1137,4 +1326,4 @@ interface SpacelrClient {
|
|
|
1137
1326
|
}
|
|
1138
1327
|
declare function createClient(config: SpacelrClientConfig): SpacelrClient;
|
|
1139
1328
|
|
|
1140
|
-
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type SearchOptions, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|
|
1329
|
+
export { type ApiResponse, type AuthLostReason, type AuthState, type AuthStateListener, type AuthorizationUrlParams, BrowserTokenStorage, CodeChallengeMethod, type ConnectionState, type CursorStorage, type DatabaseChangeEvent, type DownloadUrlResponse, type ExchangeCodeParams, type FileInfo, type FileListResponse, FileVisibility, type FunctionInvokeOptions, type FunctionInvokeResult, type GapReason, GrantType, type InitMultipartUploadParams, type InitMultipartUploadResponse, type JWK, type JWKSResponse, type ListFilesParams, type LoginParams, type LoginResponse, MemoryTokenStorage, type OpenIDConfiguration, type PKCEChallenge, type PartEtag, type PushSubscriptionInfo, type QuotaInfo, type RegisterParams, type RegisterResponse, type SearchOptions, type ShareFileParams, SharePermission, SpacelrAuthError, type SpacelrClient, type SpacelrClientConfig, SpacelrEmailVerificationRequiredError, SpacelrError, SpacelrNetworkError, SpacelrSearchFilterRequiredError, SpacelrTimeoutError, SpacelrTwoFactorRequiredError, type StoredTokens, type StreamGapInfo, type StreamSubscription, type SubscribeEventsHandlers, type SubscribeHandlers, type SubscribeWithSnapshotOptions, type TokenResponse, type TokenStorage, type TwoFactorResponse, type TwoFactorVerifyParams, type UnshareFileParams, type UploadFileParams, type UploadLargeFileParams, type UploadProgress, type UserInfo, type UserProfile, type VapidKeyResponse, createClient, generatePKCEChallenge, localStorageCursorStorage, memoryCursorStorage };
|