Dax Raad 9 months ago
parent
commit
42c7880858
4 changed files with 100 additions and 97 deletions
  1. 73 83
      app/packages/function/src/api.ts
  2. 1 3
      js/src/index.ts
  3. 22 8
      js/src/session/session.ts
  4. 4 3
      js/src/share/share.ts

+ 73 - 83
app/packages/function/src/api.ts

@@ -7,7 +7,10 @@ type Bindings = {
 }
 }
 
 
 export class SyncServer extends DurableObject {
 export class SyncServer extends DurableObject {
-  async fetch(req: Request) {
+  constructor(ctx: DurableObjectState, env: Bindings) {
+    super(ctx, env)
+  }
+  async fetch() {
     console.log("SyncServer subscribe")
     console.log("SyncServer subscribe")
 
 
     const webSocketPair = new WebSocketPair()
     const webSocketPair = new WebSocketPair()
@@ -16,11 +19,12 @@ export class SyncServer extends DurableObject {
     this.ctx.acceptWebSocket(server)
     this.ctx.acceptWebSocket(server)
 
 
     setTimeout(async () => {
     setTimeout(async () => {
-      const data = await this.ctx.storage.list()
-      data.forEach((content: any, key) => {
-        if (key === "shareID") return
-        server.send(JSON.stringify({ key, content: content }))
+      const data = await this.ctx.storage.list({
+        prefix: "data/",
       })
       })
+      for (const [key, content] of Object.entries(data)) {
+        server.send(JSON.stringify({ key, content }))
+      }
     }, 0)
     }, 0)
 
 
     return new Response(null, {
     return new Response(null, {
@@ -35,25 +39,54 @@ export class SyncServer extends DurableObject {
     ws.close(code, "Durable Object is closing WebSocket")
     ws.close(code, "Durable Object is closing WebSocket")
   }
   }
 
 
-  async publish(key: string, content: any) {
-    await this.ctx.storage.put(key, content)
-
+  async publish(secret: string, key: string, content: any) {
+    if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
+    const sessionID = await this.getSessionID()
+    if (
+      !key.startsWith(`session/info/${sessionID}`) &&
+      !key.startsWith(`session/message/${sessionID}/`)
+    )
+      return new Response("Error: Invalid key", { status: 400 })
+
+    // store message
+    await Resource.Bucket.put(`${key}.json`, JSON.stringify(content))
+    await this.ctx.storage.put("data/" + key, content)
     const clients = this.ctx.getWebSockets()
     const clients = this.ctx.getWebSockets()
     console.log("SyncServer publish", key, "to", clients.length, "subscribers")
     console.log("SyncServer publish", key, "to", clients.length, "subscribers")
     clients.forEach((client) => client.send(JSON.stringify({ key, content })))
     clients.forEach((client) => client.send(JSON.stringify({ key, content })))
   }
   }
 
 
-  async setShareID(shareID: string) {
-    await this.ctx.storage.put("shareID", shareID)
+  public async share(sessionID: string) {
+    let secret = await this.getSecret()
+    if (secret) return secret
+    secret = randomUUID()
+
+    await this.ctx.storage.put("secret", secret)
+    await this.ctx.storage.put("sessionID", sessionID)
+
+    return secret
+  }
+
+  private async getSecret() {
+    return this.ctx.storage.get<string>("secret")
   }
   }
 
 
-  async getShareID() {
-    return this.ctx.storage.get<string>("shareID")
+  private async getSessionID() {
+    return this.ctx.storage.get<string>("sessionID")
   }
   }
 
 
-  async clear() {
+  async clear(secret: string) {
+    await this.assertSecret(secret)
     await this.ctx.storage.deleteAll()
     await this.ctx.storage.deleteAll()
   }
   }
+
+  private async assertSecret(secret: string) {
+    if (secret !== (await this.getSecret())) throw new Error("Invalid secret")
+  }
+
+  static shortName(id: string) {
+    return id.substring(id.length - 8)
+  }
 }
 }
 
 
 export default {
 export default {
@@ -71,103 +104,60 @@ export default {
     if (request.method === "POST" && method === "share_create") {
     if (request.method === "POST" && method === "share_create") {
       const body = await request.json<any>()
       const body = await request.json<any>()
       const sessionID = body.sessionID
       const sessionID = body.sessionID
-
-      // Get existing shareID
-      const id = env.SYNC_SERVER.idFromName(sessionID)
+      const short = SyncServer.shortName(sessionID)
+      const id = env.SYNC_SERVER.idFromName(short)
       const stub = env.SYNC_SERVER.get(id)
       const stub = env.SYNC_SERVER.get(id)
-      if (await stub.getShareID())
-        return new Response("Error: Session already shared", { status: 400 })
-
-      const shareID = randomUUID()
-      await stub.setShareID(shareID)
-
-      return new Response(JSON.stringify({ shareID }), {
-        headers: { "Content-Type": "application/json" },
-      })
+      const secret = await stub.share(sessionID)
+      return new Response(
+        JSON.stringify({
+          secret,
+          url: "https://dev.opencode.ai/s?id=" + short,
+        }),
+        {
+          headers: { "Content-Type": "application/json" },
+        },
+      )
     }
     }
 
 
     if (request.method === "POST" && method === "share_delete") {
     if (request.method === "POST" && method === "share_delete") {
       const body = await request.json<any>()
       const body = await request.json<any>()
       const sessionID = body.sessionID
       const sessionID = body.sessionID
-      const shareID = body.shareID
-
-      // validate shareID
-      if (!shareID)
-        return new Response("Error: Share ID is required", { status: 400 })
-
-      // Delete from durable object
-      const id = env.SYNC_SERVER.idFromName(sessionID)
+      const secret = body.secret
+      const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
       const stub = env.SYNC_SERVER.get(id)
       const stub = env.SYNC_SERVER.get(id)
-      if ((await stub.getShareID()) !== shareID)
-        return new Response("Error: Share ID does not match", { status: 400 })
-
-      await stub.clear()
-
+      await stub.clear(secret)
       return new Response(JSON.stringify({}), {
       return new Response(JSON.stringify({}), {
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
       })
       })
     }
     }
 
 
     if (request.method === "POST" && method === "share_sync") {
     if (request.method === "POST" && method === "share_sync") {
-      const body = await request.json<any>()
-      const sessionID = body.sessionID
-      const shareID = body.shareID
-      const key = body.key
-      const content = body.content
-
-      console.log("share_sync", sessionID, shareID, key, content)
-
-      // validate key
-      if (
-        !key.startsWith(`session/info/${sessionID}`) &&
-        !key.startsWith(`session/message/${sessionID}/`)
-      )
-        return new Response("Error: Invalid key", { status: 400 })
-
-      // validate shareID
-      if (!shareID)
-        return new Response("Error: Share ID is required", { status: 400 })
-
-      // send message to server
-      const id = env.SYNC_SERVER.idFromName(sessionID)
+      const body = await request.json<{
+        sessionID: string
+        secret: string
+        key: string
+        content: any
+      }>()
+      const name = SyncServer.shortName(body.sessionID)
+      const id = env.SYNC_SERVER.idFromName(name)
       const stub = env.SYNC_SERVER.get(id)
       const stub = env.SYNC_SERVER.get(id)
-      if ((await stub.getShareID()) !== shareID)
-        return new Response("Error: Share ID does not match", { status: 400 })
-
-      await stub.publish(key, content)
-
-      // store message
-      await Resource.Bucket.put(
-        `${shareID}/${key}.json`,
-        JSON.stringify(content),
-      )
-
+      await stub.publish(body.secret, body.key, body.content)
       return new Response(JSON.stringify({}), {
       return new Response(JSON.stringify({}), {
         headers: { "Content-Type": "application/json" },
         headers: { "Content-Type": "application/json" },
       })
       })
     }
     }
 
 
     if (request.method === "GET" && method === "share_poll") {
     if (request.method === "GET" && method === "share_poll") {
-      // Expect to receive a WebSocket Upgrade request.
-      // If there is one, accept the request and return a WebSocket Response.
       const upgradeHeader = request.headers.get("Upgrade")
       const upgradeHeader = request.headers.get("Upgrade")
       if (!upgradeHeader || upgradeHeader !== "websocket") {
       if (!upgradeHeader || upgradeHeader !== "websocket") {
         return new Response("Error: Upgrade header is required", {
         return new Response("Error: Upgrade header is required", {
           status: 426,
           status: 426,
         })
         })
       }
       }
-
-      // get query parameters
-      const sessionID = url.searchParams.get("id")
-      if (!sessionID)
+      const id = url.searchParams.get("id")
+      if (!id)
         return new Response("Error: Share ID is required", { status: 400 })
         return new Response("Error: Share ID is required", { status: 400 })
-
-      // subscribe to server
-      const id = env.SYNC_SERVER.idFromName(sessionID)
-      const stub = env.SYNC_SERVER.get(id)
-      if (!(await stub.getShareID()))
-        return new Response("Error: Session not shared", { status: 400 })
-
+      const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
       return stub.fetch(request)
       return stub.fetch(request)
     }
     }
   },
   },

+ 1 - 3
js/src/index.ts

@@ -41,9 +41,7 @@ cli
         ? await Session.get(options.session)
         ? await Session.get(options.session)
         : await Session.create();
         : await Session.create();
       console.log("Session:", session.id);
       console.log("Session:", session.id);
-      console.log(
-        `Share: ${Share.URL.replace("api.", "")}/share?id=${session.id}`,
-      );
+      console.log(`Share: ${Share.URL.replace("api.", "")}/s?id=${session.id}`);
 
 
       Bus.subscribe(Message.Event.Updated, async (message) => {
       Bus.subscribe(Message.Event.Updated, async (message) => {
         console.log("Thinking...");
         console.log("Thinking...");

+ 22 - 8
js/src/session/session.ts

@@ -30,8 +30,17 @@ export namespace Session {
   export const Info = z
   export const Info = z
     .object({
     .object({
       id: Identifier.schema("session"),
       id: Identifier.schema("session"),
-      shareID: z.string().optional(),
+      share: z
+        .object({
+          secret: z.string(),
+          url: z.string(),
+        })
+        .optional(),
       title: z.string(),
       title: z.string(),
+      time: z.object({
+        created: z.number(),
+        updated: z.number(),
+      }),
     })
     })
     .openapi({
     .openapi({
       ref: "session.info",
       ref: "session.info",
@@ -61,13 +70,17 @@ export namespace Session {
     const result: Info = {
     const result: Info = {
       id: Identifier.descending("session"),
       id: Identifier.descending("session"),
       title: "New Session - " + new Date().toISOString(),
       title: "New Session - " + new Date().toISOString(),
+      time: {
+        created: Date.now(),
+        updated: Date.now(),
+      },
     };
     };
     log.info("created", result);
     log.info("created", result);
     state().sessions.set(result.id, result);
     state().sessions.set(result.id, result);
     await Storage.writeJSON("session/info/" + result.id, result);
     await Storage.writeJSON("session/info/" + result.id, result);
-    share(result.id).then((shareID) => {
+    share(result.id).then((share) => {
       update(result.id, (draft) => {
       update(result.id, (draft) => {
-        draft.shareID = shareID;
+        draft.share = share;
       });
       });
     });
     });
     Bus.publish(Event.Updated, {
     Bus.publish(Event.Updated, {
@@ -88,13 +101,13 @@ export namespace Session {
 
 
   export async function share(id: string) {
   export async function share(id: string) {
     const session = await get(id);
     const session = await get(id);
-    if (session.shareID) return session.shareID;
-    const shareID = await Share.create(id);
-    if (!shareID) return;
+    if (session.share) return session.share;
+    const share = await Share.create(id);
+    console.log("share", share);
     await update(id, (draft) => {
     await update(id, (draft) => {
-      draft.shareID = shareID;
+      draft.share = share;
     });
     });
-    return shareID as string;
+    return share;
   }
   }
 
 
   export async function update(id: string, editor: (session: Info) => void) {
   export async function update(id: string, editor: (session: Info) => void) {
@@ -102,6 +115,7 @@ export namespace Session {
     const session = await get(id);
     const session = await get(id);
     if (!session) return;
     if (!session) return;
     editor(session);
     editor(session);
+    session.time.updated = Date.now();
     sessions.set(id, session);
     sessions.set(id, session);
     await Storage.writeJSON("session/info/" + id, session);
     await Storage.writeJSON("session/info/" + id, session);
     Bus.publish(Event.Updated, {
     Bus.publish(Event.Updated, {

+ 4 - 3
js/src/share/share.ts

@@ -16,7 +16,8 @@ export namespace Share {
       if (root !== "session") return;
       if (root !== "session") return;
       const [, sessionID] = splits;
       const [, sessionID] = splits;
       const session = await Session.get(sessionID);
       const session = await Session.get(sessionID);
-      if (!session.shareID) return;
+      if (!session.share) return;
+      const { secret } = session.share;
 
 
       const key = payload.properties.key;
       const key = payload.properties.key;
       pending.set(key, payload.properties.content);
       pending.set(key, payload.properties.content);
@@ -31,7 +32,7 @@ export namespace Share {
             method: "POST",
             method: "POST",
             body: JSON.stringify({
             body: JSON.stringify({
               sessionID: sessionID,
               sessionID: sessionID,
-              shareID: session.shareID,
+              secret,
               key: key,
               key: key,
               content,
               content,
             }),
             }),
@@ -61,6 +62,6 @@ export namespace Share {
       body: JSON.stringify({ sessionID: sessionID }),
       body: JSON.stringify({ sessionID: sessionID }),
     })
     })
       .then((x) => x.json())
       .then((x) => x.json())
-      .then((x) => x.shareID);
+      .then((x) => x as { url: string; secret: string });
   }
   }
 }
 }