@alchemy.run/effect-cluster 0.0.0-stage → 2.0.0-beta.86

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 (233) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +105 -2
  3. package/lib/AWS/Database.d.ts +54 -0
  4. package/lib/AWS/Database.d.ts.map +1 -0
  5. package/lib/AWS/Database.js +96 -0
  6. package/lib/AWS/Database.js.map +1 -0
  7. package/lib/AWS/EcsPool.d.ts +56 -0
  8. package/lib/AWS/EcsPool.d.ts.map +1 -0
  9. package/lib/AWS/EcsPool.js +87 -0
  10. package/lib/AWS/EcsPool.js.map +1 -0
  11. package/lib/AWS/index.d.ts +7 -0
  12. package/lib/AWS/index.d.ts.map +1 -0
  13. package/lib/AWS/index.js +7 -0
  14. package/lib/AWS/index.js.map +1 -0
  15. package/lib/AWS/internal/Gateway.d.ts +23 -0
  16. package/lib/AWS/internal/Gateway.d.ts.map +1 -0
  17. package/lib/AWS/internal/Gateway.js +70 -0
  18. package/lib/AWS/internal/Gateway.js.map +1 -0
  19. package/lib/Cloudflare/DurableObjectPool.d.ts +42 -0
  20. package/lib/Cloudflare/DurableObjectPool.d.ts.map +1 -0
  21. package/lib/Cloudflare/DurableObjectPool.js +136 -0
  22. package/lib/Cloudflare/DurableObjectPool.js.map +1 -0
  23. package/lib/Cloudflare/PoolWorker.d.ts +186 -0
  24. package/lib/Cloudflare/PoolWorker.d.ts.map +1 -0
  25. package/lib/Cloudflare/PoolWorker.js +75 -0
  26. package/lib/Cloudflare/PoolWorker.js.map +1 -0
  27. package/lib/Cloudflare/index.d.ts +7 -0
  28. package/lib/Cloudflare/index.d.ts.map +1 -0
  29. package/lib/Cloudflare/index.js +7 -0
  30. package/lib/Cloudflare/index.js.map +1 -0
  31. package/lib/Cloudflare/internal/Objects.d.ts +38 -0
  32. package/lib/Cloudflare/internal/Objects.d.ts.map +1 -0
  33. package/lib/Cloudflare/internal/Objects.js +102 -0
  34. package/lib/Cloudflare/internal/Objects.js.map +1 -0
  35. package/lib/Cron.d.ts +143 -0
  36. package/lib/Cron.d.ts.map +1 -0
  37. package/lib/Cron.js +83 -0
  38. package/lib/Cron.js.map +1 -0
  39. package/lib/Database.d.ts +95 -0
  40. package/lib/Database.d.ts.map +1 -0
  41. package/lib/Database.js +38 -0
  42. package/lib/Database.js.map +1 -0
  43. package/lib/Docker/Database.d.ts +42 -0
  44. package/lib/Docker/Database.d.ts.map +1 -0
  45. package/lib/Docker/Database.js +53 -0
  46. package/lib/Docker/Database.js.map +1 -0
  47. package/lib/Docker/SwarmPool.d.ts +44 -0
  48. package/lib/Docker/SwarmPool.d.ts.map +1 -0
  49. package/lib/Docker/SwarmPool.js +56 -0
  50. package/lib/Docker/SwarmPool.js.map +1 -0
  51. package/lib/Docker/index.d.ts +7 -0
  52. package/lib/Docker/index.d.ts.map +1 -0
  53. package/lib/Docker/index.js +7 -0
  54. package/lib/Docker/index.js.map +1 -0
  55. package/lib/Entity.d.ts +348 -0
  56. package/lib/Entity.d.ts.map +1 -0
  57. package/lib/Entity.js +308 -0
  58. package/lib/Entity.js.map +1 -0
  59. package/lib/Fly/Database.d.ts +50 -0
  60. package/lib/Fly/Database.d.ts.map +1 -0
  61. package/lib/Fly/Database.js +56 -0
  62. package/lib/Fly/Database.js.map +1 -0
  63. package/lib/Fly/MachinePool.d.ts +39 -0
  64. package/lib/Fly/MachinePool.d.ts.map +1 -0
  65. package/lib/Fly/MachinePool.js +51 -0
  66. package/lib/Fly/MachinePool.js.map +1 -0
  67. package/lib/Fly/index.d.ts +7 -0
  68. package/lib/Fly/index.d.ts.map +1 -0
  69. package/lib/Fly/index.js +7 -0
  70. package/lib/Fly/index.js.map +1 -0
  71. package/lib/GCP/ComputeEnginePool.d.ts +41 -0
  72. package/lib/GCP/ComputeEnginePool.d.ts.map +1 -0
  73. package/lib/GCP/ComputeEnginePool.js +172 -0
  74. package/lib/GCP/ComputeEnginePool.js.map +1 -0
  75. package/lib/GCP/Database.d.ts +48 -0
  76. package/lib/GCP/Database.d.ts.map +1 -0
  77. package/lib/GCP/Database.js +115 -0
  78. package/lib/GCP/Database.js.map +1 -0
  79. package/lib/GCP/index.d.ts +7 -0
  80. package/lib/GCP/index.d.ts.map +1 -0
  81. package/lib/GCP/index.js +7 -0
  82. package/lib/GCP/index.js.map +1 -0
  83. package/lib/Hetzner/Database.d.ts +48 -0
  84. package/lib/Hetzner/Database.d.ts.map +1 -0
  85. package/lib/Hetzner/Database.js +91 -0
  86. package/lib/Hetzner/Database.js.map +1 -0
  87. package/lib/Hetzner/ServerPool.d.ts +35 -0
  88. package/lib/Hetzner/ServerPool.d.ts.map +1 -0
  89. package/lib/Hetzner/ServerPool.js +98 -0
  90. package/lib/Hetzner/ServerPool.js.map +1 -0
  91. package/lib/Hetzner/index.d.ts +7 -0
  92. package/lib/Hetzner/index.d.ts.map +1 -0
  93. package/lib/Hetzner/index.js +7 -0
  94. package/lib/Hetzner/index.js.map +1 -0
  95. package/lib/Host.d.ts +60 -0
  96. package/lib/Host.d.ts.map +1 -0
  97. package/lib/Host.js +31 -0
  98. package/lib/Host.js.map +1 -0
  99. package/lib/Kubernetes/Database.d.ts +40 -0
  100. package/lib/Kubernetes/Database.d.ts.map +1 -0
  101. package/lib/Kubernetes/Database.js +56 -0
  102. package/lib/Kubernetes/Database.js.map +1 -0
  103. package/lib/Kubernetes/DeploymentPool.d.ts +54 -0
  104. package/lib/Kubernetes/DeploymentPool.d.ts.map +1 -0
  105. package/lib/Kubernetes/DeploymentPool.js +49 -0
  106. package/lib/Kubernetes/DeploymentPool.js.map +1 -0
  107. package/lib/Kubernetes/index.d.ts +7 -0
  108. package/lib/Kubernetes/index.d.ts.map +1 -0
  109. package/lib/Kubernetes/index.js +7 -0
  110. package/lib/Kubernetes/index.js.map +1 -0
  111. package/lib/Pool.d.ts +66 -0
  112. package/lib/Pool.d.ts.map +1 -0
  113. package/lib/Pool.js +47 -0
  114. package/lib/Pool.js.map +1 -0
  115. package/lib/Providers.d.ts +19 -0
  116. package/lib/Providers.d.ts.map +1 -0
  117. package/lib/Providers.js +31 -0
  118. package/lib/Providers.js.map +1 -0
  119. package/lib/ShardGroups.d.ts +24 -0
  120. package/lib/ShardGroups.d.ts.map +1 -0
  121. package/lib/ShardGroups.js +19 -0
  122. package/lib/ShardGroups.js.map +1 -0
  123. package/lib/Singleton.d.ts +110 -0
  124. package/lib/Singleton.d.ts.map +1 -0
  125. package/lib/Singleton.js +60 -0
  126. package/lib/Singleton.js.map +1 -0
  127. package/lib/Workflow.d.ts +251 -0
  128. package/lib/Workflow.d.ts.map +1 -0
  129. package/lib/Workflow.js +288 -0
  130. package/lib/Workflow.js.map +1 -0
  131. package/lib/index.d.ts +12 -0
  132. package/lib/index.d.ts.map +1 -0
  133. package/lib/index.js +12 -0
  134. package/lib/index.js.map +1 -0
  135. package/lib/internal/Bootstrap.d.ts +28 -0
  136. package/lib/internal/Bootstrap.d.ts.map +1 -0
  137. package/lib/internal/Bootstrap.js +257 -0
  138. package/lib/internal/Bootstrap.js.map +1 -0
  139. package/lib/internal/EntityRuntime.d.ts +40 -0
  140. package/lib/internal/EntityRuntime.d.ts.map +1 -0
  141. package/lib/internal/EntityRuntime.js +698 -0
  142. package/lib/internal/EntityRuntime.js.map +1 -0
  143. package/lib/internal/Gateway.d.ts +82 -0
  144. package/lib/internal/Gateway.d.ts.map +1 -0
  145. package/lib/internal/Gateway.js +218 -0
  146. package/lib/internal/Gateway.js.map +1 -0
  147. package/lib/internal/HostedPlatform.d.ts +36 -0
  148. package/lib/internal/HostedPlatform.d.ts.map +1 -0
  149. package/lib/internal/HostedPlatform.js +36 -0
  150. package/lib/internal/HostedPlatform.js.map +1 -0
  151. package/lib/internal/HostedRuntime.d.ts +58 -0
  152. package/lib/internal/HostedRuntime.d.ts.map +1 -0
  153. package/lib/internal/HostedRuntime.js +72 -0
  154. package/lib/internal/HostedRuntime.js.map +1 -0
  155. package/lib/internal/Naming.d.ts +12 -0
  156. package/lib/internal/Naming.d.ts.map +1 -0
  157. package/lib/internal/Naming.js +40 -0
  158. package/lib/internal/Naming.js.map +1 -0
  159. package/lib/internal/ObjectSharding.d.ts +141 -0
  160. package/lib/internal/ObjectSharding.d.ts.map +1 -0
  161. package/lib/internal/ObjectSharding.js +337 -0
  162. package/lib/internal/ObjectSharding.js.map +1 -0
  163. package/lib/internal/PoolHost.d.ts +80 -0
  164. package/lib/internal/PoolHost.d.ts.map +1 -0
  165. package/lib/internal/PoolHost.js +203 -0
  166. package/lib/internal/PoolHost.js.map +1 -0
  167. package/lib/internal/PoolHostService.d.ts +25 -0
  168. package/lib/internal/PoolHostService.d.ts.map +1 -0
  169. package/lib/internal/PoolHostService.js +10 -0
  170. package/lib/internal/PoolHostService.js.map +1 -0
  171. package/lib/internal/Wire.d.ts +60 -0
  172. package/lib/internal/Wire.d.ts.map +1 -0
  173. package/lib/internal/Wire.js +36 -0
  174. package/lib/internal/Wire.js.map +1 -0
  175. package/lib/internal/WorkflowGateway.d.ts +36 -0
  176. package/lib/internal/WorkflowGateway.d.ts.map +1 -0
  177. package/lib/internal/WorkflowGateway.js +124 -0
  178. package/lib/internal/WorkflowGateway.js.map +1 -0
  179. package/lib/internal/external-runner.d.ts +2 -0
  180. package/lib/internal/external-runner.d.ts.map +1 -0
  181. package/lib/internal/external-runner.js +7 -0
  182. package/lib/internal/external-runner.js.map +1 -0
  183. package/lib/internal/runner.d.ts +2 -0
  184. package/lib/internal/runner.d.ts.map +1 -0
  185. package/lib/internal/runner.js +5 -0
  186. package/lib/internal/runner.js.map +1 -0
  187. package/package.json +143 -3
  188. package/src/AWS/Database.ts +139 -0
  189. package/src/AWS/EcsPool.ts +118 -0
  190. package/src/AWS/index.ts +6 -0
  191. package/src/AWS/internal/Gateway.ts +82 -0
  192. package/src/Cloudflare/DurableObjectPool.ts +198 -0
  193. package/src/Cloudflare/PoolWorker.ts +124 -0
  194. package/src/Cloudflare/index.ts +6 -0
  195. package/src/Cloudflare/internal/Objects.ts +160 -0
  196. package/src/Cron.ts +202 -0
  197. package/src/Database.ts +116 -0
  198. package/src/Docker/Database.ts +94 -0
  199. package/src/Docker/SwarmPool.ts +83 -0
  200. package/src/Docker/index.ts +6 -0
  201. package/src/Entity.ts +621 -0
  202. package/src/Fly/Database.ts +102 -0
  203. package/src/Fly/MachinePool.ts +72 -0
  204. package/src/Fly/index.ts +6 -0
  205. package/src/GCP/ComputeEnginePool.ts +203 -0
  206. package/src/GCP/Database.ts +168 -0
  207. package/src/GCP/index.ts +6 -0
  208. package/src/Hetzner/Database.ts +142 -0
  209. package/src/Hetzner/ServerPool.ts +116 -0
  210. package/src/Hetzner/index.ts +6 -0
  211. package/src/Host.ts +68 -0
  212. package/src/Kubernetes/Database.ts +89 -0
  213. package/src/Kubernetes/DeploymentPool.ts +77 -0
  214. package/src/Kubernetes/index.ts +6 -0
  215. package/src/Pool.ts +104 -0
  216. package/src/Providers.ts +49 -0
  217. package/src/ShardGroups.ts +41 -0
  218. package/src/Singleton.ts +153 -0
  219. package/src/Workflow.ts +556 -0
  220. package/src/index.ts +11 -0
  221. package/src/internal/Bootstrap.ts +368 -0
  222. package/src/internal/EntityRuntime.ts +996 -0
  223. package/src/internal/Gateway.ts +366 -0
  224. package/src/internal/HostedPlatform.ts +83 -0
  225. package/src/internal/HostedRuntime.ts +118 -0
  226. package/src/internal/Naming.ts +44 -0
  227. package/src/internal/ObjectSharding.ts +578 -0
  228. package/src/internal/PoolHost.ts +330 -0
  229. package/src/internal/PoolHostService.ts +25 -0
  230. package/src/internal/Wire.ts +83 -0
  231. package/src/internal/WorkflowGateway.ts +188 -0
  232. package/src/internal/external-runner.ts +10 -0
  233. package/src/internal/runner.ts +5 -0
