@spine-event-engine/server 2.0.0-snapshot.2 → 2.0.0-snapshot.21

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.
Files changed (227) hide show
  1. package/README.md +225 -518
  2. package/REFERENCE.md +498 -49
  3. package/dist/browser/browser-server.d.ts +95 -0
  4. package/dist/browser/browser-server.d.ts.map +1 -0
  5. package/dist/{server → browser}/browser-server.js +93 -7
  6. package/dist/browser/browser-server.js.map +1 -0
  7. package/dist/browser/durable-subscription-bindings.d.ts.map +1 -0
  8. package/dist/browser/durable-subscription-bindings.js.map +1 -0
  9. package/dist/browser/index.d.ts +12 -0
  10. package/dist/browser/index.d.ts.map +1 -0
  11. package/dist/browser/index.js +24 -0
  12. package/dist/browser/index.js.map +1 -0
  13. package/dist/browser/options.d.ts +183 -0
  14. package/dist/browser/options.d.ts.map +1 -0
  15. package/dist/browser/options.js +15 -0
  16. package/dist/browser/options.js.map +1 -0
  17. package/dist/bus/command-bus.d.ts +2 -0
  18. package/dist/bus/command-bus.d.ts.map +1 -1
  19. package/dist/bus/command-bus.js +36 -1
  20. package/dist/bus/command-bus.js.map +1 -1
  21. package/dist/bus/event-dispatcher-registry.d.ts +2 -2
  22. package/dist/bus/event-dispatcher-registry.js +3 -3
  23. package/dist/bus/event-dispatcher-registry.js.map +1 -1
  24. package/dist/context/bounded-context.d.ts +238 -83
  25. package/dist/context/bounded-context.d.ts.map +1 -1
  26. package/dist/context/bounded-context.js +1083 -286
  27. package/dist/context/bounded-context.js.map +1 -1
  28. package/dist/context/effective-tenant.d.ts +9 -0
  29. package/dist/context/effective-tenant.d.ts.map +1 -0
  30. package/dist/context/effective-tenant.js +58 -0
  31. package/dist/context/effective-tenant.js.map +1 -0
  32. package/dist/context/entity-inbox.d.ts.map +1 -1
  33. package/dist/context/entity-inbox.js +24 -17
  34. package/dist/context/entity-inbox.js.map +1 -1
  35. package/dist/context/local-inbox-handoff.d.ts +7 -3
  36. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  37. package/dist/context/local-inbox-handoff.js +32 -26
  38. package/dist/context/local-inbox-handoff.js.map +1 -1
  39. package/dist/context/projection-handoff.d.ts.map +1 -1
  40. package/dist/context/projection-handoff.js +36 -31
  41. package/dist/context/projection-handoff.js.map +1 -1
  42. package/dist/context/tenant-index.d.ts.map +1 -1
  43. package/dist/context/tenant-index.js +72 -2
  44. package/dist/context/tenant-index.js.map +1 -1
  45. package/dist/delivery/delivery-ports.d.ts +14 -4
  46. package/dist/delivery/delivery-ports.d.ts.map +1 -1
  47. package/dist/delivery/delivery.d.ts +19 -21
  48. package/dist/delivery/delivery.d.ts.map +1 -1
  49. package/dist/delivery/delivery.js +541 -158
  50. package/dist/delivery/delivery.js.map +1 -1
  51. package/dist/delivery/inbox-storage.d.ts +12 -3
  52. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  53. package/dist/delivery/inbox-storage.js +160 -67
  54. package/dist/delivery/inbox-storage.js.map +1 -1
  55. package/dist/delivery/inbox.d.ts +9 -0
  56. package/dist/delivery/inbox.d.ts.map +1 -1
  57. package/dist/delivery/inbox.js +11 -0
  58. package/dist/delivery/inbox.js.map +1 -1
  59. package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
  60. package/dist/delivery/sharded-work-registry.js +2 -1
  61. package/dist/delivery/sharded-work-registry.js.map +1 -1
  62. package/dist/entity/entity-metadata.d.ts +14 -5
  63. package/dist/entity/entity-metadata.d.ts.map +1 -1
  64. package/dist/entity/entity-metadata.js +38 -2
  65. package/dist/entity/entity-metadata.js.map +1 -1
  66. package/dist/entity/entity-storage-descriptor.d.ts +25 -5
  67. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  68. package/dist/entity/entity-storage-descriptor.js +69 -23
  69. package/dist/entity/entity-storage-descriptor.js.map +1 -1
  70. package/dist/entity/entity-transaction.d.ts +46 -42
  71. package/dist/entity/entity-transaction.d.ts.map +1 -1
  72. package/dist/entity/entity-transaction.js +130 -35
  73. package/dist/entity/entity-transaction.js.map +1 -1
  74. package/dist/entity/entity.d.ts +243 -98
  75. package/dist/entity/entity.d.ts.map +1 -1
  76. package/dist/entity/entity.js +405 -226
  77. package/dist/entity/entity.js.map +1 -1
  78. package/dist/entity/spec-scanner.d.ts +13 -1
  79. package/dist/entity/spec-scanner.d.ts.map +1 -1
  80. package/dist/entity/spec-scanner.js +20 -3
  81. package/dist/entity/spec-scanner.js.map +1 -1
  82. package/dist/handler/command-registration-readiness.d.ts +6 -6
  83. package/dist/handler/command-registration-readiness.d.ts.map +1 -1
  84. package/dist/handler/command-registration-readiness.js +4 -2
  85. package/dist/handler/command-registration-readiness.js.map +1 -1
  86. package/dist/handler/declared-rejections.d.ts +11 -0
  87. package/dist/handler/declared-rejections.d.ts.map +1 -0
  88. package/dist/handler/declared-rejections.js +34 -0
  89. package/dist/handler/declared-rejections.js.map +1 -0
  90. package/dist/handler/event-registration-readiness.d.ts +13 -56
  91. package/dist/handler/event-registration-readiness.d.ts.map +1 -1
  92. package/dist/handler/event-registration-readiness.js +23 -43
  93. package/dist/handler/event-registration-readiness.js.map +1 -1
  94. package/dist/handler/generated-handler-registry.d.ts +72 -33
  95. package/dist/handler/generated-handler-registry.d.ts.map +1 -1
  96. package/dist/handler/generated-handler-registry.js +421 -43
  97. package/dist/handler/generated-handler-registry.js.map +1 -1
  98. package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
  99. package/dist/handler/generated-registry-discovery.js +2 -4
  100. package/dist/handler/generated-registry-discovery.js.map +1 -1
  101. package/dist/handler/handler-decorators.d.ts +91 -37
  102. package/dist/handler/handler-decorators.d.ts.map +1 -1
  103. package/dist/handler/handler-decorators.js +124 -31
  104. package/dist/handler/handler-decorators.js.map +1 -1
  105. package/dist/handler/handler-metadata.d.ts +105 -60
  106. package/dist/handler/handler-metadata.d.ts.map +1 -1
  107. package/dist/handler/handler-metadata.js +275 -57
  108. package/dist/handler/handler-metadata.js.map +1 -1
  109. package/dist/handler/registration-readiness-metadata.d.ts +4 -0
  110. package/dist/handler/registration-readiness-metadata.d.ts.map +1 -1
  111. package/dist/handler/registration-readiness-metadata.js +84 -2
  112. package/dist/handler/registration-readiness-metadata.js.map +1 -1
  113. package/dist/handler/standalone.d.ts +25 -0
  114. package/dist/handler/standalone.d.ts.map +1 -0
  115. package/dist/handler/standalone.js +38 -0
  116. package/dist/handler/standalone.js.map +1 -0
  117. package/dist/index.d.ts +9 -9
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +3 -3
  120. package/dist/index.js.map +1 -1
  121. package/dist/repository/command-routing.d.ts +63 -10
  122. package/dist/repository/command-routing.d.ts.map +1 -1
  123. package/dist/repository/command-routing.js +41 -8
  124. package/dist/repository/command-routing.js.map +1 -1
  125. package/dist/repository/event-routing.d.ts +64 -9
  126. package/dist/repository/event-routing.d.ts.map +1 -1
  127. package/dist/repository/event-routing.js +42 -7
  128. package/dist/repository/event-routing.js.map +1 -1
  129. package/dist/repository/primitive-id.d.ts +12 -23
  130. package/dist/repository/primitive-id.d.ts.map +1 -1
  131. package/dist/repository/primitive-id.js +2 -27
  132. package/dist/repository/primitive-id.js.map +1 -1
  133. package/dist/repository/repository.d.ts +317 -87
  134. package/dist/repository/repository.d.ts.map +1 -1
  135. package/dist/repository/repository.js +3746 -856
  136. package/dist/repository/repository.js.map +1 -1
  137. package/dist/repository/state-update-routing.d.ts +67 -8
  138. package/dist/repository/state-update-routing.d.ts.map +1 -1
  139. package/dist/repository/state-update-routing.js +45 -6
  140. package/dist/repository/state-update-routing.js.map +1 -1
  141. package/dist/runtime/runtime.js +1 -1
  142. package/dist/runtime/runtime.js.map +1 -1
  143. package/dist/runtime/signal-metadata.d.ts +22 -47
  144. package/dist/runtime/signal-metadata.d.ts.map +1 -1
  145. package/dist/runtime/signal-metadata.js +34 -69
  146. package/dist/runtime/signal-metadata.js.map +1 -1
  147. package/dist/runtime/signal-publisher.d.ts +144 -0
  148. package/dist/runtime/signal-publisher.d.ts.map +1 -0
  149. package/dist/runtime/signal-publisher.js +248 -0
  150. package/dist/runtime/signal-publisher.js.map +1 -0
  151. package/dist/runtime/standalone-handler-runtime.d.ts +130 -0
  152. package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -0
  153. package/dist/runtime/standalone-handler-runtime.js +382 -0
  154. package/dist/runtime/standalone-handler-runtime.js.map +1 -0
  155. package/dist/server/environment-attachment.d.ts.map +1 -1
  156. package/dist/server/environment-attachment.js +2 -1
  157. package/dist/server/environment-attachment.js.map +1 -1
  158. package/dist/server/environment-delivery-worker.d.ts.map +1 -1
  159. package/dist/server/environment-delivery-worker.js +21 -12
  160. package/dist/server/environment-delivery-worker.js.map +1 -1
  161. package/dist/server/managed-server-application.d.ts +4 -1
  162. package/dist/server/managed-server-application.d.ts.map +1 -1
  163. package/dist/server/managed-server-application.js +2 -2
  164. package/dist/server/managed-server-application.js.map +1 -1
  165. package/dist/server/node-coordinator.js +2 -2
  166. package/dist/server/node-coordinator.js.map +1 -1
  167. package/dist/server/server-log.d.ts.map +1 -1
  168. package/dist/server/server-log.js +4 -0
  169. package/dist/server/server-log.js.map +1 -1
  170. package/dist/server/server.d.ts +9 -177
  171. package/dist/server/server.d.ts.map +1 -1
  172. package/dist/server/server.js +428 -178
  173. package/dist/server/server.js.map +1 -1
  174. package/dist/services/query-reader.d.ts +19 -0
  175. package/dist/services/query-reader.d.ts.map +1 -0
  176. package/dist/services/query-reader.js +43 -0
  177. package/dist/services/query-reader.js.map +1 -0
  178. package/dist/services/registered-targets.d.ts +73 -0
  179. package/dist/services/registered-targets.d.ts.map +1 -0
  180. package/dist/services/registered-targets.js +127 -0
  181. package/dist/services/registered-targets.js.map +1 -0
  182. package/dist/services/spine-services.d.ts +26 -12
  183. package/dist/services/spine-services.d.ts.map +1 -1
  184. package/dist/services/spine-services.js +1047 -969
  185. package/dist/services/spine-services.js.map +1 -1
  186. package/dist/spi/delivery.d.ts +5 -0
  187. package/dist/spi/delivery.d.ts.map +1 -0
  188. package/dist/spi/delivery.js +18 -0
  189. package/dist/spi/delivery.js.map +1 -0
  190. package/dist/spi/handler-registry.d.ts +8 -0
  191. package/dist/spi/handler-registry.d.ts.map +1 -0
  192. package/dist/spi/handler-registry.js +15 -0
  193. package/dist/spi/handler-registry.js.map +1 -0
  194. package/dist/stand/stand.d.ts +73 -1
  195. package/dist/stand/stand.d.ts.map +1 -1
  196. package/dist/stand/stand.js +221 -48
  197. package/dist/stand/stand.js.map +1 -1
  198. package/dist/stand/subscription-observer.d.ts.map +1 -1
  199. package/dist/stand/subscription-observer.js +5 -12
  200. package/dist/stand/subscription-observer.js.map +1 -1
  201. package/dist/stand/subscription-registry.js +1 -1
  202. package/dist/stand/subscription-registry.js.map +1 -1
  203. package/dist/testing/index.d.ts +25 -3
  204. package/dist/testing/index.d.ts.map +1 -1
  205. package/dist/testing/index.js +23 -3
  206. package/dist/testing/index.js.map +1 -1
  207. package/dist/tsconfig.tsbuildinfo +1 -1
  208. package/package.json +33 -23
  209. package/dist/handler/build-time-handler-analyzer.d.ts +0 -150
  210. package/dist/handler/build-time-handler-analyzer.d.ts.map +0 -1
  211. package/dist/handler/build-time-handler-analyzer.js +0 -1101
  212. package/dist/handler/build-time-handler-analyzer.js.map +0 -1
  213. package/dist/handler/generated-registry-writer.d.ts +0 -58
  214. package/dist/handler/generated-registry-writer.d.ts.map +0 -1
  215. package/dist/handler/generated-registry-writer.js +0 -458
  216. package/dist/handler/generated-registry-writer.js.map +0 -1
  217. package/dist/handler/handler-codegen.d.ts +0 -48
  218. package/dist/handler/handler-codegen.d.ts.map +0 -1
  219. package/dist/handler/handler-codegen.js +0 -214
  220. package/dist/handler/handler-codegen.js.map +0 -1
  221. package/dist/server/browser-server.d.ts +0 -49
  222. package/dist/server/browser-server.d.ts.map +0 -1
  223. package/dist/server/browser-server.js.map +0 -1
  224. package/dist/server/durable-subscription-bindings.d.ts.map +0 -1
  225. package/dist/server/durable-subscription-bindings.js.map +0 -1
  226. /package/dist/{server → browser}/durable-subscription-bindings.d.ts +0 -0
  227. /package/dist/{server → browser}/durable-subscription-bindings.js +0 -0
