@ohos-ports/rivetkit-effect 2.3.17-beta.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 (89) hide show
  1. package/dist/Action.d.ts +104 -0
  2. package/dist/Action.d.ts.map +1 -0
  3. package/dist/Action.js +50 -0
  4. package/dist/Action.js.map +1 -0
  5. package/dist/Actor.d.ts +132 -0
  6. package/dist/Actor.d.ts.map +1 -0
  7. package/dist/Actor.js +104 -0
  8. package/dist/Actor.js.map +1 -0
  9. package/dist/Client.d.ts +31 -0
  10. package/dist/Client.d.ts.map +1 -0
  11. package/dist/Client.js +100 -0
  12. package/dist/Client.js.map +1 -0
  13. package/dist/Registry.d.ts +134 -0
  14. package/dist/Registry.d.ts.map +1 -0
  15. package/dist/Registry.js +178 -0
  16. package/dist/Registry.js.map +1 -0
  17. package/dist/RivetError.d.ts +480 -0
  18. package/dist/RivetError.d.ts.map +1 -0
  19. package/dist/RivetError.js +985 -0
  20. package/dist/RivetError.js.map +1 -0
  21. package/dist/RivetLogger.d.ts +41 -0
  22. package/dist/RivetLogger.d.ts.map +1 -0
  23. package/dist/RivetLogger.js +41 -0
  24. package/dist/RivetLogger.js.map +1 -0
  25. package/dist/State.d.ts +159 -0
  26. package/dist/State.d.ts.map +1 -0
  27. package/dist/State.js +98 -0
  28. package/dist/State.js.map +1 -0
  29. package/dist/internal/ActionDispatcher.d.ts +14 -0
  30. package/dist/internal/ActionDispatcher.d.ts.map +1 -0
  31. package/dist/internal/ActionDispatcher.js +100 -0
  32. package/dist/internal/ActionDispatcher.js.map +1 -0
  33. package/dist/internal/ActionErrorEnvelope.d.ts +11 -0
  34. package/dist/internal/ActionErrorEnvelope.d.ts.map +1 -0
  35. package/dist/internal/ActionErrorEnvelope.js +14 -0
  36. package/dist/internal/ActionErrorEnvelope.js.map +1 -0
  37. package/dist/internal/ActorInstanceManager.d.ts +28 -0
  38. package/dist/internal/ActorInstanceManager.d.ts.map +1 -0
  39. package/dist/internal/ActorInstanceManager.js +51 -0
  40. package/dist/internal/ActorInstanceManager.js.map +1 -0
  41. package/dist/internal/ActorStateAdapter.d.ts +18 -0
  42. package/dist/internal/ActorStateAdapter.d.ts.map +1 -0
  43. package/dist/internal/ActorStateAdapter.js +24 -0
  44. package/dist/internal/ActorStateAdapter.js.map +1 -0
  45. package/dist/internal/StateOptions.d.ts +13 -0
  46. package/dist/internal/StateOptions.d.ts.map +1 -0
  47. package/dist/internal/StateOptions.js +2 -0
  48. package/dist/internal/StateOptions.js.map +1 -0
  49. package/dist/internal/logging.d.ts +22 -0
  50. package/dist/internal/logging.d.ts.map +1 -0
  51. package/dist/internal/logging.js +138 -0
  52. package/dist/internal/logging.js.map +1 -0
  53. package/dist/internal/tracing.d.ts +23 -0
  54. package/dist/internal/tracing.d.ts.map +1 -0
  55. package/dist/internal/tracing.js +30 -0
  56. package/dist/internal/tracing.js.map +1 -0
  57. package/dist/internal/utils.d.ts +7 -0
  58. package/dist/internal/utils.d.ts.map +1 -0
  59. package/dist/internal/utils.js +7 -0
  60. package/dist/internal/utils.js.map +1 -0
  61. package/dist/mod.d.ts +8 -0
  62. package/dist/mod.d.ts.map +1 -0
  63. package/dist/mod.js +8 -0
  64. package/dist/mod.js.map +1 -0
  65. package/package.json +56 -0
  66. package/src/Action.ts +231 -0
  67. package/src/Actor.test-d.ts +641 -0
  68. package/src/Actor.test.ts +209 -0
  69. package/src/Actor.ts +575 -0
  70. package/src/Client.test.ts +206 -0
  71. package/src/Client.ts +218 -0
  72. package/src/Registry.test-d.ts +126 -0
  73. package/src/Registry.test.ts +400 -0
  74. package/src/Registry.ts +304 -0
  75. package/src/RivetError.test.ts +199 -0
  76. package/src/RivetError.ts +1163 -0
  77. package/src/RivetLogger.ts +60 -0
  78. package/src/State.test.ts +340 -0
  79. package/src/State.ts +420 -0
  80. package/src/internal/ActionDispatcher.ts +189 -0
  81. package/src/internal/ActionErrorEnvelope.ts +19 -0
  82. package/src/internal/ActorInstanceManager.ts +137 -0
  83. package/src/internal/ActorStateAdapter.ts +82 -0
  84. package/src/internal/StateOptions.ts +21 -0
  85. package/src/internal/logging.test.ts +274 -0
  86. package/src/internal/logging.ts +197 -0
  87. package/src/internal/tracing.ts +42 -0
  88. package/src/internal/utils.ts +12 -0
  89. package/src/mod.ts +7 -0
