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.
Description
PostgresMessageQueueskips JSON serialization detection when constructed withinitialized: true. With postgres.js, messages are then serialized twice and stored as JSONB strings instead of JSONB objects.The
initializedoption 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,
#driverSerializesJsondefaults tofalseand is set bydriverSerializesJson()only at the end of#doInitialize(). Wheninitialized: true,initialize()returns before#doInitialize()runs.#json()therefore callsJSON.stringify()before passing the result to postgres.jssql.json(), whose serializer callsJSON.stringify()again.Reproduction
This occurs with
@fedify/postgres2.3.6 and postgres.js 3.4.8: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: trueshould 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
initializedcontrol only theCREATE TABLE,ALTER TABLE, andCREATE INDEXstatements.