stitchkit 0.101.0 → 0.101.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entrypoints/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,EAC9B,+BAA+B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,KAAK,wBAAwB,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,GAC9B,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,WAAW,EACX,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,mBAAmB,EACnB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKvD,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGzF,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,wBAAwB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/entrypoints/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,cAAc,GACf,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,GACtB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,KAAK,2BAA2B,EAChC,KAAK,yBAAyB,EAC9B,+BAA+B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,KAAK,wBAAwB,GAC9B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,GAC9B,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,WAAW,EACX,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,mBAAmB,EACnB,aAAa,GACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAKvD,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGzF,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,wBAAwB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  ApiError2,
10
10
  createHttpClient2
11
- } from "../index-qjeggp8w.js";
11
+ } from "../index-1h6gtdrj.js";
12
12
  import"../index-m38ej7e5.js";
13
13
  import {
14
14
  DEFAULT_CONTRACT_STREAM_FRAME_BYTES2,
@@ -24,7 +24,7 @@ import {
24
24
  createUrlBuilder2,
25
25
  createUrlBuilders2,
26
26
  createScopedUrlBuilders2
27
- } from "../index-aaj97r4y.js";
27
+ } from "../index-d82d3xg4.js";
28
28
  import"../index-w0eqdhyd.js";
29
29
  import {
30
30
  LiveStatePhaseSchema2,
@@ -95,6 +95,98 @@ import {
95
95
  zodIssues2,
96
96
  formatZodError2
97
97
  } from "../index-1eeqpadm.js";
98
+
99
+ // src/browser/chunked-upload.ts
100
+ function isRetryableUploadFailure(error) {
101
+ if (ApiError2.is(error)) {
102
+ if (error.status === 0)
103
+ return error.code === "UNKNOWN_ERROR" || error.code === "REQUEST_TIMEOUT";
104
+ return error.status === 429 || error.status >= 500;
105
+ }
106
+ return error instanceof TypeError;
107
+ }
108
+ async function uploadInChunks(config) {
109
+ const { file, chunkBytes, signal } = config;
110
+ if (!Number.isSafeInteger(chunkBytes) || chunkBytes <= 0) {
111
+ throw new TypeError("uploadInChunks: chunkBytes must be a positive integer");
112
+ }
113
+ if (!Number.isSafeInteger(file.size) || file.size < 0) {
114
+ throw new TypeError("uploadInChunks: the file size must be a whole number of bytes");
115
+ }
116
+ if (file.size === 0)
117
+ throw new TypeError("uploadInChunks: the file is empty");
118
+ const totalBytes = file.size;
119
+ const chunkCount = Math.ceil(totalBytes / chunkBytes);
120
+ const uploadId = config.uploadId ?? crypto.randomUUID();
121
+ const retries = config.retries ?? 3;
122
+ const retryDelayMs = config.retryDelayMs ?? 1000;
123
+ const report = (sentBytes, index, attempt) => {
124
+ if (!config.onProgress)
125
+ return;
126
+ try {
127
+ config.onProgress({ sentBytes, totalBytes, index, chunkCount, attempt });
128
+ } catch {}
129
+ };
130
+ throwIfAborted(signal);
131
+ const opened = await repeating(() => config.init({ uploadId, totalBytes, chunkBytes, chunkCount }, { signal }), { retries, retryDelayMs, signal });
132
+ if (opened) {
133
+ report(totalBytes, chunkCount, 1);
134
+ return opened.finished;
135
+ }
136
+ report(0, 0, 1);
137
+ for (let index = 0;index < chunkCount; index += 1) {
138
+ throwIfAborted(signal);
139
+ const start = index * chunkBytes;
140
+ const end = Math.min(totalBytes, start + chunkBytes);
141
+ const partBytes = end - start;
142
+ const bytes = file.slice(start, end);
143
+ await repeating((attempt) => config.chunk({ uploadId, index, bytes }, {
144
+ ...signal && { signal },
145
+ onUploadProgress: (progress) => report(start + fileShare(progress, partBytes), index, attempt)
146
+ }), { retries, retryDelayMs, signal });
147
+ report(end, index + 1, 1);
148
+ }
149
+ throwIfAborted(signal);
150
+ return config.finalize({ uploadId }, { signal });
151
+ }
152
+ function fileShare(progress, partBytes) {
153
+ if (progress.totalBytes === 0)
154
+ return 0;
155
+ return Math.floor(partBytes * progress.sentBytes / progress.totalBytes);
156
+ }
157
+ async function repeating(call, {
158
+ retries,
159
+ retryDelayMs,
160
+ signal
161
+ }) {
162
+ for (let attempt = 1;; attempt += 1) {
163
+ try {
164
+ return await call(attempt);
165
+ } catch (error) {
166
+ if (signal?.aborted || attempt > retries || !isRetryableUploadFailure(error))
167
+ throw error;
168
+ await pause(retryDelayMs * 2 ** (attempt - 1), signal);
169
+ }
170
+ }
171
+ }
172
+ function throwIfAborted(signal) {
173
+ if (signal?.aborted) {
174
+ throw new ApiError2("REQUEST_ABORTED", 0, undefined, "Request was aborted");
175
+ }
176
+ }
177
+ function pause(ms, signal) {
178
+ return new Promise((resolve, reject) => {
179
+ const abort = () => {
180
+ clearTimeout(timer);
181
+ reject(new ApiError2("REQUEST_ABORTED", 0, undefined, "Request was aborted"));
182
+ };
183
+ const timer = setTimeout(() => {
184
+ signal?.removeEventListener("abort", abort);
185
+ resolve();
186
+ }, ms);
187
+ signal?.addEventListener("abort", abort, { once: true });
188
+ });
189
+ }
98
190
  // src/browser/socket-io-trace.ts
99
191
  var realtimeRequestTraces = new WeakMap;
100
192
  function observeRealtimeRequestPhase(trace, phase) {
@@ -954,6 +1046,7 @@ export {
954
1046
  rateLimited2 as rateLimited,
955
1047
  resumableIterator,
956
1048
  unauthorized2 as unauthorized,
1049
+ uploadInChunks,
957
1050
  withToolView2 as withToolView,
958
1051
  zodIssues2 as zodIssues
959
1052
  };
@@ -2,7 +2,7 @@ import"../index-hby4qhax.js";
2
2
  import {
3
3
  ApiError2,
4
4
  isAbortLikeError
5
- } from "../index-qjeggp8w.js";
5
+ } from "../index-1h6gtdrj.js";
6
6
  import {
7
7
  isRecord
8
8
  } from "../index-9jfdm5tg.js";
@@ -1,12 +1,12 @@
1
1
  import"../index-hby4qhax.js";
2
2
  import {
3
3
  ApiError2
4
- } from "../index-qjeggp8w.js";
4
+ } from "../index-1h6gtdrj.js";
5
5
  import"../index-m38ej7e5.js";
