@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.
- package/dist/Action.d.ts +104 -0
- package/dist/Action.d.ts.map +1 -0
- package/dist/Action.js +50 -0
- package/dist/Action.js.map +1 -0
- package/dist/Actor.d.ts +132 -0
- package/dist/Actor.d.ts.map +1 -0
- package/dist/Actor.js +104 -0
- package/dist/Actor.js.map +1 -0
- package/dist/Client.d.ts +31 -0
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +100 -0
- package/dist/Client.js.map +1 -0
- package/dist/Registry.d.ts +134 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Registry.js +178 -0
- package/dist/Registry.js.map +1 -0
- package/dist/RivetError.d.ts +480 -0
- package/dist/RivetError.d.ts.map +1 -0
- package/dist/RivetError.js +985 -0
- package/dist/RivetError.js.map +1 -0
- package/dist/RivetLogger.d.ts +41 -0
- package/dist/RivetLogger.d.ts.map +1 -0
- package/dist/RivetLogger.js +41 -0
- package/dist/RivetLogger.js.map +1 -0
- package/dist/State.d.ts +159 -0
- package/dist/State.d.ts.map +1 -0
- package/dist/State.js +98 -0
- package/dist/State.js.map +1 -0
- package/dist/internal/ActionDispatcher.d.ts +14 -0
- package/dist/internal/ActionDispatcher.d.ts.map +1 -0
- package/dist/internal/ActionDispatcher.js +100 -0
- package/dist/internal/ActionDispatcher.js.map +1 -0
- package/dist/internal/ActionErrorEnvelope.d.ts +11 -0
- package/dist/internal/ActionErrorEnvelope.d.ts.map +1 -0
- package/dist/internal/ActionErrorEnvelope.js +14 -0
- package/dist/internal/ActionErrorEnvelope.js.map +1 -0
- package/dist/internal/ActorInstanceManager.d.ts +28 -0
- package/dist/internal/ActorInstanceManager.d.ts.map +1 -0
- package/dist/internal/ActorInstanceManager.js +51 -0
- package/dist/internal/ActorInstanceManager.js.map +1 -0
- package/dist/internal/ActorStateAdapter.d.ts +18 -0
- package/dist/internal/ActorStateAdapter.d.ts.map +1 -0
- package/dist/internal/ActorStateAdapter.js +24 -0
- package/dist/internal/ActorStateAdapter.js.map +1 -0
- package/dist/internal/StateOptions.d.ts +13 -0
- package/dist/internal/StateOptions.d.ts.map +1 -0
- package/dist/internal/StateOptions.js +2 -0
- package/dist/internal/StateOptions.js.map +1 -0
- package/dist/internal/logging.d.ts +22 -0
- package/dist/internal/logging.d.ts.map +1 -0
- package/dist/internal/logging.js +138 -0
- package/dist/internal/logging.js.map +1 -0
- package/dist/internal/tracing.d.ts +23 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +30 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/utils.d.ts +7 -0
- package/dist/internal/utils.d.ts.map +1 -0
- package/dist/internal/utils.js +7 -0
- package/dist/internal/utils.js.map +1 -0
- package/dist/mod.d.ts +8 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +8 -0
- package/dist/mod.js.map +1 -0
- package/package.json +56 -0
- package/src/Action.ts +231 -0
- package/src/Actor.test-d.ts +641 -0
- package/src/Actor.test.ts +209 -0
- package/src/Actor.ts +575 -0
- package/src/Client.test.ts +206 -0
- package/src/Client.ts +218 -0
- package/src/Registry.test-d.ts +126 -0
- package/src/Registry.test.ts +400 -0
- package/src/Registry.ts +304 -0
- package/src/RivetError.test.ts +199 -0
- package/src/RivetError.ts +1163 -0
- package/src/RivetLogger.ts +60 -0
- package/src/State.test.ts +340 -0
- package/src/State.ts +420 -0
- package/src/internal/ActionDispatcher.ts +189 -0
- package/src/internal/ActionErrorEnvelope.ts +19 -0
- package/src/internal/ActorInstanceManager.ts +137 -0
- package/src/internal/ActorStateAdapter.ts +82 -0
- package/src/internal/StateOptions.ts +21 -0
- package/src/internal/logging.test.ts +274 -0
- package/src/internal/logging.ts +197 -0
- package/src/internal/tracing.ts +42 -0
- package/src/internal/utils.ts +12 -0
- 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
|
+
});
|
package/src/Registry.ts
ADDED
|
@@ -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
|
+
};
|