@spacelr/sdk 0.2.0 → 0.2.2

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 CHANGED
@@ -381,28 +381,6 @@ interface DatabaseChangeEvent {
381
381
  * for pubsub-mode events. Used by function triggers as an idempotency key.
382
382
  */
383
383
  eventId?: string;
384
- /**
385
- * Identifies the origin of the write. Stamped on the envelope (never in the
386
- * document body) so function-service can run a tamper-resistant recursion guard
387
- * (internal TCP caller can spoof — see PR notes). Absent on legacy events
388
- * published before this field existed.
389
- */
390
- _source?: 'user' | 'function';
391
- /**
392
- * When `_source === 'function'`, the id of the function whose runtime wrote
393
- * the document. Used to skip self-chain events without blocking legitimate
394
- * cross-function chains.
395
- */
396
- _sourceFunctionId?: string;
397
- /**
398
- * Hop-counter on function-originated events. User writes have no value.
399
- * Each function-write increments the incoming envelope's depth by one,
400
- * so an A→B→A loop bumps the counter on every hop. The reader rejects
401
- * dispatch when depth >= MAX_CHAIN_DEPTH so mutual chains terminate
402
- * deterministically rather than relying on the bounded queue to drop
403
- * events.
404
- */
405
- _chainDepth?: number;
406
384
  }
407
385
  interface RealtimeConfig {
408
386
  baseUrl: string;
@@ -825,6 +803,71 @@ interface SubscribeHandlers<T = Record<string, unknown>> {
825
803
  onDelete?: (documentId: string) => void;
826
804
  onError?: (error: Error) => void;
827
805
  }
806
+ interface PaginateOptions {
807
+ where?: Record<string, unknown>;
808
+ /** Sort direction by `_id`. Defaults to -1 (newest first — chat scroll-back). */
809
+ sort?: {
810
+ _id: 1 | -1;
811
+ };
812
+ /** Page size. Default 50. Server typically caps at 100. */
813
+ limit?: number;
814
+ /**
815
+ * Optional initial cursor — paginate from this `_id` exclusively. Useful
816
+ * when the caller already holds a boundary document (e.g. the oldest
817
+ * message currently rendered in a chat view paired with `subscribeEvents`).
818
+ */
819
+ cursor?: string;
820
+ }
821
+ interface PaginatorPage<T = Record<string, unknown>> {
822
+ documents: (T & {
823
+ _id: string;
824
+ })[];
825
+ /**
826
+ * True when more documents exist past this page. The paginator also flips
827
+ * `exhausted` to true when this returns false, so callers can poll either
828
+ * `page.hasMore` after each call or `paginator.exhausted` between calls.
829
+ */
830
+ hasMore: boolean;
831
+ }
832
+ /**
833
+ * Cursor-based scroll helper around `find()`. Tracks the last-seen `_id`
834
+ * across calls and applies it as `.before()` (descending sort) or
835
+ * `.after()` (ascending sort) on the next page so the keyset stays stable
836
+ * under concurrent inserts. Use `subscribeEvents` for new docs and
837
+ * `paginate` for older — the two compose into a chat-style timeline.
838
+ */
839
+ declare class Paginator<T = Record<string, unknown>> {
840
+ private readonly http;
841
+ private readonly basePath;
842
+ private cursor?;
843
+ private _exhausted;
844
+ private readonly direction;
845
+ private readonly pageSize;
846
+ private readonly where?;
847
+ /**
848
+ * Tail of the serialized `next()` chain. Each call appends to this so
849
+ * concurrent invocations from a UI scroll handler (e.g. user spam-tapping
850
+ * "load more") don't issue parallel requests with the same cursor — which
851
+ * would return duplicate pages and clobber the cursor based on whichever
852
+ * response settles last. Calls execute strictly in invocation order.
853
+ */
854
+ private chain;
855
+ constructor(http: HttpClient, basePath: string, opts: PaginateOptions);
856
+ /**
857
+ * True once a `next()` call has returned an empty page. Subsequent
858
+ * `next()` calls return an empty page without hitting the server.
859
+ *
860
+ * Note: only the empty-page signal flips this flag; a server response
861
+ * that returns documents but `hasMore: false` does NOT exhaust. This
862
+ * lets ascending paginators (`sort: { _id: 1 }`) keep polling for
863
+ * documents inserted after the caller caught up to the current tail —
864
+ * the next `.after(lastSeen)` call will simply return zero documents
865
+ * until something new lands.
866
+ */
867
+ get exhausted(): boolean;
868
+ next(): Promise<PaginatorPage<T>>;
869
+ private fetchNextPage;
870
+ }
828
871
  declare class QueryBuilder<T = Record<string, unknown>, M extends 'offset' | 'cursor' = 'offset'> {
829
872
  private http;
830
873
  private basePath;
@@ -892,6 +935,18 @@ declare class CollectionRef<T = Record<string, unknown>> {
892
935
  _id?: string;
893
936
  })[]): Promise<InsertResult>;