6
6
  import"../index-r5c4fedv.js";
7
7
  import {
8
8
  createClient2
9
- } from "../index-aaj97r4y.js";
9
+ } from "../index-d82d3xg4.js";
10
10
  import {
11
11
  AppError2
12
12
  } from "../index-n53mswt3.js";
@@ -1,5 +1,5 @@
1
1
  import"../index-hby4qhax.js";
2
- import"../index-qjeggp8w.js";
2
+ import"../index-1h6gtdrj.js";
3
3
  import {
4
4
  joinRoutePath
5
5
  } from "../index-m38ej7e5.js";
@@ -7,7 +7,7 @@ import"../index-r5c4fedv.js";
7
7
  import {
8
8
  createClient2,
9
9
  createClients2
10
- } from "../index-aaj97r4y.js";
10
+ } from "../index-d82d3xg4.js";
11
11
  import"../index-n53mswt3.js";
12
12
  import"../index-q0th8q0z.js";
13
13
  import {
@@ -1,7 +1,7 @@
1
1
  import"../index-hby4qhax.js";
2
2
  import {
3
3
  ApiError2
4
- } from "../index-qjeggp8w.js";
4
+ } from "../index-1h6gtdrj.js";
5
5
  import"../index-m38ej7e5.js";
6
6
  import {
7
7
  defineContract2
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The server half of a chunked upload (`uploadInChunks` is the client half):
3
+ * parts land on disk under the upload's owner, each with a receipt, and come
4
+ * back in order once every part is in.
5
+ *
6
+ * - An upload is keyed by `owner` (whoever the application says sends it —
7
+ * a user, a user and a device) and the client-minted `uploadId`; the same id
8
+ * from another owner is another upload.
9
+ * - `open` is idempotent: the same declaration again is `'reopened'`, a
10
+ * different one is `UPLOAD_CONFLICT`.
11
+ * - A part's receipt is its size and sha256. The same bytes again are
12
+ * `'repeated'`; other bytes at the same index are `UPLOAD_CONFLICT`. The
13
+ * first writer wins across processes: a part's data file is named by its hash
14
+ * and its receipt is published by an exclusive link, so a loser can neither
15
+ * replace the receipt nor the bytes it names.
16
+ * - Every part but the last is exactly `chunkBytes` long.
17
+ * - `sweep` removes an upload nothing has touched for `staleAfterMs`.
18
+ *
19
+ * Refusals are `AppError`s with a 4xx status, so a handler that does not catch
20
+ * them answers the client with a typed envelope the driver does not repeat.
21
+ */
22
+ import type { ZodType } from 'zod';
23
+ export type ChunkSpoolErrorCode = 'UPLOAD_INVALID' | 'UPLOAD_TOO_LARGE' | 'UPLOAD_NOT_FOUND' | 'UPLOAD_CONFLICT' | 'UPLOAD_CHUNK_OUT_OF_RANGE' | 'UPLOAD_CHUNK_SIZE' | 'UPLOAD_INCOMPLETE';
24
+ /**
25
+ * Whether an error code is one the spool refuses with. An application that
26
+ * maps error codes onto its own must map these too — an unmapped one would
27
+ * reach its client as an unknown 500 instead of the spool's 4xx.
28
+ */
29
+ export declare function isChunkSpoolErrorCode(code: string): code is ChunkSpoolErrorCode;
30
+ /** Every code the spool refuses with. */
31
+ export declare const CHUNK_SPOOL_ERROR_CODES: readonly ChunkSpoolErrorCode[];
32
+ export interface ChunkSpoolConfig<TMeta> {
33
+ /** Where uploads in progress live; one directory per upload. */
34
+ readonly directory: string;
35
+ /** Bytes per part, the same number the client driver uses. */
36
+ readonly chunkBytes: number;
37
+ readonly maxFileBytes: number;
38
+ /** What `open` stores beside the upload — file name, type, the app's fields. */
39
+ readonly meta: ZodType<TMeta>;
40
+ /** Default 24 hours. */
41
+ readonly staleAfterMs?: number;
42
+ }
43
+ export interface ChunkSpoolKey {
44
+ readonly owner: string;
45
+ readonly uploadId: string;
46
+ }
47
+ export interface ChunkSpoolOpen<TMeta> extends ChunkSpoolKey {
48
+ readonly totalBytes: number;
49
+ readonly chunkCount: number;
50
+ readonly meta: TMeta;
51
+ }
52
+ export interface ChunkSpoolPart extends ChunkSpoolKey {
53
+ readonly index: number;
54
+ readonly bytes: Blob;
55
+ }
56
+ export interface ChunkSpoolAssembly<TMeta> {
57
+ readonly meta: TMeta;
58
+ readonly totalBytes: number;
59
+ /** The parts on disk, in order; valid until `discard`. */
60
+ readonly chunkPaths: string[];
61
+ /** The whole file, part after part. */
62
+ stream(): ReadableStream<Uint8Array>;
63
+ }
64
+ export interface ChunkSpool<TMeta> {
65
+ open(upload: ChunkSpoolOpen<TMeta>): Promise<'opened' | 'reopened'>;
66
+ put(part: ChunkSpoolPart): Promise<'stored' | 'repeated'>;
67
+ /** Every part, checked against its receipt; `UPLOAD_INCOMPLETE` names the first missing. */
68
+ assemble(key: ChunkSpoolKey): Promise<ChunkSpoolAssembly<TMeta>>;
69
+ discard(key: ChunkSpoolKey): Promise<void>;
70
+ /** Remove uploads untouched for `staleAfterMs`; resolves with how many. */
71
+ sweep(): Promise<number>;
72
+ }
73
+ export declare function createChunkSpool<TMeta>(config: ChunkSpoolConfig<TMeta>): ChunkSpool<TMeta>;
74
+ //# sourceMappingURL=chunk-spool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-spool.d.ts","sourceRoot":"","sources":["../../src/files/chunk-spool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgBH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAInC,MAAM,MAAM,mBAAmB,GAC3B,gBAAgB,GAChB,kBAAkB,GAClB,kBAAkB,GAClB,iBAAiB,GACjB,2BAA2B,GAC3B,mBAAmB,GACnB,mBAAmB,CAAC;AAYxB;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,mBAAmB,CAE/E;AAED,yCAAyC;AACzC,eAAO,MAAM,uBAAuB,EAAE,SAAS,mBAAmB,EAEjE,CAAC;AAUF,MAAM,WAAW,gBAAgB,CAAC,KAAK;IACrC,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,wBAAwB;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc,CAAC,KAAK,CAAE,SAAQ,aAAa;IAC1D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB,CAAC,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAC9B,uCAAuC;IACvC,MAAM,IAAI,cAAc,CAAC,UAAU,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,UAAU,CAAC,KAAK;IAC/B,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,QAAQ,GAAG,UAAU,CAAC,CAAC;IACpE,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,GAAG,UAAU,CAAC,CAAC;IAC1D,4FAA4F;IAC5F,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,2EAA2E;IAC3E,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1B;AAkED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CA4J1F"}
@@ -144,6 +144,207 @@ function responseTraceId(response) {
144
144
  return response?.headers.get(REQUEST_ID_HEADER) ?? undefined;
145
145
  }
146
146
 
147
+ // src/browser/upload-progress.ts
148
+ var PIECE_BYTES = 64 * 1024;
149
+ function isReactNative() {
150
+ return Reflect.get(globalThis.navigator ?? {}, "product") === "ReactNative";
151
+ }
152
+ function uploadProgressRoute(injectedTransport) {
153
+ if (isReactNative()) {
154
+ if (injectedTransport) {
155
+ throw new TypeError("onUploadProgress in React Native counts through its XMLHttpRequest and cannot count an injected fetch — drop the custom fetch for this client");
156
+ }
157
+ return "native-xhr";
158
+ }
159
+ return !injectedTransport && typeof globalThis.XMLHttpRequest === "function" ? "xhr" : "stream";
160
+ }
161
+ function reporter(listener, attempt) {
162
+ let reported = -1;
163
+ const emit = (sentBytes, totalBytes) => {
164
+ reported = sentBytes;
165
+ try {
166
+ listener({ sentBytes, totalBytes, attempt });
167
+ } catch {}
168
+ };
169
+ return (sentBytes, totalBytes) => {
170
+ if (sentBytes <= reported)
171
+ return;
172
+ if (reported < 0 && sentBytes > 0)
173
+ emit(0, totalBytes);
174
+ emit(sentBytes, totalBytes);
175
+ };
176
+ }
177
+ function withUploadProgress(transport, listener, delivery) {
178
+ let attempts = 0;
179
+ return async (input, init) => {
180
+ attempts += 1;
181
+ const report = reporter(listener, attempts);
182
+ const signal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
183
+ const credentials = init?.credentials ?? (input instanceof Request ? input.credentials : undefined);
184
+ if (delivery.route === "native-xhr") {
185
+ return sendNative(input, init, delivery.nativeBody ?? xhrBodyOf(init?.body), {
186
+ signal,
187
+ credentials,
188
+ report
189
+ });
190
+ }
191
+ const request = new Request(input, init);
192
+ const headers = new Headers(request.headers);
193
+ const bytes = new Uint8Array(await request.arrayBuffer());
194
+ const total = bytes.byteLength;
195
+ report(0, total);
196
+ if (delivery.route === "xhr") {
197
+ if (credentials === "omit") {
198
+ throw new TypeError("onUploadProgress sends through XMLHttpRequest, which cannot omit same-origin cookies — use credentials 'same-origin' or 'include'");
199
+ }
200
+ return sendThroughXhr({
201
+ method: request.method,
202
+ url: request.url,
203
+ headers,
204
+ body: total > 0 ? bytes : null,
205
+ totalBytes: total,
206
+ withCredentials: credentials === "include",
207
+ signal,
208
+ report
209
+ });
210
+ }
211
+ headers.set("content-length", String(total));
212
+ const counted = total > 0 ? { body: countedBody(bytes, (sent) => report(sent, total)), duplex: "half" } : {};
213
+ const outgoing = {
214
+ ...init,
215
+ method: request.method,
216
+ headers,
217
+ body: undefined,
218
+ ...counted,
219
+ ...signal && { signal },
220
+ ...credentials && { credentials }
221
+ };
222
+ return transport(request.url, outgoing);
223
+ };
224
+ }
225
+ function countedBody(bytes, report) {
226
+ let offset = 0;
227
+ return new ReadableStream({
228
+ pull(controller) {
229
+ if (offset >= bytes.byteLength) {
230
+ controller.close();
231
+ return;
232
+ }
233
+ const end = Math.min(offset + PIECE_BYTES, bytes.byteLength);
234
+ controller.enqueue(bytes.slice(offset, end));
235
+ offset = end;
236
+ report(offset);
237
+ }
238
+ }, { highWaterMark: 0 });
239
+ }
240
+ function sendNative(input, init, body, { signal, credentials, report }) {
241
+ const request = input instanceof Request ? input : undefined;
242
+ const headers = new Headers(request?.headers);
243
+ new Headers(init?.headers).forEach((value, name) => {
244
+ headers.set(name, value);
245
+ });
246
+ if (body instanceof FormData)
247
+ headers.delete("content-type");
248
+ const totalBytes = knownLength(body);
249
+ if (totalBytes !== undefined)
250
+ report(0, totalBytes);
251
+ return sendThroughXhr({
252
+ method: init?.method ?? request?.method ?? "GET",
253
+ url: request?.url ?? String(input),
254
+ headers,
255
+ body,
256
+ totalBytes,
257
+ ...credentials === "include" && { withCredentials: true },
258
+ ...credentials === "omit" && { withCredentials: false },
259
+ signal,
260
+ report
261
+ });
262
+ }
263
+ function xhrBodyOf(body) {
264
+ if (body instanceof ReadableStream) {
265
+ throw new TypeError("onUploadProgress in React Native cannot send a streamed body");
266
+ }
267
+ return body ?? null;
268
+ }
269
+ function knownLength(body) {
270
+ if (body === null)
271
+ return 0;
272
+ if (typeof body === "string")
273
+ return new TextEncoder().encode(body).byteLength;
274
+ if (body instanceof Blob)
275
+ return body.size;
276
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
277
+ return body.byteLength;
278
+ return;
279
+ }
280
+ var BROWSER_OWNED_HEADERS = new Set(["content-length", "host", "connection"]);
281
+ function sendThroughXhr(send) {
282
+ const { signal, report } = send;
283
+ let total = send.totalBytes;
284
+ const progress = (loaded, eventTotal) => {
285
+ if (total === undefined && eventTotal > 0)
286
+ total = eventTotal;
287
+ if (total !== undefined)
288
+ report(Math.min(loaded, total), total);
289
+ };
290
+ const finished = () => {
291
+ if (total !== undefined)
292
+ report(total, total);
293
+ };
294
+ return new Promise((resolve, reject) => {
295
+ if (signal?.aborted) {
296
+ reject(abortReason(signal));
297
+ return;
298
+ }
299
+ const xhr = new XMLHttpRequest;
300
+ xhr.open(send.method, send.url, true);
301
+ send.headers.forEach((value, name) => {
302
+ if (!BROWSER_OWNED_HEADERS.has(name))
303
+ xhr.setRequestHeader(name, value);
304
+ });
305
+ if (send.withCredentials !== undefined)
306
+ xhr.withCredentials = send.withCredentials;
307
+ xhr.responseType = "arraybuffer";
308
+ const abort = () => xhr.abort();
309
+ signal?.addEventListener("abort", abort, { once: true });
310
+ const settle = () => signal?.removeEventListener("abort", abort);
311
+ xhr.upload.onprogress = (event) => progress(event.loaded, event.total);
312
+ xhr.upload.onload = finished;
313
+ xhr.onload = () => {
314
+ settle();
315
+ if (xhr.status < 200 || xhr.status > 599) {
316
+ reject(new TypeError("Network request failed"));
317
+ return;
318
+ }
319
+ finished();
320
+ resolve(responseOf(xhr));
321
+ };
322
+ xhr.onerror = () => {
323
+ settle();
324
+ reject(new TypeError("Network request failed"));
325
+ };
326
+ xhr.onabort = () => {
327
+ settle();
328
+ reject(signal ? abortReason(signal) : new DOMException("Aborted", "AbortError"));
329
+ };
330
+ xhr.send(send.body);
331
+ });
332
+ }
333
+ function abortReason(signal) {
334
+ return signal.reason ?? new DOMException("Aborted", "AbortError");
335
+ }
336
+ var NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
337
+ function responseOf(xhr) {
338
+ const headers = new Headers;
339
+ for (const line of xhr.getAllResponseHeaders().trim().split(/[\r\n]+/)) {
340
+ const colon = line.indexOf(":");
341
+ if (colon > 0)
342
+ headers.append(line.slice(0, colon).trim(), line.slice(colon + 1).trim());
343
+ }
344
+ const body = xhr.response;
345
+ return new Response(NULL_BODY_STATUSES.has(xhr.status) || !(body instanceof ArrayBuffer) ? null : body, { status: xhr.status, statusText: xhr.statusText, headers });
346
+ }
347
+
147
348
  // src/browser/http.ts
148
349
  var API_ERROR_BRAND = Symbol.for("stitchkit.ApiError");
149
350
  function messageForCode(code, message) {
@@ -225,6 +426,18 @@ function requestFailure(error, emit) {
225
426
  const msg = error instanceof Error ? error.message : undefined;
226
427
  return new ApiError2("UNKNOWN_ERROR", status, msg ? { message: msg } : undefined, msg, undefined, responseTraceId(response), { cause: error });
227
428
  }
429
+ function jsonBodyOf(data) {
430
+ return data === undefined ? null : JSON.stringify(data);
431
+ }
432
+ function errorBodyOf(data) {
433
+ if (typeof data !== "string")
434
+ return data ?? null;
435
+ try {
436
+ return JSON.parse(data);
437
+ } catch {
438
+ return null;
439
+ }
440
+ }
228
441
  function createHttpClient2(config) {
229
442
  assertTransportChoice(config);
230
443
  let ssrCookies = null;
@@ -280,23 +493,29 @@ function createHttpClient2(config) {
280
493
  emit({ type: "unauthorized" });
281
494
  }
282
495
  }
283
- if (!response.ok) {
284
- const body = await response.clone().json().catch(() => null);
285
- if (body) {
286
- const parsed = parseError(body);
287
- if (parsed) {
288
- throw new ApiError2(parsed.code, response.status, parsed.details, parsed.message, parsed.hint, responseTraceId(response));
289
- }
290
- }
291
- }
496
+ }
497
+ ],
498
+ beforeError: [
499
+ ({ error }) => {
500
+ if (!isHTTPError(error))
501
+ return error;
502
+ const body = errorBodyOf(error.data);
503
+ const parsed = body === null ? null : parseError(body);
504
+ if (!parsed)
505
+ return error;
506
+ return new ApiError2(parsed.code, error.response.status, parsed.details, parsed.message, parsed.hint, responseTraceId(error.response));
292
507
  }
293
508
  ]
