diff --git a/src/actors/ActorId.ts b/src/actors/ActorId.ts index 4fadc8c0..0e62c02e 100644 --- a/src/actors/ActorId.ts +++ b/src/actors/ActorId.ts @@ -17,6 +17,9 @@ export default class ActorId { private readonly id: string; constructor(id: string) { + if (!id) { + throw new Error("ActorId cannot be empty"); + } this.id = id; } @@ -28,6 +31,10 @@ export default class ActorId { return this.id; } + getURLSafeId() { + return encodeURIComponent(this.id); + } + toString() { return this.id; } diff --git a/src/actors/client/ActorClient/ActorClientHTTP.ts b/src/actors/client/ActorClient/ActorClientHTTP.ts index 6cdbcaf6..a46d2b9a 100644 --- a/src/actors/client/ActorClient/ActorClientHTTP.ts +++ b/src/actors/client/ActorClient/ActorClientHTTP.ts @@ -28,7 +28,7 @@ export default class ActorClientHTTP implements IClientActor { } async invoke(actorType: string, actorId: ActorId, methodName: string, body?: any): Promise { - const result = await this.client.execute(`/actors/${actorType}/${actorId.getId()}/method/${methodName}`, { + const result = await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/method/${methodName}`, { method: "POST", // we always use POST calls for Invoking (ref: https://github.com/dapr/js-sdk/pull/137#discussion_r772636068) body, }); @@ -37,7 +37,7 @@ export default class ActorClientHTTP implements IClientActor { } async stateTransaction(actorType: string, actorId: ActorId, operations: OperationType[]): Promise { - await this.client.execute(`/actors/${actorType}/${actorId.getId()}/state`, { + await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/state`, { method: "POST", headers: { "Content-Type": "application/json", @@ -47,7 +47,7 @@ export default class ActorClientHTTP implements IClientActor { } async stateGet(actorType: string, actorId: ActorId, key: string): Promise { - const result = await this.client.execute(`/actors/${actorType}/${actorId.getId()}/state/${key}`); + const result = await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/state/${key}`); return result as any; } @@ -57,7 +57,7 @@ export default class ActorClientHTTP implements IClientActor { name: string, reminder: ActorReminderType, ): Promise { - await this.client.execute(`/actors/${actorType}/${actorId.getId()}/reminders/${name}`, { + await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/reminders/${name}`, { method: "POST", headers: { "Content-Type": "application/json", @@ -72,18 +72,18 @@ export default class ActorClientHTTP implements IClientActor { } async reminderGet(actorType: string, actorId: ActorId, name: string): Promise { - const result = await this.client.execute(`/actors/${actorType}/${actorId.getId()}/reminders/${name}`); + const result = await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/reminders/${name}`); return result as object; } async unregisterActorReminder(actorType: string, actorId: ActorId, name: string): Promise { - await this.client.execute(`/actors/${actorType}/${actorId.getId()}/reminders/${name}`, { + await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/reminders/${name}`, { method: "DELETE", }); } async registerActorTimer(actorType: string, actorId: ActorId, name: string, timer: ActorTimerType): Promise { - await this.client.execute(`/actors/${actorType}/${actorId.getId()}/timers/${name}`, { + await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/timers/${name}`, { method: "POST", headers: { "Content-Type": "application/json", @@ -99,13 +99,13 @@ export default class ActorClientHTTP implements IClientActor { } async unregisterActorTimer(actorType: string, actorId: ActorId, name: string): Promise { - await this.client.execute(`/actors/${actorType}/${actorId.getId()}/timers/${name}`, { + await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}/timers/${name}`, { method: "DELETE", }); } - async deactivate(actorType: string, actorId: string): Promise { - await this.client.execute(`/actors/${actorType}/${actorId}`, { + async deactivate(actorType: string, actorId: ActorId): Promise { + await this.client.execute(`/actors/${actorType}/${actorId.getURLSafeId()}`, { method: "DELETE", }); } diff --git a/test/e2e/http/actors.test.ts b/test/e2e/http/actors.test.ts index f6c56b66..df360801 100644 --- a/test/e2e/http/actors.test.ts +++ b/test/e2e/http/actors.test.ts @@ -113,7 +113,7 @@ describe("http/actors", () => { const config = JSON.parse(await res.text()); - expect(config.entities.length).toBe(9); + expect(config.entities.length).toBe(11); expect(config.actorIdleTimeout).toBe("1h"); expect(config.actorScanInterval).toBe("30s"); expect(config.drainOngoingCallTimeout).toBe("1m"); @@ -123,6 +123,26 @@ describe("http/actors", () => { }); }); + describe("actorId", () => { + it("should be able to create an actorId", () => { + const actorId = ActorId.createRandomId(); + expect(actorId.getId()).toBeDefined(); + expect(actorId.getURLSafeId()).toBeDefined(); + expect(actorId.toString()).toBeDefined(); + }); + + it("should not be able to create an actorId with an empty string", () => { + expect(() => new ActorId("")).toThrowError("ActorId cannot be empty"); + }); + + it("should be able to create an actorId with url unsafe characters like '/'", () => { + const actorId = new ActorId("test/actor"); + expect(actorId.getURLSafeId()).toEqual("test%2Factor"); + expect(actorId.getId()).toEqual("test/actor"); + expect(actorId.toString()).toEqual("test/actor"); + }); + }); + describe("actorProxy", () => { it("should be able to create an actor object through the proxy", async () => { const builder = new ActorProxyBuilder(DemoActorCounterImpl, client); @@ -179,15 +199,19 @@ describe("http/actors", () => { it("should register actors correctly", async () => { const actors = await server.actor.getRegisteredActors(); - expect(actors.length).toEqual(9); + expect(actors.length).toEqual(11); expect(actors).toContain(DemoActorCounterImpl.name); expect(actors).toContain(DemoActorSayImpl.name); expect(actors).toContain(DemoActorReminderImpl.name); + expect(actors).toContain(DemoActorReminder2Impl.name); + expect(actors).toContain(DemoActorReminderOnceImpl.name); expect(actors).toContain(DemoActorTimerImpl.name); + expect(actors).toContain(DemoActorTimerOnceImpl.name); expect(actors).toContain(DemoActorActivateImpl.name); expect(actors).toContain(DemoActorTimerTtlImpl.name); expect(actors).toContain(DemoActorReminderTtlImpl.name); + expect(actors).toContain(DemoActorDeleteStateImpl.name); }); it("should be able to invoke an actor through a text message", async () => {