From 43a5e4e6f462ef4c5e95a23fa71662e7cc5ee283 Mon Sep 17 00:00:00 2001
From: Gregor Martynus <39992+gr2m@users.noreply.github.com>
Date: Thu, 5 Sep 2024 11:33:58 -0700
Subject: [PATCH] feat: `create*Event` methods now return strings (#59)

BREAKING CHANGE: All `create*Event` methods now return a string instead
of an object with a `.toString()` method.

Before:

```js
esponse.write(createTextEvent("Hello, world!").toString());
```

Now:

```js
esponse.write(createTextEvent("Hello, world!"));
```
---
 README.md                      |  20 +++---
 index.d.ts                     |  83 ++-----------------------
 index.test-d.ts                | 108 +++------------------------------
 lib/response.js                |  94 +++++++++++-----------------
 test/response.test.js          |  25 ++------
 test/response.test.js.snapshot |  95 -----------------------------
 6 files changed, 64 insertions(+), 361 deletions(-)

diff --git a/README.md b/README.md
index cf51484..094fa4f 100644
--- a/README.md
+++ b/README.md
@@ -46,9 +46,9 @@ export default handler(request, response) {
   const textEvent = createTextEvent("Hello, world!");
   const doneEvent = createDoneEvent();
 
-  response.write(ackEvent.toString());
-  response.write(textEvent.toString());
-  response.end(doneEvent.toString());
+  response.write(ackEvent);
+  response.write(textEvent);
+  response.end(doneEvent);
 }
 ```
 
@@ -131,7 +131,7 @@ const payloadIsVerified = await verifyRequestPayload(
 
 ### Response
 
-All `create*Event()` methods return an object with a `.toString()` method, which is called automatically when a string is expected. Unfortunately that's not the case for `response.write()`, you need to call `.toString()` explicitly.
+All `create*Event()` methods return a string that can directly be written to the response stream.
 
 #### `createAckEvent()`
 
@@ -141,7 +141,7 @@ The `ack` event should only be sent once.
 ```js
 import { createAckEvent } from "@copilot-extensions/preview-sdk";
 
-response.write(createAckEvent().toString());
+response.write(createAckEvent());
 ```
 
 #### `createTextEvent(message)`
@@ -151,8 +151,8 @@ Send a text message to the chat UI. Multiple messages can be sent. The `message`
 ```js
 import { createTextEvent } from "@copilot-extensions/preview-sdk";
 
-response.write(createTextEvent("Hello, world!").toString());
-response.write(createTextEvent("Hello, again!").toString());
+response.write(createTextEvent("Hello, world!"));
+response.write(createTextEvent("Hello, again!"));
 ```
 
 #### `createConfirmationEvent({ id, title, message, metadata })`
@@ -171,7 +171,7 @@ response.write(
     id: "123",
     title: "Are you sure?",
     message: "This will do something.",
-  }).toString(),
+  }),
 );
 ```
 
@@ -209,7 +209,7 @@ response.write(
         display_icon: "issue-opened",
         display_url: "https://github.com/monalisa/hello-world/issues/123",
     },
-  ]).toString()
+  ])
 );
 ```
 
@@ -231,7 +231,7 @@ The `done` event should only be sent once, at the end of the response. No furthe
 ```js
 import { createDoneEvent } from "@copilot-extensions/preview-sdk";
 
