@dotpress/events 0.0.0-stage → 2.0.0-alpha.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/LICENSE +21 -0
- package/README.md +707 -2
- package/dist/aws-sqs.d.ts +127 -0
- package/dist/aws-sqs.js +440 -0
- package/dist/aws-sqs.js.map +1 -0
- package/dist/azure-service-bus.d.ts +99 -0
- package/dist/azure-service-bus.js +320 -0
- package/dist/azure-service-bus.js.map +1 -0
- package/dist/bullmq.d.ts +92 -0
- package/dist/bullmq.js +169 -0
- package/dist/bullmq.js.map +1 -0
- package/dist/bus-Bei_kN-e.d.ts +137 -0
- package/dist/chunk-E4NMMZMY.js +24 -0
- package/dist/chunk-E4NMMZMY.js.map +1 -0
- package/dist/chunk-NCQ4ZTNB.js +50 -0
- package/dist/chunk-NCQ4ZTNB.js.map +1 -0
- package/dist/cli.js +202 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloudflare-queues.d.ts +120 -0
- package/dist/cloudflare-queues.js +333 -0
- package/dist/cloudflare-queues.js.map +1 -0
- package/dist/google-pubsub.d.ts +111 -0
- package/dist/google-pubsub.js +420 -0
- package/dist/google-pubsub.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +701 -0
- package/dist/index.js.map +1 -0
- package/dist/pg-boss.d.ts +79 -0
- package/dist/pg-boss.js +189 -0
- package/dist/pg-boss.js.map +1 -0
- package/dist/plugin.d.ts +40 -0
- package/dist/plugin.js +27 -0
- package/dist/plugin.js.map +1 -0
- package/dist/transport-BtNR1ucF.d.ts +273 -0
- package/package.json +139 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Source, Worker, Transport } from './index.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
/** Producer binding of a queue (`env.EVENTS_QUEUE` in a Worker). */
|
|
5
|
+
interface CloudflareQueueBinding {
|
|
6
|
+
sendBatch(messages: Iterable<{
|
|
7
|
+
body: unknown;
|
|
8
|
+
contentType?: 'json';
|
|
9
|
+
delaySeconds?: number;
|
|
10
|
+
}>): Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** A message received by the `queue()` handler of a Worker. */
|
|
13
|
+
interface CloudflareQueueMessage {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly body: unknown;
|
|
16
|
+
/** Attempt number, from 1. */
|
|
17
|
+
readonly attempts: number;
|
|
18
|
+
ack(): void;
|
|
19
|
+
retry(options?: {
|
|
20
|
+
delaySeconds?: number;
|
|
21
|
+
}): void;
|
|
22
|
+
}
|
|
23
|
+
/** The batch received by the `queue()` handler of a Worker. */
|
|
24
|
+
interface CloudflareMessageBatch {
|
|
25
|
+
readonly queue: string;
|
|
26
|
+
readonly messages: readonly CloudflareQueueMessage[];
|
|
27
|
+
}
|
|
28
|
+
/** REST API access, to use a queue from outside Cloudflare Workers. */
|
|
29
|
+
interface CloudflareApiOptions {
|
|
30
|
+
accountId: string;
|
|
31
|
+
/** Queue ID (not its name). */
|
|
32
|
+
queueId: string;
|
|
33
|
+
/** API token with Queues permissions (read and write to pull). */
|
|
34
|
+
apiToken: string;
|
|
35
|
+
/** Base URL of the API. Defaults to `https://api.cloudflare.com/client/v4`. */
|
|
36
|
+
baseUrl?: string | undefined;
|
|
37
|
+
/** `fetch` implementation. Defaults to the global `fetch`. */
|
|
38
|
+
fetch?: typeof fetch | undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Options of `createCloudflareQueueTransport()`. */
|
|
41
|
+
type CloudflareQueueTransportOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* Producer binding of the queue, or a function returning it (when the
|
|
44
|
+
* bus is created before the `env` of the Worker is available).
|
|
45
|
+
*/
|
|
46
|
+
queue: CloudflareQueueBinding | (() => CloudflareQueueBinding);
|
|
47
|
+
} | CloudflareApiOptions;
|
|
48
|
+
/** Options of `createCloudflareQueueHandler()`. */
|
|
49
|
+
interface CloudflareQueueHandlerOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Producer binding of the dead-letter queue: messages that can never be
|
|
52
|
+
* handled are sent there at once, instead of failing until `max_retries`.
|
|
53
|
+
*/
|
|
54
|
+
deadLetterQueue?: CloudflareQueueBinding | (() => CloudflareQueueBinding) | undefined;
|
|
55
|
+
/** Delay before a retry, in seconds, given the attempt number. */
|
|
56
|
+
retryDelay?: ((attempt: number) => number) | undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Options of `createCloudflarePullSource()`. */
|
|
59
|
+
interface CloudflarePullSourceOptions extends CloudflareApiOptions {
|
|
60
|
+
/** Messages per pull (1 to 100). Defaults to 10. */
|
|
61
|
+
batchSize?: number | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Lease of the pulled messages, in milliseconds (up to 12 hours): longer
|
|
64
|
+
* than your slowest listener, since it cannot be extended. Defaults to
|
|
65
|
+
* 30 seconds.
|
|
66
|
+
*/
|
|
67
|
+
visibilityTimeoutMs?: number | undefined;
|
|
68
|
+
/** Wait before pulling again an empty queue, in milliseconds. Defaults to 1 second. */
|
|
69
|
+
pollInterval?: number | undefined;
|
|
70
|
+
/** Delay before a retry, in seconds, given the attempt number. */
|
|
71
|
+
retryDelay?: ((attempt: number) => number) | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Queue ID of the dead-letter queue: messages that can never be handled
|
|
74
|
+
* are pushed there at once, instead of failing until `max_retries`.
|
|
75
|
+
*/
|
|
76
|
+
deadLetterQueueId?: string | undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Creates a transport that sends messages to a Cloudflare Queue: through
|
|
80
|
+
* its producer binding inside a Worker, or through the REST API from
|
|
81
|
+
* anywhere else (Node.js, Lambda…).
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* // In a Worker
|
|
85
|
+
* import { env } from 'cloudflare:workers'
|
|
86
|
+
* const transport = createCloudflareQueueTransport({ queue: () => env.EVENTS_QUEUE })
|
|
87
|
+
*
|
|
88
|
+
* // Elsewhere
|
|
89
|
+
* const transport = createCloudflareQueueTransport({ accountId, queueId, apiToken })
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare const createCloudflareQueueTransport: (options: CloudflareQueueTransportOptions) => Transport;
|
|
93
|
+
/**
|
|
94
|
+
* Creates the `queue()` handler of a Worker consuming the queue. Each
|
|
95
|
+
* message is acknowledged or retried on its own: a failure does not retry
|
|
96
|
+
* the whole batch. Configure `max_retries` and `dead_letter_queue` in the
|
|
97
|
+
* consumer settings (Wrangler).
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* export default {
|
|
101
|
+
* queue: createCloudflareQueueHandler(createWorker({ listeners })),
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
declare const createCloudflareQueueHandler: (worker: Worker, options?: CloudflareQueueHandlerOptions) => (batch: CloudflareMessageBatch) => Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Body of a pulled message: JSON messages may come base64-encoded (RFC
|
|
108
|
+
* 4648), as the documentation says, or as is, as its examples show.
|
|
109
|
+
*/
|
|
110
|
+
declare const decodePulledBody: (body: unknown) => unknown;
|
|
111
|
+
/**
|
|
112
|
+
* Creates a source that pulls the queue over HTTP, for `startWorker()` and
|
|
113
|
+
* `dotpress-events work`: a worker outside Cloudflare, or your machine.
|
|
114
|
+
* Enable an HTTP pull consumer on the queue first
|
|
115
|
+
* (`wrangler queues consumer http add <queue>`).
|
|
116
|
+
*/
|
|
117
|
+
declare const createCloudflarePullSource: (options: CloudflarePullSourceOptions) => Source;
|
|
118
|
+
|
|
119
|
+
export { createCloudflarePullSource, createCloudflareQueueHandler, createCloudflareQueueTransport, decodePulledBody };
|
|
120
|
+
export type { CloudflareApiOptions, CloudflareMessageBatch, CloudflarePullSourceOptions, CloudflareQueueBinding, CloudflareQueueHandlerOptions, CloudflareQueueMessage, CloudflareQueueTransportOptions };
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PartialSendError
|
|
3
|
+
} from "./chunk-NCQ4ZTNB.js";
|
|
4
|
+
|
|
5
|
+
// src/transports/cloudflare-queues.ts
|
|
6
|
+
var MAX_MESSAGE_BYTES = 128e3 - 100;
|
|
7
|
+
var MAX_BATCH_MESSAGES = 100;
|
|
8
|
+
var MAX_BATCH_BYTES = 256e3;
|
|
9
|
+
var MAX_DELAY_SECONDS = 86400;
|
|
10
|
+
var MAX_PULL_BATCH = 100;
|
|
11
|
+
var MAX_VISIBILITY_MS = 432e5;
|
|
12
|
+
var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
13
|
+
var encoder = new TextEncoder();
|
|
14
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
var checkOptions = (label, options, allowed) => {
|
|
16
|
+
if (!isRecord(options)) {
|
|
17
|
+
throw new TypeError(`${label}: expected an object of options.`);
|
|
18
|
+
}
|
|
19
|
+
for (const key of Object.keys(options)) {
|
|
20
|
+
if (!allowed.includes(key)) {
|
|
21
|
+
throw new TypeError(`${label}: unknown option "${key}".`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return options;
|
|
25
|
+
};
|
|
26
|
+
var checkString = (label, key, value) => {
|
|
27
|
+
if (typeof value !== "string" || value === "") {
|
|
28
|
+
throw new TypeError(`${label}: ${key} must be a non-empty string.`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
var checkInteger = (label, key, value, min, max) => {
|
|
33
|
+
if (value !== void 0 && (!Number.isInteger(value) || value < min || value > max)) {
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
`${label}: ${key} must be an integer from ${min} to ${max}.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
var checkFunction = (label, key, value) => {
|
|
41
|
+
if (value !== void 0 && typeof value !== "function") {
|
|
42
|
+
throw new TypeError(`${label}: ${key} must be a function.`);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var bindingOf = (label, key, value) => {
|
|
46
|
+
if (typeof value !== "function" && typeof value?.sendBatch !== "function") {
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
`${label}: ${key} must be a queue binding, or a function returning it.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return () => typeof value === "function" ? value() : value;
|
|
52
|
+
};
|
|
53
|
+
var describe = (error) => error instanceof Error ? error.message : String(error);
|
|
54
|
+
var apiOf = (label, values) => {
|
|
55
|
+
const accountId = checkString(label, "accountId", values.accountId);
|
|
56
|
+
const queueId = checkString(label, "queueId", values.queueId);
|
|
57
|
+
const apiToken = checkString(label, "apiToken", values.apiToken);
|
|
58
|
+
const { baseUrl = DEFAULT_BASE_URL, fetch: fetchFn = globalThis.fetch } = values;
|
|
59
|
+
if (typeof baseUrl !== "string" || !/^https?:\/\/[^/]/.test(baseUrl)) {
|
|
60
|
+
throw new TypeError(`${label}: baseUrl must be an http(s) URL.`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof fetchFn !== "function") {
|
|
63
|
+
throw new TypeError(`${label}: fetch must be a function.`);
|
|
64
|
+
}
|
|
65
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
66
|
+
return async (path, body, queue = queueId) => {
|
|
67
|
+
const response = await fetchFn(
|
|
68
|
+
`${base}/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(queue)}/messages${path}`,
|
|
69
|
+
{
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
authorization: `Bearer ${apiToken}`,
|
|
73
|
+
"content-type": "application/json"
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(body)
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
let result = {};
|
|
79
|
+
try {
|
|
80
|
+
result = await response.json();
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok || result.success === false) {
|
|
84
|
+
const errors = (Array.isArray(result.errors) ? result.errors : []).map(({ code, message }) => `${code}: ${message}`).join("; ");
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Cloudflare Queues API: ${response.status} ${errors || response.statusText}`.trim()
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return result.result;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
var batchesOf = (envelopes) => {
|
|
93
|
+
const failed = [];
|
|
94
|
+
const batches = [];
|
|
95
|
+
for (const envelope of envelopes) {
|
|
96
|
+
const size = encoder.encode(JSON.stringify(envelope)).byteLength;
|
|
97
|
+
if (size > MAX_MESSAGE_BYTES) {
|
|
98
|
+
failed.push({
|
|
99
|
+
id: envelope.id,
|
|
100
|
+
error: new RangeError(
|
|
101
|
+
`Cloudflare Queues accepts messages of at most ${MAX_MESSAGE_BYTES} bytes (128 KB with its metadata), got ${size}.`
|
|
102
|
+
)
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const last = batches.at(-1);
|
|
107
|
+
if (last && last.envelopes.length < MAX_BATCH_MESSAGES && last.size + size <= MAX_BATCH_BYTES) {
|
|
108
|
+
last.envelopes.push(envelope);
|
|
109
|
+
last.size += size;
|
|
110
|
+
} else {
|
|
111
|
+
batches.push({ envelopes: [envelope], size });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { batches, failed };
|
|
115
|
+
};
|
|
116
|
+
var createCloudflareQueueTransport = (options) => {
|
|
117
|
+
const label = "createCloudflareQueueTransport()";
|
|
118
|
+
const values = checkOptions(label, options, [
|
|
119
|
+
"queue",
|
|
120
|
+
"accountId",
|
|
121
|
+
"queueId",
|
|
122
|
+
"apiToken",
|
|
123
|
+
"baseUrl",
|
|
124
|
+
"fetch"
|
|
125
|
+
]);
|
|
126
|
+
let sendBatch;
|
|
127
|
+
if (values.queue !== void 0) {
|
|
128
|
+
if (Object.keys(values).some((key) => key !== "queue")) {
|
|
129
|
+
throw new TypeError(
|
|
130
|
+
`${label}: set queue, or the REST API options, not both.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const binding = bindingOf(label, "queue", values.queue);
|
|
134
|
+
sendBatch = (envelopes, delay) => binding().sendBatch(
|
|
135
|
+
envelopes.map((body) => ({
|
|
136
|
+
body,
|
|
137
|
+
contentType: "json",
|
|
138
|
+
...delay === void 0 ? {} : { delaySeconds: delay }
|
|
139
|
+
}))
|
|
140
|
+
);
|
|
141
|
+
} else {
|
|
142
|
+
const api = apiOf(label, values);
|
|
143
|
+
sendBatch = (envelopes, delay) => api("/batch", {
|
|
144
|
+
messages: envelopes.map((body) => ({
|
|
145
|
+
body,
|
|
146
|
+
content_type: "json",
|
|
147
|
+
...delay === void 0 ? {} : { delay_seconds: delay }
|
|
148
|
+
}))
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
kind: "cloudflare-queues",
|
|
153
|
+
send: async (envelopes, { delay }) => {
|
|
154
|
+
if (delay !== void 0 && delay > MAX_DELAY_SECONDS) {
|
|
155
|
+
throw new RangeError(
|
|
156
|
+
`Cloudflare Queues accepts delays of at most ${MAX_DELAY_SECONDS} seconds, got ${delay}.`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const { batches, failed } = batchesOf(envelopes);
|
|
160
|
+
for (const batch of batches) {
|
|
161
|
+
try {
|
|
162
|
+
await sendBatch(batch.envelopes, delay);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
for (const { id } of batch.envelopes) failed.push({ id, error });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (failed.length > 0) throw new PartialSendError(failed);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
var delayOf = (retryDelay, attempt) => retryDelay === void 0 ? void 0 : Math.min(MAX_DELAY_SECONDS, Math.max(0, Math.round(retryDelay(attempt))));
|
|
172
|
+
var createCloudflareQueueHandler = (worker, options = {}) => {
|
|
173
|
+
const label = "createCloudflareQueueHandler()";
|
|
174
|
+
if (typeof worker?.handle !== "function") {
|
|
175
|
+
throw new TypeError(
|
|
176
|
+
`${label}: expected a worker created by createWorker().`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const values = checkOptions(label, options, ["deadLetterQueue", "retryDelay"]);
|
|
180
|
+
checkFunction(label, "retryDelay", values.retryDelay);
|
|
181
|
+
const retryDelay = values.retryDelay;
|
|
182
|
+
const deadLetterQueue = values.deadLetterQueue === void 0 ? void 0 : bindingOf(label, "deadLetterQueue", values.deadLetterQueue);
|
|
183
|
+
const settle = async (message, result) => {
|
|
184
|
+
if (result.status === "success") return message.ack();
|
|
185
|
+
if (result.status === "dead" && deadLetterQueue) {
|
|
186
|
+
try {
|
|
187
|
+
await deadLetterQueue().sendBatch([
|
|
188
|
+
{ body: message.body, contentType: "json" }
|
|
189
|
+
]);
|
|
190
|
+
return message.ack();
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const delaySeconds = delayOf(retryDelay, message.attempts);
|
|
195
|
+
message.retry(delaySeconds === void 0 ? void 0 : { delaySeconds });
|
|
196
|
+
};
|
|
197
|
+
return async (batch) => {
|
|
198
|
+
await Promise.all(
|
|
199
|
+
(batch?.messages ?? []).map(async (message) => {
|
|
200
|
+
const result = await worker.handle(message.body, {
|
|
201
|
+
attempt: message.attempts
|
|
202
|
+
});
|
|
203
|
+
await settle(message, result);
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
var decodePulledBody = (body) => {
|
|
209
|
+
if (typeof body !== "string") return body;
|
|
210
|
+
try {
|
|
211
|
+
const bytes = Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
|
|
212
|
+
return new TextDecoder().decode(bytes);
|
|
213
|
+
} catch {
|
|
214
|
+
return body;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
218
|
+
const timer = setTimeout(done, ms);
|
|
219
|
+
function done() {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
signal.removeEventListener("abort", done);
|
|
222
|
+
resolve();
|
|
223
|
+
}
|
|
224
|
+
signal.addEventListener("abort", done, { once: true });
|
|
225
|
+
});
|
|
226
|
+
var createCloudflarePullSource = (options) => {
|
|
227
|
+
const label = "createCloudflarePullSource()";
|
|
228
|
+
const values = checkOptions(label, options, [
|
|
229
|
+
"accountId",
|
|
230
|
+
"queueId",
|
|
231
|
+
"apiToken",
|
|
232
|
+
"baseUrl",
|
|
233
|
+
"fetch",
|
|
234
|
+
"batchSize",
|
|
235
|
+
"visibilityTimeoutMs",
|
|
236
|
+
"pollInterval",
|
|
237
|
+
"retryDelay",
|
|
238
|
+
"deadLetterQueueId"
|
|
239
|
+
]);
|
|
240
|
+
const api = apiOf(label, values);
|
|
241
|
+
const batchSize = checkInteger(label, "batchSize", values.batchSize, 1, MAX_PULL_BATCH) ?? 10;
|
|
242
|
+
const visibilityTimeoutMs = checkInteger(
|
|
243
|
+
label,
|
|
244
|
+
"visibilityTimeoutMs",
|
|
245
|
+
values.visibilityTimeoutMs,
|
|
246
|
+
1,
|
|
247
|
+
MAX_VISIBILITY_MS
|
|
248
|
+
) ?? 3e4;
|
|
249
|
+
const pollInterval = checkInteger(label, "pollInterval", values.pollInterval, 0, 36e5) ?? 1e3;
|
|
250
|
+
checkFunction(label, "retryDelay", values.retryDelay);
|
|
251
|
+
const retryDelay = values.retryDelay;
|
|
252
|
+
const deadLetterQueueId = values.deadLetterQueueId === void 0 ? void 0 : checkString(label, "deadLetterQueueId", values.deadLetterQueueId);
|
|
253
|
+
return {
|
|
254
|
+
kind: "cloudflare-queues",
|
|
255
|
+
consume: async (worker, { signal, logger }) => {
|
|
256
|
+
let errorDelay = 0;
|
|
257
|
+
while (!signal.aborted) {
|
|
258
|
+
let messages;
|
|
259
|
+
try {
|
|
260
|
+
const result = await api("/pull", {
|
|
261
|
+
batch_size: batchSize,
|
|
262
|
+
visibility_timeout_ms: visibilityTimeoutMs
|
|
263
|
+
});
|
|
264
|
+
messages = result?.messages ?? [];
|
|
265
|
+
errorDelay = 0;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
errorDelay = Math.min(3e4, Math.max(1e3, errorDelay * 2));
|
|
268
|
+
logger.error(
|
|
269
|
+
{ error: describe(error), retryInMs: errorDelay },
|
|
270
|
+
"Cannot pull Cloudflare Queues messages"
|
|
271
|
+
);
|
|
272
|
+
await sleep(errorDelay, signal);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (messages.length === 0) {
|
|
276
|
+
await sleep(pollInterval, signal);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const acks = [];
|
|
280
|
+
const retries = [];
|
|
281
|
+
await Promise.all(
|
|
282
|
+
messages.map(async (message) => {
|
|
283
|
+
const leaseId = String(message.lease_id);
|
|
284
|
+
const attempt = Number(message.attempts) || 1;
|
|
285
|
+
const body = decodePulledBody(message.body);
|
|
286
|
+
const result = await worker.handle(body, { attempt });
|
|
287
|
+
if (result.status === "success") {
|
|
288
|
+
acks.push({ lease_id: leaseId });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (result.status === "dead" && deadLetterQueueId) {
|
|
292
|
+
try {
|
|
293
|
+
const envelope = typeof body === "string" ? JSON.parse(body) : body;
|
|
294
|
+
await api(
|
|
295
|
+
"/batch",
|
|
296
|
+
{ messages: [{ body: envelope, content_type: "json" }] },
|
|
297
|
+
deadLetterQueueId
|
|
298
|
+
);
|
|
299
|
+
acks.push({ lease_id: leaseId });
|
|
300
|
+
return;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
logger.error(
|
|
303
|
+
{ deadLetterQueueId, error: describe(error) },
|
|
304
|
+
"Cannot copy the message to the dead-letter queue"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const delay = delayOf(retryDelay, attempt);
|
|
309
|
+
retries.push({
|
|
310
|
+
lease_id: leaseId,
|
|
311
|
+
...delay === void 0 ? {} : { delay_seconds: delay }
|
|
312
|
+
});
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
try {
|
|
316
|
+
await api("/ack", { acks, retries });
|
|
317
|
+
} catch (error) {
|
|
318
|
+
logger.error(
|
|
319
|
+
{ error: describe(error) },
|
|
320
|
+
"Cannot acknowledge Cloudflare Queues messages: they will be delivered again"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
export {
|
|
328
|
+
createCloudflarePullSource,
|
|
329
|
+
createCloudflareQueueHandler,
|
|
330
|
+
createCloudflareQueueTransport,
|
|
331
|
+
decodePulledBody
|
|
332
|
+
};
|
|
333
|
+
//# sourceMappingURL=cloudflare-queues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/transports/cloudflare-queues.ts"],"sourcesContent":["import type { Envelope } from '../envelope.js'\nimport { PartialSendError } from '../errors.js'\nimport type { Source, Transport } from '../transport.js'\nimport type { HandleResult, Worker } from '../worker.js'\n\n/** Producer binding of a queue (`env.EVENTS_QUEUE` in a Worker). */\nexport interface CloudflareQueueBinding {\n sendBatch(\n messages: Iterable<{\n body: unknown\n contentType?: 'json'\n delaySeconds?: number\n }>\n ): Promise<unknown>\n}\n\n/** A message received by the `queue()` handler of a Worker. */\nexport interface CloudflareQueueMessage {\n readonly id: string\n readonly body: unknown\n /** Attempt number, from 1. */\n readonly attempts: number\n ack(): void\n retry(options?: { delaySeconds?: number }): void\n}\n\n/** The batch received by the `queue()` handler of a Worker. */\nexport interface CloudflareMessageBatch {\n readonly queue: string\n readonly messages: readonly CloudflareQueueMessage[]\n}\n\n/** REST API access, to use a queue from outside Cloudflare Workers. */\nexport interface CloudflareApiOptions {\n accountId: string\n /** Queue ID (not its name). */\n queueId: string\n /** API token with Queues permissions (read and write to pull). */\n apiToken: string\n /** Base URL of the API. Defaults to `https://api.cloudflare.com/client/v4`. */\n baseUrl?: string | undefined\n /** `fetch` implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch | undefined\n}\n\n/** Options of `createCloudflareQueueTransport()`. */\nexport type CloudflareQueueTransportOptions =\n | {\n /**\n * Producer binding of the queue, or a function returning it (when the\n * bus is created before the `env` of the Worker is available).\n */\n queue: CloudflareQueueBinding | (() => CloudflareQueueBinding)\n }\n | CloudflareApiOptions\n\n/** Options of `createCloudflareQueueHandler()`. */\nexport interface CloudflareQueueHandlerOptions {\n /**\n * Producer binding of the dead-letter queue: messages that can never be\n * handled are sent there at once, instead of failing until `max_retries`.\n */\n deadLetterQueue?:\n CloudflareQueueBinding | (() => CloudflareQueueBinding) | undefined\n /** Delay before a retry, in seconds, given the attempt number. */\n retryDelay?: ((attempt: number) => number) | undefined\n}\n\n/** Options of `createCloudflarePullSource()`. */\nexport interface CloudflarePullSourceOptions extends CloudflareApiOptions {\n /** Messages per pull (1 to 100). Defaults to 10. */\n batchSize?: number | undefined\n /**\n * Lease of the pulled messages, in milliseconds (up to 12 hours): longer\n * than your slowest listener, since it cannot be extended. Defaults to\n * 30 seconds.\n */\n visibilityTimeoutMs?: number | undefined\n /** Wait before pulling again an empty queue, in milliseconds. Defaults to 1 second. */\n pollInterval?: number | undefined\n /** Delay before a retry, in seconds, given the attempt number. */\n retryDelay?: ((attempt: number) => number) | undefined\n /**\n * Queue ID of the dead-letter queue: messages that can never be handled\n * are pushed there at once, instead of failing until `max_retries`.\n */\n deadLetterQueueId?: string | undefined\n}\n\n// Limits of Cloudflare Queues (1 KB = 1000 bytes). Messages also carry\n// about 100 bytes of internal metadata.\nconst MAX_MESSAGE_BYTES = 128_000 - 100\nconst MAX_BATCH_MESSAGES = 100\nconst MAX_BATCH_BYTES = 256_000\nconst MAX_DELAY_SECONDS = 86_400\nconst MAX_PULL_BATCH = 100\nconst MAX_VISIBILITY_MS = 43_200_000\nconst DEFAULT_BASE_URL = 'https://api.cloudflare.com/client/v4'\n\nconst encoder = new TextEncoder()\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst checkOptions = (\n label: string,\n options: unknown,\n allowed: readonly string[]\n): Record<string, unknown> => {\n if (!isRecord(options)) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!allowed.includes(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n return options\n}\n\nconst checkString = (label: string, key: string, value: unknown) => {\n if (typeof value !== 'string' || value === '') {\n throw new TypeError(`${label}: ${key} must be a non-empty string.`)\n }\n return value\n}\n\nconst checkInteger = (\n label: string,\n key: string,\n value: unknown,\n min: number,\n max: number\n) => {\n if (\n value !== undefined &&\n (!Number.isInteger(value) ||\n (value as number) < min ||\n (value as number) > max)\n ) {\n throw new TypeError(\n `${label}: ${key} must be an integer from ${min} to ${max}.`\n )\n }\n return value as number | undefined\n}\n\nconst checkFunction = (label: string, key: string, value: unknown) => {\n if (value !== undefined && typeof value !== 'function') {\n throw new TypeError(`${label}: ${key} must be a function.`)\n }\n}\n\n/** A binding, or the function returning it. */\nconst bindingOf = (label: string, key: string, value: unknown) => {\n if (\n typeof value !== 'function' &&\n typeof (value as CloudflareQueueBinding | null)?.sendBatch !== 'function'\n ) {\n throw new TypeError(\n `${label}: ${key} must be a queue binding, or a function returning it.`\n )\n }\n return () =>\n typeof value === 'function'\n ? (value as () => CloudflareQueueBinding)()\n : (value as CloudflareQueueBinding)\n}\n\nconst describe = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\n/** Client of the REST API of a queue. */\nconst apiOf = (label: string, values: Record<string, unknown>) => {\n const accountId = checkString(label, 'accountId', values.accountId)\n const queueId = checkString(label, 'queueId', values.queueId)\n const apiToken = checkString(label, 'apiToken', values.apiToken)\n const { baseUrl = DEFAULT_BASE_URL, fetch: fetchFn = globalThis.fetch } =\n values\n if (typeof baseUrl !== 'string' || !/^https?:\\/\\/[^/]/.test(baseUrl)) {\n throw new TypeError(`${label}: baseUrl must be an http(s) URL.`)\n }\n if (typeof fetchFn !== 'function') {\n throw new TypeError(`${label}: fetch must be a function.`)\n }\n const base = baseUrl.replace(/\\/+$/, '')\n\n return async (path: string, body: unknown, queue = queueId) => {\n const response = await (fetchFn as typeof fetch)(\n `${base}/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(queue)}/messages${path}`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${apiToken}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n }\n )\n let result: {\n success?: unknown\n errors?: { code?: unknown; message?: unknown }[]\n result?: unknown\n } = {}\n try {\n result = (await response.json()) as typeof result\n } catch {\n // Not JSON: reported with the status below.\n }\n if (!response.ok || result.success === false) {\n const errors = (Array.isArray(result.errors) ? result.errors : [])\n .map(({ code, message }) => `${code}: ${message}`)\n .join('; ')\n throw new Error(\n `Cloudflare Queues API: ${response.status} ${errors || response.statusText}`.trim()\n )\n }\n return result.result\n }\n}\n\n/** Batches within the limits of Cloudflare Queues; oversized ones fail. */\nconst batchesOf = (envelopes: readonly Envelope[]) => {\n const failed: { id: string; error: unknown }[] = []\n const batches: { envelopes: Envelope[]; size: number }[] = []\n for (const envelope of envelopes) {\n const size = encoder.encode(JSON.stringify(envelope)).byteLength\n if (size > MAX_MESSAGE_BYTES) {\n failed.push({\n id: envelope.id,\n error: new RangeError(\n `Cloudflare Queues accepts messages of at most ${MAX_MESSAGE_BYTES} bytes (128 KB with its metadata), got ${size}.`\n ),\n })\n continue\n }\n const last = batches.at(-1)\n if (\n last &&\n last.envelopes.length < MAX_BATCH_MESSAGES &&\n last.size + size <= MAX_BATCH_BYTES\n ) {\n last.envelopes.push(envelope)\n last.size += size\n } else {\n batches.push({ envelopes: [envelope], size })\n }\n }\n return { batches, failed }\n}\n\n/**\n * Creates a transport that sends messages to a Cloudflare Queue: through\n * its producer binding inside a Worker, or through the REST API from\n * anywhere else (Node.js, Lambda…).\n *\n * ```ts\n * // In a Worker\n * import { env } from 'cloudflare:workers'\n * const transport = createCloudflareQueueTransport({ queue: () => env.EVENTS_QUEUE })\n *\n * // Elsewhere\n * const transport = createCloudflareQueueTransport({ accountId, queueId, apiToken })\n * ```\n */\nexport const createCloudflareQueueTransport = (\n options: CloudflareQueueTransportOptions\n): Transport => {\n const label = 'createCloudflareQueueTransport()'\n const values = checkOptions(label, options, [\n 'queue',\n 'accountId',\n 'queueId',\n 'apiToken',\n 'baseUrl',\n 'fetch',\n ])\n let sendBatch: (\n envelopes: Envelope[],\n delay: number | undefined\n ) => Promise<unknown>\n if (values.queue !== undefined) {\n if (Object.keys(values).some((key) => key !== 'queue')) {\n throw new TypeError(\n `${label}: set queue, or the REST API options, not both.`\n )\n }\n const binding = bindingOf(label, 'queue', values.queue)\n sendBatch = (envelopes, delay) =>\n binding().sendBatch(\n envelopes.map((body) => ({\n body,\n contentType: 'json' as const,\n ...(delay === undefined ? {} : { delaySeconds: delay }),\n }))\n )\n } else {\n const api = apiOf(label, values)\n sendBatch = (envelopes, delay) =>\n api('/batch', {\n messages: envelopes.map((body) => ({\n body,\n content_type: 'json',\n ...(delay === undefined ? {} : { delay_seconds: delay }),\n })),\n })\n }\n\n return {\n kind: 'cloudflare-queues',\n send: async (envelopes, { delay }) => {\n if (delay !== undefined && delay > MAX_DELAY_SECONDS) {\n throw new RangeError(\n `Cloudflare Queues accepts delays of at most ${MAX_DELAY_SECONDS} seconds, got ${delay}.`\n )\n }\n const { batches, failed } = batchesOf(envelopes)\n for (const batch of batches) {\n try {\n await sendBatch(batch.envelopes, delay)\n } catch (error) {\n for (const { id } of batch.envelopes) failed.push({ id, error })\n }\n }\n if (failed.length > 0) throw new PartialSendError(failed)\n },\n }\n}\n\nconst delayOf = (\n retryDelay: ((attempt: number) => number) | undefined,\n attempt: number\n) =>\n retryDelay === undefined\n ? undefined\n : Math.min(MAX_DELAY_SECONDS, Math.max(0, Math.round(retryDelay(attempt))))\n\n/**\n * Creates the `queue()` handler of a Worker consuming the queue. Each\n * message is acknowledged or retried on its own: a failure does not retry\n * the whole batch. Configure `max_retries` and `dead_letter_queue` in the\n * consumer settings (Wrangler).\n *\n * ```ts\n * export default {\n * queue: createCloudflareQueueHandler(createWorker({ listeners })),\n * }\n * ```\n */\nexport const createCloudflareQueueHandler = (\n worker: Worker,\n options: CloudflareQueueHandlerOptions = {}\n) => {\n const label = 'createCloudflareQueueHandler()'\n if (typeof worker?.handle !== 'function') {\n throw new TypeError(\n `${label}: expected a worker created by createWorker().`\n )\n }\n const values = checkOptions(label, options, ['deadLetterQueue', 'retryDelay'])\n checkFunction(label, 'retryDelay', values.retryDelay)\n const retryDelay =\n values.retryDelay as CloudflareQueueHandlerOptions['retryDelay']\n const deadLetterQueue =\n values.deadLetterQueue === undefined\n ? undefined\n : bindingOf(label, 'deadLetterQueue', values.deadLetterQueue)\n\n const settle = async (\n message: CloudflareQueueMessage,\n result: HandleResult\n ) => {\n if (result.status === 'success') return message.ack()\n if (result.status === 'dead' && deadLetterQueue) {\n try {\n await deadLetterQueue().sendBatch([\n { body: message.body, contentType: 'json' },\n ])\n return message.ack()\n } catch {\n // Retried: it reaches the dead-letter queue after max_retries.\n }\n }\n const delaySeconds = delayOf(retryDelay, message.attempts)\n message.retry(delaySeconds === undefined ? undefined : { delaySeconds })\n }\n\n return async (batch: CloudflareMessageBatch): Promise<void> => {\n await Promise.all(\n (batch?.messages ?? []).map(async (message) => {\n const result = await worker.handle(message.body, {\n attempt: message.attempts,\n })\n await settle(message, result)\n })\n )\n }\n}\n\ninterface PulledMessage {\n body?: unknown\n attempts?: unknown\n lease_id?: unknown\n metadata?: unknown\n}\n\n/**\n * Body of a pulled message: JSON messages may come base64-encoded (RFC\n * 4648), as the documentation says, or as is, as its examples show.\n */\nexport const decodePulledBody = (body: unknown) => {\n // Plain JSON is not base64 (\"{\" is not a base64 character): atob()\n // fails, and the body is kept as is.\n if (typeof body !== 'string') return body\n try {\n const bytes = Uint8Array.from(atob(body), (char) => char.charCodeAt(0))\n return new TextDecoder().decode(bytes)\n } catch {\n return body\n }\n}\n\nconst sleep = (ms: number, signal: AbortSignal) =>\n new Promise<void>((resolve) => {\n const timer = setTimeout(done, ms)\n function done() {\n clearTimeout(timer)\n signal.removeEventListener('abort', done)\n resolve()\n }\n signal.addEventListener('abort', done, { once: true })\n })\n\n/**\n * Creates a source that pulls the queue over HTTP, for `startWorker()` and\n * `dotpress-events work`: a worker outside Cloudflare, or your machine.\n * Enable an HTTP pull consumer on the queue first\n * (`wrangler queues consumer http add <queue>`).\n */\nexport const createCloudflarePullSource = (\n options: CloudflarePullSourceOptions\n): Source => {\n const label = 'createCloudflarePullSource()'\n const values = checkOptions(label, options, [\n 'accountId',\n 'queueId',\n 'apiToken',\n 'baseUrl',\n 'fetch',\n 'batchSize',\n 'visibilityTimeoutMs',\n 'pollInterval',\n 'retryDelay',\n 'deadLetterQueueId',\n ])\n const api = apiOf(label, values)\n const batchSize =\n checkInteger(label, 'batchSize', values.batchSize, 1, MAX_PULL_BATCH) ?? 10\n const visibilityTimeoutMs =\n checkInteger(\n label,\n 'visibilityTimeoutMs',\n values.visibilityTimeoutMs,\n 1,\n MAX_VISIBILITY_MS\n ) ?? 30_000\n const pollInterval =\n checkInteger(label, 'pollInterval', values.pollInterval, 0, 3_600_000) ??\n 1000\n checkFunction(label, 'retryDelay', values.retryDelay)\n const retryDelay =\n values.retryDelay as CloudflarePullSourceOptions['retryDelay']\n const deadLetterQueueId =\n values.deadLetterQueueId === undefined\n ? undefined\n : checkString(label, 'deadLetterQueueId', values.deadLetterQueueId)\n\n return {\n kind: 'cloudflare-queues',\n consume: async (worker, { signal, logger }) => {\n let errorDelay = 0\n while (!signal.aborted) {\n let messages: PulledMessage[]\n try {\n const result = (await api('/pull', {\n batch_size: batchSize,\n visibility_timeout_ms: visibilityTimeoutMs,\n })) as { messages?: PulledMessage[] } | undefined\n messages = result?.messages ?? []\n errorDelay = 0\n } catch (error) {\n errorDelay = Math.min(30_000, Math.max(1000, errorDelay * 2))\n logger.error(\n { error: describe(error), retryInMs: errorDelay },\n 'Cannot pull Cloudflare Queues messages'\n )\n await sleep(errorDelay, signal)\n continue\n }\n if (messages.length === 0) {\n await sleep(pollInterval, signal)\n continue\n }\n\n const acks: { lease_id: string }[] = []\n const retries: { lease_id: string; delay_seconds?: number }[] = []\n await Promise.all(\n messages.map(async (message) => {\n const leaseId = String(message.lease_id)\n const attempt = Number(message.attempts) || 1\n const body = decodePulledBody(message.body)\n const result = await worker.handle(body, { attempt })\n if (result.status === 'success') {\n acks.push({ lease_id: leaseId })\n return\n }\n if (result.status === 'dead' && deadLetterQueueId) {\n try {\n const envelope =\n typeof body === 'string'\n ? (JSON.parse(body) as unknown)\n : body\n await api(\n '/batch',\n { messages: [{ body: envelope, content_type: 'json' }] },\n deadLetterQueueId\n )\n acks.push({ lease_id: leaseId })\n return\n } catch (error) {\n logger.error(\n { deadLetterQueueId, error: describe(error) },\n 'Cannot copy the message to the dead-letter queue'\n )\n }\n }\n const delay = delayOf(retryDelay, attempt)\n retries.push({\n lease_id: leaseId,\n ...(delay === undefined ? {} : { delay_seconds: delay }),\n })\n })\n )\n try {\n await api('/ack', { acks, retries })\n } catch (error) {\n logger.error(\n { error: describe(error) },\n 'Cannot acknowledge Cloudflare Queues messages: they will be delivered again'\n )\n }\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { PubSub } from '@google-cloud/pubsub';
|
|
2
|
+
import { EventsLogger, Source, Worker, Transport } from './index.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OAuth access token of the publisher, or `false` without authentication
|
|
7
|
+
* (the Pub/Sub emulator). Defaults to Application Default Credentials,
|
|
8
|
+
* through `google-auth-library`.
|
|
9
|
+
*/
|
|
10
|
+
type AccessTokenOption = (() => Promise<string>) | false;
|
|
11
|
+
/** Options to publish to a topic over the REST API. */
|
|
12
|
+
interface PubSubPublishOptions {
|
|
13
|
+
/** Full topic name: `projects/{project}/topics/{topic}`. */
|
|
14
|
+
topic: string;
|
|
15
|
+
getAccessToken?: AccessTokenOption | undefined;
|
|
16
|
+
/** Base URL of the API. Defaults to `https://pubsub.googleapis.com`. */
|
|
17
|
+
baseUrl?: string | undefined;
|
|
18
|
+
/** `fetch` implementation. Defaults to the global `fetch`. */
|
|
19
|
+
fetch?: typeof fetch | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Options of `createPubSubPushHandler()`. */
|
|
22
|
+
interface PubSubPushHandlerOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Checks the OIDC token of push requests (`Authorization: Bearer`).
|
|
25
|
+
* Defaults to true; set `false` only behind a proxy that checks it.
|
|
26
|
+
*/
|
|
27
|
+
verifyToken?: boolean | undefined;
|
|
28
|
+
/** Audience of the token, as configured on the push subscription. */
|
|
29
|
+
audience?: string | undefined;
|
|
30
|
+
/** Expected service account of the push subscription (`email` claim). */
|
|
31
|
+
serviceAccountEmail?: string | undefined;
|
|
32
|
+
/** Keys of the tokens. Defaults to the Google certificates. */
|
|
33
|
+
certsUrl?: string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Topic where messages that can never be handled are published at once,
|
|
36
|
+
* instead of failing until `maxDeliveryAttempts`.
|
|
37
|
+
*/
|
|
38
|
+
deadLetter?: Omit<PubSubPublishOptions, 'fetch'> | undefined;
|
|
39
|
+
/** `fetch` implementation, for the dead-letter topic. */
|
|
40
|
+
fetch?: typeof fetch | undefined;
|
|
41
|
+
logger?: EventsLogger | undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Push endpoint of a subscription. */
|
|
44
|
+
interface PubSubPushHandler {
|
|
45
|
+
/** Handles a push request (Web `Request`), and returns the response. */
|
|
46
|
+
fetch(request: Request): Promise<Response>;
|
|
47
|
+
/**
|
|
48
|
+
* Handles a push body (parsed JSON) with its `Authorization` header, for
|
|
49
|
+
* frameworks without Web requests. Returns the HTTP status to answer.
|
|
50
|
+
*/
|
|
51
|
+
handle(body: unknown, authorization: string | undefined): Promise<number>;
|
|
52
|
+
}
|
|
53
|
+
/** Options of `createPubSubPullSource()`. */
|
|
54
|
+
interface PubSubPullSourceOptions {
|
|
55
|
+
/** Full subscription name: `projects/{project}/subscriptions/{name}`. */
|
|
56
|
+
subscription: string;
|
|
57
|
+
/** Existing client. Defaults to a client created with `clientConfig`. */
|
|
58
|
+
client?: PubSub | undefined;
|
|
59
|
+
/** Options of the created client (`projectId`, `credentials`…). */
|
|
60
|
+
clientConfig?: ConstructorParameters<typeof PubSub>[0] | undefined;
|
|
61
|
+
/** Messages handled at the same time. Defaults to 10. */
|
|
62
|
+
maxMessages?: number | undefined;
|
|
63
|
+
/** Full name of the topic for messages that can never be handled. */
|
|
64
|
+
deadLetterTopic?: string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* On stop, how long to wait for the messages in progress, in
|
|
67
|
+
* milliseconds. Defaults to 30 seconds.
|
|
68
|
+
*/
|
|
69
|
+
closeTimeout?: number | undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a transport that publishes messages to a Google Cloud Pub/Sub
|
|
73
|
+
* topic, over the REST API with `fetch`: it also runs outside Node.js.
|
|
74
|
+
* Pub/Sub has no delayed delivery: `delay` is refused.
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* const transport = createPubSubTransport({ topic: 'projects/my-project/topics/events' })
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare const createPubSubTransport: (options: PubSubPublishOptions) => Transport;
|
|
81
|
+
/**
|
|
82
|
+
* Creates the endpoint of a push subscription: Pub/Sub POSTs each message
|
|
83
|
+
* to it, and an answer other than 2xx makes it deliver the message again.
|
|
84
|
+
* Mount `fetch` on any Web-standard runtime (Cloudflare Workers, Hono,
|
|
85
|
+
* Bun, Deno, Cloud Run), or call `handle` from another framework.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const push = createPubSubPushHandler(worker, {
|
|
89
|
+
* audience: 'https://api.example.com/pubsub',
|
|
90
|
+
* serviceAccountEmail: 'pubsub-push@my-project.iam.gserviceaccount.com',
|
|
91
|
+
* })
|
|
92
|
+
* app.post('/pubsub', (c) => push.fetch(c.req.raw))
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare const createPubSubPushHandler: (worker: Worker, options?: PubSubPushHandlerOptions) => PubSubPushHandler;
|
|
96
|
+
/**
|
|
97
|
+
* Creates a source that pulls a subscription with the Pub/Sub client
|
|
98
|
+
* (`@google-cloud/pubsub`, peer dependency; Node.js), for `startWorker()`
|
|
99
|
+
* and `dotpress-events work`.
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* export default {
|
|
103
|
+
* worker: createWorker({ listeners }),
|
|
104
|
+
* source: createPubSubPullSource({ subscription: 'projects/my-project/subscriptions/events' }),
|
|
105
|
+
* }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
declare const createPubSubPullSource: (options: PubSubPullSourceOptions) => Source;
|
|
109
|
+
|
|
110
|
+
export { createPubSubPullSource, createPubSubPushHandler, createPubSubTransport };
|
|
111
|
+
export type { AccessTokenOption, PubSubPublishOptions, PubSubPullSourceOptions, PubSubPushHandler, PubSubPushHandlerOptions };
|