@@ -14,13 +14,15 @@
14
14
  import { clone, create } from "@bufbuild/protobuf";
15
15
  import { AnySchema } from "@bufbuild/protobuf/wkt";
16
16
  import { randomUUID } from "node:crypto";
17
+ import { TenantIdSchema } from "@spine-event-engine/proto";
17
18
  import { WorkerIdSchema } from "@spine-event-engine/proto/delivery";
18
19
  import { AlreadyPickedUp, DeliveryMonitor, FailedPickUp, FailedReception, } from "./delivery-monitor.js";
19
20
  import { Inbox, InboxTargets } from "./inbox.js";
20
- import { InboxStorage } from "./inbox-storage.js";
21
+ import { InboxStorage, InboxStorageClock } from "./inbox-storage.js";
21
22
  import { ShardIndex } from "./shard-index.js";
22
23
  import { ShardedWorkRegistry } from "./sharded-work-registry.js";
23
24
  import { withDeliveryCommitFence } from "../repository/commit-fence.js";
25
+ const deliveryFailureLimit = 100;
24
26
  /**
25
27
  * Executes direct Inbox delivery for one complete worker identity.
26
28
  */
@@ -70,7 +72,7 @@ export class Delivery {
70
72
  options.worker.nodeId?.value !== options.node) {
71
73
  throw new Error("Delivery worker node must match the configured delivery node.");
72
74
  }
