@effect/cluster 0.36.3 → 0.37.1

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 (90) hide show
  1. package/ClusterWorkflowEngine/package.json +6 -0
  2. package/dist/cjs/ClusterSchema.js +9 -1
  3. package/dist/cjs/ClusterSchema.js.map +1 -1
  4. package/dist/cjs/ClusterWorkflowEngine.js +386 -0
  5. package/dist/cjs/ClusterWorkflowEngine.js.map +1 -0
  6. package/dist/cjs/Envelope.js +14 -4
  7. package/dist/cjs/Envelope.js.map +1 -1
  8. package/dist/cjs/Message.js +22 -2
  9. package/dist/cjs/Message.js.map +1 -1
  10. package/dist/cjs/MessageStorage.js +59 -21
  11. package/dist/cjs/MessageStorage.js.map +1 -1
  12. package/dist/cjs/Reply.js +15 -0
  13. package/dist/cjs/Reply.js.map +1 -1
  14. package/dist/cjs/Runners.js +2 -2
  15. package/dist/cjs/Runners.js.map +1 -1
  16. package/dist/cjs/Sharding.js +23 -8
  17. package/dist/cjs/Sharding.js.map +1 -1
  18. package/dist/cjs/Snowflake.js +2 -2
  19. package/dist/cjs/Snowflake.js.map +1 -1
  20. package/dist/cjs/SqlMessageStorage.js +36 -12
  21. package/dist/cjs/SqlMessageStorage.js.map +1 -1
  22. package/dist/cjs/SqlShardStorage.js +6 -6
  23. package/dist/cjs/SqlShardStorage.js.map +1 -1
  24. package/dist/cjs/index.js +3 -1
  25. package/dist/cjs/internal/entityManager.js +4 -4
  26. package/dist/cjs/internal/entityManager.js.map +1 -1
  27. package/dist/dts/ClusterSchema.d.ts +7 -0
  28. package/dist/dts/ClusterSchema.d.ts.map +1 -1
  29. package/dist/dts/ClusterWorkflowEngine.d.ts +48 -0
  30. package/dist/dts/ClusterWorkflowEngine.d.ts.map +1 -0
  31. package/dist/dts/Envelope.d.ts +9 -0
  32. package/dist/dts/Envelope.d.ts.map +1 -1
  33. package/dist/dts/Message.d.ts +11 -1
  34. package/dist/dts/Message.d.ts.map +1 -1
  35. package/dist/dts/MessageStorage.d.ts +56 -0
  36. package/dist/dts/MessageStorage.d.ts.map +1 -1
  37. package/dist/dts/Reply.d.ts +7 -0
  38. package/dist/dts/Reply.d.ts.map +1 -1
  39. package/dist/dts/ShardStorage.d.ts +1 -1
  40. package/dist/dts/Sharding.d.ts +9 -0
  41. package/dist/dts/Sharding.d.ts.map +1 -1
  42. package/dist/dts/SqlMessageStorage.d.ts +10 -1
  43. package/dist/dts/SqlMessageStorage.d.ts.map +1 -1
  44. package/dist/dts/SqlShardStorage.d.ts +1 -1
  45. package/dist/dts/index.d.ts +4 -0
  46. package/dist/dts/index.d.ts.map +1 -1
  47. package/dist/dts/internal/resourceMap.d.ts +1 -1
  48. package/dist/dts/internal/resourceMap.d.ts.map +1 -1
  49. package/dist/dts/internal/resourceRef.d.ts +1 -1
  50. package/dist/dts/internal/resourceRef.d.ts.map +1 -1
  51. package/dist/esm/ClusterSchema.js +7 -0
  52. package/dist/esm/ClusterSchema.js.map +1 -1
  53. package/dist/esm/ClusterWorkflowEngine.js +378 -0
  54. package/dist/esm/ClusterWorkflowEngine.js.map +1 -0
  55. package/dist/esm/Envelope.js +12 -3
  56. package/dist/esm/Envelope.js.map +1 -1
  57. package/dist/esm/Message.js +20 -1
  58. package/dist/esm/Message.js.map +1 -1
  59. package/dist/esm/MessageStorage.js +59 -21
  60. package/dist/esm/MessageStorage.js.map +1 -1
  61. package/dist/esm/Reply.js +15 -0
  62. package/dist/esm/Reply.js.map +1 -1
  63. package/dist/esm/Runners.js +2 -2
  64. package/dist/esm/Runners.js.map +1 -1
  65. package/dist/esm/Sharding.js +24 -9
  66. package/dist/esm/Sharding.js.map +1 -1
  67. package/dist/esm/Snowflake.js +2 -2
  68. package/dist/esm/Snowflake.js.map +1 -1
  69. package/dist/esm/SqlMessageStorage.js +36 -12
  70. package/dist/esm/SqlMessageStorage.js.map +1 -1
  71. package/dist/esm/SqlShardStorage.js +6 -6
  72. package/dist/esm/SqlShardStorage.js.map +1 -1
  73. package/dist/esm/index.js +4 -0
  74. package/dist/esm/index.js.map +1 -1
  75. package/dist/esm/internal/entityManager.js +5 -5
  76. package/dist/esm/internal/entityManager.js.map +1 -1
  77. package/package.json +14 -5
  78. package/src/ClusterSchema.ts +10 -0
  79. package/src/ClusterWorkflowEngine.ts +475 -0
  80. package/src/Envelope.ts +17 -3
  81. package/src/Message.ts +24 -2
  82. package/src/MessageStorage.ts +122 -22
  83. package/src/Reply.ts +18 -0
  84. package/src/Runners.ts +2 -2
  85. package/src/Sharding.ts +45 -9
  86. package/src/Snowflake.ts +2 -2
  87. package/src/SqlMessageStorage.ts +74 -16
  88. package/src/SqlShardStorage.ts +6 -6
  89. package/src/index.ts +5 -0
  90. package/src/internal/entityManager.ts +6 -4
