@ptkl/sdk 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2172,6 +2172,70 @@ var ProtokolSDK010 = (function (exports, axios) {
2172
2172
  }
2173
2173
  }
2174
2174
 
2175
+ /**
2176
+ * Build an RFC 7233 Range header value from the SDK's range options.
2177
+ *
2178
+ * Validation happens here rather than at the server so a typo (a float offset, a
2179
+ * negative length) fails immediately and locally instead of arriving as an opaque
2180
+ * 400 after a round trip.
2181
+ */
2182
+ function buildRangeHeader(range) {
2183
+ const { offset, length, suffix } = range;
2184
+ if (suffix !== undefined) {
2185
+ if (offset !== undefined || length !== undefined) {
2186
+ throw new Error('readRange: `suffix` cannot be combined with `offset` or `length`');
2187
+ }
2188
+ if (!Number.isInteger(suffix) || suffix <= 0) {
2189
+ throw new Error('readRange: `suffix` must be a positive integer');
2190
+ }
2191
+ return `bytes=-${suffix}`;
2192
+ }
2193
+ const start = offset !== null && offset !== void 0 ? offset : 0;
2194
+ if (!Number.isInteger(start) || start < 0) {
2195
+ throw new Error('readRange: `offset` must be a non-negative integer');
2196
+ }
2197
+ if (length === undefined) {
2198
+ return `bytes=${start}-`;
2199
+ }
2200
+ if (!Number.isInteger(length) || length <= 0) {
2201
+ throw new Error('readRange: `length` must be a positive integer');
2202
+ }
2203
+ // Range end offsets are inclusive, so a length of N ends at start + N - 1.
2204
+ return `bytes=${start}-${start + length - 1}`;
2205
+ }
2206
+ /**
2207
+ * Turn a 206 response into a ByteRangeResult.
2208
+ *
2209
+ * The Content-Range check is the important part. Requests reach media-api through
2210
+ * two proxies, and if any hop ever dropped the Range header the response would be
2211
+ * a perfectly valid 200 carrying the *entire* file — a caller asking for 64 KiB
2212
+ * of a 2 GB object would quietly receive all 2 GB. Failing loudly here turns that
2213
+ * from a silent memory blow-up into an actionable error.
2214
+ */
2215
+ function toByteRangeResult(resp) {
2216
+ var _a, _b, _c, _d, _e, _f, _g;
2217
+ const contentRange = (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _a === void 0 ? void 0 : _a['content-range']) !== null && _b !== void 0 ? _b : (_c = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _c === void 0 ? void 0 : _c['Content-Range'];
2218
+ const parsed = /^bytes\s+(\d+)-(\d+)\/(\d+)$/.exec(String(contentRange !== null && contentRange !== void 0 ? contentRange : '').trim());
2219
+ if (!parsed) {
2220
+ throw new Error('readRange: response did not include a Content-Range header, so the returned body ' +
2221
+ 'may be the entire object rather than the requested range. ' +
2222
+ 'Check that the media host supports ranged reads and that no proxy is stripping the Range header.');
2223
+ }
2224
+ const start = Number(parsed[1]);
2225
+ const end = Number(parsed[2]);
2226
+ const totalSize = Number(parsed[3]);
2227
+ const etag = (_e = (_d = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _d === void 0 ? void 0 : _d.etag) !== null && _e !== void 0 ? _e : (_f = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _f === void 0 ? void 0 : _f.ETag;
2228
+ return {
2229
+ data: resp.data,
2230
+ start,
2231
+ end,
2232
+ length: end - start + 1,
2233
+ totalSize,
2234
+ eof: end >= totalSize - 1,
2235
+ etag: typeof etag === 'string' ? etag.replace(/^"|"$/g, '') : undefined,
2236
+ contentType: (_g = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _g === void 0 ? void 0 : _g['content-type'],
2237
+ };
2238
+ }
2175
2239
  /**
2176
2240
  * Document Management System (DMS) API client
2177
2241
  *
@@ -2346,6 +2410,93 @@ var ProtokolSDK010 = (function (exports, axios) {
2346
2410
  responseType: (!encoding) ? 'blob' : null
2347
2411
  });
2348
2412
  }
2413
+ /**
2414
+ * Read a slice of a stored file without transferring the whole object.
2415
+ *
2416
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
2417
+ * Slicing is positional and format-agnostic — it works on any file.
2418
+ *
2419
+ * ```ts
2420
+ * // First 64 KiB
2421
+ * const head = await dms.readRange('reports/big.csv', { length: 65536 })
2422
+ *
2423
+ * // Continue from where that stopped
2424
+ * const next = await dms.readRange('reports/big.csv', { offset: head.end + 1, length: 65536 })
2425
+ *
2426
+ * // Final 1 KiB
2427
+ * const tail = await dms.readRange('reports/big.csv', { suffix: 1024 })
2428
+ * ```
2429
+ *
2430
+ * The server caps how much a single call may return, so `length` is an upper
2431
+ * bound rather than a guarantee: always advance using the returned `end`
2432
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
2433
+ *
2434
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
2435
+ * compressed as a unit, so no byte window corresponds to a range of rows.
2436
+ *
2437
+ * @param key Path of the file within the library
2438
+ * @param range Which bytes to read
2439
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
2440
+ * the response came back without range metadata — see `readRange`'s
2441
+ * Content-Range check, which prevents silently receiving a whole file.
2442
+ */
2443
+ async readRange(key, range = {}) {
2444
+ const { responseType = 'arraybuffer' } = range;
2445
+ const resp = await this.request('GET', `media/get/${key}`, {
2446
+ headers: { Range: buildRangeHeader(range) },
2447
+ responseType,
2448
+ });
2449
+ return toByteRangeResult(resp);
2450
+ }
2451
+ /**
2452
+ * Read a file as a sequence of byte windows, newest request issued only when
2453
+ * the previous window has been consumed.
2454
+ *
2455
+ * This is the memory-bounded way to process a large file: the whole object is
2456
+ * never held at once, and a caller can stop early simply by breaking out.
2457
+ *
2458
+ * ```ts
2459
+ * for await (const chunk of dms.streamRanges('logs/huge.ndjson', { chunkSize: 1 << 20 })) {
2460
+ * process(chunk.data) // one window at a time
2461
+ * if (foundWhatIWanted) break // no further requests are made
2462
+ * }
2463
+ * ```
2464
+ *
2465
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
2466
+ * reproduces the file byte for byte. A record spanning a window boundary is
2467
+ * the caller's to reassemble — this yields bytes, not records.
2468
+ *
2469
+ * An empty object yields nothing rather than throwing.
2470
+ *
2471
+ * @param key Path of the file within the library
2472
+ * @param options Where to start and how large each window should be
2473
+ */
2474
+ async *streamRanges(key, options = {}) {
2475
+ var _a, _b;
2476
+ const { chunkSize = 1024 * 1024, responseType = 'arraybuffer' } = options;
2477
+ let offset = (_a = options.offset) !== null && _a !== void 0 ? _a : 0;
2478
+ for (;;) {
2479
+ let chunk;
2480
+ try {
2481
+ chunk = await this.readRange(key, { offset, length: chunkSize, responseType });
2482
+ }
2483
+ catch (err) {
2484
+ // 416 means the offset is at or past the end. For the first request
2485
+ // that is an empty object; afterwards it is a benign race with a
2486
+ // truncating writer. Either way the stream is simply over.
2487
+ if (((_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status) === 416)
2488
+ return;
2489
+ throw err;
2490
+ }
2491
+ yield chunk;
2492
+ if (chunk.eof)
2493
+ return;
2494
+ // Advance from what the server actually returned, never from chunkSize:
2495
+ // the per-request cap can make a window shorter than requested, and
2496
+ // assuming otherwise would skip bytes.
2497
+ offset = chunk.end + 1;
2498
+ }
2499
+ }
2349
2500
  async download(key) {
2350
2501
  return this.request('POST', `media/download`, {
2351
2502
  data: {
package/dist/index.0.9.js CHANGED
@@ -1355,6 +1355,70 @@ var ProtokolSDK09 = (function (exports, axios) {
1355
1355
  }
1356
1356
  }
1357
1357
 
1358
+ /**
1359
+ * Build an RFC 7233 Range header value from the SDK's range options.
1360
+ *
1361
+ * Validation happens here rather than at the server so a typo (a float offset, a
1362
+ * negative length) fails immediately and locally instead of arriving as an opaque
1363
+ * 400 after a round trip.
1364
+ */
1365
+ function buildRangeHeader(range) {
1366
+ const { offset, length, suffix } = range;
1367
+ if (suffix !== undefined) {
1368
+ if (offset !== undefined || length !== undefined) {
1369
+ throw new Error('readRange: `suffix` cannot be combined with `offset` or `length`');
1370
+ }
1371
+ if (!Number.isInteger(suffix) || suffix <= 0) {
1372
+ throw new Error('readRange: `suffix` must be a positive integer');
1373
+ }
1374
+ return `bytes=-${suffix}`;
1375
+ }
1376
+ const start = offset !== null && offset !== void 0 ? offset : 0;
1377
+ if (!Number.isInteger(start) || start < 0) {
1378
+ throw new Error('readRange: `offset` must be a non-negative integer');
1379
+ }
1380
+ if (length === undefined) {
1381
+ return `bytes=${start}-`;
1382
+ }
1383
+ if (!Number.isInteger(length) || length <= 0) {
1384
+ throw new Error('readRange: `length` must be a positive integer');
1385
+ }
1386
+ // Range end offsets are inclusive, so a length of N ends at start + N - 1.
1387
+ return `bytes=${start}-${start + length - 1}`;
1388
+ }
1389
+ /**
1390
+ * Turn a 206 response into a ByteRangeResult.
1391
+ *
1392
+ * The Content-Range check is the important part. Requests reach media-api through
1393
+ * two proxies, and if any hop ever dropped the Range header the response would be
1394
+ * a perfectly valid 200 carrying the *entire* file — a caller asking for 64 KiB
1395
+ * of a 2 GB object would quietly receive all 2 GB. Failing loudly here turns that
1396
+ * from a silent memory blow-up into an actionable error.
1397
+ */
1398
+ function toByteRangeResult(resp) {
1399
+ var _a, _b, _c, _d, _e, _f, _g;
1400
+ const contentRange = (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _a === void 0 ? void 0 : _a['content-range']) !== null && _b !== void 0 ? _b : (_c = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _c === void 0 ? void 0 : _c['Content-Range'];
1401
+ const parsed = /^bytes\s+(\d+)-(\d+)\/(\d+)$/.exec(String(contentRange !== null && contentRange !== void 0 ? contentRange : '').trim());
1402
+ if (!parsed) {
1403
+ throw new Error('readRange: response did not include a Content-Range header, so the returned body ' +
1404
+ 'may be the entire object rather than the requested range. ' +
1405
+ 'Check that the media host supports ranged reads and that no proxy is stripping the Range header.');
1406
+ }
1407
+ const start = Number(parsed[1]);
1408
+ const end = Number(parsed[2]);
1409
+ const totalSize = Number(parsed[3]);
1410
+ const etag = (_e = (_d = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _d === void 0 ? void 0 : _d.etag) !== null && _e !== void 0 ? _e : (_f = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _f === void 0 ? void 0 : _f.ETag;
1411
+ return {
1412
+ data: resp.data,
1413
+ start,
1414
+ end,
1415
+ length: end - start + 1,
1416
+ totalSize,
1417
+ eof: end >= totalSize - 1,
1418
+ etag: typeof etag === 'string' ? etag.replace(/^"|"$/g, '') : undefined,
1419
+ contentType: (_g = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _g === void 0 ? void 0 : _g['content-type'],
1420
+ };
1421
+ }
1358
1422
  /**
1359
1423
  * Document Management System (DMS) API client
1360
1424
  *
@@ -1488,6 +1552,95 @@ var ProtokolSDK09 = (function (exports, axios) {
1488
1552
  responseType: (!encoding) ? 'blob' : null
1489
1553
  });
1490
1554
  }
1555
+ /**
1556
+ * Read a slice of a stored file without transferring the whole object.
1557
+ *
1558
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
1559
+ * Slicing is positional and format-agnostic — it works on any file.
1560
+ *
1561
+ * ```ts
1562
+ * // First 64 KiB
1563
+ * const head = await dms.readRange(libraryUuid, 'reports/big.csv', { length: 65536 })
1564
+ *
1565
+ * // Continue from where that stopped
1566
+ * const next = await dms.readRange(libraryUuid, 'reports/big.csv', { offset: head.end + 1, length: 65536 })
1567
+ *
1568
+ * // Final 1 KiB
1569
+ * const tail = await dms.readRange(libraryUuid, 'reports/big.csv', { suffix: 1024 })
1570
+ * ```
1571
+ *
1572
+ * The server caps how much a single call may return, so `length` is an upper
1573
+ * bound rather than a guarantee: always advance using the returned `end`
1574
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
1575
+ *
1576
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
1577
+ * compressed as a unit, so no byte window corresponds to a range of rows.
1578
+ *
1579
+ * @param lib Library UUID
1580
+ * @param key Path of the file within the library
1581
+ * @param range Which bytes to read
1582
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
1583
+ * the response came back without range metadata — see `readRange`'s
1584
+ * Content-Range check, which prevents silently receiving a whole file.
1585
+ */
1586
+ async readRange(lib, key, range = {}) {
1587
+ const { responseType = 'arraybuffer' } = range;
1588
+ const resp = await this.request('GET', `media/library/${lib}/get/${key}`, {
1589
+ headers: { Range: buildRangeHeader(range) },
1590
+ responseType,
1591
+ });
1592
+ return toByteRangeResult(resp);
1593
+ }
1594
+ /**
1595
+ * Read a file as a sequence of byte windows, newest request issued only when
1596
+ * the previous window has been consumed.
1597
+ *
1598
+ * This is the memory-bounded way to process a large file: the whole object is
1599
+ * never held at once, and a caller can stop early simply by breaking out.
1600
+ *
1601
+ * ```ts
1602
+ * for await (const chunk of dms.streamRanges(libraryUuid, 'logs/huge.ndjson', { chunkSize: 1 << 20 })) {
1603
+ * process(chunk.data) // one window at a time
1604
+ * if (foundWhatIWanted) break // no further requests are made
1605
+ * }
1606
+ * ```
1607
+ *
1608
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
1609
+ * reproduces the file byte for byte. A record spanning a window boundary is
1610
+ * the caller's to reassemble — this yields bytes, not records.
1611
+ *
1612
+ * An empty object yields nothing rather than throwing.
1613
+ *
1614
+ * @param lib Library UUID
1615
+ * @param key Path of the file within the library
1616
+ * @param options Where to start and how large each window should be
1617
+ */
1618
+ async *streamRanges(lib, key, options = {}) {
1619
+ var _a, _b;
1620
+ const { chunkSize = 1024 * 1024, responseType = 'arraybuffer' } = options;
1621
+ let offset = (_a = options.offset) !== null && _a !== void 0 ? _a : 0;
1622
+ for (;;) {
1623
+ let chunk;
1624
+ try {
1625
+ chunk = await this.readRange(lib, key, { offset, length: chunkSize, responseType });
1626
+ }
1627
+ catch (err) {
1628
+ // 416 means the offset is at or past the end. For the first request
1629
+ // that is an empty object; afterwards it is a benign race with a
1630
+ // truncating writer. Either way the stream is simply over.
1631
+ if (((_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status) === 416)
1632
+ return;
1633
+ throw err;
1634
+ }
1635
+ yield chunk;
1636
+ if (chunk.eof)
1637
+ return;
1638
+ // Advance from what the server actually returned, never from chunkSize:
1639
+ // the per-request cap can make a window shorter than requested, and
1640
+ // assuming otherwise would skip bytes.
1641
+ offset = chunk.end + 1;
1642
+ }
1643
+ }
1491
1644
  async download(lib, key) {
1492
1645
  return this.request('POST', `media/library/${lib}/download`, {
1493
1646
  data: {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptkl/sdk",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "scripts": {
5
5
  "build": "rollup -c",
6
6
  "build:monaco": "npm run build && node scripts/generate-monaco-types.cjs",
@@ -1,6 +1,6 @@
1
1
  import IntegrationsBaseClient from "../integrationsBaseClient";
2
2
  import { AxiosResponse } from "axios";
3
- import { PDFFillOptions, DataConversionParams, DataValidationParams, DataInfoParams, DataInfo, DataValidationResult, ConversionOptions, HTML2PDFOptions, PDF2HTMLOptions, MediaUploadPayload, MediaUploadBase64Payload, DocumentListParams, DocumentListResponse, DocumentCreatePayload, DocumentUpdatePayload, DocumentRestorePayload, DocumentCommentPayload, DocumentResponse, DocumentGeneratePayload, DocumentGenerateResponse, DocumentRevisionListResponse, ExtractTextPayload, ExtractTextResult, OCRExtractPayload, OCRExtractResult, LibraryExportCreatePayload, LibraryExportListResponse, MediaExportCreateResult, LibraryExportGetResult, LibraryPolicy, LibraryPolicyPayload, DirPolicy, DirPolicyPayload, NotificationRuleCreatePayload, NotificationRule, NotificationRuleListResponse, FileNotificationCreatePayload, FileNotification, FileNotificationListResponse } from "../../types/integrations";
3
+ import { PDFFillOptions, DataConversionParams, DataValidationParams, DataInfoParams, DataInfo, DataValidationResult, ConversionOptions, HTML2PDFOptions, PDF2HTMLOptions, MediaUploadPayload, MediaUploadBase64Payload, DocumentListParams, DocumentListResponse, DocumentCreatePayload, DocumentUpdatePayload, DocumentRestorePayload, DocumentCommentPayload, DocumentResponse, DocumentGeneratePayload, DocumentGenerateResponse, DocumentRevisionListResponse, ExtractTextPayload, ExtractTextResult, OCRExtractPayload, OCRExtractResult, LibraryExportCreatePayload, LibraryExportListResponse, MediaExportCreateResult, LibraryExportGetResult, LibraryPolicy, LibraryPolicyPayload, DirPolicy, DirPolicyPayload, NotificationRuleCreatePayload, NotificationRule, NotificationRuleListResponse, FileNotificationCreatePayload, FileNotification, FileNotificationListResponse, ByteRangeRequest, ByteRangeResult, ByteStreamOptions } from "../../types/integrations";
4
4
  /**
5
5
  * Document Management System (DMS) API client
6
6
  *
@@ -126,6 +126,61 @@ export default class DMS extends IntegrationsBaseClient {
126
126
  */
127
127
  uploadBase64(data: MediaUploadBase64Payload): Promise<AxiosResponse<any, any>>;
128
128
  getMedia(key: string, encoding: string): Promise<AxiosResponse<any, any>>;
129
+ /**
130
+ * Read a slice of a stored file without transferring the whole object.
131
+ *
132
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
133
+ * Slicing is positional and format-agnostic — it works on any file.
134
+ *
135
+ * ```ts
136
+ * // First 64 KiB
137
+ * const head = await dms.readRange('reports/big.csv', { length: 65536 })
138
+ *
139
+ * // Continue from where that stopped
140
+ * const next = await dms.readRange('reports/big.csv', { offset: head.end + 1, length: 65536 })
141
+ *
142
+ * // Final 1 KiB
143
+ * const tail = await dms.readRange('reports/big.csv', { suffix: 1024 })
144
+ * ```
145
+ *
146
+ * The server caps how much a single call may return, so `length` is an upper
147
+ * bound rather than a guarantee: always advance using the returned `end`
148
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
149
+ *
150
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
151
+ * compressed as a unit, so no byte window corresponds to a range of rows.
152
+ *
153
+ * @param key Path of the file within the library
154
+ * @param range Which bytes to read
155
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
156
+ * the response came back without range metadata — see `readRange`'s
157
+ * Content-Range check, which prevents silently receiving a whole file.
158
+ */
159
+ readRange(key: string, range?: ByteRangeRequest): Promise<ByteRangeResult>;
160
+ /**
161
+ * Read a file as a sequence of byte windows, newest request issued only when
162
+ * the previous window has been consumed.
163
+ *
164
+ * This is the memory-bounded way to process a large file: the whole object is
165
+ * never held at once, and a caller can stop early simply by breaking out.
166
+ *
167
+ * ```ts
168
+ * for await (const chunk of dms.streamRanges('logs/huge.ndjson', { chunkSize: 1 << 20 })) {
169
+ * process(chunk.data) // one window at a time
170
+ * if (foundWhatIWanted) break // no further requests are made
171
+ * }
172
+ * ```
173
+ *
174
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
175
+ * reproduces the file byte for byte. A record spanning a window boundary is
176
+ * the caller's to reassemble — this yields bytes, not records.
177
+ *
178
+ * An empty object yields nothing rather than throwing.
179
+ *
180
+ * @param key Path of the file within the library
181
+ * @param options Where to start and how large each window should be
182
+ */
183
+ streamRanges(key: string, options?: ByteStreamOptions): AsyncGenerator<ByteRangeResult>;
129
184
  download(key: string): Promise<AxiosResponse<any, any>>;
130
185
  getExifData(key: string): Promise<AxiosResponse<any, any>>;
131
186
  html2pdf(data: HTML2PDFOptions): Promise<AxiosResponse<any, any>>;
@@ -21138,6 +21138,70 @@ class IntegrationsBaseClient extends BaseClient {
21138
21138
  }
21139
21139
  }
21140
21140
 
21141
+ /**
21142
+ * Build an RFC 7233 Range header value from the SDK's range options.
21143
+ *
21144
+ * Validation happens here rather than at the server so a typo (a float offset, a
21145
+ * negative length) fails immediately and locally instead of arriving as an opaque
21146
+ * 400 after a round trip.
21147
+ */
21148
+ function buildRangeHeader(range) {
21149
+ const { offset, length, suffix } = range;
21150
+ if (suffix !== undefined) {
21151
+ if (offset !== undefined || length !== undefined) {
21152
+ throw new Error('readRange: `suffix` cannot be combined with `offset` or `length`');
21153
+ }
21154
+ if (!Number.isInteger(suffix) || suffix <= 0) {
21155
+ throw new Error('readRange: `suffix` must be a positive integer');
21156
+ }
21157
+ return `bytes=-${suffix}`;
21158
+ }
21159
+ const start = offset !== null && offset !== void 0 ? offset : 0;
21160
+ if (!Number.isInteger(start) || start < 0) {
21161
+ throw new Error('readRange: `offset` must be a non-negative integer');
21162
+ }
21163
+ if (length === undefined) {
21164
+ return `bytes=${start}-`;
21165
+ }
21166
+ if (!Number.isInteger(length) || length <= 0) {
21167
+ throw new Error('readRange: `length` must be a positive integer');
21168
+ }
21169
+ // Range end offsets are inclusive, so a length of N ends at start + N - 1.
21170
+ return `bytes=${start}-${start + length - 1}`;
21171
+ }
21172
+ /**
21173
+ * Turn a 206 response into a ByteRangeResult.
21174
+ *
21175
+ * The Content-Range check is the important part. Requests reach media-api through
21176
+ * two proxies, and if any hop ever dropped the Range header the response would be
21177
+ * a perfectly valid 200 carrying the *entire* file — a caller asking for 64 KiB
21178
+ * of a 2 GB object would quietly receive all 2 GB. Failing loudly here turns that
21179
+ * from a silent memory blow-up into an actionable error.
21180
+ */
21181
+ function toByteRangeResult(resp) {
21182
+ var _a, _b, _c, _d, _e, _f, _g;
21183
+ const contentRange = (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _a === void 0 ? void 0 : _a['content-range']) !== null && _b !== void 0 ? _b : (_c = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _c === void 0 ? void 0 : _c['Content-Range'];
21184
+ const parsed = /^bytes\s+(\d+)-(\d+)\/(\d+)$/.exec(String(contentRange !== null && contentRange !== void 0 ? contentRange : '').trim());
21185
+ if (!parsed) {
21186
+ throw new Error('readRange: response did not include a Content-Range header, so the returned body ' +
21187
+ 'may be the entire object rather than the requested range. ' +
21188
+ 'Check that the media host supports ranged reads and that no proxy is stripping the Range header.');
21189
+ }
21190
+ const start = Number(parsed[1]);
21191
+ const end = Number(parsed[2]);
21192
+ const totalSize = Number(parsed[3]);
21193
+ const etag = (_e = (_d = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _d === void 0 ? void 0 : _d.etag) !== null && _e !== void 0 ? _e : (_f = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _f === void 0 ? void 0 : _f.ETag;
21194
+ return {
21195
+ data: resp.data,
21196
+ start,
21197
+ end,
21198
+ length: end - start + 1,
21199
+ totalSize,
21200
+ eof: end >= totalSize - 1,
21201
+ etag: typeof etag === 'string' ? etag.replace(/^"|"$/g, '') : undefined,
21202
+ contentType: (_g = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _g === void 0 ? void 0 : _g['content-type'],
21203
+ };
21204
+ }
21141
21205
  /**
21142
21206
  * Document Management System (DMS) API client
21143
21207
  *
@@ -21312,6 +21376,93 @@ class DMS extends IntegrationsBaseClient {
21312
21376
  responseType: (!encoding) ? 'blob' : null
21313
21377
  });
21314
21378
  }
21379
+ /**
21380
+ * Read a slice of a stored file without transferring the whole object.
21381
+ *
21382
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
21383
+ * Slicing is positional and format-agnostic — it works on any file.
21384
+ *
21385
+ * ```ts
21386
+ * // First 64 KiB
21387
+ * const head = await dms.readRange('reports/big.csv', { length: 65536 })
21388
+ *
21389
+ * // Continue from where that stopped
21390
+ * const next = await dms.readRange('reports/big.csv', { offset: head.end + 1, length: 65536 })
21391
+ *
21392
+ * // Final 1 KiB
21393
+ * const tail = await dms.readRange('reports/big.csv', { suffix: 1024 })
21394
+ * ```
21395
+ *
21396
+ * The server caps how much a single call may return, so `length` is an upper
21397
+ * bound rather than a guarantee: always advance using the returned `end`
21398
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
21399
+ *
21400
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
21401
+ * compressed as a unit, so no byte window corresponds to a range of rows.
21402
+ *
21403
+ * @param key Path of the file within the library
21404
+ * @param range Which bytes to read
21405
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
21406
+ * the response came back without range metadata — see `readRange`'s
21407
+ * Content-Range check, which prevents silently receiving a whole file.
21408
+ */
21409
+ async readRange(key, range = {}) {
21410
+ const { responseType = 'arraybuffer' } = range;
21411
+ const resp = await this.request('GET', `media/get/${key}`, {
21412
+ headers: { Range: buildRangeHeader(range) },
21413
+ responseType,
21414
+ });
21415
+ return toByteRangeResult(resp);
21416
+ }
21417
+ /**
21418
+ * Read a file as a sequence of byte windows, newest request issued only when
21419
+ * the previous window has been consumed.
21420
+ *
21421
+ * This is the memory-bounded way to process a large file: the whole object is
21422
+ * never held at once, and a caller can stop early simply by breaking out.
21423
+ *
21424
+ * ```ts
21425
+ * for await (const chunk of dms.streamRanges('logs/huge.ndjson', { chunkSize: 1 << 20 })) {
21426
+ * process(chunk.data) // one window at a time
21427
+ * if (foundWhatIWanted) break // no further requests are made
21428
+ * }
21429
+ * ```
21430
+ *
21431
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
21432
+ * reproduces the file byte for byte. A record spanning a window boundary is
21433
+ * the caller's to reassemble — this yields bytes, not records.
21434
+ *
21435
+ * An empty object yields nothing rather than throwing.
21436
+ *
21437
+ * @param key Path of the file within the library
21438
+ * @param options Where to start and how large each window should be
21439
+ */
21440
+ async *streamRanges(key, options = {}) {
21441
+ var _a, _b;
21442
+ const { chunkSize = 1024 * 1024, responseType = 'arraybuffer' } = options;
21443
+ let offset = (_a = options.offset) !== null && _a !== void 0 ? _a : 0;
21444
+ for (;;) {
21445
+ let chunk;
21446
+ try {
21447
+ chunk = await this.readRange(key, { offset, length: chunkSize, responseType });
21448
+ }
21449
+ catch (err) {
21450
+ // 416 means the offset is at or past the end. For the first request
21451
+ // that is an empty object; afterwards it is a benign race with a
21452
+ // truncating writer. Either way the stream is simply over.
21453
+ if (((_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status) === 416)
21454
+ return;
21455
+ throw err;
21456
+ }
21457
+ yield chunk;
21458
+ if (chunk.eof)
21459
+ return;
21460
+ // Advance from what the server actually returned, never from chunkSize:
21461
+ // the per-request cap can make a window shorter than requested, and
21462
+ // assuming otherwise would skip bytes.
21463
+ offset = chunk.end + 1;
21464
+ }
21465
+ }
21315
21466
  async download(key) {
21316
21467
  return this.request('POST', `media/download`, {
21317
21468
  data: {
@@ -2171,6 +2171,70 @@ class IntegrationsBaseClient extends BaseClient {
2171
2171
  }
2172
2172
  }
2173
2173
 
2174
+ /**
2175
+ * Build an RFC 7233 Range header value from the SDK's range options.
2176
+ *
2177
+ * Validation happens here rather than at the server so a typo (a float offset, a
2178
+ * negative length) fails immediately and locally instead of arriving as an opaque
2179
+ * 400 after a round trip.
2180
+ */
2181
+ function buildRangeHeader(range) {
2182
+ const { offset, length, suffix } = range;
2183
+ if (suffix !== undefined) {
2184
+ if (offset !== undefined || length !== undefined) {
2185
+ throw new Error('readRange: `suffix` cannot be combined with `offset` or `length`');
2186
+ }
2187
+ if (!Number.isInteger(suffix) || suffix <= 0) {
2188
+ throw new Error('readRange: `suffix` must be a positive integer');
2189
+ }
2190
+ return `bytes=-${suffix}`;
2191
+ }
2192
+ const start = offset !== null && offset !== void 0 ? offset : 0;
2193
+ if (!Number.isInteger(start) || start < 0) {
2194
+ throw new Error('readRange: `offset` must be a non-negative integer');
2195
+ }
2196
+ if (length === undefined) {
2197
+ return `bytes=${start}-`;
2198
+ }
2199
+ if (!Number.isInteger(length) || length <= 0) {
2200
+ throw new Error('readRange: `length` must be a positive integer');
2201
+ }
2202
+ // Range end offsets are inclusive, so a length of N ends at start + N - 1.
2203
+ return `bytes=${start}-${start + length - 1}`;
2204
+ }
2205
+ /**
2206
+ * Turn a 206 response into a ByteRangeResult.
2207
+ *
2208
+ * The Content-Range check is the important part. Requests reach media-api through
2209
+ * two proxies, and if any hop ever dropped the Range header the response would be
2210
+ * a perfectly valid 200 carrying the *entire* file — a caller asking for 64 KiB
2211
+ * of a 2 GB object would quietly receive all 2 GB. Failing loudly here turns that
2212
+ * from a silent memory blow-up into an actionable error.
2213
+ */
2214
+ function toByteRangeResult(resp) {
2215
+ var _a, _b, _c, _d, _e, _f, _g;
2216
+ const contentRange = (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _a === void 0 ? void 0 : _a['content-range']) !== null && _b !== void 0 ? _b : (_c = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _c === void 0 ? void 0 : _c['Content-Range'];
2217
+ const parsed = /^bytes\s+(\d+)-(\d+)\/(\d+)$/.exec(String(contentRange !== null && contentRange !== void 0 ? contentRange : '').trim());
2218
+ if (!parsed) {
2219
+ throw new Error('readRange: response did not include a Content-Range header, so the returned body ' +
2220
+ 'may be the entire object rather than the requested range. ' +
2221
+ 'Check that the media host supports ranged reads and that no proxy is stripping the Range header.');
2222
+ }
2223
+ const start = Number(parsed[1]);
2224
+ const end = Number(parsed[2]);
2225
+ const totalSize = Number(parsed[3]);
2226
+ const etag = (_e = (_d = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _d === void 0 ? void 0 : _d.etag) !== null && _e !== void 0 ? _e : (_f = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _f === void 0 ? void 0 : _f.ETag;
2227
+ return {
2228
+ data: resp.data,
2229
+ start,
2230
+ end,
2231
+ length: end - start + 1,
2232
+ totalSize,
2233
+ eof: end >= totalSize - 1,
2234
+ etag: typeof etag === 'string' ? etag.replace(/^"|"$/g, '') : undefined,
2235
+ contentType: (_g = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _g === void 0 ? void 0 : _g['content-type'],
2236
+ };
2237
+ }
2174
2238
  /**
2175
2239
  * Document Management System (DMS) API client
2176
2240
  *
@@ -2345,6 +2409,93 @@ class DMS extends IntegrationsBaseClient {
2345
2409
  responseType: (!encoding) ? 'blob' : null
2346
2410
  });
2347
2411
  }
2412
+ /**
2413
+ * Read a slice of a stored file without transferring the whole object.
2414
+ *
2415
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
2416
+ * Slicing is positional and format-agnostic — it works on any file.
2417
+ *
2418
+ * ```ts
2419
+ * // First 64 KiB
2420
+ * const head = await dms.readRange('reports/big.csv', { length: 65536 })
2421
+ *
2422
+ * // Continue from where that stopped
2423
+ * const next = await dms.readRange('reports/big.csv', { offset: head.end + 1, length: 65536 })
2424
+ *
2425
+ * // Final 1 KiB
2426
+ * const tail = await dms.readRange('reports/big.csv', { suffix: 1024 })
2427
+ * ```
2428
+ *
2429
+ * The server caps how much a single call may return, so `length` is an upper
2430
+ * bound rather than a guarantee: always advance using the returned `end`
2431
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
2432
+ *
2433
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
2434
+ * compressed as a unit, so no byte window corresponds to a range of rows.
2435
+ *
2436
+ * @param key Path of the file within the library
2437
+ * @param range Which bytes to read
2438
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
2439
+ * the response came back without range metadata — see `readRange`'s
2440
+ * Content-Range check, which prevents silently receiving a whole file.
2441
+ */
2442
+ async readRange(key, range = {}) {
2443
+ const { responseType = 'arraybuffer' } = range;
2444
+ const resp = await this.request('GET', `media/get/${key}`, {
2445
+ headers: { Range: buildRangeHeader(range) },
2446
+ responseType,
2447
+ });
2448
+ return toByteRangeResult(resp);
2449
+ }
2450
+ /**
2451
+ * Read a file as a sequence of byte windows, newest request issued only when
2452
+ * the previous window has been consumed.
2453
+ *
2454
+ * This is the memory-bounded way to process a large file: the whole object is
2455
+ * never held at once, and a caller can stop early simply by breaking out.
2456
+ *
2457
+ * ```ts
2458
+ * for await (const chunk of dms.streamRanges('logs/huge.ndjson', { chunkSize: 1 << 20 })) {
2459
+ * process(chunk.data) // one window at a time
2460
+ * if (foundWhatIWanted) break // no further requests are made
2461
+ * }
2462
+ * ```
2463
+ *
2464
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
2465
+ * reproduces the file byte for byte. A record spanning a window boundary is
2466
+ * the caller's to reassemble — this yields bytes, not records.
2467
+ *
2468
+ * An empty object yields nothing rather than throwing.
2469
+ *
2470
+ * @param key Path of the file within the library
2471
+ * @param options Where to start and how large each window should be
2472
+ */
2473
+ async *streamRanges(key, options = {}) {
2474
+ var _a, _b;
2475
+ const { chunkSize = 1024 * 1024, responseType = 'arraybuffer' } = options;
2476
+ let offset = (_a = options.offset) !== null && _a !== void 0 ? _a : 0;
2477
+ for (;;) {
2478
+ let chunk;
2479
+ try {
2480
+ chunk = await this.readRange(key, { offset, length: chunkSize, responseType });
2481
+ }
2482
+ catch (err) {
2483
+ // 416 means the offset is at or past the end. For the first request
2484
+ // that is an empty object; afterwards it is a benign race with a
2485
+ // truncating writer. Either way the stream is simply over.
2486
+ if (((_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status) === 416)
2487
+ return;
2488
+ throw err;
2489
+ }
2490
+ yield chunk;
2491
+ if (chunk.eof)
2492
+ return;
2493
+ // Advance from what the server actually returned, never from chunkSize:
2494
+ // the per-request cap can make a window shorter than requested, and
2495
+ // assuming otherwise would skip bytes.
2496
+ offset = chunk.end + 1;
2497
+ }
2498
+ }
2348
2499
  async download(key) {
2349
2500
  return this.request('POST', `media/download`, {
2350
2501
  data: {
@@ -584,3 +584,47 @@ export type FileNotification = {
584
584
  recipient_email?: string;
585
585
  };
586
586
  export type FileNotificationListResponse = FileNotification[];
587
+ /**
588
+ * Which bytes to read. Supply either `offset`/`length` or `suffix`, not both.
589
+ */
590
+ export type ByteRangeRequest = {
591
+ /** First byte to read, 0-based. Defaults to 0. */
592
+ offset?: number;
593
+ /** How many bytes to read. Omit to read to the end of the object, subject to the server's per-request cap. */
594
+ length?: number;
595
+ /** Read the final N bytes instead. Mutually exclusive with `offset`/`length`. */
596
+ suffix?: number;
597
+ /** Axios response type for the body. Defaults to `arraybuffer`. */
598
+ responseType?: 'arraybuffer' | 'blob' | 'text';
599
+ };
600
+ /**
601
+ * A single window of bytes plus the position metadata needed to request the next one.
602
+ */
603
+ export type ByteRangeResult<T = any> = {
604
+ /** The bytes, in the shape requested via `responseType`. */
605
+ data: T;
606
+ /** Offset of the first byte returned. */
607
+ start: number;
608
+ /** Offset of the last byte returned, inclusive. */
609
+ end: number;
610
+ /** Number of bytes returned. May be less than requested — see `ByteRangeRequest.length`. */
611
+ length: number;
612
+ /** Total size of the whole object. */
613
+ totalSize: number;
614
+ /** True when this window ends at the last byte of the object. */
615
+ eof: boolean;
616
+ /** Object validator. Changes if the object is replaced, so a multi-window scan can detect it. */
617
+ etag?: string;
618
+ contentType?: string;
619
+ };
620
+ /**
621
+ * Options for walking an object window by window.
622
+ */
623
+ export type ByteStreamOptions = {
624
+ /** Byte offset to start from. Defaults to 0. */
625
+ offset?: number;
626
+ /** Bytes per window. Defaults to 1 MiB. The server may return less. */
627
+ chunkSize?: number;
628
+ /** Axios response type for each window. Defaults to `arraybuffer`. */
629
+ responseType?: 'arraybuffer' | 'blob' | 'text';
630
+ };
@@ -1,6 +1,6 @@
1
1
  import IntegrationsBaseClient from "../integrationsBaseClient";
2
2
  import { AxiosResponse } from "axios";
3
- import { PDFFillOptions, DataConversionParams, DataValidationParams, DataInfoParams, DataInfo, DataValidationResult, ConversionOptions, HTML2PDFOptions } from "../../types/integrations";
3
+ import { PDFFillOptions, DataConversionParams, DataValidationParams, DataInfoParams, DataInfo, DataValidationResult, ConversionOptions, HTML2PDFOptions, ByteRangeRequest, ByteRangeResult, ByteStreamOptions } from "../../types/integrations";
4
4
  /**
5
5
  * Document Management System (DMS) API client
6
6
  *
@@ -87,6 +87,63 @@ export default class DMS extends IntegrationsBaseClient {
87
87
  delete(data: any): Promise<AxiosResponse<any, any>>;
88
88
  uploadBase64(data: any): Promise<AxiosResponse<any, any>>;
89
89
  getMedia(lib: string, key: string, encoding: string): Promise<AxiosResponse<any, any>>;
90
+ /**
91
+ * Read a slice of a stored file without transferring the whole object.
92
+ *
93
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
94
+ * Slicing is positional and format-agnostic — it works on any file.
95
+ *
96
+ * ```ts
97
+ * // First 64 KiB
98
+ * const head = await dms.readRange(libraryUuid, 'reports/big.csv', { length: 65536 })
99
+ *
100
+ * // Continue from where that stopped
101
+ * const next = await dms.readRange(libraryUuid, 'reports/big.csv', { offset: head.end + 1, length: 65536 })
102
+ *
103
+ * // Final 1 KiB
104
+ * const tail = await dms.readRange(libraryUuid, 'reports/big.csv', { suffix: 1024 })
105
+ * ```
106
+ *
107
+ * The server caps how much a single call may return, so `length` is an upper
108
+ * bound rather than a guarantee: always advance using the returned `end`
109
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
110
+ *
111
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
112
+ * compressed as a unit, so no byte window corresponds to a range of rows.
113
+ *
114
+ * @param lib Library UUID
115
+ * @param key Path of the file within the library
116
+ * @param range Which bytes to read
117
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
118
+ * the response came back without range metadata — see `readRange`'s
119
+ * Content-Range check, which prevents silently receiving a whole file.
120
+ */
121
+ readRange(lib: string, key: string, range?: ByteRangeRequest): Promise<ByteRangeResult>;
122
+ /**
123
+ * Read a file as a sequence of byte windows, newest request issued only when
124
+ * the previous window has been consumed.
125
+ *
126
+ * This is the memory-bounded way to process a large file: the whole object is
127
+ * never held at once, and a caller can stop early simply by breaking out.
128
+ *
129
+ * ```ts
130
+ * for await (const chunk of dms.streamRanges(libraryUuid, 'logs/huge.ndjson', { chunkSize: 1 << 20 })) {
131
+ * process(chunk.data) // one window at a time
132
+ * if (foundWhatIWanted) break // no further requests are made
133
+ * }
134
+ * ```
135
+ *
136
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
137
+ * reproduces the file byte for byte. A record spanning a window boundary is
138
+ * the caller's to reassemble — this yields bytes, not records.
139
+ *
140
+ * An empty object yields nothing rather than throwing.
141
+ *
142
+ * @param lib Library UUID
143
+ * @param key Path of the file within the library
144
+ * @param options Where to start and how large each window should be
145
+ */
146
+ streamRanges(lib: string, key: string, options?: ByteStreamOptions): AsyncGenerator<ByteRangeResult>;
90
147
  download(lib: string, key: string): Promise<AxiosResponse<any, any>>;
91
148
  getExifData(lib: string, key: string): Promise<AxiosResponse<any, any>>;
92
149
  html2pdf(lib: string, data: HTML2PDFOptions): Promise<AxiosResponse<any, any>>;
@@ -20323,6 +20323,70 @@ class IntegrationsBaseClient extends BaseClient {
20323
20323
  }
20324
20324
  }
20325
20325
 
20326
+ /**
20327
+ * Build an RFC 7233 Range header value from the SDK's range options.
20328
+ *
20329
+ * Validation happens here rather than at the server so a typo (a float offset, a
20330
+ * negative length) fails immediately and locally instead of arriving as an opaque
20331
+ * 400 after a round trip.
20332
+ */
20333
+ function buildRangeHeader(range) {
20334
+ const { offset, length, suffix } = range;
20335
+ if (suffix !== undefined) {
20336
+ if (offset !== undefined || length !== undefined) {
20337
+ throw new Error('readRange: `suffix` cannot be combined with `offset` or `length`');
20338
+ }
20339
+ if (!Number.isInteger(suffix) || suffix <= 0) {
20340
+ throw new Error('readRange: `suffix` must be a positive integer');
20341
+ }
20342
+ return `bytes=-${suffix}`;
20343
+ }
20344
+ const start = offset !== null && offset !== void 0 ? offset : 0;
20345
+ if (!Number.isInteger(start) || start < 0) {
20346
+ throw new Error('readRange: `offset` must be a non-negative integer');
20347
+ }
20348
+ if (length === undefined) {
20349
+ return `bytes=${start}-`;
20350
+ }
20351
+ if (!Number.isInteger(length) || length <= 0) {
20352
+ throw new Error('readRange: `length` must be a positive integer');
20353
+ }
20354
+ // Range end offsets are inclusive, so a length of N ends at start + N - 1.
20355
+ return `bytes=${start}-${start + length - 1}`;
20356
+ }
20357
+ /**
20358
+ * Turn a 206 response into a ByteRangeResult.
20359
+ *
20360
+ * The Content-Range check is the important part. Requests reach media-api through
20361
+ * two proxies, and if any hop ever dropped the Range header the response would be
20362
+ * a perfectly valid 200 carrying the *entire* file — a caller asking for 64 KiB
20363
+ * of a 2 GB object would quietly receive all 2 GB. Failing loudly here turns that
20364
+ * from a silent memory blow-up into an actionable error.
20365
+ */
20366
+ function toByteRangeResult(resp) {
20367
+ var _a, _b, _c, _d, _e, _f, _g;
20368
+ const contentRange = (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _a === void 0 ? void 0 : _a['content-range']) !== null && _b !== void 0 ? _b : (_c = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _c === void 0 ? void 0 : _c['Content-Range'];
20369
+ const parsed = /^bytes\s+(\d+)-(\d+)\/(\d+)$/.exec(String(contentRange !== null && contentRange !== void 0 ? contentRange : '').trim());
20370
+ if (!parsed) {
20371
+ throw new Error('readRange: response did not include a Content-Range header, so the returned body ' +
20372
+ 'may be the entire object rather than the requested range. ' +
20373
+ 'Check that the media host supports ranged reads and that no proxy is stripping the Range header.');
20374
+ }
20375
+ const start = Number(parsed[1]);
20376
+ const end = Number(parsed[2]);
20377
+ const totalSize = Number(parsed[3]);
20378
+ const etag = (_e = (_d = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _d === void 0 ? void 0 : _d.etag) !== null && _e !== void 0 ? _e : (_f = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _f === void 0 ? void 0 : _f.ETag;
20379
+ return {
20380
+ data: resp.data,
20381
+ start,
20382
+ end,
20383
+ length: end - start + 1,
20384
+ totalSize,
20385
+ eof: end >= totalSize - 1,
20386
+ etag: typeof etag === 'string' ? etag.replace(/^"|"$/g, '') : undefined,
20387
+ contentType: (_g = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _g === void 0 ? void 0 : _g['content-type'],
20388
+ };
20389
+ }
20326
20390
  /**
20327
20391
  * Document Management System (DMS) API client
20328
20392
  *
@@ -20456,6 +20520,95 @@ class DMS extends IntegrationsBaseClient {
20456
20520
  responseType: (!encoding) ? 'blob' : null
20457
20521
  });
20458
20522
  }
20523
+ /**
20524
+ * Read a slice of a stored file without transferring the whole object.
20525
+ *
20526
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
20527
+ * Slicing is positional and format-agnostic — it works on any file.
20528
+ *
20529
+ * ```ts
20530
+ * // First 64 KiB
20531
+ * const head = await dms.readRange(libraryUuid, 'reports/big.csv', { length: 65536 })
20532
+ *
20533
+ * // Continue from where that stopped
20534
+ * const next = await dms.readRange(libraryUuid, 'reports/big.csv', { offset: head.end + 1, length: 65536 })
20535
+ *
20536
+ * // Final 1 KiB
20537
+ * const tail = await dms.readRange(libraryUuid, 'reports/big.csv', { suffix: 1024 })
20538
+ * ```
20539
+ *
20540
+ * The server caps how much a single call may return, so `length` is an upper
20541
+ * bound rather than a guarantee: always advance using the returned `end`
20542
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
20543
+ *
20544
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
20545
+ * compressed as a unit, so no byte window corresponds to a range of rows.
20546
+ *
20547
+ * @param lib Library UUID
20548
+ * @param key Path of the file within the library
20549
+ * @param range Which bytes to read
20550
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
20551
+ * the response came back without range metadata — see `readRange`'s
20552
+ * Content-Range check, which prevents silently receiving a whole file.
20553
+ */
20554
+ async readRange(lib, key, range = {}) {
20555
+ const { responseType = 'arraybuffer' } = range;
20556
+ const resp = await this.request('GET', `media/library/${lib}/get/${key}`, {
20557
+ headers: { Range: buildRangeHeader(range) },
20558
+ responseType,
20559
+ });
20560
+ return toByteRangeResult(resp);
20561
+ }
20562
+ /**
20563
+ * Read a file as a sequence of byte windows, newest request issued only when
20564
+ * the previous window has been consumed.
20565
+ *
20566
+ * This is the memory-bounded way to process a large file: the whole object is
20567
+ * never held at once, and a caller can stop early simply by breaking out.
20568
+ *
20569
+ * ```ts
20570
+ * for await (const chunk of dms.streamRanges(libraryUuid, 'logs/huge.ndjson', { chunkSize: 1 << 20 })) {
20571
+ * process(chunk.data) // one window at a time
20572
+ * if (foundWhatIWanted) break // no further requests are made
20573
+ * }
20574
+ * ```
20575
+ *
20576
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
20577
+ * reproduces the file byte for byte. A record spanning a window boundary is
20578
+ * the caller's to reassemble — this yields bytes, not records.
20579
+ *
20580
+ * An empty object yields nothing rather than throwing.
20581
+ *
20582
+ * @param lib Library UUID
20583
+ * @param key Path of the file within the library
20584
+ * @param options Where to start and how large each window should be
20585
+ */
20586
+ async *streamRanges(lib, key, options = {}) {
20587
+ var _a, _b;
20588
+ const { chunkSize = 1024 * 1024, responseType = 'arraybuffer' } = options;
20589
+ let offset = (_a = options.offset) !== null && _a !== void 0 ? _a : 0;
20590
+ for (;;) {
20591
+ let chunk;
20592
+ try {
20593
+ chunk = await this.readRange(lib, key, { offset, length: chunkSize, responseType });
20594
+ }
20595
+ catch (err) {
20596
+ // 416 means the offset is at or past the end. For the first request
20597
+ // that is an empty object; afterwards it is a benign race with a
20598
+ // truncating writer. Either way the stream is simply over.
20599
+ if (((_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status) === 416)
20600
+ return;
20601
+ throw err;
20602
+ }
20603
+ yield chunk;
20604
+ if (chunk.eof)
20605
+ return;
20606
+ // Advance from what the server actually returned, never from chunkSize:
20607
+ // the per-request cap can make a window shorter than requested, and
20608
+ // assuming otherwise would skip bytes.
20609
+ offset = chunk.end + 1;
20610
+ }
20611
+ }
20459
20612
  async download(lib, key) {
20460
20613
  return this.request('POST', `media/library/${lib}/download`, {
20461
20614
  data: {
@@ -1354,6 +1354,70 @@ class IntegrationsBaseClient extends BaseClient {
1354
1354
  }
1355
1355
  }
1356
1356
 
1357
+ /**
1358
+ * Build an RFC 7233 Range header value from the SDK's range options.
1359
+ *
1360
+ * Validation happens here rather than at the server so a typo (a float offset, a
1361
+ * negative length) fails immediately and locally instead of arriving as an opaque
1362
+ * 400 after a round trip.
1363
+ */
1364
+ function buildRangeHeader(range) {
1365
+ const { offset, length, suffix } = range;
1366
+ if (suffix !== undefined) {
1367
+ if (offset !== undefined || length !== undefined) {
1368
+ throw new Error('readRange: `suffix` cannot be combined with `offset` or `length`');
1369
+ }
1370
+ if (!Number.isInteger(suffix) || suffix <= 0) {
1371
+ throw new Error('readRange: `suffix` must be a positive integer');
1372
+ }
1373
+ return `bytes=-${suffix}`;
1374
+ }
1375
+ const start = offset !== null && offset !== void 0 ? offset : 0;
1376
+ if (!Number.isInteger(start) || start < 0) {
1377
+ throw new Error('readRange: `offset` must be a non-negative integer');
1378
+ }
1379
+ if (length === undefined) {
1380
+ return `bytes=${start}-`;
1381
+ }
1382
+ if (!Number.isInteger(length) || length <= 0) {
1383
+ throw new Error('readRange: `length` must be a positive integer');
1384
+ }
1385
+ // Range end offsets are inclusive, so a length of N ends at start + N - 1.
1386
+ return `bytes=${start}-${start + length - 1}`;
1387
+ }
1388
+ /**
1389
+ * Turn a 206 response into a ByteRangeResult.
1390
+ *
1391
+ * The Content-Range check is the important part. Requests reach media-api through
1392
+ * two proxies, and if any hop ever dropped the Range header the response would be
1393
+ * a perfectly valid 200 carrying the *entire* file — a caller asking for 64 KiB
1394
+ * of a 2 GB object would quietly receive all 2 GB. Failing loudly here turns that
1395
+ * from a silent memory blow-up into an actionable error.
1396
+ */
1397
+ function toByteRangeResult(resp) {
1398
+ var _a, _b, _c, _d, _e, _f, _g;
1399
+ const contentRange = (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _a === void 0 ? void 0 : _a['content-range']) !== null && _b !== void 0 ? _b : (_c = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _c === void 0 ? void 0 : _c['Content-Range'];
1400
+ const parsed = /^bytes\s+(\d+)-(\d+)\/(\d+)$/.exec(String(contentRange !== null && contentRange !== void 0 ? contentRange : '').trim());
1401
+ if (!parsed) {
1402
+ throw new Error('readRange: response did not include a Content-Range header, so the returned body ' +
1403
+ 'may be the entire object rather than the requested range. ' +
1404
+ 'Check that the media host supports ranged reads and that no proxy is stripping the Range header.');
1405
+ }
1406
+ const start = Number(parsed[1]);
1407
+ const end = Number(parsed[2]);
1408
+ const totalSize = Number(parsed[3]);
1409
+ const etag = (_e = (_d = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _d === void 0 ? void 0 : _d.etag) !== null && _e !== void 0 ? _e : (_f = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _f === void 0 ? void 0 : _f.ETag;
1410
+ return {
1411
+ data: resp.data,
1412
+ start,
1413
+ end,
1414
+ length: end - start + 1,
1415
+ totalSize,
1416
+ eof: end >= totalSize - 1,
1417
+ etag: typeof etag === 'string' ? etag.replace(/^"|"$/g, '') : undefined,
1418
+ contentType: (_g = resp === null || resp === void 0 ? void 0 : resp.headers) === null || _g === void 0 ? void 0 : _g['content-type'],
1419
+ };
1420
+ }
1357
1421
  /**
1358
1422
  * Document Management System (DMS) API client
1359
1423
  *
@@ -1487,6 +1551,95 @@ class DMS extends IntegrationsBaseClient {
1487
1551
  responseType: (!encoding) ? 'blob' : null
1488
1552
  });
1489
1553
  }
1554
+ /**
1555
+ * Read a slice of a stored file without transferring the whole object.
1556
+ *
1557
+ * Uses an HTTP Range request, so only the requested bytes cross the wire.
1558
+ * Slicing is positional and format-agnostic — it works on any file.
1559
+ *
1560
+ * ```ts
1561
+ * // First 64 KiB
1562
+ * const head = await dms.readRange(libraryUuid, 'reports/big.csv', { length: 65536 })
1563
+ *
1564
+ * // Continue from where that stopped
1565
+ * const next = await dms.readRange(libraryUuid, 'reports/big.csv', { offset: head.end + 1, length: 65536 })
1566
+ *
1567
+ * // Final 1 KiB
1568
+ * const tail = await dms.readRange(libraryUuid, 'reports/big.csv', { suffix: 1024 })
1569
+ * ```
1570
+ *
1571
+ * The server caps how much a single call may return, so `length` is an upper
1572
+ * bound rather than a guarantee: always advance using the returned `end`
1573
+ * rather than assuming the window you asked for. `eof` tells you when to stop.
1574
+ *
1575
+ * Byte ranges are not meaningful for .xlsx, .zip or .gz — their contents are
1576
+ * compressed as a unit, so no byte window corresponds to a range of rows.
1577
+ *
1578
+ * @param lib Library UUID
1579
+ * @param key Path of the file within the library
1580
+ * @param range Which bytes to read
1581
+ * @throws If the object is smaller than `offset` (nothing left to read), or if
1582
+ * the response came back without range metadata — see `readRange`'s
1583
+ * Content-Range check, which prevents silently receiving a whole file.
1584
+ */
1585
+ async readRange(lib, key, range = {}) {
1586
+ const { responseType = 'arraybuffer' } = range;
1587
+ const resp = await this.request('GET', `media/library/${lib}/get/${key}`, {
1588
+ headers: { Range: buildRangeHeader(range) },
1589
+ responseType,
1590
+ });
1591
+ return toByteRangeResult(resp);
1592
+ }
1593
+ /**
1594
+ * Read a file as a sequence of byte windows, newest request issued only when
1595
+ * the previous window has been consumed.
1596
+ *
1597
+ * This is the memory-bounded way to process a large file: the whole object is
1598
+ * never held at once, and a caller can stop early simply by breaking out.
1599
+ *
1600
+ * ```ts
1601
+ * for await (const chunk of dms.streamRanges(libraryUuid, 'logs/huge.ndjson', { chunkSize: 1 << 20 })) {
1602
+ * process(chunk.data) // one window at a time
1603
+ * if (foundWhatIWanted) break // no further requests are made
1604
+ * }
1605
+ * ```
1606
+ *
1607
+ * Windows are contiguous and non-overlapping, so concatenating every `data`
1608
+ * reproduces the file byte for byte. A record spanning a window boundary is
1609
+ * the caller's to reassemble — this yields bytes, not records.
1610
+ *
1611
+ * An empty object yields nothing rather than throwing.
1612
+ *
1613
+ * @param lib Library UUID
1614
+ * @param key Path of the file within the library
1615
+ * @param options Where to start and how large each window should be
1616
+ */
1617
+ async *streamRanges(lib, key, options = {}) {
1618
+ var _a, _b;
1619
+ const { chunkSize = 1024 * 1024, responseType = 'arraybuffer' } = options;
1620
+ let offset = (_a = options.offset) !== null && _a !== void 0 ? _a : 0;
1621
+ for (;;) {
1622
+ let chunk;
1623
+ try {
1624
+ chunk = await this.readRange(lib, key, { offset, length: chunkSize, responseType });
1625
+ }
1626
+ catch (err) {
1627
+ // 416 means the offset is at or past the end. For the first request
1628
+ // that is an empty object; afterwards it is a benign race with a
1629
+ // truncating writer. Either way the stream is simply over.
1630
+ if (((_b = err === null || err === void 0 ? void 0 : err.response) === null || _b === void 0 ? void 0 : _b.status) === 416)
1631
+ return;
1632
+ throw err;
1633
+ }
1634
+ yield chunk;
1635
+ if (chunk.eof)
1636
+ return;
1637
+ // Advance from what the server actually returned, never from chunkSize:
1638
+ // the per-request cap can make a window shorter than requested, and
1639
+ // assuming otherwise would skip bytes.
1640
+ offset = chunk.end + 1;
1641
+ }
1642
+ }
1490
1643
  async download(lib, key) {
1491
1644
  return this.request('POST', `media/library/${lib}/download`, {
1492
1645
  data: {
@@ -307,3 +307,47 @@ export type DocumentCommentPayload = {
307
307
  /** Comment end position */
308
308
  to?: number;
309
309
  };
310
+ /**
311
+ * Which bytes to read. Supply either `offset`/`length` or `suffix`, not both.
312
+ */
313
+ export type ByteRangeRequest = {
314
+ /** First byte to read, 0-based. Defaults to 0. */
315
+ offset?: number;
316
+ /** How many bytes to read. Omit to read to the end of the object, subject to the server's per-request cap. */
317
+ length?: number;
318
+ /** Read the final N bytes instead. Mutually exclusive with `offset`/`length`. */
319
+ suffix?: number;
320
+ /** Axios response type for the body. Defaults to `arraybuffer`. */
321
+ responseType?: 'arraybuffer' | 'blob' | 'text';
322
+ };
323
+ /**
324
+ * A single window of bytes plus the position metadata needed to request the next one.
325
+ */
326
+ export type ByteRangeResult<T = any> = {
327
+ /** The bytes, in the shape requested via `responseType`. */
328
+ data: T;
329
+ /** Offset of the first byte returned. */
330
+ start: number;
331
+ /** Offset of the last byte returned, inclusive. */
332
+ end: number;
333
+ /** Number of bytes returned. May be less than requested — see `ByteRangeRequest.length`. */
334
+ length: number;
335
+ /** Total size of the whole object. */
336
+ totalSize: number;
337
+ /** True when this window ends at the last byte of the object. */
338
+ eof: boolean;
339
+ /** Object validator. Changes if the object is replaced, so a multi-window scan can detect it. */
340
+ etag?: string;
341
+ contentType?: string;
342
+ };
343
+ /**
344
+ * Options for walking an object window by window.
345
+ */
346
+ export type ByteStreamOptions = {
347
+ /** Byte offset to start from. Defaults to 0. */
348
+ offset?: number;
349
+ /** Bytes per window. Defaults to 1 MiB. The server may return less. */
350
+ chunkSize?: number;
351
+ /** Axios response type for each window. Defaults to `arraybuffer`. */
352
+ responseType?: 'arraybuffer' | 'blob' | 'text';
353
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptkl/sdk",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "scripts": {
5
5
  "build": "rollup -c",
6
6
  "build:monaco": "npm run build && node scripts/generate-monaco-types.cjs",