@fedify/netlify 2.4.0-dev.0 → 2.4.0-pr.934.40

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 CHANGED
@@ -38,6 +38,17 @@ yarn add @fedify/netlify @netlify/async-workloads # Yarn
38
38
  bun add @fedify/netlify @netlify/async-workloads # Bun
39
39
  ~~~~
40
40
 
41
+ The PostgreSQL-backed `orderingKv` example below also needs
42
+ `@fedify/postgres`, `@netlify/database`, and `postgres`:
43
+
44
+ ~~~~ sh
45
+ deno add jsr:@fedify/postgres npm:@netlify/database npm:postgres # Deno
46
+ npm add @fedify/postgres @netlify/database postgres # npm
47
+ pnpm add @fedify/postgres @netlify/database postgres # pnpm
48
+ yarn add @fedify/postgres @netlify/database postgres # Yarn
49
+ bun add @fedify/postgres @netlify/database postgres # Bun
50
+ ~~~~
51
+
41
52
 
42
53
  Usage
43
54
  -----
@@ -137,6 +148,13 @@ table by default; do not pass `unlogged: true` for `orderingKv`.
137
148
  Netlify limits an event payload to 500 KB. Fedify messages, including any
138
149
  embedded activity, must remain below that limit.
139
150
 
151
+ `enqueueMany()` sends one Async Workloads event per message because Netlify's
152
+ public client has no batch operation. The queue therefore declares
153
+ `atomicEnqueueMany` as `false`: ordinary batches still send concurrently, but
154
+ Fedify rejects a multi-message `enqueueTaskMany()` call with one
155
+ `deduplicationKey` before sending anything. Such a key requires an atomic
156
+ batch enqueue so a failed partial send cannot produce duplicates on retry.
157
+
140
158
  See the [deployment manual] and the [Netlify Astro example] for a complete
141
159
  setup with Netlify Database.
142
160
 
package/dist/mod.cjs CHANGED
@@ -3617,11 +3617,12 @@ function getOrderingKv(queue, orderingKey) {
3617
3617
  if (orderingKey.length < 1) throw new TypeError("orderingKey must not be empty.");
3618
3618
  return requireCas(getOrderingOptions(queue).kv);
3619
3619
  }
