Skip to content

PostgresMessageQueue initialized option skips JSON serialization detection #1014

Description

@dahlia

Description

PostgresMessageQueue skips JSON serialization detection when constructed with initialized: true. With postgres.js, messages are then serialized twice and stored as JSONB strings instead of JSONB objects.

The initialized option is documented as indicating that the queue table has already been initialized. It should skip schema DDL, but it should not skip driver-specific runtime setup.

In packages/postgres/src/mq.ts, #driverSerializesJson defaults to false and is set by driverSerializesJson() only at the end of #doInitialize(). When initialized: true, initialize() returns before #doInitialize() runs. #json() therefore calls JSON.stringify() before passing the result to postgres.js sql.json(), whose serializer calls JSON.stringify() again.

Reproduction

This occurs with @fedify/postgres 2.3.6 and postgres.js 3.4.8:

import { PostgresMessageQueue } from "@fedify/postgres";
import postgres from "postgres";

const sql = postgres(process.env.DATABASE_URL!);

await sql`
  CREATE TABLE IF NOT EXISTS fedify_message_v2 (
    id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    message jsonb NOT NULL,
    delay interval DEFAULT '0 seconds',
    created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
    ordering_key text
  )
`;

const queue = new PostgresMessageQueue(sql, { initialized: true });
await queue.enqueue({ type: "inbox", payload: { id: "example" } });

const [row] = await sql`
  SELECT message, jsonb_typeof(message) AS json_type
  FROM fedify_message_v2
  ORDER BY created DESC
  LIMIT 1
`;

console.log(row.json_type); // "string"
console.log(typeof row.message); // "string"

await sql.end();

Without initialized: true, the same message is stored as a JSONB object and postgres.js returns it as an object.

Impact

Queue consumers receive a string rather than the original message object. Fedify dispatches queued tasks by message.type, so the string has no recognized task type. A listener can delete the row without processing the activity or reporting an error, causing silent loss of queued federation work.

Expected behavior

initialized: true should skip queue schema DDL while preserving the same enqueue and dequeue behavior as the default initialization path.

One possible fix is to run the driver serialization probe independently from schema initialization, then let initialized control only the CREATE TABLE, ALTER TABLE, and CREATE INDEX statements.

Metadata

Metadata

Assignees

No one assigned

    Labels

    driver/postgresPostgreSQL driver (@fedify/postgres)

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions