Skip to content

Commit d38c91e

Browse files
committed
feat: add create actor input
1 parent f10a144 commit d38c91e

35 files changed

+456
-120
lines changed

packages/actor-core-cli/src/commands/deploy.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,43 @@ export const deploy = new Command()
336336
export default createActorHandler({ app });
337337
`,
338338
);
339+
//console.log('Hello world');
340+
//export default {
341+
// async start() {
342+
// console.log('Start');
343+
// await new Promise(resolve => {})
344+
// }
345+
//};
346+
// yield fs.writeFile(
347+
// entrypoint,
348+
// dedent`
349+
//console.log(Deno.env.toObject());
350+
//
351+
//export default {
352+
// async start(ctx) {
353+
// let server = Deno.serve({
354+
// handler,
355+
// port: 80,
356+
// });
357+
//
358+
// await server.finished;
359+
// },
360+
//};
361+
//
362+
//function handler(req) {
363+
// console.log("req", req);
364+
//
365+
// let url = new URL(req.url);
366+
//
367+
// if (url.pathname == "/exit") Deno.exit(parseInt(req.body));
368+
//
369+
// return new Response(req.body, {
370+
// status: 200,
371+
// headers: { "Content-Type": "application/json" },
372+
// });
373+
//}
374+
// `,
375+
// );
339376

340377
const buildTags = {
341378
role: "actor",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { actor, setup } from "actor-core";
2+
3+
interface State {
4+
initialInput?: unknown;
5+
onCreateInput?: unknown;
6+
}
7+
8+
// Test actor that can capture input during creation
9+
const inputActor = actor({
10+
createState: (c, { input }): State => {
11+
return {
12+
initialInput: input,
13+
onCreateInput: undefined,
14+
};
15+
},
16+
17+
onCreate: (c, { input }) => {
18+
c.state.onCreateInput = input;
19+
},
20+
21+
actions: {
22+
getInputs: (c) => {
23+
return {
24+
initialInput: c.state.initialInput,
25+
onCreateInput: c.state.onCreateInput,
26+
};
27+
},
28+
},
29+
});
30+
31+
export const app = setup({
32+
actors: { inputActor },
33+
});
34+
35+
export type App = typeof app;

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ export const ActorConfigSchema = z
7474
},
7575
);
7676

77+
export interface OnCreateOptions {
78+
input?: unknown;
79+
}
80+
81+
export interface CreateStateOptions {
82+
input?: unknown;
83+
}
84+
7785
export interface OnConnectOptions<CP> {
7886
/**
7987
* The request object associated with the connection.
@@ -92,6 +100,7 @@ type CreateState<S, CP, CS, V> =
92100
| {
93101
createState: (
94102
c: ActorContext<undefined, undefined, undefined, undefined>,
103+
opts: CreateStateOptions,
95104
) => S | Promise<S>;
96105
}
97106
| Record<never, never>;
@@ -149,7 +158,10 @@ interface BaseActorConfig<S, CP, CS, V, R extends Actions<S, CP, CS, V>> {
149158
* Use this hook to initialize your actor's state.
150159
* This is called before any other lifecycle hooks.
151160
*/
152-
onCreate?: (c: ActorContext<S, CP, CS, V>) => void | Promise<void>;
161+
onCreate?: (
162+
c: ActorContext<S, CP, CS, V>,
163+
opts: OnCreateOptions,
164+
) => void | Promise<void>;
153165

154166
/**
155167
* Called when the actor is started and ready to receive connections and action.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export interface ActorDriver {
99
//load(): Promise<LoadOutput>;
1010
getContext(actorId: string): unknown;
1111

12+
readInput(actorId: string): Promise<unknown | undefined>;
13+
1214
readPersistedData(actorId: string): Promise<unknown | undefined>;
1315
writePersistedData(actorId: string, unknown: unknown): Promise<void>;
1416

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,7 @@ export class ActorInstance<S, CP, CS, V> {
498498
} else {
499499
logger().info("actor creating");
500500

501-
if (this.#config.onCreate) {
502-
await this.#config.onCreate(this.actorContext);
503-
}
501+
const input = await this.#actorDriver.readInput(this.#actorId);
504502

505503
// Initialize actor state
506504
let stateData: unknown = undefined;
@@ -518,6 +516,7 @@ export class ActorInstance<S, CP, CS, V> {
518516
undefined,
519517
undefined
520518
>,
519+
{ input },
521520
);
522521
} else if ("state" in this.#config) {
523522
stateData = structuredClone(this.#config.state);
@@ -539,6 +538,11 @@ export class ActorInstance<S, CP, CS, V> {
539538
await this.#actorDriver.writePersistedData(this.#actorId, persist);
540539

541540
this.#setPersist(persist);
541+
542+
// Notify creation
543+
if (this.#config.onCreate) {
544+
await this.#config.onCreate(this.actorContext, { input });
545+
}
542546
}
543547
}
544548

@@ -846,7 +850,7 @@ export class ActorInstance<S, CP, CS, V> {
846850

847851
// Prevent calling private or reserved methods
848852
if (!(actionName in this.#config.actions)) {
849-
logger().warn("action does not exist", { actionName });
853+
logger().warn("action does not exist", { actionName });
850854
throw new errors.ActionNotFound();
851855
}
852856

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

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ export interface ActorAccessor<AD extends AnyActorDefinition> {
4141
* @param {GetOptions} [opts] - Options for getting the actor.
4242
* @returns {ActorHandle<AD>} - A handle to the actor.
4343
*/
44-
getOrCreate(key?: string | string[], opts?: GetOptions): ActorHandle<AD>;
44+
getOrCreate(
45+
key?: string | string[],
46+
opts?: GetOrCreateOptions,
47+
): ActorHandle<AD>;
4548

4649
/**
4750
* Gets a stateless handle to an actor by its ID.
@@ -63,7 +66,7 @@ export interface ActorAccessor<AD extends AnyActorDefinition> {
6366
* @returns {Promise<ActorHandle<AD>>} - A promise that resolves to a handle to the actor.
6467
*/
6568
create(
66-
key: string | string[],
69+
key?: string | string[],
6770
opts?: CreateOptions,
6871
): Promise<ActorHandle<AD>>;
6972
}
@@ -104,9 +107,11 @@ export interface GetOptions extends QueryOptions {}
104107
* @typedef {QueryOptions} GetOrCreateOptions
105108
* @property {string} [createInRegion] - Region to create the actor in if it doesn't exist.
106109
*/
107-
export interface GetOptions extends QueryOptions {
110+
export interface GetOrCreateOptions extends QueryOptions {
108111
/** Region to create the actor in if it doesn't exist. */
109112
createInRegion?: string;
113+
/** Input data to pass to the actor. */
114+
createWithInput?: unknown;
110115
}
111116

112117
/**
@@ -117,6 +122,8 @@ export interface GetOptions extends QueryOptions {
117122
export interface CreateOptions extends QueryOptions {
118123
/** The region to create the actor in. */
119124
region?: string;
125+
/** Input data to pass to the actor. */
126+
input?: unknown;
120127
}
121128