package/src/Entity.ts ADDED
@@ -0,0 +1,621 @@
1
+ import * as nodePath from "node:path";
2
+ import type { InputProps } from "alchemy/Input";
3
+ import type { Named } from "alchemy/Named";
4
+ import * as Output from "alchemy/Output";
5
+ import {
6
+ Platform,
7
+ type PlatformIdentity,
8
+ type PlatformProps,
9
+ type PlatformServices,
10
+ } from "alchemy/Platform";
11
+ import * as Provider from "alchemy/Provider";
12
+ import type { Resource } from "alchemy/Resource";
13
+ import { RuntimeContext, sanitizeKey } from "alchemy/RuntimeContext";
14
+ import type { Self } from "alchemy/Self";
15
+ import { ClusterSchema, Entity as EffectEntity } from "effect/cluster";
16
+ import type {
17
+ AlreadyProcessingMessage,
18
+ EntityNotAssignedToRunner,
19
+ MailboxFull,
20
+ PersistenceError,
21
+ } from "effect/cluster/ClusterError";
22
+ import type * as ConfigError from "effect/Config";
23
+ import type * as Context from "effect/Context";
24
+ import * as Data from "effect/Data";
25
+ import type * as Duration from "effect/Duration";
26
+ import * as Effect from "effect/Effect";
27
+ import * as Effectable from "effect/Effectable";
28
+ import * as Layer from "effect/Layer";
29
+ import * as Option from "effect/Option";
30
+ import * as Redacted from "effect/Redacted";
31
+ import type { Rpc, RpcClient, RpcClientError, RpcGroup } from "effect/rpc";
32
+ import type { Scope } from "effect/Scope";
33
+ import { bindRoute, makeNamespace } from "./internal/Gateway.ts";
34
+ import { type HostedRuntimeContext, makeHostedRuntimeContext } from "./internal/HostedRuntime.ts";
35
+ import { PoolHost } from "./internal/PoolHostService.ts";
36
+ import type { Providers } from "./Providers.ts";
37
+
38
+ export const EntityTypeId = "Cluster.Entity";
39
+ export type EntityTypeId = typeof EntityTypeId;
40
+
41
+ export interface EntityProps extends PlatformProps {
42
+ /**
43
+ * The module that exports this entity: the class, or the Layer its
44
+ * `.make(...)` returned. Usually `import.meta.url`.
45
+ */
46
+ main: string;
47
+ /**
48
+ * The export of {@link main} that holds this entity.
49
+ * @default "default"
50
+ */
51
+ handler?: string;
52
+ /**
53
+ * How long an instance may sit idle before the runner stops it. Its next
54
+ * message starts it again.
55
+ * @default the runner's default (1 minute)
56
+ */
57
+ maxIdleTime?: Duration.Input;
58
+ /**
59
+ * How many of an instance's messages run at once. A handler that returns
60
+ * `Rpc.fork(...)` runs outside this limit.
61
+ * @default 1
62
+ */
63
+ concurrency?: number | "unbounded";
64
+ /**
65
+ * How many messages an instance may hold, running or waiting to run.
66
+ * Past that, a message fails with `MailboxFull`, unless it is persisted:
67
+ * a persisted message is stored and runs once there is room.
68
+ * @default 4096
69
+ */
70
+ mailboxCapacity?: number | "unbounded";
71
+ /**
72
+ * Return a handler's defect to its caller instead of restarting the
73
+ * instance. By default, as on Effect Cluster's runners, a defect rebuilds
74
+ * the instance and runs its messages again, backing off between restarts.
75
+ * @default false
76
+ */
77
+ disableFatalDefects?: boolean;
78
+ }
79
+
80
+ export interface EntityAttributes {
81
+ /** The entity type: the class's logical id. */
82
+ type: string;
83
+ /** The shard group of the pool that hosts it. */
84
+ pool: string | undefined;
85
+ /** The gateway URL callers send requests to. */
86
+ gateway: string | undefined;
87
+ /** The token callers present to the gateway. */
88
+ token: Redacted.Redacted<string> | undefined;
89
+ }
90
+
91
+ /** Data a pool Layer binds onto the entities it hosts. */
92
+ export interface EntityBindingContract {
93
+ pool?: string;
94
+ gateway?: string;
95
+ token?: Redacted.Redacted<string>;
96
+ }
97
+
98
+ export interface Entity extends Resource<
99
+ EntityTypeId,
100
+ EntityProps,
101
+ EntityAttributes,
102
+ EntityBindingContract,
103
+ Providers
104
+ > {}
105
+
106
+ /** The errors a call through the gateway can fail with, beyond the RPC's own. */
107
+ export type GatewayError =
108
+ | RpcClientError.RpcClientError
109
+ | MailboxFull
110
+ | AlreadyProcessingMessage
111
+ | PersistenceError
112
+ | EntityNotAssignedToRunner;
113
+
114
+ /**
115
+ * Every instance of one entity type. `getByName(entityId)` returns a client
116
+ * with the same shape as Effect Cluster's `Entity.client(entityId)`; its
117
+ * calls take Effect's client options (`discard`, `headers`).
118
+ */
119
+ export interface EntityNamespace<Rpcs extends Rpc.Any> {
120
+ readonly getByName: (
121
+ entityId: string,
122
+ ) => Effect.Effect<
123
+ RpcClient.RpcClient.From<Rpcs, GatewayError>,
124
+ never,
125
+ Rpc.MiddlewareClient<Rpcs>
126
+ >;
127
+ }
128
+
129
+ /** What an instance's constructor runs with: once per instance. */
130
+ export type InstanceServices =
131
+ | RuntimeContext
132
+ | Scope
133
+ | EffectEntity.CurrentAddress
134
+ | EffectEntity.CurrentRunnerAddress;
135
+
136
+ /** Services the Construction phase may use without the caller providing them. */
137
+ export type EntityServices = Entity | Providers | RuntimeContext | Self | Self<Entity>;
138
+
139
+ type ClassRequirements<Req> = Providers | Exclude<Req, EntityServices | PlatformServices>;
140
+
141
+ /**
142
+ * An entity's implementation. The outer Effect runs once per runner and
143
+ * resolves bindings; the inner one runs once per instance and returns the
144
+ * instance's handlers as an object, one per RPC in the schema.
145
+ */
146
+ export type EntityImpl<Rpcs extends Rpc.Any, InitReq> = Effect.Effect<
147
+ Effect.Effect<EffectEntity.HandlersFrom<Rpcs>, never, InstanceServices>,
148
+ ConfigError.ConfigError,
149
+ InitReq
150
+ >;
151
+
152
+ /**
153
+ * A declared entity: the tag callers `yield*`, carrying Effect Cluster's
154
+ * entity surface (`.of`, `.protocol`, `.entity`), implemented by
155
+ * `.make(props, impl)`.
156
+ */
157
+ export type EntityTag<Self, Type extends string, Rpcs extends Rpc.Any> = Effect.Effect<
158
+ EntityNamespace<Rpcs>,
159
+ never,
160
+ Providers
161
+ > &
162
+ Named<Type> &
163
+ PlatformIdentity<Type> & {
164
+ /**
165
+ * The Effect Cluster entity behind the class, for Effect Cluster's own
166
+ * APIs: `Counter.entity.client` inside runners, `EntityProxy`,
167
+ * `Entity.makeTestClient`.
168
+ */
169
+ readonly entity: EffectEntity.Entity<Type, Rpcs>;
170
+ /** The entity's RPCs, as an `RpcGroup`. */
171
+ readonly protocol: RpcGroup.RpcGroup<Rpcs>;
172
+ /** Type an instance's handlers against the entity's RPCs, as Effect Cluster's `Entity.of`. */
173
+ of<Handlers extends EffectEntity.HandlersFrom<Rpcs>>(handlers: Handlers): Handlers;
174
+ /**
175
+ * The entity's implementation, as a Layer. Provide a pool Layer to
176
+ * `impl` to deploy it, with its RPCs' middleware provided to the pool;
177
+ * then provide the returned Layer to the Stack.
178
+ */
179
+ make<InitReq = never>(
180
+ props: InputProps<EntityProps>,
181
+ impl: EntityImpl<Rpcs, InitReq>,
182
+ ): Layer.Layer<Self, never, ClassRequirements<InitReq>>;
183
+ new (_: never): EntityNamespace<Rpcs>;
184
+ };
185
+
186
+ /**
187
+ * An entity declared with its implementation: the class callers `yield*`,
188
+ * which also deploys it.
189
+ */
190
+ export type EntityInline<Type extends string, Rpcs extends Rpc.Any, Req> = Effect.Effect<
191
+ EntityNamespace<Rpcs>,
192
+ never,
193
+ Req
194
+ > &
195
+ Named<Type> &
196
+ PlatformIdentity<Type> & {
197
+ /** The Effect Cluster entity behind the class, for Effect Cluster's own APIs. */
198
+ readonly entity: EffectEntity.Entity<Type, Rpcs>;
199
+ /** The entity's RPCs, as an `RpcGroup`. */
200
+ readonly protocol: RpcGroup.RpcGroup<Rpcs>;
201
+ /** Type an instance's handlers against the entity's RPCs, as Effect Cluster's `Entity.of`. */
202
+ of<Handlers extends EffectEntity.HandlersFrom<Rpcs>>(handlers: Handlers): Handlers;
203
+ new (_: never): EntityNamespace<Rpcs>;
204
+ };
205
+
206
+ /** Props of an external entity: an existing Effect Cluster program, run as it is. */
207
+ export interface ExternalEntityProps {
208
+ /**
209
+ * The module that exports the Effect Cluster entity, under its type's
210
+ * name, relative to where the deploy runs. The gateway imports it to
211
+ * serve the entity's RPCs.
212
+ */
213
+ main: string;
214
+ /**
215
+ * Your existing Effect Cluster program, relative to where the deploy
216
+ * runs: one that launches its entities on `NodeClusterHttp` with HTTP
217
+ * transport and SQL storage. It is bundled into the pool and run as
218
+ * its own process on every runner, with `HOST`, `PORT` and `DATABASE_URL` set.
219
+ */
220
+ program: string;
221
+ }
222
+
223
+ /** The class surface of {@link Entity}. */
224
+ export interface EntityClass {
225
+ /**
226
+ * An external entity: an existing Effect Cluster program, run as it is.
227
+ * Provide a pool Layer to deploy it.
228
+ */
229
+ <const Type extends string, Rpcs extends Rpc.Any>(
230
+ entity: EffectEntity.Entity<Type, Rpcs>,
231
+ props: ExternalEntityProps,
232
+ ): Effect.Effect<EntityNamespace<Rpcs>, never, PoolHost | Providers> & {
233
+ readonly entity: EffectEntity.Entity<Type, Rpcs>;
234
+ };
235
+ // `Self` names the class (`extends Cluster.Entity<Counter>()`).
236
+ <Self>(): {
237
+ /**
238
+ * Declared from an Effect Cluster entity, with its implementation, which
239
+ * types its handlers with the entity's `.of`. Provide a pool Layer to
240
+ * `impl` to deploy it, with its RPCs' middleware provided to the pool.
241
+ */
242
+ <const Type extends string, Rpcs extends Rpc.Any, InitReq = never>(
243
+ entity: EffectEntity.Entity<Type, Rpcs>,
244
+ props: InputProps<EntityProps>,
245
+ impl: EntityImpl<Rpcs, InitReq>,
246
+ ): EntityInline<Type, Rpcs, ClassRequirements<InitReq>>;
247
+ /** Declared from its RPCs, as Effect Cluster's `Entity.make`. */
248
+ <const Type extends string, const Rpcs extends ReadonlyArray<Rpc.Any>>(
249
+ type: Type,
250
+ rpcs: Rpcs,
251
+ ): EntityTag<Self, Type, Rpcs[number]>;
252
+ /** Declared from an existing Effect Cluster entity. */
253
+ <const Type extends string, Rpcs extends Rpc.Any>(
254
+ entity: EffectEntity.Entity<Type, Rpcs>,
255
+ ): EntityTag<Self, Type, Rpcs>;
256
+ };
257
+ }
258
+
259
+ /** A class yielded outside any runtime was called. */
260
+ export class EntityCalledOutsideRuntime extends Data.TaggedError(
261
+ "Cluster.EntityCalledOutsideRuntime",
262
+ )<{
263
+ readonly message: string;
264
+ }> {}
265
+
266
+ /**
267
+ * An entity class as a runner runs it: the Effect entity, the instance
268
+ * constructor its Construction phase returned, and the services that phase
269
+ * ran with.
270
+ * @internal
271
+ */
272
+ export interface HostedEntity {
273
+ readonly kind: "entity";
274
+ readonly entity: EffectEntity.Entity<string, Rpc.Any>;
275
+ readonly instance: Effect.Effect<Record<string, unknown>, never, any>;
276
+ readonly context: Context.Context<never>;
277
+ readonly options: Pick<
278
+ EntityProps,
279
+ "maxIdleTime" | "concurrency" | "mailboxCapacity" | "disableFatalDefects"
280
+ >;
281
+ }
282
+
283
+ export type EntityRuntimeContext = HostedRuntimeContext<HostedEntity>;
284
+
285
+ /** The Effect entity behind each declared type, for the runtime context. */
286
+ const entities = new Map<string, EffectEntity.Entity<string, Rpc.Any>>();
287
+
288
+ const createRuntimeContext = makeHostedRuntimeContext<HostedEntity>(
289
+ EntityTypeId,
290
+ (id, instance, context, props?: EntityProps) => ({
291
+ kind: "entity",
292
+ entity: entities.get(id)!,
293
+ instance: instance as HostedEntity["instance"],
294
+ context,
295
+ options: {
296
+ maxIdleTime: props?.maxIdleTime,
297
+ concurrency: props?.concurrency,
298
+ mailboxCapacity: props?.mailboxCapacity,
299
+ disableFatalDefects: props?.disableFatalDefects,
300
+ },
301
+ }),
302
+ // Every RPC's middleware must reach the pool with the entity.
303
+ (id, context) => {
304
+ const missing = [...entities.get(id)!.protocol.requests.values()].flatMap((rpc) =>
305
+ [...(rpc as unknown as Rpc.AnyWithProps).middlewares]
306
+ .filter((middleware) => !context.mapUnsafe.has(middleware.key))
307
+ .map((middleware) => `${rpc._tag} uses ${middleware.key}`),
308
+ );
309
+ return missing.length === 0
310
+ ? Effect.void
311
+ : Effect.die(
312
+ new EntityMiddlewareMissing({
313
+ message:
314
+ `Cluster.Entity '${id}' is missing RPC middleware (${missing.join(", ")}). ` +
315
+ `Provide its Layer with the pool: Effect.provide(Default.pipe(Layer.provide(AuthLive))).`,
316
+ }),
317
+ );
318
+ },
319
+ );
320
+
321
+ /** An entity's RPC uses middleware nothing provided to its pool. */
322
+ export class EntityMiddlewareMissing extends Data.TaggedError("Cluster.EntityMiddlewareMissing")<{
323
+ readonly message: string;
324
+ }> {}
325
+
326
+ type EntityPlatform = Platform<Entity, EntityServices, unknown, EntityRuntimeContext>;
327
+
328
+ const platform = Platform(EntityTypeId, { createRuntimeContext }) as EntityPlatform;
329
+
330
+ /** The resource type behind every class, which the providers register. @internal */
331
+ export const EntityResource = platform;
332
+
333
+ /** The Platform declaration behind a class, which a runner builds. @internal */
334
+ export const PlatformDeclaration = Symbol.for("alchemy/Cluster/Entity/Platform");
335
+
336
+ /** The Effect entity a class hosts, which a runner reads. @internal */
337
+ export const EntityDefinition = Symbol.for("alchemy/Cluster/Entity/Definition");
338
+
339
+ /**
340
+ * An **Entity** in an Effect Cluster: a stateful, addressable instance per
341
+ * entity id, whose messages are typed by its RPCs. Runners place each
342
+ * instance on one runner and deliver its messages one at a time.
343
+ *
344
+ * Declare it from an Effect Cluster entity with its implementation, and
345
+ * the class is both what callers `yield*` and what deploys it. Or declare
346
+ * it bare, like Effect Cluster's `Entity.make`, from a type and its RPCs,
347
+ * and implement it with `.make(props, impl)`, as a Layer: callers import
348
+ * the bare declaration without bundling the implementation or its pool.
349
+ *
350
+ * The implementation has two phases. The outer Effect runs once per runner
351
+ * and resolves bindings. The inner Effect runs once per instance, on its
352
+ * first message, and returns its handlers, typed by the entity's `.of`. Providing a
353
+ * pool Layer to the implementation, such as
354
+ * `ClusterDocker.SwarmPool("Default")`, is what deploys it.
355
+ *
356
+ * `yield* Counter` from any runtime returns the entity's namespace, like a
357
+ * Durable Object's: `getByName(entityId)` returns a client with the same
358
+ * shape as Effect Cluster's `Entity.client(entityId)`. Inside the cluster,
359
+ * calls go through Effect Cluster directly; elsewhere they go through the
360
+ * pool's gateway over HTTP, so callers need no database access and no
361
+ * route to the pool.
362
+ *
363
+ * ### Declaring an entity
364
+ * **Example:** A counter, with its implementation
365
+ * ```typescript
366
+ * // src/Counter.ts
367
+ * const Counters = Entity.make("Counter", [
368
+ * Rpc.make("Increment", { payload: { amount: Schema.Int }, success: Schema.Int }),
369
+ * ]);
370
+ *
371
+ * export default class Counter extends Cluster.Entity<Counter>()(
372
+ * Counters,
373
+ * { main: import.meta.url },
374
+ * Effect.gen(function* () {
375
+ * return Effect.gen(function* () {
376
+ * const count = yield* Ref.make(0);
377
+ * return Counters.of({
378
+ * Increment: ({ payload }) => Ref.updateAndGet(count, (n) => n + payload.amount),
379
+ * });
380
+ * });
381
+ * }).pipe(Effect.provide(Default)),
382
+ * ) {}
383
+ * ```
384
+ *
385
+ * ### Keeping its code out of its callers
386
+ * **Example:** A bare declaration, which callers import
387
+ * ```typescript
388
+ * // src/Counter.ts
389
+ * export const Increment = Rpc.make("Increment", {
390
+ * payload: { amount: Schema.Int },
391
+ * success: Schema.Int,
392
+ * });
393
+ *
394
+ * export class Counter extends Cluster.Entity<Counter>()("Counter", [Increment]) {}
395
+ * ```
396
+ *
397
+ * **Example:** An existing Effect Cluster entity
398
+ * ```typescript
399
+ * import { Counter as CounterEntity } from "./entities/counter.ts";
400
+ *
401
+ * export class Counter extends Cluster.Entity<Counter>()(CounterEntity) {}
402
+ * ```
403
+ *
404
+ * **Example:** Its implementation, as a Layer, deployed to a pool
405
+ * ```typescript
406
+ * // src/CounterLive.ts
407
+ * export default Counter.make(
408
+ * { main: import.meta.url },
409
+ * Effect.gen(function* () {
410
+ * return Effect.gen(function* () {
411
+ * const count = yield* Ref.make(0);
412
+ * return Counter.of({
413
+ * Increment: ({ payload }) => Ref.updateAndGet(count, (n) => n + payload.amount),
414
+ * });
415
+ * });
416
+ * }).pipe(Effect.provide(Default)),
417
+ * );
418
+ * ```
419
+ *
420
+ * **Example:** Middleware, provided with the pool
421
+ * ```typescript
422
+ * export const Increment = Rpc.make("Increment", {
423
+ * payload: { amount: Schema.Int },
424
+ * success: Schema.Int,
425
+ * }).middleware(Auth);
426
+ *
427
+ * export default Counter.make(
428
+ * { main: import.meta.url },
429
+ * Effect.gen(function* () {
430
+ * return Effect.gen(function* () {
431
+ * return Counter.of({
432
+ * Increment: () => Effect.map(CurrentUser, (user) => user.id.length),
433
+ * });
434
+ * });
435
+ * }).pipe(Effect.provide(Default.pipe(Layer.provide(AuthLive)))),
436
+ * );
437
+ * ```
438
+ *
439
+ * **Example:** An existing Effect Cluster program, run as it is
440
+ * ```typescript
441
+ * import { Tally as TallyEntity } from "./entities/tally.ts";
442
+ *
443
+ * export const Tally = Cluster.Entity(TallyEntity, {
444
+ * main: "./entities/tally.ts",
445
+ * program: "./runner.ts",
446
+ * }).pipe(Effect.provide(Legacy));
447
+ * ```
448
+ *
449
+ * ### Calling an entity
450
+ * **Example:** From any runtime
451
+ * ```typescript
452
+ * const counters = yield* Counter;
453
+ * const counter = yield* counters.getByName("visits");
454
+ * const count = yield* counter.Increment({ amount: 1 });
455
+ * ```
456
+ *
457
+ * **Example:** Without waiting for the reply
458
+ * ```typescript
459
+ * yield* counter.Increment({ amount: 1 }, { discard: true });
460
+ * ```
461
+ *
462
+ * **Example:** Through client middleware
463
+ * ```typescript
464
+ * const counter = yield* counters.getByName("visits");
465
+ * yield* counter.Increment({ amount: 1 }).pipe(Effect.provide(AuthClient));
466
+ * ```
467
+ *
468
+ * @resource
469
+ */
470
+ export const Entity: EntityClass = ((...outer: ReadonlyArray<unknown>) =>
471
+ outer.length > 0
472
+ ? external(outer[0] as AnyEntity, outer[1] as ExternalEntityProps)
473
+ : (typeOrEntity: string | AnyEntity, ...rest: ReadonlyArray<unknown>) => {
474
+ const [rpcs, props, impl] = typeof typeOrEntity === "string" ? rest : [undefined, ...rest];
475
+ const entity = placedInShardGroup(
476
+ typeof typeOrEntity === "string"
477
+ ? EffectEntity.make(typeOrEntity, rpcs as ReadonlyArray<Rpc.Any>)
478
+ : typeOrEntity,
479
+ );
480
+ entities.set(entity.type, entity);
481
+ const members = { of: (handlers: unknown) => handlers, protocol: entity.protocol };
482
+ // With an implementation, the class deploys it; without, `.make` does.
483
+ if (impl !== undefined) {
484
+ return declare(entity, (platform as any)()(entity.type, props, impl), members);
485
+ }
486
+ const tag = (platform as any)()(entity.type);
487
+ return declare(entity, tag, { ...members, make: tag.make });
488
+ }) as unknown as EntityClass;
489
+
490
+ type AnyEntity = EffectEntity.Entity<string, Rpc.Any>;
491
+
492
+ /** The environment key holding the shard group an entity's pool serves. @internal */
493
+ export const shardGroupKey = (type: string) => sanitizeKey(`CLUSTER_${type}_SHARD_GROUP`);
494
+
495
+ /**
496
+ * Place an entity in its pool's shard group, read when a message is
497
+ * routed: its pool, and every runtime that yields the class, have it in
498
+ * their environment. Effect Cluster's own client (`Counter.entity.client`)
499
+ * then routes to the right pool too.
500
+ */
501
+ const placedInShardGroup = (entity: AnyEntity) => {
502
+ const key = shardGroupKey(entity.type);
503
+ return entity.annotate(
504
+ ClusterSchema.ShardGroup,
505
+ // A synchronous lookup at routing time; workerd has no `process`.
506
+ () =>
507
+ (globalThis as { process?: { env: Record<string, string | undefined> } }).process?.env[key] ??
508
+ "default",
509
+ );
510
+ };
511
+
512
+ /**
513
+ * An external entity: declare its resource, then have the pool it was
514
+ * given run its program. The pool Layer is what provides `PoolHost`.
515
+ */
516
+ const external = (entity: AnyEntity, props: ExternalEntityProps) => {
517
+ entities.set(entity.type, entity);
518
+ const declaration = Effect.gen(function* () {
519
+ const resource = yield* (platform as any)(entity.type, {
520
+ main: props.main,
521
+ }) as Effect.Effect<Entity, never, Providers>;
522
+ if (globalThis.__ALCHEMY_RUNTIME__) return resource;
523
+ const host = yield* PoolHost;
524
+ yield* host.deployExternal({
525
+ type: entity.type,
526
+ main: yield* Effect.sync(() => nodePath.resolve(props.main)),
527
+ handler: entity.type,
528
+ program: props.program,
529
+ resource,
530
+ });
531
+ return resource;
532
+ });
533
+ return Object.assign(clientOf(entity, declaration), { entity });
534
+ };
535
+
536
+ /**
537
+ * A class whose `yield*` is its client. While deploying it registers the
538
+ * class and binds what a caller needs into the caller's environment; at
539
+ * runtime it only reads them back, so the Construction phase never runs in
540
+ * a caller.
541
+ */
542
+ const declare = (entity: AnyEntity, declaration: any, members: object) =>
543
+ Object.assign(
544
+ class {},
545
+ members,
546
+ {
547
+ LogicalId: entity.type,
548
+ entity,
549
+ [PlatformDeclaration]: declaration,
550
+ [EntityDefinition]: entity,
551
+ },
552
+ Effectable.Prototype({
553
+ label: `${EntityTypeId}<${entity.type}>`,
554
+ evaluate: () => clientOf(entity, declaration),
555
+ }),
556
+ );
557
+
558
+ const clientOf = (entity: AnyEntity, declaration: Effect.Effect<Entity, never, any>) =>
559
+ Effect.gen(function* () {
560
+ // Built on a memo map of its own: a class built inside another's phase
561
+ // would otherwise reuse the caller's already-built pool Layer.
562
+ const resource = globalThis.__ALCHEMY_RUNTIME__
563
+ ? undefined
564
+ : yield* declaration.pipe(
565
+ Effect.provideServiceEffect(Layer.CurrentMemoMap, Layer.makeMemoMap),
566
+ );
567
+ const ctx = Option.getOrUndefined(yield* Effect.serviceOption(RuntimeContext));
568
+ if (ctx === undefined) return outsideRuntime(entity);
569
+ // Bound as environment values while deploying; read back at runtime.
570
+ const bind = <T>(key: string, value: unknown) =>
571
+ ctx
572
+ .set(
573
+ sanitizeKey(`CLUSTER_${entity.type}_${key}`),
574
+ Output.isOutput(value) ? value : Output.literal(value),
575
+ )
576
+ .pipe(Effect.flatMap((bound) => ctx.get<T>(bound)));
577
+ const url = (yield* bind<string>("GATEWAY", resource?.gateway)) ?? "";
578
+ // The pool's shard group, for Effect Cluster's own client (`.entity.client`).
579
+ yield* bind<string>("SHARD_GROUP", resource?.pool);
580
+ const token = yield* bind<Redacted.Redacted<string> | string>("TOKEN", resource?.token);
581
+ // A Worker calling a gateway Worker reaches it through a service binding.
582
+ const service = sanitizeKey(`CLUSTER_${entity.type}_SERVICE`);
583
+ if (!globalThis.__ALCHEMY_RUNTIME__) yield* bindRoute(entity.type, service);
584
+ return makeNamespace(entity, {
585
+ url,
586
+ token: token === undefined ? "" : Redacted.isRedacted(token) ? Redacted.value(token) : token,
587
+ service,
588
+ }) as unknown as EntityNamespace<Rpc.Any>;
589
+ });
590
+
591
+ const outsideRuntime = (entity: AnyEntity): EntityNamespace<Rpc.Any> => ({
592
+ getByName: (entityId) =>
593
+ Effect.die(
594
+ new EntityCalledOutsideRuntime({
595
+ message: `Cluster.Entity '${entity.type}/${entityId}' can only be called inside a runtime.`,
596
+ }),
597
+ ),
598
+ });
599
+
600
+ export const EntityProvider = () =>
601
+ Provider.succeed(platform, {
602
+ stables: ["type"],
603
+ reconcile: ({ id, bindings }) =>
604
+ Effect.sync(() => {
605
+ const attrs: EntityAttributes = {
606
+ type: id,
607
+ pool: undefined,
608
+ gateway: undefined,
609
+ token: undefined,
610
+ };
611
+ for (const { data } of bindings) {
612
+ attrs.pool = data.pool ?? attrs.pool;
613
+ attrs.gateway = data.gateway ?? attrs.gateway;
614
+ attrs.token = data.token ?? attrs.token;
615
+ }
616
+ return attrs;
617
+ }),
618
+ delete: () => Effect.void,
619
+ });
620
+
621
+ export type { RpcGroup };