@pylonsync/sync 0.22.47 → 0.22.58

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.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { LocalStore } from "./local-store";
2
- import { MutationQueue } from "./mutation-queue";
2
+ import { MutationQueue, type Outbox } from "./mutation-queue";
3
+ import { UploadManager, type QueuedUploadHandle, type UploadAttachTarget } from "./upload-queue";
4
+ import type { FileVisibility } from "./upload";
3
5
  import { type RoomError, type RoomMember, type RoomSubscriber } from "./room-subscriptions";
4
6
  import { SessionResolver } from "./session-resolver";
5
7
  export { RoomSubscriptions, rowRoom, type RoomError, type RoomErrorCode, type RoomMember, type RoomMessage, type RoomMessageSubscriber, type RoomSubscriber, } from "./room-subscriptions";
@@ -8,8 +10,30 @@ export type { ReplicaPersistence } from "./persistence";
8
10
  export { BEARER_REJECTED_HEADER, bearerRejected, buildRequest, clearRejectedToken, pylonFetch, pylonFetchRaw, PylonHttpError, resolveBaseUrl, } from "./transport";
9
11
  export type { PylonRequestInit, TransportConfig } from "./transport";
10
12
  export { LocalStore } from "./local-store";
11
- export { MutationQueue, MutationRejectedError, type MutationQueuePersistence, type PendingMutation, } from "./mutation-queue";
13
+ export { MutationQueue, MutationRejectedError, type MutationQueuePersistence, type Outbox, type OutboxItem, type PendingMutation, } from "./mutation-queue";
14
+ /** The store channel the outbox notifies on. `store.subscribe(cb,
15
+ * OUTBOX_CHANNEL)` hears outbox changes and no entity changes. */
16
+ export declare const OUTBOX_CHANNEL = "$outbox";
17
+ /** The store channel `syncProgress()` changes notify on. */
18
+ export declare const SYNC_PROGRESS_CHANNEL = "$sync-progress";
19
+ /** How far the current pull has got. See `SyncEngine.syncProgress()`. */
20
+ export interface SyncProgress {
21
+ /** A pull is running. */
22
+ pulling: boolean;
23
+ /** The pull is a snapshot of current rows (first sync, or a far-behind
24
+ * catch-up) rather than the change log. */
25
+ snapshot: boolean;
26
+ /** Rows received by this pull so far. */
27
+ rows: number;
28
+ /** Pages received by this pull so far. */
29
+ pages: number;
30
+ /** A pull has completed against the server since the last replica
31
+ * reset (`isSynced()`). */
32
+ synced: boolean;
33
+ }
12
34
  export { generateId } from "./ids";
35
+ export { uploadBytes, type FileVisibility, type UploadBytesOptions, type UploadedFile, } from "./upload";
36
+ export { DEFAULT_MAX_QUEUED_UPLOAD_BYTES, IndexedDBUploadStore, LOCAL_FILE_PREFIX, localUploadId, MemoryUploadStore, UploadQueueFullError, UploadRejectedError, type QueuedUpload, type QueuedUploadHandle, type UploadAttachTarget, type UploadStore, } from "./upload-queue";
13
37
  export type { ChangeEvent, ClientChange, PullResponse, PushOpResult, PushResponse, ReactiveMessage, ReactiveSpec, ResolvedSession, Row, SyncConnectionStatus, SyncCursor, TransportType, } from "./types";
14
38
  export { defaultStorage, createWriteThroughStorage, type Storage, } from "./storage";
15
39
  import type { ReactiveMessage, ResolvedSession, Row, SyncConnectionStatus, SyncCursor, TransportType } from "./types";
@@ -64,6 +88,19 @@ export interface SyncEngineConfig {
64
88
  * becomes visible).
65
89
  */
66
90
  reconcileOnVisibility?: boolean;
