diff --git a/src/routes/invitations.ts b/src/routes/invitations.ts index eb7f08f..428347b 100644 --- a/src/routes/invitations.ts +++ b/src/routes/invitations.ts @@ -5,6 +5,7 @@ import type { StoreSQLite } from '../services/invitation-store.js'; import { parseInvitation } from '../utils/invitation-parser.js'; import Z from 'zod'; +import { encodeExtendedJson } from '../utils/ext-json.js'; export class InvitationsRoute { constructor( @@ -27,6 +28,12 @@ export class InvitationsRoute { ]; } + /** + * Get an invitation, and if the text/event-stream header is present, subscribe the client to the SSE stream. + * @param request - The request. + * @param reply - The reply. + * @returns The invitation. + */ async getInvitation(request: FastifyRequest, reply: FastifyReply) { // Get the invitation identifier from the query const { invitationIdentifier } = request.query as { invitationIdentifier?: string }; @@ -39,30 +46,29 @@ export class InvitationsRoute { // Get the invitation from the store const storedInvitation = await this.invitationStore.get(invitationIdentifier); - if (request.headers['accept'] === 'text/event-stream') { - // Subscribe the client to the SSE stream. - await this.sseBroadcaster.subscribe(request, reply); + // If the client is not subscribing to the SSE stream, return the invitation. + if (request.headers['accept'] !== 'text/event-stream') { + return encodeExtendedJson(storedInvitation || {}); + } - // If the invitation doesn't exist, don't send anything. - if (!storedInvitation) { - return; - } + // Its an SSE request, so we need to subscribe the client to the SSE stream. + await this.sseBroadcaster.subscribe(request, reply); - // Send the invitation to the client as if it was a get request. - this.sseBroadcaster.sendEvent(reply, 'invitation-updated', storedInvitation); - - // Return early + // If the invitation doesn't exist, don't send anything. + if (!storedInvitation) { return; } - if(!storedInvitation) { - return reply.status(200).send({}); - } - - // If the client is not subscribing to the SSE stream, return the invitation. - return reply.status(200).send(storedInvitation); + // Send the invitation to the client as if it was a get request. + this.sseBroadcaster.sendEvent(reply, 'invitation-updated', storedInvitation); } + /** + * Update the invitation. + * @param request - The request. + * @param reply - The reply. + * @returns The merged invitation. + */ async updateInvitation(request: FastifyRequest, reply: FastifyReply) { // Parse the invitation const invitation = parseInvitation.parse(request.body); @@ -79,7 +85,7 @@ export class InvitationsRoute { // Broadcast the invitation update (We send down the whole invitation. Clients will have to compare commitIds) await this.sseBroadcaster.broadcast(invitation.invitationIdentifier, 'invitation-updated', invitation); - return reply.status(200).send(invitation); + return invitation; } /** diff --git a/src/services/http-router.ts b/src/services/http-router.ts index 4d173cc..ecfc5a3 100644 --- a/src/services/http-router.ts +++ b/src/services/http-router.ts @@ -5,7 +5,7 @@ import { z } from "zod"; import { decodeExtendedJsonObject, encodeExtendedJsonObject, -} from "../utils/ext-json"; +} from "../utils/ext-json.js"; // Interface to add to our route classes so that we can register them. // NOTE: I hate this pattern. But ExpressJS is odd in that it is structured as a singleton that still needs registration. diff --git a/src/services/invitation-store.ts b/src/services/invitation-store.ts index 0a3a397..b9a6c31 100644 --- a/src/services/invitation-store.ts +++ b/src/services/invitation-store.ts @@ -1,5 +1,7 @@ import sqlite3, { type Database } from 'better-sqlite3'; import { pack, unpack } from 'msgpackr'; +import { encodeExtendedJsonObject, decodeExtendedJsonObject } from '../utils/ext-json.js'; +import { binToHex, hexToBin } from '@bitauth/libauth'; export interface SQLiteOptions { wal: boolean; @@ -107,25 +109,34 @@ export class StoreSQLite { async get(key: string): Promise { const result = this.db .prepare(`SELECT value FROM "${this.storeName}" WHERE key = ?`) - .get(key) as { value: Buffer } | undefined; + .get(key) as { value: string } | undefined; if (!result) { return undefined; } - // Deserialize using msgpackr for consistency with other implementations - return unpack(result.value) as T; + const binValue = hexToBin(result.value); + + const unpackedValue = unpack(binValue); + + const decodedValue = decodeExtendedJsonObject(unpackedValue); + + return decodedValue as T; } async set(key: string, value: T): Promise { + const encodedValue = encodeExtendedJsonObject(value); + // Serialize using msgpackr for consistency with other implementations - const serializedValue = pack(value); + const packedValue = pack(encodedValue); + + const serializedValue = binToHex(packedValue); this.db .prepare( `INSERT OR REPLACE INTO "${this.storeName}" (key, value) VALUES (?, ?)`, ) - .run(key, Buffer.from(serializedValue)); + .run(key, serializedValue); } async delete(key: string): Promise { diff --git a/src/services/sse-broadcast.ts b/src/services/sse-broadcast.ts index db82bbb..b308b8d 100644 --- a/src/services/sse-broadcast.ts +++ b/src/services/sse-broadcast.ts @@ -2,6 +2,8 @@ import type { FastifyReply, FastifyRequest } from "fastify"; import debug, { type Debugger } from "debug"; +import { encodeExtendedJson } from "../utils/ext-json.js"; + /** * Represents an event stored in the history buffer. * Used for replaying missed events to reconnecting clients. @@ -87,7 +89,7 @@ export class SSEBroadcaster { const timestamp = Date.now(); client.raw.write(`id: ${timestamp}\n`); client.raw.write(`event: ${topic}\n`); - client.raw.write(`data: ${JSON.stringify(data)}\n\n`); + client.raw.write(`data: ${encodeExtendedJson(data)}\n\n`); } /**