-response.write(createDoneEvent().toString());
+response.write(createDoneEvent());
 ```
 
 ### Parsing
diff --git a/index.d.ts b/index.d.ts
index 22b8870..24ae984 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -33,11 +33,10 @@ interface VerifyRequestByKeyIdInterface {
 // response types
 
 export interface CreateAckEventInterface {
-  (): ResponseEvent<"ack">;
+  (): string;
 }
-
 export interface CreateTextEventInterface {
-  (message: string): ResponseEvent<"text">;
+  (message: string): string;
 }
 
 export type CreateConfirmationEventOptions = {
@@ -50,88 +49,18 @@ export type CreateConfirmationEventOptions = {
 export interface CreateConfirmationEventInterface {
   (
     options: CreateConfirmationEventOptions,
-  ): ResponseEvent<"copilot_confirmation">;
+  ): string;
 }
 export interface CreateReferencesEventInterface {
-  (references: CopilotReference[]): ResponseEvent<"copilot_references">;
+  (references: CopilotReference[]): string;
 }
 export interface CreateErrorsEventInterface {
-  (errors: CopilotError[]): ResponseEvent<"copilot_errors">;
+  (errors: CopilotError[]): string;
 }
 export interface CreateDoneEventInterface {
-  (): ResponseEvent<"done">;
+  (): string;
 }
 
-type ResponseEventType =
-  | "ack"
-  | "done"
-  | "text"
-  | "copilot_references"
-  | "copilot_confirmation"
-  | "copilot_errors";
-type EventsWithoutEventKey = "ack" | "done" | "text";
-type ResponseEvent<T extends ResponseEventType = "text"> =
-  T extends EventsWithoutEventKey
-    ? {
-        data: T extends "ack"
-          ? CopilotAckResponseEventData
-          : T extends "done"
-            ? CopilotDoneResponseEventData
-            : T extends "text"
-              ? CopilotTextResponseEventData
-              : never;
-        toString: () => string;
-      }
-    : {
-        event: T;
-        data: T extends "copilot_references"
-          ? CopilotReferenceResponseEventData
-          : T extends "copilot_confirmation"
-            ? CopilotConfirmationResponseEventData
-            : T extends "copilot_errors"
-              ? CopilotErrorsResponseEventData
-              : never;
-        toString: () => string;
-      };
-
-type CopilotAckResponseEventData = {
-  choices: [
-    {
-      delta: InteropMessage<"assistant">;
-    },
-  ];
-};
-
-type CopilotDoneResponseEventData = {
-  choices: [
-    {
-      finish_reason: "stop";
-      delta: {
-        content: null;
-      };
-    },
-  ];
-};
-
-type CopilotTextResponseEventData = {
-  choices: [
-    {
-      delta: InteropMessage<"assistant">;
-    },
-  ];
-};
-type CopilotConfirmationResponseEventData = {
-  type: "action";
-  title: string;
-  message: string;
-  confirmation?: {
-    id: string;
-    [key: string]: any;
-  };
-};
-type CopilotErrorsResponseEventData = CopilotError[];
-type CopilotReferenceResponseEventData = CopilotReference[];
-
 type CopilotError = {
   type: "reference" | "function" | "agent";
   code: string;
diff --git a/index.test-d.ts b/index.test-d.ts
index 844b63a..0a41a4d 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -87,36 +87,12 @@ export async function fetchVerificationKeysTest() {
 
 export function createAckEventTest() {
   const event = createAckEvent();
-  expectType<() => string>(event.toString);
-  expectType<string>(event.toString());
-
-  expectType<{
-    choices: [
-      {
-        delta: InteropMessage<"assistant">;
-      },
-    ];
-  }>(event.data);
-
-  // @ts-expect-error - .event is required
-  event.event;
+  expectType<string>(event);
 }
 
 export function createTextEventTest() {
   const event = createTextEvent("test");
-  expectType<() => string>(event.toString);
-  expectType<string>(event.toString());
-
-  expectType<{
-    choices: [
-      {
-        delta: InteropMessage<"assistant">;
-      },
-    ];
-  }>(event.data);
-
-  // @ts-expect-error - .event is required
-  event.event;
+  expectType<string>(event);
 }
 
 export function createConfirmationEventTest() {
@@ -125,31 +101,7 @@ export function createConfirmationEventTest() {
     title: "test",
     message: "test",
   });
-
-  // optional metadata
-  createConfirmationEvent({
-    id: "test",
-    title: "test",
-    message: "test",
-    metadata: {
-      someOtherId: "test",
-    },
-  });
-
-  expectType<() => string>(event.toString);
-  expectType<string>(event.toString());
-
-  expectType<{
-    type: "action";
-    title: string;
-    message: string;
-    confirmation?: {
-      id: string;
-      [key: string]: any;
-    };
-  }>(event.data);
-
-  expectType<"copilot_confirmation">(event.event);
+  expectType<string>(event);
 }
 
 export function createReferencesEventTest() {
@@ -172,26 +124,7 @@ export function createReferencesEventTest() {
       },
     },
   ]);
-  expectType<() => string>(event.toString);
-  expectType<string>(event.toString());
-
-  expectType<
-    {
-      type: string;
-      id: string;
-      data?: {
-        [key: string]: unknown;
-      };
-      is_implicit?: boolean;
-      metadata?: {
-        display_name: string;
-        display_icon?: string;
-        display_url?: string;
-      };
-    }[]
-  >(event.data);
-
-  expectType<"copilot_references">(event.event);
+  expectType<string>(event);
 }
 
 export function createErrorsEventTest() {
@@ -215,39 +148,12 @@ export function createErrorsEventTest() {
       identifier: "agent-identifier",
     },
   ]);
-  expectType<() => string>(event.toString);
-  expectType<string>(event.toString());
-
-  expectType<
-    {
-      type: "reference" | "function" | "agent";
-      code: string;
-      message: string;
-      identifier: string;
-    }[]
-  >(event.data);
-
-  expectType<"copilot_errors">(event.event);
+  expectType<string>(event);
 }
 
 export function createDoneEventTest() {
   const event = createDoneEvent();
-  expectType<() => string>(event.toString);
-  expectType<string>(event.toString());
-
-  expectType<{
-    choices: [
-      {
-        finish_reason: "stop";
-        delta: {
-          content: null;
-        };
-      },
-    ];
-  }>(event.data);
-
-  // @ts-expect-error - .event is required
-  event.event;
+  expectType<string>(event);
 }
 
 export function parseRequestBodyTest(body: string) {
@@ -312,7 +218,7 @@ export async function promptTest() {
   await prompt("What is the capital of France?", {
     token: "secret",
     request: {
-      fetch: () => {},
+      fetch: () => { },
     },
   });
 
diff --git a/lib/response.js b/lib/response.js
index d1344b3..caf226d 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -2,87 +2,63 @@
 
 /** @type {import('..').CreateAckEventInterface} */
 export function createAckEvent() {
-  return {
-    data: {
-      choices: [
-        {
-          delta: { content: ``, role: "assistant" },
-        },
-      ],
-    },
-    toString() {
-      return `data: ${JSON.stringify(this.data)}\n\n`;
-    },
+  const data = {
+    choices: [
+      {
+        delta: { content: ``, role: "assistant" },
+      },
+    ],
   };
+  return `data: ${JSON.stringify(data)}\n\n`;
 }
 
 /** @type {import('..').CreateTextEventInterface} */
 export function createTextEvent(message) {
-  return {
-    data: {
-      choices: [
-        {
-          delta: { content: message, role: "assistant" },
-        },
-      ],
-    },
-    toString() {
-      return `data: ${JSON.stringify(this.data)}\n\n`;
-    },
+  const data = {
+    choices: [
+      {
+        delta: { content: message, role: "assistant" },
+      },
+    ],
   };
+  return `data: ${JSON.stringify(data)}\n\n`;
 }
 
 /** @type {import('..').CreateConfirmationEventInterface} */
 export function createConfirmationEvent({ id, title, message, metadata }) {
-  return {
-    event: "copilot_confirmation",
-    data: {
-      type: "action",
-      title,
-      message,
-      confirmation: { id, ...metadata },
-    },
-    toString() {
-      return `event: ${this.event}\ndata: ${JSON.stringify(this.data)}\n\n`;
-    },
+  const event = "copilot_confirmation";
+  const data = {
+    type: "action",
+    title,
+    message,
+    confirmation: { id, ...metadata },
   };
+  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
 }
 
 /** @type {import('..').CreateReferencesEventInterface} */
 export function createReferencesEvent(references) {
-  return {
-    event: "copilot_references",
-    data: references,
-    toString() {
-      return `event: ${this.event}\ndata: ${JSON.stringify(this.data)}\n\n`;
-    },
-  };
+  const event = "copilot_references";
+  const data = references;
+  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
 }
 
 /** @type {import('..').CreateErrorsEventInterface} */
 export function createErrorsEvent(errors) {
-  return {
-    event: "copilot_errors",
-    data: errors,
-    toString() {
-      return `event: ${this.event}\ndata: ${JSON.stringify(this.data)}\n\n`;
-    },
-  };
+  const event = "copilot_errors";
+  const data = errors;
+  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
 }
 
 /** @type {import('..').CreateDoneEventInterface} */
 export function createDoneEvent() {
-  return {
-    data: {
-      choices: [
-        {
-          finish_reason: "stop",
-          delta: { content: null },
-        },
-      ],
-    },
-    toString() {
-      return `data: ${JSON.stringify(this.data)}\n\ndata: [DONE]\n\n`;
-    },
+  const data = {
+    choices: [
+      {
+        finish_reason: "stop",
+        delta: { content: null },
+      },
+    ],
   };
+  return `data: ${JSON.stringify(data)}\n\ndata: [DONE]\n\n`;
 }
diff --git a/test/response.test.js b/test/response.test.js
index cd17199..b08ff00 100644
--- a/test/response.test.js
+++ b/test/response.test.js
@@ -16,23 +16,17 @@ suite("response", () => {
 
   test("createAckEvent()", (t) => {
     const event = createAckEvent();
-    t.assert.equal(undefined, event.event);
-    t.assert.snapshot(event.data);
-    t.assert.snapshot(event.toString());
+    t.assert.snapshot(event);
   });
 
   test("createDoneEvent()", (t) => {
     const event = createDoneEvent();
-    t.assert.equal(undefined, event.event);
-    t.assert.snapshot(event.data);
-    t.assert.snapshot(event.toString());
+    t.assert.snapshot(event);
   });
 
   test("createTextEvent()", (t) => {
     const event = createTextEvent("test");
-    t.assert.equal(undefined, event.event);
-    t.assert.snapshot(event.data);
-    t.assert.snapshot(event.toString());
+    t.assert.snapshot(event);
   });
 
   test("createConfirmationEvent()", (t) => {
@@ -42,9 +36,7 @@ suite("response", () => {
       message: "message",
       metadata: { foo: "bar" },
     });
-    t.assert.equal("copilot_confirmation", event.event);
-    t.assert.snapshot(event.data);
-    t.assert.snapshot(event.toString());
+    t.assert.snapshot(event);
   });
 
   test("createErrorsEvent()", (t) => {
@@ -71,9 +63,7 @@ suite("response", () => {
       functionError,
       agentError,
     ]);
-    t.assert.equal("copilot_errors", event.event);
-    t.assert.snapshot(event.data);
-    t.assert.snapshot(event.toString());
+    t.assert.snapshot(event);
   });
 
   test("createReferencesEvent()", (t) => {
@@ -96,9 +86,6 @@ suite("response", () => {
         },
       },
     ]);
-
-    t.assert.equal("copilot_references", event.event);
-    t.assert.snapshot(event.data);
-    t.assert.snapshot(event.toString());
+    t.assert.snapshot(event);
   });
 });
diff --git a/test/response.test.js.snapshot b/test/response.test.js.snapshot
index a5db3d5..ef81396 100644
--- a/test/response.test.js.snapshot
+++ b/test/response.test.js.snapshot
@@ -1,118 +1,23 @@
 exports[`response > createAckEvent() 1`] = `