122129
/**
@@ -258,7 +265,7 @@ export class ClientRaw {
258265
getOrCreate<AD extends AnyActorDefinition>(
259266
name: string,
260267
key?: string | string[],
261-
opts?: GetOptions,
268+
opts?: GetOrCreateOptions,
262269
): ActorHandle<AD> {
263270
// Convert string to array of strings
264271
const keyArray: string[] = typeof key === "string" ? [key] : key || [];
@@ -274,6 +281,7 @@ export class ClientRaw {
274281
getOrCreateForKey: {
275282
name,
276283
key: keyArray,
284+
input: opts?.createWithInput,
277285
region: opts?.createInRegion,
278286
},
279287
};
@@ -299,31 +307,29 @@ export class ClientRaw {
299307
*/
300308
async create<AD extends AnyActorDefinition>(
301309
name: string,
302-
key: string | string[],
303-
opts: CreateOptions = {},
310+
key?: string | string[],
311+
opts?: CreateOptions,
304312
): Promise<ActorHandle<AD>> {
305313
// Convert string to array of strings
306-
const keyArray: string[] = typeof key === "string" ? [key] : key;
314+
const keyArray: string[] = typeof key === "string" ? [key] : key || [];
307315

308-
// Build create config
309-
const create = {
310-
...opts,
311-
// Do these last to override `opts`
312-
name,
313-
key: keyArray,
314-
};
316+
const createQuery = {
317+
create: {
318+
...opts,
319+
// Do these last to override `opts`
320+
name,
321+
key: keyArray,
322+
},
323+
} satisfies ActorQuery;
315324

316325
logger().debug("create actor handle", {
317326
name,
318327
key: keyArray,
319328
parameters: opts?.params,
320-
create,
329+
create: createQuery.create,
321330
});
322331

323332
// Create the actor
324-
const createQuery = {
325-
create,
326-
} satisfies ActorQuery;
327333
const actorId = await resolveActorId(
328334
this.#managerEndpoint,
329335
createQuery,

packages/actor-core/src/driver-test-suite/test-apps.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type { App as VarsApp } from "../../fixtures/driver-test-suite/vars";
1010
export type { App as ConnStateApp } from "../../fixtures/driver-test-suite/conn-state";
1111
export type { App as MetadataApp } from "../../fixtures/driver-test-suite/metadata";
1212
export type { App as ErrorHandlingApp } from "../../fixtures/driver-test-suite/error-handling";
13+
export type { App as ActionInputsApp } from "../../fixtures/driver-test-suite/action-inputs";
1314

1415
export const COUNTER_APP_PATH = resolve(
1516
__dirname,
@@ -50,4 +51,8 @@ export const METADATA_APP_PATH = resolve(
5051
export const ERROR_HANDLING_APP_PATH = resolve(
5152
__dirname,
5253
"../../fixtures/driver-test-suite/error-handling.ts",
54+
);
55+
export const ACTION_INPUTS_APP_PATH = resolve(
56+
__dirname,
57+
"../../fixtures/driver-test-suite/action-inputs.ts",
5358
);

packages/actor-core/src/driver-test-suite/tests/manager-driver.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { describe, test, expect, vi } from "vitest";
22
import { setupDriverTest } from "../utils";
33
import { ActorError } from "@/client/mod";
4-
import { COUNTER_APP_PATH, type CounterApp } from "../test-apps";
4+
import {
5+
COUNTER_APP_PATH,
6+
ACTION_INPUTS_APP_PATH,
7+
type CounterApp,
8+
type ActionInputsApp,
9+
} from "../test-apps";
510
import { DriverTestConfig } from "../mod";
611

712
export function runManagerDriverTests(driverTestConfig: DriverTestConfig) {
@@ -135,6 +140,96 @@ export function runManagerDriverTests(driverTestConfig: DriverTestConfig) {
135140
expect(count).toBe(10);
136141
});
137142

143+
test("passes input to actor during creation", async (c) => {
144+
const { client } = await setupDriverTest<ActionInputsApp>(
145+
c,
146+
driverTestConfig,
147+
ACTION_INPUTS_APP_PATH,
148+
);
149+
150+
// Test data to pass as input
151+
const testInput = {
152+
name: "test-actor",
153+
value: 42,
154+
nested: { foo: "bar" },
155+
};
156+
157+
// Create actor with input
158+
const actor = await client.inputActor.create(undefined, {
159+
input: testInput,
160+
});
161+
162+
// Verify both createState and onCreate received the input
163+
const inputs = await actor.getInputs();
164+
165+
// Input should be available in createState
166+
expect(inputs.initialInput).toEqual(testInput);
167+
168+
// Input should also be available in onCreate lifecycle hook
169+
expect(inputs.onCreateInput).toEqual(testInput);
170+
});
171+
172+
test("input is undefined when not provided", async (c) => {
173+
const { client } = await setupDriverTest<ActionInputsApp>(
174+
c,
175+
driverTestConfig,
176+
ACTION_INPUTS_APP_PATH,
177+
);
178+
179+
// Create actor without providing input
180+
const actor = await client.inputActor.create();
181+
182+
// Get inputs and verify they're undefined
183+
const inputs = await actor.getInputs();
184+
185+
// Should be undefined in createState
186+
expect(inputs.initialInput).toBeUndefined();
187+
188+
// Should be undefined in onCreate lifecycle hook too
189+
expect(inputs.onCreateInput).toBeUndefined();
190+
});
191+
192+
test("getOrCreate passes input to actor during creation", async (c) => {
193+
const { client } = await setupDriverTest<ActionInputsApp>(
194+
c,
195+
driverTestConfig,
196+
ACTION_INPUTS_APP_PATH,
197+
);
198+
199+
// Create a unique key for this test
200+
const uniqueKey = [`input-test-${crypto.randomUUID()}`];
201+
202+
// Test data to pass as input
203+
const testInput = {
204+
name: "getorcreate-test",
205+
value: 100,
206+
nested: { baz: "qux" },
207+
};
208+
209+
// Use getOrCreate with input
210+
const actor = client.inputActor.getOrCreate(uniqueKey, {
211+
createWithInput: testInput,
212+
});
213+
214+
// Verify both createState and onCreate received the input
215+
const inputs = await actor.getInputs();
216+
217+
// Input should be available in createState
218+
expect(inputs.initialInput).toEqual(testInput);
219+
220+
// Input should also be available in onCreate lifecycle hook
221+
expect(inputs.onCreateInput).toEqual(testInput);
222+
223+
// Verify that calling getOrCreate again with the same key
224+
// returns the existing actor and doesn't create a new one
225+
const existingActor = client.inputActor.getOrCreate(uniqueKey);
226+
const existingInputs = await existingActor.getInputs();
227+
228+
// Should still have the original inputs
229+
expect(existingInputs.initialInput).toEqual(testInput);
230+
expect(existingInputs.onCreateInput).toEqual(testInput);
231+
});
232+
138233
// TODO: Correctly test region for each provider
139234
//test("creates and retrieves actors with region", async (c) => {
140235
// const { client } = await setupDriverTest<CounterApp>(c,

0 commit comments

Comments
 (0)