Improve router encoding

This commit is contained in:
2026-05-22 12:12:13 +00:00
parent 85dc121333
commit bd1431f3ba
4 changed files with 44 additions and 25 deletions

View File

@@ -5,6 +5,7 @@ import type { StoreSQLite } from '../services/invitation-store.js';
import { parseInvitation } from '../utils/invitation-parser.js'; import { parseInvitation } from '../utils/invitation-parser.js';
import Z from 'zod'; import Z from 'zod';
import { encodeExtendedJson } from '../utils/ext-json.js';
export class InvitationsRoute { export class InvitationsRoute {
constructor( 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) { async getInvitation(request: FastifyRequest, reply: FastifyReply) {
// Get the invitation identifier from the query // Get the invitation identifier from the query
const { invitationIdentifier } = request.query as { invitationIdentifier?: string }; const { invitationIdentifier } = request.query as { invitationIdentifier?: string };
@@ -39,30 +46,29 @@ export class InvitationsRoute {
// Get the invitation from the store // Get the invitation from the store
const storedInvitation = await this.invitationStore.get(invitationIdentifier); const storedInvitation = await this.invitationStore.get(invitationIdentifier);
if (request.headers['accept'] === 'text/event-stream') { // If the client is not subscribing to the SSE stream, return the invitation.
// Subscribe the client to the SSE stream. if (request.headers['accept'] !== 'text/event-stream') {
await this.sseBroadcaster.subscribe(request, reply); return encodeExtendedJson(storedInvitation || {});
}
// If the invitation doesn't exist, don't send anything. // Its an SSE request, so we need to subscribe the client to the SSE stream.
if (!storedInvitation) { await this.sseBroadcaster.subscribe(request, reply);
return;
}
// Send the invitation to the client as if it was a get request. // If the invitation doesn't exist, don't send anything.
this.sseBroadcaster.sendEvent(reply, 'invitation-updated', storedInvitation); if (!storedInvitation) {
// Return early
return; return;
} }
if(!storedInvitation) { // Send the invitation to the client as if it was a get request.
return reply.status(200).send({}); this.sseBroadcaster.sendEvent(reply, 'invitation-updated', storedInvitation);
}
// If the client is not subscribing to the SSE stream, return the invitation.
return reply.status(200).send(storedInvitation);
} }
/**
* Update the invitation.
* @param request - The request.
* @param reply - The reply.
* @returns The merged invitation.
*/
async updateInvitation(request: FastifyRequest, reply: FastifyReply) { async updateInvitation(request: FastifyRequest, reply: FastifyReply) {
// Parse the invitation // Parse the invitation
const invitation = parseInvitation.parse(request.body); 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) // 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); await this.sseBroadcaster.broadcast(invitation.invitationIdentifier, 'invitation-updated', invitation);
return reply.status(200).send(invitation); return invitation;
} }
/** /**

View File

@@ -5,7 +5,7 @@ import { z } from "zod";
import { import {
decodeExtendedJsonObject, decodeExtendedJsonObject,
encodeExtendedJsonObject, encodeExtendedJsonObject,
} from "../utils/ext-json"; } from "../utils/ext-json.js";
// Interface to add to our route classes so that we can register them. // 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. // NOTE: I hate this pattern. But ExpressJS is odd in that it is structured as a singleton that still needs registration.

View File

@@ -1,5 +1,7 @@
import sqlite3, { type Database } from 'better-sqlite3'; import sqlite3, { type Database } from 'better-sqlite3';
import { pack, unpack } from 'msgpackr'; import { pack, unpack } from 'msgpackr';
import { encodeExtendedJsonObject, decodeExtendedJsonObject } from '../utils/ext-json.js';
import { binToHex, hexToBin } from '@bitauth/libauth';
export interface SQLiteOptions { export interface SQLiteOptions {
wal: boolean; wal: boolean;
@@ -107,25 +109,34 @@ export class StoreSQLite<T> {
async get(key: string): Promise<T | undefined> { async get(key: string): Promise<T | undefined> {
const result = this.db const result = this.db
.prepare(`SELECT value FROM "${this.storeName}" WHERE key = ?`) .prepare(`SELECT value FROM "${this.storeName}" WHERE key = ?`)
.get(key) as { value: Buffer } | undefined; .get(key) as { value: string } | undefined;
if (!result) { if (!result) {
return undefined; return undefined;
} }
// Deserialize using msgpackr for consistency with other implementations const binValue = hexToBin(result.value);
return unpack(result.value) as T;
const unpackedValue = unpack(binValue);
const decodedValue = decodeExtendedJsonObject(unpackedValue);
return decodedValue as T;
} }
async set(key: string, value: T): Promise<void> { async set(key: string, value: T): Promise<void> {
const encodedValue = encodeExtendedJsonObject(value);
// Serialize using msgpackr for consistency with other implementations // Serialize using msgpackr for consistency with other implementations
const serializedValue = pack(value); const packedValue = pack(encodedValue);
const serializedValue = binToHex(packedValue);
this.db this.db
.prepare( .prepare(
`INSERT OR REPLACE INTO "${this.storeName}" (key, value) VALUES (?, ?)`, `INSERT OR REPLACE INTO "${this.storeName}" (key, value) VALUES (?, ?)`,
) )
.run(key, Buffer.from(serializedValue)); .run(key, serializedValue);
} }
async delete(key: string): Promise<void> { async delete(key: string): Promise<void> {

View File

@@ -2,6 +2,8 @@ import type { FastifyReply, FastifyRequest } from "fastify";
import debug, { type Debugger } from "debug"; import debug, { type Debugger } from "debug";
import { encodeExtendedJson } from "../utils/ext-json.js";
/** /**
* Represents an event stored in the history buffer. * Represents an event stored in the history buffer.
* Used for replaying missed events to reconnecting clients. * Used for replaying missed events to reconnecting clients.
@@ -87,7 +89,7 @@ export class SSEBroadcaster {
const timestamp = Date.now(); const timestamp = Date.now();
client.raw.write(`id: ${timestamp}\n`); client.raw.write(`id: ${timestamp}\n`);
client.raw.write(`event: ${topic}\n`); client.raw.write(`event: ${topic}\n`);
client.raw.write(`data: ${JSON.stringify(data)}\n\n`); client.raw.write(`data: ${encodeExtendedJson(data)}\n\n`);
} }
/** /**