91
+ /**
92
+ * The automatic sweep (on connect and when the tab becomes visible)
93
+ * downloads every replicated row to catch changes the change log does not
94
+ * carry. It runs only when the app was offline (the connection dropped,
95
+ * or it started from a cached replica) and the replica last matched the
96
+ * server (a complete snapshot, or a full sweep) at least this long ago;
97
+ * or after a snapshot that could not remove deleted rows itself. It
98
+ * never runs right after a complete snapshot or merely because the tab
99
+ * came to the foreground. Server hints (`sync-reconcile`) reconcile at
100
+ * once, and an explicit `reconcile()` call always runs. Default 300000
101
+ * (5 minutes).
102
+ */
103
+ autoReconcileIntervalMs?: number;
67
104
  /**
68
105
  * Keep the local replica across ORG switches (same user). Default
69
106
  * `true`: a tenant flip wipes the replica and re-bootstraps, because
@@ -105,6 +142,9 @@ export interface SyncEngineConfig {
105
142
  * its own leader (the pre-multi-tab semantics).
106
143
  */
107
144
  multiTab?: boolean;
145
+ /** Cap on the bytes `queueUpload` keeps on the device. Default 200 MB.
146
+ * Past it `queueUpload` throws `UploadQueueFullError`. */
147
+ maxQueuedUploadBytes?: number;
108
148
  }
