Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit ec58033

Browse files
committed
feat(core): add inline client driver to ActorContext & ActionContext
1 parent 921b4eb commit ec58033

File tree

16 files changed

+296
-10
lines changed

16 files changed

+296
-10
lines changed

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,6 @@ Always include a README.md. The `README.md` should always follow this structure:
196196
Apache 2.0
197197
```
198198

199+
## Test Notes
200+
201+
- Using setTimeout in tests & test actors will not work unless you call `await waitFor(driverTestConfig, <ts>)`
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { actor } from "@rivetkit/core";
2+
import type { Registry } from "./registry";
3+
4+
export const inlineClientActor = actor({
5+
onAuth: () => {},
6+
state: { messages: [] as string[] },
7+
actions: {
8+
// Action that uses client to call another actor (stateless)
9+
callCounterIncrement: async (c, amount: number) => {
10+
const client = c.client<Registry>();
11+
const result = await client.counter.getOrCreate(["inline-test"]).increment(amount);
12+
c.state.messages.push(`Called counter.increment(${amount}), result: ${result}`);
13+
return result;
14+
},
15+
16+
// Action that uses client to get counter state (stateless)
17+
getCounterState: async (c) => {
18+
const client = c.client<Registry>();
19+
const count = await client.counter.getOrCreate(["inline-test"]).getCount();
20+
c.state.messages.push(`Got counter state: ${count}`);
21+
return count;
22+
},
23+
24+
// Action that uses client with .connect() for stateful communication
25+
connectToCounterAndIncrement: async (c, amount: number) => {
26+
const client = c.client<Registry>();
27+
const handle = client.counter.getOrCreate(["inline-test-stateful"]);
28+
const connection = handle.connect();
29+
30+
// Set up event listener
31+
const events: number[] = [];
32+
connection.on("newCount", (count: number) => {
33+
events.push(count);
34+
});
35+
36+
// Perform increments
37+
const result1 = await connection.increment(amount);
38+
const result2 = await connection.increment(amount * 2);
39+
40+
await connection.dispose();
41+
42+
c.state.messages.push(`Connected to counter, incremented by ${amount} and ${amount * 2}, results: ${result1}, ${result2}, events: ${JSON.stringify(events)}`);
43+
44+
return { result1, result2, events };
45+
},
46+
47+
// Get all messages from this actor's state
48+
getMessages: (c) => {
49+
return c.state.messages;
50+
},
51+
52+
// Clear messages
53+
clearMessages: (c) => {
54+
c.state.messages = [];
55+
},
56+
},
57+
});

packages/core/fixtures/driver-test-suite/registry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { connStateActor } from "./conn-state";
2424
// Import actors from individual files
2525
import { counter } from "./counter";
2626
import { customTimeoutActor, errorHandlingActor } from "./error-handling";
27+
import { inlineClientActor } from "./inline-client";
2728
import { counterWithLifecycle } from "./lifecycle";
2829
import { metadataActor } from "./metadata";
2930
import { scheduled } from "./scheduled";
@@ -47,6 +48,8 @@ export const registry = setup({
4748
// From error-handling.ts
4849
errorHandlingActor,
4950
customTimeoutActor,
51+
// From inline-client.ts
52+
inlineClientActor,
5053
// From action-inputs.ts
5154
inputActor,
5255
// From action-timeout.ts

packages/core/src/actor/action.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import type { Logger } from "@/common/log";
21
import type { ActorKey } from "@/actor/mod";
2+
import type { Logger } from "@/common/log";
33
import type { Conn } from "./connection";
44
import type { ConnId } from "./connection";
55
import type { ActorContext } from "./context";
66
import type { SaveStateOptions } from "./instance";
77
import type { Schedule } from "./schedule";
8+
import { Registry } from "@/registry/mod";
9+
import { Client } from "@/client/client";
810

911
/**
1012
* Context for a remote procedure call.
@@ -97,6 +99,13 @@ export class ActionContext<S, CP, CS, V, I, AD, DB> {
9799
return this.#actorContext.conns;
98100
}
99101

102+
/**
103+
* Returns the client for the given registry.
104+
*/
105+
client<R extends Registry<any>>(): Client<R> {
106+
return this.#actorContext.client<R>();
107+
}
108+
100109
/**
101110
* @experimental
102111
*/

packages/core/src/actor/context.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import type { Logger } from "@/common/log";
21
import type { ActorKey } from "@/actor/mod";
2+
import type { Logger } from "@/common/log";
33
import type { Conn, ConnId } from "./connection";
44
import type { ActorInstance, SaveStateOptions } from "./instance";
55
import type { Schedule } from "./schedule";
6+
import { Client } from "@/client/client";
7+
import { Registry } from "@/registry/mod";
68

79
/**
810
* ActorContext class that provides access to actor methods and state
@@ -87,6 +89,13 @@ export class ActorContext<S, CP, CS, V, I, AD, DB> {
8789
return this.#actor.conns;
8890
}
8991

92+
/**
93+
* Returns the client for the given registry.
94+
*/
95+
client<R extends Registry<any>>(): Client<R> {
96+
return this.#actor.inlineClient as Client<R>;
97+
}
98+
9099
/**
91100
* Gets the database.
92101
* @experimental

packages/core/src/actor/instance.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import type { ActorKey } from "@/actor/mod";
12
import type * as wsToClient from "@/actor/protocol/message/to-client";
23
import type * as wsToServer from "@/actor/protocol/message/to-server";
4+
import type { Client } from "@/client/client";
35
import type { Logger } from "@/common/log";
4-
import type { ActorKey } from "@/actor/mod";
56
import { isJsonSerializable, stringifyError } from "@/common/utils";
7+
import type { Registry } from "@/mod";
68
import invariant from "invariant";
79
import onChange from "on-change";
810
import type { ActionContext } from "./action";
@@ -136,6 +138,7 @@ export class ActorInstance<S, CP, CS, V, I, AD, DB> {
136138
#config: ActorConfig<S, CP, CS, V, I, AD, DB>;
137139
#connectionDrivers!: ConnDrivers;
138140
#actorDriver!: ActorDriver;
141+
#inlineClient!: Client<Registry<any>>;
139142
#actorId!: string;
140143
#name!: string;
141144
#key!: ActorKey;
@@ -154,6 +157,10 @@ export class ActorInstance<S, CP, CS, V, I, AD, DB> {
154157
return this.#actorId;
155158
}
156159

160+
get inlineClient(): Client<Registry<any>> {
161+
return this.#inlineClient;
162+
}
163+
157164
/**
158165
* This constructor should never be used directly.
159166
*
@@ -169,13 +176,15 @@ export class ActorInstance<S, CP, CS, V, I, AD, DB> {
169176
async start(
170177
connectionDrivers: ConnDrivers,
171178
actorDriver: ActorDriver,
179+
inlineClient: Client<Registry<any>>,
172180
actorId: string,
173181
name: string,
174182
key: ActorKey,
175183
region: string,
176184
) {
177185
this.#connectionDrivers = connectionDrivers;
178186
this.#actorDriver = actorDriver;
187+
this.#inlineClient = inlineClient;
179188
this.#actorId = actorId;
180189
this.#name = name;
181190
this.#key = key;

packages/core/src/driver-test-suite/mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { runActorConnStateTests } from "./tests/actor-conn-state";
2020
import { runActorDriverTests } from "./tests/actor-driver";
2121
import { runActorErrorHandlingTests } from "./tests/actor-error-handling";
2222
import { runActorHandleTests } from "./tests/actor-handle";
23+
import { runActorInlineClientTests } from "./tests/actor-inline-client";
2324
import { runActorMetadataTests } from "./tests/actor-metadata";
2425
import { runActorVarsTests } from "./tests/actor-vars";
2526
import { runManagerDriverTests } from "./tests/manager-driver";
@@ -94,6 +95,8 @@ export function runDriverTests(
9495
runActorErrorHandlingTests(driverTestConfig);
9596

9697
runActorAuthTests(driverTestConfig);
98+
99+
runActorInlineClientTests(driverTestConfig);
97100
});
98101
}
99102
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { describe, expect, test } from "vitest";
2+
import type { DriverTestConfig } from "../mod";
3+
import { setupDriverTest } from "../utils";
4+
5+
export function runActorInlineClientTests(driverTestConfig: DriverTestConfig) {
6+
describe("Actor Inline Client Tests", () => {
7+
describe("Stateless Client Calls", () => {
8+
test("should make stateless calls to other actors", async (c) => {
9+
const { client } = await setupDriverTest(c, driverTestConfig);
10+
11+
// Create the inline client actor
12+
const inlineClientHandle = client.inlineClientActor.getOrCreate(["inline-client-test"]);
13+
14+
// Test calling counter.increment via inline client
15+
const result = await inlineClientHandle.callCounterIncrement(5);
16+
expect(result).toBe(5);
17+
18+
// Verify the counter state was actually updated
19+
const counterState = await inlineClientHandle.getCounterState();
20+
expect(counterState).toBe(5);
21+
22+
// Check that messages were logged
23+
const messages = await inlineClientHandle.getMessages();
24+
expect(messages).toHaveLength(2);
25+
expect(messages[0]).toContain("Called counter.increment(5), result: 5");
26+
expect(messages[1]).toContain("Got counter state: 5");
27+
});
28+
29+
test("should handle multiple stateless calls", async (c) => {
30+
const { client } = await setupDriverTest(c, driverTestConfig);
31+
32+
// Create the inline client actor
33+
const inlineClientHandle = client.inlineClientActor.getOrCreate(["inline-client-multi"]);
34+
35+
// Clear any existing messages
36+
await inlineClientHandle.clearMessages();
37+
38+
// Make multiple calls
39+
const result1 = await inlineClientHandle.callCounterIncrement(3);
40+
const result2 = await inlineClientHandle.callCounterIncrement(7);
41+
const finalState = await inlineClientHandle.getCounterState();
42+
43+
expect(result1).toBe(3);
44+
expect(result2).toBe(10); // 3 + 7
45+
expect(finalState).toBe(10);
46+
47+
// Check messages
48+
const messages = await inlineClientHandle.getMessages();
49+
expect(messages).toHaveLength(3);
50+
expect(messages[0]).toContain("Called counter.increment(3), result: 3");
51+
expect(messages[1]).toContain("Called counter.increment(7), result: 10");
52+
expect(messages[2]).toContain("Got counter state: 10");
53+
});
54+
});
55+
56+
describe("Stateful Client Calls", () => {
57+
test("should connect to other actors and receive events", async (c) => {
58+
const { client } = await setupDriverTest(c, driverTestConfig);
59+
60+
// Create the inline client actor
61+
const inlineClientHandle = client.inlineClientActor.getOrCreate(["inline-client-stateful"]);
62+
63+
// Clear any existing messages
64+
await inlineClientHandle.clearMessages();
65+
66+
// Test stateful connection with events
67+
const result = await inlineClientHandle.connectToCounterAndIncrement(4);
68+
69+
expect(result.result1).toBe(4);
70+
expect(result.result2).toBe(12); // 4 + 8
71+
expect(result.events).toEqual([4, 12]); // Should have received both events
72+
73+
// Check that message was logged
74+
const messages = await inlineClientHandle.getMessages();
75+
expect(messages).toHaveLength(1);
76+
expect(messages[0]).toContain("Connected to counter, incremented by 4 and 8");
77+
expect(messages[0]).toContain("results: 4, 12");
78+
expect(messages[0]).toContain("events: [4,12]");
79+
});
80+
81+
test("should handle stateful connection independently", async (c) => {
82+
const { client } = await setupDriverTest(c, driverTestConfig);
83+
84+
// Create the inline client actor
85+
const inlineClientHandle = client.inlineClientActor.getOrCreate(["inline-client-independent"]);
86+
87+
// Clear any existing messages
88+
await inlineClientHandle.clearMessages();
89+
90+
// Test with different increment values
91+
const result = await inlineClientHandle.connectToCounterAndIncrement(2);
92+
93+
expect(result.result1).toBe(2);
94+
expect(result.result2).toBe(6); // 2 + 4
95+
expect(result.events).toEqual([2, 6]);
96+
97+
// Verify the state is independent from previous tests
98+
const messages = await inlineClientHandle.getMessages();
99+
expect(messages).toHaveLength(1);
100+
expect(messages[0]).toContain("Connected to counter, incremented by 2 and 4");
101+
});
102+
});
103+
104+
describe("Mixed Client Usage", () => {
105+
test("should handle both stateless and stateful calls", async (c) => {
106+
const { client } = await setupDriverTest(c, driverTestConfig);
107+
108+
// Create the inline client actor
109+
const inlineClientHandle = client.inlineClientActor.getOrCreate(["inline-client-mixed"]);
110+
111+
// Clear any existing messages
112+
await inlineClientHandle.clearMessages();
113+
114+
// Start with stateless calls
115+
await inlineClientHandle.callCounterIncrement(1);
116+
const statelessResult = await inlineClientHandle.getCounterState();
117+
expect(statelessResult).toBe(1);
118+
119+
// Then do stateful call
120+
const statefulResult = await inlineClientHandle.connectToCounterAndIncrement(3);
121+
expect(statefulResult.result1).toBe(3);
122+
expect(statefulResult.result2).toBe(9); // 3 + 6
123+
124+
// Check all messages were logged
125+
const messages = await inlineClientHandle.getMessages();
126+
expect(messages).toHaveLength(3);
127+
expect(messages[0]).toContain("Called counter.increment(1), result: 1");
128+
expect(messages[1]).toContain("Got counter state: 1");
129+
expect(messages[2]).toContain("Connected to counter, incremented by 3 and 6");
130+
});
131+
});
132+
});
133+
}

packages/core/src/topologies/coordinate/actor-peer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { ActorDriver } from "@/actor/driver";
22
import type { AnyActorInstance } from "@/actor/instance";
33
import type { ActorKey } from "@/actor/mod";
4+
import type { Client } from "@/client/client";
5+
import type { Registry } from "@/mod";
46
import type { RegistryConfig } from "@/registry/config";
57
import type { RunConfig } from "@/registry/run-config";
68
import type { GlobalState } from "@/topologies/coordinate/topology";
@@ -16,6 +18,7 @@ export class ActorPeer {
1618
#runConfig: RunConfig;
1719
#coordinateDriver: CoordinateDriver;
1820
#actorDriver: ActorDriver;
21+
#inlineClient: Client<Registry<any>>;
1922
#globalState: GlobalState;
2023
#actorId: string;
2124
#actorName?: string;
@@ -47,13 +50,15 @@ export class ActorPeer {
4750
runConfig: RunConfig,
4851
CoordinateDriver: CoordinateDriver,
4952
actorDriver: ActorDriver,
53+
inlineClient: Client<Registry<any>>,
5054
globalState: GlobalState,
5155
actorId: string,
5256
) {
5357
this.#registryConfig = registryConfig;
5458
this.#runConfig = runConfig;
5559
this.#coordinateDriver = CoordinateDriver;
5660
this.#actorDriver = actorDriver;
61+
this.#inlineClient = inlineClient;
5762
this.#globalState = globalState;
5863
this.#actorId = actorId;
5964
}
@@ -63,6 +68,7 @@ export class ActorPeer {
6368
registryConfig: RegistryConfig,
6469
runConfig: RunConfig,
6570
actorDriver: ActorDriver,
71+
inlineClient: Client<Registry<any>>,
6672
CoordinateDriver: CoordinateDriver,
6773
globalState: GlobalState,
6874
actorId: string,
@@ -77,6 +83,7 @@ export class ActorPeer {
7783
runConfig,
7884
CoordinateDriver,
7985
actorDriver,
86+
inlineClient,
8087
globalState,
8188
actorId,
8289
);
@@ -226,6 +233,7 @@ export class ActorPeer {
226233
),
227234
},
228235
this.#actorDriver,
236+
this.#inlineClient,
229237
this.#actorId,
230238
this.#actorName,
231239
this.#actorKey,

0 commit comments

Comments
 (0)