Skip to content

Commit 262e496

Browse files
committed
feat: add ActorHandle.resolve to resolve actor ID
1 parent 7c7e92a commit 262e496

File tree

8 files changed

+269
-61
lines changed

8 files changed

+269
-61
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- **Build:** `yarn build` - Production build using Turbopack
2626
- **Build specific package:** `yarn build -F actor-core` - Build only specified package
2727
- **Format:** `yarn fmt` - Format code with Biome
28+
- Do not run the format command automatically.
2829

2930
## Core Concepts
3031

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { z } from "zod";
2+
3+
export const ResolveResponseSchema = z.object({
4+
// Actor ID
5+
i: z.string(),
6+
});
7+
8+
export type ResolveResponse = z.infer<typeof ResolveResponseSchema>;

packages/actor-core/src/client/actor_common.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import type { AnyActorDefinition, ActorDefinition } from "@/actor/definition";
2+
import type * as protoHttpResolve from "@/actor/protocol/http/resolve";
3+
import type { Encoding } from "@/actor/protocol/serde";
4+
import type { ActorQuery } from "@/manager/protocol/query";
5+
import { logger } from "./log";
6+
import * as errors from "./errors";
7+
import { sendHttpRequest } from "./utils";
28

39
/**
410
* RPC function returned by Actor connections and handles.
@@ -20,10 +26,55 @@ export type ActorRPCFunction<
2026
* Maps RPC methods from actor definition to typed function signatures.
2127
*/
2228
export type ActorDefinitionRpcs<AD extends AnyActorDefinition> =
23-
AD extends ActorDefinition<any, any, any, any, infer R> ? {
24-
[K in keyof R]: R[K] extends (
25-
...args: infer Args
26-
) => infer Return
27-
? ActorRPCFunction<Args, Return>
28-
: never;
29-
} : never;
29+
AD extends ActorDefinition<any, any, any, any, infer R>
30+
? {
31+
[K in keyof R]: R[K] extends (...args: infer Args) => infer Return
32+
? ActorRPCFunction<Args, Return>
33+
: never;
34+
}
35+
: never;
36+
37+
/**
38+
* Resolves an actor ID from a query by making a request to the /actors/resolve endpoint
39+
*
40+
* @param {string} endpoint - The manager endpoint URL
41+
* @param {ActorQuery} actorQuery - The query to resolve
42+
* @param {Encoding} encodingKind - The encoding to use (json or cbor)
43+
* @returns {Promise<string>} - A promise that resolves to the actor's ID
44+
*/
45+
export async function resolveActorId(
46+
endpoint: string,
47+
actorQuery: ActorQuery,
48+
encodingKind: Encoding,
49+
): Promise<string> {
50+
logger().debug("resolving actor ID", { query: actorQuery });
51+
52+
// Construct the URL using the current actor query
53+
const queryParam = encodeURIComponent(JSON.stringify(actorQuery));
54+
const url = `${endpoint}/actors/resolve?encoding=${encodingKind}&query=${queryParam}`;
55+
56+
// Use the shared HTTP request utility with integrated serialization
57+
try {
58+
const result = await sendHttpRequest<
59+
Record<never, never>,
60+
protoHttpResolve.ResolveResponse
61+
>({
62+
url,
63+
method: "POST",
64+
body: {},
65+
encoding: encodingKind,
66+
});
67+
68+
logger().debug("resolved actor ID", { actorId: result.i });
69+
return result.i;
70+
} catch (error) {
71+
logger().error("failed to resolve actor ID", { error });
72+
if (error instanceof errors.ActorError) {
73+
throw error;
74+
} else {
75+
throw new errors.InternalError(
76+
`Failed to resolve actor ID: ${String(error)}`,
77+
);
78+
}
79+
}
80+
}

packages/actor-core/src/client/actor_conn.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1+
import type { AnyActorDefinition } from "@/actor/definition";
12
import type { Transport } from "@/actor/protocol/message/mod";
2-
import type { Encoding } from "@/actor/protocol/serde";
33
import type * as wsToClient from "@/actor/protocol/message/to-client";
44
import type * as wsToServer from "@/actor/protocol/message/to-server";
5+
import type { Encoding } from "@/actor/protocol/serde";
6+
import { importEventSource } from "@/common/eventsource";
57
import { MAX_CONN_PARAMS_SIZE } from "@/common/network";
68
import { assertUnreachable, stringifyError } from "@/common/utils";
9+
import { importWebSocket } from "@/common/websocket";
10+
import type { ActorQuery } from "@/manager/protocol/query";
711
import * as cbor from "cbor-x";
12+
import pRetry from "p-retry";
13+
import type { ActorDefinitionRpcs as ActorDefinitionRpcsImport } from "./actor_common";
14+
import { ACTOR_CONNS_SYMBOL, type ClientRaw, TRANSPORT_SYMBOL } from "./client";
815
import * as errors from "./errors";
916
import { logger } from "./log";
1017
import { type WebSocketMessage as ConnMessage, messageLength } from "./utils";
11-
import { ACTOR_CONNS_SYMBOL, TRANSPORT_SYMBOL, type ClientRaw } from "./client";
12-
import type { AnyActorDefinition } from "@/actor/definition";
13-
import pRetry from "p-retry";
14-
import { importWebSocket } from "@/common/websocket";
15-
import { importEventSource } from "@/common/eventsource";
16-
import type { ActorQuery } from "@/manager/protocol/query";
17-
import { ActorDefinitionRpcs as ActorDefinitionRpcsImport } from "./actor_common";
1818

