@pylonsync/sync 0.22.47 → 0.22.50

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,13 @@ 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";
12
17
  export { generateId } from "./ids";
18
+ export { uploadBytes, type FileVisibility, type UploadBytesOptions, type UploadedFile, } from "./upload";
19
+ 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
20
  export type { ChangeEvent, ClientChange, PullResponse, PushOpResult, PushResponse, ReactiveMessage, ReactiveSpec, ResolvedSession, Row, SyncConnectionStatus, SyncCursor, TransportType, } from "./types";
14
21
  export { defaultStorage, createWriteThroughStorage, type Storage, } from "./storage";
15
22
  import type { ReactiveMessage, ResolvedSession, Row, SyncConnectionStatus, SyncCursor, TransportType } from "./types";
@@ -105,6 +112,9 @@ export interface SyncEngineConfig {
105
112
  * its own leader (the pre-multi-tab semantics).
106
113
  */
107
114
  multiTab?: boolean;
115
+ /** Cap on the bytes `queueUpload` keeps on the device. Default 200 MB.
116
+ * Past it `queueUpload` throws `UploadQueueFullError`. */
117
+ maxQueuedUploadBytes?: number;
108
118
  }
109
119
  export declare class SyncEngine {
110
120
  private config;
@@ -212,6 +222,8 @@ export declare class SyncEngine {
212
222
  private persistDegraded;
213
223
  readonly store: LocalStore;
214
224
  readonly mutations: MutationQueue;
225
+ /** Files queued with `queueUpload`, sent when there is a network. */
226
+ readonly uploads: UploadManager;
215
227
  /**
216
228
  * Stable per-client identifier. Minted on first construction, not
217
229
  * necessarily persisted (depends on what the host provides).
@@ -399,6 +411,65 @@ export declare class SyncEngine {
399
411
  * dev HUD's sync row; also handy for a "saving…" indicator.
400
412
  */
401
413
  pendingCount(): number;
414
+ private outboxCache;
415
+ /**
416
+ * Writes made here that the server has not applied yet (`pending`:
417
+ * offline, or waiting for a sign-in), and writes the server rejected
418
+ * (`failed`, with its error). Oldest first. The same object is
419
+ * returned until the queue changes, so it can back
420
+ * `useSyncExternalStore`. Changes notify store subscribers on
421
+ * `OUTBOX_CHANNEL`.
422
+ */
423
+ outbox(): Outbox;
424
+ /**
425
+ * Send a failed write again, with `data` (when given) merged over the
426
+ * fields it set. The write is applied locally again at once (the
427
+ * rejection rolled it back) and queued under a new op id. Settles like
428
+ * `insert`: resolves with the new op id once the server applies it or
429
+ * it is queued offline, and rejects with a `MutationRejectedError` when
430
+ * the server rejects it again (it is then failed again, with the new
431
+ * error).
432
+ *
433
+ * A write is only ever sent as the user who made it. Throws
434
+ * `MutationRejectedError` with code `OWNER_MISMATCH` when someone else
435
+ * is signed in, and `OWNER_UNKNOWN` when it is not yet known who is
436
+ * signed in (or the write predates owner tracking). Dismiss those.
437
+ */
438
+ retryWrite(id: string, data?: Row): Promise<string>;
439
+ /** Drop a failed write after the user has seen it. Its local change
440
+ * was already rolled back. Returns false when there is no failed write
441
+ * with that id; a pending write cannot be dismissed. */
442
+ dismissWrite(id: string): boolean;
443
+ /**
444
+ * Keep a file on the device and upload it when there is a network, then
445
+ * set it on the record it belongs to. Returns at once.
446
+ *
447
+ * At once, `attach.entity`/`attach.id` gets `attach.field` (and
448
+ * `attach.urlField`, when given) = `local:<localId>` as a queued write,
449
+ * so the record shows the file from the device (`uploadLocalUrl`). When
450
+ * the upload finishes, a second queued write sets the file's id (and
451
+ * URL). Both are ordinary writes: policies and owner rules apply.
452
+ *
453
+ * The file is sent only as the user who queued it. Throws
454
+ * `UploadQueueFullError` past `maxQueuedUploadBytes`. Pending and failed
455
+ * uploads show in `outbox()` with kind `"upload"`; retry and dismiss them
456
+ * with `retryWrite` / `dismissWrite`.
457
+ */
458
+ queueUpload(file: Blob, opts: {
459
+ attach: UploadAttachTarget;
460
+ filename?: string;
461
+ contentType?: string;
462
+ visibility?: FileVisibility;
463
+ }): Promise<QueuedUploadHandle>;
464
+ /** An object URL for a queued file (`local:<localId>` without the
465
+ * prefix), or null when no queued upload has that id. */
466
+ uploadLocalUrl(localId: string): string | null;
467
+ /** Apply the queued writes to the replica again after a reload. An
468
+ * optimistic change lives only in memory, so without this a row
469
+ * written offline vanished from the screen on reload (it was still
470
+ * queued and still reached the server later). Oldest first, so an
471
+ * update of a row inserted offline finds the row. */
472
+ private reapplyPendingWrites;
402
473
  /**
403
474
  * Mutate connection status + notify subscribers. Idempotent — same-
404
475
  * status calls are a no-op so the WS onopen → connected transition
@@ -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[];
@@ -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";
@@ -223,3 +229,81 @@ export declare class TestServer {
223
229
  private bumpSeq;
224
230
  private appendLog;
225
231
  }
232
+ /**
233
+ * In-memory stand-in for the runtime's file routes. Records every request
234
+ * so a test can count uploads, and lets a test refuse a file (`rejectInit`)
235
+ * or drop the connection during the next PUT (`dropNextPut`).
236
+ */
237
+ export declare class TestFileServer {
238
+ private readonly server;
239
+ private nextId;
240
+ private slots;
241
+ readonly stored: Map<string, {
242
+ filename: string;
243
+ size: number;
244
+ owner: string | null;
245
+ }>;
246
+ initCount: number;
247
+ putCount: number;
248
+ private rejection;
249
+ private dropPut;
250
+ constructor(server: TestServer);
251
+ /** Refuse every init with this status, like a type or size check. */
252
+ rejectInit(status: number, code: string, message: string): void;
253
+ clearRejection(): void;
254
+ /** When set, every PUT waits for it before it answers. */
255
+ putGate: Promise<void> | null;
256
+ /** The next PUT fails like a dropped connection (no status). */
257
+ dropNextPut(): void;
258
+ init(token: string | undefined, body: {
259
+ filename?: string;
260
+ size?: number;
261
+ }): {
262
+ status: number;
263
+ body: {
264
+ error: {
265
+ code: string;
266
+ message: string;
267
+ };
268
+ uploadUrl?: undefined;
269
+ assetId?: undefined;
270
+ };
271
+ } | {
272
+ status: number;
273
+ body: {
274
+ uploadUrl: string;
275
+ assetId: string;
276
+ error?: undefined;
277
+ };
278
+ };
279
+ put(id: string, bytes: unknown): {
280
+ status: number;
281
+ body: {
282
+ error: {
283
+ code: string;
284
+ };
285
+ };
286
+ } | {
287
+ status: number;
288
+ body: null;
289
+ };
290
+ confirm(token: string | undefined, id: string): {
291
+ status: number;
292
+ body: {
293
+ error: {
294
+ code: string;
295
+ };
296
+ id?: undefined;
297
+ url?: undefined;
298
+ size?: undefined;
299
+ };
300
+ } | {
301
+ status: number;
302
+ body: {
303
+ id: string;
304
+ url: string;
305
+ size: number;
306
+ error?: undefined;
307
+ };
308
+ };
309
+ }
@@ -0,0 +1,190 @@
1
+ import type { OutboxItem } from "./mutation-queue";
2
+ import type { TransportConfig } from "./transport";
3
+ import { type FileVisibility, type UploadedFile } from "./upload";
4
+ /** Where an upload is attached when it finishes: `entity`/`id` gets
5
+ * `field` = the file id, and `urlField` (when set) = the file URL. */
6
+ export interface UploadAttachTarget {
7
+ entity: string;
8
+ id: string;
9
+ field: string;
10
+ urlField?: string;
11
+ }
12
+ /** One queued upload, as stored. */
13
+ export interface QueuedUpload {
14
+ /** `upl_...`. The record's field holds `local:<localId>` until the
15
+ * upload finishes. */
16
+ localId: string;
17
+ blob: Blob;
18
+ filename: string;
19
+ contentType: string;
20
+ size: number;
21
+ visibility: FileVisibility;
22
+ /** User id the upload was made as (`null` = anonymous). */
23
+ owner: string | null;
24
+ /** Made before this run knew who is signed in; stamped by the first
25
+ * resolved session. */
26
+ ownerPending?: boolean;
27
+ attach: UploadAttachTarget;
28
+ createdAt: number;
29
+ status: "pending" | "failed";
30
+ error?: string;
31
+ errorCode?: string;
32
+ /** Set once the server confirmed the file, before the attach update is
33
+ * queued. A restart in between attaches without uploading again. */
34
+ uploaded?: UploadedFile;
35
+ }
36
+ /** Persistence for queued uploads. */
37
+ export interface UploadStore {
38
+ put(record: QueuedUpload): Promise<void>;
39
+ delete(localId: string): Promise<void>;
40
+ list(): Promise<QueuedUpload[]>;
41
+ }
42
+ /** The prefix of a field value that names a queued upload. */
43
+ export declare const LOCAL_FILE_PREFIX = "local:";
44
+ /** `local:<localId>` → `<localId>`; null for any other value. */
45
+ export declare function localUploadId(ref: unknown): string | null;
46
+ /** Default cap on the bytes waiting in the queue: 200 MB. */
47
+ export declare const DEFAULT_MAX_QUEUED_UPLOAD_BYTES: number;
48
+ /** Thrown by `queueUpload` when the file would take the queue past its
49
+ * size limit. Past the limit the browser may evict the app's storage,
50
+ * which would lose every queued file, so the queue refuses instead. */
51
+ export declare class UploadQueueFullError extends Error {
52
+ readonly code = "UPLOAD_QUEUE_FULL";
53
+ readonly limitBytes: number;
54
+ readonly queuedBytes: number;
55
+ constructor(limitBytes: number, queuedBytes: number, fileBytes: number);
56
+ }
57
+ export declare class MemoryUploadStore implements UploadStore {
58
+ private rows;
59
+ put(record: QueuedUpload): Promise<void>;
60
+ delete(localId: string): Promise<void>;
61
+ list(): Promise<QueuedUpload[]>;
62
+ }
63
+ /** Queued uploads in IndexedDB database `pylon_uploads_<appName>`. */
64
+ export declare class IndexedDBUploadStore implements UploadStore {
65
+ private readonly appName;
66
+ private db;
67
+ constructor(appName?: string);
68
+ private open;
69
+ private run;
70
+ put(record: QueuedUpload): Promise<void>;
71
+ delete(localId: string): Promise<void>;
72
+ list(): Promise<QueuedUpload[]>;
73
+ /** Close the connection (tests; a deleteDatabase waits for it). */
74
+ close(): Promise<void>;
75
+ }
76
+ /** What the queue does with an upload, by its owner and who is signed in. */
77
+ export type UploadOwnerVerdict = "send" | "hold" | "discard";
78
+ /**
79
+ * The owner rule for uploads, the same as for queued writes: send when the
80
+ * upload's owner is signed in (or it was a guest's and that guest signed in
81
+ * to an account), hold while nobody or nobody known is signed in, discard
82
+ * when another user is signed in.
83
+ */
84
+ export declare function uploadOwnerVerdict(owner: string | null, current: string | null | undefined): UploadOwnerVerdict;
85
+ /** Whether an upload error is worth trying again later: no network, a
86
+ * server error, a timeout, or a rate limit. Anything else (the type or
87
+ * size was refused, a policy) is the server's answer. */
88
+ export declare function isTransientUploadError(err: unknown): boolean;
89
+ /** One queued upload as the outbox shows it. */
90
+ export declare function uploadOutboxItem(r: QueuedUpload): OutboxItem;
91
+ export declare function newUploadId(): string;
92
+ /** What `queueUpload` returns. */
93
+ export interface QueuedUploadHandle {
94
+ localId: string;
95
+ /** `local:<localId>`: the value the record's field holds until the
96
+ * upload finishes. */
97
+ ref: string;
98
+ /** An object URL of the file, to show it at once. Null where the
99
+ * platform has no `URL.createObjectURL`. */
100
+ localUrl: string | null;
101
+ /** Resolves with the uploaded file once it is attached; rejects with an
102
+ * `UploadRejectedError` when the server refuses it or another user is
103
+ * signed in. A page that reloads loses this promise, not the upload. */
104
+ done: Promise<UploadedFile>;
105
+ }
106
+ /** Why a queued upload was not sent. `code` is the server's error code
107
+ * (`FILE_TOO_LARGE`, `POLICY_DENIED`, ...), or `DISCARDED` when another
108
+ * user is signed in. */
109
+ export declare class UploadRejectedError extends Error {
110
+ readonly code: string;
111
+ readonly localId: string;
112
+ constructor(message: string, code: string, localId: string);
113
+ }
114
+ /** What the upload manager needs from the sync engine. */
115
+ export interface UploadManagerHost {
116
+ transport(): TransportConfig;
117
+ /** The signed-in user (`null` = anonymous), or `undefined` while it is
118
+ * not known (no answer from the server yet this run, or a token change
119
+ * in flight). */
120
+ currentOwner(): string | null | undefined;
121
+ /** The user a new upload belongs to; `undefined` = not known yet. */
122
+ ownerForNewUpload(): string | null | undefined;
123
+ /** Only the leader tab uploads. */
124
+ isLeader(): boolean;
125
+ isRunning(): boolean;
126
+ /** Queue an ordinary write that sets fields on a record. */
127
+ update(entity: string, id: string, patch: Record<string, unknown>): void;
128
+ /** The outbox changed. */
129
+ changed(): void;
130
+ /** Tell other tabs to reload the queue. */
131
+ broadcast(): void;
132
+ /** Run `fn` after `ms` unless the engine stops. */
133
+ later(ms: number, fn: () => void): void;
134
+ }
135
+ /**
136
+ * The offline upload queue: storage, the one-at-a-time upload loop, the
137
+ * owner rule, the size limit, and the outbox entries.
138
+ */
139
+ export declare class UploadManager {
140
+ private readonly host;
141
+ private readonly maxBytes;
142
+ private store;
143
+ private records;
144
+ private objectUrls;
145
+ private waiters;
146
+ private running;
147
+ private failureCount;
148
+ private retryScheduled;
149
+ private _version;
150
+ constructor(host: UploadManagerHost, maxBytes?: number);
151
+ /** Bumped on every change to the queue. */
152
+ version(): number;
153
+ private changed;
154
+ /** Swap in persistent storage and load what it holds. */
155
+ attachStore(store: UploadStore): Promise<void>;
156
+ /** Re-read the queue from storage (another tab changed it). */
157
+ reload(): Promise<void>;
158
+ /** Queued uploads, oldest first. */
159
+ list(): QueuedUpload[];
160
+ get(localId: string): QueuedUpload | undefined;
161
+ /** Bytes waiting on the device. */
162
+ queuedBytes(): number;
163
+ enqueue(blob: Blob, opts: {
164
+ attach: UploadAttachTarget;
165
+ filename?: string;
166
+ contentType?: string;
167
+ visibility?: FileVisibility;
168
+ }): Promise<QueuedUploadHandle>;
169
+ /** An object URL for a queued file, or null. */
170
+ localUrl(localId: string): string | null;
171
+ private revoke;
172
+ private settle;
173
+ private remove;
174
+ /** Stamp uploads made before anyone was known to be signed in. */
175
+ stampPendingOwner(owner: string | null): Promise<void>;
176
+ /**
177
+ * Upload every queued file the signed-in user owns, oldest first, one at
178
+ * a time. Stops at the first error that says the network is down and
179
+ * tries again later (and on the next reconnect). A file the server
180
+ * refuses is marked failed and not tried again until `retry`.
181
+ */
182
+ process(): Promise<void>;
183
+ private scheduleRetry;
184
+ /** Queue a failed upload again. Throws `UploadRejectedError` with code
185
+ * `OWNER_MISMATCH` / `OWNER_UNKNOWN` as `retryWrite` does. */
186
+ retry(localId: string): Promise<void>;
187
+ /** Drop a failed upload and its bytes. False when there is no failed
188
+ * upload with that id. The record keeps its `local:` value. */
189
+ dismiss(localId: string): boolean;
190
+ }
@@ -0,0 +1,37 @@
1
+ import { type TransportConfig } from "./transport";
2
+ export interface UploadedFile {
3
+ id: string;
4
+ /** Where to read the file. Store this on your row. */
5
+ url: string;
6
+ size: number;
7
+ }
8
+ /**
9
+ * Who can read an uploaded file through `url`.
10
+ * - "private" (default): only the uploader.
11
+ * - "public": anyone, signed in or not. Use it for content every visitor
12
+ * sees, such as feed photos or listing images.
13
+ * Writes and deletes stay with the uploader either way. On a CDN-backed
14
+ * store (Stack0, a public bucket) every file is served by its URL, so the
15
+ * setting only changes reads that go through `/api/files/<id>`.
16
+ */
17
+ export type FileVisibility = "private" | "public";
18
+ export interface UploadBytesOptions {
19
+ filename?: string;
20
+ contentType?: string;
21
+ visibility?: FileVisibility;
22
+ }
23
+ /**
24
+ * Upload a file (File, Blob, or raw bytes) in three steps:
25
+ * 1. POST /api/files/init with the name, type, size, and visibility;
26
+ * 2. PUT the bytes to the returned upload URL;
27
+ * 3. POST /api/files/confirm, which returns `{ id, url, size }`.
28
+ *
29
+ * The upload URL is either pylon's own receiver (local disk), which needs
30
+ * the session like any API call, or a presigned storage URL, whose
31
+ * signature is the credential. The session is never sent to a presigned
32
+ * URL on another origin.
33
+ *
34
+ * Throws `PylonHttpError` (with `status`) when the server answers with an
35
+ * error, and the fetch error when there is no network.
36
+ */
37
+ export declare function uploadBytes(transport: TransportConfig, input: File | Blob | ArrayBuffer | Uint8Array, options?: UploadBytesOptions): Promise<UploadedFile>;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.22.47",
6
+ "version": "0.22.50",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",