@effect/cluster 0.37.1 → 0.38.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.
Files changed (142) hide show
  1. package/ClusterCron/package.json +6 -0
  2. package/dist/cjs/ClusterCron.js +86 -0
  3. package/dist/cjs/ClusterCron.js.map +1 -0
  4. package/dist/cjs/ClusterSchema.js +9 -1
  5. package/dist/cjs/ClusterSchema.js.map +1 -1
  6. package/dist/cjs/ClusterWorkflowEngine.js +99 -78
  7. package/dist/cjs/ClusterWorkflowEngine.js.map +1 -1
  8. package/dist/cjs/Entity.js +6 -1
  9. package/dist/cjs/Entity.js.map +1 -1
  10. package/dist/cjs/EntityAddress.js +8 -1
  11. package/dist/cjs/EntityAddress.js.map +1 -1
  12. package/dist/cjs/MessageStorage.js +6 -4
  13. package/dist/cjs/MessageStorage.js.map +1 -1
  14. package/dist/cjs/Runner.js +15 -0
  15. package/dist/cjs/Runner.js.map +1 -1
  16. package/dist/cjs/RunnerAddress.js +8 -1
  17. package/dist/cjs/RunnerAddress.js.map +1 -1
  18. package/dist/cjs/Runners.js +5 -0
  19. package/dist/cjs/Runners.js.map +1 -1
  20. package/dist/cjs/ShardId.js +75 -7
  21. package/dist/cjs/ShardId.js.map +1 -1
  22. package/dist/cjs/ShardManager.js +63 -43
  23. package/dist/cjs/ShardManager.js.map +1 -1
  24. package/dist/cjs/ShardStorage.js +45 -36
  25. package/dist/cjs/ShardStorage.js.map +1 -1
  26. package/dist/cjs/Sharding.js +45 -37
  27. package/dist/cjs/Sharding.js.map +1 -1
  28. package/dist/cjs/ShardingConfig.js +9 -2
  29. package/dist/cjs/ShardingConfig.js.map +1 -1
  30. package/dist/cjs/Singleton.js +2 -2
  31. package/dist/cjs/Singleton.js.map +1 -1
  32. package/dist/cjs/SingletonAddress.js +2 -2
  33. package/dist/cjs/SingletonAddress.js.map +1 -1
  34. package/dist/cjs/SqlMessageStorage.js +32 -27
  35. package/dist/cjs/SqlMessageStorage.js.map +1 -1
  36. package/dist/cjs/SqlShardStorage.js +14 -14
  37. package/dist/cjs/SqlShardStorage.js.map +1 -1
  38. package/dist/cjs/index.js +3 -1
  39. package/dist/cjs/internal/entityManager.js +2 -1
  40. package/dist/cjs/internal/entityManager.js.map +1 -1
  41. package/dist/cjs/internal/shardManager.js +138 -37
  42. package/dist/cjs/internal/shardManager.js.map +1 -1
  43. package/dist/dts/ClusterCron.d.ts +37 -0
  44. package/dist/dts/ClusterCron.d.ts.map +1 -0
  45. package/dist/dts/ClusterSchema.d.ts +8 -0
  46. package/dist/dts/ClusterSchema.d.ts.map +1 -1
  47. package/dist/dts/ClusterWorkflowEngine.d.ts +4 -4
  48. package/dist/dts/ClusterWorkflowEngine.d.ts.map +1 -1
  49. package/dist/dts/Entity.d.ts +10 -0
  50. package/dist/dts/Entity.d.ts.map +1 -1
  51. package/dist/dts/EntityAddress.d.ts +9 -3
  52. package/dist/dts/EntityAddress.d.ts.map +1 -1
  53. package/dist/dts/MessageStorage.d.ts +3 -3
  54. package/dist/dts/MessageStorage.d.ts.map +1 -1
  55. package/dist/dts/Runner.d.ts +15 -0
  56. package/dist/dts/Runner.d.ts.map +1 -1
  57. package/dist/dts/RunnerAddress.d.ts +5 -0
  58. package/dist/dts/RunnerAddress.d.ts.map +1 -1
  59. package/dist/dts/Runners.d.ts.map +1 -1
  60. package/dist/dts/ShardId.d.ts +60 -6
  61. package/dist/dts/ShardId.d.ts.map +1 -1
  62. package/dist/dts/ShardManager.d.ts +13 -13
  63. package/dist/dts/ShardManager.d.ts.map +1 -1
  64. package/dist/dts/ShardStorage.d.ts +11 -14
  65. package/dist/dts/ShardStorage.d.ts.map +1 -1
  66. package/dist/dts/Sharding.d.ts +4 -2
  67. package/dist/dts/Sharding.d.ts.map +1 -1
  68. package/dist/dts/ShardingConfig.d.ts +32 -6
  69. package/dist/dts/ShardingConfig.d.ts.map +1 -1
  70. package/dist/dts/Singleton.d.ts +3 -1
  71. package/dist/dts/Singleton.d.ts.map +1 -1
  72. package/dist/dts/SingletonAddress.d.ts +4 -3
  73. package/dist/dts/SingletonAddress.d.ts.map +1 -1
  74. package/dist/dts/SqlMessageStorage.d.ts +3 -2
  75. package/dist/dts/SqlMessageStorage.d.ts.map +1 -1
  76. package/dist/dts/SqlShardStorage.d.ts +1 -1
  77. package/dist/dts/index.d.ts +4 -0
  78. package/dist/dts/index.d.ts.map +1 -1
  79. package/dist/esm/ClusterCron.js +77 -0
  80. package/dist/esm/ClusterCron.js.map +1 -0
  81. package/dist/esm/ClusterSchema.js +7 -0
  82. package/dist/esm/ClusterSchema.js.map +1 -1
  83. package/dist/esm/ClusterWorkflowEngine.js +99 -78
  84. package/dist/esm/ClusterWorkflowEngine.js.map +1 -1
  85. package/dist/esm/Entity.js +6 -1
  86. package/dist/esm/Entity.js.map +1 -1
  87. package/dist/esm/EntityAddress.js +8 -1
  88. package/dist/esm/EntityAddress.js.map +1 -1
  89. package/dist/esm/MessageStorage.js +6 -4
  90. package/dist/esm/MessageStorage.js.map +1 -1
  91. package/dist/esm/Runner.js +15 -0
  92. package/dist/esm/Runner.js.map +1 -1
  93. package/dist/esm/RunnerAddress.js +8 -1
  94. package/dist/esm/RunnerAddress.js.map +1 -1
  95. package/dist/esm/Runners.js +5 -0
  96. package/dist/esm/Runners.js.map +1 -1
  97. package/dist/esm/ShardId.js +73 -6
  98. package/dist/esm/ShardId.js.map +1 -1
  99. package/dist/esm/ShardManager.js +64 -45
  100. package/dist/esm/ShardManager.js.map +1 -1
  101. package/dist/esm/ShardStorage.js +44 -36
  102. package/dist/esm/ShardStorage.js.map +1 -1
  103. package/dist/esm/Sharding.js +45 -37
  104. package/dist/esm/Sharding.js.map +1 -1
  105. package/dist/esm/ShardingConfig.js +9 -2
  106. package/dist/esm/ShardingConfig.js.map +1 -1
  107. package/dist/esm/Singleton.js +2 -2
  108. package/dist/esm/Singleton.js.map +1 -1
  109. package/dist/esm/SingletonAddress.js +2 -2
  110. package/dist/esm/SingletonAddress.js.map +1 -1
  111. package/dist/esm/SqlMessageStorage.js +32 -27
  112. package/dist/esm/SqlMessageStorage.js.map +1 -1
  113. package/dist/esm/SqlShardStorage.js +14 -14
  114. package/dist/esm/SqlShardStorage.js.map +1 -1
  115. package/dist/esm/index.js +4 -0
  116. package/dist/esm/index.js.map +1 -1
  117. package/dist/esm/internal/entityManager.js +2 -1
  118. package/dist/esm/internal/entityManager.js.map +1 -1
  119. package/dist/esm/internal/shardManager.js +136 -36
  120. package/dist/esm/internal/shardManager.js.map +1 -1
  121. package/package.json +13 -5
  122. package/src/ClusterCron.ts +129 -0
  123. package/src/ClusterSchema.ts +9 -0
  124. package/src/ClusterWorkflowEngine.ts +93 -58
  125. package/src/Entity.ts +20 -1
  126. package/src/EntityAddress.ts +11 -1
  127. package/src/MessageStorage.ts +12 -7
  128. package/src/Runner.ts +18 -0
  129. package/src/RunnerAddress.ts +9 -1
  130. package/src/Runners.ts +5 -0
  131. package/src/ShardId.ts +81 -11
  132. package/src/ShardManager.ts +74 -45
  133. package/src/ShardStorage.ts +51 -47
  134. package/src/Sharding.ts +45 -39
  135. package/src/ShardingConfig.ts +36 -7
  136. package/src/Singleton.ts +5 -2
  137. package/src/SingletonAddress.ts +2 -2
  138. package/src/SqlMessageStorage.ts +36 -30
  139. package/src/SqlShardStorage.ts +15 -15
  140. package/src/index.ts +5 -0
  141. package/src/internal/entityManager.ts +2 -1
  142. package/src/internal/shardManager.ts +158 -52