294
509
  }
295
510
  });
296
511
  async function request(method, url, data, options = {}) {
297
512
  const cancellation = createRequestCancellation(options.signal, options.timeout ?? config.timeout ?? 30000);
513
+ const transport = config.fetch ?? globalThis.fetch.bind(globalThis);
298
514
  const kyOptions = {
299
- fetch: createRetryAwareFetch(config.fetch ?? globalThis.fetch.bind(globalThis), config.unix),
515
+ fetch: createRetryAwareFetch(options.onUploadProgress ? withUploadProgress(transport, options.onUploadProgress, {
516
+ route: uploadProgressRoute(config.fetch !== undefined || config.unix !== undefined),
517
+ nativeBody: data instanceof FormData ? data : jsonBodyOf(data)
518
+ }) : transport, config.unix),
300
519
  timeout: false,
301
520
  signal: cancellation.signal
302
521
  };
@@ -339,4 +558,4 @@ function createHttpClient2(config) {
339
558
  };
340
559
  }
341
560
 
342
- export { RequestCancellationError, createRequestCancellation, responseTraceId, ApiError2, refuseLocally, isAbortLikeError, parseApiErrorBody, createHttpClient2 };
561
+ export { RequestCancellationError, createRequestCancellation, responseTraceId, uploadProgressRoute, withUploadProgress, ApiError2, refuseLocally, isAbortLikeError, parseApiErrorBody, createHttpClient2 };
@@ -2,10 +2,12 @@ import {
2
2
  RequestCancellationError,
3
3
  createRequestCancellation,
4
4
  responseTraceId,
5
+ uploadProgressRoute,
6
+ withUploadProgress,
5
7
  ApiError2,
6
8
  refuseLocally,
7
9
  parseApiErrorBody
8
- } from "./index-qjeggp8w.js";
10
+ } from "./index-1h6gtdrj.js";
9
11
  import {
10
12
  parseTrailingWildcard
11
13
  } from "./index-m38ej7e5.js";