package/src/Message.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import type * as Rpc from "@effect/rpc/Rpc"
4
+ import * as Rpc from "@effect/rpc/Rpc"
5
5
  import type { Context } from "effect/Context"
6
6
  import * as Data from "effect/Data"
7
7
  import * as Effect from "effect/Effect"
@@ -10,8 +10,10 @@ import * as Option from "effect/Option"
10
10
  import * as Schema from "effect/Schema"
11
11
  import type { PersistenceError } from "./ClusterError.js"
12
12
  import { MalformedMessage } from "./ClusterError.js"
13
+ import type { EntityAddress } from "./EntityAddress.js"
13
14
  import * as Envelope from "./Envelope.js"
14
15
  import type * as Reply from "./Reply.js"
16
+ import type { Snowflake } from "./Snowflake.js"
15
17
 
16
18
  /**
17
19
  * @since 1.0.0
@@ -99,7 +101,27 @@ export class OutgoingRequest<R extends Rpc.Any> extends Data.TaggedClass("Outgoi
99
101
  export class OutgoingEnvelope extends Data.TaggedClass("OutgoingEnvelope")<{
100
102
  readonly envelope: Envelope.AckChunk | Envelope.Interrupt
101
103
  readonly rpc: Rpc.AnyWithProps
102
- }> {}
104
+ }> {
105
+ /**
106
+ * @since 1.0.0
107
+ */
108
+ static interrupt(options: {
109
+ readonly address: EntityAddress
110
+ readonly id: Snowflake
111
+ readonly requestId: Snowflake
112
+ }): OutgoingEnvelope {
113
+ return new OutgoingEnvelope({
114
+ envelope: new Envelope.Interrupt(options),
115
+ rpc: neverRpc
116
+ })
117
+ }
118
+ }
119
+
120
+ const neverRpc = Rpc.make("Never", {
121
+ success: Schema.Never as any,
122
+ error: Schema.Never,
123
+ payload: {}
124
+ })
103
125
 
104
126
  /**
105
127
  * @since 1.0.0
@@ -52,6 +52,11 @@ export class MessageStorage extends Context.Tag("@effect/cluster/MessageStorage"
52
52
  reply: Reply.ReplyWithContext<R>
53
53
  ) => Effect.Effect<void, PersistenceError | MalformedMessage>
54
54
 
55
+ /**
56
+ * Clear the `Reply`s for the given request id.
57
+ */
58
+ readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError>
59
+
55
60
  /**
56
61
  * Retrieves the replies for the specified requests.
57
62
  *
@@ -62,6 +67,24 @@ export class MessageStorage extends Context.Tag("@effect/cluster/MessageStorage"
62
67
  requests: Iterable<Message.OutgoingRequest<R>>
63
68
  ) => Effect.Effect<Array<Reply.Reply<R>>, PersistenceError | MalformedMessage>
64
69
 
70
+ /**
71
+ * Retrieves the encoded replies for the specified request ids.
72
+ */
73
+ readonly repliesForUnfiltered: <R extends Rpc.Any>(
74
+ requestIds: Iterable<Snowflake.Snowflake>
75
+ ) => Effect.Effect<Array<Reply.ReplyEncoded<R>>, PersistenceError | MalformedMessage>
76
+
77
+ /**
78
+ * Retrieves the request id for the specified primary key.
79
+ */
80
+ readonly requestIdForPrimaryKey: (
81
+ options: {
82
+ readonly address: EntityAddress
83
+ readonly tag: string
84
+ readonly id: string
85
+ }
86
+ ) => Effect.Effect<Option.Option<Snowflake.Snowflake>, PersistenceError>
87
+
65
88
  /**
66
89
  * For locally sent messages, register a handler to process the replies.
67
90
  */
