@fedify/netlify 2.4.0-dev.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/dist/mod.d.cts ADDED
@@ -0,0 +1,177 @@
1
+ /// <reference lib="esnext.temporal" />
2
+ import { Federation, KvStore, Message, MessageQueue, MessageQueueEnqueueOptions, MessageQueueListenOptions } from "@fedify/fedify/federation";
3
+ import { AsyncWorkloadEvent, CustomAsyncWorkloadEvent, asyncWorkloadFn } from "@netlify/async-workloads";
4
+
5
+ //#region src/types.d.ts
6
+ /**
7
+ * The subset of Netlify's `AsyncWorkloadsClient` used by this package.
8
+ *
9
+ * The real `AsyncWorkloadsClient` satisfies this interface. The narrower
10
+ * interface also makes it possible to supply a test double.
11
+ *
12
+ * @since 2.4.0
13
+ */
14
+ interface NetlifyAsyncWorkloadsClient {
15
+ send(eventName: string, options?: {
16
+ readonly data?: NetlifyQueueEventData;
17
+ readonly delayUntil?: number | string;
18
+ readonly priority?: number;
19
+ }): Promise<{
20
+ readonly sendStatus: "succeeded" | "failed";
21
+ readonly eventId: string;
22
+ }>;
23
+ }
24
+ //#endregion
25
+ //#region src/mq.d.ts
26
+ /**
27
+ * A Fedify queue message wrapped for Netlify Async Workloads.
28
+ *
29
+ * @since 2.4.0
30
+ */
31
+ interface NetlifyQueueEventData {
32
+ readonly message: Message;
33
+ readonly orderingKey?: string;
34
+ readonly orderingSequence?: number;
35
+ }
36
+ /**
37
+ * Options for {@link NetlifyMessageQueue}.
38
+ *
39
+ * @since 2.4.0
40
+ */
41
+ interface NetlifyMessageQueueOptions {
42
+ /** A Netlify Async Workloads client. */
43
+ readonly client: NetlifyAsyncWorkloadsClient;
44
+ /**
45
+ * The Async Workloads event name. It must match the event configured for
46
+ * the workload function. `"fedify:queue"` by default.
47
+ * @default `"fedify:queue"`
48
+ */
49
+ readonly eventName?: string;
50
+ /**
51
+ * A CAS-capable key–value store used to serialize messages that have the
52
+ * same ordering key. Required when an `orderingKey` is enqueued.
53
+ */
54
+ readonly orderingKv?: KvStore;
55
+ /**
56
+ * The durable sleep interval used while an earlier ordered message is still
57
+ * running. Five seconds by default.
58
+ * @default `{ seconds: 5 }`
59
+ */
60
+ readonly orderingRetryDelay?: Temporal.Duration | Temporal.DurationLike;
61
+ }
62
+ /**
63
+ * An error raised when an event send is not acknowledged.
64
+ *
65
+ * For an ordered event, {@link orderingSequence} remains reserved because the
66
+ * router may already have accepted the event. It can be passed to
67
+ * {@link NetlifyMessageQueue.skipOrderingSequence} only after an operator has
68
+ * confirmed that the event cannot still be delivered.
69
+ *
70
+ * @since 2.4.0
71
+ */
72
+ declare class NetlifyMessageQueueSendError extends Error {
73
+ readonly eventName: string;
74
+ readonly eventId?: string;
75
+ readonly orderingKey?: string;
76
+ readonly orderingSequence?: number;
77
+ /** Creates a queue send error. @internal */
78
+ constructor(options: {
79
+ readonly eventName: string;
80
+ readonly eventId?: string;
81
+ readonly orderingKey?: string;
82
+ readonly orderingSequence?: number;
83
+ readonly cause?: unknown;
84
+ });
85
+ }
86
+ /**
87
+ * A message queue that publishes Fedify jobs to Netlify Async Workloads.
88
+ *
89
+ * Async Workloads invokes a separate function for each event, so this queue
90
+ * cannot consume messages through {@link listen}. Use
91
+ * {@link createNetlifyQueueHandler} in a Netlify Function and set Fedify's
92
+ * `manuallyStartQueue` option to `true`.
93
+ *
94
+ * @since 2.4.0
95
+ */
96
+ declare class NetlifyMessageQueue implements MessageQueue {
97
+ #private;
98
+ readonly eventName: string;
99
+ readonly nativeRetrial = true;
100
+ readonly nativeDeduplication = false;
101
+ /** Creates a Netlify Async Workloads-backed message queue. */
102
+ constructor(options: NetlifyMessageQueueOptions);
103
+ /** {@inheritDoc MessageQueue.enqueue} */
104
+ enqueue(message: Message, options?: MessageQueueEnqueueOptions): Promise<void>;
105
+ /**
106
+ * Skips a reserved ordering sequence that can no longer be processed.
107
+ *
108
+ * Use this only after confirming that the corresponding Async Workloads
109
+ * event is permanently dead-lettered or was never accepted. Skipping an
110
+ * event that can still be delivered causes that event to be ignored.
111
+ *
112
+ * @param orderingKey The ordering key of the blocked sequence.
113
+ * @param sequence The sequence to skip.
114
+ */
115
+ skipOrderingSequence(orderingKey: string, sequence: number): Promise<void>;
116
+ /** {@inheritDoc MessageQueue.enqueueMany} */
117
+ enqueueMany(messages: readonly Message[], options?: MessageQueueEnqueueOptions): Promise<void>;
118
+ /**
119
+ * This operation is unsupported because Netlify invokes workload functions
120
+ * for queued events.
121
+ */
122
+ listen(_handler: (message: Message) => Promise<void> | void, _options?: MessageQueueListenOptions): Promise<void>;
123
+ }
124
+ //#endregion
125
+ //#region src/handler.d.ts
126
+ /**
127
+ * The Async Workloads event emitted by {@link NetlifyMessageQueue}.
128
+ *
129
+ * @since 2.4.0
130
+ */
131
+ interface NetlifyQueueEvent extends CustomAsyncWorkloadEvent {
132
+ readonly eventName: string;
133
+ readonly eventData: NetlifyQueueEventData;
134
+ }
135
+ /**
136
+ * Options for {@link createNetlifyQueueHandler}.
137
+ *
138
+ * @typeParam TContextData The context data passed to Fedify dispatchers.
139
+ * @since 2.4.0
140
+ */
141
+ interface NetlifyQueueHandlerOptions<TContextData> {
142
+ /** The queue that receives events for this handler. */
143
+ readonly queue: NetlifyMessageQueue;
144
+ /**
145
+ * Creates the federation for a workload invocation. The factory is called
146
+ * once per event so resources do not have to survive between invocations.
147
+ */
148
+ readonly federation: (event: AsyncWorkloadEvent<NetlifyQueueEvent>) => Federation<TContextData> | Promise<Federation<TContextData>>;
149
+ /**
150
+ * Creates the context data passed to `Federation.processQueuedTask()`.
151
+ * When omitted, `undefined` is used.
152
+ */
153
+ readonly contextData?: (event: AsyncWorkloadEvent<NetlifyQueueEvent>) => TContextData | Promise<TContextData>;
154
+ /**
155
+ * The workload's configured retry count. This must equal
156
+ * `asyncWorkloadConfig.maxRetries` so a permanently failed ordered event can
157
+ * release its sequence. Four by default, matching Async Workloads.
158
+ * @default 4
159
+ */
160
+ readonly maxRetries?: number;
161
+ }
162
+ type WorkloadFunction = ReturnType<typeof asyncWorkloadFn<NetlifyQueueEvent>>;
163
+ /**
164
+ * Creates a Netlify Async Workloads function that processes Fedify jobs.
165
+ *
166
+ * Export the returned function as the default export of a file under
167
+ * *netlify/functions/*. The workload's `asyncWorkloadConfig.events` must
168
+ * contain the associated queue's {@link NetlifyMessageQueue.eventName}.
169
+ *
170
+ * @typeParam TContextData The context data passed to Fedify dispatchers.
171
+ * @param options The workload handler options.
172
+ * @returns A function produced by Netlify's `asyncWorkloadFn()`.
173
+ * @since 2.4.0
174
+ */
175
+ declare function createNetlifyQueueHandler<TContextData>(options: NetlifyQueueHandlerOptions<TContextData>): WorkloadFunction;
176
+ //#endregion
177
+ export { type NetlifyAsyncWorkloadsClient, NetlifyMessageQueue, type NetlifyMessageQueueOptions, NetlifyMessageQueueSendError, type NetlifyQueueEvent, type NetlifyQueueEventData, type NetlifyQueueHandlerOptions, createNetlifyQueueHandler };
package/dist/mod.d.ts ADDED
@@ -0,0 +1,177 @@
1
+ /// <reference lib="esnext.temporal" />
2
+ import { AsyncWorkloadEvent, CustomAsyncWorkloadEvent, asyncWorkloadFn } from "@netlify/async-workloads";
3
+ import { Federation, KvStore, Message, MessageQueue, MessageQueueEnqueueOptions, MessageQueueListenOptions } from "@fedify/fedify/federation";
4
+
5
+ //#region src/types.d.ts
6
+ /**
7
+ * The subset of Netlify's `AsyncWorkloadsClient` used by this package.
8
+ *
9
+ * The real `AsyncWorkloadsClient` satisfies this interface. The narrower
10
+ * interface also makes it possible to supply a test double.
11
+ *
12
+ * @since 2.4.0
13
+ */
14
+ interface NetlifyAsyncWorkloadsClient {
15
+ send(eventName: string, options?: {
16
+ readonly data?: NetlifyQueueEventData;
17
+ readonly delayUntil?: number | string;
18
+ readonly priority?: number;
19
+ }): Promise<{
20
+ readonly sendStatus: "succeeded" | "failed";
21
+ readonly eventId: string;
22
+ }>;
23
+ }
24
+ //#endregion
25
+ //#region src/mq.d.ts
26
+ /**
27
+ * A Fedify queue message wrapped for Netlify Async Workloads.
28
+ *
29
+ * @since 2.4.0
30
+ */
31
+ interface NetlifyQueueEventData {
32
+ readonly message: Message;
33
+ readonly orderingKey?: string;
34
+ readonly orderingSequence?: number;
35
+ }
36
+ /**
37
+ * Options for {@link NetlifyMessageQueue}.
38
+ *
39
+ * @since 2.4.0
40
+ */
41
+ interface NetlifyMessageQueueOptions {
42
+ /** A Netlify Async Workloads client. */
43
+ readonly client: NetlifyAsyncWorkloadsClient;
44
+ /**
45
+ * The Async Workloads event name. It must match the event configured for
46
+ * the workload function. `"fedify:queue"` by default.
47
+ * @default `"fedify:queue"`
48
+ */
49
+ readonly eventName?: string;
50
+ /**
51
+ * A CAS-capable key–value store used to serialize messages that have the
52
+ * same ordering key. Required when an `orderingKey` is enqueued.
53
+ */
54
+ readonly orderingKv?: KvStore;
55
+ /**
56
+ * The durable sleep interval used while an earlier ordered message is still
57
+ * running. Five seconds by default.
58
+ * @default `{ seconds: 5 }`
59
+ */
60
+ readonly orderingRetryDelay?: Temporal.Duration | Temporal.DurationLike;
61
+ }
62
+ /**
63
+ * An error raised when an event send is not acknowledged.
64
+ *
65
+ * For an ordered event, {@link orderingSequence} remains reserved because the
66
+ * router may already have accepted the event. It can be passed to
67
+ * {@link NetlifyMessageQueue.skipOrderingSequence} only after an operator has
68
+ * confirmed that the event cannot still be delivered.
69
+ *
70
+ * @since 2.4.0
71
+ */
72
+ declare class NetlifyMessageQueueSendError extends Error {
73
+ readonly eventName: string;
74
+ readonly eventId?: string;
75
+ readonly orderingKey?: string;
76
+ readonly orderingSequence?: number;
77
+ /** Creates a queue send error. @internal */
78
+ constructor(options: {
79
+ readonly eventName: string;
80
+ readonly eventId?: string;
81
+ readonly orderingKey?: string;
82
+ readonly orderingSequence?: number;
83
+ readonly cause?: unknown;
84
+ });
85
+ }
86
+ /**
87
+ * A message queue that publishes Fedify jobs to Netlify Async Workloads.
88
+ *
89
+ * Async Workloads invokes a separate function for each event, so this queue
90
+ * cannot consume messages through {@link listen}. Use
91
+ * {@link createNetlifyQueueHandler} in a Netlify Function and set Fedify's
92
+ * `manuallyStartQueue` option to `true`.
93
+ *
94
+ * @since 2.4.0
95
+ */
96
+ declare class NetlifyMessageQueue implements MessageQueue {
97
+ #private;
98
+ readonly eventName: string;
99
+ readonly nativeRetrial = true;
100
+ readonly nativeDeduplication = false;
101
+ /** Creates a Netlify Async Workloads-backed message queue. */
102
+ constructor(options: NetlifyMessageQueueOptions);
103
+ /** {@inheritDoc MessageQueue.enqueue} */
104
+ enqueue(message: Message, options?: MessageQueueEnqueueOptions): Promise<void>;
105
+ /**
106
+ * Skips a reserved ordering sequence that can no longer be processed.
107
+ *
108
+ * Use this only after confirming that the corresponding Async Workloads
109
+ * event is permanently dead-lettered or was never accepted. Skipping an
110
+ * event that can still be delivered causes that event to be ignored.
111
+ *
112
+ * @param orderingKey The ordering key of the blocked sequence.
113
+ * @param sequence The sequence to skip.
114
+ */
115
+ skipOrderingSequence(orderingKey: string, sequence: number): Promise<void>;
116
+ /** {@inheritDoc MessageQueue.enqueueMany} */
117
+ enqueueMany(messages: readonly Message[], options?: MessageQueueEnqueueOptions): Promise<void>;
118
+ /**
119
+ * This operation is unsupported because Netlify invokes workload functions
120
+ * for queued events.
121
+ */
122
+ listen(_handler: (message: Message) => Promise<void> | void, _options?: MessageQueueListenOptions): Promise<void>;
123
+ }
124
+ //#endregion
125
+ //#region src/handler.d.ts
126
+ /**
127
+ * The Async Workloads event emitted by {@link NetlifyMessageQueue}.
128
+ *
129
+ * @since 2.4.0
130
+ */
131
+ interface NetlifyQueueEvent extends CustomAsyncWorkloadEvent {
132
+ readonly eventName: string;
133
+ readonly eventData: NetlifyQueueEventData;
134
+ }
135
+ /**
136
+ * Options for {@link createNetlifyQueueHandler}.
137
+ *
138
+ * @typeParam TContextData The context data passed to Fedify dispatchers.
139
+ * @since 2.4.0
140
+ */
141
+ interface NetlifyQueueHandlerOptions<TContextData> {
142
+ /** The queue that receives events for this handler. */
143
+ readonly queue: NetlifyMessageQueue;
144
+ /**
145
+ * Creates the federation for a workload invocation. The factory is called
146
+ * once per event so resources do not have to survive between invocations.
147
+ */
148
+ readonly federation: (event: AsyncWorkloadEvent<NetlifyQueueEvent>) => Federation<TContextData> | Promise<Federation<TContextData>>;
149
+ /**
150
+ * Creates the context data passed to `Federation.processQueuedTask()`.
151
+ * When omitted, `undefined` is used.
152
+ */
153
+ readonly contextData?: (event: AsyncWorkloadEvent<NetlifyQueueEvent>) => TContextData | Promise<TContextData>;
154
+ /**
155
+ * The workload's configured retry count. This must equal
156
+ * `asyncWorkloadConfig.maxRetries` so a permanently failed ordered event can
157
+ * release its sequence. Four by default, matching Async Workloads.
158
+ * @default 4
159
+ */
160
+ readonly maxRetries?: number;
161
+ }
162
+ type WorkloadFunction = ReturnType<typeof asyncWorkloadFn<NetlifyQueueEvent>>;
163
+ /**
164
+ * Creates a Netlify Async Workloads function that processes Fedify jobs.
165
+ *
166
+ * Export the returned function as the default export of a file under
167
+ * *netlify/functions/*. The workload's `asyncWorkloadConfig.events` must
168
+ * contain the associated queue's {@link NetlifyMessageQueue.eventName}.
169
+ *
170
+ * @typeParam TContextData The context data passed to Fedify dispatchers.
171
+ * @param options The workload handler options.
172
+ * @returns A function produced by Netlify's `asyncWorkloadFn()`.
173
+ * @since 2.4.0
174
+ */
175
+ declare function createNetlifyQueueHandler<TContextData>(options: NetlifyQueueHandlerOptions<TContextData>): WorkloadFunction;
176
+ //#endregion
177
+ export { type NetlifyAsyncWorkloadsClient, NetlifyMessageQueue, type NetlifyMessageQueueOptions, NetlifyMessageQueueSendError, type NetlifyQueueEvent, type NetlifyQueueEventData, type NetlifyQueueHandlerOptions, createNetlifyQueueHandler };
package/dist/mod.js ADDED
@@ -0,0 +1,297 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { ErrorDoNotRetry, asyncWorkloadFn } from "@netlify/async-workloads";
3
+ //#region src/mq.ts
4
+ const defaultEventName = "fedify:queue";
5
+ const defaultOrderingRetryDelay = Temporal.Duration.from({ seconds: 5 });
6
+ /**
7
+ * An error raised when an event send is not acknowledged.
8
+ *
9
+ * For an ordered event, {@link orderingSequence} remains reserved because the
10
+ * router may already have accepted the event. It can be passed to
11
+ * {@link NetlifyMessageQueue.skipOrderingSequence} only after an operator has
12
+ * confirmed that the event cannot still be delivered.
13
+ *
14
+ * @since 2.4.0
15
+ */
16
+ var NetlifyMessageQueueSendError = class extends Error {
17
+ eventName;
18
+ eventId;
19
+ orderingKey;
20
+ orderingSequence;
21
+ /** Creates a queue send error. @internal */
22
+ constructor(options) {
23
+ const ordering = options.orderingKey == null || options.orderingSequence == null ? "" : `; ordering key ${options.orderingKey}, sequence ` + options.orderingSequence;
24
+ super(`Failed to send Netlify Async Workloads event ${options.eventId ?? "without an acknowledgement"} for ${options.eventName}${ordering}.`, { cause: options.cause });
25
+ this.name = "NetlifyMessageQueueSendError";
26
+ this.eventName = options.eventName;
27
+ this.eventId = options.eventId;
28
+ this.orderingKey = options.orderingKey;
29
+ this.orderingSequence = options.orderingSequence;
30
+ }
31
+ };
32
+ const orderingOptions = /* @__PURE__ */ new WeakMap();
33
+ function duration(value, defaultValue, name) {
34
+ const result = value == null ? defaultValue : Temporal.Duration.from(value);
35
+ const milliseconds = result.total("milliseconds");
36
+ if (!Number.isFinite(milliseconds) || milliseconds <= 0) throw new RangeError(`${name} must be a positive finite duration.`);
37
+ return result;
38
+ }
39
+ function requireCas(kv) {
40
+ if (kv == null || kv.cas == null) throw new TypeError("Messages with an orderingKey require orderingKv with a cas() method.");
41
+ return kv;
42
+ }
43
+ /**
44
+ * Gets the queue's ordering configuration for the workload handler.
45
+ * @internal
46
+ */
47
+ function getOrderingOptions(queue) {
48
+ const options = orderingOptions.get(queue);
49
+ if (options == null) throw new TypeError("Invalid NetlifyMessageQueue.");
50
+ return options;
51
+ }
52
+ /**
53
+ * Ensures that a queue can accept the supplied ordering key.
54
+ * @internal
55
+ */
56
+ function getOrderingKv(queue, orderingKey) {
57
+ if (orderingKey.length < 1) throw new TypeError("orderingKey must not be empty.");
58
+ return requireCas(getOrderingOptions(queue).kv);
59
+ }
60
+ function getOrderingStateKey(orderingKey) {
61
+ return [
62
+ "fedify",
63
+ "netlify",
64
+ "ordering",
65
+ orderingKey
66
+ ];
67
+ }
68
+ function compactOrderingState(state) {
69
+ const cancelled = new Set(state.cancelledSequences);
70
+ let completedSequence = state.completedSequence;
71
+ while (cancelled.delete(completedSequence + 1)) completedSequence++;
72
+ return {
73
+ nextSequence: state.nextSequence,
74
+ completedSequence,
75
+ cancelledSequences: [...cancelled].sort((a, b) => a - b)
76
+ };
77
+ }
78
+ function validateOrderingState(value) {
79
+ if (value === void 0) return {
80
+ nextSequence: 1,
81
+ completedSequence: 0,
82
+ cancelledSequences: []
83
+ };
84
+ if (typeof value !== "object" || value == null) throw new TypeError("Invalid Netlify queue ordering state.");
85
+ const nextSequence = "nextSequence" in value ? value.nextSequence : void 0;
86
+ const completedSequence = "completedSequence" in value ? value.completedSequence : void 0;
87
+ const cancelledSequences = "cancelledSequences" in value ? value.cancelledSequences : void 0;
88
+ if (typeof nextSequence !== "number" || !Number.isSafeInteger(nextSequence) || nextSequence < 1 || typeof completedSequence !== "number" || !Number.isSafeInteger(completedSequence) || completedSequence < 0 || !Array.isArray(cancelledSequences) || !cancelledSequences.every((sequence) => Number.isSafeInteger(sequence) && sequence > 0)) throw new TypeError("Invalid Netlify queue ordering state.");
89
+ return {
90
+ nextSequence,
91
+ completedSequence,
92
+ cancelledSequences
93
+ };
94
+ }
95
+ async function updateOrderingState(queue, orderingKey, update) {
96
+ const kv = getOrderingKv(queue, orderingKey);
97
+ const key = getOrderingStateKey(orderingKey);
98
+ while (true) {
99
+ const stored = await kv.get(key);
100
+ const [updated, result] = update(validateOrderingState(stored));
101
+ if (await kv.cas(key, stored, updated)) return result;
102
+ }
103
+ }
104
+ async function reserveOrderingSequence(queue, orderingKey) {
105
+ return await updateOrderingState(queue, orderingKey, (state) => [{
106
+ ...state,
107
+ nextSequence: state.nextSequence + 1
108
+ }, state.nextSequence]);
109
+ }
110
+ async function cancelOrderingSequence(queue, orderingKey, sequence) {
111
+ await updateOrderingState(queue, orderingKey, (state) => {
112
+ if (sequence >= state.nextSequence) throw new RangeError(`Netlify queue ordering sequence ${sequence} has not been reserved.`);
113
+ if (sequence <= state.completedSequence) return [state, void 0];
114
+ return [compactOrderingState({
115
+ ...state,
116
+ cancelledSequences: [...state.cancelledSequences, sequence]
117
+ }), void 0];
118
+ });
119
+ }
120
+ /** Gets the last completed sequence for an ordering key. @internal */
121
+ async function getCompletedOrderingSequence(queue, orderingKey) {
122
+ return validateOrderingState(await getOrderingKv(queue, orderingKey).get(getOrderingStateKey(orderingKey))).completedSequence;
123
+ }
124
+ /** Marks an ordered message as completed. @internal */
125
+ async function completeOrderingSequence(queue, orderingKey, sequence) {
126
+ await updateOrderingState(queue, orderingKey, (state) => {
127
+ if (state.completedSequence >= sequence) return [state, void 0];
128
+ if (state.completedSequence !== sequence - 1) throw new Error(`Cannot complete Netlify queue ordering sequence ${sequence} after ${state.completedSequence}.`);
129
+ return [compactOrderingState({
130
+ ...state,
131
+ completedSequence: sequence
132
+ }), void 0];
133
+ });
134
+ }
135
+ /**
136
+ * A message queue that publishes Fedify jobs to Netlify Async Workloads.
137
+ *
138
+ * Async Workloads invokes a separate function for each event, so this queue
139
+ * cannot consume messages through {@link listen}. Use
140
+ * {@link createNetlifyQueueHandler} in a Netlify Function and set Fedify's
141
+ * `manuallyStartQueue` option to `true`.
142
+ *
143
+ * @since 2.4.0
144
+ */
145
+ var NetlifyMessageQueue = class {
146
+ eventName;
147
+ nativeRetrial = true;
148
+ nativeDeduplication = false;
149
+ #client;
150
+ /** Creates a Netlify Async Workloads-backed message queue. */
151
+ constructor(options) {
152
+ const eventName = options.eventName ?? defaultEventName;
153
+ if (eventName.trim().length < 1) throw new TypeError("eventName must not be empty.");
154
+ this.eventName = eventName;
155
+ this.#client = options.client;
156
+ orderingOptions.set(this, {
157
+ kv: options.orderingKv,
158
+ retryDelay: duration(options.orderingRetryDelay, defaultOrderingRetryDelay, "orderingRetryDelay")
159
+ });
160
+ }
161
+ /** {@inheritDoc MessageQueue.enqueue} */
162
+ async enqueue(message, options) {
163
+ const delay = options?.delay?.total("milliseconds");
164
+ if (delay != null && (!Number.isFinite(delay) || delay < 0)) throw new RangeError("delay must be a non-negative finite duration.");
165
+ const orderingKey = options?.orderingKey;
166
+ const orderingSequence = orderingKey == null ? void 0 : await reserveOrderingSequence(this, orderingKey);
167
+ let result;
168
+ try {
169
+ result = await this.#client.send(this.eventName, {
170
+ data: {
171
+ message,
172
+ orderingKey,
173
+ orderingSequence
174
+ },
175
+ ...delay == null ? {} : { delayUntil: Date.now() + delay }
176
+ });
177
+ } catch (cause) {
178
+ throw new NetlifyMessageQueueSendError({
179
+ eventName: this.eventName,
180
+ orderingKey,
181
+ orderingSequence,
182
+ cause
183
+ });
184
+ }
185
+ if (result.sendStatus !== "succeeded") throw new NetlifyMessageQueueSendError({
186
+ eventName: this.eventName,
187
+ eventId: result.eventId,
188
+ orderingKey,
189
+ orderingSequence
190
+ });
191
+ }
192
+ /**
193
+ * Skips a reserved ordering sequence that can no longer be processed.
194
+ *
195
+ * Use this only after confirming that the corresponding Async Workloads
196
+ * event is permanently dead-lettered or was never accepted. Skipping an
197
+ * event that can still be delivered causes that event to be ignored.
198
+ *
199
+ * @param orderingKey The ordering key of the blocked sequence.
200
+ * @param sequence The sequence to skip.
201
+ */
202
+ async skipOrderingSequence(orderingKey, sequence) {
203
+ if (!Number.isSafeInteger(sequence) || sequence < 1) throw new RangeError("sequence must be a positive safe integer.");
204
+ await cancelOrderingSequence(this, orderingKey, sequence);
205
+ }
206
+ /** {@inheritDoc MessageQueue.enqueueMany} */
207
+ async enqueueMany(messages, options) {
208
+ if (options?.orderingKey == null) await Promise.all(messages.map((message) => this.enqueue(message, options)));
209
+ else for (const message of messages) await this.enqueue(message, options);
210
+ }
211
+ /**
212
+ * This operation is unsupported because Netlify invokes workload functions
213
+ * for queued events.
214
+ */
215
+ listen(_handler, _options) {
216
+ throw new TypeError("NetlifyMessageQueue.listen() is unsupported; use createNetlifyQueueHandler() and set manuallyStartQueue to true.");
217
+ }
218
+ };
219
+ //#endregion
220
+ //#region src/handler.ts
221
+ const messageTypes = new Set([
222
+ "fanout",
223
+ "inbox",
224
+ "outbox",
225
+ "task"
226
+ ]);
227
+ function isObject(value) {
228
+ return typeof value === "object" && value != null && !Array.isArray(value);
229
+ }
230
+ function decodeEventData(value) {
231
+ if (!isObject(value) || !isObject(value.message)) throw new ErrorDoNotRetry("Invalid Fedify queue event envelope.");
232
+ if (typeof value.message.type !== "string" || !messageTypes.has(value.message.type)) throw new ErrorDoNotRetry("Invalid Fedify queue message type.");
233
+ if (value.orderingKey !== void 0 && (typeof value.orderingKey !== "string" || value.orderingKey.length < 1)) throw new ErrorDoNotRetry("Invalid Fedify queue ordering key.");
234
+ const orderingSequence = value.orderingSequence;
235
+ if (orderingSequence !== void 0 && (typeof orderingSequence !== "number" || !Number.isSafeInteger(orderingSequence) || orderingSequence < 1)) throw new ErrorDoNotRetry("Invalid Fedify queue ordering sequence.");
236
+ if (value.orderingKey === void 0 !== (orderingSequence === void 0)) throw new ErrorDoNotRetry("Incomplete Fedify queue ordering metadata.");
237
+ return {
238
+ message: value.message,
239
+ orderingKey: value.orderingKey,
240
+ orderingSequence
241
+ };
242
+ }
243
+ /**
244
+ * Creates the event callback that processes one Netlify queue event.
245
+ * @internal
246
+ */
247
+ function createNetlifyQueueEventHandler(options) {
248
+ const maxRetries = options.maxRetries ?? 4;
249
+ if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new RangeError("maxRetries must be a non-negative safe integer.");
250
+ return async (event) => {
251
+ const data = decodeEventData(event.eventData);
252
+ const ordering = getOrderingOptions(options.queue);
253
+ if (data.orderingKey != null && data.orderingSequence != null) {
254
+ let wait = 0;
255
+ while (true) {
256
+ const completed = await getCompletedOrderingSequence(options.queue, data.orderingKey);
257
+ if (completed >= data.orderingSequence) return;
258
+ if (completed === data.orderingSequence - 1) break;
259
+ await event.step.sleep(`fedify-ordering-wait-${wait++}`, ordering.retryDelay.total("milliseconds"));
260
+ }
261
+ }
262
+ try {
263
+ const federation = await options.federation(event);
264
+ const contextData = options.contextData == null ? void 0 : await options.contextData(event);
265
+ await federation.processQueuedTask(contextData, data.message);
266
+ } catch (error) {
267
+ if (data.orderingKey != null && data.orderingSequence != null && (error instanceof ErrorDoNotRetry || event.attempt >= maxRetries)) await options.queue.skipOrderingSequence(data.orderingKey, data.orderingSequence);
268
+ throw error;
269
+ }
270
+ if (data.orderingKey != null && data.orderingSequence != null) await completeOrderingSequence(options.queue, data.orderingKey, data.orderingSequence);
271
+ };
272
+ }
273
+ /**
274
+ * Wraps a queue event callback. This is exposed for cross-runtime tests; use
275
+ * {@link createNetlifyQueueHandler} in applications.
276
+ * @internal
277
+ */
278
+ function createNetlifyQueueHandlerWith(wrapper, options) {
279
+ return wrapper(createNetlifyQueueEventHandler(options));
280
+ }
281
+ /**
282
+ * Creates a Netlify Async Workloads function that processes Fedify jobs.
283
+ *
284
+ * Export the returned function as the default export of a file under
285
+ * *netlify/functions/*. The workload's `asyncWorkloadConfig.events` must
286
+ * contain the associated queue's {@link NetlifyMessageQueue.eventName}.
287
+ *
288
+ * @typeParam TContextData The context data passed to Fedify dispatchers.
289
+ * @param options The workload handler options.
290
+ * @returns A function produced by Netlify's `asyncWorkloadFn()`.
291
+ * @since 2.4.0
292
+ */
293
+ function createNetlifyQueueHandler(options) {
294
+ return createNetlifyQueueHandlerWith(asyncWorkloadFn, options);
295
+ }
296
+ //#endregion
297
+ export { NetlifyMessageQueue, NetlifyMessageQueueSendError, createNetlifyQueueHandler };