894
937
  find(filter?: Record<string, unknown>): QueryBuilder<T>;
938
+ /**
939
+ * Cursor-based scroll-back helper. Returns a `Paginator` whose `.next()`
940
+ * yields successive pages and tracks the last-seen `_id` internally.
941
+ * Defaults to descending sort and 50 docs per page (chat scroll-back is
942
+ * the canonical use case). See `PaginateOptions` for tuning.
943
+ *
944
+ * **Cursor constraint:** uses `_id` keyset pagination, which requires
945
+ * 24-hex ObjectId `_id`s. Collections with custom string `_id` schemes
946
+ * fall back to comparing strings as ObjectIds — the server's handler
947
+ * documents this limitation on `before` / `after` directly.
948
+ */
949
+ paginate(opts?: PaginateOptions): Paginator<T>;
895
950
  /**
896
951
  * Server-side substring search across the specified fields.
897
952
  *
package/dist/index.d.ts CHANGED
@@ -381,28 +381,6 @@ interface DatabaseChangeEvent {
381
381
  * for pubsub-mode events. Used by function triggers as an idempotency key.
382
382
  */
383
383
  eventId?: string;
384
- /**
385
- * Identifies the origin of the write. Stamped on the envelope (never in the
386
- * document body) so function-service can run a tamper-resistant recursion guard
387
- * (internal TCP caller can spoof — see PR notes). Absent on legacy events
388
- * published before this field existed.
389
- */
390
- _source?: 'user' | 'function';
391
- /**
392
- * When `_source === 'function'`, the id of the function whose runtime wrote
393
- * the document. Used to skip self-chain events without blocking legitimate
394
- * cross-function chains.
395
- */
396
- _sourceFunctionId?: string;
397
- /**
398
- * Hop-counter on function-originated events. User writes have no value.
399
- * Each function-write increments the incoming envelope's depth by one,
400
- * so an A→B→A loop bumps the counter on every hop. The reader rejects
401
- * dispatch when depth >= MAX_CHAIN_DEPTH so mutual chains terminate
402
- * deterministically rather than relying on the bounded queue to drop
403
- * events.
404
- */
405
- _chainDepth?: number;
406
384
  }