-{
-  "choices": [
-    {
-      "delta": {
-        "content": "",
-        "role": "assistant"
-      }
-    }
-  ]
-}
-`;
-
-exports[`response > createAckEvent() 2`] = `
 "data: {\\"choices\\":[{\\"delta\\":{\\"content\\":\\"\\",\\"role\\":\\"assistant\\"}}]}\\n\\n"
 `;
 
 exports[`response > createConfirmationEvent() 1`] = `
-{
-  "type": "action",
-  "title": "title",
-  "message": "message",
-  "confirmation": {
-    "id": "123",
-    "foo": "bar"
-  }
-}
-`;
-
-exports[`response > createConfirmationEvent() 2`] = `
 "event: copilot_confirmation\\ndata: {\\"type\\":\\"action\\",\\"title\\":\\"title\\",\\"message\\":\\"message\\",\\"confirmation\\":{\\"id\\":\\"123\\",\\"foo\\":\\"bar\\"}}\\n\\n"
 `;
 
 exports[`response > createDoneEvent() 1`] = `
-{
-  "choices": [
-    {
-      "finish_reason": "stop",
-      "delta": {
-        "content": null
-      }
-    }
-  ]
-}
-`;
-
-exports[`response > createDoneEvent() 2`] = `
 "data: {\\"choices\\":[{\\"finish_reason\\":\\"stop\\",\\"delta\\":{\\"content\\":null}}]}\\n\\ndata: [DONE]\\n\\n"
 `;
 
 exports[`response > createErrorsEvent() 1`] = `