1919
// Re-export the type with the original name to maintain compatibility
2020
type ActorDefinitionRpcs<AD extends AnyActorDefinition> =
@@ -679,7 +679,7 @@ enc
679679
// Get the manager endpoint from the endpoint provided
680680
const actorQueryStr = encodeURIComponent(JSON.stringify(this.actorQuery));
681681

682-
let url = `${this.endpoint}/actors/connections/${this.#connectionId}/message?encoding=${this.encodingKind}&connectionToken=${encodeURIComponent(this.#connectionToken)}&query=${actorQueryStr}`;
682+
const url = `${this.endpoint}/actors/connections/${this.#connectionId}/message?encoding=${this.encodingKind}&connectionToken=${encodeURIComponent(this.#connectionToken)}&query=${actorQueryStr}`;
683683

684684
// TODO: Implement ordered messages, this is not guaranteed order. Needs to use an index in order to ensure we can pipeline requests efficiently.
685685
// TODO: Validate that we're using HTTP/3 whenever possible for pipelining requests
@@ -845,4 +845,4 @@ enc
845845
*/
846846

847847
export type ActorConn<AD extends AnyActorDefinition> = ActorConnRaw &
848-
ActorDefinitionRpcs<AD>;
848+
ActorDefinitionRpcs<AD>;

packages/actor-core/src/client/actor_handle.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import type { Encoding } from "@/actor/protocol/serde";
2-
import { logger } from "./log";
3-
import { sendHttpRequest } from "./utils";
41
import type { AnyActorDefinition } from "@/actor/definition";
5-
import type { ActorQuery } from "@/manager/protocol/query";
6-
import type { ActorDefinitionRpcs } from "./actor_common";
72
import type { RpcRequest, RpcResponse } from "@/actor/protocol/http/rpc";
3+
import type { Encoding } from "@/actor/protocol/serde";
4+
import type { ActorQuery } from "@/manager/protocol/query";
5+
import { type ActorDefinitionRpcs, resolveActorId } from "./actor_common";
86
import { type ActorConn, ActorConnRaw } from "./actor_conn";
97
import { CREATE_ACTOR_CONN_PROXY, type ClientRaw } from "./client";
8+
import { logger } from "./log";
9+
import { sendHttpRequest } from "./utils";
10+
import invariant from "invariant";
11+
import { assertUnreachable } from "@/actor/utils";
1012

1113
/**
1214
* Provides underlying functions for stateless {@link ActorHandle} for RPC calls.
@@ -111,6 +113,34 @@ export class ActorHandleRaw {
111113
conn,
112114
) as ActorConn<AnyActorDefinition>;
113115
}
116+
117+
/**
118+
* Resolves the actor to get its unique actor ID
119+
*
120+
* @returns {Promise<string>} - A promise that resolves to the actor's ID
121+
*/
122+
async resolve(): Promise<string> {
123+
if (
124+
"getForKey" in this.#actorQuery ||
125+
"getOrCreateForKey" in this.#actorQuery
126+
) {
127+
const actorId = await resolveActorId(
128+
this.#endpoint,
129+
this.#actorQuery,
130+
this.#encodingKind,
131+
);
132+
this.#actorQuery = { getForId: { actorId } };
133+
return actorId;
134+
} else if ("getForId" in this.#actorQuery) {
135+
// SKip since it's already resolved
136+
return this.#actorQuery.getForId.actorId;
137+
} else if ("create" in this.#actorQuery) {
138+
// Cannot create a handle with this query
139+
invariant(false, "actorQuery cannot be create");
140+
} else {
141+
assertUnreachable(this.#actorQuery);
142+
}
143+
}
114144
}
115145