407
385
  interface RealtimeConfig {
408
386
  baseUrl: string;
@@ -825,6 +803,71 @@ interface SubscribeHandlers<T = Record<string, unknown>> {
825
803
  onDelete?: (documentId: string) => void;
826
804
  onError?: (error: Error) => void;
827
805
  }
806
+ interface PaginateOptions {
807
+ where?: Record<string, unknown>;
808
+ /** Sort direction by `_id`. Defaults to -1 (newest first — chat scroll-back). */
809
+ sort?: {
810
+ _id: 1 | -1;
811
+ };
812
+ /** Page size. Default 50. Server typically caps at 100. */
813
+ limit?: number;
814
+ /**
815
+ * Optional initial cursor — paginate from this `_id` exclusively. Useful
816
+ * when the caller already holds a boundary document (e.g. the oldest
817
+ * message currently rendered in a chat view paired with `subscribeEvents`).
818
+ */
819
+ cursor?: string;
820
+ }
821
+ interface PaginatorPage<T = Record<string, unknown>> {
822
+ documents: (T & {
823
+ _id: string;
824
+ })[];
825
+ /**
826
+ * True when more documents exist past this page. The paginator also flips
827
+ * `exhausted` to true when this returns false, so callers can poll either
828
+ * `page.hasMore` after each call or `paginator.exhausted` between calls.
829
+ */
830
+ hasMore: boolean;
831
+ }
832
+ /**
833
+ * Cursor-based scroll helper around `find()`. Tracks the last-seen `_id`
834
+ * across calls and applies it as `.before()` (descending sort) or
835
+ * `.after()` (ascending sort) on the next page so the keyset stays stable
836
+ * under concurrent inserts. Use `subscribeEvents` for new docs and
837
+ * `paginate` for older — the two compose into a chat-style timeline.
838
+ */
839
+ declare class Paginator<T = Record<string, unknown>> {
840
+ private readonly http;
841
+ private readonly basePath;
842
+ private cursor?;
843
+ private _exhausted;
844
+ private readonly direction;
845
+ private readonly pageSize;
846
+ private readonly where?;
847
+ /**
848
+ * Tail of the serialized `next()` chain. Each call appends to this so
849
+ * concurrent invocations from a UI scroll handler (e.g. user spam-tapping
850
+ * "load more") don't issue parallel requests with the same cursor — which
851
+ * would return duplicate pages and clobber the cursor based on whichever
852
+ * response settles last. Calls execute strictly in invocation order.
853
+ */
854
+ private chain;
855
+ constructor(http: HttpClient, basePath: string, opts: PaginateOptions);
856
+ /**
857
+ * True once a `next()` call has returned an empty page. Subsequent
858
+ * `next()` calls return an empty page without hitting the server.
859
+ *
860
+ * Note: only the empty-page signal flips this flag; a server response
861
+ * that returns documents but `hasMore: false` does NOT exhaust. This
862
+ * lets ascending paginators (`sort: { _id: 1 }`) keep polling for
863
+ * documents inserted after the caller caught up to the current tail —
864
+ * the next `.after(lastSeen)` call will simply return zero documents
865
+ * until something new lands.
866
+ */
867
+ get exhausted(): boolean;
868
+ next(): Promise<PaginatorPage<T>>;
869
+ private fetchNextPage;
870
+ }
828
871
  declare class QueryBuilder<T = Record<string, unknown>, M extends 'offset' | 'cursor' = 'offset'> {
829
872
  private http;
830
873
  private basePath;
@@ -892,6 +935,18 @@ declare class CollectionRef<T = Record<string, unknown>> {
892
935
  _id?: string;
893
936
  })[]): Promise<InsertResult>;
894
937
  find(filter?: Record<string, unknown>): QueryBuilder<T>;
