@gigadrive/sdk 0.8.6 → 0.9.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.
- package/README.md +81 -0
- package/dist/index.d.mts +1262 -99
- package/dist/index.d.ts +1262 -99
- package/dist/index.js +1274 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1253 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -22,11 +22,17 @@ var ApiError = class extends GigadriveError {
|
|
|
22
22
|
status;
|
|
23
23
|
/** An optional machine-readable error code from the API response body. */
|
|
24
24
|
code;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* An optional finer-grained reason next to {@link code}, for example `daily_add_limit` with
|
|
27
|
+
* `quota_exceeded`, or `other_organization` with `domain_in_use`.
|
|
28
|
+
*/
|
|
29
|
+
reason;
|
|
30
|
+
constructor(message, status, code, reason) {
|
|
26
31
|
super(message);
|
|
27
32
|
this.name = "ApiError";
|
|
28
33
|
this.status = status;
|
|
29
34
|
this.code = code;
|
|
35
|
+
this.reason = reason;
|
|
30
36
|
}
|
|
31
37
|
};
|
|
32
38
|
var UploadError = class extends GigadriveError {
|
|
@@ -514,10 +520,13 @@ var HttpClient = class {
|
|
|
514
520
|
async toApiError(response) {
|
|
515
521
|
let message;
|
|
516
522
|
let code;
|
|
523
|
+
let reason;
|
|
517
524
|
try {
|
|
518
525
|
const body = await response.json();
|
|
519
526
|
if (typeof body.error === "string") {
|
|
520
527
|
message = body.error;
|
|
528
|
+
if (typeof body.code === "string") code = body.code;
|
|
529
|
+
if (typeof body.reason === "string") reason = body.reason;
|
|
521
530
|
} else if (body.error && typeof body.error === "object") {
|
|
522
531
|
message = body.error.message ?? response.statusText;
|
|
523
532
|
code = body.error.code;
|
|
@@ -527,7 +536,7 @@ var HttpClient = class {
|
|
|
527
536
|
} catch {
|
|
528
537
|
message = response.statusText;
|
|
529
538
|
}
|
|
530
|
-
return new ApiError(message, response.status, code);
|
|
539
|
+
return new ApiError(message, response.status, code, reason);
|
|
531
540
|
}
|
|
532
541
|
buildUrl(path, query) {
|
|
533
542
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -887,6 +896,7 @@ var ApplicationRequestsResource = class extends BaseResource {
|
|
|
887
896
|
};
|
|
888
897
|
|
|
889
898
|
// src/upload/checksum.ts
|
|
899
|
+
var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
890
900
|
var toHex = (buffer) => {
|
|
891
901
|
const bytes = new Uint8Array(buffer);
|
|
892
902
|
let hex = "";
|
|
@@ -1005,6 +1015,8 @@ var inferContentType = (nameOrKey) => {
|
|
|
1005
1015
|
};
|
|
1006
1016
|
|
|
1007
1017
|
// src/upload/source.ts
|
|
1018
|
+
var noop = () => {
|
|
1019
|
+
};
|
|
1008
1020
|
var isNode = () => typeof process !== "undefined" && !!process.versions?.node;
|
|
1009
1021
|
var toBytes = async (data) => {
|
|
1010
1022
|
if (typeof Blob !== "undefined" && data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
@@ -1051,7 +1063,8 @@ var resolveUploadSource = async (input, options = {}) => {
|
|
|
1051
1063
|
size,
|
|
1052
1064
|
contentType,
|
|
1053
1065
|
checksums: buildChecksums(input, sha256),
|
|
1054
|
-
requiresFiniteChunkSize: false
|
|
1066
|
+
requiresFiniteChunkSize: false,
|
|
1067
|
+
release: noop
|
|
1055
1068
|
};
|
|
1056
1069
|
}
|
|
1057
1070
|
if (input.path !== void 0) {
|
|
@@ -1059,22 +1072,34 @@ var resolveUploadSource = async (input, options = {}) => {
|
|
|
1059
1072
|
const fs = await import("fs");
|
|
1060
1073
|
const size = input.contentLength ?? fs.statSync(input.path).size;
|
|
1061
1074
|
const sha256 = input.checksumSha256 ?? (hash ? (await hashNodeFile(input.path)).sha256 : "");
|
|
1062
|
-
const
|
|
1063
|
-
return {
|
|
1075
|
+
const stream = fs.createReadStream(input.path);
|
|
1076
|
+
return {
|
|
1077
|
+
tusFile: stream,
|
|
1078
|
+
size,
|
|
1079
|
+
contentType,
|
|
1080
|
+
checksums: buildChecksums(input, sha256),
|
|
1081
|
+
requiresFiniteChunkSize: true,
|
|
1082
|
+
release: () => {
|
|
1083
|
+
stream.on("error", noop);
|
|
1084
|
+
stream.destroy();
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1064
1087
|
}
|
|
1065
1088
|
if (input.stream !== void 0) {
|
|
1066
1089
|
if (input.contentLength === void 0) {
|
|
1067
1090
|
throw new Error("Uploading from a stream requires contentLength to be provided.");
|
|
1068
1091
|
}
|
|
1069
|
-
|
|
1092
|
+
const sha256 = input.checksumSha256 ?? (input.contentLength === 0 ? EMPTY_SHA256 : void 0);
|
|
1093
|
+
if (hash && !sha256) {
|
|
1070
1094
|
throw new Error("Uploading from a stream requires checksumSha256 to be provided.");
|
|
1071
1095
|
}
|
|
1072
1096
|
return {
|
|
1073
1097
|
tusFile: input.stream,
|
|
1074
1098
|
size: input.contentLength,
|
|
1075
1099
|
contentType,
|
|
1076
|
-
checksums: buildChecksums(input,
|
|
1077
|
-
requiresFiniteChunkSize: true
|
|
1100
|
+
checksums: buildChecksums(input, sha256 ?? ""),
|
|
1101
|
+
requiresFiniteChunkSize: true,
|
|
1102
|
+
release: noop
|
|
1078
1103
|
};
|
|
1079
1104
|
}
|
|
1080
1105
|
throw new Error("No upload source provided. Pass one of: data, path, or stream.");
|
|
@@ -1447,7 +1472,7 @@ var StorageUploadSessionsResource = class extends BaseResource {
|
|
|
1447
1472
|
async uploadToUrl(url, source, options) {
|
|
1448
1473
|
const resolved = await resolveUploadSource({ key: "", ...source }, { hash: false });
|
|
1449
1474
|
const headers = { "Tus-Resumable": "1.0.0", ...options?.headers };
|
|
1450
|
-
await runResolvedUpload(this.transport, url, resolved, options, headers).catch(toUploadError);
|
|
1475
|
+
await runResolvedUpload(this.transport, url, resolved, options, headers).catch(toUploadError).finally(resolved.release);
|
|
1451
1476
|
}
|
|
1452
1477
|
/**
|
|
1453
1478
|
* Resume an interrupted upload to a previously issued signed upload URL. The
|
|
@@ -1464,6 +1489,7 @@ var StorageUploadSessionsResource = class extends BaseResource {
|
|
|
1464
1489
|
|
|
1465
1490
|
// src/resources/application-storage.ts
|
|
1466
1491
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1492
|
+
var buildStorageObjectUrl = (cdnHostname, key) => `https://${cdnHostname}/${encodeURIComponent(key).replace(/%2F/g, "/")}`;
|
|
1467
1493
|
var ApplicationStorageResource = class {
|
|
1468
1494
|
constructor(httpClient, transport = tusUploadTransport, defaultApplicationId) {
|
|
1469
1495
|
this.defaultApplicationId = defaultApplicationId;
|
|
@@ -1493,6 +1519,10 @@ var ApplicationStorageResource = class {
|
|
|
1493
1519
|
* Node filesystem `path`, or a Node readable `stream` (with `contentLength`
|
|
1494
1520
|
* and `checksumSha256`). The content type is inferred from `key` when omitted.
|
|
1495
1521
|
*
|
|
1522
|
+
* Empty (zero-byte) files are supported for every input kind. The API stores
|
|
1523
|
+
* them during session creation, so no bytes are transferred, `onProgress` is
|
|
1524
|
+
* not called, `waitForCompletion` does not poll, and `object` is always set.
|
|
1525
|
+
*
|
|
1496
1526
|
* @param input - What and where to upload, plus optional transfer options.
|
|
1497
1527
|
* @returns The upload session and public object URL (and the finalized object when `waitForCompletion` is set).
|
|
1498
1528
|
* @throws {@link UploadError} if the byte upload fails.
|
|
@@ -1523,19 +1553,42 @@ var ApplicationStorageResource = class {
|
|
|
1523
1553
|
checksumSha1: input.checksumSha1,
|
|
1524
1554
|
checksumMd5: input.checksumMd5
|
|
1525
1555
|
});
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1556
|
+
if (input.signal?.aborted) {
|
|
1557
|
+
resolved.release();
|
|
1558
|
+
throw createAbortError();
|
|
1559
|
+
}
|
|
1560
|
+
let created;
|
|
1561
|
+
try {
|
|
1562
|
+
created = await this.uploadSessions.create(
|
|
1563
|
+
applicationId,
|
|
1564
|
+
bucketRef,
|
|
1565
|
+
{
|
|
1566
|
+
key: input.key,
|
|
1567
|
+
contentLength: resolved.size,
|
|
1568
|
+
checksumSha256: resolved.checksums.sha256,
|
|
1569
|
+
contentType: resolved.contentType,
|
|
1570
|
+
checksumSha1: resolved.checksums.sha1,
|
|
1571
|
+
checksumMd5: resolved.checksums.md5
|
|
1572
|
+
},
|
|
1573
|
+
{ environment }
|
|
1574
|
+
);
|
|
1575
|
+
} catch (error) {
|
|
1576
|
+
resolved.release();
|
|
1577
|
+
throw error;
|
|
1578
|
+
}
|
|
1579
|
+
const { session, upload } = created;
|
|
1580
|
+
if (upload === null) {
|
|
1581
|
+
resolved.release();
|
|
1582
|
+
const object = created.object;
|
|
1583
|
+
if (!object) {
|
|
1584
|
+
throw new UploadError("The upload session completed without returning the stored object.");
|
|
1585
|
+
}
|
|
1586
|
+
const url = created.publicObjectUrl ?? buildStorageObjectUrl(
|
|
1587
|
+
(await this.buckets.get(applicationId, bucketRef, { environment })).cdnHostname,
|
|
1588
|
+
object.key
|
|
1589
|
+
);
|
|
1590
|
+
return { session, url, object };
|
|
1591
|
+
}
|
|
1539
1592
|
await runResolvedUpload(
|
|
1540
1593
|
this.transport,
|
|
1541
1594
|
upload.url,
|
|
@@ -1550,7 +1603,7 @@ var ApplicationStorageResource = class {
|
|
|
1550
1603
|
},
|
|
1551
1604
|
// Forward any required headers the API issued with the session.
|
|
1552
1605
|
{ "Tus-Resumable": "1.0.0", ...upload.headers }
|
|
1553
|
-
).catch(toUploadError);
|
|
1606
|
+
).catch(toUploadError).finally(resolved.release);
|
|
1554
1607
|
if (input.waitForCompletion) {
|
|
1555
1608
|
const options = completionOptions ?? {};
|
|
1556
1609
|
const completed = await this.waitForCompletion(applicationId, bucketRef, session.id, {
|
|
@@ -1629,6 +1682,201 @@ var ApplicationStorageResource = class {
|
|
|
1629
1682
|
}
|
|
1630
1683
|
};
|
|
1631
1684
|
|
|
1685
|
+
// src/resources/domains.ts
|
|
1686
|
+
var DomainNotActiveError = class extends GigadriveError {
|
|
1687
|
+
/** The domain as of the last successful check. */
|
|
1688
|
+
domain;
|
|
1689
|
+
/** `timeout` when the wait ran out, `removing` when the domain was removed, otherwise the state that ended it. */
|
|
1690
|
+
reason;
|
|
1691
|
+
constructor(domain, reason) {
|
|
1692
|
+
const detail = domain.error ? `: ${domain.error.message}` : "";
|
|
1693
|
+
super(
|
|
1694
|
+
reason === "timeout" ? `${domain.unicodeHostname} is not active yet (state: ${domain.state})${detail}` : `${domain.unicodeHostname} is ${reason}${detail}`
|
|
1695
|
+
);
|
|
1696
|
+
this.name = "DomainNotActiveError";
|
|
1697
|
+
this.domain = domain;
|
|
1698
|
+
this.reason = reason;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
var SERVING_STATES = /* @__PURE__ */ new Set(["active", "degraded"]);
|
|
1702
|
+
var isTransient = (error) => error instanceof ApiError ? error.status === 429 || error.status >= 500 : error instanceof TypeError;
|
|
1703
|
+
var pollSignal = (ms, signal) => {
|
|
1704
|
+
const controller = new AbortController();
|
|
1705
|
+
const timer = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), ms);
|
|
1706
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
1707
|
+
if (signal?.aborted) onAbort();
|
|
1708
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1709
|
+
return {
|
|
1710
|
+
signal: controller.signal,
|
|
1711
|
+
dispose: () => {
|
|
1712
|
+
clearTimeout(timer);
|
|
1713
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
};
|
|
1717
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
1718
|
+
if (signal?.aborted) {
|
|
1719
|
+
reject(signal.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
const timer = setTimeout(() => {
|
|
1723
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1724
|
+
resolve();
|
|
1725
|
+
}, ms);
|
|
1726
|
+
const onAbort = () => {
|
|
1727
|
+
clearTimeout(timer);
|
|
1728
|
+
reject(signal?.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
|
|
1729
|
+
};
|
|
1730
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1731
|
+
});
|
|
1732
|
+
var ApplicationDomainsResource = class extends BaseResource {
|
|
1733
|
+
/**
|
|
1734
|
+
* List the custom domains of an application.
|
|
1735
|
+
*
|
|
1736
|
+
* @example
|
|
1737
|
+
* ```ts
|
|
1738
|
+
* const { items } = await client.applications.domains.list('app-id');
|
|
1739
|
+
* for (const domain of items) console.log(domain.hostname, domain.state);
|
|
1740
|
+
* ```
|
|
1741
|
+
*/
|
|
1742
|
+
async list(applicationId, query) {
|
|
1743
|
+
return this.httpClient.get(`/applications/${applicationId}/domains`, {
|
|
1744
|
+
query
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Attach a hostname to an application.
|
|
1749
|
+
*
|
|
1750
|
+
* @returns The new domain, including the `requiredRecords` to publish at your DNS provider.
|
|
1751
|
+
*
|
|
1752
|
+
* @example
|
|
1753
|
+
* ```ts
|
|
1754
|
+
* const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
|
|
1755
|
+
* for (const record of domain.requiredRecords) console.log(record.type, record.host, record.value);
|
|
1756
|
+
* ```
|
|
1757
|
+
*/
|
|
1758
|
+
async add(applicationId, data) {
|
|
1759
|
+
return this.httpClient.post(`/applications/${applicationId}/domains`, data);
|
|
1760
|
+
}
|
|
1761
|
+
/** Get one custom domain with its current state. */
|
|
1762
|
+
async get(applicationId, domainId, options) {
|
|
1763
|
+
const path = `/applications/${applicationId}/domains/${domainId}`;
|
|
1764
|
+
return options?.signal ? this.httpClient.get(path, { signal: options.signal }) : this.httpClient.get(path);
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Change what a domain serves, or make it the primary domain.
|
|
1768
|
+
*
|
|
1769
|
+
* @example
|
|
1770
|
+
* ```ts
|
|
1771
|
+
* await client.applications.domains.update('app-id', 'domain-id', {
|
|
1772
|
+
* target: { type: 'redirect', to: 'www.example.com' },
|
|
1773
|
+
* });
|
|
1774
|
+
* ```
|
|
1775
|
+
*/
|
|
1776
|
+
async update(applicationId, domainId, data) {
|
|
1777
|
+
return this.httpClient.patch(`/applications/${applicationId}/domains/${domainId}`, data);
|
|
1778
|
+
}
|
|
1779
|
+
/** Check the domain now instead of waiting for the next scheduled check. Rate limited. */
|
|
1780
|
+
async refresh(applicationId, domainId) {
|
|
1781
|
+
return this.httpClient.post(`/applications/${applicationId}/domains/${domainId}/refresh`);
|
|
1782
|
+
}
|
|
1783
|
+
/** Stop serving the domain and remove it. */
|
|
1784
|
+
async remove(applicationId, domainId) {
|
|
1785
|
+
return this.httpClient.delete(`/applications/${applicationId}/domains/${domainId}`);
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Move a hostname another organization attached to this application. Verify the domain for your
|
|
1789
|
+
* organization first and keep its TXT record published: the claim checks the record again.
|
|
1790
|
+
*/
|
|
1791
|
+
async claim(applicationId, hostname) {
|
|
1792
|
+
return this.httpClient.post(`/applications/${applicationId}/domains/claim`, { hostname });
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Poll a domain until it serves traffic.
|
|
1796
|
+
*
|
|
1797
|
+
* Rate limits, server errors and network failures do not end the wait; the next poll retries until
|
|
1798
|
+
* the timeout. Aborting the signal or reaching the timeout also cancels a request in flight.
|
|
1799
|
+
*
|
|
1800
|
+
* @returns The domain once it is `active` (or `degraded`, which also serves).
|
|
1801
|
+
* @throws {DomainNotActiveError} When it fails, is suspended or removed, or the timeout runs out.
|
|
1802
|
+
*
|
|
1803
|
+
* @example
|
|
1804
|
+
* ```ts
|
|
1805
|
+
* const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
|
|
1806
|
+
* await client.applications.domains.waitUntilActive('app-id', domain.id, {
|
|
1807
|
+
* onState: (current) => console.log(current.state),
|
|
1808
|
+
* });
|
|
1809
|
+
* ```
|
|
1810
|
+
*/
|
|
1811
|
+
async waitUntilActive(applicationId, domainId, options = {}) {
|
|
1812
|
+
const { timeoutMs = 10 * 6e4, intervalMs = 5e3, signal, onState } = options;
|
|
1813
|
+
const deadline = Date.now() + timeoutMs;
|
|
1814
|
+
let last;
|
|
1815
|
+
for (; ; ) {
|
|
1816
|
+
const poll = pollSignal(Math.max(deadline - Date.now(), 1), signal);
|
|
1817
|
+
let domain;
|
|
1818
|
+
try {
|
|
1819
|
+
domain = await this.get(applicationId, domainId, { signal: poll.signal });
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
if (signal?.aborted) throw error;
|
|
1822
|
+
if (last && error instanceof ApiError && error.status === 404) throw new DomainNotActiveError(last, "removing");
|
|
1823
|
+
if (!poll.signal.aborted && !isTransient(error)) throw error;
|
|
1824
|
+
if (Date.now() >= deadline && !last) throw error;
|
|
1825
|
+
} finally {
|
|
1826
|
+
poll.dispose();
|
|
1827
|
+
}
|
|
1828
|
+
if (domain) {
|
|
1829
|
+
if (domain.state !== last?.state) onState?.(domain);
|
|
1830
|
+
last = domain;
|
|
1831
|
+
if (SERVING_STATES.has(domain.state)) return domain;
|
|
1832
|
+
if (domain.state === "failed" || domain.state === "suspended" || domain.state === "removing") {
|
|
1833
|
+
throw new DomainNotActiveError(domain, domain.state);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
const untilDeadline = deadline - Date.now();
|
|
1837
|
+
if (untilDeadline <= 0) {
|
|
1838
|
+
if (last) throw new DomainNotActiveError(last, "timeout");
|
|
1839
|
+
throw new GigadriveError("Timed out before the domain could be read");
|
|
1840
|
+
}
|
|
1841
|
+
await sleep(Math.min(intervalMs, untilDeadline), signal);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
var OrganizationDomainsResource = class extends BaseResource {
|
|
1846
|
+
/** List the organization's claimed and verified domains. */
|
|
1847
|
+
async list(organizationId, query) {
|
|
1848
|
+
return this.httpClient.get(`/organizations/${organizationId}/domains`, {
|
|
1849
|
+
query
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Claim a domain for verification.
|
|
1854
|
+
*
|
|
1855
|
+
* @returns The claim, including the TXT `record` to publish.
|
|
1856
|
+
*
|
|
1857
|
+
* @example
|
|
1858
|
+
* ```ts
|
|
1859
|
+
* const claim = await client.organizations.domains.add('org-id', 'example.com');
|
|
1860
|
+
* console.log(claim.record.host, claim.record.value);
|
|
1861
|
+
* ```
|
|
1862
|
+
*/
|
|
1863
|
+
async add(organizationId, name) {
|
|
1864
|
+
return this.httpClient.post(`/organizations/${organizationId}/domains`, { name });
|
|
1865
|
+
}
|
|
1866
|
+
/** Get one claimed or verified domain. */
|
|
1867
|
+
async get(organizationId, domainId) {
|
|
1868
|
+
return this.httpClient.get(`/organizations/${organizationId}/domains/${domainId}`);
|
|
1869
|
+
}
|
|
1870
|
+
/** Check the TXT record now. Rate limited. */
|
|
1871
|
+
async verify(organizationId, domainId) {
|
|
1872
|
+
return this.httpClient.post(`/organizations/${organizationId}/domains/${domainId}/verify`);
|
|
1873
|
+
}
|
|
1874
|
+
/** Remove a domain. Refused while custom domains still rely on it. */
|
|
1875
|
+
async remove(organizationId, domainId) {
|
|
1876
|
+
return this.httpClient.delete(`/organizations/${organizationId}/domains/${domainId}`);
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1632
1880
|
// src/resources/applications.ts
|
|
1633
1881
|
var ApplicationsResource = class extends BaseResource {
|
|
1634
1882
|
/**
|
|
@@ -1670,11 +1918,22 @@ var ApplicationsResource = class extends BaseResource {
|
|
|
1670
1918
|
* ```
|
|
1671
1919
|
*/
|
|
1672
1920
|
requests;
|
|
1921
|
+
/**
|
|
1922
|
+
* Custom domains attached to the application.
|
|
1923
|
+
*
|
|
1924
|
+
* @example
|
|
1925
|
+
* ```ts
|
|
1926
|
+
* const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
|
|
1927
|
+
* await client.applications.domains.waitUntilActive('app-id', domain.id);
|
|
1928
|
+
* ```
|
|
1929
|
+
*/
|
|
1930
|
+
domains;
|
|
1673
1931
|
constructor(httpClient, defaultApplicationId) {
|
|
1674
1932
|
super(httpClient);
|
|
1675
1933
|
this.envVars = new ApplicationEnvVarsResource(this.httpClient);
|
|
1676
1934
|
this.storage = new ApplicationStorageResource(this.httpClient, void 0, defaultApplicationId);
|
|
1677
1935
|
this.requests = new ApplicationRequestsResource(this.httpClient);
|
|
1936
|
+
this.domains = new ApplicationDomainsResource(this.httpClient);
|
|
1678
1937
|
}
|
|
1679
1938
|
/**
|
|
1680
1939
|
* List applications the authenticated actor has access to.
|
|
@@ -2300,12 +2559,23 @@ var OrganizationsResource = class extends BaseResource {
|
|
|
2300
2559
|
* ```
|
|
2301
2560
|
*/
|
|
2302
2561
|
products;
|
|
2562
|
+
/**
|
|
2563
|
+
* Domains the organization verified. A verified domain lets every application attach hostnames
|
|
2564
|
+
* below it without another TXT record.
|
|
2565
|
+
*
|
|
2566
|
+
* @example
|
|
2567
|
+
* ```ts
|
|
2568
|
+
* const claim = await client.organizations.domains.add('org-id', 'example.com');
|
|
2569
|
+
* ```
|
|
2570
|
+
*/
|
|
2571
|
+
domains;
|
|
2303
2572
|
constructor(...args) {
|
|
2304
2573
|
super(...args);
|
|
2305
2574
|
this.envVars = new OrganizationEnvVarsResource(this.httpClient);
|
|
2306
2575
|
this.aiGateway = new OrganizationAiGatewayResource(this.httpClient);
|
|
2307
2576
|
this.members = new OrganizationMembersResource(this.httpClient);
|
|
2308
2577
|
this.products = new OrganizationProductsResource(this.httpClient);
|
|
2578
|
+
this.domains = new OrganizationDomainsResource(this.httpClient);
|
|
2309
2579
|
}
|
|
2310
2580
|
/**
|
|
2311
2581
|
* List organizations the authenticated actor has access to.
|
|
@@ -2359,6 +2629,783 @@ var OrganizationsResource = class extends BaseResource {
|
|
|
2359
2629
|
}
|
|
2360
2630
|
};
|
|
2361
2631
|
|
|
2632
|
+
// src/queues/encoding.ts
|
|
2633
|
+
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
2634
|
+
var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
|
|
2635
|
+
var durationToSeconds = (value) => {
|
|
2636
|
+
let seconds;
|
|
2637
|
+
if (typeof value === "number") {
|
|
2638
|
+
seconds = value;
|
|
2639
|
+
} else {
|
|
2640
|
+
const match = DURATION_PATTERN.exec(value.trim());
|
|
2641
|
+
if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
|
|
2642
|
+
seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
|
|
2643
|
+
}
|
|
2644
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
|
|
2645
|
+
return Math.ceil(seconds);
|
|
2646
|
+
};
|
|
2647
|
+
var textEncoder = new TextEncoder();
|
|
2648
|
+
var textDecoder = new TextDecoder();
|
|
2649
|
+
var utf8Encode = (value) => textEncoder.encode(value);
|
|
2650
|
+
var utf8Decode = (bytes) => textDecoder.decode(bytes);
|
|
2651
|
+
var bytesToBase64 = (bytes) => {
|
|
2652
|
+
let binary = "";
|
|
2653
|
+
for (let index = 0; index < bytes.length; index += 32768) {
|
|
2654
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
2655
|
+
}
|
|
2656
|
+
return btoa(binary);
|
|
2657
|
+
};
|
|
2658
|
+
var base64ToBytes = (value) => {
|
|
2659
|
+
const binary = atob(value);
|
|
2660
|
+
const bytes = new Uint8Array(binary.length);
|
|
2661
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
2662
|
+
return bytes;
|
|
2663
|
+
};
|
|
2664
|
+
var toHex2 = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2665
|
+
var toArrayBuffer = (bytes) => {
|
|
2666
|
+
const copy = new ArrayBuffer(bytes.byteLength);
|
|
2667
|
+
new Uint8Array(copy).set(bytes);
|
|
2668
|
+
return copy;
|
|
2669
|
+
};
|
|
2670
|
+
|
|
2671
|
+
// src/queues/errors.ts
|
|
2672
|
+
var RetryLaterError = class extends GigadriveError {
|
|
2673
|
+
/** Seconds until the next delivery. */
|
|
2674
|
+
delaySeconds;
|
|
2675
|
+
constructor(delay2, message = "Message deferred") {
|
|
2676
|
+
super(message);
|
|
2677
|
+
this.name = "RetryLaterError";
|
|
2678
|
+
this.delaySeconds = durationToSeconds(delay2);
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
var NonRetryableError = class extends GigadriveError {
|
|
2682
|
+
constructor(message) {
|
|
2683
|
+
super(message);
|
|
2684
|
+
this.name = "NonRetryableError";
|
|
2685
|
+
}
|
|
2686
|
+
};
|
|
2687
|
+
|
|
2688
|
+
// src/queues/signature.ts
|
|
2689
|
+
var QUEUE_SIGNATURE_HEADER = "x-gigadrive-queue-signature";
|
|
2690
|
+
var QUEUE_SIGNATURE_TOLERANCE_MS = 5 * 6e4;
|
|
2691
|
+
var QUEUE_SIGNING_SECRET_ENV = "GIGADRIVE_QUEUE_SIGNING_SECRET";
|
|
2692
|
+
var hmacSha256Hex = async (secret, payload) => {
|
|
2693
|
+
const key = await crypto.subtle.importKey(
|
|
2694
|
+
"raw",
|
|
2695
|
+
toArrayBuffer(utf8Encode(secret)),
|
|
2696
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2697
|
+
false,
|
|
2698
|
+
["sign"]
|
|
2699
|
+
);
|
|
2700
|
+
return toHex2(await crypto.subtle.sign("HMAC", key, toArrayBuffer(utf8Encode(payload))));
|
|
2701
|
+
};
|
|
2702
|
+
var signedPayload = async (input, timestamp) => {
|
|
2703
|
+
const body = typeof input.body === "string" ? utf8Encode(input.body) : input.body;
|
|
2704
|
+
const bodyHash = toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(body)));
|
|
2705
|
+
return `${String(timestamp)}.${input.queue}.${input.messageId}.${String(input.attempt)}.${bodyHash}`;
|
|
2706
|
+
};
|
|
2707
|
+
var safeEqual = (left, right) => {
|
|
2708
|
+
if (left.length !== right.length) return false;
|
|
2709
|
+
let difference = 0;
|
|
2710
|
+
for (let index = 0; index < left.length; index++) difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
2711
|
+
return difference === 0;
|
|
2712
|
+
};
|
|
2713
|
+
async function signQueueDelivery(input, timestamp = Date.now()) {
|
|
2714
|
+
return `t=${String(timestamp)},v1=${await hmacSha256Hex(input.secret, await signedPayload(input, timestamp))}`;
|
|
2715
|
+
}
|
|
2716
|
+
async function verifyQueueSignature(header, input, now = Date.now()) {
|
|
2717
|
+
if (!header || !input.secret) return false;
|
|
2718
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2719
|
+
for (const part of header.split(",")) {
|
|
2720
|
+
const separator = part.indexOf("=");
|
|
2721
|
+
if (separator > 0) fields.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
|
|
2722
|
+
}
|
|
2723
|
+
const timestamp = Number(fields.get("t"));
|
|
2724
|
+
const signature = fields.get("v1");
|
|
2725
|
+
if (!Number.isSafeInteger(timestamp) || !signature || Math.abs(now - timestamp) > QUEUE_SIGNATURE_TOLERANCE_MS) {
|
|
2726
|
+
return false;
|
|
2727
|
+
}
|
|
2728
|
+
const expected = await hmacSha256Hex(input.secret, await signedPayload(input, timestamp));
|
|
2729
|
+
return safeEqual(signature.toLowerCase(), expected);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// src/queues/queue.ts
|
|
2733
|
+
var QUEUE_DELIVERY_HEADERS = {
|
|
2734
|
+
name: "x-gigadrive-queue-name",
|
|
2735
|
+
messageId: "x-gigadrive-queue-message-id",
|
|
2736
|
+
attempt: "x-gigadrive-queue-attempt",
|
|
2737
|
+
createdAt: "x-gigadrive-queue-created-at",
|
|
2738
|
+
expiresAt: "x-gigadrive-queue-expires-at",
|
|
2739
|
+
deadline: "x-gigadrive-queue-deadline",
|
|
2740
|
+
groupKey: "x-gigadrive-queue-group-key",
|
|
2741
|
+
deduplicationKey: "x-gigadrive-queue-deduplication-key",
|
|
2742
|
+
deploymentId: "x-gigadrive-queue-deployment-id",
|
|
2743
|
+
deploymentFallback: "x-gigadrive-queue-deployment-fallback",
|
|
2744
|
+
signature: QUEUE_SIGNATURE_HEADER
|
|
2745
|
+
};
|
|
2746
|
+
var QUEUE_RESPONSE_HEADERS = {
|
|
2747
|
+
/** On a 2xx: deliver again after this many seconds without spending an attempt. */
|
|
2748
|
+
retryAfter: "x-gigadrive-queue-retry-after",
|
|
2749
|
+
/** `dead-letter`: stop retrying now, whatever the status. */
|
|
2750
|
+
action: "x-gigadrive-queue-action",
|
|
2751
|
+
/**
|
|
2752
|
+
* Reason recorded on the message. Read only together with `action:
|
|
2753
|
+
* dead-letter`; for any other failure the platform records `HTTP <status>`
|
|
2754
|
+
* and the first 512 bytes of the response body.
|
|
2755
|
+
*/
|
|
2756
|
+
error: "x-gigadrive-queue-error"
|
|
2757
|
+
};
|
|
2758
|
+
var isJsonContentType = (contentType) => /^application\/(?:[\w.+-]+\+)?json\b/i.test(contentType);
|
|
2759
|
+
var defaultQueueCodec = {
|
|
2760
|
+
encode(payload) {
|
|
2761
|
+
if (payload instanceof Uint8Array || payload instanceof ArrayBuffer) {
|
|
2762
|
+
const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
|
|
2763
|
+
return { body: bytesToBase64(bytes), encoding: "base64", contentType: "application/octet-stream" };
|
|
2764
|
+
}
|
|
2765
|
+
const json = JSON.stringify(payload);
|
|
2766
|
+
if (json === void 0) throw new TypeError("Queue payloads must be JSON-serializable or binary");
|
|
2767
|
+
return { body: json, encoding: "utf8", contentType: "application/json" };
|
|
2768
|
+
},
|
|
2769
|
+
decode(body, contentType) {
|
|
2770
|
+
if (isJsonContentType(contentType)) return body.length === 0 ? null : JSON.parse(utf8Decode(body));
|
|
2771
|
+
if (/^text\//i.test(contentType)) return utf8Decode(body);
|
|
2772
|
+
return body;
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
var errorText = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2e3);
|
|
2776
|
+
var headerText = (text) => text.replace(/[^\x20-\x7e]/g, " ").trim().slice(0, 500);
|
|
2777
|
+
var toDate = (value) => {
|
|
2778
|
+
if (!value) return null;
|
|
2779
|
+
const date = new Date(/^\d+$/.test(value) ? Number(value) : value);
|
|
2780
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
2781
|
+
};
|
|
2782
|
+
var BATCH_LIMIT = 100;
|
|
2783
|
+
var ACK_FLUSH_MS = 25;
|
|
2784
|
+
var ACK_RETRY_DELAYS_MS = [200, 1e3];
|
|
2785
|
+
var EMPTY_POLL_MIN_MS = 1e3;
|
|
2786
|
+
var EMPTY_POLL_MAX_MS = 5e3;
|
|
2787
|
+
var sleep2 = (ms, signal) => new Promise((resolve) => {
|
|
2788
|
+
if (signal?.aborted) return resolve();
|
|
2789
|
+
const timer = setTimeout(done, ms);
|
|
2790
|
+
function done() {
|
|
2791
|
+
clearTimeout(timer);
|
|
2792
|
+
signal?.removeEventListener("abort", done);
|
|
2793
|
+
resolve();
|
|
2794
|
+
}
|
|
2795
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
2796
|
+
});
|
|
2797
|
+
var Queue = class {
|
|
2798
|
+
constructor(name, resource, options = {}) {
|
|
2799
|
+
this.name = name;
|
|
2800
|
+
this.resource = resource;
|
|
2801
|
+
this.options = options;
|
|
2802
|
+
this.codec = options.codec ?? defaultQueueCodec;
|
|
2803
|
+
}
|
|
2804
|
+
name;
|
|
2805
|
+
resource;
|
|
2806
|
+
options;
|
|
2807
|
+
codec;
|
|
2808
|
+
get scope() {
|
|
2809
|
+
return { applicationId: this.options.applicationId, environment: this.options.environment };
|
|
2810
|
+
}
|
|
2811
|
+
toMessage(payload, options = {}) {
|
|
2812
|
+
const encoded = this.codec.encode(payload);
|
|
2813
|
+
const at = options.at === void 0 ? void 0 : new Date(options.at);
|
|
2814
|
+
if (at && Number.isNaN(at.getTime())) throw new ConfigurationError("Invalid `at` time for a queue message");
|
|
2815
|
+
return {
|
|
2816
|
+
body: encoded.body,
|
|
2817
|
+
encoding: encoded.encoding,
|
|
2818
|
+
contentType: encoded.contentType,
|
|
2819
|
+
...options.headers ? { headers: options.headers } : {},
|
|
2820
|
+
...at ? { deliverAt: at.toISOString() } : {},
|
|
2821
|
+
...options.delay !== void 0 && !at ? { delaySeconds: durationToSeconds(options.delay) } : {},
|
|
2822
|
+
...options.groupKey ? { groupKey: options.groupKey } : {},
|
|
2823
|
+
...options.deduplicationKey ? { deduplicationKey: options.deduplicationKey } : {},
|
|
2824
|
+
...options.deploymentId ? { deploymentId: options.deploymentId } : {}
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
decode(body, contentType) {
|
|
2828
|
+
const decoded = this.codec.decode(body, contentType);
|
|
2829
|
+
return this.options.validate ? this.options.validate(decoded) : decoded;
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Sends one message.
|
|
2833
|
+
*
|
|
2834
|
+
* @example
|
|
2835
|
+
* ```ts
|
|
2836
|
+
* await emails.send({ to: 'jane@example.com' });
|
|
2837
|
+
* await emails.send({ to: 'jane@example.com' }, { delay: '10m' });
|
|
2838
|
+
* await emails.send(reminder, { at: new Date('2027-01-01T09:00:00Z'), deduplicationKey: `reminder:${id}` });
|
|
2839
|
+
* ```
|
|
2840
|
+
*
|
|
2841
|
+
* @throws {@link ApiError} when the message is refused, for example with
|
|
2842
|
+
* `code: 'backlog_full'` when your organization's queued storage is full.
|
|
2843
|
+
* Its `status` is 429 when sending again later may succeed and 400 when it
|
|
2844
|
+
* will not, so retry logic that keys on the status stays correct.
|
|
2845
|
+
*/
|
|
2846
|
+
async send(payload, options) {
|
|
2847
|
+
const result = await this.resource().send(this.name, this.toMessage(payload, options), {
|
|
2848
|
+
...this.scope,
|
|
2849
|
+
autoCreate: this.options.autoCreate
|
|
2850
|
+
});
|
|
2851
|
+
if (result.messageId === null) throw new ApiError(result.error, result.retryable ? 429 : 400, result.code);
|
|
2852
|
+
return { messageId: result.messageId, deduplicated: result.deduplicated, deliverAt: new Date(result.deliverAt) };
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Sends many messages, 100 per request. Results come back in input order;
|
|
2856
|
+
* a refused message does not fail the rest; its error carries status 429
|
|
2857
|
+
* when it may be sent again later and 400 when it may not. Chunks are split by count only:
|
|
2858
|
+
* a request whose message bodies exceed 4 MiB in total is refused whole.
|
|
2859
|
+
*/
|
|
2860
|
+
async sendBatch(messages) {
|
|
2861
|
+
const results = [];
|
|
2862
|
+
for (let index = 0; index < messages.length; index += BATCH_LIMIT) {
|
|
2863
|
+
const chunk = messages.slice(index, index + BATCH_LIMIT);
|
|
2864
|
+
const sent = await this.resource().sendBatch(
|
|
2865
|
+
this.name,
|
|
2866
|
+
chunk.map(({ payload, ...options }) => this.toMessage(payload, options)),
|
|
2867
|
+
{ ...this.scope, autoCreate: this.options.autoCreate }
|
|
2868
|
+
);
|
|
2869
|
+
for (const result of sent) {
|
|
2870
|
+
results.push(
|
|
2871
|
+
result.messageId === null ? { messageId: null, error: new ApiError(result.error, result.retryable ? 429 : 400, result.code) } : { messageId: result.messageId, deduplicated: result.deduplicated, deliverAt: new Date(result.deliverAt) }
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
return results;
|
|
2876
|
+
}
|
|
2877
|
+
/** Deletes a message that has not been processed yet, such as a scheduled send. */
|
|
2878
|
+
async cancel(messageId) {
|
|
2879
|
+
await this.resource().deleteMessage(this.name, messageId, this.scope);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Leases messages from a pull queue. Waits up to `wait` (at most 20
|
|
2883
|
+
* seconds) for the first message, then returns what is available.
|
|
2884
|
+
*/
|
|
2885
|
+
async receive(options = {}) {
|
|
2886
|
+
const records = await this.resource().receive(
|
|
2887
|
+
this.name,
|
|
2888
|
+
{
|
|
2889
|
+
maxMessages: options.maxMessages ?? 1,
|
|
2890
|
+
...options.wait === void 0 ? {} : { waitSeconds: Math.min(durationToSeconds(options.wait), 20) },
|
|
2891
|
+
...options.visibilityTimeout === void 0 ? {} : { visibilityTimeoutSeconds: durationToSeconds(options.visibilityTimeout) }
|
|
2892
|
+
},
|
|
2893
|
+
{ ...this.scope, signal: options.signal }
|
|
2894
|
+
);
|
|
2895
|
+
return records.map((record) => this.toReceived(record));
|
|
2896
|
+
}
|
|
2897
|
+
toReceived(record) {
|
|
2898
|
+
const resource = this.resource();
|
|
2899
|
+
const receipt = record.receipt ?? "";
|
|
2900
|
+
const bytes = record.encoding === "base64" ? base64ToBytes(record.body) : utf8Encode(record.body);
|
|
2901
|
+
const nack = (request) => resource.nack(this.name, record.id, { receipt, ...request }, this.scope).then(() => void 0);
|
|
2902
|
+
let payload;
|
|
2903
|
+
let decodeError;
|
|
2904
|
+
try {
|
|
2905
|
+
payload = this.decode(bytes, record.contentType);
|
|
2906
|
+
} catch (error) {
|
|
2907
|
+
decodeError = error;
|
|
2908
|
+
}
|
|
2909
|
+
return {
|
|
2910
|
+
messageId: record.id,
|
|
2911
|
+
queue: this.name,
|
|
2912
|
+
attempt: record.attempts,
|
|
2913
|
+
createdAt: toDate(record.createdAt),
|
|
2914
|
+
groupKey: record.groupKey,
|
|
2915
|
+
deduplicationKey: record.deduplicationKey,
|
|
2916
|
+
deploymentId: record.deploymentId,
|
|
2917
|
+
headers: record.headers,
|
|
2918
|
+
receipt,
|
|
2919
|
+
leaseExpiresAt: toDate(record.leaseExpiresAt),
|
|
2920
|
+
get payload() {
|
|
2921
|
+
if (decodeError !== void 0) throw new NonRetryableError(`Undecodable payload: ${errorText(decodeError)}`);
|
|
2922
|
+
return payload;
|
|
2923
|
+
},
|
|
2924
|
+
ack: () => resource.ack(this.name, record.id, receipt, this.scope).then(() => void 0),
|
|
2925
|
+
retry: (options = {}) => nack({
|
|
2926
|
+
...options.delay === void 0 ? {} : { delaySeconds: durationToSeconds(options.delay) },
|
|
2927
|
+
...options.error ? { error: options.error } : {}
|
|
2928
|
+
}),
|
|
2929
|
+
defer: (delay2) => nack({ delaySeconds: durationToSeconds(delay2), countAttempt: false }),
|
|
2930
|
+
deadLetter: (reason) => nack({ deadLetter: true, ...reason ? { error: reason } : {} }),
|
|
2931
|
+
extendLease: (duration) => resource.extend(this.name, record.id, receipt, durationToSeconds(duration), this.scope).then(() => void 0)
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Receives and processes messages until `signal` aborts, or until the queue
|
|
2936
|
+
* is empty with `stopWhenEmpty`. A handler that returns acknowledges the
|
|
2937
|
+
* message; {@link RetryLaterError} defers it, {@link NonRetryableError}
|
|
2938
|
+
* dead-letters it, and any other error retries it with backoff.
|
|
2939
|
+
*
|
|
2940
|
+
* @example
|
|
2941
|
+
* ```ts
|
|
2942
|
+
* const controller = new AbortController();
|
|
2943
|
+
* await jobs.consume(async (job) => processJob(job), { maxMessages: 10, signal: controller.signal });
|
|
2944
|
+
* ```
|
|
2945
|
+
*/
|
|
2946
|
+
async consume(handler, options = {}) {
|
|
2947
|
+
const wait = options.wait ?? (options.stopWhenEmpty ? 0 : 20);
|
|
2948
|
+
const waitMs = Math.min(durationToSeconds(wait), 20) * 1e3;
|
|
2949
|
+
const pending = [];
|
|
2950
|
+
let timer;
|
|
2951
|
+
const sendAcks = async (batch) => {
|
|
2952
|
+
const resource = this.resource();
|
|
2953
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
2954
|
+
try {
|
|
2955
|
+
const { results } = await resource.ackBatch(
|
|
2956
|
+
this.name,
|
|
2957
|
+
batch.map((message) => ({ messageId: message.messageId, receipt: message.receipt })),
|
|
2958
|
+
this.scope
|
|
2959
|
+
);
|
|
2960
|
+
return batch.map((_message, index) => {
|
|
2961
|
+
const result = results[index];
|
|
2962
|
+
if (result?.acknowledged || attempt > 0 && result?.code === "message_not_found") return void 0;
|
|
2963
|
+
const code = result?.code ?? "no_result";
|
|
2964
|
+
return new ApiError(
|
|
2965
|
+
`Acknowledging message failed: ${code}`,
|
|
2966
|
+
code === "message_not_found" ? 404 : 409,
|
|
2967
|
+
code
|
|
2968
|
+
);
|
|
2969
|
+
});
|
|
2970
|
+
} catch (error) {
|
|
2971
|
+
if (error instanceof ApiError && error.status === 404 && error.code !== "queue_not_found") {
|
|
2972
|
+
const settled = await Promise.allSettled(
|
|
2973
|
+
batch.map((message) => resource.ack(this.name, message.messageId, message.receipt, this.scope))
|
|
2974
|
+
);
|
|
2975
|
+
return settled.map(
|
|
2976
|
+
(outcome) => outcome.status === "fulfilled" ? void 0 : outcome.reason instanceof Error ? outcome.reason : new Error(errorText(outcome.reason))
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
const retryable = !(error instanceof ApiError) || error.status >= 500 || error.status === 429;
|
|
2980
|
+
const delay2 = ACK_RETRY_DELAYS_MS[attempt];
|
|
2981
|
+
if (!retryable || delay2 === void 0) throw error;
|
|
2982
|
+
await sleep2(delay2, void 0);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
const flush = () => {
|
|
2987
|
+
clearTimeout(timer);
|
|
2988
|
+
timer = void 0;
|
|
2989
|
+
const batch = pending.splice(0, BATCH_LIMIT);
|
|
2990
|
+
if (pending.length > 0) timer = setTimeout(flush, 0);
|
|
2991
|
+
if (batch.length === 0) return;
|
|
2992
|
+
sendAcks(batch.map(({ message }) => message)).then((errors) => batch.forEach(({ settled }, index) => settled(errors[index]))).catch((error) => batch.forEach(({ settled }) => settled(error)));
|
|
2993
|
+
};
|
|
2994
|
+
const acknowledge = (message) => new Promise((resolve, reject) => {
|
|
2995
|
+
pending.push({
|
|
2996
|
+
message,
|
|
2997
|
+
settled: (error) => {
|
|
2998
|
+
if (error === void 0) resolve();
|
|
2999
|
+
else reject(error instanceof Error ? error : new Error(errorText(error)));
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
if (pending.length >= BATCH_LIMIT) flush();
|
|
3003
|
+
else timer ??= setTimeout(flush, ACK_FLUSH_MS);
|
|
3004
|
+
});
|
|
3005
|
+
while (!options.signal?.aborted) {
|
|
3006
|
+
let messages;
|
|
3007
|
+
const started = Date.now();
|
|
3008
|
+
try {
|
|
3009
|
+
messages = await this.receive({
|
|
3010
|
+
maxMessages: options.maxMessages ?? 10,
|
|
3011
|
+
wait,
|
|
3012
|
+
visibilityTimeout: options.visibilityTimeout,
|
|
3013
|
+
signal: options.signal
|
|
3014
|
+
});
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
if (options.signal?.aborted) return;
|
|
3017
|
+
throw error;
|
|
3018
|
+
}
|
|
3019
|
+
if (messages.length === 0) {
|
|
3020
|
+
if (options.stopWhenEmpty) return;
|
|
3021
|
+
const remaining = Math.max(waitMs, EMPTY_POLL_MIN_MS) - (Date.now() - started);
|
|
3022
|
+
if (remaining > 0) await sleep2(Math.min(remaining, EMPTY_POLL_MAX_MS), options.signal);
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
await Promise.all(
|
|
3026
|
+
messages.map(async (message) => {
|
|
3027
|
+
let outcome;
|
|
3028
|
+
try {
|
|
3029
|
+
await handler(message.payload, message);
|
|
3030
|
+
outcome = acknowledge(message);
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
outcome = error instanceof RetryLaterError ? message.defer(error.delaySeconds) : error instanceof NonRetryableError ? message.deadLetter(error.message) : message.retry({ error: errorText(error) });
|
|
3033
|
+
}
|
|
3034
|
+
try {
|
|
3035
|
+
await outcome;
|
|
3036
|
+
} catch (settleError) {
|
|
3037
|
+
options.onError?.(settleError);
|
|
3038
|
+
}
|
|
3039
|
+
})
|
|
3040
|
+
);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Builds a push consumer: a `(request: Request) => Promise<Response>`
|
|
3045
|
+
* function for the queue's consumer path, such as a Next.js route handler.
|
|
3046
|
+
*
|
|
3047
|
+
* It verifies the delivery signature, decodes the payload and calls
|
|
3048
|
+
* `handler`. Returning acknowledges the message. Throw
|
|
3049
|
+
* {@link RetryLaterError} to defer it without spending an attempt,
|
|
3050
|
+
* {@link NonRetryableError} to dead-letter it, or anything else to retry
|
|
3051
|
+
* with backoff.
|
|
3052
|
+
*
|
|
3053
|
+
* @example
|
|
3054
|
+
* ```ts
|
|
3055
|
+
* // app/api/queues/emails/route.ts
|
|
3056
|
+
* export const POST = emails.handler(async (email, { attempt }) => {
|
|
3057
|
+
* await sendEmail(email);
|
|
3058
|
+
* });
|
|
3059
|
+
* ```
|
|
3060
|
+
*/
|
|
3061
|
+
handler(handler) {
|
|
3062
|
+
return async (request) => {
|
|
3063
|
+
const header = (name) => request.headers.get(name);
|
|
3064
|
+
const messageId = header(QUEUE_DELIVERY_HEADERS.messageId);
|
|
3065
|
+
const queueName = header(QUEUE_DELIVERY_HEADERS.name);
|
|
3066
|
+
const attempt = Number(header(QUEUE_DELIVERY_HEADERS.attempt) ?? "1");
|
|
3067
|
+
if (!messageId || !queueName) {
|
|
3068
|
+
return Response.json({ error: "Not a queue delivery" }, { status: 400 });
|
|
3069
|
+
}
|
|
3070
|
+
if (queueName !== this.name) {
|
|
3071
|
+
console.error(`[gigadrive] Rejected a delivery for queue "${queueName}" at the handler of "${this.name}"`);
|
|
3072
|
+
return Response.json({ error: `This endpoint consumes queue "${this.name}"` }, { status: 400 });
|
|
3073
|
+
}
|
|
3074
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
3075
|
+
if (!this.options.allowUnsigned) {
|
|
3076
|
+
const secret = this.options.signingSecret ?? readSigningSecret();
|
|
3077
|
+
if (!secret) {
|
|
3078
|
+
console.error(
|
|
3079
|
+
`[gigadrive] Rejected a queue delivery: ${QUEUE_SIGNING_SECRET_ENV} is not set. Deployments receive it automatically; set allowUnsigned for local development.`
|
|
3080
|
+
);
|
|
3081
|
+
return Response.json({ error: "Queue signing secret is not configured" }, { status: 500 });
|
|
3082
|
+
}
|
|
3083
|
+
const valid = await verifyQueueSignature(header(QUEUE_SIGNATURE_HEADER), {
|
|
3084
|
+
secret,
|
|
3085
|
+
queue: queueName,
|
|
3086
|
+
messageId,
|
|
3087
|
+
attempt,
|
|
3088
|
+
body
|
|
3089
|
+
});
|
|
3090
|
+
if (!valid) return Response.json({ error: "Invalid queue signature" }, { status: 401 });
|
|
3091
|
+
}
|
|
3092
|
+
const meta = {
|
|
3093
|
+
messageId,
|
|
3094
|
+
queue: queueName,
|
|
3095
|
+
attempt,
|
|
3096
|
+
createdAt: toDate(header(QUEUE_DELIVERY_HEADERS.createdAt)),
|
|
3097
|
+
groupKey: header(QUEUE_DELIVERY_HEADERS.groupKey),
|
|
3098
|
+
deduplicationKey: header(QUEUE_DELIVERY_HEADERS.deduplicationKey),
|
|
3099
|
+
deploymentId: header(QUEUE_DELIVERY_HEADERS.deploymentId),
|
|
3100
|
+
headers: Object.fromEntries(
|
|
3101
|
+
[...request.headers.entries()].filter(([name]) => !name.startsWith("x-gigadrive-queue-"))
|
|
3102
|
+
)
|
|
3103
|
+
};
|
|
3104
|
+
let payload;
|
|
3105
|
+
try {
|
|
3106
|
+
payload = this.decode(body, header("content-type") ?? "application/octet-stream");
|
|
3107
|
+
} catch (error) {
|
|
3108
|
+
return deadLetterResponse(`Undecodable payload: ${errorText(error)}`);
|
|
3109
|
+
}
|
|
3110
|
+
try {
|
|
3111
|
+
await handler(payload, meta);
|
|
3112
|
+
return Response.json({ ok: true });
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
if (error instanceof RetryLaterError) {
|
|
3115
|
+
return Response.json(
|
|
3116
|
+
{ ok: true, retryAfter: error.delaySeconds },
|
|
3117
|
+
{ headers: { [QUEUE_RESPONSE_HEADERS.retryAfter]: String(error.delaySeconds) } }
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
if (error instanceof NonRetryableError) return deadLetterResponse(error.message);
|
|
3121
|
+
console.error(`[gigadrive] Queue handler for "${queueName}" failed (attempt ${String(attempt)})`, error);
|
|
3122
|
+
return Response.json({ error: errorText(error) }, { status: 500 });
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
}
|
|
3126
|
+
/** Creates the queue, or applies `settings` to it. Use it to make a push queue from code. */
|
|
3127
|
+
async ensure(settings = {}) {
|
|
3128
|
+
return this.resource().ensure(this.name, settings, this.scope);
|
|
3129
|
+
}
|
|
3130
|
+
/** The queue's settings and live depth. */
|
|
3131
|
+
async info() {
|
|
3132
|
+
return this.resource().get(this.name, this.scope);
|
|
3133
|
+
}
|
|
3134
|
+
/** Stops deliveries until {@link resume}. Sends are still accepted. */
|
|
3135
|
+
async pause() {
|
|
3136
|
+
await this.resource().pause(this.name, this.scope);
|
|
3137
|
+
}
|
|
3138
|
+
async resume() {
|
|
3139
|
+
await this.resource().resume(this.name, this.scope);
|
|
3140
|
+
}
|
|
3141
|
+
/** Deletes every message, or only the dead letters. */
|
|
3142
|
+
async purge(scope = "all") {
|
|
3143
|
+
return (await this.resource().purge(this.name, scope, this.scope)).purgedMessages;
|
|
3144
|
+
}
|
|
3145
|
+
/** Moves dead letters back for another round of attempts. */
|
|
3146
|
+
async redrive(messageIds) {
|
|
3147
|
+
return (await this.resource().redrive(this.name, messageIds, this.scope)).redrivenMessages;
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Creates or updates a cron schedule that sends `payload` into this queue.
|
|
3151
|
+
*
|
|
3152
|
+
* @example
|
|
3153
|
+
* ```ts
|
|
3154
|
+
* await digests.schedule('weekday-digest', { cron: '0 8 * * MON-FRI', timezone: 'Europe/Berlin', payload: { kind: 'digest' } });
|
|
3155
|
+
* ```
|
|
3156
|
+
*/
|
|
3157
|
+
async schedule(name, schedule) {
|
|
3158
|
+
const encoded = schedule.payload === void 0 ? void 0 : this.codec.encode(schedule.payload);
|
|
3159
|
+
if (encoded?.encoding === "base64") throw new ConfigurationError("Scheduled payloads must be text or JSON");
|
|
3160
|
+
const input = {
|
|
3161
|
+
cron: schedule.cron,
|
|
3162
|
+
...schedule.timezone ? { timezone: schedule.timezone } : {},
|
|
3163
|
+
...encoded ? { body: encoded.body, contentType: encoded.contentType } : {},
|
|
3164
|
+
...schedule.headers ? { headers: schedule.headers } : {},
|
|
3165
|
+
...schedule.enabled === void 0 ? {} : { enabled: schedule.enabled }
|
|
3166
|
+
};
|
|
3167
|
+
try {
|
|
3168
|
+
return await this.resource().createSchedule(this.name, name, input, this.scope);
|
|
3169
|
+
} catch (error) {
|
|
3170
|
+
if (error instanceof ApiError && error.code === "schedule_exists") {
|
|
3171
|
+
return this.resource().updateSchedule(this.name, name, input, this.scope);
|
|
3172
|
+
}
|
|
3173
|
+
throw error;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
/** Deletes a schedule. */
|
|
3177
|
+
async unschedule(name) {
|
|
3178
|
+
await this.resource().deleteSchedule(this.name, name, this.scope);
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
var deadLetterResponse = (reason) => Response.json(
|
|
3182
|
+
{ error: reason },
|
|
3183
|
+
{
|
|
3184
|
+
status: 422,
|
|
3185
|
+
headers: { [QUEUE_RESPONSE_HEADERS.action]: "dead-letter", [QUEUE_RESPONSE_HEADERS.error]: headerText(reason) }
|
|
3186
|
+
}
|
|
3187
|
+
);
|
|
3188
|
+
var readSigningSecret = () => {
|
|
3189
|
+
const env = globalThis.process?.env;
|
|
3190
|
+
return env?.[QUEUE_SIGNING_SECRET_ENV] || void 0;
|
|
3191
|
+
};
|
|
3192
|
+
|
|
3193
|
+
// src/resources/queues.ts
|
|
3194
|
+
var QueuesResource = class extends BaseResource {
|
|
3195
|
+
constructor(httpClient, defaultApplicationId) {
|
|
3196
|
+
super(httpClient);
|
|
3197
|
+
this.defaultApplicationId = defaultApplicationId;
|
|
3198
|
+
}
|
|
3199
|
+
defaultApplicationId;
|
|
3200
|
+
/**
|
|
3201
|
+
* Returns a typed handle for one queue. Nothing is sent until you call a
|
|
3202
|
+
* method on it, and the queue does not need to exist: the first send
|
|
3203
|
+
* creates a pull queue with default settings.
|
|
3204
|
+
*
|
|
3205
|
+
* @typeParam T - The payload type sent and received through this queue.
|
|
3206
|
+
* @param name - Queue name, unique in the environment and case-sensitive.
|
|
3207
|
+
* @param options - Scope, codec and default send options.
|
|
3208
|
+
*/
|
|
3209
|
+
queue(name, options = {}) {
|
|
3210
|
+
return new Queue(name, () => this, options);
|
|
3211
|
+
}
|
|
3212
|
+
base(options) {
|
|
3213
|
+
return `/applications/${encodeURIComponent(
|
|
3214
|
+
resolveStorageApplicationId(this.defaultApplicationId, options?.applicationId)
|
|
3215
|
+
)}/queues`;
|
|
3216
|
+
}
|
|
3217
|
+
path(name, options, suffix = "") {
|
|
3218
|
+
return `${this.base(options)}/${encodeURIComponent(name)}${suffix}`;
|
|
3219
|
+
}
|
|
3220
|
+
/** Lists the environment's queues with their live depth. */
|
|
3221
|
+
async list(options) {
|
|
3222
|
+
return this.httpClient.get(this.base(options), { query: { environment: options?.environment } });
|
|
3223
|
+
}
|
|
3224
|
+
/** Fetches one queue with its live depth. */
|
|
3225
|
+
async get(name, options) {
|
|
3226
|
+
return this.httpClient.get(this.path(name, options), { query: { environment: options?.environment } });
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Creates a queue. Fails with `queue_exists` when the name is taken; use
|
|
3230
|
+
* {@link ensure} for create-or-update.
|
|
3231
|
+
*/
|
|
3232
|
+
async create(name, settings = {}, options) {
|
|
3233
|
+
return this.httpClient.post(this.base(options), { name, environment: options?.environment, ...settings });
|
|
3234
|
+
}
|
|
3235
|
+
/** Creates the queue, or applies `settings` to the existing one. */
|
|
3236
|
+
async ensure(name, settings = {}, options) {
|
|
3237
|
+
return this.httpClient.put(this.path(name, options), { environment: options?.environment, ...settings });
|
|
3238
|
+
}
|
|
3239
|
+
/** Changes a queue's settings. Messages already queued keep flowing. */
|
|
3240
|
+
async update(name, settings, options) {
|
|
3241
|
+
return this.httpClient.patch(this.path(name, options), { environment: options?.environment, ...settings });
|
|
3242
|
+
}
|
|
3243
|
+
/** Deletes a queue, its messages and its schedules. */
|
|
3244
|
+
async delete(name, options) {
|
|
3245
|
+
return this.httpClient.delete(this.path(name, options), { query: { environment: options?.environment } });
|
|
3246
|
+
}
|
|
3247
|
+
/** Stops deliveries. Sends are still accepted and wait until you resume. */
|
|
3248
|
+
async pause(name, options) {
|
|
3249
|
+
return this.httpClient.post(this.path(name, options, "/pause"), { environment: options?.environment });
|
|
3250
|
+
}
|
|
3251
|
+
/** Restarts deliveries after {@link pause}. */
|
|
3252
|
+
async resume(name, options) {
|
|
3253
|
+
return this.httpClient.post(this.path(name, options, "/resume"), { environment: options?.environment });
|
|
3254
|
+
}
|
|
3255
|
+
/** Deletes every message, or only the dead letters. */
|
|
3256
|
+
async purge(name, scope = "all", options) {
|
|
3257
|
+
return this.httpClient.post(this.path(name, options, "/purge"), { environment: options?.environment, scope });
|
|
3258
|
+
}
|
|
3259
|
+
/** Moves dead letters back to the queue with fresh attempts: all of them, or the given ids. */
|
|
3260
|
+
async redrive(name, messageIds, options) {
|
|
3261
|
+
return this.httpClient.post(this.path(name, options, "/redrive"), {
|
|
3262
|
+
environment: options?.environment,
|
|
3263
|
+
...messageIds ? { messageIds } : {}
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Sends one message.
|
|
3268
|
+
*
|
|
3269
|
+
* @param autoCreate - Create a pull queue with default settings when the name is new. Default `true`.
|
|
3270
|
+
*/
|
|
3271
|
+
async send(name, message, options) {
|
|
3272
|
+
return this.httpClient.post(this.path(name, options, "/messages"), {
|
|
3273
|
+
environment: options?.environment,
|
|
3274
|
+
autoCreate: options?.autoCreate,
|
|
3275
|
+
...message
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
/**
|
|
3279
|
+
* Sends up to 100 messages in one request. Results come back in input
|
|
3280
|
+
* order; one rejected message does not fail the others.
|
|
3281
|
+
*/
|
|
3282
|
+
async sendBatch(name, messages, options) {
|
|
3283
|
+
const { results } = await this.httpClient.post(
|
|
3284
|
+
this.path(name, options, "/messages"),
|
|
3285
|
+
{ environment: options?.environment, autoCreate: options?.autoCreate, messages }
|
|
3286
|
+
);
|
|
3287
|
+
return results;
|
|
3288
|
+
}
|
|
3289
|
+
/** Lists messages in one state without leasing them. Bodies are cut to a preview. */
|
|
3290
|
+
async listMessages(name, query = {}, options) {
|
|
3291
|
+
return this.httpClient.get(this.path(name, options, "/messages"), {
|
|
3292
|
+
query: { environment: options?.environment, ...query }
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
/** Fetches one message without leasing it. */
|
|
3296
|
+
async getMessage(name, messageId, options) {
|
|
3297
|
+
return this.httpClient.get(this.path(name, options, `/messages/${encodeURIComponent(messageId)}`), {
|
|
3298
|
+
query: { environment: options?.environment }
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Cancels a message that has not been delivered yet, such as a scheduled
|
|
3303
|
+
* send you no longer want. A message being delivered or dead-lettered
|
|
3304
|
+
* answers 400 `invalid_request`; settle it from its consumer, or purge or
|
|
3305
|
+
* redrive dead letters.
|
|
3306
|
+
*/
|
|
3307
|
+
async deleteMessage(name, messageId, options) {
|
|
3308
|
+
return this.httpClient.delete(this.path(name, options, `/messages/${encodeURIComponent(messageId)}`), {
|
|
3309
|
+
query: { environment: options?.environment }
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
/**
|
|
3313
|
+
* Leases up to `maxMessages` messages from a pull queue, waiting up to
|
|
3314
|
+
* `waitSeconds` (at most 20) for the first one. Each must be acknowledged
|
|
3315
|
+
* with its receipt before the lease ends, or it is delivered again.
|
|
3316
|
+
*/
|
|
3317
|
+
async receive(name, request = {}, options) {
|
|
3318
|
+
const { messages } = await this.httpClient.post(
|
|
3319
|
+
this.path(name, options, "/messages/receive"),
|
|
3320
|
+
{ environment: options?.environment, ...request },
|
|
3321
|
+
{ signal: options?.signal }
|
|
3322
|
+
);
|
|
3323
|
+
return messages;
|
|
3324
|
+
}
|
|
3325
|
+
/** Acknowledges a received message, deleting it. */
|
|
3326
|
+
async ack(name, messageId, receipt, options) {
|
|
3327
|
+
return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/ack`), {
|
|
3328
|
+
environment: options?.environment,
|
|
3329
|
+
receipt
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* Acknowledges up to 100 received messages in one request. Each settles on
|
|
3334
|
+
* its own: `results` reports, in order, which were acknowledged and why any
|
|
3335
|
+
* were not (`message_not_found`, `receipt_mismatch`). A batch counts one
|
|
3336
|
+
* operation per started ten acknowledged messages.
|
|
3337
|
+
*/
|
|
3338
|
+
async ackBatch(name, messages, options) {
|
|
3339
|
+
return this.httpClient.post(this.path(name, options, "/messages/ack"), {
|
|
3340
|
+
environment: options?.environment,
|
|
3341
|
+
messages
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* Releases a received message: retry with backoff (or after `delaySeconds`),
|
|
3346
|
+
* or dead-letter it now with `deadLetter: true`. `countAttempt: false`
|
|
3347
|
+
* reschedules without spending an attempt.
|
|
3348
|
+
*/
|
|
3349
|
+
async nack(name, messageId, request, options) {
|
|
3350
|
+
return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/nack`), {
|
|
3351
|
+
environment: options?.environment,
|
|
3352
|
+
...request
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
/** Extends a received message's lease to `visibilityTimeoutSeconds` from now. */
|
|
3356
|
+
async extend(name, messageId, receipt, visibilityTimeoutSeconds, options) {
|
|
3357
|
+
return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/extend`), {
|
|
3358
|
+
environment: options?.environment,
|
|
3359
|
+
receipt,
|
|
3360
|
+
visibilityTimeoutSeconds
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
/** Lists a queue's schedules. */
|
|
3364
|
+
async listSchedules(name, options) {
|
|
3365
|
+
return this.httpClient.get(this.path(name, options, "/schedules"), {
|
|
3366
|
+
query: { environment: options?.environment }
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3369
|
+
/**
|
|
3370
|
+
* Creates a schedule that sends a message into the queue on a cron.
|
|
3371
|
+
*
|
|
3372
|
+
* @example
|
|
3373
|
+
* ```ts
|
|
3374
|
+
* await client.queues.createSchedule('emails', 'daily-digest', {
|
|
3375
|
+
* cron: '0 8 * * MON-FRI',
|
|
3376
|
+
* timezone: 'Europe/Berlin',
|
|
3377
|
+
* json: { kind: 'digest' },
|
|
3378
|
+
* });
|
|
3379
|
+
* ```
|
|
3380
|
+
*/
|
|
3381
|
+
async createSchedule(name, schedule, input, options) {
|
|
3382
|
+
return this.httpClient.post(this.path(name, options, "/schedules"), {
|
|
3383
|
+
environment: options?.environment,
|
|
3384
|
+
name: schedule,
|
|
3385
|
+
...input
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
/** Changes a schedule. Its next run is recomputed when the cron or time zone changes. */
|
|
3389
|
+
async updateSchedule(name, schedule, input, options) {
|
|
3390
|
+
return this.httpClient.patch(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}`), {
|
|
3391
|
+
environment: options?.environment,
|
|
3392
|
+
...input
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
/** Deletes a schedule. Messages it already sent stay queued. */
|
|
3396
|
+
async deleteSchedule(name, schedule, options) {
|
|
3397
|
+
return this.httpClient.delete(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}`), {
|
|
3398
|
+
query: { environment: options?.environment }
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
/** Sends the schedule's message now, outside its cron. */
|
|
3402
|
+
async triggerSchedule(name, schedule, options) {
|
|
3403
|
+
return this.httpClient.post(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}/trigger`), {
|
|
3404
|
+
environment: options?.environment
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
};
|
|
3408
|
+
|
|
2362
3409
|
// src/resources/sticky-sessions.ts
|
|
2363
3410
|
var StickySessionsResource = class extends BaseResource {
|
|
2364
3411
|
/**
|
|
@@ -2402,6 +3449,12 @@ var GigadriveClient = class {
|
|
|
2402
3449
|
stickySessions;
|
|
2403
3450
|
/** Managed image optimization inspection and cache purge operations. */
|
|
2404
3451
|
imageOptimization;
|
|
3452
|
+
/**
|
|
3453
|
+
* Network Queues: manage queues and schedules, send, receive and
|
|
3454
|
+
* acknowledge messages. `client.queues.queue<T>(name)` returns a typed
|
|
3455
|
+
* handle bound to one queue.
|
|
3456
|
+
*/
|
|
3457
|
+
queues;
|
|
2405
3458
|
constructor(config = {}) {
|
|
2406
3459
|
const baseUrl = config.baseUrl ?? readEnv("GIGADRIVE_API_BASE_URL") ?? DEFAULT_BASE_URL2;
|
|
2407
3460
|
const applicationId = config.applicationId ?? readEnv("GIGADRIVE_APPLICATION_ID");
|
|
@@ -2417,6 +3470,7 @@ var GigadriveClient = class {
|
|
|
2417
3470
|
this.apiKeys = new ApiKeysResource(httpClient);
|
|
2418
3471
|
this.stickySessions = new StickySessionsResource(httpClient);
|
|
2419
3472
|
this.imageOptimization = new ImageOptimizationResource(httpClient);
|
|
3473
|
+
this.queues = new QueuesResource(httpClient, applicationId);
|
|
2420
3474
|
}
|
|
2421
3475
|
};
|
|
2422
3476
|
|
|
@@ -2431,6 +3485,161 @@ async function* paginate(fetchPage) {
|
|
|
2431
3485
|
cursor = page.nextCursor;
|
|
2432
3486
|
} while (cursor);
|
|
2433
3487
|
}
|
|
3488
|
+
|
|
3489
|
+
// src/queues/default-client.ts
|
|
3490
|
+
var defaultClient;
|
|
3491
|
+
var defaultQueuesResource = () => {
|
|
3492
|
+
defaultClient ??= new GigadriveClient();
|
|
3493
|
+
return defaultClient.queues;
|
|
3494
|
+
};
|
|
3495
|
+
function queue(name, options = {}) {
|
|
3496
|
+
const { client, ...queueOptions } = options;
|
|
3497
|
+
return new Queue(name, () => client?.queues ?? defaultQueuesResource(), queueOptions);
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
// src/queues/workflow.ts
|
|
3501
|
+
var WORKFLOW_CONSUMER_PATH = "/.well-known/workflow/v1/flow";
|
|
3502
|
+
var WORKFLOW_QUEUE_NAME_HEADER = "x-workflow-queue-name";
|
|
3503
|
+
var WORKFLOW_PREFIX = /^__(?:[a-z][a-z0-9]*_)?wkf_workflow_/;
|
|
3504
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3505
|
+
var RESERVED_HEADER = /^(host|content-length|content-type|connection|transfer-encoding|te|upgrade|keep-alive|proxy-.*|authorization|cookie|x-gigadrive-.*|x-nebula-.*|x-substrate-.*|x-vercel-.*|x-forwarded-.*|x-bunny-.*|x-real-ip|true-client-ip|forwarded)$/i;
|
|
3506
|
+
var encodeWorkflowMessage = (message) => JSON.stringify(
|
|
3507
|
+
message,
|
|
3508
|
+
(_key, value) => value instanceof Uint8Array ? { __type: "Uint8Array", data: bytesToBase64(value) } : value
|
|
3509
|
+
);
|
|
3510
|
+
var decodeWorkflowMessage = (text) => JSON.parse(text, (_key, value) => {
|
|
3511
|
+
if (typeof value === "object" && value !== null && value.__type === "Uint8Array" && typeof value.data === "string") {
|
|
3512
|
+
return base64ToBytes(value.data);
|
|
3513
|
+
}
|
|
3514
|
+
return value;
|
|
3515
|
+
});
|
|
3516
|
+
var readEnv2 = (name) => globalThis.process?.env?.[name] || void 0;
|
|
3517
|
+
var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(utf8Encode(idempotencyKey))))}`;
|
|
3518
|
+
function createWorkflowQueue(options = {}) {
|
|
3519
|
+
const resource = () => options.client?.queues ?? defaultQueuesResource();
|
|
3520
|
+
const scope = { applicationId: options.applicationId, environment: options.environment };
|
|
3521
|
+
const ensured = /* @__PURE__ */ new Map();
|
|
3522
|
+
const physicalQueue = async (queueName) => {
|
|
3523
|
+
const prefix = WORKFLOW_PREFIX.exec(queueName)?.[0];
|
|
3524
|
+
if (!prefix) throw new Error(`"${queueName}" is not a Workflow SDK queue name`);
|
|
3525
|
+
let ready = ensured.get(prefix);
|
|
3526
|
+
if (!ready) {
|
|
3527
|
+
const consumerPath = options.consumerPath ?? WORKFLOW_CONSUMER_PATH;
|
|
3528
|
+
ready = options.queueSettings ? resource().ensure(prefix, { ...options.queueSettings, consumerPath }, scope) : (
|
|
3529
|
+
// Create-only: a queue that exists keeps the settings someone gave it in the console.
|
|
3530
|
+
resource().create(prefix, { maxAttempts: 100, visibilityTimeoutSeconds: 900, deadLetter: true, consumerPath }, scope).catch((error) => {
|
|
3531
|
+
if (error instanceof ApiError && error.code === "queue_exists") return void 0;
|
|
3532
|
+
throw error;
|
|
3533
|
+
})
|
|
3534
|
+
);
|
|
3535
|
+
ensured.set(prefix, ready);
|
|
3536
|
+
ready.catch(() => ensured.delete(prefix));
|
|
3537
|
+
}
|
|
3538
|
+
await ready;
|
|
3539
|
+
return prefix;
|
|
3540
|
+
};
|
|
3541
|
+
const toMessage = async (queueName, message, opts = {}) => {
|
|
3542
|
+
const headers = Object.fromEntries(
|
|
3543
|
+
Object.entries(opts.headers ?? {}).filter(([name]) => !RESERVED_HEADER.test(name))
|
|
3544
|
+
);
|
|
3545
|
+
headers[WORKFLOW_QUEUE_NAME_HEADER] = queueName;
|
|
3546
|
+
const pin = opts.deploymentId ?? options.deploymentId ?? readEnv2("GIGADRIVE_DEPLOYMENT_ID");
|
|
3547
|
+
return {
|
|
3548
|
+
body: encodeWorkflowMessage(message),
|
|
3549
|
+
contentType: "application/json",
|
|
3550
|
+
headers,
|
|
3551
|
+
...opts.delaySeconds && opts.delaySeconds > 0 ? { delaySeconds: Math.ceil(opts.delaySeconds) } : {},
|
|
3552
|
+
...opts.idempotencyKey ? { deduplicationKey: await deduplicationKeyFor(opts.idempotencyKey) } : {},
|
|
3553
|
+
...pin && UUID.test(pin) ? { deploymentId: pin } : {}
|
|
3554
|
+
};
|
|
3555
|
+
};
|
|
3556
|
+
return {
|
|
3557
|
+
getDeploymentId: () => Promise.resolve(options.deploymentId ?? readEnv2("GIGADRIVE_DEPLOYMENT_ID") ?? "local"),
|
|
3558
|
+
async queue(queueName, message, opts) {
|
|
3559
|
+
const physical = await physicalQueue(queueName);
|
|
3560
|
+
const result = await resource().send(physical, await toMessage(queueName, message, opts), {
|
|
3561
|
+
...scope,
|
|
3562
|
+
autoCreate: false
|
|
3563
|
+
});
|
|
3564
|
+
if (result.messageId === null) throw new Error(`Workflow message was refused: ${result.error}`);
|
|
3565
|
+
return { messageId: result.messageId };
|
|
3566
|
+
},
|
|
3567
|
+
async queueBatch(queueName, messages) {
|
|
3568
|
+
const physical = await physicalQueue(queueName);
|
|
3569
|
+
const results = [];
|
|
3570
|
+
for (let index = 0; index < messages.length; index += 100) {
|
|
3571
|
+
const chunk = messages.slice(index, index + 100);
|
|
3572
|
+
const sent = await resource().sendBatch(
|
|
3573
|
+
physical,
|
|
3574
|
+
await Promise.all(chunk.map(({ message, opts }) => toMessage(queueName, message, opts))),
|
|
3575
|
+
{ ...scope, autoCreate: false }
|
|
3576
|
+
);
|
|
3577
|
+
for (const result of sent) {
|
|
3578
|
+
results.push(
|
|
3579
|
+
result.messageId === null ? { messageId: null, error: result.error, retryable: result.retryable } : { messageId: result.messageId }
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
return results;
|
|
3584
|
+
},
|
|
3585
|
+
createQueueHandler(queueNamePrefix, handler) {
|
|
3586
|
+
return async (request) => {
|
|
3587
|
+
const messageId = request.headers.get(QUEUE_DELIVERY_HEADERS.messageId);
|
|
3588
|
+
const physicalName = request.headers.get(QUEUE_DELIVERY_HEADERS.name);
|
|
3589
|
+
const attempt = Number(request.headers.get(QUEUE_DELIVERY_HEADERS.attempt) ?? "1");
|
|
3590
|
+
const queueName = request.headers.get(WORKFLOW_QUEUE_NAME_HEADER);
|
|
3591
|
+
if (!messageId || !physicalName || !queueName) {
|
|
3592
|
+
return Response.json({ error: "Missing required headers" }, { status: 400 });
|
|
3593
|
+
}
|
|
3594
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
3595
|
+
if (!options.allowUnsigned) {
|
|
3596
|
+
const secret = options.signingSecret ?? readSigningSecret();
|
|
3597
|
+
const valid = secret !== void 0 && await verifyQueueSignature(request.headers.get(QUEUE_SIGNATURE_HEADER), {
|
|
3598
|
+
secret,
|
|
3599
|
+
queue: physicalName,
|
|
3600
|
+
messageId,
|
|
3601
|
+
attempt,
|
|
3602
|
+
body
|
|
3603
|
+
});
|
|
3604
|
+
if (!valid) return Response.json({ error: "Invalid queue signature" }, { status: 401 });
|
|
3605
|
+
}
|
|
3606
|
+
if (physicalName !== queueNamePrefix || !queueName.startsWith(queueNamePrefix)) {
|
|
3607
|
+
return Response.json({ error: "Unhandled queue" }, { status: 400 });
|
|
3608
|
+
}
|
|
3609
|
+
let message;
|
|
3610
|
+
try {
|
|
3611
|
+
message = decodeWorkflowMessage(utf8Decode(body));
|
|
3612
|
+
} catch {
|
|
3613
|
+
return Response.json(
|
|
3614
|
+
{ error: "Malformed workflow message" },
|
|
3615
|
+
{ status: 422, headers: { [QUEUE_RESPONSE_HEADERS.action]: "dead-letter" } }
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
try {
|
|
3619
|
+
const result = await handler(message, {
|
|
3620
|
+
attempt,
|
|
3621
|
+
queueName,
|
|
3622
|
+
messageId,
|
|
3623
|
+
requestId: request.headers.get("x-request-id") ?? void 0
|
|
3624
|
+
});
|
|
3625
|
+
if (typeof message === "object" && message !== null && message.invoke === true) {
|
|
3626
|
+
return Response.json({ result });
|
|
3627
|
+
}
|
|
3628
|
+
const timeoutSeconds = typeof result === "object" && result !== null ? result.timeoutSeconds : void 0;
|
|
3629
|
+
if (typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds >= 0) {
|
|
3630
|
+
return Response.json(
|
|
3631
|
+
{ timeoutSeconds },
|
|
3632
|
+
{ headers: { [QUEUE_RESPONSE_HEADERS.retryAfter]: String(Math.ceil(timeoutSeconds)) } }
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
return Response.json({ ok: true });
|
|
3636
|
+
} catch (error) {
|
|
3637
|
+
return Response.json(String(error), { status: 500 });
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
2434
3643
|
export {
|
|
2435
3644
|
AiGatewayAudioResource,
|
|
2436
3645
|
AiGatewayBudgetsResource,
|
|
@@ -2440,6 +3649,7 @@ export {
|
|
|
2440
3649
|
AiGatewayVideosResource,
|
|
2441
3650
|
ApiError,
|
|
2442
3651
|
ApiKeysResource,
|
|
3652
|
+
ApplicationDomainsResource,
|
|
2443
3653
|
ApplicationEnvVarsResource,
|
|
2444
3654
|
ApplicationRequestsResource,
|
|
2445
3655
|
ApplicationStorageResource,
|
|
@@ -2447,14 +3657,25 @@ export {
|
|
|
2447
3657
|
AuthenticationError,
|
|
2448
3658
|
ConfigurationError,
|
|
2449
3659
|
DeploymentsResource,
|
|
3660
|
+
DomainNotActiveError,
|
|
2450
3661
|
GigadriveClient,
|
|
2451
3662
|
GigadriveError,
|
|
2452
3663
|
ImageOptimizationResource,
|
|
3664
|
+
NonRetryableError,
|
|
2453
3665
|
OrganizationAiGatewayResource,
|
|
3666
|
+
OrganizationDomainsResource,
|
|
2454
3667
|
OrganizationEnvVarsResource,
|
|
2455
3668
|
OrganizationMembersResource,
|
|
2456
3669
|
OrganizationProductsResource,
|
|
2457
3670
|
OrganizationsResource,
|
|
3671
|
+
QUEUE_DELIVERY_HEADERS,
|
|
3672
|
+
QUEUE_RESPONSE_HEADERS,
|
|
3673
|
+
QUEUE_SIGNATURE_HEADER,
|
|
3674
|
+
QUEUE_SIGNATURE_TOLERANCE_MS,
|
|
3675
|
+
QUEUE_SIGNING_SECRET_ENV,
|
|
3676
|
+
Queue,
|
|
3677
|
+
QueuesResource,
|
|
3678
|
+
RetryLaterError,
|
|
2458
3679
|
StickySessionsResource,
|
|
2459
3680
|
StorageBucketsResource,
|
|
2460
3681
|
StorageObjectsResource,
|
|
@@ -2462,8 +3683,16 @@ export {
|
|
|
2462
3683
|
StorageUploadSessionsResource,
|
|
2463
3684
|
UploadError,
|
|
2464
3685
|
UploadSessionExpiredError,
|
|
3686
|
+
WORKFLOW_CONSUMER_PATH,
|
|
3687
|
+
WORKFLOW_QUEUE_NAME_HEADER,
|
|
2465
3688
|
createManagedImageUrl,
|
|
3689
|
+
createWorkflowQueue,
|
|
3690
|
+
defaultQueueCodec,
|
|
3691
|
+
durationToSeconds,
|
|
2466
3692
|
paginate,
|
|
2467
|
-
parseSSEStream
|
|
3693
|
+
parseSSEStream,
|
|
3694
|
+
queue,
|
|
3695
|
+
signQueueDelivery,
|
|
3696
|
+
verifyQueueSignature
|
|
2468
3697
|
};
|
|
2469
3698
|
//# sourceMappingURL=index.mjs.map
|