@@ -2,6 +2,7 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
  import * as Rpc from "@effect/rpc/Rpc"
5
+ import { DurableDeferred } from "@effect/workflow"
5
6
  import * as Activity from "@effect/workflow/Activity"
6
7
  import * as DurableClock from "@effect/workflow/DurableClock"
7
8
  import * as Workflow from "@effect/workflow/Workflow"
@@ -23,9 +24,7 @@ import * as Entity from "./Entity.js"
23
24
  import { EntityAddress } from "./EntityAddress.js"
24
25
  import { EntityId } from "./EntityId.js"
25
26
  import { EntityType } from "./EntityType.js"
26
- import * as Message from "./Message.js"
27
27
  import { MessageStorage } from "./MessageStorage.js"
28
- import * as Reply from "./Reply.js"
29
28
  import type { WithExitEncoded } from "./Reply.js"
30
29
  import * as Sharding from "./Sharding.js"
31
30
  import * as Snowflake from "./Snowflake.js"
@@ -37,8 +36,8 @@ import * as Snowflake from "./Snowflake.js"
37
36
  export const make = Effect.gen(function*() {
38
37
  const sharding = yield* Sharding.Sharding
39
38
  const storage = yield* MessageStorage
40
- const snowflakeGen = yield* Snowflake.Generator
41
39
 
40
+ const workflows = new Map<string, Workflow.Any>()
42
41
  const entities = new Map<
43
42
  string,
44
43
  Entity.Entity<
@@ -69,16 +68,20 @@ export const make = Effect.gen(function*() {
69
68
  const deferredClient = yield* DeferredEntity.client
70
69
 
71
70
  const requestIdFor = Effect.fnUntraced(function*(options: {
71
+ readonly workflow: Workflow.Any
72
72
  readonly entityType: string
73
73
  readonly executionId: string
74
74
  readonly tag: string
75
75
  readonly id: string
76
76
  }) {
77
+ const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)(
78
+ options.executionId as EntityId
79
+ )
77
80
  const entityId = EntityId.make(options.executionId)
78
81
  const address = new EntityAddress({
79
82
  entityType: EntityType.make(options.entityType),
80
83
  entityId,
81
- shardId: sharding.getShardId(entityId)
84
+ shardId: sharding.getShardId(entityId, shardGroup)
82
85
  })
83
86
  return yield* storage.requestIdForPrimaryKey({ address, tag: options.tag, id: options.id })
84
87
  })
@@ -94,6 +97,7 @@ export const make = Effect.gen(function*() {
94
97
  })
95
98
 
96
99
  const requestReply = Effect.fnUntraced(function*(options: {
100
+ readonly workflow: Workflow.Any
97
101
  readonly entityType: string
98
102
  readonly executionId: string
99
103
  readonly tag: string
@@ -114,6 +118,7 @@ export const make = Effect.gen(function*() {
114
118
  readonly attempt: number
115
119
  }) {
116
120
  const requestId = yield* requestIdFor({
121
+ workflow: options.workflow,
117
122
  entityType: `Workflow/${options.workflow.name}`,
118
123
  executionId: options.executionId,
119
124
  tag: "activity",
@@ -129,13 +134,33 @@ export const make = Effect.gen(function*() {
129
134
  Effect.orDie
130
135
  )
131
136
 
137
+ const clearClock = Effect.fnUntraced(function*(options: {
138
+ readonly workflow: Workflow.Any
139
+ readonly executionId: string
140
+ }) {
141
+ const shardGroup = Context.get(options.workflow.annotations, ClusterSchema.ShardGroup)(
142
+ options.executionId as EntityId
143
+ )
144
+ const entityId = EntityId.make(options.executionId)
145
+ const shardId = sharding.getShardId(entityId, shardGroup)
146
+ const clockAddress = new EntityAddress({
147
+ entityType: ClockEntity.type,
148
+ entityId,
149
+ shardId
150
+ })
151
+ yield* storage.clearAddress(clockAddress)
152
+ })
153
+
132
154
  return WorkflowEngine.of({
133
- register: (workflow, execute) =>
134
- Effect.suspend(() => {
155
+ register(workflow, execute) {
156
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
157
+ const engine = this
158
+ return Effect.suspend(() => {
135
159
  if (entities.has(workflow.name)) {
136
160
  return Effect.dieMessage(`Workflow ${workflow.name} already registered`)
137
161
  }
138
162
  const entity = makeWorkflowEntity(workflow)
163
+ workflows.set(workflow.name, workflow)
139
164
  entities.set(workflow.name, entity as any)
140
165
  return sharding.registerEntity(
141
166
  entity,
@@ -143,18 +168,36 @@ export const make = Effect.gen(function*() {
143
168
  const address = yield* Entity.CurrentAddress
144
169
  const executionId = address.entityId
145
170
  return {
146
- run: (request: Entity.Request<any>) =>
147
- execute(request.payload, executionId).pipe(
171
+ run: (request: Entity.Request<any>) => {
172
+ const instance = WorkflowInstance.of({
173
+ workflow,
174
+ executionId,
175
+ suspended: false
176
+ })
177
+ return execute(request.payload, executionId).pipe(
178
+ Effect.onExit(() => {
179
+ if (!instance.suspended) {
180
+ return Effect.void
181
+ }
182
+ return engine.deferredResult(InterruptSignal).pipe(
183
+ Effect.flatMap((maybeResult) => {
184
+ if (Option.isNone(maybeResult)) {
185
+ return Effect.void
186
+ }
187
+ instance.suspended = false
188
+ return Effect.zipRight(
189
+ Effect.ignore(clearClock({ workflow, executionId })),
190
+ Effect.interrupt
191
+ )
192
+ }),
193
+ Effect.orDie
194
+ )
195
+ }),
196
+ Effect.scoped,
148
197
  Workflow.intoResult,
149
- Effect.provideService(
150
- WorkflowInstance,
151
- WorkflowInstance.of({
152
- workflow,
153
- executionId,
154
- suspended: false
155
- })
156
- )
157
- ) as any,
198
+ Effect.provideService(WorkflowInstance, instance)
199
+ ) as any
200
+ },
158
201
  activity: Effect.fnUntraced(function*(request: Entity.Request<any>) {
159
202
  const activityId = `${executionId}/${request.payload.name}`
160
203
  let entry = activities.get(activityId)
@@ -185,7 +228,8 @@ export const make = Effect.gen(function*() {
185
228
  }
186
229
  })
187
230
  ) as Effect.Effect<void>
188
- }),
231
+ })
232
+ },
189
233
 
190
234
  execute: ({ discard, executionId, payload, workflow }) =>
191
235
  RcMap.get(clients, workflow.name).pipe(
@@ -195,8 +239,9 @@ export const make = Effect.gen(function*() {
195
239
  ),
196
240
 
197
241
  interrupt: Effect.fnUntraced(
198
- function*(workflow, executionId) {
242
+ function*(this: WorkflowEngine["Type"], workflow, executionId) {
199
243
  const requestId = yield* requestIdFor({
244
+ workflow,
200
245
  entityType: `Workflow/${workflow.name}`,
201
246
  executionId,
202
247
  tag: "run",
@@ -213,41 +258,12 @@ export const make = Effect.gen(function*() {
213
258
  return
214
259
  }
215
260
 
216
- const entityId = EntityId.make(executionId)
217
- const shardId = sharding.getShardId(entityId)
218
- const workflowAddress = new EntityAddress({
219
- entityType: EntityType.make(`Workflow/${workflow.name}`),
220
- entityId,
221
- shardId
222
- })
223
- const deferredAddress = new EntityAddress({
224
- entityType: DeferredEntity.type,
225
- entityId,
226
- shardId
227
- })
228
- const clockAddress = new EntityAddress({
229
- entityType: ClockEntity.type,
230
- entityId,
231
- shardId
261
+ yield* this.deferredDone({
262
+ workflowName: workflow.name,
263
+ executionId,
264
+ deferred: InterruptSignal,
265
+ exit: { _tag: "Success", value: void 0 }
232
266
  })
233
- if (Option.isNone(reply)) {
234
- yield* sharding.sendOutgoing(
235
- Message.OutgoingEnvelope.interrupt({
236
- address: workflowAddress,
237
- id: snowflakeGen.unsafeNext(),
238
- requestId: requestId.value
239
- }),
240
- true
241
- )
242
- } else {
243
- yield* sharding.reset(requestId.value)
244
- }
245
- yield* storage.saveReply(Reply.ReplyWithContext.interrupt({
246
- id: snowflakeGen.unsafeNext(),
247
- requestId: requestId.value
248
- }))
249
- yield* storage.clearAddress(deferredAddress)
250
- yield* storage.clearAddress(clockAddress)
251
267
  },
252
268
  Effect.retry({
253
269
  while: (e) => e._tag === "PersistenceError",
@@ -259,7 +275,12 @@ export const make = Effect.gen(function*() {
259
275
 
260
276
  resume: Effect.fnUntraced(
261
277
  function*(workflowName: string, executionId: string) {
278
+ const workflow = workflows.get(workflowName)
279
+ if (!workflow) {
280
+ return yield* Effect.dieMessage(`WorkflowEngine.resume: ${workflowName} not registered`)
281
+ }
262
282
  const maybeReply = yield* requestReply({
283
+ workflow,
263
284
  entityType: `Workflow/${workflowName}`,
264
285
  executionId,
265
286
  tag: "run",
@@ -313,6 +334,7 @@ export const make = Effect.gen(function*() {
313
334
  WorkflowInstance.pipe(
314
335
  Effect.flatMap((instance) =>
315
336
  requestReply({
337
+ workflow: instance.workflow,
316
338
  entityType: DeferredEntity.type,
317
339
  executionId: instance.executionId,
318
340
  tag: "set",
@@ -353,6 +375,17 @@ export const make = Effect.gen(function*() {
353
375
  })
354
376
  })
355
377
 
378
+ const retryPolicy = Schedule.exponential(200, 1.5).pipe(
379
+ Schedule.union(Schedule.spaced("1 minute"))
380
+ )
381
+
382
+ const ensureSuccess = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
383
+ effect.pipe(
384
+ Effect.sandbox,
385
+ Effect.retry(retryPolicy),
386
+ Effect.orDie
387
+ )
388
+
356
389
  const ActivityRpc = Rpc.make("activity", {
357
390
  payload: {
358
391
  name: Schema.String,
@@ -377,6 +410,7 @@ const makeWorkflowEntity = (workflow: Workflow.Any) =>
377
410
  }),
378
411
  ActivityRpc
379
412
  ])
413
+ .annotateContext(workflow.annotations)
380
414
  .annotateRpcs(ClusterSchema.Persisted, true)
381
415
  .annotateRpcs(ClusterSchema.Uninterruptible, true)
382
416
 
@@ -417,7 +451,7 @@ const DeferredEntityLayer = DeferredEntity.toLayer(Effect.gen(function*() {
417
451
  return {
418
452
  set: (request) =>
419
453
  Effect.as(
420
- Effect.orDie(client.resume(request.payload, { discard: true })),
454
+ ensureSuccess(client.resume(request.payload, { discard: true })),
421
455
  request.payload.exit
422
456
  ),
423
457
  resume: (request) => engine.resume(request.payload.workflowName, executionId)
@@ -450,16 +484,18 @@ const ClockEntityLayer = ClockEntity.toLayer(Effect.gen(function*() {
450
484
  return {
451
485
  run(request) {
452
486
  const deferred = DurableClock.make({ name: request.payload.name, duration: Duration.zero }).deferred
453
- return engine.deferredDone({
487
+ return ensureSuccess(engine.deferredDone({
454
488
  workflowName: request.payload.workflowName,
455
489
  executionId,
456
490
  deferred,
457
491
  exit: { _tag: "Success", value: void 0 }
458
- })
492
+ }))
459
493
  }
460
494
  }
461
495
  }))
462
496
 
497
+ const InterruptSignal = DurableDeferred.make("Workflow/InterruptSignal")
498
+
463
499
  /**
464
500
  * @since 1.0.0
465
501
  * @category Layers
@@ -470,6 +506,5 @@ export const layer: Layer.Layer<
470
506
  Sharding.Sharding | MessageStorage
471
507
  > = DeferredEntityLayer.pipe(
472
508
  Layer.merge(ClockEntityLayer),
473
- Layer.provideMerge(Layer.scoped(WorkflowEngine, make)),
474
- Layer.provide(Snowflake.layerGenerator)
509
+ Layer.provideMerge(Layer.scoped(WorkflowEngine, make))
475
510
  )
package/src/Entity.ts CHANGED
@@ -26,6 +26,7 @@ import type {
26
26
  MailboxFull,
27
27
  PersistenceError
28
28
  } from "./ClusterError.js"
29
+ import { ShardGroup } from "./ClusterSchema.js"
29
30
  import { EntityAddress } from "./EntityAddress.js"
30
31
  import type { EntityId } from "./EntityId.js"
31
32
  import { EntityType } from "./EntityType.js"
@@ -68,6 +69,16 @@ export interface Entity<in out Rpcs extends Rpc.Any> extends Equal.Equal {
68
69
  */
69
70
  readonly protocol: RpcGroup.RpcGroup<Rpcs>
70
71
 
72
+ /**
73
+ * Get the shard group for the given EntityId.
74
+ */
75
+ getShardGroup(entityId: EntityId): string
76
+
77
+ /**
78
+ * Get the ShardId for the given EntityId.
79
+ */
80
+ getShardId(entityId: EntityId): Effect.Effect<ShardId.ShardId, never, Sharding>
81
+
71
82
  /**
72
83
  * Annotate the entity with a value.
73
84
  */
@@ -205,6 +216,9 @@ const Proto = {
205
216
  annotateRpcsContext<S>(this: Entity<any>, context: Context.Context<S>) {
206
217
  return fromRpcGroup(this.type, this.protocol.annotateRpcsContext(context))
207
218
  },
219
+ getShardId(this: Entity<any>, entityId: EntityId) {
220
+ return Effect.map(shardingTag, (sharding) => sharding.getShardId(entityId, this.getShardGroup(entityId)))
221
+ },
208
222
  get client() {
209
223
  return shardingTag.pipe(
210
224
  Effect.flatMap((sharding) => sharding.makeClient(this as any))
@@ -341,6 +355,7 @@ export const fromRpcGroup = <Rpcs extends Rpc.Any>(
341
355
  const self = Object.create(Proto)
342
356
  self.type = EntityType.make(type)
343
357
  self.protocol = protocol
358
+ self.getShardGroup = Context.get(protocol.annotations, ShardGroup)
344
359
  return self
345
360
  }
346
361
 
@@ -470,7 +485,11 @@ export const makeTestClient: <Rpcs extends Rpc.Any, LA, LE, LR>(
470
485
  layer: Layer.Layer<LA, LE, LR>
471
486
  ) {
472
487
  const config = yield* ShardingConfig
473
- const makeShardId = (entityId: string) => ShardId.make((Math.abs(hashString(entityId) % config.numberOfShards)) + 1)
488
+ const makeShardId = (entityId: string) =>
489
+ ShardId.make(
490
+ entity.getShardGroup(entityId as EntityId),
491
+ (Math.abs(hashString(entityId) % config.shardsPerGroup)) + 1
492
+ )
474
493
  const snowflakeGen = yield* Snowflake.makeGenerator
475
494
  const runnerAddress = new RunnerAddress({ host: "localhost", port: 3000 })
476
495
  const entityMap = new Map<string, {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import * as Equal from "effect/Equal"
4
5
  import * as Hash from "effect/Hash"
5
6
  import * as Schema from "effect/Schema"
6
7
  import { EntityId } from "./EntityId.js"
@@ -36,11 +37,20 @@ export class EntityAddress extends Schema.Class<EntityAddress>(SymbolKey)({
36
37
  * @since 1.0.0
37
38
  */
38
39
  readonly [TypeId] = TypeId;
40
+
41
+ /**
42
+ * @since 1.0.0
43
+ */
44
+ [Equal.symbol](that: EntityAddress): boolean {
45
+ return this.entityType === that.entityType && this.entityId === that.entityId &&
46
+ this.shardId[Equal.symbol](that.shardId)
47
+ }
48
+
39
49
  /**
40
50
  * @since 1.0.0
41
51
  */
42
52
  [Hash.symbol]() {
43
- return Hash.cached(this)(Hash.string(`${this.shardId}:${this.entityType}:${this.entityId}`))
53
+ return Hash.cached(this, Hash.string(`${this.entityType}:${this.entityId}:${this.shardId.toString()}`))
44
54
  }
45
55
  }
46
56
 
@@ -22,7 +22,7 @@ import type { EntityAddress } from "./EntityAddress.js"
22
22
  import * as Envelope from "./Envelope.js"
23
23
  import * as Message from "./Message.js"
24
24
  import * as Reply from "./Reply.js"
25
- import type { ShardId } from "./ShardId.js"
25
+ import { ShardId } from "./ShardId.js"
26
26
  import type { ShardingConfig } from "./ShardingConfig.js"
27
27
  import * as Snowflake from "./Snowflake.js"
28
28
 
@@ -266,7 +266,7 @@ export type Encoded = {
266
266
  * - All Interrupt's for unprocessed requests
267
267
  */
268
268
  readonly unprocessedMessages: (
269
- shardIds: ReadonlyArray<number>,
269
+ shardIds: ReadonlyArray<string>,
270
270
  now: number
271
271
  ) => Effect.Effect<
272
272
  Array<{
@@ -308,7 +308,7 @@ export type Encoded = {
308
308
  * Reset the mailbox state for the provided shards.
309
309
  */
310
310
  readonly resetShards: (
311
- shardIds: ReadonlyArray<number>
311
+ shardIds: ReadonlyArray<string>
312
312
  ) => Effect.Effect<void, PersistenceError>
313
313
  }
314
314
 
@@ -441,7 +441,9 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
441
441
  const shards = Array.from(shardIds)
442
442
  if (shards.length === 0) return Effect.succeed([])
443
443
  return Effect.flatMap(
444
- Effect.suspend(() => encoded.unprocessedMessages(shards, clock.unsafeCurrentTimeMillis())),
444
+ Effect.suspend(() =>
445
+ encoded.unprocessedMessages(shards.map((id) => id.toString()), clock.unsafeCurrentTimeMillis())
446
+ ),
445
447
  decodeMessages
446
448
  )
447
449
  },
@@ -455,7 +457,7 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect<
455
457
  },
456
458
  resetAddress: encoded.resetAddress,
457
459
  clearAddress: encoded.clearAddress,
458
- resetShards: (shardIds) => encoded.resetShards(Array.from(shardIds))
460
+ resetShards: (shardIds) => encoded.resetShards(Array.from(shardIds, (id) => id.toString()))
459
461
  })
460
462
 
461
463
  const decodeMessages = (
@@ -666,7 +668,9 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
666
668
  if (existing) {
667
669
  return SaveResultEncoded.Duplicate({
668
670
  originalId: Snowflake.Snowflake(existing.envelope.requestId),
669
- lastReceivedReply: existing.lastReceivedChunk
671
+ lastReceivedReply: existing.replies.length === 1 && existing.replies[0]._tag === "WithExit"
672
+ ? Option.some(existing.replies[0])
673
+ : existing.lastReceivedChunk
670
674
  })
671
675
  }
672
676
  if (envelope._tag === "Request") {
@@ -726,7 +730,8 @@ export class MemoryDriver extends Effect.Service<MemoryDriver>()("@effect/cluste
726
730
  let index = journal.indexOf(Iterable.unsafeHead(unprocessed))
727
731
  for (; index < journal.length; index++) {
728
732
  const envelope = journal[index]
729
- if (!shardIds.includes(envelope.address.shardId)) {
733
+ const shardId = ShardId.make(envelope.address.shardId)
734
+ if (!shardIds.includes(shardId.toString())) {
730
735
  continue
731
736
  }
732
737
  if (envelope._tag === "Request") {
package/src/Runner.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import * as Equal from "effect/Equal"
5
+ import * as Hash from "effect/Hash"
4
6
  import { NodeInspectSymbol } from "effect/Inspectable"
5
7
  import * as Pretty from "effect/Pretty"
6
8
  import * as Schema from "effect/Schema"
@@ -35,6 +37,7 @@ export type TypeId = typeof TypeId
35
37
  */
36
38
  export class Runner extends Schema.Class<Runner>(SymbolKey)({
37
39
  address: RunnerAddress,
40
+ groups: Schema.Array(Schema.String),
38
41
  version: Schema.Int
39
42
  }) {
40
43
  /**
@@ -63,6 +66,20 @@ export class Runner extends Schema.Class<Runner>(SymbolKey)({
63
66
  [NodeInspectSymbol](): string {
64
67
  return this.toString()
65
68
  }
69
+
70
+ /**
71
+ * @since 1.0.0
72
+ */
73
+ [Equal.symbol](that: Runner): boolean {
74
+ return this.address[Equal.symbol](that.address) && this.version === that.version
75
+ }
76
+
77
+ /**
78
+ * @since 1.0.0
79
+ */
80
+ [Hash.symbol](): number {
81
+ return Hash.cached(this, Hash.string(`${this.address.toString()}:${this.version}`))
82
+ }
66
83
  }
67
84
 
68
85
  /**
@@ -80,5 +97,6 @@ export class Runner extends Schema.Class<Runner>(SymbolKey)({
80
97
  */
81
98
  export const make = (props: {
82
99
  readonly address: RunnerAddress
100
+ readonly groups: ReadonlyArray<string>
83
101
  readonly version: number
84
102
  }): Runner => new Runner(props)
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import * as Equal from "effect/Equal"
4
5
  import * as Hash from "effect/Hash"
5
6
  import { NodeInspectSymbol } from "effect/Inspectable"
6
7
  import * as Schema from "effect/Schema"
@@ -32,11 +33,18 @@ export class RunnerAddress extends Schema.Class<RunnerAddress>(SymbolKey)({
32
33
  */
33
34
  readonly [TypeId] = TypeId;
34
35
 
36
+ /**
37
+ * @since 1.0.0
38
+ */
39
+ [Equal.symbol](that: RunnerAddress): boolean {
40
+ return this.host === that.host && this.port === that.port
41
+ }
42
+
35
43
  /**
36
44
  * @since 1.0.0
37
45
  */
38
46
  [Hash.symbol]() {
39
- return Hash.cached(this)(Hash.string(this.toString()))
47
+ return Hash.cached(this, Hash.string(this.toString()))
40
48
  }
41
49
 
42
50
  /**
package/src/Runners.ts CHANGED
@@ -164,6 +164,11 @@ export const make: (options: Omit<Runners["Type"], "sendLocal" | "notifyLocal">)
164
164
  MessageStorage.SaveResult.$match({
165
165
  Success: () => afterPersist(message, false),
166
166
  Duplicate: ({ lastReceivedReply, originalId }) => {
167
+ // If the last received reply is an exit, we can just return it
168
+ // as the response.
169
+ if (Option.isSome(lastReceivedReply) && lastReceivedReply.value._tag === "WithExit") {
170
+ return message.respond(lastReceivedReply.value.withRequestId(message.envelope.requestId))
171
+ }
167
172
  requestIdRewrites.set(message.envelope.requestId, originalId)
168
173
  return afterPersist(
169
174
  new Message.OutgoingRequest({
package/src/ShardId.ts CHANGED
@@ -1,27 +1,97 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import * as Schema from "effect/Schema"
4
+ import * as Equal from "effect/Equal"
5
+ import * as Hash from "effect/Hash"
6
+ import * as S from "effect/Schema"
5
7
 
6
8
  /**
7
9
  * @since 1.0.0
8
- * @category constructors
10
+ * @category Symbols
9
11
  */
10
- export const ShardId = Schema.Int.pipe(
11
- Schema.brand("ShardId"),
12
- Schema.annotations({
13
- pretty: () => (shardId) => `ShardId(${shardId})`
14
- })
15
- )
12
+ export const TypeId: unique symbol = Symbol.for("@effect/cluster/ShardId")
16
13
 
17
14
  /**
18
15
  * @since 1.0.0
19
- * @category models
16
+ * @category Symbols
20
17
  */
21
- export type ShardId = typeof ShardId.Type
18
+ export type TypeId = typeof TypeId
19
+
20
+ const constDisableValidation = { disableValidation: true }
22
21
 
23
22
  /**
24
23
  * @since 1.0.0
25
24
  * @category Constructors
26
25
  */
27
- export const make = (shardId: number): ShardId => ShardId.make(shardId)
26
+ export const make = (group: string, id: number): ShardId => new ShardId({ group, id }, constDisableValidation)
27
+
28
+ /**
29
+ * @since 1.0.0
30
+ * @category Models
31
+ */
32
+ export class ShardId extends S.Class<ShardId>("@effect/cluster/ShardId")({
33
+ group: S.String,
34
+ id: S.Int
35
+ }) {
36
+ /**
37
+ * @since 1.0.0
38
+ */
39
+ readonly [TypeId]: TypeId = TypeId;
40
+
41
+ /**
42
+ * @since 1.0.0
43
+ */
44
+ [Equal.symbol](that: ShardId): boolean {
45
+ return this.group === that.group && this.id === that.id
46
+ }
47
+
48
+ /**
49
+ * @since 1.0.0
50
+ */
51
+ [Hash.symbol](): number {
52
+ return Hash.cached(this, Hash.string(this.toString()))
53
+ }
54
+
55
+ /**
56
+ * @since 1.0.0
57
+ */
58
+ toString(): string {
59
+ return `${this.group}:${this.id}`
60
+ }
61
+
62
+ /**
63
+ * @since 1.0.0
64
+ */
65
+ static toString(shardId: {
66
+ readonly group: string
67
+ readonly id: number
68
+ }): string {
69
+ return `${shardId.group}:${shardId.id}`
70
+ }
71
+
72
+ /**
73
+ * @since 1.0.0
74
+ */
75
+ static fromStringEncoded(s: string): {
76
+ readonly group: string
77
+ readonly id: number
78
+ } {
79
+ const index = s.lastIndexOf(":")
80
+ if (index === -1) {
81
+ throw new Error(`Invalid ShardId format`)
82
+ }
83
+ const group = s.substring(0, index)
84
+ const id = Number(s.substring(index + 1))
85
+ if (isNaN(id)) {
86
+ throw new Error(`ShardId id must be a number`)
87
+ }
88
+ return { group, id }
89
+ }
90
+
91
+ /**
92
+ * @since 1.0.0
93
+ */
94
+ static fromString(s: string): ShardId {
95
+ return new ShardId(ShardId.fromStringEncoded(s), constDisableValidation)
96
+ }
97
+ }