@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.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(src_exports, {
|
|
|
38
38
|
AiGatewayVideosResource: () => AiGatewayVideosResource,
|
|
39
39
|
ApiError: () => ApiError,
|
|
40
40
|
ApiKeysResource: () => ApiKeysResource,
|
|
41
|
+
ApplicationDomainsResource: () => ApplicationDomainsResource,
|
|
41
42
|
ApplicationEnvVarsResource: () => ApplicationEnvVarsResource,
|
|
42
43
|
ApplicationRequestsResource: () => ApplicationRequestsResource,
|
|
43
44
|
ApplicationStorageResource: () => ApplicationStorageResource,
|
|
@@ -45,14 +46,25 @@ __export(src_exports, {
|
|
|
45
46
|
AuthenticationError: () => AuthenticationError,
|
|
46
47
|
ConfigurationError: () => ConfigurationError,
|
|
47
48
|
DeploymentsResource: () => DeploymentsResource,
|
|
49
|
+
DomainNotActiveError: () => DomainNotActiveError,
|
|
48
50
|
GigadriveClient: () => GigadriveClient,
|
|
49
51
|
GigadriveError: () => GigadriveError,
|
|
50
52
|
ImageOptimizationResource: () => ImageOptimizationResource,
|
|
53
|
+
NonRetryableError: () => NonRetryableError,
|
|
51
54
|
OrganizationAiGatewayResource: () => OrganizationAiGatewayResource,
|
|
55
|
+
OrganizationDomainsResource: () => OrganizationDomainsResource,
|
|
52
56
|
OrganizationEnvVarsResource: () => OrganizationEnvVarsResource,
|
|
53
57
|
OrganizationMembersResource: () => OrganizationMembersResource,
|
|
54
58
|
OrganizationProductsResource: () => OrganizationProductsResource,
|
|
55
59
|
OrganizationsResource: () => OrganizationsResource,
|
|
60
|
+
QUEUE_DELIVERY_HEADERS: () => QUEUE_DELIVERY_HEADERS,
|
|
61
|
+
QUEUE_RESPONSE_HEADERS: () => QUEUE_RESPONSE_HEADERS,
|
|
62
|
+
QUEUE_SIGNATURE_HEADER: () => QUEUE_SIGNATURE_HEADER,
|
|
63
|
+
QUEUE_SIGNATURE_TOLERANCE_MS: () => QUEUE_SIGNATURE_TOLERANCE_MS,
|
|
64
|
+
QUEUE_SIGNING_SECRET_ENV: () => QUEUE_SIGNING_SECRET_ENV,
|
|
65
|
+
Queue: () => Queue,
|
|
66
|
+
QueuesResource: () => QueuesResource,
|
|
67
|
+
RetryLaterError: () => RetryLaterError,
|
|
56
68
|
StickySessionsResource: () => StickySessionsResource,
|
|
57
69
|
StorageBucketsResource: () => StorageBucketsResource,
|
|
58
70
|
StorageObjectsResource: () => StorageObjectsResource,
|
|
@@ -60,9 +72,17 @@ __export(src_exports, {
|
|
|
60
72
|
StorageUploadSessionsResource: () => StorageUploadSessionsResource,
|
|
61
73
|
UploadError: () => UploadError,
|
|
62
74
|
UploadSessionExpiredError: () => UploadSessionExpiredError,
|
|
75
|
+
WORKFLOW_CONSUMER_PATH: () => WORKFLOW_CONSUMER_PATH,
|
|
76
|
+
WORKFLOW_QUEUE_NAME_HEADER: () => WORKFLOW_QUEUE_NAME_HEADER,
|
|
63
77
|
createManagedImageUrl: () => createManagedImageUrl,
|
|
78
|
+
createWorkflowQueue: () => createWorkflowQueue,
|
|
79
|
+
defaultQueueCodec: () => defaultQueueCodec,
|
|
80
|
+
durationToSeconds: () => durationToSeconds,
|
|
64
81
|
paginate: () => paginate,
|
|
65
|
-
parseSSEStream: () => parseSSEStream
|
|
82
|
+
parseSSEStream: () => parseSSEStream,
|
|
83
|
+
queue: () => queue,
|
|
84
|
+
signQueueDelivery: () => signQueueDelivery,
|
|
85
|
+
verifyQueueSignature: () => verifyQueueSignature
|
|
66
86
|
});
|
|
67
87
|
module.exports = __toCommonJS(src_exports);
|
|
68
88
|
|
|
@@ -90,11 +110,17 @@ var ApiError = class extends GigadriveError {
|
|
|
90
110
|
status;
|
|
91
111
|
/** An optional machine-readable error code from the API response body. */
|
|
92
112
|
code;
|
|
93
|
-
|
|
113
|
+
/**
|
|
114
|
+
* An optional finer-grained reason next to {@link code}, for example `daily_add_limit` with
|
|
115
|
+
* `quota_exceeded`, or `other_organization` with `domain_in_use`.
|
|
116
|
+
*/
|
|
117
|
+
reason;
|
|
118
|
+
constructor(message, status, code, reason) {
|
|
94
119
|
super(message);
|
|
95
120
|
this.name = "ApiError";
|
|
96
121
|
this.status = status;
|
|
97
122
|
this.code = code;
|
|
123
|
+
this.reason = reason;
|
|
98
124
|
}
|
|
99
125
|
};
|
|
100
126
|
var UploadError = class extends GigadriveError {
|
|
@@ -582,10 +608,13 @@ var HttpClient = class {
|
|
|
582
608
|
async toApiError(response) {
|
|
583
609
|
let message;
|
|
584
610
|
let code;
|
|
611
|
+
let reason;
|
|
585
612
|
try {
|
|
586
613
|
const body = await response.json();
|
|
587
614
|
if (typeof body.error === "string") {
|
|
588
615
|
message = body.error;
|
|
616
|
+
if (typeof body.code === "string") code = body.code;
|
|
617
|
+
if (typeof body.reason === "string") reason = body.reason;
|
|
589
618
|
} else if (body.error && typeof body.error === "object") {
|
|
590
619
|
message = body.error.message ?? response.statusText;
|
|
591
620
|
code = body.error.code;
|
|
@@ -595,7 +624,7 @@ var HttpClient = class {
|
|
|
595
624
|
} catch {
|
|
596
625
|
message = response.statusText;
|
|
597
626
|
}
|
|
598
|
-
return new ApiError(message, response.status, code);
|
|
627
|
+
return new ApiError(message, response.status, code, reason);
|
|
599
628
|
}
|
|
600
629
|
buildUrl(path, query) {
|
|
601
630
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -955,6 +984,7 @@ var ApplicationRequestsResource = class extends BaseResource {
|
|
|
955
984
|
};
|
|
956
985
|
|
|
957
986
|
// src/upload/checksum.ts
|
|
987
|
+
var EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
958
988
|
var toHex = (buffer) => {
|
|
959
989
|
const bytes = new Uint8Array(buffer);
|
|
960
990
|
let hex = "";
|
|
@@ -1073,6 +1103,8 @@ var inferContentType = (nameOrKey) => {
|
|
|
1073
1103
|
};
|
|
1074
1104
|
|
|
1075
1105
|
// src/upload/source.ts
|
|
1106
|
+
var noop = () => {
|
|
1107
|
+
};
|
|
1076
1108
|
var isNode = () => typeof process !== "undefined" && !!process.versions?.node;
|
|
1077
1109
|
var toBytes = async (data) => {
|
|
1078
1110
|
if (typeof Blob !== "undefined" && data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
@@ -1119,7 +1151,8 @@ var resolveUploadSource = async (input, options = {}) => {
|
|
|
1119
1151
|
size,
|
|
1120
1152
|
contentType,
|
|
1121
1153
|
checksums: buildChecksums(input, sha256),
|
|
1122
|
-
requiresFiniteChunkSize: false
|
|
1154
|
+
requiresFiniteChunkSize: false,
|
|
1155
|
+
release: noop
|
|
1123
1156
|
};
|
|
1124
1157
|
}
|
|
1125
1158
|
if (input.path !== void 0) {
|
|
@@ -1127,22 +1160,34 @@ var resolveUploadSource = async (input, options = {}) => {
|
|
|
1127
1160
|
const fs = await import("fs");
|
|
1128
1161
|
const size = input.contentLength ?? fs.statSync(input.path).size;
|
|
1129
1162
|
const sha256 = input.checksumSha256 ?? (hash ? (await hashNodeFile(input.path)).sha256 : "");
|
|
1130
|
-
const
|
|
1131
|
-
return {
|
|
1163
|
+
const stream = fs.createReadStream(input.path);
|
|
1164
|
+
return {
|
|
1165
|
+
tusFile: stream,
|
|
1166
|
+
size,
|
|
1167
|
+
contentType,
|
|
1168
|
+
checksums: buildChecksums(input, sha256),
|
|
1169
|
+
requiresFiniteChunkSize: true,
|
|
1170
|
+
release: () => {
|
|
1171
|
+
stream.on("error", noop);
|
|
1172
|
+
stream.destroy();
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1132
1175
|
}
|
|
1133
1176
|
if (input.stream !== void 0) {
|
|
1134
1177
|
if (input.contentLength === void 0) {
|
|
1135
1178
|
throw new Error("Uploading from a stream requires contentLength to be provided.");
|
|
1136
1179
|
}
|
|
1137
|
-
|
|
1180
|
+
const sha256 = input.checksumSha256 ?? (input.contentLength === 0 ? EMPTY_SHA256 : void 0);
|
|
1181
|
+
if (hash && !sha256) {
|
|
1138
1182
|
throw new Error("Uploading from a stream requires checksumSha256 to be provided.");
|
|
1139
1183
|
}
|
|
1140
1184
|
return {
|
|
1141
1185
|
tusFile: input.stream,
|
|
1142
1186
|
size: input.contentLength,
|
|
1143
1187
|
contentType,
|
|
1144
|
-
checksums: buildChecksums(input,
|
|
1145
|
-
requiresFiniteChunkSize: true
|
|
1188
|
+
checksums: buildChecksums(input, sha256 ?? ""),
|
|
1189
|
+
requiresFiniteChunkSize: true,
|
|
1190
|
+
release: noop
|
|
1146
1191
|
};
|
|
1147
1192
|
}
|
|
1148
1193
|
throw new Error("No upload source provided. Pass one of: data, path, or stream.");
|
|
@@ -1515,7 +1560,7 @@ var StorageUploadSessionsResource = class extends BaseResource {
|
|
|
1515
1560
|
async uploadToUrl(url, source, options) {
|
|
1516
1561
|
const resolved = await resolveUploadSource({ key: "", ...source }, { hash: false });
|
|
1517
1562
|
const headers = { "Tus-Resumable": "1.0.0", ...options?.headers };
|
|
1518
|
-
await runResolvedUpload(this.transport, url, resolved, options, headers).catch(toUploadError);
|
|
1563
|
+
await runResolvedUpload(this.transport, url, resolved, options, headers).catch(toUploadError).finally(resolved.release);
|
|
1519
1564
|
}
|
|
1520
1565
|
/**
|
|
1521
1566
|
* Resume an interrupted upload to a previously issued signed upload URL. The
|
|
@@ -1532,6 +1577,7 @@ var StorageUploadSessionsResource = class extends BaseResource {
|
|
|
1532
1577
|
|
|
1533
1578
|
// src/resources/application-storage.ts
|
|
1534
1579
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1580
|
+
var buildStorageObjectUrl = (cdnHostname, key) => `https://${cdnHostname}/${encodeURIComponent(key).replace(/%2F/g, "/")}`;
|
|
1535
1581
|
var ApplicationStorageResource = class {
|
|
1536
1582
|
constructor(httpClient, transport = tusUploadTransport, defaultApplicationId) {
|
|
1537
1583
|
this.defaultApplicationId = defaultApplicationId;
|
|
@@ -1561,6 +1607,10 @@ var ApplicationStorageResource = class {
|
|
|
1561
1607
|
* Node filesystem `path`, or a Node readable `stream` (with `contentLength`
|
|
1562
1608
|
* and `checksumSha256`). The content type is inferred from `key` when omitted.
|
|
1563
1609
|
*
|
|
1610
|
+
* Empty (zero-byte) files are supported for every input kind. The API stores
|
|
1611
|
+
* them during session creation, so no bytes are transferred, `onProgress` is
|
|
1612
|
+
* not called, `waitForCompletion` does not poll, and `object` is always set.
|
|
1613
|
+
*
|
|
1564
1614
|
* @param input - What and where to upload, plus optional transfer options.
|
|
1565
1615
|
* @returns The upload session and public object URL (and the finalized object when `waitForCompletion` is set).
|
|
1566
1616
|
* @throws {@link UploadError} if the byte upload fails.
|
|
@@ -1591,19 +1641,42 @@ var ApplicationStorageResource = class {
|
|
|
1591
1641
|
checksumSha1: input.checksumSha1,
|
|
1592
1642
|
checksumMd5: input.checksumMd5
|
|
1593
1643
|
});
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1644
|
+
if (input.signal?.aborted) {
|
|
1645
|
+
resolved.release();
|
|
1646
|
+
throw createAbortError();
|
|
1647
|
+
}
|
|
1648
|
+
let created;
|
|
1649
|
+
try {
|
|
1650
|
+
created = await this.uploadSessions.create(
|
|
1651
|
+
applicationId,
|
|
1652
|
+
bucketRef,
|
|
1653
|
+
{
|
|
1654
|
+
key: input.key,
|
|
1655
|
+
contentLength: resolved.size,
|
|
1656
|
+
checksumSha256: resolved.checksums.sha256,
|
|
1657
|
+
contentType: resolved.contentType,
|
|
1658
|
+
checksumSha1: resolved.checksums.sha1,
|
|
1659
|
+
checksumMd5: resolved.checksums.md5
|
|
1660
|
+
},
|
|
1661
|
+
{ environment }
|
|
1662
|
+
);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
resolved.release();
|
|
1665
|
+
throw error;
|
|
1666
|
+
}
|
|
1667
|
+
const { session, upload } = created;
|
|
1668
|
+
if (upload === null) {
|
|
1669
|
+
resolved.release();
|
|
1670
|
+
const object = created.object;
|
|
1671
|
+
if (!object) {
|
|
1672
|
+
throw new UploadError("The upload session completed without returning the stored object.");
|
|
1673
|
+
}
|
|
1674
|
+
const url = created.publicObjectUrl ?? buildStorageObjectUrl(
|
|
1675
|
+
(await this.buckets.get(applicationId, bucketRef, { environment })).cdnHostname,
|
|
1676
|
+
object.key
|
|
1677
|
+
);
|
|
1678
|
+
return { session, url, object };
|
|
1679
|
+
}
|
|
1607
1680
|
await runResolvedUpload(
|
|
1608
1681
|
this.transport,
|
|
1609
1682
|
upload.url,
|
|
@@ -1618,7 +1691,7 @@ var ApplicationStorageResource = class {
|
|
|
1618
1691
|
},
|
|
1619
1692
|
// Forward any required headers the API issued with the session.
|
|
1620
1693
|
{ "Tus-Resumable": "1.0.0", ...upload.headers }
|
|
1621
|
-
).catch(toUploadError);
|
|
1694
|
+
).catch(toUploadError).finally(resolved.release);
|
|
1622
1695
|
if (input.waitForCompletion) {
|
|
1623
1696
|
const options = completionOptions ?? {};
|
|
1624
1697
|
const completed = await this.waitForCompletion(applicationId, bucketRef, session.id, {
|
|
@@ -1697,6 +1770,201 @@ var ApplicationStorageResource = class {
|
|
|
1697
1770
|
}
|
|
1698
1771
|
};
|
|
1699
1772
|
|
|
1773
|
+
// src/resources/domains.ts
|
|
1774
|
+
var DomainNotActiveError = class extends GigadriveError {
|
|
1775
|
+
/** The domain as of the last successful check. */
|
|
1776
|
+
domain;
|
|
1777
|
+
/** `timeout` when the wait ran out, `removing` when the domain was removed, otherwise the state that ended it. */
|
|
1778
|
+
reason;
|
|
1779
|
+
constructor(domain, reason) {
|
|
1780
|
+
const detail = domain.error ? `: ${domain.error.message}` : "";
|
|
1781
|
+
super(
|
|
1782
|
+
reason === "timeout" ? `${domain.unicodeHostname} is not active yet (state: ${domain.state})${detail}` : `${domain.unicodeHostname} is ${reason}${detail}`
|
|
1783
|
+
);
|
|
1784
|
+
this.name = "DomainNotActiveError";
|
|
1785
|
+
this.domain = domain;
|
|
1786
|
+
this.reason = reason;
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
var SERVING_STATES = /* @__PURE__ */ new Set(["active", "degraded"]);
|
|
1790
|
+
var isTransient = (error) => error instanceof ApiError ? error.status === 429 || error.status >= 500 : error instanceof TypeError;
|
|
1791
|
+
var pollSignal = (ms, signal) => {
|
|
1792
|
+
const controller = new AbortController();
|
|
1793
|
+
const timer = setTimeout(() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")), ms);
|
|
1794
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
1795
|
+
if (signal?.aborted) onAbort();
|
|
1796
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
1797
|
+
return {
|
|
1798
|
+
signal: controller.signal,
|
|
1799
|
+
dispose: () => {
|
|
1800
|
+
clearTimeout(timer);
|
|
1801
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
};
|
|
1805
|
+
var sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
1806
|
+
if (signal?.aborted) {
|
|
1807
|
+
reject(signal.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const timer = setTimeout(() => {
|
|
1811
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1812
|
+
resolve();
|
|
1813
|
+
}, ms);
|
|
1814
|
+
const onAbort = () => {
|
|
1815
|
+
clearTimeout(timer);
|
|
1816
|
+
reject(signal?.reason instanceof Error ? signal.reason : new GigadriveError("Aborted"));
|
|
1817
|
+
};
|
|
1818
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1819
|
+
});
|
|
1820
|
+
var ApplicationDomainsResource = class extends BaseResource {
|
|
1821
|
+
/**
|
|
1822
|
+
* List the custom domains of an application.
|
|
1823
|
+
*
|
|
1824
|
+
* @example
|
|
1825
|
+
* ```ts
|
|
1826
|
+
* const { items } = await client.applications.domains.list('app-id');
|
|
1827
|
+
* for (const domain of items) console.log(domain.hostname, domain.state);
|
|
1828
|
+
* ```
|
|
1829
|
+
*/
|
|
1830
|
+
async list(applicationId, query) {
|
|
1831
|
+
return this.httpClient.get(`/applications/${applicationId}/domains`, {
|
|
1832
|
+
query
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Attach a hostname to an application.
|
|
1837
|
+
*
|
|
1838
|
+
* @returns The new domain, including the `requiredRecords` to publish at your DNS provider.
|
|
1839
|
+
*
|
|
1840
|
+
* @example
|
|
1841
|
+
* ```ts
|
|
1842
|
+
* const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
|
|
1843
|
+
* for (const record of domain.requiredRecords) console.log(record.type, record.host, record.value);
|
|
1844
|
+
* ```
|
|
1845
|
+
*/
|
|
1846
|
+
async add(applicationId, data) {
|
|
1847
|
+
return this.httpClient.post(`/applications/${applicationId}/domains`, data);
|
|
1848
|
+
}
|
|
1849
|
+
/** Get one custom domain with its current state. */
|
|
1850
|
+
async get(applicationId, domainId, options) {
|
|
1851
|
+
const path = `/applications/${applicationId}/domains/${domainId}`;
|
|
1852
|
+
return options?.signal ? this.httpClient.get(path, { signal: options.signal }) : this.httpClient.get(path);
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Change what a domain serves, or make it the primary domain.
|
|
1856
|
+
*
|
|
1857
|
+
* @example
|
|
1858
|
+
* ```ts
|
|
1859
|
+
* await client.applications.domains.update('app-id', 'domain-id', {
|
|
1860
|
+
* target: { type: 'redirect', to: 'www.example.com' },
|
|
1861
|
+
* });
|
|
1862
|
+
* ```
|
|
1863
|
+
*/
|
|
1864
|
+
async update(applicationId, domainId, data) {
|
|
1865
|
+
return this.httpClient.patch(`/applications/${applicationId}/domains/${domainId}`, data);
|
|
1866
|
+
}
|
|
1867
|
+
/** Check the domain now instead of waiting for the next scheduled check. Rate limited. */
|
|
1868
|
+
async refresh(applicationId, domainId) {
|
|
1869
|
+
return this.httpClient.post(`/applications/${applicationId}/domains/${domainId}/refresh`);
|
|
1870
|
+
}
|
|
1871
|
+
/** Stop serving the domain and remove it. */
|
|
1872
|
+
async remove(applicationId, domainId) {
|
|
1873
|
+
return this.httpClient.delete(`/applications/${applicationId}/domains/${domainId}`);
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Move a hostname another organization attached to this application. Verify the domain for your
|
|
1877
|
+
* organization first and keep its TXT record published: the claim checks the record again.
|
|
1878
|
+
*/
|
|
1879
|
+
async claim(applicationId, hostname) {
|
|
1880
|
+
return this.httpClient.post(`/applications/${applicationId}/domains/claim`, { hostname });
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Poll a domain until it serves traffic.
|
|
1884
|
+
*
|
|
1885
|
+
* Rate limits, server errors and network failures do not end the wait; the next poll retries until
|
|
1886
|
+
* the timeout. Aborting the signal or reaching the timeout also cancels a request in flight.
|
|
1887
|
+
*
|
|
1888
|
+
* @returns The domain once it is `active` (or `degraded`, which also serves).
|
|
1889
|
+
* @throws {DomainNotActiveError} When it fails, is suspended or removed, or the timeout runs out.
|
|
1890
|
+
*
|
|
1891
|
+
* @example
|
|
1892
|
+
* ```ts
|
|
1893
|
+
* const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
|
|
1894
|
+
* await client.applications.domains.waitUntilActive('app-id', domain.id, {
|
|
1895
|
+
* onState: (current) => console.log(current.state),
|
|
1896
|
+
* });
|
|
1897
|
+
* ```
|
|
1898
|
+
*/
|
|
1899
|
+
async waitUntilActive(applicationId, domainId, options = {}) {
|
|
1900
|
+
const { timeoutMs = 10 * 6e4, intervalMs = 5e3, signal, onState } = options;
|
|
1901
|
+
const deadline = Date.now() + timeoutMs;
|
|
1902
|
+
let last;
|
|
1903
|
+
for (; ; ) {
|
|
1904
|
+
const poll = pollSignal(Math.max(deadline - Date.now(), 1), signal);
|
|
1905
|
+
let domain;
|
|
1906
|
+
try {
|
|
1907
|
+
domain = await this.get(applicationId, domainId, { signal: poll.signal });
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
if (signal?.aborted) throw error;
|
|
1910
|
+
if (last && error instanceof ApiError && error.status === 404) throw new DomainNotActiveError(last, "removing");
|
|
1911
|
+
if (!poll.signal.aborted && !isTransient(error)) throw error;
|
|
1912
|
+
if (Date.now() >= deadline && !last) throw error;
|
|
1913
|
+
} finally {
|
|
1914
|
+
poll.dispose();
|
|
1915
|
+
}
|
|
1916
|
+
if (domain) {
|
|
1917
|
+
if (domain.state !== last?.state) onState?.(domain);
|
|
1918
|
+
last = domain;
|
|
1919
|
+
if (SERVING_STATES.has(domain.state)) return domain;
|
|
1920
|
+
if (domain.state === "failed" || domain.state === "suspended" || domain.state === "removing") {
|
|
1921
|
+
throw new DomainNotActiveError(domain, domain.state);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
const untilDeadline = deadline - Date.now();
|
|
1925
|
+
if (untilDeadline <= 0) {
|
|
1926
|
+
if (last) throw new DomainNotActiveError(last, "timeout");
|
|
1927
|
+
throw new GigadriveError("Timed out before the domain could be read");
|
|
1928
|
+
}
|
|
1929
|
+
await sleep(Math.min(intervalMs, untilDeadline), signal);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
var OrganizationDomainsResource = class extends BaseResource {
|
|
1934
|
+
/** List the organization's claimed and verified domains. */
|
|
1935
|
+
async list(organizationId, query) {
|
|
1936
|
+
return this.httpClient.get(`/organizations/${organizationId}/domains`, {
|
|
1937
|
+
query
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Claim a domain for verification.
|
|
1942
|
+
*
|
|
1943
|
+
* @returns The claim, including the TXT `record` to publish.
|
|
1944
|
+
*
|
|
1945
|
+
* @example
|
|
1946
|
+
* ```ts
|
|
1947
|
+
* const claim = await client.organizations.domains.add('org-id', 'example.com');
|
|
1948
|
+
* console.log(claim.record.host, claim.record.value);
|
|
1949
|
+
* ```
|
|
1950
|
+
*/
|
|
1951
|
+
async add(organizationId, name) {
|
|
1952
|
+
return this.httpClient.post(`/organizations/${organizationId}/domains`, { name });
|
|
1953
|
+
}
|
|
1954
|
+
/** Get one claimed or verified domain. */
|
|
1955
|
+
async get(organizationId, domainId) {
|
|
1956
|
+
return this.httpClient.get(`/organizations/${organizationId}/domains/${domainId}`);
|
|
1957
|
+
}
|
|
1958
|
+
/** Check the TXT record now. Rate limited. */
|
|
1959
|
+
async verify(organizationId, domainId) {
|
|
1960
|
+
return this.httpClient.post(`/organizations/${organizationId}/domains/${domainId}/verify`);
|
|
1961
|
+
}
|
|
1962
|
+
/** Remove a domain. Refused while custom domains still rely on it. */
|
|
1963
|
+
async remove(organizationId, domainId) {
|
|
1964
|
+
return this.httpClient.delete(`/organizations/${organizationId}/domains/${domainId}`);
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1700
1968
|
// src/resources/applications.ts
|
|
1701
1969
|
var ApplicationsResource = class extends BaseResource {
|
|
1702
1970
|
/**
|
|
@@ -1738,11 +2006,22 @@ var ApplicationsResource = class extends BaseResource {
|
|
|
1738
2006
|
* ```
|
|
1739
2007
|
*/
|
|
1740
2008
|
requests;
|
|
2009
|
+
/**
|
|
2010
|
+
* Custom domains attached to the application.
|
|
2011
|
+
*
|
|
2012
|
+
* @example
|
|
2013
|
+
* ```ts
|
|
2014
|
+
* const domain = await client.applications.domains.add('app-id', { hostname: 'shop.example.com' });
|
|
2015
|
+
* await client.applications.domains.waitUntilActive('app-id', domain.id);
|
|
2016
|
+
* ```
|
|
2017
|
+
*/
|
|
2018
|
+
domains;
|
|
1741
2019
|
constructor(httpClient, defaultApplicationId) {
|
|
1742
2020
|
super(httpClient);
|
|
1743
2021
|
this.envVars = new ApplicationEnvVarsResource(this.httpClient);
|
|
1744
2022
|
this.storage = new ApplicationStorageResource(this.httpClient, void 0, defaultApplicationId);
|
|
1745
2023
|
this.requests = new ApplicationRequestsResource(this.httpClient);
|
|
2024
|
+
this.domains = new ApplicationDomainsResource(this.httpClient);
|
|
1746
2025
|
}
|
|
1747
2026
|
/**
|
|
1748
2027
|
* List applications the authenticated actor has access to.
|
|
@@ -2368,12 +2647,23 @@ var OrganizationsResource = class extends BaseResource {
|
|
|
2368
2647
|
* ```
|
|
2369
2648
|
*/
|
|
2370
2649
|
products;
|
|
2650
|
+
/**
|
|
2651
|
+
* Domains the organization verified. A verified domain lets every application attach hostnames
|
|
2652
|
+
* below it without another TXT record.
|
|
2653
|
+
*
|
|
2654
|
+
* @example
|
|
2655
|
+
* ```ts
|
|
2656
|
+
* const claim = await client.organizations.domains.add('org-id', 'example.com');
|
|
2657
|
+
* ```
|
|
2658
|
+
*/
|
|
2659
|
+
domains;
|
|
2371
2660
|
constructor(...args) {
|
|
2372
2661
|
super(...args);
|
|
2373
2662
|
this.envVars = new OrganizationEnvVarsResource(this.httpClient);
|
|
2374
2663
|
this.aiGateway = new OrganizationAiGatewayResource(this.httpClient);
|
|
2375
2664
|
this.members = new OrganizationMembersResource(this.httpClient);
|
|
2376
2665
|
this.products = new OrganizationProductsResource(this.httpClient);
|
|
2666
|
+
this.domains = new OrganizationDomainsResource(this.httpClient);
|
|
2377
2667
|
}
|
|
2378
2668
|
/**
|
|
2379
2669
|
* List organizations the authenticated actor has access to.
|
|
@@ -2427,6 +2717,783 @@ var OrganizationsResource = class extends BaseResource {
|
|
|
2427
2717
|
}
|
|
2428
2718
|
};
|
|
2429
2719
|
|
|
2720
|
+
// src/queues/encoding.ts
|
|
2721
|
+
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/;
|
|
2722
|
+
var UNIT_SECONDS = { ms: 1e-3, s: 1, m: 60, h: 3600, d: 86400 };
|
|
2723
|
+
var durationToSeconds = (value) => {
|
|
2724
|
+
let seconds;
|
|
2725
|
+
if (typeof value === "number") {
|
|
2726
|
+
seconds = value;
|
|
2727
|
+
} else {
|
|
2728
|
+
const match = DURATION_PATTERN.exec(value.trim());
|
|
2729
|
+
if (!match) throw new Error(`Invalid duration "${value}". Use seconds or a string such as 30s, 10m, 12h or 7d.`);
|
|
2730
|
+
seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
|
|
2731
|
+
}
|
|
2732
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new Error(`Invalid duration "${String(value)}"`);
|
|
2733
|
+
return Math.ceil(seconds);
|
|
2734
|
+
};
|
|
2735
|
+
var textEncoder = new TextEncoder();
|
|
2736
|
+
var textDecoder = new TextDecoder();
|
|
2737
|
+
var utf8Encode = (value) => textEncoder.encode(value);
|
|
2738
|
+
var utf8Decode = (bytes) => textDecoder.decode(bytes);
|
|
2739
|
+
var bytesToBase64 = (bytes) => {
|
|
2740
|
+
let binary = "";
|
|
2741
|
+
for (let index = 0; index < bytes.length; index += 32768) {
|
|
2742
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
2743
|
+
}
|
|
2744
|
+
return btoa(binary);
|
|
2745
|
+
};
|
|
2746
|
+
var base64ToBytes = (value) => {
|
|
2747
|
+
const binary = atob(value);
|
|
2748
|
+
const bytes = new Uint8Array(binary.length);
|
|
2749
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
2750
|
+
return bytes;
|
|
2751
|
+
};
|
|
2752
|
+
var toHex2 = (buffer) => Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2753
|
+
var toArrayBuffer = (bytes) => {
|
|
2754
|
+
const copy = new ArrayBuffer(bytes.byteLength);
|
|
2755
|
+
new Uint8Array(copy).set(bytes);
|
|
2756
|
+
return copy;
|
|
2757
|
+
};
|
|
2758
|
+
|
|
2759
|
+
// src/queues/errors.ts
|
|
2760
|
+
var RetryLaterError = class extends GigadriveError {
|
|
2761
|
+
/** Seconds until the next delivery. */
|
|
2762
|
+
delaySeconds;
|
|
2763
|
+
constructor(delay2, message = "Message deferred") {
|
|
2764
|
+
super(message);
|
|
2765
|
+
this.name = "RetryLaterError";
|
|
2766
|
+
this.delaySeconds = durationToSeconds(delay2);
|
|
2767
|
+
}
|
|
2768
|
+
};
|
|
2769
|
+
var NonRetryableError = class extends GigadriveError {
|
|
2770
|
+
constructor(message) {
|
|
2771
|
+
super(message);
|
|
2772
|
+
this.name = "NonRetryableError";
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
|
|
2776
|
+
// src/queues/signature.ts
|
|
2777
|
+
var QUEUE_SIGNATURE_HEADER = "x-gigadrive-queue-signature";
|
|
2778
|
+
var QUEUE_SIGNATURE_TOLERANCE_MS = 5 * 6e4;
|
|
2779
|
+
var QUEUE_SIGNING_SECRET_ENV = "GIGADRIVE_QUEUE_SIGNING_SECRET";
|
|
2780
|
+
var hmacSha256Hex = async (secret, payload) => {
|
|
2781
|
+
const key = await crypto.subtle.importKey(
|
|
2782
|
+
"raw",
|
|
2783
|
+
toArrayBuffer(utf8Encode(secret)),
|
|
2784
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2785
|
+
false,
|
|
2786
|
+
["sign"]
|
|
2787
|
+
);
|
|
2788
|
+
return toHex2(await crypto.subtle.sign("HMAC", key, toArrayBuffer(utf8Encode(payload))));
|
|
2789
|
+
};
|
|
2790
|
+
var signedPayload = async (input, timestamp) => {
|
|
2791
|
+
const body = typeof input.body === "string" ? utf8Encode(input.body) : input.body;
|
|
2792
|
+
const bodyHash = toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(body)));
|
|
2793
|
+
return `${String(timestamp)}.${input.queue}.${input.messageId}.${String(input.attempt)}.${bodyHash}`;
|
|
2794
|
+
};
|
|
2795
|
+
var safeEqual = (left, right) => {
|
|
2796
|
+
if (left.length !== right.length) return false;
|
|
2797
|
+
let difference = 0;
|
|
2798
|
+
for (let index = 0; index < left.length; index++) difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
|
|
2799
|
+
return difference === 0;
|
|
2800
|
+
};
|
|
2801
|
+
async function signQueueDelivery(input, timestamp = Date.now()) {
|
|
2802
|
+
return `t=${String(timestamp)},v1=${await hmacSha256Hex(input.secret, await signedPayload(input, timestamp))}`;
|
|
2803
|
+
}
|
|
2804
|
+
async function verifyQueueSignature(header, input, now = Date.now()) {
|
|
2805
|
+
if (!header || !input.secret) return false;
|
|
2806
|
+
const fields = /* @__PURE__ */ new Map();
|
|
2807
|
+
for (const part of header.split(",")) {
|
|
2808
|
+
const separator = part.indexOf("=");
|
|
2809
|
+
if (separator > 0) fields.set(part.slice(0, separator).trim(), part.slice(separator + 1).trim());
|
|
2810
|
+
}
|
|
2811
|
+
const timestamp = Number(fields.get("t"));
|
|
2812
|
+
const signature = fields.get("v1");
|
|
2813
|
+
if (!Number.isSafeInteger(timestamp) || !signature || Math.abs(now - timestamp) > QUEUE_SIGNATURE_TOLERANCE_MS) {
|
|
2814
|
+
return false;
|
|
2815
|
+
}
|
|
2816
|
+
const expected = await hmacSha256Hex(input.secret, await signedPayload(input, timestamp));
|
|
2817
|
+
return safeEqual(signature.toLowerCase(), expected);
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// src/queues/queue.ts
|
|
2821
|
+
var QUEUE_DELIVERY_HEADERS = {
|
|
2822
|
+
name: "x-gigadrive-queue-name",
|
|
2823
|
+
messageId: "x-gigadrive-queue-message-id",
|
|
2824
|
+
attempt: "x-gigadrive-queue-attempt",
|
|
2825
|
+
createdAt: "x-gigadrive-queue-created-at",
|
|
2826
|
+
expiresAt: "x-gigadrive-queue-expires-at",
|
|
2827
|
+
deadline: "x-gigadrive-queue-deadline",
|
|
2828
|
+
groupKey: "x-gigadrive-queue-group-key",
|
|
2829
|
+
deduplicationKey: "x-gigadrive-queue-deduplication-key",
|
|
2830
|
+
deploymentId: "x-gigadrive-queue-deployment-id",
|
|
2831
|
+
deploymentFallback: "x-gigadrive-queue-deployment-fallback",
|
|
2832
|
+
signature: QUEUE_SIGNATURE_HEADER
|
|
2833
|
+
};
|
|
2834
|
+
var QUEUE_RESPONSE_HEADERS = {
|
|
2835
|
+
/** On a 2xx: deliver again after this many seconds without spending an attempt. */
|
|
2836
|
+
retryAfter: "x-gigadrive-queue-retry-after",
|
|
2837
|
+
/** `dead-letter`: stop retrying now, whatever the status. */
|
|
2838
|
+
action: "x-gigadrive-queue-action",
|
|
2839
|
+
/**
|
|
2840
|
+
* Reason recorded on the message. Read only together with `action:
|
|
2841
|
+
* dead-letter`; for any other failure the platform records `HTTP <status>`
|
|
2842
|
+
* and the first 512 bytes of the response body.
|
|
2843
|
+
*/
|
|
2844
|
+
error: "x-gigadrive-queue-error"
|
|
2845
|
+
};
|
|
2846
|
+
var isJsonContentType = (contentType) => /^application\/(?:[\w.+-]+\+)?json\b/i.test(contentType);
|
|
2847
|
+
var defaultQueueCodec = {
|
|
2848
|
+
encode(payload) {
|
|
2849
|
+
if (payload instanceof Uint8Array || payload instanceof ArrayBuffer) {
|
|
2850
|
+
const bytes = payload instanceof Uint8Array ? payload : new Uint8Array(payload);
|
|
2851
|
+
return { body: bytesToBase64(bytes), encoding: "base64", contentType: "application/octet-stream" };
|
|
2852
|
+
}
|
|
2853
|
+
const json = JSON.stringify(payload);
|
|
2854
|
+
if (json === void 0) throw new TypeError("Queue payloads must be JSON-serializable or binary");
|
|
2855
|
+
return { body: json, encoding: "utf8", contentType: "application/json" };
|
|
2856
|
+
},
|
|
2857
|
+
decode(body, contentType) {
|
|
2858
|
+
if (isJsonContentType(contentType)) return body.length === 0 ? null : JSON.parse(utf8Decode(body));
|
|
2859
|
+
if (/^text\//i.test(contentType)) return utf8Decode(body);
|
|
2860
|
+
return body;
|
|
2861
|
+
}
|
|
2862
|
+
};
|
|
2863
|
+
var errorText = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2e3);
|
|
2864
|
+
var headerText = (text) => text.replace(/[^\x20-\x7e]/g, " ").trim().slice(0, 500);
|
|
2865
|
+
var toDate = (value) => {
|
|
2866
|
+
if (!value) return null;
|
|
2867
|
+
const date = new Date(/^\d+$/.test(value) ? Number(value) : value);
|
|
2868
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
2869
|
+
};
|
|
2870
|
+
var BATCH_LIMIT = 100;
|
|
2871
|
+
var ACK_FLUSH_MS = 25;
|
|
2872
|
+
var ACK_RETRY_DELAYS_MS = [200, 1e3];
|
|
2873
|
+
var EMPTY_POLL_MIN_MS = 1e3;
|
|
2874
|
+
var EMPTY_POLL_MAX_MS = 5e3;
|
|
2875
|
+
var sleep2 = (ms, signal) => new Promise((resolve) => {
|
|
2876
|
+
if (signal?.aborted) return resolve();
|
|
2877
|
+
const timer = setTimeout(done, ms);
|
|
2878
|
+
function done() {
|
|
2879
|
+
clearTimeout(timer);
|
|
2880
|
+
signal?.removeEventListener("abort", done);
|
|
2881
|
+
resolve();
|
|
2882
|
+
}
|
|
2883
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
2884
|
+
});
|
|
2885
|
+
var Queue = class {
|
|
2886
|
+
constructor(name, resource, options = {}) {
|
|
2887
|
+
this.name = name;
|
|
2888
|
+
this.resource = resource;
|
|
2889
|
+
this.options = options;
|
|
2890
|
+
this.codec = options.codec ?? defaultQueueCodec;
|
|
2891
|
+
}
|
|
2892
|
+
name;
|
|
2893
|
+
resource;
|
|
2894
|
+
options;
|
|
2895
|
+
codec;
|
|
2896
|
+
get scope() {
|
|
2897
|
+
return { applicationId: this.options.applicationId, environment: this.options.environment };
|
|
2898
|
+
}
|
|
2899
|
+
toMessage(payload, options = {}) {
|
|
2900
|
+
const encoded = this.codec.encode(payload);
|
|
2901
|
+
const at = options.at === void 0 ? void 0 : new Date(options.at);
|
|
2902
|
+
if (at && Number.isNaN(at.getTime())) throw new ConfigurationError("Invalid `at` time for a queue message");
|
|
2903
|
+
return {
|
|
2904
|
+
body: encoded.body,
|
|
2905
|
+
encoding: encoded.encoding,
|
|
2906
|
+
contentType: encoded.contentType,
|
|
2907
|
+
...options.headers ? { headers: options.headers } : {},
|
|
2908
|
+
...at ? { deliverAt: at.toISOString() } : {},
|
|
2909
|
+
...options.delay !== void 0 && !at ? { delaySeconds: durationToSeconds(options.delay) } : {},
|
|
2910
|
+
...options.groupKey ? { groupKey: options.groupKey } : {},
|
|
2911
|
+
...options.deduplicationKey ? { deduplicationKey: options.deduplicationKey } : {},
|
|
2912
|
+
...options.deploymentId ? { deploymentId: options.deploymentId } : {}
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
decode(body, contentType) {
|
|
2916
|
+
const decoded = this.codec.decode(body, contentType);
|
|
2917
|
+
return this.options.validate ? this.options.validate(decoded) : decoded;
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Sends one message.
|
|
2921
|
+
*
|
|
2922
|
+
* @example
|
|
2923
|
+
* ```ts
|
|
2924
|
+
* await emails.send({ to: 'jane@example.com' });
|
|
2925
|
+
* await emails.send({ to: 'jane@example.com' }, { delay: '10m' });
|
|
2926
|
+
* await emails.send(reminder, { at: new Date('2027-01-01T09:00:00Z'), deduplicationKey: `reminder:${id}` });
|
|
2927
|
+
* ```
|
|
2928
|
+
*
|
|
2929
|
+
* @throws {@link ApiError} when the message is refused, for example with
|
|
2930
|
+
* `code: 'backlog_full'` when your organization's queued storage is full.
|
|
2931
|
+
* Its `status` is 429 when sending again later may succeed and 400 when it
|
|
2932
|
+
* will not, so retry logic that keys on the status stays correct.
|
|
2933
|
+
*/
|
|
2934
|
+
async send(payload, options) {
|
|
2935
|
+
const result = await this.resource().send(this.name, this.toMessage(payload, options), {
|
|
2936
|
+
...this.scope,
|
|
2937
|
+
autoCreate: this.options.autoCreate
|
|
2938
|
+
});
|
|
2939
|
+
if (result.messageId === null) throw new ApiError(result.error, result.retryable ? 429 : 400, result.code);
|
|
2940
|
+
return { messageId: result.messageId, deduplicated: result.deduplicated, deliverAt: new Date(result.deliverAt) };
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Sends many messages, 100 per request. Results come back in input order;
|
|
2944
|
+
* a refused message does not fail the rest; its error carries status 429
|
|
2945
|
+
* when it may be sent again later and 400 when it may not. Chunks are split by count only:
|
|
2946
|
+
* a request whose message bodies exceed 4 MiB in total is refused whole.
|
|
2947
|
+
*/
|
|
2948
|
+
async sendBatch(messages) {
|
|
2949
|
+
const results = [];
|
|
2950
|
+
for (let index = 0; index < messages.length; index += BATCH_LIMIT) {
|
|
2951
|
+
const chunk = messages.slice(index, index + BATCH_LIMIT);
|
|
2952
|
+
const sent = await this.resource().sendBatch(
|
|
2953
|
+
this.name,
|
|
2954
|
+
chunk.map(({ payload, ...options }) => this.toMessage(payload, options)),
|
|
2955
|
+
{ ...this.scope, autoCreate: this.options.autoCreate }
|
|
2956
|
+
);
|
|
2957
|
+
for (const result of sent) {
|
|
2958
|
+
results.push(
|
|
2959
|
+
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) }
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
return results;
|
|
2964
|
+
}
|
|
2965
|
+
/** Deletes a message that has not been processed yet, such as a scheduled send. */
|
|
2966
|
+
async cancel(messageId) {
|
|
2967
|
+
await this.resource().deleteMessage(this.name, messageId, this.scope);
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* Leases messages from a pull queue. Waits up to `wait` (at most 20
|
|
2971
|
+
* seconds) for the first message, then returns what is available.
|
|
2972
|
+
*/
|
|
2973
|
+
async receive(options = {}) {
|
|
2974
|
+
const records = await this.resource().receive(
|
|
2975
|
+
this.name,
|
|
2976
|
+
{
|
|
2977
|
+
maxMessages: options.maxMessages ?? 1,
|
|
2978
|
+
...options.wait === void 0 ? {} : { waitSeconds: Math.min(durationToSeconds(options.wait), 20) },
|
|
2979
|
+
...options.visibilityTimeout === void 0 ? {} : { visibilityTimeoutSeconds: durationToSeconds(options.visibilityTimeout) }
|
|
2980
|
+
},
|
|
2981
|
+
{ ...this.scope, signal: options.signal }
|
|
2982
|
+
);
|
|
2983
|
+
return records.map((record) => this.toReceived(record));
|
|
2984
|
+
}
|
|
2985
|
+
toReceived(record) {
|
|
2986
|
+
const resource = this.resource();
|
|
2987
|
+
const receipt = record.receipt ?? "";
|
|
2988
|
+
const bytes = record.encoding === "base64" ? base64ToBytes(record.body) : utf8Encode(record.body);
|
|
2989
|
+
const nack = (request) => resource.nack(this.name, record.id, { receipt, ...request }, this.scope).then(() => void 0);
|
|
2990
|
+
let payload;
|
|
2991
|
+
let decodeError;
|
|
2992
|
+
try {
|
|
2993
|
+
payload = this.decode(bytes, record.contentType);
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
decodeError = error;
|
|
2996
|
+
}
|
|
2997
|
+
return {
|
|
2998
|
+
messageId: record.id,
|
|
2999
|
+
queue: this.name,
|
|
3000
|
+
attempt: record.attempts,
|
|
3001
|
+
createdAt: toDate(record.createdAt),
|
|
3002
|
+
groupKey: record.groupKey,
|
|
3003
|
+
deduplicationKey: record.deduplicationKey,
|
|
3004
|
+
deploymentId: record.deploymentId,
|
|
3005
|
+
headers: record.headers,
|
|
3006
|
+
receipt,
|
|
3007
|
+
leaseExpiresAt: toDate(record.leaseExpiresAt),
|
|
3008
|
+
get payload() {
|
|
3009
|
+
if (decodeError !== void 0) throw new NonRetryableError(`Undecodable payload: ${errorText(decodeError)}`);
|
|
3010
|
+
return payload;
|
|
3011
|
+
},
|
|
3012
|
+
ack: () => resource.ack(this.name, record.id, receipt, this.scope).then(() => void 0),
|
|
3013
|
+
retry: (options = {}) => nack({
|
|
3014
|
+
...options.delay === void 0 ? {} : { delaySeconds: durationToSeconds(options.delay) },
|
|
3015
|
+
...options.error ? { error: options.error } : {}
|
|
3016
|
+
}),
|
|
3017
|
+
defer: (delay2) => nack({ delaySeconds: durationToSeconds(delay2), countAttempt: false }),
|
|
3018
|
+
deadLetter: (reason) => nack({ deadLetter: true, ...reason ? { error: reason } : {} }),
|
|
3019
|
+
extendLease: (duration) => resource.extend(this.name, record.id, receipt, durationToSeconds(duration), this.scope).then(() => void 0)
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
/**
|
|
3023
|
+
* Receives and processes messages until `signal` aborts, or until the queue
|
|
3024
|
+
* is empty with `stopWhenEmpty`. A handler that returns acknowledges the
|
|
3025
|
+
* message; {@link RetryLaterError} defers it, {@link NonRetryableError}
|
|
3026
|
+
* dead-letters it, and any other error retries it with backoff.
|
|
3027
|
+
*
|
|
3028
|
+
* @example
|
|
3029
|
+
* ```ts
|
|
3030
|
+
* const controller = new AbortController();
|
|
3031
|
+
* await jobs.consume(async (job) => processJob(job), { maxMessages: 10, signal: controller.signal });
|
|
3032
|
+
* ```
|
|
3033
|
+
*/
|
|
3034
|
+
async consume(handler, options = {}) {
|
|
3035
|
+
const wait = options.wait ?? (options.stopWhenEmpty ? 0 : 20);
|
|
3036
|
+
const waitMs = Math.min(durationToSeconds(wait), 20) * 1e3;
|
|
3037
|
+
const pending = [];
|
|
3038
|
+
let timer;
|
|
3039
|
+
const sendAcks = async (batch) => {
|
|
3040
|
+
const resource = this.resource();
|
|
3041
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
3042
|
+
try {
|
|
3043
|
+
const { results } = await resource.ackBatch(
|
|
3044
|
+
this.name,
|
|
3045
|
+
batch.map((message) => ({ messageId: message.messageId, receipt: message.receipt })),
|
|
3046
|
+
this.scope
|
|
3047
|
+
);
|
|
3048
|
+
return batch.map((_message, index) => {
|
|
3049
|
+
const result = results[index];
|
|
3050
|
+
if (result?.acknowledged || attempt > 0 && result?.code === "message_not_found") return void 0;
|
|
3051
|
+
const code = result?.code ?? "no_result";
|
|
3052
|
+
return new ApiError(
|
|
3053
|
+
`Acknowledging message failed: ${code}`,
|
|
3054
|
+
code === "message_not_found" ? 404 : 409,
|
|
3055
|
+
code
|
|
3056
|
+
);
|
|
3057
|
+
});
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
if (error instanceof ApiError && error.status === 404 && error.code !== "queue_not_found") {
|
|
3060
|
+
const settled = await Promise.allSettled(
|
|
3061
|
+
batch.map((message) => resource.ack(this.name, message.messageId, message.receipt, this.scope))
|
|
3062
|
+
);
|
|
3063
|
+
return settled.map(
|
|
3064
|
+
(outcome) => outcome.status === "fulfilled" ? void 0 : outcome.reason instanceof Error ? outcome.reason : new Error(errorText(outcome.reason))
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
const retryable = !(error instanceof ApiError) || error.status >= 500 || error.status === 429;
|
|
3068
|
+
const delay2 = ACK_RETRY_DELAYS_MS[attempt];
|
|
3069
|
+
if (!retryable || delay2 === void 0) throw error;
|
|
3070
|
+
await sleep2(delay2, void 0);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
const flush = () => {
|
|
3075
|
+
clearTimeout(timer);
|
|
3076
|
+
timer = void 0;
|
|
3077
|
+
const batch = pending.splice(0, BATCH_LIMIT);
|
|
3078
|
+
if (pending.length > 0) timer = setTimeout(flush, 0);
|
|
3079
|
+
if (batch.length === 0) return;
|
|
3080
|
+
sendAcks(batch.map(({ message }) => message)).then((errors) => batch.forEach(({ settled }, index) => settled(errors[index]))).catch((error) => batch.forEach(({ settled }) => settled(error)));
|
|
3081
|
+
};
|
|
3082
|
+
const acknowledge = (message) => new Promise((resolve, reject) => {
|
|
3083
|
+
pending.push({
|
|
3084
|
+
message,
|
|
3085
|
+
settled: (error) => {
|
|
3086
|
+
if (error === void 0) resolve();
|
|
3087
|
+
else reject(error instanceof Error ? error : new Error(errorText(error)));
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
if (pending.length >= BATCH_LIMIT) flush();
|
|
3091
|
+
else timer ??= setTimeout(flush, ACK_FLUSH_MS);
|
|
3092
|
+
});
|
|
3093
|
+
while (!options.signal?.aborted) {
|
|
3094
|
+
let messages;
|
|
3095
|
+
const started = Date.now();
|
|
3096
|
+
try {
|
|
3097
|
+
messages = await this.receive({
|
|
3098
|
+
maxMessages: options.maxMessages ?? 10,
|
|
3099
|
+
wait,
|
|
3100
|
+
visibilityTimeout: options.visibilityTimeout,
|
|
3101
|
+
signal: options.signal
|
|
3102
|
+
});
|
|
3103
|
+
} catch (error) {
|
|
3104
|
+
if (options.signal?.aborted) return;
|
|
3105
|
+
throw error;
|
|
3106
|
+
}
|
|
3107
|
+
if (messages.length === 0) {
|
|
3108
|
+
if (options.stopWhenEmpty) return;
|
|
3109
|
+
const remaining = Math.max(waitMs, EMPTY_POLL_MIN_MS) - (Date.now() - started);
|
|
3110
|
+
if (remaining > 0) await sleep2(Math.min(remaining, EMPTY_POLL_MAX_MS), options.signal);
|
|
3111
|
+
continue;
|
|
3112
|
+
}
|
|
3113
|
+
await Promise.all(
|
|
3114
|
+
messages.map(async (message) => {
|
|
3115
|
+
let outcome;
|
|
3116
|
+
try {
|
|
3117
|
+
await handler(message.payload, message);
|
|
3118
|
+
outcome = acknowledge(message);
|
|
3119
|
+
} catch (error) {
|
|
3120
|
+
outcome = error instanceof RetryLaterError ? message.defer(error.delaySeconds) : error instanceof NonRetryableError ? message.deadLetter(error.message) : message.retry({ error: errorText(error) });
|
|
3121
|
+
}
|
|
3122
|
+
try {
|
|
3123
|
+
await outcome;
|
|
3124
|
+
} catch (settleError) {
|
|
3125
|
+
options.onError?.(settleError);
|
|
3126
|
+
}
|
|
3127
|
+
})
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Builds a push consumer: a `(request: Request) => Promise<Response>`
|
|
3133
|
+
* function for the queue's consumer path, such as a Next.js route handler.
|
|
3134
|
+
*
|
|
3135
|
+
* It verifies the delivery signature, decodes the payload and calls
|
|
3136
|
+
* `handler`. Returning acknowledges the message. Throw
|
|
3137
|
+
* {@link RetryLaterError} to defer it without spending an attempt,
|
|
3138
|
+
* {@link NonRetryableError} to dead-letter it, or anything else to retry
|
|
3139
|
+
* with backoff.
|
|
3140
|
+
*
|
|
3141
|
+
* @example
|
|
3142
|
+
* ```ts
|
|
3143
|
+
* // app/api/queues/emails/route.ts
|
|
3144
|
+
* export const POST = emails.handler(async (email, { attempt }) => {
|
|
3145
|
+
* await sendEmail(email);
|
|
3146
|
+
* });
|
|
3147
|
+
* ```
|
|
3148
|
+
*/
|
|
3149
|
+
handler(handler) {
|
|
3150
|
+
return async (request) => {
|
|
3151
|
+
const header = (name) => request.headers.get(name);
|
|
3152
|
+
const messageId = header(QUEUE_DELIVERY_HEADERS.messageId);
|
|
3153
|
+
const queueName = header(QUEUE_DELIVERY_HEADERS.name);
|
|
3154
|
+
const attempt = Number(header(QUEUE_DELIVERY_HEADERS.attempt) ?? "1");
|
|
3155
|
+
if (!messageId || !queueName) {
|
|
3156
|
+
return Response.json({ error: "Not a queue delivery" }, { status: 400 });
|
|
3157
|
+
}
|
|
3158
|
+
if (queueName !== this.name) {
|
|
3159
|
+
console.error(`[gigadrive] Rejected a delivery for queue "${queueName}" at the handler of "${this.name}"`);
|
|
3160
|
+
return Response.json({ error: `This endpoint consumes queue "${this.name}"` }, { status: 400 });
|
|
3161
|
+
}
|
|
3162
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
3163
|
+
if (!this.options.allowUnsigned) {
|
|
3164
|
+
const secret = this.options.signingSecret ?? readSigningSecret();
|
|
3165
|
+
if (!secret) {
|
|
3166
|
+
console.error(
|
|
3167
|
+
`[gigadrive] Rejected a queue delivery: ${QUEUE_SIGNING_SECRET_ENV} is not set. Deployments receive it automatically; set allowUnsigned for local development.`
|
|
3168
|
+
);
|
|
3169
|
+
return Response.json({ error: "Queue signing secret is not configured" }, { status: 500 });
|
|
3170
|
+
}
|
|
3171
|
+
const valid = await verifyQueueSignature(header(QUEUE_SIGNATURE_HEADER), {
|
|
3172
|
+
secret,
|
|
3173
|
+
queue: queueName,
|
|
3174
|
+
messageId,
|
|
3175
|
+
attempt,
|
|
3176
|
+
body
|
|
3177
|
+
});
|
|
3178
|
+
if (!valid) return Response.json({ error: "Invalid queue signature" }, { status: 401 });
|
|
3179
|
+
}
|
|
3180
|
+
const meta = {
|
|
3181
|
+
messageId,
|
|
3182
|
+
queue: queueName,
|
|
3183
|
+
attempt,
|
|
3184
|
+
createdAt: toDate(header(QUEUE_DELIVERY_HEADERS.createdAt)),
|
|
3185
|
+
groupKey: header(QUEUE_DELIVERY_HEADERS.groupKey),
|
|
3186
|
+
deduplicationKey: header(QUEUE_DELIVERY_HEADERS.deduplicationKey),
|
|
3187
|
+
deploymentId: header(QUEUE_DELIVERY_HEADERS.deploymentId),
|
|
3188
|
+
headers: Object.fromEntries(
|
|
3189
|
+
[...request.headers.entries()].filter(([name]) => !name.startsWith("x-gigadrive-queue-"))
|
|
3190
|
+
)
|
|
3191
|
+
};
|
|
3192
|
+
let payload;
|
|
3193
|
+
try {
|
|
3194
|
+
payload = this.decode(body, header("content-type") ?? "application/octet-stream");
|
|
3195
|
+
} catch (error) {
|
|
3196
|
+
return deadLetterResponse(`Undecodable payload: ${errorText(error)}`);
|
|
3197
|
+
}
|
|
3198
|
+
try {
|
|
3199
|
+
await handler(payload, meta);
|
|
3200
|
+
return Response.json({ ok: true });
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
if (error instanceof RetryLaterError) {
|
|
3203
|
+
return Response.json(
|
|
3204
|
+
{ ok: true, retryAfter: error.delaySeconds },
|
|
3205
|
+
{ headers: { [QUEUE_RESPONSE_HEADERS.retryAfter]: String(error.delaySeconds) } }
|
|
3206
|
+
);
|
|
3207
|
+
}
|
|
3208
|
+
if (error instanceof NonRetryableError) return deadLetterResponse(error.message);
|
|
3209
|
+
console.error(`[gigadrive] Queue handler for "${queueName}" failed (attempt ${String(attempt)})`, error);
|
|
3210
|
+
return Response.json({ error: errorText(error) }, { status: 500 });
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
/** Creates the queue, or applies `settings` to it. Use it to make a push queue from code. */
|
|
3215
|
+
async ensure(settings = {}) {
|
|
3216
|
+
return this.resource().ensure(this.name, settings, this.scope);
|
|
3217
|
+
}
|
|
3218
|
+
/** The queue's settings and live depth. */
|
|
3219
|
+
async info() {
|
|
3220
|
+
return this.resource().get(this.name, this.scope);
|
|
3221
|
+
}
|
|
3222
|
+
/** Stops deliveries until {@link resume}. Sends are still accepted. */
|
|
3223
|
+
async pause() {
|
|
3224
|
+
await this.resource().pause(this.name, this.scope);
|
|
3225
|
+
}
|
|
3226
|
+
async resume() {
|
|
3227
|
+
await this.resource().resume(this.name, this.scope);
|
|
3228
|
+
}
|
|
3229
|
+
/** Deletes every message, or only the dead letters. */
|
|
3230
|
+
async purge(scope = "all") {
|
|
3231
|
+
return (await this.resource().purge(this.name, scope, this.scope)).purgedMessages;
|
|
3232
|
+
}
|
|
3233
|
+
/** Moves dead letters back for another round of attempts. */
|
|
3234
|
+
async redrive(messageIds) {
|
|
3235
|
+
return (await this.resource().redrive(this.name, messageIds, this.scope)).redrivenMessages;
|
|
3236
|
+
}
|
|
3237
|
+
/**
|
|
3238
|
+
* Creates or updates a cron schedule that sends `payload` into this queue.
|
|
3239
|
+
*
|
|
3240
|
+
* @example
|
|
3241
|
+
* ```ts
|
|
3242
|
+
* await digests.schedule('weekday-digest', { cron: '0 8 * * MON-FRI', timezone: 'Europe/Berlin', payload: { kind: 'digest' } });
|
|
3243
|
+
* ```
|
|
3244
|
+
*/
|
|
3245
|
+
async schedule(name, schedule) {
|
|
3246
|
+
const encoded = schedule.payload === void 0 ? void 0 : this.codec.encode(schedule.payload);
|
|
3247
|
+
if (encoded?.encoding === "base64") throw new ConfigurationError("Scheduled payloads must be text or JSON");
|
|
3248
|
+
const input = {
|
|
3249
|
+
cron: schedule.cron,
|
|
3250
|
+
...schedule.timezone ? { timezone: schedule.timezone } : {},
|
|
3251
|
+
...encoded ? { body: encoded.body, contentType: encoded.contentType } : {},
|
|
3252
|
+
...schedule.headers ? { headers: schedule.headers } : {},
|
|
3253
|
+
...schedule.enabled === void 0 ? {} : { enabled: schedule.enabled }
|
|
3254
|
+
};
|
|
3255
|
+
try {
|
|
3256
|
+
return await this.resource().createSchedule(this.name, name, input, this.scope);
|
|
3257
|
+
} catch (error) {
|
|
3258
|
+
if (error instanceof ApiError && error.code === "schedule_exists") {
|
|
3259
|
+
return this.resource().updateSchedule(this.name, name, input, this.scope);
|
|
3260
|
+
}
|
|
3261
|
+
throw error;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
/** Deletes a schedule. */
|
|
3265
|
+
async unschedule(name) {
|
|
3266
|
+
await this.resource().deleteSchedule(this.name, name, this.scope);
|
|
3267
|
+
}
|
|
3268
|
+
};
|
|
3269
|
+
var deadLetterResponse = (reason) => Response.json(
|
|
3270
|
+
{ error: reason },
|
|
3271
|
+
{
|
|
3272
|
+
status: 422,
|
|
3273
|
+
headers: { [QUEUE_RESPONSE_HEADERS.action]: "dead-letter", [QUEUE_RESPONSE_HEADERS.error]: headerText(reason) }
|
|
3274
|
+
}
|
|
3275
|
+
);
|
|
3276
|
+
var readSigningSecret = () => {
|
|
3277
|
+
const env = globalThis.process?.env;
|
|
3278
|
+
return env?.[QUEUE_SIGNING_SECRET_ENV] || void 0;
|
|
3279
|
+
};
|
|
3280
|
+
|
|
3281
|
+
// src/resources/queues.ts
|
|
3282
|
+
var QueuesResource = class extends BaseResource {
|
|
3283
|
+
constructor(httpClient, defaultApplicationId) {
|
|
3284
|
+
super(httpClient);
|
|
3285
|
+
this.defaultApplicationId = defaultApplicationId;
|
|
3286
|
+
}
|
|
3287
|
+
defaultApplicationId;
|
|
3288
|
+
/**
|
|
3289
|
+
* Returns a typed handle for one queue. Nothing is sent until you call a
|
|
3290
|
+
* method on it, and the queue does not need to exist: the first send
|
|
3291
|
+
* creates a pull queue with default settings.
|
|
3292
|
+
*
|
|
3293
|
+
* @typeParam T - The payload type sent and received through this queue.
|
|
3294
|
+
* @param name - Queue name, unique in the environment and case-sensitive.
|
|
3295
|
+
* @param options - Scope, codec and default send options.
|
|
3296
|
+
*/
|
|
3297
|
+
queue(name, options = {}) {
|
|
3298
|
+
return new Queue(name, () => this, options);
|
|
3299
|
+
}
|
|
3300
|
+
base(options) {
|
|
3301
|
+
return `/applications/${encodeURIComponent(
|
|
3302
|
+
resolveStorageApplicationId(this.defaultApplicationId, options?.applicationId)
|
|
3303
|
+
)}/queues`;
|
|
3304
|
+
}
|
|
3305
|
+
path(name, options, suffix = "") {
|
|
3306
|
+
return `${this.base(options)}/${encodeURIComponent(name)}${suffix}`;
|
|
3307
|
+
}
|
|
3308
|
+
/** Lists the environment's queues with their live depth. */
|
|
3309
|
+
async list(options) {
|
|
3310
|
+
return this.httpClient.get(this.base(options), { query: { environment: options?.environment } });
|
|
3311
|
+
}
|
|
3312
|
+
/** Fetches one queue with its live depth. */
|
|
3313
|
+
async get(name, options) {
|
|
3314
|
+
return this.httpClient.get(this.path(name, options), { query: { environment: options?.environment } });
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
3317
|
+
* Creates a queue. Fails with `queue_exists` when the name is taken; use
|
|
3318
|
+
* {@link ensure} for create-or-update.
|
|
3319
|
+
*/
|
|
3320
|
+
async create(name, settings = {}, options) {
|
|
3321
|
+
return this.httpClient.post(this.base(options), { name, environment: options?.environment, ...settings });
|
|
3322
|
+
}
|
|
3323
|
+
/** Creates the queue, or applies `settings` to the existing one. */
|
|
3324
|
+
async ensure(name, settings = {}, options) {
|
|
3325
|
+
return this.httpClient.put(this.path(name, options), { environment: options?.environment, ...settings });
|
|
3326
|
+
}
|
|
3327
|
+
/** Changes a queue's settings. Messages already queued keep flowing. */
|
|
3328
|
+
async update(name, settings, options) {
|
|
3329
|
+
return this.httpClient.patch(this.path(name, options), { environment: options?.environment, ...settings });
|
|
3330
|
+
}
|
|
3331
|
+
/** Deletes a queue, its messages and its schedules. */
|
|
3332
|
+
async delete(name, options) {
|
|
3333
|
+
return this.httpClient.delete(this.path(name, options), { query: { environment: options?.environment } });
|
|
3334
|
+
}
|
|
3335
|
+
/** Stops deliveries. Sends are still accepted and wait until you resume. */
|
|
3336
|
+
async pause(name, options) {
|
|
3337
|
+
return this.httpClient.post(this.path(name, options, "/pause"), { environment: options?.environment });
|
|
3338
|
+
}
|
|
3339
|
+
/** Restarts deliveries after {@link pause}. */
|
|
3340
|
+
async resume(name, options) {
|
|
3341
|
+
return this.httpClient.post(this.path(name, options, "/resume"), { environment: options?.environment });
|
|
3342
|
+
}
|
|
3343
|
+
/** Deletes every message, or only the dead letters. */
|
|
3344
|
+
async purge(name, scope = "all", options) {
|
|
3345
|
+
return this.httpClient.post(this.path(name, options, "/purge"), { environment: options?.environment, scope });
|
|
3346
|
+
}
|
|
3347
|
+
/** Moves dead letters back to the queue with fresh attempts: all of them, or the given ids. */
|
|
3348
|
+
async redrive(name, messageIds, options) {
|
|
3349
|
+
return this.httpClient.post(this.path(name, options, "/redrive"), {
|
|
3350
|
+
environment: options?.environment,
|
|
3351
|
+
...messageIds ? { messageIds } : {}
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Sends one message.
|
|
3356
|
+
*
|
|
3357
|
+
* @param autoCreate - Create a pull queue with default settings when the name is new. Default `true`.
|
|
3358
|
+
*/
|
|
3359
|
+
async send(name, message, options) {
|
|
3360
|
+
return this.httpClient.post(this.path(name, options, "/messages"), {
|
|
3361
|
+
environment: options?.environment,
|
|
3362
|
+
autoCreate: options?.autoCreate,
|
|
3363
|
+
...message
|
|
3364
|
+
});
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Sends up to 100 messages in one request. Results come back in input
|
|
3368
|
+
* order; one rejected message does not fail the others.
|
|
3369
|
+
*/
|
|
3370
|
+
async sendBatch(name, messages, options) {
|
|
3371
|
+
const { results } = await this.httpClient.post(
|
|
3372
|
+
this.path(name, options, "/messages"),
|
|
3373
|
+
{ environment: options?.environment, autoCreate: options?.autoCreate, messages }
|
|
3374
|
+
);
|
|
3375
|
+
return results;
|
|
3376
|
+
}
|
|
3377
|
+
/** Lists messages in one state without leasing them. Bodies are cut to a preview. */
|
|
3378
|
+
async listMessages(name, query = {}, options) {
|
|
3379
|
+
return this.httpClient.get(this.path(name, options, "/messages"), {
|
|
3380
|
+
query: { environment: options?.environment, ...query }
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
/** Fetches one message without leasing it. */
|
|
3384
|
+
async getMessage(name, messageId, options) {
|
|
3385
|
+
return this.httpClient.get(this.path(name, options, `/messages/${encodeURIComponent(messageId)}`), {
|
|
3386
|
+
query: { environment: options?.environment }
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
/**
|
|
3390
|
+
* Cancels a message that has not been delivered yet, such as a scheduled
|
|
3391
|
+
* send you no longer want. A message being delivered or dead-lettered
|
|
3392
|
+
* answers 400 `invalid_request`; settle it from its consumer, or purge or
|
|
3393
|
+
* redrive dead letters.
|
|
3394
|
+
*/
|
|
3395
|
+
async deleteMessage(name, messageId, options) {
|
|
3396
|
+
return this.httpClient.delete(this.path(name, options, `/messages/${encodeURIComponent(messageId)}`), {
|
|
3397
|
+
query: { environment: options?.environment }
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Leases up to `maxMessages` messages from a pull queue, waiting up to
|
|
3402
|
+
* `waitSeconds` (at most 20) for the first one. Each must be acknowledged
|
|
3403
|
+
* with its receipt before the lease ends, or it is delivered again.
|
|
3404
|
+
*/
|
|
3405
|
+
async receive(name, request = {}, options) {
|
|
3406
|
+
const { messages } = await this.httpClient.post(
|
|
3407
|
+
this.path(name, options, "/messages/receive"),
|
|
3408
|
+
{ environment: options?.environment, ...request },
|
|
3409
|
+
{ signal: options?.signal }
|
|
3410
|
+
);
|
|
3411
|
+
return messages;
|
|
3412
|
+
}
|
|
3413
|
+
/** Acknowledges a received message, deleting it. */
|
|
3414
|
+
async ack(name, messageId, receipt, options) {
|
|
3415
|
+
return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/ack`), {
|
|
3416
|
+
environment: options?.environment,
|
|
3417
|
+
receipt
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Acknowledges up to 100 received messages in one request. Each settles on
|
|
3422
|
+
* its own: `results` reports, in order, which were acknowledged and why any
|
|
3423
|
+
* were not (`message_not_found`, `receipt_mismatch`). A batch counts one
|
|
3424
|
+
* operation per started ten acknowledged messages.
|
|
3425
|
+
*/
|
|
3426
|
+
async ackBatch(name, messages, options) {
|
|
3427
|
+
return this.httpClient.post(this.path(name, options, "/messages/ack"), {
|
|
3428
|
+
environment: options?.environment,
|
|
3429
|
+
messages
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Releases a received message: retry with backoff (or after `delaySeconds`),
|
|
3434
|
+
* or dead-letter it now with `deadLetter: true`. `countAttempt: false`
|
|
3435
|
+
* reschedules without spending an attempt.
|
|
3436
|
+
*/
|
|
3437
|
+
async nack(name, messageId, request, options) {
|
|
3438
|
+
return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/nack`), {
|
|
3439
|
+
environment: options?.environment,
|
|
3440
|
+
...request
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
/** Extends a received message's lease to `visibilityTimeoutSeconds` from now. */
|
|
3444
|
+
async extend(name, messageId, receipt, visibilityTimeoutSeconds, options) {
|
|
3445
|
+
return this.httpClient.post(this.path(name, options, `/messages/${encodeURIComponent(messageId)}/extend`), {
|
|
3446
|
+
environment: options?.environment,
|
|
3447
|
+
receipt,
|
|
3448
|
+
visibilityTimeoutSeconds
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
/** Lists a queue's schedules. */
|
|
3452
|
+
async listSchedules(name, options) {
|
|
3453
|
+
return this.httpClient.get(this.path(name, options, "/schedules"), {
|
|
3454
|
+
query: { environment: options?.environment }
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Creates a schedule that sends a message into the queue on a cron.
|
|
3459
|
+
*
|
|
3460
|
+
* @example
|
|
3461
|
+
* ```ts
|
|
3462
|
+
* await client.queues.createSchedule('emails', 'daily-digest', {
|
|
3463
|
+
* cron: '0 8 * * MON-FRI',
|
|
3464
|
+
* timezone: 'Europe/Berlin',
|
|
3465
|
+
* json: { kind: 'digest' },
|
|
3466
|
+
* });
|
|
3467
|
+
* ```
|
|
3468
|
+
*/
|
|
3469
|
+
async createSchedule(name, schedule, input, options) {
|
|
3470
|
+
return this.httpClient.post(this.path(name, options, "/schedules"), {
|
|
3471
|
+
environment: options?.environment,
|
|
3472
|
+
name: schedule,
|
|
3473
|
+
...input
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
/** Changes a schedule. Its next run is recomputed when the cron or time zone changes. */
|
|
3477
|
+
async updateSchedule(name, schedule, input, options) {
|
|
3478
|
+
return this.httpClient.patch(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}`), {
|
|
3479
|
+
environment: options?.environment,
|
|
3480
|
+
...input
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
/** Deletes a schedule. Messages it already sent stay queued. */
|
|
3484
|
+
async deleteSchedule(name, schedule, options) {
|
|
3485
|
+
return this.httpClient.delete(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}`), {
|
|
3486
|
+
query: { environment: options?.environment }
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
/** Sends the schedule's message now, outside its cron. */
|
|
3490
|
+
async triggerSchedule(name, schedule, options) {
|
|
3491
|
+
return this.httpClient.post(this.path(name, options, `/schedules/${encodeURIComponent(schedule)}/trigger`), {
|
|
3492
|
+
environment: options?.environment
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3496
|
+
|
|
2430
3497
|
// src/resources/sticky-sessions.ts
|
|
2431
3498
|
var StickySessionsResource = class extends BaseResource {
|
|
2432
3499
|
/**
|
|
@@ -2470,6 +3537,12 @@ var GigadriveClient = class {
|
|
|
2470
3537
|
stickySessions;
|
|
2471
3538
|
/** Managed image optimization inspection and cache purge operations. */
|
|
2472
3539
|
imageOptimization;
|
|
3540
|
+
/**
|
|
3541
|
+
* Network Queues: manage queues and schedules, send, receive and
|
|
3542
|
+
* acknowledge messages. `client.queues.queue<T>(name)` returns a typed
|
|
3543
|
+
* handle bound to one queue.
|
|
3544
|
+
*/
|
|
3545
|
+
queues;
|
|
2473
3546
|
constructor(config = {}) {
|
|
2474
3547
|
const baseUrl = config.baseUrl ?? readEnv("GIGADRIVE_API_BASE_URL") ?? DEFAULT_BASE_URL2;
|
|
2475
3548
|
const applicationId = config.applicationId ?? readEnv("GIGADRIVE_APPLICATION_ID");
|
|
@@ -2485,6 +3558,7 @@ var GigadriveClient = class {
|
|
|
2485
3558
|
this.apiKeys = new ApiKeysResource(httpClient);
|
|
2486
3559
|
this.stickySessions = new StickySessionsResource(httpClient);
|
|
2487
3560
|
this.imageOptimization = new ImageOptimizationResource(httpClient);
|
|
3561
|
+
this.queues = new QueuesResource(httpClient, applicationId);
|
|
2488
3562
|
}
|
|
2489
3563
|
};
|
|
2490
3564
|
|
|
@@ -2499,6 +3573,161 @@ async function* paginate(fetchPage) {
|
|
|
2499
3573
|
cursor = page.nextCursor;
|
|
2500
3574
|
} while (cursor);
|
|
2501
3575
|
}
|
|
3576
|
+
|
|
3577
|
+
// src/queues/default-client.ts
|
|
3578
|
+
var defaultClient;
|
|
3579
|
+
var defaultQueuesResource = () => {
|
|
3580
|
+
defaultClient ??= new GigadriveClient();
|
|
3581
|
+
return defaultClient.queues;
|
|
3582
|
+
};
|
|
3583
|
+
function queue(name, options = {}) {
|
|
3584
|
+
const { client, ...queueOptions } = options;
|
|
3585
|
+
return new Queue(name, () => client?.queues ?? defaultQueuesResource(), queueOptions);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// src/queues/workflow.ts
|
|
3589
|
+
var WORKFLOW_CONSUMER_PATH = "/.well-known/workflow/v1/flow";
|
|
3590
|
+
var WORKFLOW_QUEUE_NAME_HEADER = "x-workflow-queue-name";
|
|
3591
|
+
var WORKFLOW_PREFIX = /^__(?:[a-z][a-z0-9]*_)?wkf_workflow_/;
|
|
3592
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3593
|
+
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;
|
|
3594
|
+
var encodeWorkflowMessage = (message) => JSON.stringify(
|
|
3595
|
+
message,
|
|
3596
|
+
(_key, value) => value instanceof Uint8Array ? { __type: "Uint8Array", data: bytesToBase64(value) } : value
|
|
3597
|
+
);
|
|
3598
|
+
var decodeWorkflowMessage = (text) => JSON.parse(text, (_key, value) => {
|
|
3599
|
+
if (typeof value === "object" && value !== null && value.__type === "Uint8Array" && typeof value.data === "string") {
|
|
3600
|
+
return base64ToBytes(value.data);
|
|
3601
|
+
}
|
|
3602
|
+
return value;
|
|
3603
|
+
});
|
|
3604
|
+
var readEnv2 = (name) => globalThis.process?.env?.[name] || void 0;
|
|
3605
|
+
var deduplicationKeyFor = async (idempotencyKey) => idempotencyKey.length <= 256 ? idempotencyKey : `wkf:sha256:${toHex2(await crypto.subtle.digest("SHA-256", toArrayBuffer(utf8Encode(idempotencyKey))))}`;
|
|
3606
|
+
function createWorkflowQueue(options = {}) {
|
|
3607
|
+
const resource = () => options.client?.queues ?? defaultQueuesResource();
|
|
3608
|
+
const scope = { applicationId: options.applicationId, environment: options.environment };
|
|
3609
|
+
const ensured = /* @__PURE__ */ new Map();
|
|
3610
|
+
const physicalQueue = async (queueName) => {
|
|
3611
|
+
const prefix = WORKFLOW_PREFIX.exec(queueName)?.[0];
|
|
3612
|
+
if (!prefix) throw new Error(`"${queueName}" is not a Workflow SDK queue name`);
|
|
3613
|
+
let ready = ensured.get(prefix);
|
|
3614
|
+
if (!ready) {
|
|
3615
|
+
const consumerPath = options.consumerPath ?? WORKFLOW_CONSUMER_PATH;
|
|
3616
|
+
ready = options.queueSettings ? resource().ensure(prefix, { ...options.queueSettings, consumerPath }, scope) : (
|
|
3617
|
+
// Create-only: a queue that exists keeps the settings someone gave it in the console.
|
|
3618
|
+
resource().create(prefix, { maxAttempts: 100, visibilityTimeoutSeconds: 900, deadLetter: true, consumerPath }, scope).catch((error) => {
|
|
3619
|
+
if (error instanceof ApiError && error.code === "queue_exists") return void 0;
|
|
3620
|
+
throw error;
|
|
3621
|
+
})
|
|
3622
|
+
);
|
|
3623
|
+
ensured.set(prefix, ready);
|
|
3624
|
+
ready.catch(() => ensured.delete(prefix));
|
|
3625
|
+
}
|
|
3626
|
+
await ready;
|
|
3627
|
+
return prefix;
|
|
3628
|
+
};
|
|
3629
|
+
const toMessage = async (queueName, message, opts = {}) => {
|
|
3630
|
+
const headers = Object.fromEntries(
|
|
3631
|
+
Object.entries(opts.headers ?? {}).filter(([name]) => !RESERVED_HEADER.test(name))
|
|
3632
|
+
);
|
|
3633
|
+
headers[WORKFLOW_QUEUE_NAME_HEADER] = queueName;
|
|
3634
|
+
const pin = opts.deploymentId ?? options.deploymentId ?? readEnv2("GIGADRIVE_DEPLOYMENT_ID");
|
|
3635
|
+
return {
|
|
3636
|
+
body: encodeWorkflowMessage(message),
|
|
3637
|
+
contentType: "application/json",
|
|
3638
|
+
headers,
|
|
3639
|
+
...opts.delaySeconds && opts.delaySeconds > 0 ? { delaySeconds: Math.ceil(opts.delaySeconds) } : {},
|
|
3640
|
+
...opts.idempotencyKey ? { deduplicationKey: await deduplicationKeyFor(opts.idempotencyKey) } : {},
|
|
3641
|
+
...pin && UUID.test(pin) ? { deploymentId: pin } : {}
|
|
3642
|
+
};
|
|
3643
|
+
};
|
|
3644
|
+
return {
|
|
3645
|
+
getDeploymentId: () => Promise.resolve(options.deploymentId ?? readEnv2("GIGADRIVE_DEPLOYMENT_ID") ?? "local"),
|
|
3646
|
+
async queue(queueName, message, opts) {
|
|
3647
|
+
const physical = await physicalQueue(queueName);
|
|
3648
|
+
const result = await resource().send(physical, await toMessage(queueName, message, opts), {
|
|
3649
|
+
...scope,
|
|
3650
|
+
autoCreate: false
|
|
3651
|
+
});
|
|
3652
|
+
if (result.messageId === null) throw new Error(`Workflow message was refused: ${result.error}`);
|
|
3653
|
+
return { messageId: result.messageId };
|
|
3654
|
+
},
|
|
3655
|
+
async queueBatch(queueName, messages) {
|
|
3656
|
+
const physical = await physicalQueue(queueName);
|
|
3657
|
+
const results = [];
|
|
3658
|
+
for (let index = 0; index < messages.length; index += 100) {
|
|
3659
|
+
const chunk = messages.slice(index, index + 100);
|
|
3660
|
+
const sent = await resource().sendBatch(
|
|
3661
|
+
physical,
|
|
3662
|
+
await Promise.all(chunk.map(({ message, opts }) => toMessage(queueName, message, opts))),
|
|
3663
|
+
{ ...scope, autoCreate: false }
|
|
3664
|
+
);
|
|
3665
|
+
for (const result of sent) {
|
|
3666
|
+
results.push(
|
|
3667
|
+
result.messageId === null ? { messageId: null, error: result.error, retryable: result.retryable } : { messageId: result.messageId }
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
return results;
|
|
3672
|
+
},
|
|
3673
|
+
createQueueHandler(queueNamePrefix, handler) {
|
|
3674
|
+
return async (request) => {
|
|
3675
|
+
const messageId = request.headers.get(QUEUE_DELIVERY_HEADERS.messageId);
|
|
3676
|
+
const physicalName = request.headers.get(QUEUE_DELIVERY_HEADERS.name);
|
|
3677
|
+
const attempt = Number(request.headers.get(QUEUE_DELIVERY_HEADERS.attempt) ?? "1");
|
|
3678
|
+
const queueName = request.headers.get(WORKFLOW_QUEUE_NAME_HEADER);
|
|
3679
|
+
if (!messageId || !physicalName || !queueName) {
|
|
3680
|
+
return Response.json({ error: "Missing required headers" }, { status: 400 });
|
|
3681
|
+
}
|
|
3682
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
3683
|
+
if (!options.allowUnsigned) {
|
|
3684
|
+
const secret = options.signingSecret ?? readSigningSecret();
|
|
3685
|
+
const valid = secret !== void 0 && await verifyQueueSignature(request.headers.get(QUEUE_SIGNATURE_HEADER), {
|
|
3686
|
+
secret,
|
|
3687
|
+
queue: physicalName,
|
|
3688
|
+
messageId,
|
|
3689
|
+
attempt,
|
|
3690
|
+
body
|
|
3691
|
+
});
|
|
3692
|
+
if (!valid) return Response.json({ error: "Invalid queue signature" }, { status: 401 });
|
|
3693
|
+
}
|
|
3694
|
+
if (physicalName !== queueNamePrefix || !queueName.startsWith(queueNamePrefix)) {
|
|
3695
|
+
return Response.json({ error: "Unhandled queue" }, { status: 400 });
|
|
3696
|
+
}
|
|
3697
|
+
let message;
|
|
3698
|
+
try {
|
|
3699
|
+
message = decodeWorkflowMessage(utf8Decode(body));
|
|
3700
|
+
} catch {
|
|
3701
|
+
return Response.json(
|
|
3702
|
+
{ error: "Malformed workflow message" },
|
|
3703
|
+
{ status: 422, headers: { [QUEUE_RESPONSE_HEADERS.action]: "dead-letter" } }
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
try {
|
|
3707
|
+
const result = await handler(message, {
|
|
3708
|
+
attempt,
|
|
3709
|
+
queueName,
|
|
3710
|
+
messageId,
|
|
3711
|
+
requestId: request.headers.get("x-request-id") ?? void 0
|
|
3712
|
+
});
|
|
3713
|
+
if (typeof message === "object" && message !== null && message.invoke === true) {
|
|
3714
|
+
return Response.json({ result });
|
|
3715
|
+
}
|
|
3716
|
+
const timeoutSeconds = typeof result === "object" && result !== null ? result.timeoutSeconds : void 0;
|
|
3717
|
+
if (typeof timeoutSeconds === "number" && Number.isFinite(timeoutSeconds) && timeoutSeconds >= 0) {
|
|
3718
|
+
return Response.json(
|
|
3719
|
+
{ timeoutSeconds },
|
|
3720
|
+
{ headers: { [QUEUE_RESPONSE_HEADERS.retryAfter]: String(Math.ceil(timeoutSeconds)) } }
|
|
3721
|
+
);
|
|
3722
|
+
}
|
|
3723
|
+
return Response.json({ ok: true });
|
|
3724
|
+
} catch (error) {
|
|
3725
|
+
return Response.json(String(error), { status: 500 });
|
|
3726
|
+
}
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
2502
3731
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2503
3732
|
0 && (module.exports = {
|
|
2504
3733
|
AiGatewayAudioResource,
|
|
@@ -2509,6 +3738,7 @@ async function* paginate(fetchPage) {
|
|
|
2509
3738
|
AiGatewayVideosResource,
|
|
2510
3739
|
ApiError,
|
|
2511
3740
|
ApiKeysResource,
|
|
3741
|
+
ApplicationDomainsResource,
|
|
2512
3742
|
ApplicationEnvVarsResource,
|
|
2513
3743
|
ApplicationRequestsResource,
|
|
2514
3744
|
ApplicationStorageResource,
|
|
@@ -2516,14 +3746,25 @@ async function* paginate(fetchPage) {
|
|
|
2516
3746
|
AuthenticationError,
|
|
2517
3747
|
ConfigurationError,
|
|
2518
3748
|
DeploymentsResource,
|
|
3749
|
+
DomainNotActiveError,
|
|
2519
3750
|
GigadriveClient,
|
|
2520
3751
|
GigadriveError,
|
|
2521
3752
|
ImageOptimizationResource,
|
|
3753
|
+
NonRetryableError,
|
|
2522
3754
|
OrganizationAiGatewayResource,
|
|
3755
|
+
OrganizationDomainsResource,
|
|
2523
3756
|
OrganizationEnvVarsResource,
|
|
2524
3757
|
OrganizationMembersResource,
|
|
2525
3758
|
OrganizationProductsResource,
|
|
2526
3759
|
OrganizationsResource,
|
|
3760
|
+
QUEUE_DELIVERY_HEADERS,
|
|
3761
|
+
QUEUE_RESPONSE_HEADERS,
|
|
3762
|
+
QUEUE_SIGNATURE_HEADER,
|
|
3763
|
+
QUEUE_SIGNATURE_TOLERANCE_MS,
|
|
3764
|
+
QUEUE_SIGNING_SECRET_ENV,
|
|
3765
|
+
Queue,
|
|
3766
|
+
QueuesResource,
|
|
3767
|
+
RetryLaterError,
|
|
2527
3768
|
StickySessionsResource,
|
|
2528
3769
|
StorageBucketsResource,
|
|
2529
3770
|
StorageObjectsResource,
|
|
@@ -2531,8 +3772,16 @@ async function* paginate(fetchPage) {
|
|
|
2531
3772
|
StorageUploadSessionsResource,
|
|
2532
3773
|
UploadError,
|
|
2533
3774
|
UploadSessionExpiredError,
|
|
3775
|
+
WORKFLOW_CONSUMER_PATH,
|
|
3776
|
+
WORKFLOW_QUEUE_NAME_HEADER,
|
|
2534
3777
|
createManagedImageUrl,
|
|
3778
|
+
createWorkflowQueue,
|
|
3779
|
+
defaultQueueCodec,
|
|
3780
|
+
durationToSeconds,
|
|
2535
3781
|
paginate,
|
|
2536
|
-
parseSSEStream
|
|
3782
|
+
parseSSEStream,
|
|
3783
|
+
queue,
|
|
3784
|
+
signQueueDelivery,
|
|
3785
|
+
verifyQueueSignature
|
|
2537
3786
|
});
|
|
2538
3787
|
//# sourceMappingURL=index.js.map
|