mirror of
https://github.com/immich-app/immich.git
synced 2026-03-23 01:19:00 +03:00
feat: socket.io redis->postgres socket.io, add broadcastchannel option
This commit is contained in:
80
server/src/middleware/broadcast-channel.adapter.ts
Normal file
80
server/src/middleware/broadcast-channel.adapter.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
ClusterAdapterWithHeartbeat,
|
||||
type ClusterAdapterOptions,
|
||||
type ClusterMessage,
|
||||
type ClusterResponse,
|
||||
type ServerId,
|
||||
} from 'socket.io-adapter';
|
||||
|
||||
const BC_CHANNEL_NAME = 'immich:socketio';
|
||||
|
||||
interface BroadcastChannelPayload {
|
||||
type: 'message' | 'response';
|
||||
sourceUid: string;
|
||||
targetUid?: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket.IO adapter using Node.js BroadcastChannel
|
||||
*
|
||||
* Relays messages between worker_threads within a single OS process.
|
||||
* Zero external dependencies. Does NOT work across containers — use
|
||||
* the Postgres adapter for multi-replica deployments.
|
||||
*/
|
||||
class BroadcastChannelAdapter extends ClusterAdapterWithHeartbeat {
|
||||
private readonly channel: BroadcastChannel;
|
||||
|
||||
constructor(nsp: any, opts?: Partial<ClusterAdapterOptions>) {
|
||||
super(nsp, opts ?? {});
|
||||
|
||||
this.channel = new BroadcastChannel(BC_CHANNEL_NAME);
|
||||
this.channel.addEventListener('message', (event: MessageEvent<BroadcastChannelPayload>) => {
|
||||
const msg = event.data;
|
||||
if (msg.sourceUid === this.uid) {
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'message') {
|
||||
this.onMessage(msg.data as ClusterMessage);
|
||||
} else if (msg.type === 'response' && msg.targetUid === this.uid) {
|
||||
this.onResponse(msg.data as ClusterResponse);
|
||||
}
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
override doPublish(message: ClusterMessage): Promise<string> {
|
||||
this.channel.postMessage({
|
||||
type: 'message',
|
||||
sourceUid: this.uid,
|
||||
data: message,
|
||||
});
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
override doPublishResponse(requesterUid: ServerId, response: ClusterResponse): Promise<void> {
|
||||
this.channel.postMessage({
|
||||
type: 'response',
|
||||
sourceUid: this.uid,
|
||||
targetUid: requesterUid,
|
||||
data: response,
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
override close(): void {
|
||||
super.close();
|
||||
this.channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function createBroadcastChannelAdapter(opts?: Partial<ClusterAdapterOptions>) {
|
||||
const options: Partial<ClusterAdapterOptions> = {
|
||||
...opts,
|
||||
};
|
||||
|
||||
return function (nsp: any) {
|
||||
return new BroadcastChannelAdapter(nsp, options);
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,103 @@
|
||||
import { INestApplicationContext } from '@nestjs/common';
|
||||
import { INestApplication, Logger } from '@nestjs/common';
|
||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { Redis } from 'ioredis';
|
||||
import { ServerOptions } from 'socket.io';
|
||||
import { Pool, PoolConfig } from 'pg';
|
||||
import type { ServerOptions } from 'socket.io';
|
||||
import { SocketIoAdapter } from 'src/enum';
|
||||
import { createBroadcastChannelAdapter } from 'src/middleware/broadcast-channel.adapter';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { asPostgresConnectionConfig } from 'src/utils/database';
|
||||
|
||||
export class WebSocketAdapter extends IoAdapter {
|
||||
constructor(private app: INestApplicationContext) {
|
||||
export type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
|
||||
|
||||
export function asPgPoolSsl(ssl?: Ssl): PoolConfig['ssl'] {
|
||||
if (ssl === undefined || ssl === false || ssl === 'allow') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ssl === true || ssl === 'prefer' || ssl === 'require') {
|
||||
return { rejectUnauthorized: false };
|
||||
}
|
||||
|
||||
if (ssl === 'verify-full') {
|
||||
return { rejectUnauthorized: true };
|
||||
}
|
||||
|
||||
return ssl;
|
||||
}
|
||||
|
||||
class BroadcastChannelSocketAdapter extends IoAdapter {
|
||||
private adapterConstructor: ReturnType<typeof createBroadcastChannelAdapter>;
|
||||
|
||||
constructor(app: INestApplication) {
|
||||
super(app);
|
||||
this.adapterConstructor = createBroadcastChannelAdapter();
|
||||
}
|
||||
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const { redis } = this.app.get(ConfigRepository).getEnv();
|
||||
const server = super.createIOServer(port, options);
|
||||
const pubClient = new Redis(redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
server.adapter(this.adapterConstructor);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
class PostgresSocketAdapter extends IoAdapter {
|
||||
private adapterConstructor: any;
|
||||
|
||||
constructor(app: INestApplication, adapterConstructor: any) {
|
||||
super(app);
|
||||
this.adapterConstructor = adapterConstructor;
|
||||
}
|
||||
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const server = super.createIOServer(port, options);
|
||||
server.adapter(this.adapterConstructor);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWebSocketAdapter(
|
||||
app: INestApplication,
|
||||
adapterOverride?: SocketIoAdapter,
|
||||
): Promise<IoAdapter> {
|
||||
const logger = new Logger('WebSocketAdapter');
|
||||
const config = new ConfigRepository();
|
||||
const { database, socketIo } = config.getEnv();
|
||||
const adapter = adapterOverride ?? socketIo.adapter;
|
||||
|
||||
switch (adapter) {
|
||||
case SocketIoAdapter.Postgres: {
|
||||
logger.log('Using Postgres Socket.IO adapter');
|
||||
const { createAdapter } = await import('@socket.io/postgres-adapter');
|
||||
const config = asPostgresConnectionConfig(database.config);
|
||||
const pool = new Pool({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.username,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
ssl: asPgPoolSsl(config.ssl),
|
||||
max: 2,
|
||||
});
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS socket_io_attachments (
|
||||
id bigserial UNIQUE,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
payload bytea
|
||||
);
|
||||
`);
|
||||
|
||||
pool.on('error', (error) => {
|
||||
logger.error(' Postgres pool error', error);
|
||||
});
|
||||
|
||||
const adapterConstructor = createAdapter(pool);
|
||||
return new PostgresSocketAdapter(app, adapterConstructor);
|
||||
}
|
||||
|
||||
case SocketIoAdapter.BroadcastChannel: {
|
||||
logger.log('Using BroadcastChannel Socket.IO adapter');
|
||||
return new BroadcastChannelSocketAdapter(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user