-[
-  {
-    "type": "reference",
-    "code": "1",
-    "message": "test reference error",
-    "identifier": "reference-identifier"
-  },
-  {
-    "type": "function",
-    "code": "1",
-    "message": "test function error",
-    "identifier": "function-identifier"
-  },
-  {
-    "type": "agent",
-    "code": "1",
-    "message": "test agent error",
-    "identifier": "agent-identifier"
-  }
-]
-`;
-
-exports[`response > createErrorsEvent() 2`] = `
 "event: copilot_errors\\ndata: [{\\"type\\":\\"reference\\",\\"code\\":\\"1\\",\\"message\\":\\"test reference error\\",\\"identifier\\":\\"reference-identifier\\"},{\\"type\\":\\"function\\",\\"code\\":\\"1\\",\\"message\\":\\"test function error\\",\\"identifier\\":\\"function-identifier\\"},{\\"type\\":\\"agent\\",\\"code\\":\\"1\\",\\"message\\":\\"test agent error\\",\\"identifier\\":\\"agent-identifier\\"}]\\n\\n"
 `;
 
 exports[`response > createReferencesEvent() 1`] = `
-[
-  {
-    "type": "test.story",
-    "id": "test",
-    "data": {
-      "file": "test.js",
-      "start": "1",
-      "end": "42",
-      "content": "function test() {...}"
-    },
-    "is_implicit": false,
-    "metadata": {
-      "display_name": "Lines 1-42 from test.js",
-      "display_icon": "test-icon",
-      "display_url": "http://github.com/monalisa/hello-world/blob/main/test.js#L1-L42"
-    }
-  }
-]
-`;
-
-exports[`response > createReferencesEvent() 2`] = `
 "event: copilot_references\\ndata: [{\\"type\\":\\"test.story\\",\\"id\\":\\"test\\",\\"data\\":{\\"file\\":\\"test.js\\",\\"start\\":\\"1\\",\\"end\\":\\"42\\",\\"content\\":\\"function test() {...}\\"},\\"is_implicit\\":false,\\"metadata\\":{\\"display_name\\":\\"Lines 1-42 from test.js\\",\\"display_icon\\":\\"test-icon\\",\\"display_url\\":\\"http://github.com/monalisa/hello-world/blob/main/test.js#L1-L42\\"}}]\\n\\n"
 `;
 
 exports[`response > createTextEvent() 1`] = `
-{
-  "choices": [
-    {
-      "delta": {
-        "content": "test",
-        "role": "assistant"
-      }
-    }
-  ]
-}
-`;
-
-exports[`response > createTextEvent() 2`] = `
 "data: {\\"choices\\":[{\\"delta\\":{\\"content\\":\\"test\\",\\"role\\":\\"assistant\\"}}]}\\n\\n"
 `;