109
149
  export declare class SyncEngine {
110
150
  private config;
@@ -159,6 +199,16 @@ export declare class SyncEngine {
159
199
  */
160
200
  private _synced;
161
201
  isSynced(): boolean;
202
+ private _progress;
203
+ /**
204
+ * How far the current pull has got: whether one is running, whether it
205
+ * is a snapshot, and the rows and pages received. Apps render from the
206
+ * session as soon as it resolves and show this while the replica fills.
207
+ * A new object on every change, so it works as a `useSyncExternalStore`
208
+ * snapshot. Follower tabs report `pulling: false` (the leader pulls).
209
+ */
210
+ syncProgress(): SyncProgress;
211
+ private setProgress;
162
212
  /** Flip `_synced` true (idempotent), notify, and tell follower tabs. */
163
213
  private markSynced;
164
214
  /** Flip `_initialSyncSettled` true (idempotent) + notify so `useQuery`
@@ -212,6 +262,8 @@ export declare class SyncEngine {
212
262
  private persistDegraded;
213
263
  readonly store: LocalStore;
214
264
  readonly mutations: MutationQueue;
265
+ /** Files queued with `queueUpload`, sent when there is a network. */
266
+ readonly uploads: UploadManager;
215
267
  /**
216
268
  * Stable per-client identifier. Minted on first construction, not
217
269
  * necessarily persisted (depends on what the host provides).
@@ -399,6 +451,65 @@ export declare class SyncEngine {
399
451
  * dev HUD's sync row; also handy for a "saving…" indicator.
400
452
  */
401
453
  pendingCount(): number;
454
+ private outboxCache;
455
+ /**
456
+ * Writes made here that the server has not applied yet (`pending`:
457
+ * offline, or waiting for a sign-in), and writes the server rejected
458
+ * (`failed`, with its error). Oldest first. The same object is
459
+ * returned until the queue changes, so it can back
460
+ * `useSyncExternalStore`. Changes notify store subscribers on
461
+ * `OUTBOX_CHANNEL`.
462
+ */
463
+ outbox(): Outbox;
464
+ /**
465
+ * Send a failed write again, with `data` (when given) merged over the
466
+ * fields it set. The write is applied locally again at once (the
467
+ * rejection rolled it back) and queued under a new op id. Settles like
468
+ * `insert`: resolves with the new op id once the server applies it or
469
+ * it is queued offline, and rejects with a `MutationRejectedError` when
470
+ * the server rejects it again (it is then failed again, with the new
471
+ * error).
472
+ *
473
+ * A write is only ever sent as the user who made it. Throws
474
+ * `MutationRejectedError` with code `OWNER_MISMATCH` when someone else
475
+ * is signed in, and `OWNER_UNKNOWN` when it is not yet known who is
476
+ * signed in (or the write predates owner tracking). Dismiss those.
477
+ */
478
+ retryWrite(id: string, data?: Row): Promise<string>;
479
+ /** Drop a failed write after the user has seen it. Its local change
480
+ * was already rolled back. Returns false when there is no failed write
481
+ * with that id; a pending write cannot be dismissed. */
482
+ dismissWrite(id: string): boolean;
483
+ /**
484
+ * Keep a file on the device and upload it when there is a network, then
485
+ * set it on the record it belongs to. Returns at once.
486
+ *
487
+ * At once, `attach.entity`/`attach.id` gets `attach.field` (and
488
+ * `attach.urlField`, when given) = `local:<localId>` as a queued write,
489
+ * so the record shows the file from the device (`uploadLocalUrl`). When
490
+ * the upload finishes, a second queued write sets the file's id (and
491
+ * URL). Both are ordinary writes: policies and owner rules apply.
492
+ *
493
+ * The file is sent only as the user who queued it. Throws
494
+ * `UploadQueueFullError` past `maxQueuedUploadBytes`. Pending and failed
495
+ * uploads show in `outbox()` with kind `"upload"`; retry and dismiss them
496
+ * with `retryWrite` / `dismissWrite`.
497
+ */
498
+ queueUpload(file: Blob, opts: {
499
+ attach: UploadAttachTarget;
500
+ filename?: string;
501
+ contentType?: string;
502
+ visibility?: FileVisibility;
503
+ }): Promise<QueuedUploadHandle>;
504
+ /** An object URL for a queued file (`local:<localId>` without the
505
+ * prefix), or null when no queued upload has that id. */
506
+ uploadLocalUrl(localId: string): string | null;
507
+ /** Apply the queued writes to the replica again after a reload. An
508
+ * optimistic change lives only in memory, so without this a row
509
+ * written offline vanished from the screen on reload (it was still
510
+ * queued and still reached the server later). Oldest first, so an
511
+ * update of a row inserted offline finds the row. */
512
+ private reapplyPendingWrites;
402
513
  /**
403
514
  * Mutate connection status + notify subscribers. Idempotent — same-
404
515
  * status calls are a no-op so the WS onopen → connected transition
@@ -500,6 +611,9 @@ export declare class SyncEngine {
500
611
  * push whose current token differs holds owned writes: the token may
501
612
  * belong to someone else. `undefined` until a session is committed. */
502
613
  private sessionToken;
614
+ /** The `/api/auth/me` request in flight, and the token it was sent with.
615
+ * `refreshSessionIfStale` joins it instead of sending another. */
616
+ private sessionFetch;
503
617
  private identityRetryTimer;
504
618
  /** Delayed push / pull retries scheduled through `later()`. `stop()`
505
619
  * clears them, so a stopped engine makes no further requests. */
@@ -616,6 +730,21 @@ export declare class SyncEngine {
616
730
  * queue. */
617
731
  pull(): Promise<void>;
618
732
  private pullInner;
733
+ /** Rows a reconcile fetches per entity, at most. */
734
+ private static readonly RECONCILE_ROW_CAP;
735
+ /** The server's page maximum for replication reads (`?sync=1`). */
736
+ private static readonly MAX_RECONCILE_PAGE_ROWS;
737
+ /** Rows the first page of a pull asks for. Later pages double. */
738
+ private static readonly FIRST_PULL_PAGE_ROWS;
739
+ /** The server's page maximum (MAX_PAGE_ROWS in routes/sync.rs). */
740
+ private static readonly MAX_PULL_PAGE_ROWS;
741
+ /**
742
+ * After a complete snapshot: delete local rows of `entities` that the
743
+ * snapshot did not send. Rows with writes not yet on the server are kept.
744
+ * The deletes are tombstoned at the snapshot's seq, so an older event
745
+ * cannot bring a row back.
746
+ */
747
+ private sweepAfterSnapshot;
619
748
  /** Consecutive 410 RESYNC_REQUIRED responses since the last successful
620
749
  * pull. Used by the circuit breaker in pull() to bound the retry
621
750
  * storm against a misconfigured server. Resets to 0 on any pull
@@ -636,6 +765,30 @@ export declare class SyncEngine {
636
765
  * cleared on read so a subsequent reconnect-after-disconnect still
637
766
  * runs reconcile normally. */
638
767
  private lastPullStartedFromZero;
768
+ /** When the replica last matched the server: a complete snapshot
769
+ * (local rows it did not hold deleted) or a full reconcile. 0 = never
770
+ * this run. */
771
+ private matchedServerAt;
772
+ /** The replica epoch a complete snapshot filled. An entity with no
773
+ * rows in it is empty on the server too. */
774
+ private snapshotEpoch;
775
+ /** Whether an automatic sweep (connect, visibility) should run. */
776
+ /** The connection dropped (or this run started from a cached replica)
777
+ * since the replica last matched the server. */
778
+ private offlineSinceMatch;
779
+ /** A snapshot replaced cached rows without saying which entities it
780
+ * covers, so rows deleted on the server may still be here. */
781
+ private sweepNeeded;
782
+ /**
783
+ * Whether an automatic sweep (connect, visibility) should run: after a
784
+ * snapshot that could not sweep its own deletes, or when the app was
785
+ * offline since the replica last matched the server and the last match
786
+ * is `autoReconcileIntervalMs` old. Never right after a complete
787
+ * snapshot, and not merely because the app came to the foreground.
788
+ */
789
+ private autoReconcileDue;
790
+ /** The replica matches the server now. */
791
+ private markMatchedServer;
639
792
  /** Timestamp of the last `reconcile()` invocation. Used to debounce —
640
793
  * reconcile runs on connect, WS reconnect, AND visibility-change, so
641
794
  * a quick tab-flick after a normal reconnect shouldn't refetch every
@@ -709,6 +862,11 @@ export declare class SyncEngine {
709
862
  * entities where the client missed an insert get the row back.
710
863
  */
711
864
  observeEntity(entity: string): void;
865
+ /** Whether an entity with no local rows may still have rows on the
866
+ * server that no pull will deliver. Not when a snapshot is coming (the
867
+ * cursor is 0 and no pull has completed since the last reset) or one
868
+ * already filled this replica: a snapshot sends every visible row. */
869
+ private emptyMayBeStale;
712
870
  reconcile(entities?: string[]): Promise<void>;
713
871
  /** Entities to reconcile that have not reconciled yet: named by a server
714
872
  * hint (`sync-reconcile`), or skipped by an earlier reconcile. */
@@ -790,6 +948,7 @@ export declare class SyncEngine {
790
948
  * will retry. Errors must not abort bootstrap.
791
949
  */
792
950
  private fetchSessionBootstrap;
951
+ private fetchSession;
793
952
  /**
794
953
  * A stored token that /api/auth/me resolves to no user is either expired
795
954
  * or — the common footgun — a FOREIGN token: two Pylon apps sharing an
@@ -868,6 +1027,16 @@ export declare class SyncEngine {
868
1027
  * before the WS round-trip lands.
869
1028
  */
870
1029
  notifySessionChanged(): Promise<void>;
1030
+ /**
1031
+ * Refresh the session only when it is not known for the current token:
1032
+ * no answer yet this run, or the token changed since the last answer.
1033
+ * A request already in flight for the token is joined, not repeated.
1034
+ * `useSession` calls this on mount, so a screen with many session
1035
+ * readers makes at most one `/api/auth/me` request per resolve.
1036
+ * After a server-side session change, use `notifySessionChanged`,
1037
+ * which always fetches.
1038
+ */
1039
+ refreshSessionIfStale(): Promise<void>;
871
1040
  /**
872
1041
  * Drop a row from the local replica because the server signaled
873
1042
  * that the current subscriber's read policy was revoked for it.
@@ -51,6 +51,10 @@ export interface MultiTabOrchestratorHooks {
51
51
  /** Leader → followers: the leader's push failed transiently and
52
52
  * these op ids stay queued for retry. */
53
53
  onMutationsQueued(opIds: string[]): void;
54
+ /** Another tab retried or dismissed these failed writes. */
55
+ onMutationsRemoved(opIds: string[]): void;
56
+ /** Another tab changed the upload queue in storage. */
57
+ onUploadsChanged(): void;
54
58
  /** Leader → follower: a binary frame from the WS. Engine routes
55
59
  * to its local binary handlers. */
56
60
  onBinaryReceived(bytes: Uint8Array): void;
@@ -29,6 +29,9 @@ export interface PendingMutation {
29
29
  error?: string;
30
30
  /** Server error code for a failed mutation, when the server sent one. */
31
31
  errorCode?: string;
32
+ /** When the write was made, in ms since the epoch. Missing on writes
33
+ * queued by an older client; those sort first. */
34
+ createdAt?: number;
32
35
  /** User id the write was made as (`null` = anonymous). Persisted with
33
36
  * the queue so a write is only ever pushed as the user who made it.
34
37
  * `undefined` = unknown (queued by an older client). */
@@ -44,6 +47,32 @@ export interface PendingMutation {
44
47
  * before the mutation; `undefined` = not captured (inserts). */
45
48
  prevRow?: Row | null;
46
49
  }
50
+ /** One write as the outbox shows it. */
51
+ export interface OutboxItem {
52
+ /** Op id. Pass it to `retryWrite` / `dismissWrite`. */
53
+ id: string;
54
+ entity: string;
55
+ rowId: string;
56
+ /** `"upload"` is a file from `queueUpload`; `entity`/`rowId` is the
57
+ * record it attaches to. */
58
+ kind: "insert" | "update" | "delete" | "upload";
59
+ /** The fields the write sets. Empty for a delete. For an upload:
60
+ * `field`, `urlField`, `filename`, `contentType`, `size`. */
61
+ data: Row;
62
+ /** When the write was made, in ms since the epoch (0 when unknown). */
63
+ createdAt: number;
64
+ /** The server's message, for a failed write. */
65
+ error?: string;
66
+ /** The server's error code (`POLICY_DENIED`, `VALIDATION_FAILED`, ...),
67
+ * for a failed write. */
68
+ errorCode?: string;
69
+ }
70
+ /** Writes not yet on the server, and writes the server rejected. */
71
+ export interface Outbox {
72
+ pending: OutboxItem[];
73
+ failed: OutboxItem[];
74
+ }
75
+ export declare function toOutboxItem(m: PendingMutation): OutboxItem;
47
76
  /**
48
77
  * Optional persistence backend. The default IndexedDB persistence
49
78
  * layer supports incremental writes with `saveChanges`. Custom backends
@@ -61,7 +90,18 @@ export declare class MutationQueue {
61
90
  /** Callers waiting for a mutation's outcome, by op id. Memory-only:
62
91
  * a promise cannot outlive the page that created it. */
63
92
  private waiters;
93
+ /** Called after every change to what the queue holds or to a write's
94
+ * status. The engine turns it into a store notification so the outbox
95
+ * re-renders. */
96
+ private changeListener;
97
+ /** Bumped on every change; lets readers cache a snapshot. */
98
+ private _version;
64
99
  constructor(persistence?: MutationQueuePersistence);
100
+ /** Set the one listener called after each queue change. */
101
+ setChangeListener(listener: (() => void) | null): void;
102
+ /** Changes so far. Equal versions mean the same contents. */
103
+ version(): number;
104
+ private changed;
65
105
  /**
66
106
  * Attach a persistence backend after construction. The SyncEngine
67
107
  * swaps in IndexedDB-backed persistence once the DB has opened.
@@ -79,8 +119,27 @@ export declare class MutationQueue {
79
119
  * entry with the same op_id is already queued — a follower
80
120
  * retrying its forward of the same op shouldn't double-queue on
81
121
  * the leader. */
82
- add(change: ClientChange, prevRow?: Row | null, owner?: string | null, ownerPending?: boolean): string;
122
+ add(change: ClientChange, prevRow?: Row | null, owner?: string | null, ownerPending?: boolean, createdAt?: number): string;
83
123
  pending(): PendingMutation[];
124
+ /** Writes the server rejected, oldest first. They stay until the user
125
+ * retries or dismisses them. */
126
+ failed(): PendingMutation[];
127
+ /**
128
+ * Queue a failed write again under a new op id, with `data` (when
129
+ * given) merged over the original change's fields. The failed entry is
130
+ * removed in the same persistence write, so a reload never sees both.
131
+ * The new write keeps the original's owner and `createdAt`. `prevRow`
132
+ * is the row before the caller re-applied the write locally, for the
133
+ * rollback if the server rejects it again. Returns the new op id.
134
+ *
135
+ * The server forgets a rejected op id (it never applied it), so the new
136
+ * id cannot be applied alongside the old one.
137
+ */
138
+ retry(id: string, data: Row | undefined, prevRow: Row | null | undefined): string;
139
+ /** Drop a failed write after the user has seen it. Returns false when
140
+ * there is no failed write with that id (pending writes cannot be
141
+ * dismissed: they may already be on the server). */
142
+ dismiss(id: string): boolean;
84
143
  /** Look up a queued mutation by op id (any status). Used by the
85
144
  * follower's mutations-failed handler to reach the captured
86
145
  * `prevRow` for rollback. */
@@ -145,3 +204,5 @@ export declare class MutationQueue {
145
204
  /** Fire-and-forget persistence write. */
146
205
  private flush;
147
206
  }
207
+ /** Oldest first; writes without `createdAt` (older clients) first of all. */
208
+ export declare function byCreatedAt(list: PendingMutation[]): PendingMutation[];
@@ -28,6 +28,8 @@ export interface CreateTestEnvOptions {
28
28
  * wall-clock waiting on every reconnect scenario; pass a small
29
29
  * value when the test specifically exercises the reconnect path. */
30
30
  reconnectDelay?: number;
31
+ /** Passed through to the engine config (SyncEngineConfig). */
32
+ autoReconcileIntervalMs?: number;
31
33
  }
32
34
  export interface TestEnv {
33
35
  server: TestServer;
@@ -129,10 +129,16 @@ export declare class TestServer {
129
129
  /** When set, `/api/sync/push` waits for this promise before it
130
130
  * answers. Lets a test queue a write while a push is in flight. */
131
131
  pushGate: Promise<void> | null;
132
+ /** The file endpoints (`/api/files/init`, PUT, `/api/files/confirm`). */
133
+ readonly files: TestFileServer;
132
134
  /** Entities whose pushed writes are rejected per op, like a write
133
135
  * policy denial on the real server (HTTP 200 with a per-op error). */
134
136
  private deniedWrites;
135
137
  denyWrites(entity: string, code?: string, message?: string): void;
138
+ /** Reject a pushed insert/update of `entity` whose fields match
139
+ * `predicate`, like a write policy that checks a field value. */
140
+ private conditionalDenials;
141
+ rejectWritesWhere(entity: string, predicate: (data: Row) => boolean, code?: string, message?: string): void;
136
142
  private nextPushOutcome;
137
143
  primeNextPushOutcome(o: {
138
144
  kind: "network";
@@ -205,6 +211,26 @@ export declare class TestServer {
205
211
  * the response the engine sees, matching what a real server does
206
212
  * when the session mutates mid-request. */
207
213
  listEntityRows(entity: string, token: string | undefined): Promise<Row[]>;
214
+ /** When true, /api/sync/pull answers like the real server's snapshot
215
+ * path: a pull from 0 (or a delta pull with `snapshot_ok=1` and a cursor
216
+ * `farBehind` accepts) gets the current visible rows as insert events,
217
+ * paged by `limit`, flagged `snapshot`, with `snapshot_entities` on the
218
+ * last page. Off by default: older tests expect the log replay. */
219
+ snapshotMode: boolean;
220
+ /** In `snapshotMode`: delta cursors answered with a snapshot. */
221
+ farBehind: (since: number) => boolean;
222
+ /** In `snapshotMode`: leave `snapshot_entities` off the last page, like a
223
+ * server from before it was added. */
224
+ omitSnapshotEntities: boolean;
225
+ /** One snapshot page for `snapshotMode`. `offset` is the continuation. */
226
+ snapshotPage(token: string | undefined, offset: number, limit: number): {
227
+ changes: ChangeEvent[];
228
+ cursor: SyncCursor;
229
+ has_more: boolean;
230
+ snapshot: true;
231
+ snapshot_after: string | null;
232
+ snapshot_entities?: string[];
233
+ };
208
234
  /** /api/sync/pull — every visible change since `since`. */
209
235
  pull(token: string | undefined, since: number): Promise<{
210
236
  changes: ChangeEvent[];
@@ -223,3 +249,81 @@ export declare class TestServer {
223
249
  private bumpSeq;
224
250
  private appendLog;
225
251
  }
252
+ /**
253
+ * In-memory stand-in for the runtime's file routes. Records every request
254
+ * so a test can count uploads, and lets a test refuse a file (`rejectInit`)
255
+ * or drop the connection during the next PUT (`dropNextPut`).
256
+ */
257
+ export declare class TestFileServer {
258
+ private readonly server;
259
+ private nextId;
260
+ private slots;
261
+ readonly stored: Map<string, {
262
+ filename: string;
263
+ size: number;
264
+ owner: string | null;
265
+ }>;
266
+ initCount: number;
267
+ putCount: number;
268
+ private rejection;
269
+ private dropPut;
270
+ constructor(server: TestServer);
271
+ /** Refuse every init with this status, like a type or size check. */
272
+ rejectInit(status: number, code: string, message: string): void;
273
+ clearRejection(): void;
274
+ /** When set, every PUT waits for it before it answers. */
275
+ putGate: Promise<void> | null;
276
+ /** The next PUT fails like a dropped connection (no status). */
277
+ dropNextPut(): void;
278
+ init(token: string | undefined, body: {
279
+ filename?: string;
280
+ size?: number;
281
+ }): {
282
+ status: number;
283
+ body: {
284
+ error: {
285
+ code: string;
286
+ message: string;
287
+ };
288
+ uploadUrl?: undefined;
289
+ assetId?: undefined;
290
+ };
291
+ } | {
292
+ status: number;
293
+ body: {
294
+ uploadUrl: string;
295
+ assetId: string;
296
+ error?: undefined;
297
+ };
298
+ };
299
+ put(id: string, bytes: unknown): {
300
+ status: number;
301
+ body: {
302
+ error: {
303
+ code: string;
304
+ };
305
+ };
306
+ } | {
307
+ status: number;
308
+ body: null;
309
+ };
310
+ confirm(token: string | undefined, id: string): {
311
+ status: number;
312
+ body: {
313
+ error: {
314
+ code: string;
315
+ };
316
+ id?: undefined;
317
+ url?: undefined;
318
+ size?: undefined;
319
+ };
320
+ } | {
321
+ status: number;
322
+ body: {
323
+ id: string;
324
+ url: string;
325
+ size: number;
326
+ error?: undefined;
327
+ };
328
+ };
329
+ }
package/dist/types.d.ts CHANGED
@@ -13,6 +13,16 @@ export interface PullResponse {
13
13
  changes: ChangeEvent[];
14
14
  cursor: SyncCursor;
15
15
  has_more: boolean;
16
+ /** Set on every page of a snapshot (current rows, not the log): a pull
17
+ * from 0, or a far-behind catch-up the client accepted with
18
+ * `snapshot_ok=1`. */
19
+ snapshot?: boolean;
20
+ /** Continuation of a snapshot that spans pages. Opaque; send it back
21
+ * verbatim. */
22
+ snapshot_after?: string | null;
23
+ /** On a snapshot's last page: the entities it holds every visible row
24
+ * of. Rows of these entities that it did not send are gone. */
25
+ snapshot_entities?: string[];
16
26
  }
17
27
  /**
18
28
  * Server-resolved auth/session state. Shape mirrors what `/api/auth/me`