73
- this.context = Object.freeze({ ...options.context });
75
+ this.context = Delivery.#contextSnapshot(options.context);
74
76
  this.storageFactory = options.storageFactory;
75
77
  this.strategy = options.strategy ?? { shardCount: 1, shardFor: () => ShardIndex.single() };
76
78
  this.worker =
@@ -137,18 +139,34 @@ export class Delivery {
137
139
  * @returns The terminal direct-delivery result.
138
140
  */
139
141
  async drainMessage(message, input) {
140
- const onMessage = typeof input === "function" ? input : input.onMessage;
141
- const observeDelivered = typeof input === "function" ? undefined : input.onDelivered;
142
+ return this.#drainDirect(message, this.#directInput(input));
143
+ }
144
+ #directInput(input) {
145
+ return typeof input === "function" ? { onMessage: input } : input;
146
+ }
147
+ static #contextSnapshot(context) {
148
+ return Object.freeze(context.multitenant
149
+ ? {
150
+ name: context.name,
151
+ multitenant: true,
152
+ tenantId: clone(TenantIdSchema, context.tenantId),
153
+ }
154
+ : { name: context.name, multitenant: false });
155
+ }
156
+ async #drainDirect(message, input) {
142
157
  let acknowledged = false;
143
158
  const run = await this.drain(message.shard, {
144
- onMessage,
145
- ...(typeof input === "function" || input.acceptMessage === undefined
146
- ? {}
147
- : { acceptMessage: input.acceptMessage }),
159
+ onMessage: input.onMessage,
160
+ ...(input.acceptMessage === undefined ? {} : { acceptMessage: input.acceptMessage }),
148
161
  onDelivered: (next) => {
149
162
  acknowledged ||=
150
163
  next.id.value === message.id.value && next.id.shard.key() === message.id.shard.key();
151
- observeDelivered?.(next);
164
+ input.onDelivered?.(next);
165
+ },
166
+ onDuplicateRemoved: (next) => {
167
+ acknowledged ||=
168
+ next.id.value === message.id.value && next.id.shard.key() === message.id.shard.key();
169
+ input.onDuplicateRemoved?.(next);
152
170
  },
153
171
  });