@@ -103,6 +126,13 @@ export class MessageStorage extends Context.Tag("@effect/cluster/MessageStorage"
103
126
  readonly resetAddress: (
104
127
  address: EntityAddress
105
128
  ) => Effect.Effect<void, PersistenceError>
129
+
130
+ /**
131
+ * Clear all messages and replies for the provided address.
132
+ */
133
+ readonly clearAddress: (
134
+ address: EntityAddress
135
+ ) => Effect.Effect<void, PersistenceError>
106
136
  }>() {}
107
137
 
108
138
  /**
@@ -194,6 +224,18 @@ export type Encoded = {
194
224
  reply: Reply.ReplyEncoded<any>
195
225
  ) => Effect.Effect<void, PersistenceError>
196
226
 
227
+ /**
228
+ * Remove the replies for the specified request.
229
+ */
230
+ readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError>
231
+
232
+ /**
233
+ * Retrieves the request id for the specified primary key.
234
+ */
235
+ readonly requestIdForPrimaryKey: (
236
+ primaryKey: string
237
+ ) => Effect.Effect<Option.Option<Snowflake.Snowflake>, PersistenceError>
238
+
197
239
  /**
198
240
  * Retrieves the replies for the specified requests.
199
241
  *
@@ -205,6 +247,14 @@ export type Encoded = {
205
247
  PersistenceError
206
248
  >
207
249
 
250
+ /**
251
+ * Retrieves the replies for the specified request ids.
252
+ */
253
+ readonly repliesForUnfiltered: (requestIds: Array<string>) => Effect.Effect<
254
+ Array<Reply.ReplyEncoded<any>>,
255
+ PersistenceError
256
+ >
257
+
208
258
  /**
209
259
  * Retrieves the unprocessed messages for the given shards.
210
260
  *
@@ -247,6 +297,13 @@ export type Encoded = {
247
297
  address: EntityAddress
248
298
  ) => Effect.Effect<void, PersistenceError>
249
299
 
300
+ /**
301
+ * Clear all messages and replies for the provided address.
302
+ */
303
+ readonly clearAddress: (
304
+ address: EntityAddress
305
+ ) => Effect.Effect<void, PersistenceError>
306
+
250
307
  /**
251
308
  * Reset the mailbox state for the provided shards.
252
309
  */
@@ -362,6 +419,7 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
362
419
  Effect.asVoid
363
420
  ),
364
421
  saveReply: (reply) => Effect.flatMap(Reply.serialize(reply), encoded.saveReply),
422
+ clearReplies: encoded.clearReplies,
365
423
  repliesFor: Effect.fnUntraced(function*(messages) {
366
424
  const requestIds = Arr.empty<string>()
367
425
  const map = new Map<string, Message.OutgoingRequest<any>>()
@@ -374,6 +432,11 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
374
432
  const encodedReplies = yield* encoded.repliesFor(requestIds)
375
433
  return yield* decodeReplies(map, encodedReplies)
376
434
  }),
435
+ repliesForUnfiltered: (ids) => encoded.repliesForUnfiltered(Array.from(ids, String)),
436
+ requestIdForPrimaryKey(options) {
437
+ const primaryKey = Envelope.primaryKeyByAddress(options)
438
+ return encoded.requestIdForPrimaryKey(primaryKey)
439
+ },
377
440
  unprocessedMessages: (shardIds) => {
378
441
  const shards = Array.from(shardIds)
379
442
  if (shards.length === 0) return Effect.succeed([])
@@ -390,7 +453,8 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
390
453
  decodeMessages
391
454
  )
392
455
  },
393
- resetAddress: (address) => encoded.resetAddress(address),
456
+ resetAddress: encoded.resetAddress,
457
+ clearAddress: encoded.clearAddress,
394
458
  resetShards: (shardIds) => encoded.resetShards(Array.from(shardIds))
395
459
  })
396
460
 
