mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
framework: add event stream support
This commit is contained in:
3 files changed
+82
-48
No files matched your search
@@ -27,8 +27,8 @@ export default (logger, xff, xhost) => async (ctx: KoaContext, next: Next) => {
|
|||||||
template: null,
|
template: null,
|
||||||
redirect: null,
|
redirect: null,
|
||||||
attachment: (name, streamOrBuffer) => {
|
attachment: (name, streamOrBuffer) => {
|
||||||
ctx.attachment(name);
|
if (name) ctx.attachment(name);
|
||||||
if (streamOrBuffer instanceof Buffer) {
|
if (streamOrBuffer instanceof Buffer || streamOrBuffer instanceof PassThrough) {
|
||||||
response.body = null;
|
response.body = null;
|
||||||
ctx.body = streamOrBuffer;
|
ctx.body = streamOrBuffer;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"@hydrooj/utils": "workspace:^",
|
"@hydrooj/utils": "workspace:^",
|
||||||
"emoji-regex": "^10.4.0",
|
"emoji-regex": "^10.4.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"koa": "^2.15.3",
|
"koa": "^2.15.4",
|
||||||
"koa-body": "^6.0.1",
|
"koa-body": "^6.0.1",
|
||||||
"koa-compress": "^5.1.1",
|
"koa-compress": "^5.1.1",
|
||||||
"koa-router": "^13.0.1",
|
"koa-router": "^13.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
import { Context, Service } from 'cordis';
|
import { Context, Service } from 'cordis';
|
||||||
import type { Files } from 'formidable';
|
import type { Files } from 'formidable';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
@@ -345,7 +346,7 @@ export class WebService extends Service {
|
|||||||
ConnectionHandler = ConnectionHandler;
|
ConnectionHandler = ConnectionHandler;
|
||||||
|
|
||||||
constructor(ctx: Context, public config: WebServiceConfig) {
|
constructor(ctx: Context, public config: WebServiceConfig) {
|
||||||
super(ctx, 'server', true);
|
super(ctx, 'server');
|
||||||
ctx.mixin('server', ['Route', 'Connection', 'withHandlerClass']);
|
ctx.mixin('server', ['Route', 'Connection', 'withHandlerClass']);
|
||||||
this.server.keys = this.config.keys;
|
this.server.keys = this.config.keys;
|
||||||
this.server.proxy = this.config.proxy;
|
this.server.proxy = this.config.proxy;
|
||||||
@@ -353,13 +354,17 @@ export class WebService extends Service {
|
|||||||
this.server.use(Compress());
|
this.server.use(Compress());
|
||||||
this.server.use(async (c, next) => {
|
this.server.use(async (c, next) => {
|
||||||
if (c.request.headers.origin && this.config.cors) {
|
if (c.request.headers.origin && this.config.cors) {
|
||||||
const host = new URL(c.request.headers.origin).host;
|
try {
|
||||||
if (host !== c.request.headers.host && `,${this.config.cors},`.includes(`,${host},`)) {
|
const host = new URL(c.request.headers.origin).host;
|
||||||
c.set('Access-Control-Allow-Credentials', 'true');
|
if (host !== c.request.headers.host && `,${this.config.cors},`.includes(`,${host},`)) {
|
||||||
c.set('Access-Control-Allow-Origin', c.request.headers.origin);
|
c.set('Access-Control-Allow-Credentials', 'true');
|
||||||
c.set('Access-Control-Allow-Headers', corsAllowHeaders);
|
c.set('Access-Control-Allow-Origin', c.request.headers.origin);
|
||||||
c.set('Vary', 'Origin');
|
c.set('Access-Control-Allow-Headers', corsAllowHeaders);
|
||||||
c.cors = true;
|
c.set('Vary', 'Origin');
|
||||||
|
c.cors = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// invalid origin header, ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (c.request.method.toLowerCase() === 'options') {
|
if (c.request.method.toLowerCase() === 'options') {
|
||||||
@@ -378,7 +383,7 @@ export class WebService extends Service {
|
|||||||
await next();
|
await next();
|
||||||
} finally {
|
} finally {
|
||||||
const endTime = Date.now();
|
const endTime = Date.now();
|
||||||
if (!(c.nolog || c.response.headers.nolog)) {
|
if (!c.nolog && !c.response.headers.nolog) {
|
||||||
logger.debug(`${c.request.method} /${c.domainId || 'system'}${c.request.path} \
|
logger.debug(`${c.request.method} /${c.domainId || 'system'}${c.request.path} \
|
||||||
${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
||||||
}
|
}
|
||||||
@@ -547,10 +552,33 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleWS(ctx: KoaContext, HandlerClass, checker, conn, layer) {
|
private async handleWS(ctx: KoaContext, HandlerClass, checker, conn, layer?) {
|
||||||
const { args } = ctx.HydroContext;
|
const { args } = ctx.HydroContext;
|
||||||
const h = new HandlerClass(ctx, this.ctx);
|
const h = new HandlerClass(ctx, this.ctx);
|
||||||
await this.ctx.parallel('connection/create', h);
|
await this.ctx.parallel('connection/create', h);
|
||||||
|
const stream = new PassThrough();
|
||||||
|
if (!conn) {
|
||||||
|
// By HTTP
|
||||||
|
ctx.request.socket.setTimeout(0);
|
||||||
|
ctx.req.socket.setNoDelay(true);
|
||||||
|
ctx.req.socket.setKeepAlive(true);
|
||||||
|
ctx.set({
|
||||||
|
'X-Accel-Buffering': 'no',
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
});
|
||||||
|
ctx.HydroContext.request.websocket = true;
|
||||||
|
ctx.compress = false;
|
||||||
|
conn = {
|
||||||
|
close() {
|
||||||
|
stream.end();
|
||||||
|
},
|
||||||
|
send(data: any) {
|
||||||
|
stream.write(`${args.sse ? 'data: ' : ''}${data}\n${args.sse ? '\n' : ''}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
ctx.handler = h;
|
ctx.handler = h;
|
||||||
h.conn = conn;
|
h.conn = conn;
|
||||||
const disposables = [];
|
const disposables = [];
|
||||||
@@ -562,54 +590,59 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
if (h.prepare) await h.prepare(args);
|
if (h.prepare) await h.prepare(args);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
for (const { name, target } of h.__subscribe || []) disposables.push(this.ctx.on(name, target.bind(h)));
|
for (const { name, target } of h.__subscribe || []) disposables.push(this.ctx.on(name, target.bind(h)));
|
||||||
let lastHeartbeat = Date.now();
|
|
||||||
let closed = false;
|
let closed = false;
|
||||||
let interval: NodeJS.Timeout;
|
let interval: NodeJS.Timeout;
|
||||||
const clean = () => {
|
const clean = () => {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
closed = true;
|
closed = true;
|
||||||
this.ctx.emit('connection/close', h);
|
this.ctx.emit('connection/close', h);
|
||||||
layer.clients.delete(conn);
|
if (layer) layer.clients.delete(conn);
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
for (const d of disposables) d();
|
for (const d of disposables) d();
|
||||||
h.cleanup?.(args);
|
h.cleanup?.(args);
|
||||||
};
|
};
|
||||||
interval = setInterval(() => {
|
if (layer) {
|
||||||
if (Date.now() - lastHeartbeat > 80000) {
|
let lastHeartbeat = Date.now();
|
||||||
clean();
|
interval = setInterval(() => {
|
||||||
conn.terminate();
|
if (Date.now() - lastHeartbeat > 80000) {
|
||||||
}
|
clean();
|
||||||
if (Date.now() - lastHeartbeat > 30000) conn.send('ping');
|
conn.terminate();
|
||||||
}, 40000);
|
}
|
||||||
conn.on('pong', () => {
|
if (Date.now() - lastHeartbeat > 30000) conn.send('ping');
|
||||||
lastHeartbeat = Date.now();
|
}, 40000);
|
||||||
});
|
conn.on('pong', () => {
|
||||||
conn.onmessage = (e) => {
|
lastHeartbeat = Date.now();
|
||||||
lastHeartbeat = Date.now();
|
});
|
||||||
if (e.data === 'pong') return;
|
conn.onmessage = (e) => {
|
||||||
if (e.data === 'ping') {
|
lastHeartbeat = Date.now();
|
||||||
conn.send('pong');
|
if (e.data === 'pong') return;
|
||||||
return;
|
if (e.data === 'ping') {
|
||||||
}
|
conn.send('pong');
|
||||||
let payload;
|
return;
|
||||||
try {
|
}
|
||||||
payload = JSON.parse(e.data.toString());
|
let payload;
|
||||||
} catch {
|
try {
|
||||||
conn.close();
|
payload = JSON.parse(e.data.toString());
|
||||||
}
|
} catch {
|
||||||
try {
|
conn.close();
|
||||||
h.message?.(payload);
|
}
|
||||||
} catch (err) {
|
try {
|
||||||
logger.error(e);
|
h.message?.(payload);
|
||||||
}
|
} catch (err) {
|
||||||
};
|
logger.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else ctx.body = stream;
|
||||||
await this.ctx.parallel('connection/active', h);
|
await this.ctx.parallel('connection/active', h);
|
||||||
if (conn.readyState === conn.OPEN) {
|
if (layer) {
|
||||||
conn.on('close', clean);
|
if (conn.readyState === conn.OPEN) {
|
||||||
conn.resume();
|
conn.on('close', clean);
|
||||||
} else clean();
|
conn.resume();
|
||||||
|
} else clean();
|
||||||
|
} else stream.on('close', clean);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await h.onerror(e);
|
await h.onerror(e);
|
||||||
|
if (!layer) ctx.status = 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,6 +661,7 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`);
|
|||||||
const layer = router.ws(path, async (conn, _req, ctx) => {
|
const layer = router.ws(path, async (conn, _req, ctx) => {
|
||||||
await this.handleWS(ctx as any, HandlerClass, checker, conn, layer);
|
await this.handleWS(ctx as any, HandlerClass, checker, conn, layer);
|
||||||
});
|
});
|
||||||
|
router.get(path, (ctx) => this.handleWS(ctx as any, HandlerClass, checker, null, null));
|
||||||
}
|
}
|
||||||
const dispose = router.disposeLastOp;
|
const dispose = router.disposeLastOp;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
Reference in new issue
Block a user