154
172
  return Object.freeze({ run, acknowledged });
@@ -179,162 +197,405 @@ export class Delivery {
179
197
  const released = await safelyValue(() => this.shards.release(session), false);
180
198
  return result(released ? "STOPPED" : "FAILED");
181
199
  }
182
- const statistics = counts();
183
- const failures = [];
184
- const complete = (status, value = statistics, runFailures = failures) => result(status, value, runFailures);
185
- let current = session;
186
- const ownership = { lost: false };
187
- const validate = async () => {
188
- const validated = await safelyValue(() => this.shards.validateOwnership(current, options.operation), undefined);
189
- if (validated === undefined) {
190
- ownership.lost = true;
191
- return false;
200
+ return new DeliveryDrain({
201
+ inbox: this.inbox,
202
+ monitor: this.#monitor,
203
+ options,
204
+ pageSize: this.pageSize,
205
+ session,
206
+ shards: this.shards,
207
+ shard,
208
+ }).run();
209
+ }
210
+ }
211
+ /**
212
+ * Processes inbox pages while retaining one shard session.
213
+ */
214
+ class DeliveryDrain {
215
+ input;
216
+ #statistics = counts();
217
+ #failures = [];
218
+ #deduplication;
219
+ #blockedTargets = new Set();
220
+ #removedInPage = new Set();
221
+ #current;
222
+ #ownershipLost = false;
223
+ #failureLimitReached = false;
224
+ /**
225
+ * Creates a drain for one acquired shard session.
226
+ *
227
+ * @param input Supplies the inbox, callbacks, and shard session.
228
+ */
229
+ constructor(input) {
230
+ this.input = input;
231
+ this.#current = input.session;
232
+ this.#deduplication = new DeliveryDeduplication(input.inbox);
233
+ }
234
+ /**
235
+ * Executes delivery and releases the shard session.
236
+ *
237
+ * @returns The terminal delivery outcome and counts.
238
+ */
239
+ async run() {
240
+ let run;
241
+ try {
242
+ run = (await this.#canStart()) ? await this.#readPages() : this.#complete("STOPPED");
243
+ }
244
+ catch (error) {
245
+ if (!(await this.#release()))
246
+ return this.#complete("FAILED");
247
+ throw error;
248
+ }
249
+ return (await this.#release()) ? run : this.#complete("FAILED");
250
+ }
251
+ /**
252
+ * Removes the session and reports completion when release succeeds.
253
+ *
254
+ * @returns Whether the session was released.
255
+ */
256
+ async #release() {
257
+ const released = await safelyValue(() => this.input.shards.release(this.#current, this.input.options.operation), false);
258
+ if (released)
259
+ await safely(() => this.#completeMonitoring());
260
+ return released;
261
+ }
262
+ /**
263
+ * Checks whether monitoring permits this drain to start.
264
+ *
265
+ * @returns Whether page processing may start.
266
+ */
267
+ async #canStart() {
268
+ if (!(await safelyBoolean(() => this.input.monitor.shouldContinueAfter("DELIVERY"))))
269
+ return false;
270
+ return safely(() => this.input.monitor.onDeliveryStarted(this.input.shard));
271
+ }
272
+ /**
273
+ * Reads and processes inbox pages until the shard is drained or stopped.
274
+ *
275
+ * @returns The terminal delivery outcome.
276
+ */
277
+ async #readPages() {
278
+ let after;
279
+ let deliveredInScan = false;
280
+ for (;;) {
281
+ const messages = await this.#readPage(after);
282
+ this.#removedInPage.clear();
283
+ if (messages.length === 0) {
284
+ if (!deliveredInScan)
285
+ return this.#complete("DRAINED");
286
+ after = undefined;
287
+ deliveredInScan = false;
288
+ continue;
192
289
  }
193
- current = validated;
194
- return true;
195
- };
196
- const cleanupPage = async () => {
197
- if (this.inbox.removeDelivered === undefined)
198
- return true;
199
- if (!(await validate()))
200
- return false;
201
- let after;
202
- for (let page = 0; page < 2; page += 1) {
203
- let removedAny = false;
204
- const delivered = await this.inbox.read(shard, {
205
- statuses: ["DELIVERED"],
206
- limit: this.pageSize,
207
- ...(after === undefined ? {} : { after }),
208
- ...(options.operation ?? {}),
209
- });
210
- for (const message of delivered) {
211
- if (options.operation?.signal?.aborted || !(await validate()))
212
- return false;
213
- const removed = await this.inbox.removeDelivered(message, current, options.operation);
214
- if (options.operation?.signal?.aborted || (!removed && !(await validate())))
215
- return false;
216
- removedAny ||= removed;
217
- }
218
- const last = delivered.at(-1);
219
- if (removedAny || last === undefined || delivered.length < this.pageSize)
220
- break;
221
- after = {
222
- messageId: last.id.value,
223
- whenReceived: last.whenReceived,
224
- version: last.version,
225
- };
290
+ const deliveredBefore = this.#statistics.delivered;
291
+ if (!(await this.#processPage(messages)))
292
+ return this.#complete("STOPPED");
293
+ if (!(await this.#cleanupPage(messages)))
294
+ return this.#complete("STOPPED");
295
+ deliveredInScan ||= this.#statistics.delivered !== deliveredBefore;
296
+ if (messages.length < this.input.pageSize) {
297
+ if (!deliveredInScan)
298
+ return this.#complete("DRAINED");
299
+ after = undefined;
300
+ deliveredInScan = false;
301
+ continue;
226
302
  }
303
+ const survivor = messages.findLast((message) => !this.#removedInPage.has(message));
304
+ if (survivor !== undefined)
305
+ after = this.#continuation([survivor]);
306
+ }
307
+ }
308
+ /**
309
+ * Reads the next inbox page after an optional continuation.
310
+ *
311
+ * @param after Supplies the prior page position, when present.
312
+ * @returns The ordered inbox messages in the next page.
313
+ */
314
+ #readPage(after) {
315
+ return this.input.inbox.read(this.input.shard, {
316
+ limit: this.input.pageSize,
317
+ ...(after === undefined ? {} : { after }),
318
+ ...(this.input.options.operation ?? {}),
319
+ });
320
+ }
321
+ /**
322
+ * Processes every message in one page.
323
+ *
324
+ * @param messages Supplies the inbox page to process.
325
+ * @returns Whether page processing may continue.
326
+ */
327
+ async #processPage(messages) {
328
+ const page = this.#deduplication.page(messages, this.#retentionTime());
329
+ for (const message of messages) {
330
+ if (!(await this.#processMessage(message, page)))
331
+ return false;
332
+ if (this.#failureLimitReached)
333
+ return false;
334
+ }
335
+ return true;
336
+ }
337
+ /**
338
+ * Processes one message unless it is skipped or its target is blocked.
339
+ *
340
+ * @param message Supplies the inbox message.
341
+ * @param page Provides duplicate checks for this page.
342
+ * @returns Whether processing may continue.
343
+ */
344
+ async #processMessage(message, page) {
345
+ if (this.input.options.operation?.signal?.aborted)
346
+ return false;
347
+ if (this.#shouldSkip(message))
227
348
  return true;
228
- };
229
- const dispatch = (message) => withDeliveryCommitFence(async () => {
230
- if (!(await validate()))
231
- throw new Error("Shard ownership was lost.");
232
- }, () => Promise.resolve(options.onMessage(message)));
233
- const markDelivered = async (message) => {
234
- if ((await this.inbox.markDelivered(message, options.operation)) === undefined)
235
- throw new Error("Inbox message was not marked delivered.");
236
- statistics.delivered += 1;
237
- options.onDelivered?.(message);
238
- };
349
+ const target = this.#targetKey(message);
350
+ if (this.#blockedTargets.has(target))
351
+ return true;
352
+ this.#statistics.processed += 1;
353
+ if (!(await this.#mayProcess()))
354
+ return false;
355
+ if (page.isDuplicate(message))
356
+ return this.#removeDuplicate(message, target);
357
+ return this.#deliver(message, target);
358
+ }
359
+ /**
360
+ * Determines whether an inbox message is ineligible for endpoint delivery.
361
+ *
362
+ * @param message Supplies the message to check.
363
+ * @returns Whether the message should be skipped.
364
+ */
365
+ #shouldSkip(message) {
366
+ return (message.status !== "TO_DELIVER" ||
367
+ !isEndpointMessage(message) ||
368
+ (this.input.options.acceptMessage !== undefined && !this.input.options.acceptMessage(message)));
369
+ }
370
+ /**
371
+ * Builds the key used to block further messages for one target.
372
+ *
373
+ * @param message Supplies the inbox message.
374
+ * @returns The target type and ID key.
375
+ */
376
+ #targetKey(message) {
377
+ return `${message.inboxId.targetTypeUrl}:${InboxTargets.key(message.inboxId.targetId)}`;
378
+ }
379
+ /**
380
+ * Checks monitoring and verifies that the shard session remains current.
381
+ *
382
+ * @returns Whether this message may be processed.
383
+ */
384
+ async #mayProcess() {
385
+ return ((await safelyBoolean(() => this.input.monitor.shouldContinueAfter("PAGE"))) &&
386
+ (await this.#validate()));
387
+ }
388
+ /**
389
+ * Removes a duplicate message and records a failure if removal fails.
390
+ *
391
+ * @param message Supplies the duplicate message.
392
+ * @param target Identifies the target to block after a failure.
393
+ * @returns Whether processing may continue.
394
+ */
395
+ async #removeDuplicate(message, target) {
239
396
  try {
240
- if (!(await safelyBoolean(() => this.#monitor.shouldContinueAfter("DELIVERY"))))
241
- return complete("STOPPED");
242
- if (!(await safely(() => this.#monitor.onDeliveryStarted(shard))))
243
- return complete("STOPPED");
244
- const blockedTargets = new Set();
245
- let after;
246
- for (;;) {
247
- const messages = await this.inbox.read(shard, {
248
- statuses: ["TO_DELIVER"],
249
- limit: this.pageSize,
250
- ...(after === undefined ? {} : { after }),
251
- ...(options.operation ?? {}),
252
- });
253
- if (messages.length === 0) {
254
- if (!(await cleanupPage()))
255
- return complete("STOPPED");
256
- break;
257
- }
258
- const deliveredBefore = statistics.delivered;
259
- for (const message of messages) {
260
- if (options.operation?.signal?.aborted)
261
- return complete("STOPPED");
262
- if (!isEndpointMessage(message))
263
- continue;
264
- if (options.acceptMessage !== undefined && !options.acceptMessage(message))
265
- continue;
266
- const target = `${message.inboxId.targetTypeUrl}:${InboxTargets.key(message.inboxId.targetId)}`;
267
- if (blockedTargets.has(target))
268
- continue;
269
- statistics.processed += 1;
270
- if (!(await safelyBoolean(() => this.#monitor.shouldContinueAfter("PAGE"))))
271
- return complete("STOPPED");
272
- if (!(await validate()))
273
- return complete("STOPPED");
274
- try {
275
- statistics.accepted += 1;
276
- await dispatch(message);
277
- if (options.operation?.signal?.aborted)
278
- return complete("STOPPED");
279
- if (!(await validate()))
280
- return complete("STOPPED");
281
- await markDelivered(message);
282
- }
283
- catch (error) {
284
- statistics.failed += 1;
285
- failures.push(Object.freeze({ message: snapshot(message), error }));
286
- const reception = new FailedReception(message, error, async () => {
287
- if (!(await validate()))
288
- throw new Error("Shard ownership was lost.");
289
- await markDelivered(message);
290
- }, async () => {
291
- await dispatch(message);
292
- if (!(await validate()))
293
- throw new Error("Shard ownership was lost.");
294
- await markDelivered(message);
295
- });
296
- const action = await safelyValue(() => this.#monitor.onReceptionFailure(reception), reception.markDelivered());
297
- if (!(await safely(() => action.execute())) &&
298
- !(await safely(() => reception.markDelivered().execute()))) {
299
- blockedTargets.add(target);
300
- }
301
- if (ownership.lost)
302
- return complete("STOPPED");
303
- }
304
- }
305
- if (!(await cleanupPage()))
306
- return complete("STOPPED");
307
- const last = messages.at(-1);
308
- if (statistics.delivered !== deliveredBefore) {
309
- after = undefined;
310
- continue;
311
- }
312
- if (last === undefined || messages.length < this.pageSize)
313
- break;
314
- after = {
315
- messageId: last.id.value,
316
- whenReceived: last.whenReceived,
317
- version: last.version,
318
- };
319
- // Reached a full page without progress: continue past it once. A later
320
- // independent target may still be actionable; exhaustion ends the run.
321
- }
322
- return complete("DRAINED");
397
+ if (!(await this.input.inbox.removeDuplicate(message, this.#current, this.input.options.operation)))
398
+ throw new Error("Inbox duplicate was not removed.");
399
+ this.#removedInPage.add(message);
400
+ this.input.options.onDuplicateRemoved?.(message);
401
+ }
402
+ catch (error) {
403
+ this.#recordFailure(message, error);
404
+ this.#blockedTargets.add(target);
405
+ }
406
+ return true;
407
+ }
408
+ /**
409
+ * Delivers one message and records its outcome.
410
+ *
411
+ * @param message Supplies the inbox message.
412
+ * @param target Identifies the message target.
413
+ * @returns Whether processing may continue.
414
+ */
415
+ async #deliver(message, target) {
416
+ try {
417
+ this.#statistics.accepted += 1;
418
+ await this.#dispatch(message);
419
+ if (this.input.options.operation?.signal?.aborted || !(await this.#validate()))
420
+ return false;
421
+ await this.#markDelivered(message);
323
422
  }
324
- finally {
325
- const released = await safelyValue(() => this.shards.release(current, options.operation), false);
326
- if (!released) {
327
- // The release result changes the terminal delivery outcome: a shard is
328
- // not complete until ownership is confirmed released.
329
- // eslint-disable-next-line no-unsafe-finally
330
- return complete("FAILED");
423
+ catch (error) {
424
+ await this.#handleReceptionFailure(message, target, error);
425
+ if (this.#ownershipLost)
426
+ return false;
427
+ }
428
+ return true;
429
+ }
430
+ /**
431
+ * Records endpoint failure and runs the configured recovery action.
432
+ *
433
+ * @param message Supplies the failed inbox message.
434
+ * @param target Identifies the message target.
435
+ * @param error Supplies the endpoint or acknowledgement failure.
436
+ * @returns Completion after the recovery action finishes.
437
+ */
438
+ async #handleReceptionFailure(message, target, error) {
439
+ this.#recordFailure(message, error);
440
+ const reception = new FailedReception(message, error, () => this.#acknowledge(message), () => this.#repeat(message));
441
+ const action = await safelyValue(() => this.input.monitor.onReceptionFailure(reception), reception.markDelivered());
442
+ if (!(await safely(() => action.execute())) &&
443
+ !(await safely(() => reception.markDelivered().execute())))
444
+ this.#blockedTargets.add(target);
445
+ }
446
+ /**
447
+ * Marks a message delivered after checking the shard session.
448
+ *
449
+ * @param message Supplies the inbox message to acknowledge.
450
+ * @returns Completion after the message is marked delivered.
451
+ */
452
+ async #acknowledge(message) {
453
+ if (!(await this.#validate()))
454
+ throw new Error("Shard ownership was lost.");
455
+ await this.#markDelivered(message);
456
+ }
457
+ /**
458
+ * Dispatches the message again and acknowledges it.
459
+ *
460
+ * @param message Supplies the inbox message to repeat.
461
+ * @returns Completion after dispatch and acknowledgement.
462
+ */
463
+ async #repeat(message) {
464
+ await this.#dispatch(message);
465
+ await this.#acknowledge(message);
466
+ }
467
+ /**
468
+ * Dispatches a message while validating the shard session before the call.
469
+ *
470
+ * @param message Supplies the inbox message for the endpoint.
471
+ * @returns Completion after endpoint dispatch.
472
+ */
473
+ async #dispatch(message) {
474
+ await withDeliveryCommitFence(async () => {
475
+ if (!(await this.#validate()))
476
+ throw new Error("Shard ownership was lost.");
477
+ }, () => Promise.resolve(this.input.options.onMessage(message)));
478
+ }
479
+ /**
480
+ * Marks a message delivered and updates local counts and callbacks.
481
+ *
482
+ * @param message Supplies the inbox message to acknowledge.
483
+ * @returns Completion after durable acknowledgement.
484
+ */
485
+ async #markDelivered(message) {
486
+ if ((await this.input.inbox.markDelivered(message, this.input.options.operation)) === undefined)
487
+ throw new Error("Inbox message was not marked delivered.");
488
+ this.#deduplication.recordDelivered(message);
489
+ this.#statistics.delivered += 1;
490
+ this.input.options.onDelivered?.(message);
491
+ }
492
+ /**
493
+ * Removes expired delivered messages from one page when supported.
494
+ *
495
+ * @param messages Supplies the inbox page to clean up.
496
+ * @returns Whether cleanup completed while the session remained valid.
497
+ */
498
+ async #cleanupPage(messages) {
499
+ if (this.input.inbox.removeDelivered === undefined)
500
+ return true;
501
+ if (!(await this.#validate()))
502
+ return false;
503
+ const now = this.#retentionTime().getTime();
504
+ for (const message of messages) {
505
+ if (message.status === "DELIVERED" &&
506
+ (message.keepUntil === undefined || message.keepUntil.getTime() <= now)) {
507
+ const removed = await this.#removeDelivered(message);
508
+ if (removed === undefined)
509
+ return false;
510
+ if (removed)
511
+ this.#removedInPage.add(message);
331
512
  }
332
- await safely(() => this.#monitor.onDeliveryCompleted(Object.freeze({
333
- processed: statistics.processed,
334
- delivered: statistics.delivered,
335
- failed: statistics.failed,
336
- })));
337
513
  }
514
+ return true;
515
+ }
516
+ /**
517
+ * Removes one expired delivered message when supported.
518
+ *
519
+ * @param message Supplies the delivered message to remove.
520
+ * @returns Whether the row was removed, or undefined if the session was lost.
521
+ */
522
+ async #removeDelivered(message) {
523
+ if (this.input.inbox.removeDelivered === undefined)
524
+ return false;
525
+ if (this.input.options.operation?.signal?.aborted || !(await this.#validate()))
526
+ return undefined;
527
+ const removed = await this.input.inbox.removeDelivered(message, this.#current, this.input.options.operation);
528
+ if (this.input.options.operation?.signal?.aborted || (!removed && !(await this.#validate())))
529
+ return undefined;
530
+ return removed;
531
+ }
532
+ /**
533
+ * Creates the continuation following the last message in a page.
534
+ *
535
+ * @param messages Supplies the non-empty inbox page.
536
+ * @returns The continuation identifying its last message.
537
+ */
538
+ #continuation(messages) {
539
+ const last = messages.at(-1);
540
+ if (last === undefined)
541
+ throw new Error("Inbox page has no continuation.");
542
+ return { messageId: last.id.value, whenReceived: last.whenReceived, version: last.version };
543
+ }
544
+ /**
545
+ * Reads the inbox retention clock or the current time.
546
+ *
547
+ * @returns The time used to evaluate message retention.
548
+ */
549
+ #retentionTime() {
550
+ return this.input.inbox instanceof Inbox
551
+ ? InboxStorageClock.read(this.input.inbox.storage)
552
+ : new Date();
553
+ }
554
+ /**
555
+ * Checks the shard session and records when it is lost.
556
+ *
557
+ * @returns Whether the shard session remains valid.
558
+ */
559
+ async #validate() {
560
+ const current = await safelyValue(() => this.input.shards.validateOwnership(this.#current, this.input.options.operation), undefined);
561
+ if (current === undefined)
562
+ this.#ownershipLost = true;
563
+ else
564
+ this.#current = current;
565
+ return current !== undefined;
566
+ }
567
+ /**
568
+ * Records an ephemeral delivery failure up to the configured limit.
569
+ *
570
+ * @param message Supplies the failed inbox message.
571
+ * @param error Supplies the observed failure.
572
+ */
573
+ #recordFailure(message, error) {
574
+ this.#statistics.failed += 1;
575
+ if (this.#failures.length < deliveryFailureLimit)
576
+ this.#failures.push(Object.freeze({ message: snapshot(message), error }));
577
+ this.#failureLimitReached = this.#failures.length === deliveryFailureLimit;
578
+ }
579
+ /**
580
+ * Creates a terminal result from the current counters and failures.
581
+ *
582
+ * @param status Identifies the terminal delivery outcome.
583
+ * @returns The immutable delivery result.
584
+ */
585
+ #complete(status) {
586
+ return result(status, this.#statistics, this.#failures);
587
+ }
588
+ /**
589
+ * Sends delivery completion to the monitor.
590
+ *
591
+ * @returns Completion after the monitor callback settles.
592
+ */
593
+ #completeMonitoring() {
594
+ return Promise.resolve(this.input.monitor.onDeliveryCompleted(Object.freeze({
595
+ processed: this.#statistics.processed,
596
+ delivered: this.#statistics.delivered,
597
+ failed: this.#statistics.failed,
598
+ })));
338
599
  }
339
600
  }
340
601
  function workerId(node) {
@@ -384,6 +645,14 @@ async function safelyBoolean(action) {
384
645
  return false;
385
646
  }
386
647
  }
648
+ /**
649
+ * Returns an action result or a fallback when the action fails.
650
+ *
651
+ * @typeParam T Action result type.
652
+ * @param action Supplies the action to run.
653
+ * @param fallback Supplies the result to use after failure.
654
+ * @returns The action result or fallback value.
655
+ */
387
656
  async function safelyValue(action, fallback) {
388
657
  try {
389
658
  return await action();
@@ -397,4 +666,118 @@ function isEndpointMessage(message) {
397
666
  message.label === "UPDATE_SUBSCRIBER" ||
398
667
  message.label === "REACT_UPON_EVENT");
399
668
  }
669
+ /**
670
+ * Tracks recently delivered messages for one inbox.
671
+ */
672
+ class DeliveryDeduplication {
673
+ #recent;
674
+ /**
675
+ * Creates or reuses recent delivery state for the supplied inbox.
676
+ *
677
+ * @param inbox Supplies the inbox used as the deduplication key.
678
+ */
679
+ constructor(inbox) {
680
+ const current = recentDeliveries.get(inbox);
681
+ this.#recent = current ?? new RecentDeliveries();
682
+ if (current === undefined)
683
+ recentDeliveries.set(inbox, this.#recent);
684
+ }
685
+ /**
686
+ * Creates duplicate checks for one page and retention time.
687
+ *
688
+ * @param messages Supplies the messages in the page.
689
+ * @param now Supplies the time used to check message retention.
690
+ * @returns Duplicate checks for the page.
691
+ */
692
+ page(messages, now) {
693
+ return new DeliveryPageDeduplication(messages, this.#recent, now);
694
+ }
695
+ /**
696
+ * Records a message as recently delivered.
697
+ *
698
+ * @param message Supplies the delivered message.
699
+ */
700
+ recordDelivered(message) {
701
+ this.#recent.add(message);
702
+ }
703
+ }
704
+ /**
705
+ * Checks duplicates within a page and the recent-delivery cache.
706
+ */
707
+ class DeliveryPageDeduplication {
708
+ recent;
709
+ #identities;
710
+ /**
711
+ * Creates duplicate checks from retained messages in one page.
712
+ *
713
+ * @param messages Supplies the inbox page.
714
+ * @param recent Provides recently delivered message identities.
715
+ * @param now Supplies the time used to check message retention.
716
+ */
717
+ constructor(messages, recent, now) {
718
+ this.recent = recent;
719
+ this.#identities = new Set(messages
720
+ .filter((message) => message.status === "DELIVERED" &&
721
+ message.keepUntil !== undefined &&
722
+ message.keepUntil.getTime() > now.getTime())
723
+ .map((message) => RecentDeliveries.key(message)));
724
+ }
725
+ /**
726
+ * Checks whether a message appeared earlier in the page or cache.
727
+ *
728
+ * @param message Supplies the message to check.
729
+ * @returns Whether this message is a duplicate.
730
+ */
731
+ isDuplicate(message) {
732
+ const identity = RecentDeliveries.key(message);
733
+ if (this.#identities.has(identity) || this.recent.has(identity))
734
+ return true;
735
+ this.#identities.add(identity);
736
+ return false;
737
+ }
738
+ }
739
+ /**
740
+ * Stores a bounded set of recently delivered message identities.
741
+ */
742
+ class RecentDeliveries {
743
+ #identities = new Map();
744
+ /**
745
+ * Checks whether an identity is in the recent-delivery set.
746
+ *
747
+ * @param identity Supplies the encoded message identity.
748
+ * @returns Whether that identity is present.
749
+ */
750
+ has(identity) {
751
+ return this.#identities.has(identity);
752
+ }
753
+ /**
754
+ * Adds a message identity and removes the oldest entry when over capacity.
755
+ *
756
+ * @param message Supplies the delivered message.
757
+ */
758
+ add(message) {
759
+ const identity = RecentDeliveries.key(message);
760
+ this.#identities.delete(identity);
761
+ this.#identities.set(identity, undefined);
762
+ if (this.#identities.size > 1_000) {
763
+ const oldest = this.#identities.keys().next().value;
764
+ if (oldest !== undefined)
765
+ this.#identities.delete(oldest);
766
+ }
767
+ }
768
+ /**
769
+ * Creates the identity key used for recent-delivery checks.
770
+ *
771
+ * @param message Supplies the message to identify.
772
+ * @returns The serialized message identity.
773
+ */
774
+ static key(message) {
775
+ return JSON.stringify([
776
+ message.signalId,
777
+ message.inboxId.targetTypeUrl,
778
+ InboxTargets.key(message.inboxId.targetId),
779
+ ]);
780
+ }
781
+ }
782
+ const recentDeliveries = new WeakMap();
400
783
  //# sourceMappingURL=delivery.js.map