@@ -506,10 +570,14 @@ export const noop: MessageStorage["Type"] = globalValue(
506
570
  saveRequest: () => Effect.succeed(SaveResult.Success()),
507
571
  saveEnvelope: () => Effect.void,
508
572
  saveReply: () => Effect.void,
573
+ clearReplies: () => Effect.void,
509
574
  repliesFor: () => Effect.succeed([]),
575
+ repliesForUnfiltered: () => Effect.succeed([]),
576
+ requestIdForPrimaryKey: () => Effect.succeedNone,
510
577
  unprocessedMessages: () => Effect.succeed([]),
511
578
  unprocessedMessagesById: () => Effect.succeed([]),
512
579
  resetAddress: () => Effect.void,
580
+ clearAddress: () => Effect.void,
513
581
  resetShards: () => Effect.void
514
582
  }))
515
583
  )
@@ -567,9 +635,31 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
567
635
 
568
636
  const replyLatch = yield* Effect.makeLatch()
569
637
 
638
+ function repliesFor(requestIds: Array<string>) {
639
+ const replies = Arr.empty<Reply.ReplyEncoded<any>>()
640
+ for (const requestId of requestIds) {
641
+ const request = requests.get(requestId)
642
+ if (!request) continue
643
+ else if (Option.isNone(request.lastReceivedChunk)) {
644
+ // eslint-disable-next-line no-restricted-syntax
645
+ replies.push(...request.replies)
646
+ continue
647
+ }
648
+ const sequence = request.lastReceivedChunk.value.sequence
649
+ for (const reply of request.replies) {
650
+ if (reply._tag === "Chunk" && reply.sequence <= sequence) {
651
+ continue
652
+ }
653
+ replies.push(reply)
654
+ }
655
+ }
656
+ return replies
657
+ }
658
+
570
659
  const encoded: Encoded = {
571
- saveEnvelope: ({ envelope, primaryKey }) =>
660
+ saveEnvelope: ({ envelope: envelope_, primaryKey }) =>
572
661
  Effect.sync(() => {
662
+ const envelope = JSON.parse(JSON.stringify(envelope_)) as Envelope.Envelope.Encoded
573
663
  const existing = primaryKey
574
664
  ? requestsByPrimaryKey.get(primaryKey)
575
665
  : envelope._tag === "Request" && requests.get(envelope.requestId)
@@ -598,8 +688,9 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
598
688
  journal.push(envelope)
599
689
  return SaveResultEncoded.Success()
600
690
  }),
601
- saveReply: (reply) =>
691
+ saveReply: (reply_) =>
602
692
  Effect.sync(() => {
693
+ const reply = JSON.parse(JSON.stringify(reply_)) as Reply.ReplyEncoded<any>
603
694
  const entry = requests.get(reply.requestId)
604
695
  if (!entry || replyIds.has(reply.id)) return
605
696
  if (reply._tag === "WithExit") {
@@ -609,27 +700,22 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
609
700
  replyIds.add(reply.id)
610
701
  replyLatch.unsafeOpen()
611
702
  }),
612
- repliesFor: (requestIds) =>
703
+ clearReplies: (id) =>
613
704
  Effect.sync(() => {
614
- const replies = Arr.empty<Reply.ReplyEncoded<any>>()
615
- for (const requestId of requestIds) {
616
- const request = requests.get(requestId)
617
- if (!request) continue
618
- else if (Option.isNone(request.lastReceivedChunk)) {
619
- // eslint-disable-next-line no-restricted-syntax
620
- replies.push(...request.replies)
621
- continue
622
- }
623
- const sequence = request.lastReceivedChunk.value.sequence
624
- for (const reply of request.replies) {
625
- if (reply._tag === "Chunk" && reply.sequence <= sequence) {
626
- continue
627
- }
628
- replies.push(reply)
629
- }
630
- }
631
- return replies
705
+ const entry = requests.get(String(id))
706
+ if (!entry) return
707
+ entry.replies = []
708
+ entry.lastReceivedChunk = Option.none()
709
+ unprocessed.add(entry.envelope)
632
710
  }),
711
+ requestIdForPrimaryKey: (primaryKey) =>
712
+ Effect.sync(() => {
713
+ const entry = requestsByPrimaryKey.get(primaryKey)
714
+ return Option.fromNullable(entry?.envelope.requestId).pipe(Option.map(Snowflake.Snowflake))
715
+ }),
716
+ repliesFor: (requestIds) => Effect.sync(() => repliesFor(requestIds)),
717
+ repliesForUnfiltered: (requestIds) =>
718
+ Effect.sync(() => requestIds.flatMap((id) => requests.get(String(id))?.replies ?? [])),
633
719
  unprocessedMessages: (shardIds) =>
634
720
  Effect.sync(() => {
635
721
  if (unprocessed.size === 0) return []
@@ -667,6 +753,20 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
667
753
  return unprocessedWith((envelope) => envelopeIds.has(envelope.requestId))
668
754
  }),
669
755
  resetAddress: () => Effect.void,
756
+ clearAddress: (address) =>
757
+ Effect.sync(() => {
758
+ for (let i = journal.length - 1; i >= 0; i--) {
759
+ const envelope = journal[i]
760
+ const sameAddress = address.entityType === envelope.address.entityType &&
761
+ address.entityId === envelope.address.entityId
762
+ if (!sameAddress || envelope._tag !== "Request") {
763
+ continue
764
+ }
765
+ unprocessed.delete(envelope)
766
+ requests.delete(envelope.requestId)
767
+ journal.splice(i, 1)
768
+ }
769
+ }),
670
770
  resetShards: () => Effect.void
671
771
  }
672
772
 
package/src/Reply.ts CHANGED
@@ -8,6 +8,7 @@ import * as Context from "effect/Context"
8
8
  import * as Data from "effect/Data"
9
9
  import * as Effect from "effect/Effect"
10
10
  import * as Exit from "effect/Exit"
11
+ import * as FiberId from "effect/FiberId"
11
12
  import * as FiberRef from "effect/FiberRef"
12
13
  import { identity } from "effect/Function"
13
14
  import type * as Option from "effect/Option"
@@ -68,6 +69,23 @@ export class ReplyWithContext<R extends Rpc.Any> extends Data.TaggedClass("Reply
68
69
  rpc: neverRpc
69
70
  })