116146
/**
@@ -135,4 +165,6 @@ export type ActorHandle<AD extends AnyActorDefinition> = Omit<
135165
> & {
136166
// Add typed version of ActorConn (instead of using AnyActorDefinition)
137167
connect(): ActorConn<AD>;
168+
// Resolve method returns the actor ID
169+
resolve(): Promise<string>;
138170
} & ActorDefinitionRpcs<AD>;

packages/actor-core/src/client/client.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { ActorQuery } from "@/manager/protocol/query";
44
import * as errors from "./errors";
55
import { ActorConn, ActorConnRaw, CONNECT_SYMBOL } from "./actor_conn";
66
import { ActorHandle, ActorHandleRaw } from "./actor_handle";
7-
import { ActorRPCFunction } from "./actor_common";
7+
import { ActorRPCFunction, resolveActorId } from "./actor_common";
88
import { logger } from "./log";
99
import type { ActorCoreApp } from "@/mod";
1010
import type { AnyActorDefinition } from "@/actor/definition";
@@ -55,14 +55,17 @@ export interface ActorAccessor<AD extends AnyActorDefinition> {
5555

5656
/**
5757
* Creates a new actor with the name automatically injected from the property accessor,
58-
* and returns a stateless handle to it.
58+
* and returns a stateless handle to it with the actor ID resolved.
5959
*
6060
* @template AD The actor class that this handle is for.
6161
* @param {string | string[]} key - The key to identify the actor. Can be a single string or an array of strings.
6262
* @param {CreateOptions} [opts] - Options for creating the actor (excluding name and key).
63-
* @returns {ActorHandle<AD>} - A handle to the actor.
63+
* @returns {Promise<ActorHandle<AD>>} - A promise that resolves to a handle to the actor.
6464
*/
65-
create(key: string | string[], opts?: CreateOptions): ActorHandle<AD>;
65+
create(
66+
key: string | string[],
67+
opts?: CreateOptions,
68+
): Promise<ActorHandle<AD>>;
6669
}
6770

6871
/**
@@ -286,18 +289,19 @@ export class ClientRaw {
286289

287290
/**
288291
* Creates a new actor with the provided key and returns a stateless handle to it.
292+
* Resolves the actor ID and returns a handle with getForId query.
289293
*
290294
* @template AD The actor class that this handle is for.
291295
* @param {string} name - The name of the actor.
292296
* @param {string | string[]} key - The key to identify the actor. Can be a single string or an array of strings.
293297
* @param {CreateOptions} [opts] - Options for creating the actor (excluding name and key).
294-
* @returns {ActorHandle<AD>} - A handle to the actor.
298+
* @returns {Promise<ActorHandle<AD>>} - A promise that resolves to a handle to the actor.
295299
*/
296-
create<AD extends AnyActorDefinition>(
300+
async create<AD extends AnyActorDefinition>(
297301
name: string,
298302
key: string | string[],
299303
opts: CreateOptions = {},
300-
): ActorHandle<AD> {
304+
): Promise<ActorHandle<AD>> {
301305
// Convert string to array of strings
302306
const keyArray: string[] = typeof key === "string" ? [key] : key;
303307

@@ -316,17 +320,36 @@ export class ClientRaw {
316320
create,
317321
});
318322

319-
const actorQuery = {
323+
// Create the actor
324+
const createQuery = {
320325
create,
321-
};
326+
} satisfies ActorQuery;
327+
const actorId = await resolveActorId(
328+
this.#managerEndpoint,
329+
createQuery,
330+
this.#encodingKind,
331+
);
332+
logger().debug("created actor with ID", {
333+
name,
334+
key: keyArray,
335+
actorId,
336+
});
322337

323-
const managerEndpoint = this.#managerEndpoint;
338+
// Create handle with actor ID
339+
const getForIdQuery = {
340+
getForId: {
341+
actorId,
342+
},
343+
} satisfies ActorQuery;
324344
const handle = this.#createHandle(
325-
managerEndpoint,
345+
this.#managerEndpoint,
326346
opts?.params,
327-
actorQuery,
347+
getForIdQuery,
328348
);
329-
return createActorProxy(handle) as ActorHandle<AD>;
349+
350+
const proxy = createActorProxy(handle) as ActorHandle<AD>;
351+
352+
return proxy;
330353
}
331354

332355
#createHandle(
@@ -454,11 +477,11 @@ export function createClient<A extends ActorCoreApp<any>>(
454477
opts,
455478
);
456479
},
457-
create: (
480+
create: async (
458481
key: string | string[],
459482
opts: CreateOptions = {},
460-
): ActorHandle<ExtractActorsFromApp<A>[typeof prop]> => {
461-
return target.create<ExtractActorsFromApp<A>[typeof prop]>(
483+
): Promise<ActorHandle<ExtractActorsFromApp<A>[typeof prop]>> => {
484+
return await target.create<ExtractActorsFromApp<A>[typeof prop]>(
462485
prop,
463486
key,
464487
opts,
@@ -499,6 +522,9 @@ function createActorProxy<AD extends AnyActorDefinition>(
499522

500523
// Create RPC function that preserves 'this' context
501524
if (typeof prop === "string") {
525+
// If JS is attempting to calling this as a promise, ignore it
526+
if (prop === "then") return undefined;
527+
502528
let method = methodCache.get(prop);
503529
if (!method) {
504530
method = (...args: unknown[]) => target.action(prop, ...args);

0 commit comments

Comments
 (0)