938
+ /**
939
+ * Cursor-based scroll-back helper. Returns a `Paginator` whose `.next()`
940
+ * yields successive pages and tracks the last-seen `_id` internally.
941
+ * Defaults to descending sort and 50 docs per page (chat scroll-back is
942
+ * the canonical use case). See `PaginateOptions` for tuning.
943
+ *
944
+ * **Cursor constraint:** uses `_id` keyset pagination, which requires
945
+ * 24-hex ObjectId `_id`s. Collections with custom string `_id` schemes
946
+ * fall back to comparing strings as ObjectIds — the server's handler
947
+ * documents this limitation on `before` / `after` directly.
948
+ */
949
+ paginate(opts?: PaginateOptions): Paginator<T>;
895
950
  /**
896
951
  * Server-side substring search across the specified fields.
897
952
  *
package/dist/index.js CHANGED
@@ -412,7 +412,9 @@ var TokenManager = class {
412
412
  return tokens.accessToken;
413
413
  }
414
414
  async setTokens(tokens) {
415
- await this.storage.setTokens(tokens);
415
+ const needsDefault = tokens.expiresAt === void 0 && !!tokens.refreshToken;
416
+ const normalised = needsDefault ? { ...tokens, expiresAt: Math.floor(Date.now() / 1e3) } : tokens;
417
+ await this.storage.setTokens(normalised);
416
418
  this.authLostEmitted = false;
417
419
  }
418
420
  async clearTokens() {
@@ -1674,6 +1676,68 @@ var StorageModule = class {
1674
1676
  };
1675
1677
 
1676
1678
  // libs/sdk/src/modules/database.module.ts
1679
+ var Paginator = class {
1680
+ constructor(http, basePath, opts) {
1681
+ this.http = http;
1682
+ this.basePath = basePath;
1683
+ this._exhausted = false;
1684
+ /**
1685
+ * Tail of the serialized `next()` chain. Each call appends to this so
1686
+ * concurrent invocations from a UI scroll handler (e.g. user spam-tapping
1687
+ * "load more") don't issue parallel requests with the same cursor — which
1688
+ * would return duplicate pages and clobber the cursor based on whichever
1689
+ * response settles last. Calls execute strictly in invocation order.
1690
+ */
1691
+ this.chain = Promise.resolve({
1692
+ documents: [],
1693
+ hasMore: false
1694
+ });
1695
+ this.cursor = opts.cursor;
1696
+ this.direction = opts.sort?._id ?? -1;
1697
+ this.pageSize = opts.limit ?? 50;
1698
+ this.where = opts.where;
1699
+ }
1700
+ /**
1701
+ * True once a `next()` call has returned an empty page. Subsequent
1702
+ * `next()` calls return an empty page without hitting the server.
1703
+ *
1704
+ * Note: only the empty-page signal flips this flag; a server response
1705
+ * that returns documents but `hasMore: false` does NOT exhaust. This
1706
+ * lets ascending paginators (`sort: { _id: 1 }`) keep polling for
1707
+ * documents inserted after the caller caught up to the current tail —
1708
+ * the next `.after(lastSeen)` call will simply return zero documents
1709
+ * until something new lands.
1710
+ */
1711
+ get exhausted() {
1712
+ return this._exhausted;
1713
+ }
1714
+ next() {
1715
+ const run = this.chain.then(
1716
+ () => this.fetchNextPage(),
1717
+ () => this.fetchNextPage()
1718
+ );
1719
+ this.chain = run;
1720
+ return run;
1721
+ }
1722
+ async fetchNextPage() {
1723
+ if (this._exhausted) return { documents: [], hasMore: false };
1724
+ const builder = new QueryBuilder(this.http, this.basePath, this.where).sort({ _id: this.direction }).limit(this.pageSize);
1725
+ let result;
1726
+ if (this.cursor !== void 0) {
1727
+ const cursorBuilder = this.direction === -1 ? builder.before(this.cursor) : builder.after(this.cursor);
1728
+ result = await cursorBuilder.execute();
1729
+ } else {
1730
+ result = await builder.execute();
1731
+ }
1732
+ if (result.documents.length === 0) {
1733
+ this._exhausted = true;
1734
+ return { documents: [], hasMore: false };
1735
+ }
1736
+ this.cursor = result.documents[result.documents.length - 1]._id;
1737
+ const hasMore = result.mode === "cursor" ? result.hasMore : result.documents.length < result.total;
1738
+ return { documents: result.documents, hasMore };
1739
+ }
1740
+ };
1677
1741
  var QueryBuilder = class {
1678
1742
  constructor(http, basePath, filter) {
1679
1743
  this.http = http;
@@ -1790,6 +1854,20 @@ var CollectionRef = class {
1790
1854
  find(filter) {
1791
1855
  return new QueryBuilder(this.http, this.basePath, filter);
1792
1856
  }
1857
+ /**
1858
+ * Cursor-based scroll-back helper. Returns a `Paginator` whose `.next()`
1859
+ * yields successive pages and tracks the last-seen `_id` internally.
1860
+ * Defaults to descending sort and 50 docs per page (chat scroll-back is
1861
+ * the canonical use case). See `PaginateOptions` for tuning.
1862
+ *
1863
+ * **Cursor constraint:** uses `_id` keyset pagination, which requires
1864
+ * 24-hex ObjectId `_id`s. Collections with custom string `_id` schemes
1865
+ * fall back to comparing strings as ObjectIds — the server's handler
1866
+ * documents this limitation on `before` / `after` directly.
1867
+ */
1868
+ paginate(opts = {}) {
1869
+ return new Paginator(this.http, this.basePath, opts);
1870
+ }
1793
1871
  /**
1794
1872
  * Server-side substring search across the specified fields.
1795
1873
  *