3620
- function getOrderingStateKey(orderingKey) {
3620
+ function getOrderingStateKey(queue, orderingKey) {
3621
3621
  return [
3622
3622
  "fedify",
3623
3623
  "netlify",
3624
3624
  "ordering",
3625
+ queue.eventName.toLowerCase(),
3625
3626
  orderingKey
3626
3627
  ];
3627
3628
  }
@@ -3654,10 +3655,11 @@ function validateOrderingState(value) {
3654
3655
  }
3655
3656
  async function updateOrderingState(queue, orderingKey, update) {
3656
3657
  const kv = getOrderingKv(queue, orderingKey);
3657
- const key = getOrderingStateKey(orderingKey);
3658
+ const key = getOrderingStateKey(queue, orderingKey);
3658
3659
  while (true) {
3659
3660
  const stored = await kv.get(key);
3660
3661
  const [updated, result] = update(validateOrderingState(stored));
3662
+ if (updated === void 0) return result;
3661
3663
  if (await kv.cas(key, stored, updated)) return result;
3662
3664
  }
3663
3665
  }
@@ -3670,7 +3672,7 @@ async function reserveOrderingSequence(queue, orderingKey) {
3670
3672
  async function cancelOrderingSequence(queue, orderingKey, sequence) {
3671
3673
  await updateOrderingState(queue, orderingKey, (state) => {
3672
3674
  if (sequence >= state.nextSequence) throw new RangeError(`Netlify queue ordering sequence ${sequence} has not been reserved.`);
3673
- if (sequence <= state.completedSequence) return [state, void 0];
3675
+ if (sequence <= state.completedSequence) return [void 0, void 0];
3674
3676
  return [compactOrderingState({
3675
3677
  ...state,
3676
3678
  cancelledSequences: [...state.cancelledSequences, sequence]
@@ -3679,12 +3681,12 @@ async function cancelOrderingSequence(queue, orderingKey, sequence) {
3679
3681
  }
3680
3682
  /** Gets the last completed sequence for an ordering key. @internal */
3681
3683
  async function getCompletedOrderingSequence(queue, orderingKey) {
3682
- return validateOrderingState(await getOrderingKv(queue, orderingKey).get(getOrderingStateKey(orderingKey))).completedSequence;
3684
+ return validateOrderingState(await getOrderingKv(queue, orderingKey).get(getOrderingStateKey(queue, orderingKey))).completedSequence;
3683
3685
  }
3684
3686
  /** Marks an ordered message as completed. @internal */
3685
3687
  async function completeOrderingSequence(queue, orderingKey, sequence) {
3686
3688
  await updateOrderingState(queue, orderingKey, (state) => {
3687
- if (state.completedSequence >= sequence) return [state, void 0];
3689
+ if (state.completedSequence >= sequence) return [void 0, void 0];
3688
3690
  if (state.completedSequence !== sequence - 1) throw new Error(`Cannot complete Netlify queue ordering sequence ${sequence} after ${state.completedSequence}.`);
3689
3691
  return [compactOrderingState({
3690
3692
  ...state,
@@ -3706,6 +3708,8 @@ var NetlifyMessageQueue = class {
3706
3708
  eventName;
3707
3709
  nativeRetrial = true;
3708
3710
  nativeDeduplication = false;
3711
+ /** {@inheritDoc MessageQueue.atomicEnqueueMany} */
3712
+ atomicEnqueueMany = false;
3709
3713
  #client;
3710
3714
  /** Creates a Netlify Async Workloads-backed message queue. */
3711
3715
  constructor(options) {
@@ -3787,19 +3791,25 @@ const messageTypes = new Set([
3787
3791
  function isObject(value) {
3788
3792
  return typeof value === "object" && value != null && !Array.isArray(value);
3789
3793
  }
3790
- function decodeEventData(value) {
3791
- if (!isObject(value) || !isObject(value.message)) throw new _netlify_async_workloads.ErrorDoNotRetry("Invalid Fedify queue event envelope.");
3792
- if (typeof value.message.type !== "string" || !messageTypes.has(value.message.type)) throw new _netlify_async_workloads.ErrorDoNotRetry("Invalid Fedify queue message type.");
3794
+ function decodeOrderingMetadata(value) {
3795
+ if (!isObject(value)) throw new _netlify_async_workloads.ErrorDoNotRetry("Invalid Fedify queue event envelope.");
3793
3796
  if (value.orderingKey !== void 0 && (typeof value.orderingKey !== "string" || value.orderingKey.length < 1)) throw new _netlify_async_workloads.ErrorDoNotRetry("Invalid Fedify queue ordering key.");
3794
3797
  const orderingSequence = value.orderingSequence;
3795
3798
  if (orderingSequence !== void 0 && (typeof orderingSequence !== "number" || !Number.isSafeInteger(orderingSequence) || orderingSequence < 1)) throw new _netlify_async_workloads.ErrorDoNotRetry("Invalid Fedify queue ordering sequence.");
3796
3799
  if (value.orderingKey === void 0 !== (orderingSequence === void 0)) throw new _netlify_async_workloads.ErrorDoNotRetry("Incomplete Fedify queue ordering metadata.");
3797
3800
  return {
3798
- message: value.message,
3799
3801
  orderingKey: value.orderingKey,
3800
3802
  orderingSequence
3801
3803
  };
3802
3804
  }
3805
+ function decodeEventData(value, ordering) {
3806
+ if (!isObject(value) || !isObject(value.message)) throw new _netlify_async_workloads.ErrorDoNotRetry("Invalid Fedify queue event envelope.");
3807
+ if (typeof value.message.type !== "string" || !messageTypes.has(value.message.type)) throw new _netlify_async_workloads.ErrorDoNotRetry("Invalid Fedify queue message type.");
3808
+ return {
3809
+ message: value.message,
3810
+ ...ordering
3811
+ };
3812
+ }
3803
3813
  /**
3804
3814
  * Creates the event callback that processes one Netlify queue event.
3805
3815
  * @internal
@@ -3808,7 +3818,14 @@ function createNetlifyQueueEventHandler(options) {
3808
3818
  const maxRetries = options.maxRetries ?? 4;
3809
3819
  if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new RangeError("maxRetries must be a non-negative safe integer.");
3810
3820
  return async (event) => {
3811
- const data = decodeEventData(event.eventData);
3821
+ const orderingMetadata = decodeOrderingMetadata(event.eventData);
3822
+ let data;
3823
+ try {
3824
+ data = decodeEventData(event.eventData, orderingMetadata);
3825
+ } catch (error) {
3826
+ if (error instanceof _netlify_async_workloads.ErrorDoNotRetry && orderingMetadata.orderingKey != null && orderingMetadata.orderingSequence != null) await options.queue.skipOrderingSequence(orderingMetadata.orderingKey, orderingMetadata.orderingSequence);
3827
+ throw error;
3828
+ }
3812
3829
  const ordering = getOrderingOptions(options.queue);
3813
3830
  if (data.orderingKey != null && data.orderingSequence != null) {
3814
3831
  let wait = 0;
@@ -3820,9 +3837,16 @@ function createNetlifyQueueEventHandler(options) {
3820
3837
  }
3821
3838
  }
3822
3839
  try {
3823
- const federation = await options.federation(event);
3824
- const contextData = options.contextData == null ? void 0 : await options.contextData(event);
3825
- await federation.processQueuedTask(contextData, data.message);
3840
+ const process = async () => {
3841
+ const federation = await options.federation(event);
3842
+ const contextData = options.contextData == null ? void 0 : await options.contextData(event);
3843
+ await federation.processQueuedTask(contextData, data.message);
3844
+ };
3845
+ if (data.orderingKey != null && data.orderingSequence != null) await event.step.run("fedify-process-queued-task", async () => {
3846
+ await process();
3847
+ return null;
3848
+ });
3849
+ else await process();
3826
3850
  } catch (error) {
3827
3851
  if (data.orderingKey != null && data.orderingSequence != null && (error instanceof _netlify_async_workloads.ErrorDoNotRetry || event.attempt >= maxRetries)) await options.queue.skipOrderingSequence(data.orderingKey, data.orderingSequence);
3828
3852
  throw error;
package/dist/mod.d.cts CHANGED
@@ -98,6 +98,8 @@ declare class NetlifyMessageQueue implements MessageQueue {
98
98
  readonly eventName: string;
99
99
  readonly nativeRetrial = true;
100
100
  readonly nativeDeduplication = false;
101
+ /** {@inheritDoc MessageQueue.atomicEnqueueMany} */
102
+ readonly atomicEnqueueMany = false;
101
103
  /** Creates a Netlify Async Workloads-backed message queue. */
102
104
  constructor(options: NetlifyMessageQueueOptions);
103
105
  /** {@inheritDoc MessageQueue.enqueue} */
package/dist/mod.d.ts CHANGED
@@ -98,6 +98,8 @@ declare class NetlifyMessageQueue implements MessageQueue {
98
98
  readonly eventName: string;
99
99
  readonly nativeRetrial = true;
100
100
  readonly nativeDeduplication = false;
101
+ /** {@inheritDoc MessageQueue.atomicEnqueueMany} */
102
+ readonly atomicEnqueueMany = false;
101
103
  /** Creates a Netlify Async Workloads-backed message queue. */
102
104
  constructor(options: NetlifyMessageQueueOptions);
103
105
  /** {@inheritDoc MessageQueue.enqueue} */
package/dist/mod.js CHANGED
@@ -57,11 +57,12 @@ function getOrderingKv(queue, orderingKey) {
57
57
  if (orderingKey.length < 1) throw new TypeError("orderingKey must not be empty.");
58
58
  return requireCas(getOrderingOptions(queue).kv);
59
59
  }
60
- function getOrderingStateKey(orderingKey) {
60
+ function getOrderingStateKey(queue, orderingKey) {
61
61
  return [
62
62
  "fedify",
63
63
  "netlify",
64
64
  "ordering",
65
+ queue.eventName.toLowerCase(),
65
66
  orderingKey
66
67
  ];
67
68
  }
@@ -94,10 +95,11 @@ function validateOrderingState(value) {
94
95
  }
95
96
  async function updateOrderingState(queue, orderingKey, update) {
96
97
  const kv = getOrderingKv(queue, orderingKey);
97
- const key = getOrderingStateKey(orderingKey);
98
+ const key = getOrderingStateKey(queue, orderingKey);
98
99
  while (true) {
99
100
  const stored = await kv.get(key);
100
101
  const [updated, result] = update(validateOrderingState(stored));
102
+ if (updated === void 0) return result;
101
103
  if (await kv.cas(key, stored, updated)) return result;
102
104
  }
103
105
  }
@@ -110,7 +112,7 @@ async function reserveOrderingSequence(queue, orderingKey) {
110
112
  async function cancelOrderingSequence(queue, orderingKey, sequence) {
111
113
  await updateOrderingState(queue, orderingKey, (state) => {
112
114
  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];
115
+ if (sequence <= state.completedSequence) return [void 0, void 0];
114
116
  return [compactOrderingState({
115
117
  ...state,
116
118
  cancelledSequences: [...state.cancelledSequences, sequence]
@@ -119,12 +121,12 @@ async function cancelOrderingSequence(queue, orderingKey, sequence) {
119
121
  }
120
122
  /** Gets the last completed sequence for an ordering key. @internal */
121
123
  async function getCompletedOrderingSequence(queue, orderingKey) {
122
- return validateOrderingState(await getOrderingKv(queue, orderingKey).get(getOrderingStateKey(orderingKey))).completedSequence;
124
+ return validateOrderingState(await getOrderingKv(queue, orderingKey).get(getOrderingStateKey(queue, orderingKey))).completedSequence;
123
125
  }
124
126
  /** Marks an ordered message as completed. @internal */
125
127
  async function completeOrderingSequence(queue, orderingKey, sequence) {
126
128
  await updateOrderingState(queue, orderingKey, (state) => {
127
- if (state.completedSequence >= sequence) return [state, void 0];
129
+ if (state.completedSequence >= sequence) return [void 0, void 0];
128
130
  if (state.completedSequence !== sequence - 1) throw new Error(`Cannot complete Netlify queue ordering sequence ${sequence} after ${state.completedSequence}.`);
129
131
  return [compactOrderingState({
130
132
  ...state,
@@ -146,6 +148,8 @@ var NetlifyMessageQueue = class {
146
148
  eventName;
147
149
  nativeRetrial = true;
148
150
  nativeDeduplication = false;
151
+ /** {@inheritDoc MessageQueue.atomicEnqueueMany} */
152
+ atomicEnqueueMany = false;
149
153
  #client;
150
154
  /** Creates a Netlify Async Workloads-backed message queue. */
151
155
  constructor(options) {
@@ -227,19 +231,25 @@ const messageTypes = new Set([
227
231
  function isObject(value) {
228
232
  return typeof value === "object" && value != null && !Array.isArray(value);
229
233
  }
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.");
234
+ function decodeOrderingMetadata(value) {
235
+ if (!isObject(value)) throw new ErrorDoNotRetry("Invalid Fedify queue event envelope.");
233
236
  if (value.orderingKey !== void 0 && (typeof value.orderingKey !== "string" || value.orderingKey.length < 1)) throw new ErrorDoNotRetry("Invalid Fedify queue ordering key.");
234
237
  const orderingSequence = value.orderingSequence;
235
238
  if (orderingSequence !== void 0 && (typeof orderingSequence !== "number" || !Number.isSafeInteger(orderingSequence) || orderingSequence < 1)) throw new ErrorDoNotRetry("Invalid Fedify queue ordering sequence.");
236
239
  if (value.orderingKey === void 0 !== (orderingSequence === void 0)) throw new ErrorDoNotRetry("Incomplete Fedify queue ordering metadata.");
237
240
  return {
238
- message: value.message,
239
241
  orderingKey: value.orderingKey,
240
242
  orderingSequence
241
243
  };
242
244
  }
245
+ function decodeEventData(value, ordering) {
246
+ if (!isObject(value) || !isObject(value.message)) throw new ErrorDoNotRetry("Invalid Fedify queue event envelope.");
247
+ if (typeof value.message.type !== "string" || !messageTypes.has(value.message.type)) throw new ErrorDoNotRetry("Invalid Fedify queue message type.");
248
+ return {
249
+ message: value.message,
250
+ ...ordering
251
+ };
252
+ }
243
253
  /**
244
254
  * Creates the event callback that processes one Netlify queue event.
245
255
  * @internal
@@ -248,7 +258,14 @@ function createNetlifyQueueEventHandler(options) {
248
258
  const maxRetries = options.maxRetries ?? 4;
249
259
  if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new RangeError("maxRetries must be a non-negative safe integer.");
250
260
  return async (event) => {
251
- const data = decodeEventData(event.eventData);
261
+ const orderingMetadata = decodeOrderingMetadata(event.eventData);
262
+ let data;
263
+ try {
264
+ data = decodeEventData(event.eventData, orderingMetadata);
265
+ } catch (error) {
266
+ if (error instanceof ErrorDoNotRetry && orderingMetadata.orderingKey != null && orderingMetadata.orderingSequence != null) await options.queue.skipOrderingSequence(orderingMetadata.orderingKey, orderingMetadata.orderingSequence);
267
+ throw error;
268
+ }
252
269
  const ordering = getOrderingOptions(options.queue);
253
270
  if (data.orderingKey != null && data.orderingSequence != null) {
254
271
  let wait = 0;
@@ -260,9 +277,16 @@ function createNetlifyQueueEventHandler(options) {
260
277
  }
261
278
  }
262
279
  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);
280
+ const process = async () => {
281
+ const federation = await options.federation(event);
282
+ const contextData = options.contextData == null ? void 0 : await options.contextData(event);
283
+ await federation.processQueuedTask(contextData, data.message);
284
+ };
285
+ if (data.orderingKey != null && data.orderingSequence != null) await event.step.run("fedify-process-queued-task", async () => {
286
+ await process();
287
+ return null;
288
+ });
289
+ else await process();
266
290
  } catch (error) {
267
291
  if (data.orderingKey != null && data.orderingSequence != null && (error instanceof ErrorDoNotRetry || event.attempt >= maxRetries)) await options.queue.skipOrderingSequence(data.orderingKey, data.orderingSequence);
268
292
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/netlify",
3
- "version": "2.4.0-dev.0",
3
+ "version": "2.4.0-pr.934.40+3331d302",
4
4
  "description": "Integrate Fedify with Netlify Async Workloads",
5
5
  "keywords": [
6
6
  "Fedify",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@netlify/async-workloads": "^0.0.106",
54
- "@fedify/fedify": "^2.4.0"
54
+ "@fedify/fedify": "^2.4.0-pr.934.40+3331d302"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@js-temporal/polyfill": "^0.5.1",
@@ -62,7 +62,7 @@
62
62
  "postgres": "^3.4.7",
63
63
  "tsdown": "^0.22.0",
64
64
  "typescript": "^6.0.0",
65
- "@fedify/postgres": "^2.4.0"
65
+ "@fedify/postgres": "^2.4.0-pr.934.40+3331d302"
66
66
  },
67
67
  "scripts": {
68
68
  "build:self": "tsdown",