70
71
  }
72
+ /**
73
+ * @since 1.0.0
74
+ */
75
+ static interrupt(options: {
76
+ readonly id: Snowflake
77
+ readonly requestId: Snowflake
78
+ }): ReplyWithContext<any> {
79
+ return new ReplyWithContext({
80
+ reply: new WithExit({
81
+ requestId: options.requestId,
82
+ id: options.id,
83
+ exit: Exit.interrupt(FiberId.none)
84
+ }),
85
+ context: Context.empty() as any,
86
+ rpc: neverRpc
87
+ })
88
+ }
71
89
  }
72
90
 
73
91
  const neverRpc = Rpc.make("Never", {
package/src/Runners.ts CHANGED
@@ -312,7 +312,7 @@ export const make: (options: Omit<Runners["Type"], "sendLocal" | "notifyLocal">)
312
312
  notify(options_) {
313
313
  const { discard, message } = options_
314
314
  return notifyWith(message, (message, duplicate) => {
315
- if (message._tag === "OutgoingEnvelope") {
315
+ if (discard || message._tag === "OutgoingEnvelope") {
316
316
  return options.notify(options_)
317
317
  } else if (!duplicate && options_.address._tag === "Some") {
318
318
  return Effect.catchAll(
@@ -328,7 +328,7 @@ export const make: (options: Omit<Runners["Type"], "sendLocal" | "notifyLocal">)
328
328
  }
329
329
  )
330
330
  }
331
- return discard ? options.notify(options_) : options.notify(options_).pipe(
331
+ return options.notify(options_).pipe(
332
332
  Effect.andThen(replyFromStorage(message))
333
333
  )
334
334
  })
package/src/Sharding.ts CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  EntityNotManagedByRunner,
36
36
  RunnerUnavailable
37
37
  } from "./ClusterError.js"
38
- import { Persisted } from "./ClusterSchema.js"
38
+ import { Persisted, Uninterruptible } from "./ClusterSchema.js"
39
39
  import type { CurrentAddress, CurrentRunnerAddress, Entity, HandlersFrom } from "./Entity.js"
40
40
  import { EntityAddress } from "./EntityAddress.js"
41
41
  import { EntityId } from "./EntityId.js"
@@ -126,6 +126,17 @@ export class Sharding extends Context.Tag("@effect/cluster/Sharding")<Sharding,
126
126
  EntityNotManagedByRunner | EntityNotAssignedToRunner | MailboxFull | AlreadyProcessingMessage
127
127
  >
128
128
 
129
+ /**
130
+ * Sends an outgoing message
131
+ */
132
+ readonly sendOutgoing: (
133
+ message: Message.Outgoing<any>,
134
+ discard: boolean
135
+ ) => Effect.Effect<
136
+ void,
137
+ EntityNotManagedByRunner | MailboxFull | AlreadyProcessingMessage | PersistenceError
138
+ >
139
+
129
140
  /**
130
141
  * Notify sharding that a message has been persisted to storage.
131
142
  */
@@ -134,6 +145,11 @@ export class Sharding extends Context.Tag("@effect/cluster/Sharding")<Sharding,
134
145
  EntityNotManagedByRunner | EntityNotAssignedToRunner | AlreadyProcessingMessage
135
146
  >
136
147
 
148
+ /**
149
+ * Reset the state of a message
150
+ */
151
+ readonly reset: (requestId: Snowflake.Snowflake) => Effect.Effect<boolean>
152
+
137
153
  /**
138
154
  * Retrieves the active entity count for the current runner.
139
155
  */
@@ -247,7 +263,7 @@ const make = Effect.gen(function*() {
247
263
  Effect.forkIn(shardingScope)
248
264
  )
249
265
 
250
- // refresh the shard locks every 10s
266
+ // refresh the shard locks every 4s
251
267
  yield* Effect.suspend(() =>
252
268
  shardStorage.refresh(selfAddress, [
253
269
  ...acquiredShards,
@@ -270,14 +286,14 @@ const make = Effect.gen(function*() {
270
286
  }),
271
287
  Effect.retry({
272
288
  times: 5,
273
- schedule: Schedule.spaced(250)
289
+ schedule: Schedule.spaced(50)
274
290
  }),
275
291
  Effect.catchAllCause((cause) =>
276
292
  Effect.logError("Could not refresh shard locks", cause).pipe(
277
293
  Effect.andThen(clearSelfShards)
278
294
  )
279
295
  ),
280
- Effect.delay("10 seconds"),
296
+ Effect.delay("4 seconds"),
281
297
  Effect.forever,
282
298
  Effect.interruptible,
283
299
  Effect.forkIn(shardingScope)
@@ -384,6 +400,11 @@ const make = Effect.gen(function*() {
384
400
 
385
401
  let storageAlreadyProcessed = (_message: Message.IncomingRequest<any>) => true
386
402
 
403
+ // keep track of the last sent request ids to avoid duplicates
404
+ // we only keep the last 30 sets to avoid memory leaks
405
+ const sentRequestIds = new Set<Snowflake.Snowflake>()
406
+ const sentRequestIdSets = new Set<Set<Snowflake.Snowflake>>()
407
+
387
408
  if (storageEnabled && Option.isSome(config.runnerAddress)) {
388
409
  const selfAddress = config.runnerAddress.value
389
410
 
@@ -391,10 +412,8 @@ const make = Effect.gen(function*() {
391
412
  yield* Effect.logDebug("Starting")
392
413
  yield* Effect.addFinalizer(() => Effect.logDebug("Shutting down"))
393
414
 
394
- // keep track of the last sent request ids to avoid duplicates
395
- // we only keep the last 30 sets to avoid memory leaks
396
- const sentRequestIds = new Set<Snowflake.Snowflake>()
397
- const sentRequestIdSets = new Set<Set<Snowflake.Snowflake>>()
415
+ sentRequestIds.clear()
416
+ sentRequestIdSets.clear()
398
417
 
399
418
  storageAlreadyProcessed = (message: Message.IncomingRequest<any>) => {
400
419
  if (!sentRequestIds.has(message.envelope.requestId)) {
@@ -755,6 +774,17 @@ const make = Effect.gen(function*() {
755
774
  )
756
775
  }
757
776
 
777
+ const reset: Sharding["Type"]["reset"] = Effect.fnUntraced(
778
+ function*(requestId) {
779
+ yield* storage.clearReplies(requestId)
780
+ sentRequestIds.delete(requestId)
781
+ },
782
+ Effect.matchCause({
783
+ onSuccess: () => true,
784
+ onFailure: () => false
785
+ })
786
+ )
787
+
758
788
  // --- Shard Manager sync ---
759
789
 
760
790
  const shardManagerTimeoutFiber = yield* FiberHandle.make().pipe(
@@ -903,6 +933,7 @@ const make = Effect.gen(function*() {
903
933
  never
904
934
  > = yield* ResourceMap.make(Effect.fnUntraced(function*(entity: Entity<any>) {
905
935
  const client = yield* RpcClient.makeNoSerialization(entity.protocol, {
936
+ spanPrefix: `${entity.type}.client`,
906
937
  supportsAck: true,
907
938
  generateRequestId: () => RequestId(snowflakeGen.unsafeNext()),
908
939
  onFromClient(options): Effect.Effect<
@@ -968,6 +999,9 @@ const make = Effect.gen(function*() {
968
999
  const entry = clientRequests.get(requestId)!
969
1000
  if (!entry) return Effect.void
970
1001
  clientRequests.delete(requestId)
1002
+ if (Context.get(entry.rpc.annotations, Uninterruptible)) {
1003
+ return Effect.void
1004
+ }
971
1005
  // for durable messages, we ignore interrupts on shutdown or as a
972
1006
  // result of a shard being resassigned
973
1007
  const isTransientInterrupt = MutableRef.get(isShutdown) ||
@@ -1148,8 +1182,10 @@ const make = Effect.gen(function*() {
1148
1182
  registerSingleton,
1149
1183
  makeClient,
1150
1184
  send: sendLocal,
1185
+ sendOutgoing: (message, discard) => sendOutgoing(message, discard),
1151
1186
  notify: (message) => notifyLocal(message, false),
1152
- activeEntityCount
1187
+ activeEntityCount,
1188
+ reset
1153
1189
  })
1154
1190
 
1155
1191
  return sharding
package/src/Snowflake.ts CHANGED
@@ -145,7 +145,7 @@ export const makeGenerator: Effect.Effect<Snowflake.Generator> = Effect.gen(func
145
145
  const clock = yield* Effect.clock
146
146
 
147
147
  let sequence = 0
148
- let sequenceAt = clock.unsafeCurrentTimeMillis()
148
+ let sequenceAt = Math.floor(clock.unsafeCurrentTimeMillis())
149
149
 
150
150
  return identity<Snowflake.Generator>({
151
151
  setMachineId: (newMachineId) =>
@@ -153,7 +153,7 @@ export const makeGenerator: Effect.Effect<Snowflake.Generator> = Effect.gen(func
153
153
  machineId = newMachineId
154
154
  }),
155
155
  unsafeNext() {
156
- let now = clock.unsafeCurrentTimeMillis()
156
+ let now = Math.floor(clock.unsafeCurrentTimeMillis())
157
157
 
158
158
  // account for clock drift, only allow time to move forward
159
159
  if (now < sequenceAt) {
@@ -663,6 +663,28 @@ export const make = Effect.fnUntraced(function*(options?: {
663
663
  PersistenceError.refail
664
664
  ),
665
665
 
666
+ clearReplies: Effect.fnUntraced(
667
+ function*(requestId) {
668
+ yield* sql`DELETE FROM ${repliesTableSql} WHERE request_id = ${String(requestId)}`
669
+ yield* sql`UPDATE ${messagesTableSql} SET processed = ${sqlFalse}, last_reply_id = NULL, last_read = NULL WHERE request_id = ${
670
+ String(requestId)
671
+ }`
672
+ },
673
+ sql.withTransaction,
674
+ PersistenceError.refail
675
+ ),
676
+
677
+ requestIdForPrimaryKey: (primaryKey) =>
678
+ sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${primaryKey}`.pipe(
679
+ Effect.map((rows) =>
680
+ Option.fromNullable(rows[0]?.id).pipe(
681
+ Option.map(Snowflake.Snowflake)
682
+ )
683
+ ),
684
+ Effect.provideService(SqlClient.SafeIntegers, true),
685
+ PersistenceError.refail
686
+ ),
687
+
666
688
  repliesFor: (requestIds) =>
667
689
  // replies where:
668
690
  // - the request is in the list
@@ -682,22 +704,20 @@ export const make = Effect.fnUntraced(function*(options?: {
682
704
  ORDER BY rowid ASC
683
705
  `.unprepared.pipe(
684
706
  Effect.provideService(SqlClient.SafeIntegers, true),
685
- Effect.map(Arr.map((row): Reply.ReplyEncoded<any> =>
686
- Number(row.kind) === replyKind.WithExit ?
687
- ({
688
- _tag: "WithExit",
689
- id: String(row.id),
690
- requestId: String(row.request_id),
691
- exit: JSON.parse(row.payload)
692
- }) :
693
- {
694
- _tag: "Chunk",
695
- id: String(row.id),
696
- requestId: String(row.request_id),
697
- values: JSON.parse(row.payload),
698
- sequence: Number(row.sequence!)
699
- }
700
- )),
707
+ Effect.map(Arr.map(replyFromRow)),
708
+ PersistenceError.refail,
709
+ withTracerDisabled
710
+ ),
711
+
712
+ repliesForUnfiltered: (requestIds) =>
713
+ sql<ReplyRow>`
714
+ SELECT id, kind, request_id, payload, sequence
715
+ FROM ${repliesTableSql}
716
+ WHERE request_id IN (${sql.literal(requestIds.join(","))})
717
+ ORDER BY rowid ASC
718
+ `.unprepared.pipe(
719
+ Effect.provideService(SqlClient.SafeIntegers, true),
720
+ Effect.map(Arr.map(replyFromRow)),
701
721
  PersistenceError.refail,
702
722
  withTracerDisabled
703
723
  ),
@@ -761,6 +781,28 @@ export const make = Effect.fnUntraced(function*(options?: {
761
781
  withTracerDisabled
762
782
  ),
763
783
 
784
+ clearAddress: (address) =>
785
+ sql`
786
+ DELETE FROM ${repliesTableSql}
787
+ WHERE request_id IN (
788
+ SELECT id FROM ${messagesTableSql}
789
+ WHERE entity_type = ${address.entityType}
790
+ AND entity_id = ${address.entityId}
791
+ )
792
+ `.pipe(
793
+ Effect.andThen(
794
+ sql`
795
+ DELETE FROM ${messagesTableSql}
796
+ WHERE entity_type = ${address.entityType}
797
+ AND entity_id = ${address.entityId}
798
+ `
799
+ ),
800
+ sql.withTransaction,
801
+ Effect.asVoid,
802
+ PersistenceError.refail,
803
+ withTracerDisabled
804
+ ),
805
+
764
806
  resetShards: (shardIds) =>
765
807
  sql`
766
808
  UPDATE ${messagesTableSql}
@@ -814,6 +856,22 @@ const replyKind = {
814
856
  "Chunk": null
815
857
  } as const satisfies Record<Reply.Reply<any>["_tag"], number | null>
816
858
 
859
+ const replyFromRow = (row: ReplyRow): Reply.ReplyEncoded<any> =>
860
+ Number(row.kind) === replyKind.WithExit ?
861
+ {
862
+ _tag: "WithExit",
863
+ id: String(row.id),
864
+ requestId: String(row.request_id),
865
+ exit: JSON.parse(row.payload)
866
+ } :
867
+ {
868
+ _tag: "Chunk",
869
+ id: String(row.id),
870
+ requestId: String(row.request_id),
871
+ values: JSON.parse(row.payload),
872
+ sequence: Number(row.sequence!)
873
+ }
874
+
817
875
  type MessageRow = {
818
876
  readonly id: string | bigint
819
877
  readonly message_id: string | null
@@ -143,10 +143,10 @@ export const make = Effect.fnUntraced(function*(options?: {
143
143
  const sqlNow = sql.literal(sqlNowString)
144
144
 
145
145
  const lockExpiresAt = sql.onDialectOrElse({
146
- pg: () => sql`${sqlNow} - INTERVAL '15 seconds'`,
147
- mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL 15 SECOND)`,
148
- mssql: () => sql`DATEADD(SECOND, -15, ${sqlNow})`,
149
- orElse: () => sql`datetime(${sqlNow}, '-15 seconds')`
146
+ pg: () => sql`${sqlNow} - INTERVAL '5 seconds'`,
147
+ mysql: () => sql`DATE_SUB(${sqlNow}, INTERVAL 5 SECOND)`,
148
+ mssql: () => sql`DATEADD(SECOND, -5, ${sqlNow})`,
149
+ orElse: () => sql`datetime(${sqlNow}, '-5 seconds')`
150
150
  })
151
151
 
152
152
  const acquireLock = sql.onDialectOrElse({
@@ -170,7 +170,7 @@ export const make = Effect.fnUntraced(function*(options?: {
170
170
  MERGE ${locksTableSql} WITH (HOLDLOCK) AS target
171
171
  USING (SELECT * FROM (VALUES ${sql.csv(values)})) AS source (shard_id, address, acquired_at)
172
172
  ON target.shard_id = source.shard_id
173
- WHEN MATCHED AND (target.address = source.address OR DATEDIFF(SECOND, target.acquired_at, ${sqlNow}) > 15) THEN
173
+ WHEN MATCHED AND (target.address = source.address OR DATEDIFF(SECOND, target.acquired_at, ${sqlNow}) > 5) THEN
174
174
  UPDATE SET address = source.address, acquired_at = source.acquired_at
175
175
  WHEN NOT MATCHED THEN
176
176
  INSERT (shard_id, address, acquired_at)
@@ -187,7 +187,7 @@ export const make = Effect.fnUntraced(function*(options?: {
187
187
  SELECT 1 FROM ${locksTableSql}
188
188
  WHERE shard_id = source.shard_id
189
189
  AND address != ${address}
190
- AND (strftime('%s', ${sqlNow}) - strftime('%s', acquired_at)) <= 15
190
+ AND (strftime('%s', ${sqlNow}) - strftime('%s', acquired_at)) <= 5
191
191
  )
192
192
  ON CONFLICT(shard_id) DO UPDATE
193
193
  SET address = ${address}, acquired_at = ${sqlNow}
package/src/index.ts CHANGED
@@ -13,6 +13,11 @@ export * as ClusterMetrics from "./ClusterMetrics.js"
13
13
  */
14
14
  export * as ClusterSchema from "./ClusterSchema.js"
15
15
 
16
+ /**
17
+ * @since 1.0.0
18
+ */
19
+ export * as ClusterWorkflowEngine from "./ClusterWorkflowEngine.js"
20
+
16
21
  /**
17
22
  * @since 1.0.0
18
23
  */