@@ -0,0 +1,400 @@
1
+ import { assert, describe, it } from "@effect/vitest";
2
+ import { Action, Actor, Registry, RivetLogger } from "@rivetkit/effect";
3
+ import { Effect, Layer } from "effect";
4
+ import { HttpEffect } from "effect/unstable/http";
5
+ import * as RivetkitLog from "@ohos-ports/rivetkit/log";
6
+ import { vi } from "vitest";
7
+
8
+ const TestActor = Actor.make("TestActor", {
9
+ actions: [Action.make("Test")],
10
+ });
11
+
12
+ const TestActorLive = TestActor.toLayer({
13
+ Test: () => Effect.void,
14
+ });
15
+
16
+ const ActorsLayer = Layer.mergeAll(TestActorLive);
17
+
18
+ const RegistryLive = ActorsLayer.pipe(
19
+ Layer.provideMerge(
20
+ Registry.layer({
21
+ endpoint: "http://127.0.0.1:6420",
22
+ noWelcome: true,
23
+ }),
24
+ ),
25
+ );
26
+
27
+ function makeTestLogger(): RivetkitLog.Logger {
28
+ const logger: Record<string, unknown> = {
29
+ level: "debug",
30
+ child: () => logger,
31
+ };
32
+ for (const level of ["trace", "debug", "info", "warn", "error", "fatal"]) {
33
+ logger[level] = (): void => {};
34
+ }
35
+
36
+ return logger as unknown as RivetkitLog.Logger;
37
+ }
38
+
39
+ describe("Registry.toWebHandler", () => {
40
+ it("serves registered actors as a Fetch handler", async () => {
41
+ const { handler, dispose } = Registry.toWebHandler(RegistryLive);
42
+
43
+ try {
44
+ const response = await handler(
45
+ new Request("http://runner.test/api/rivet/metadata"),
46
+ );
47
+
48
+ assert.strictEqual(response.status, 200);
49
+ const body = (await response.json()) as {
50
+ readonly actorNames: Record<string, unknown>;
51
+ };
52
+ assert.ok(body.actorNames.TestActor);
53
+ } finally {
54
+ await dispose();
55
+ }
56
+ });
57
+
58
+ it("uses a custom serverless base path", async () => {
59
+ const { handler, dispose } = Registry.toWebHandler(RegistryLive, {
60
+ basePath: "/",
61
+ });
62
+
63
+ try {
64
+ const response = await handler(
65
+ new Request("http://runner.test/metadata"),
66
+ );
67
+
68
+ assert.strictEqual(response.status, 200);
69
+ const body = (await response.json()) as {
70
+ readonly actorNames: Record<string, unknown>;
71
+ };
72
+ assert.ok(body.actorNames.TestActor);
73
+ } finally {
74
+ await dispose();
75
+ }
76
+ });
77
+
78
+ it("uses the custom base path to identify start requests", async () => {
79
+ const { handler, dispose } = Registry.toWebHandler(RegistryLive, {
80
+ basePath: "/custom",
81
+ maxStartPayloadBytes: 1,
82
+ });
83
+
84
+ try {
85
+ const defaultPrefix = await handler(
86
+ new Request("http://runner.test/api/rivet/start", {
87
+ method: "POST",
88
+ body: new Uint8Array([1, 2]),
89
+ }),
90
+ );
91
+ assert.notStrictEqual(defaultPrefix.status, 413);
92
+
93
+ const customPrefix = await handler(
94
+ new Request("http://runner.test/custom/start", {
95
+ method: "POST",
96
+ body: new Uint8Array([1, 2]),
97
+ }),
98
+ );
99
+ assert.strictEqual(customPrefix.status, 413);
100
+ const body = (await customPrefix.json()) as {
101
+ readonly group: string;
102
+ readonly code: string;
103
+ readonly message: string;
104
+ };
105
+ assert.deepStrictEqual(
106
+ { group: body.group, code: body.code },
107
+ { group: "message", code: "incoming_too_long" },
108
+ );
109
+ assert.match(body.message, /limit is 1 bytes/);
110
+ } finally {
111
+ await dispose();
112
+ }
113
+ });
114
+
115
+ it("uses a custom serverless start payload size limit", async () => {
116
+ const { handler, dispose } = Registry.toWebHandler(RegistryLive, {
117
+ maxStartPayloadBytes: 1,
118
+ });
119
+
120
+ try {
121
+ const response = await handler(
122
+ new Request("http://runner.test/api/rivet/start", {
123
+ method: "POST",
124
+ body: new Uint8Array([1, 2]),
125
+ }),
126
+ );
127
+
128
+ assert.strictEqual(response.status, 413);
129
+ const body = (await response.json()) as {
130
+ readonly group: string;
131
+ readonly code: string;
132
+ readonly message: string;
133
+ };
134
+ assert.deepStrictEqual(
135
+ { group: body.group, code: body.code },
136
+ { group: "message", code: "incoming_too_long" },
137
+ );
138
+ assert.match(body.message, /limit is 1 bytes/);
139
+ } finally {
140
+ await dispose();
141
+ }
142
+ });
143
+
144
+ it("does not print the welcome banner when disabled", async () => {
145
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
146
+ const { handler, dispose } = Registry.toWebHandler(RegistryLive);
147
+
148
+ try {
149
+ const response = await handler(
150
+ new Request("http://runner.test/api/rivet/metadata"),
151
+ );
152
+
153
+ assert.strictEqual(response.status, 200);
154
+ assert.strictEqual(log.mock.calls.length, 0);
155
+ } finally {
156
+ await dispose();
157
+ log.mockRestore();
158
+ }
159
+ });
160
+
161
+ it("builds the registry layer once across requests", async () => {
162
+ let builds = 0;
163
+ const CountingRegistryLive = Layer.mergeAll(
164
+ RegistryLive,
165
+ Layer.effectDiscard(
166
+ Effect.sync(() => {
167
+ builds += 1;
168
+ }),
169
+ ),
170
+ );
171
+ const { handler, dispose } =
172
+ Registry.toWebHandler(CountingRegistryLive);
173
+
174
+ try {
175
+ const first = await handler(
176
+ new Request("http://runner.test/api/rivet/metadata"),
177
+ );
178
+ const second = await handler(
179
+ new Request("http://runner.test/api/rivet/metadata"),
180
+ );
181
+
182
+ assert.strictEqual(first.status, 200);
183
+ assert.strictEqual(second.status, 200);
184
+ assert.strictEqual(builds, 1);
185
+ } finally {
186
+ await dispose();
187
+ }
188
+ });
189
+
190
+ it("initializes the underlying RivetKit registry once across requests", async () => {
191
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
192
+ const WelcomeRegistryLive = ActorsLayer.pipe(
193
+ Layer.provideMerge(
194
+ Registry.layer({
195
+ endpoint: "http://127.0.0.1:6420",
196
+ }),
197
+ ),
198
+ );
199
+ const { handler, dispose } = Registry.toWebHandler(WelcomeRegistryLive);
200
+
201
+ try {
202
+ const first = await handler(
203
+ new Request("http://runner.test/api/rivet/metadata"),
204
+ );
205
+ const callsAfterFirst = log.mock.calls.length;
206
+ const second = await handler(
207
+ new Request("http://runner.test/api/rivet/metadata"),
208
+ );
209
+
210
+ assert.strictEqual(first.status, 200);
211
+ assert.strictEqual(second.status, 200);
212
+ assert.ok(callsAfterFirst > 0);
213
+ assert.strictEqual(log.mock.calls.length, callsAfterFirst);
214
+ } finally {
215
+ await dispose();
216
+ log.mockRestore();
217
+ }
218
+ });
219
+
220
+ it("uses a custom logger layer for the underlying RivetKit registry", async () => {
221
+ const baseLogger = makeTestLogger();
222
+ const CustomLoggerRegistryLive = RegistryLive.pipe(
223
+ Layer.provide(RivetLogger.layerFromPino(baseLogger)),
224
+ );
225
+ const { handler, dispose } = Registry.toWebHandler(
226
+ CustomLoggerRegistryLive,
227
+ );
228
+
229
+ try {
230
+ const response = await handler(
231
+ new Request("http://runner.test/api/rivet/metadata"),
232
+ );
233
+
234
+ assert.strictEqual(response.status, 200);
235
+ assert.strictEqual(RivetkitLog.getBaseLogger(), baseLogger);
236
+ } finally {
237
+ await dispose();
238
+ RivetkitLog.configureDefaultLogger("silent");
239
+ }
240
+ });
241
+
242
+ it("closes registry layer finalizers on dispose", async () => {
243
+ let finalizers = 0;
244
+ const FinalizedRegistryLive = Layer.mergeAll(
245
+ RegistryLive,
246
+ Layer.effectDiscard(
247
+ Effect.addFinalizer(() =>
248
+ Effect.sync(() => {
249
+ finalizers += 1;
250
+ }),
251
+ ),
252
+ ),
253
+ );
254
+ const { handler, dispose } = Registry.toWebHandler(
255
+ FinalizedRegistryLive,
256
+ );
257
+
258
+ try {
259
+ const response = await handler(
260
+ new Request("http://runner.test/api/rivet/metadata"),
261
+ );
262
+
263
+ assert.strictEqual(response.status, 200);
264
+ assert.strictEqual(finalizers, 0);
265
+ } finally {
266
+ await dispose();
267
+ }
268
+ assert.strictEqual(finalizers, 1);
269
+ });
270
+ });
271
+
272
+ describe("Registry.toHttpEffect", () => {
273
+ it.effect("serves registered actors as an Effect HTTP handler", () =>
274
+ Effect.scoped(
275
+ Effect.gen(function* () {
276
+ const httpEffect = yield* Registry.toHttpEffect(RegistryLive);
277
+ const handler = HttpEffect.toWebHandler(httpEffect);
278
+ const response = yield* Effect.promise(() =>
279
+ handler(
280
+ new Request("http://runner.test/api/rivet/metadata"),
281
+ ),
282
+ );
283
+
284
+ yield* Effect.promise(() =>
285
+ (async (response: Response) => {
286
+ assert.strictEqual(response.status, 200);
287
+ const body = (await response.json()) as {
288
+ readonly actorNames: Record<string, unknown>;
289
+ };
290
+ assert.ok(body.actorNames.TestActor);
291
+ })(response),
292
+ );
293
+ }),
294
+ ),
295
+ );
296
+
297
+ it.effect("uses a custom serverless base path", () =>
298
+ Effect.scoped(
299
+ Effect.gen(function* () {
300
+ const httpEffect = yield* Registry.toHttpEffect(RegistryLive, {
301
+ basePath: "/",
302
+ });
303
+ const handler = HttpEffect.toWebHandler(httpEffect);
304
+ const response = yield* Effect.promise(() =>
305
+ handler(new Request("http://runner.test/metadata")),
306
+ );
307
+
308
+ yield* Effect.promise(() =>
309
+ (async (response: Response) => {
310
+ assert.strictEqual(response.status, 200);
311
+ const body = (await response.json()) as {
312
+ readonly actorNames: Record<string, unknown>;
313
+ };
314
+ assert.ok(body.actorNames.TestActor);
315
+ })(response),
316
+ );
317
+ }),
318
+ ),
319
+ );
320
+
321
+ it.effect("uses the custom base path to identify start requests", () =>
322
+ Effect.scoped(
323
+ Effect.gen(function* () {
324
+ const httpEffect = yield* Registry.toHttpEffect(RegistryLive, {
325
+ basePath: "/custom",
326
+ maxStartPayloadBytes: 1,
327
+ });
328
+ const handler = HttpEffect.toWebHandler(httpEffect);
329
+ const defaultPrefix = yield* Effect.promise(() =>
330
+ handler(
331
+ new Request("http://runner.test/api/rivet/start", {
332
+ method: "POST",
333
+ body: new Uint8Array([1, 2]),
334
+ }),
335
+ ),
336
+ );
337
+ assert.notStrictEqual(defaultPrefix.status, 413);
338
+
339
+ const customPrefix = yield* Effect.promise(() =>
340
+ handler(
341
+ new Request("http://runner.test/custom/start", {
342
+ method: "POST",
343
+ body: new Uint8Array([1, 2]),
344
+ }),
345
+ ),
346
+ );
347
+ yield* Effect.promise(() =>
348
+ (async (response: Response) => {
349
+ assert.strictEqual(response.status, 413);
350
+ const body = (await response.json()) as {
351
+ readonly group: string;
352
+ readonly code: string;
353
+ readonly message: string;
354
+ };
355
+ assert.deepStrictEqual(
356
+ { group: body.group, code: body.code },
357
+ { group: "message", code: "incoming_too_long" },
358
+ );
359
+ assert.match(body.message, /limit is 1 bytes/);
360
+ })(customPrefix),
361
+ );
362
+ }),
363
+ ),
364
+ );
365
+
366
+ it.effect("uses a custom serverless start payload size limit", () =>
367
+ Effect.scoped(
368
+ Effect.gen(function* () {
369
+ const httpEffect = yield* Registry.toHttpEffect(RegistryLive, {
370
+ maxStartPayloadBytes: 1,
371
+ });
372
+ const handler = HttpEffect.toWebHandler(httpEffect);
373
+ const response = yield* Effect.promise(() =>
374
+ handler(
375
+ new Request("http://runner.test/api/rivet/start", {
376
+ method: "POST",
377
+ body: new Uint8Array([1, 2]),
378
+ }),
379
+ ),
380
+ );
381
+
382
+ yield* Effect.promise(() =>
383
+ (async (response: Response) => {
384
+ assert.strictEqual(response.status, 413);
385
+ const body = (await response.json()) as {
386
+ readonly group: string;
387
+ readonly code: string;
388
+ readonly message: string;
389
+ };
390
+ assert.deepStrictEqual(
391
+ { group: body.group, code: body.code },
392
+ { group: "message", code: "incoming_too_long" },
393
+ );
394
+ assert.match(body.message, /limit is 1 bytes/);
395
+ })(response),
396
+ );
397
+ }),
398
+ ),
399
+ );
400
+ });
@@ -0,0 +1,304 @@
1
+ import { Context, Effect, Layer, type Scope } from "effect";
2
+ import {
3
+ HttpEffect,
4
+ type HttpMiddleware,
5
+ type HttpServerError,
6
+ type HttpServerRequest,
7
+ type HttpServerResponse,
8
+ } from "effect/unstable/http";
9
+ import * as Rivetkit from "@ohos-ports/rivetkit";
10
+ import * as RivetkitLog from "@ohos-ports/rivetkit/log";
11
+ import * as Client from "./Client.ts";
12
+ import { BaseLogger, getOrCreateBaseLogger } from "./internal/logging.ts";
13
+ import * as RivetLogger from "./RivetLogger.ts";
14
+
15
+ const TypeId = "~@rivetkit/effect/Registry";
16
+ type ServerlessOptions = NonNullable<
17
+ Rivetkit.RegistryConfigInput<Rivetkit.RegistryActors>["serverless"]
18
+ >;
19
+
20
+ export type Options = Pick<
21
+ Rivetkit.RegistryConfigInput<Rivetkit.RegistryActors>,
22
+ "endpoint" | "token" | "namespace" | "noWelcome" | "sqlite"
23
+ >;
24
+
25
+ export interface Registry {
26
+ readonly [TypeId]: typeof TypeId;
27
+
28
+ readonly options: Options;
29
+
30
+ readonly baseLogger: RivetkitLog.Logger;
31
+
32
+ readonly rivetkitActors: Map<string, Rivetkit.AnyActorDefinition>;
33
+ }
34
+
35
+ export const Registry: Context.Service<Registry, Registry> =
36
+ Context.Service<Registry>("@rivetkit/effect/Registry");
37
+
38
+ /**
39
+ * Creates the shared registry that actor layers register into.
40
+ *
41
+ * @remarks
42
+ * Actor layers created with {@link Actor.Actor.toLayer|`Actor.toLayer()`}
43
+ * register into this service. Registry entrypoints such as
44
+ * {@link serve|`Registry.serve()`}, {@link toWebHandler|`Registry.toWebHandler()`},
45
+ * and {@link toHttpEffect|`Registry.toHttpEffect()`} later use the collected
46
+ * actors to create the underlying RivetKit runtime.
47
+ *
48
+ * @param options - RivetKit runtime options shared by the registered actors.
49
+ *
50
+ * @returns A layer that provides the {@link Registry|`Registry.Registry`}
51
+ * service.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { Registry } from "@rivetkit/effect"
56
+ * import { Layer } from "effect"
57
+ *
58
+ * const ActorsLayer = Layer.mergeAll(ChatRoomLive, ModeratorLive)
59
+ *
60
+ * const MainLayer = Registry.serve(ActorsLayer).pipe(
61
+ * Layer.provide(Registry.layer({ endpoint: "http://127.0.0.1:6420" }))
62
+ * )
63
+ * ```
64
+ *
65
+ * @see {@link serve|`Registry.serve()`} for starting a long-running actor
66
+ * server.
67
+ * @see {@link toWebHandler|`Registry.toWebHandler()`} for Fetch-compatible
68
+ * serverless handlers.
69
+ * @see {@link toHttpEffect|`Registry.toHttpEffect()`} for Effect HTTP handlers.
70
+ */
71
+ export const layer = (options: Options = {}): Layer.Layer<Registry> =>
72
+ Layer.effect(
73
+ Registry,
74
+ Effect.map(getOrCreateBaseLogger, (baseLogger) =>
75
+ Registry.of({
76
+ [TypeId]: TypeId,
77
+ options,
78
+ baseLogger,
79
+ rivetkitActors: new Map(),
80
+ }),
81
+ ),
82
+ );
83
+
84
+ const setupRivetkitRegistry = (
85
+ registry: Registry,
86
+ options?: {
87
+ readonly serverless?: ServerlessOptions | undefined;
88
+ },
89
+ ) =>
90
+ Effect.sync(() =>
91
+ Rivetkit.setup({
92
+ use: Object.fromEntries(registry.rivetkitActors),
93
+ ...registry.options,
94
+ logging: { baseLogger: registry.baseLogger },
95
+ ...(options?.serverless === undefined
96
+ ? {}
97
+ : { serverless: options.serverless }),
98
+ }),
99
+ );
100
+
101
+ /**
102
+ * Serves the actors registered by `actorsLayer`.
103
+ *
104
+ * @remarks
105
+ * Provide this with {@link layer|`Registry.layer()`} at the application root.
106
+ * Building the returned layer evaluates the actor layer, collects its registered
107
+ * actors, and starts the underlying RivetKit runtime using the options from
108
+ * {@link layer|`Registry.layer()`}. Launch the resulting layer with
109
+ * {@link Layer.launch|`Layer.launch()`} to keep the server process alive.
110
+ *
111
+ * @param actorsLayer - A layer containing one or more
112
+ * {@link Actor.Actor.toLayer|`Actor.toLayer()`} registrations.
113
+ *
114
+ * @returns A side-effecting startup layer that requires the
115
+ * {@link Registry|`Registry.Registry`} service and the services required by
116
+ * `actorsLayer`.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { Registry } from "@rivetkit/effect"
121
+ * import { Layer } from "effect"
122
+ *
123
+ * const ActorsLayer = Layer.mergeAll(ChatRoomLive, ModeratorLive)
124
+ *
125
+ * const MainLayer = Registry.serve(ActorsLayer).pipe(
126
+ * Layer.provide(Registry.layer({ endpoint: "http://127.0.0.1:6420" }))
127
+ * )
128
+ *
129
+ * Layer.launch(MainLayer).pipe(NodeRuntime.runMain);
130
+ * ```
131
+ *
132
+ * @see {@link toWebHandler|`Registry.toWebHandler()`} for Fetch-compatible
133
+ * serverless handlers.
134
+ * @see {@link toHttpEffect|`Registry.toHttpEffect()`} for Effect HTTP handlers.
135
+ *
136
+ */
137
+ export const serve = <E, R>(
138
+ actorsLayer: Layer.Layer<never, E, R>,
139
+ ): Layer.Layer<never, E, R | Registry> =>
140
+ Layer.effectDiscard(
141
+ Effect.gen(function* () {
142
+ const registry = yield* Registry;
143
+ yield* Layer.build(
144
+ actorsLayer.pipe(
145
+ Layer.provideMerge(
146
+ RivetLogger.layerFromPino(registry.baseLogger),
147
+ ),
148
+ ),
149
+ );
150
+ const rivetkitRegistry = yield* setupRivetkitRegistry(registry);
151
+ yield* Effect.sync(() => rivetkitRegistry.start());
152
+ }),
153
+ );
154
+
155
+ /**
156
+ * In-process test runtime. Boots the rivetkit registry against the
157
+ * configured engine, waits for `/health` to answer, and provides
158
+ * `Client` from the same Layer so consumers don't need to wire
159
+ * `Client.layer` separately. Mirrors `Registry.start` plus test-mode
160
+ * flags and a scoped client dispose. The registry itself is leaked
161
+ * to process exit because the public rivetkit `Registry` doesn't
162
+ * expose a public `shutdown()` today; only the SIGINT handler can
163
+ * drive `#runShutdown`. This matches `setupTest`'s existing behavior.
164
+ */
165
+ export const test: Layer.Layer<Client.Client, never, Registry> = Layer.effect(
166
+ Client.Client,
167
+ Effect.gen(function* () {
168
+ const registry = yield* Registry;
169
+ const rivetkitRegistry = yield* setupRivetkitRegistry(registry);
170
+ rivetkitRegistry.config.test = {
171
+ ...rivetkitRegistry.config.test,
172
+ enabled: true,
173
+ };
174
+ rivetkitRegistry.config.noWelcome = true;
175
+ // Auto-spawn the engine when no endpoint was provided, so
176
+ // `Registry.test` works out of the box without requiring the
177
+ // caller to start an engine externally. If the user wired an
178
+ // explicit endpoint via `Registry.layer({ endpoint: ... })`,
179
+ // honor it and skip the local spawn.
180
+ if (registry.options.endpoint === undefined) {
181
+ rivetkitRegistry.config.startEngine = true;
182
+ }
183
+ yield* Effect.sync(() => rivetkitRegistry.start());
184
+
185
+ // The rivetkitRegistry itself is leaked until process exit (matches
186
+ // setupTest's behavior). The public Rivetkit.Registry doesn't
187
+ // expose a shutdown method; only the SIGINT handler can drive the
188
+ // inner .shutdown(). Disposing the client is the only cleanup we
189
+ // can do cleanly today.
190
+ //
191
+ // When the engine was auto-spawned, propagate its resolved
192
+ // endpoint to the client so `createClient` doesn't fall back
193
+ // to its (warning-emitting) default.
194
+ const resolvedEndpoint = rivetkitRegistry.parseConfig().endpoint;
195
+
196
+ return yield* Client.make({
197
+ ...registry.options,
198
+ endpoint: registry.options.endpoint ?? resolvedEndpoint,
199
+ }).pipe(Effect.provideService(BaseLogger, registry.baseLogger));
200
+ }),
201
+ );
202
+
203
+ const makeHttpEffect = (
204
+ registry: Registry,
205
+ options?: ToHttpEffectOptions,
206
+ ): Effect.Effect<
207
+ Effect.Effect<
208
+ HttpServerResponse.HttpServerResponse,
209
+ HttpServerError.HttpServerError,
210
+ HttpServerRequest.HttpServerRequest
211
+ >
212
+ > =>
213
+ Effect.map(
214
+ setupRivetkitRegistry(registry, {
215
+ serverless: options,
216
+ }),
217
+ (rivetkitRegistry) =>
218
+ HttpEffect.fromWebHandler((request) =>
219
+ rivetkitRegistry.handler(request),
220
+ ),
221
+ );
222
+
223
+ export type ToHttpEffectOptions = ServerlessOptions;
224
+
225
+ /**
226
+ * Builds a scoped Effect HTTP handler from a registry layer.
227
+ *
228
+ * The registry layer is built once in the surrounding scope. Registered Rivet
229
+ * Actors are materialized into a single underlying RivetKit registry, and each
230
+ * request is delegated to that registry's serverless handler.
231
+ */
232
+ export const toHttpEffect = Effect.fnUntraced(function* <E>(
233
+ registryLayer: Layer.Layer<Registry, E>,
234
+ options?: ToHttpEffectOptions,
235
+ ): Effect.fn.Return<
236
+ Effect.Effect<
237
+ HttpServerResponse.HttpServerResponse,
238
+ HttpServerError.HttpServerError,
239
+ HttpServerRequest.HttpServerRequest
240
+ >,
241
+ E,
242
+ Scope.Scope
243
+ > {
244
+ const context = yield* Layer.build(
245
+ registryLayer.pipe(Layer.provideMerge(RivetLogger.defaultLayer)),
246
+ );
247
+ const httpEffect = yield* makeHttpEffect(
248
+ Context.get(context, Registry),
249
+ options,
250
+ );
251
+ // @effect-diagnostics-next-line returnEffectInGen:off
252
+ return httpEffect.pipe(Effect.provide(context));
253
+ });
254
+
255
+ export type ToWebHandlerOptions = ServerlessOptions & {
256
+ /**
257
+ * Effect HTTP middleware applied around the generated handler.
258
+ */
259
+ readonly middleware?: HttpMiddleware.HttpMiddleware | undefined;
260
+ /**
261
+ * Memo map used while building the registry layer.
262
+ */
263
+ readonly memoMap?: Layer.MemoMap | undefined;
264
+ };
265
+
266
+ /**
267
+ * Builds a Fetch-compatible request handler from a registry layer.
268
+ *
269
+ * This is the serverless entrypoint for the Effect SDK. The registry layer must
270
+ * provide `Registry`, usually by composing actor layers with `Registry.layer`
271
+ * via `Layer.provideMerge`.
272
+ */
273
+ export const toWebHandler = <E>(
274
+ registryLayer: Layer.Layer<Registry, E>,
275
+ options?: ToWebHandlerOptions,
276
+ ) => {
277
+ const { middleware, memoMap } = options ?? {};
278
+ let serverlessOptions: ServerlessOptions | undefined;
279
+ if (options !== undefined) {
280
+ const {
281
+ middleware: _middleware,
282
+ memoMap: _memoMap,
283
+ ...handlerOptions
284
+ } = options;
285
+ serverlessOptions = handlerOptions;
286
+ }
287
+
288
+ const registryLayerWithLogging = registryLayer.pipe(
289
+ Layer.provideMerge(RivetLogger.defaultLayer),
290
+ );
291
+
292
+ return HttpEffect.toWebHandlerLayerWith(registryLayerWithLogging, {
293
+ toHandler: (context) =>
294
+ Effect.map(
295
+ makeHttpEffect(
296
+ Context.get(context, Registry),
297
+ serverlessOptions,
298
+ ),
299
+ (httpEffect) => httpEffect.pipe(Effect.provide(context)),
300
+ ),
301
+ middleware,
302
+ memoMap,
303
+ });
304
+ };