@@ -538,10 +540,14 @@ function createFetchExecutor(endpoint, prefix, config, contractConfig) {
538
540
  const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && Object.keys(plan.remainingArgs).length > 0;
539
541
  if (hasBody)
540
542
  headers["Content-Type"] = "application/json";
543
+ const progressRoute = options?.onUploadProgress ? uploadProgressRoute(config.fetch !== undefined) : undefined;
541
544
  try {
542
545
  return await cancellation.run(async (signal) => {
543
546
  const body = endpoint.multipart ? buildMultipartForm(endpoint.multipart, plan.remainingArgs) : hasBody ? JSON.stringify(plan.remainingArgs) : undefined;
544
- const res = await executeFetch(url, {
547
+ const deliver = options?.onUploadProgress && progressRoute ? withUploadProgress(executeFetch, options.onUploadProgress, {
548
+ route: progressRoute
549
+ }) : executeFetch;
550
+ const res = await deliver(url, {
545
551
  method: endpoint.method,
546
552
  headers,
547
553
  credentials: config.credentials,
@@ -660,7 +666,7 @@ function createEndpointMethod(endpoint, execute, contractConfig) {
660
666
  return Object.assign(method, {
661
667
  withOptions: (...args) => {
662
668
  refuseExtraWithOptionsArguments(endpoint, args.length, 2);
663
- return settle(execute, readClientRequestArgs(args[0]), readClientRequestOptions(args[1]));
669
+ return settle(execute, readClientRequestArgs(args[0]), readClientRequestOptions(endpoint, args[1]));
664
670
  }
665
671
  });
666
672
  }
@@ -668,7 +674,7 @@ function createEndpointMethod(endpoint, execute, contractConfig) {
668
674
  return Object.assign(method, {
669
675
  withOptions: (...args) => {
670
676
  refuseExtraWithOptionsArguments(endpoint, args.length, 1);
671
- return settle(execute, {}, readClientRequestOptions(args[0]));
677
+ return settle(execute, {}, readClientRequestOptions(endpoint, args[0]));
672
678
  }
673
679
  });
674
680
  }
@@ -694,16 +700,30 @@ function readClientRequestArgs(requestArgs) {
694
700
  }
695
701
  return requestArgs ?? {};
696
702
  }
697
- function readClientRequestOptions(value) {
703
+ function readClientRequestOptions(endpoint, value) {
698
704
  if (!isRecord(value))
699
705
  throw new TypeError("Client request options must be an object");
700
- const signal = value.signal;
701
- if (signal === undefined)
702
- return {};
703
- if (!isAbortSignal(signal)) {
706
+ const { signal, onUploadProgress } = value;
707
+ if (signal !== undefined && !isAbortSignal(signal)) {
704
708
  throw new TypeError("Client request signal must be an AbortSignal");
705
709
  }
706
- return { signal };
710
+ if (onUploadProgress === undefined)
711
+ return signal === undefined ? {} : { signal };
712
+ if (typeof onUploadProgress !== "function") {
713
+ throw new TypeError("Client request onUploadProgress must be a function");
714
+ }
715
+ if (!endpointSendsBody(endpoint)) {
716
+ throw new TypeError(`${endpoint.method} ${endpoint.path}: onUploadProgress needs a request body — ` + "a GET, HEAD or DELETE sends none, and a streaming endpoint keeps its response live, " + "which the XMLHttpRequest route would buffer.");
717
+ }
718
+ return {
719
+ ...signal !== undefined && { signal },
720
+ onUploadProgress: (progress) => {
721
+ onUploadProgress(progress);
722
+ }
723
+ };
724
+ }
725
+ function endpointSendsBody(endpoint) {
726
+ return endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !(("stream" in endpoint) && endpoint.stream);
707
727
  }
708
728
  function isAbortSignal(value) {
709
729
  return typeof value === "object" && value !== null && "aborted" in value && typeof value.aborted === "boolean" && "addEventListener" in value && typeof value.addEventListener === "function" && "removeEventListener" in value && typeof value.removeEventListener === "function";
package/llms/files.txt CHANGED
@@ -29,5 +29,9 @@ available from `stitchkit/contract`.
29
29
  | `writeFileAtomic` / `writeFileAtomicSync` / `WriteFileAtomicOptions` | function / _type_ | replace a file atomically: a random staging name created exclusively (never through a planted link), the mode set on the descriptor before the file is visible (default `0o600`, not masked by the umask), `fsync`, rename; a failure leaves the target and no staging file. The asynchronous form keeps the event loop running |
30
30
  | `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; an ownerless lock only after `ownerlessGraceMs` |
31
31
  | `ExclusiveLockError` | class | `LOCK_TIMEOUT` naming the resource and its holder, or `LOCK_ABORTED` with the signal's reason |
32
+ | `createChunkSpool` | function | the server half of a chunked upload: parts on disk under `owner` + client-minted `uploadId`, each with a size+sha256 receipt; idempotent `open` and `put`, the first writer of a part wins across processes, `assemble` in order once every part is in, `sweep` of untouched uploads — [guide](../guide/client.md#chunked-uploads) |
33
+ | `ChunkSpool` / `ChunkSpoolConfig` / `ChunkSpoolKey` / `ChunkSpoolOpen` / `ChunkSpoolPart` / `ChunkSpoolAssembly` | _type_ | the spool, its directory, part size, file limit, meta schema and age; an upload's key; its declaration; one part; the assembled parts with a concatenating `stream()` |
34
+ | `ChunkSpoolErrorCode` | _type_ | the `AppError` codes a spool refuses with, each with its 4xx status: `UPLOAD_INVALID`, `UPLOAD_TOO_LARGE`, `UPLOAD_NOT_FOUND`, `UPLOAD_CONFLICT`, `UPLOAD_CHUNK_OUT_OF_RANGE`, `UPLOAD_CHUNK_SIZE`, `UPLOAD_INCOMPLETE` |
35
+ | `CHUNK_SPOOL_ERROR_CODES` / `isChunkSpoolErrorCode` | const / function | every `ChunkSpoolErrorCode` as a frozen list, and the guard for one — map them onto the app's own error codes, or a spool refusal reaches its client as an unknown 500 |
32
36
 
33
37
  ---