@mt-tl/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @mt-tl/server might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/auth/handshake.d.ts +35 -0
- package/dist/auth/handshake.d.ts.map +1 -0
- package/dist/auth/handshake.js +208 -0
- package/dist/auth/handshake.js.map +1 -0
- package/dist/auth/nonce-store.d.ts +22 -0
- package/dist/auth/nonce-store.d.ts.map +1 -0
- package/dist/auth/nonce-store.js +23 -0
- package/dist/auth/nonce-store.js.map +1 -0
- package/dist/bootstrap.d.ts +36 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +82 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/config.d.ts +103 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -0
- package/dist/core/context.d.ts +57 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +27 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/errors.d.ts +33 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/rpc.d.ts +83 -0
- package/dist/core/rpc.d.ts.map +1 -0
- package/dist/core/rpc.js +102 -0
- package/dist/core/rpc.js.map +1 -0
- package/dist/core/updates.d.ts +56 -0
- package/dist/core/updates.d.ts.map +1 -0
- package/dist/core/updates.js +34 -0
- package/dist/core/updates.js.map +1 -0
- package/dist/create-server.d.ts +89 -0
- package/dist/create-server.d.ts.map +1 -0
- package/dist/create-server.js +109 -0
- package/dist/create-server.js.map +1 -0
- package/dist/crypto/aes-ige.d.ts +3 -0
- package/dist/crypto/aes-ige.d.ts.map +1 -0
- package/dist/crypto/aes-ige.js +55 -0
- package/dist/crypto/aes-ige.js.map +1 -0
- package/dist/crypto/dh.d.ts +21 -0
- package/dist/crypto/dh.d.ts.map +1 -0
- package/dist/crypto/dh.js +99 -0
- package/dist/crypto/dh.js.map +1 -0
- package/dist/crypto/hashes.d.ts +6 -0
- package/dist/crypto/hashes.d.ts.map +1 -0
- package/dist/crypto/hashes.js +14 -0
- package/dist/crypto/hashes.js.map +1 -0
- package/dist/crypto/msg-key.d.ts +15 -0
- package/dist/crypto/msg-key.d.ts.map +1 -0
- package/dist/crypto/msg-key.js +24 -0
- package/dist/crypto/msg-key.js.map +1 -0
- package/dist/crypto/rsa.d.ts +27 -0
- package/dist/crypto/rsa.d.ts.map +1 -0
- package/dist/crypto/rsa.js +50 -0
- package/dist/crypto/rsa.js.map +1 -0
- package/dist/dispatch/dispatcher.d.ts +72 -0
- package/dist/dispatch/dispatcher.d.ts.map +1 -0
- package/dist/dispatch/dispatcher.js +503 -0
- package/dist/dispatch/dispatcher.js.map +1 -0
- package/dist/dispatch/forwarders/in-process.d.ts +12 -0
- package/dist/dispatch/forwarders/in-process.d.ts.map +1 -0
- package/dist/dispatch/forwarders/in-process.js +15 -0
- package/dist/dispatch/forwarders/in-process.js.map +1 -0
- package/dist/dispatch/forwarders/print.d.ts +14 -0
- package/dist/dispatch/forwarders/print.d.ts.map +1 -0
- package/dist/dispatch/forwarders/print.js +23 -0
- package/dist/dispatch/forwarders/print.js.map +1 -0
- package/dist/dispatch/rpc-forwarder.d.ts +14 -0
- package/dist/dispatch/rpc-forwarder.d.ts.map +1 -0
- package/dist/dispatch/rpc-forwarder.js +2 -0
- package/dist/dispatch/rpc-forwarder.js.map +1 -0
- package/dist/dispatch/types.d.ts +32 -0
- package/dist/dispatch/types.d.ts.map +1 -0
- package/dist/dispatch/types.js +23 -0
- package/dist/dispatch/types.js.map +1 -0
- package/dist/gateway.d.ts +57 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +150 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +12 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +13 -0
- package/dist/lib.js.map +1 -0
- package/dist/server/message-pipeline.d.ts +57 -0
- package/dist/server/message-pipeline.d.ts.map +1 -0
- package/dist/server/message-pipeline.js +199 -0
- package/dist/server/message-pipeline.js.map +1 -0
- package/dist/session/inbound-tracker.d.ts +118 -0
- package/dist/session/inbound-tracker.d.ts.map +1 -0
- package/dist/session/inbound-tracker.js +170 -0
- package/dist/session/inbound-tracker.js.map +1 -0
- package/dist/session/message-id.d.ts +19 -0
- package/dist/session/message-id.d.ts.map +1 -0
- package/dist/session/message-id.js +30 -0
- package/dist/session/message-id.js.map +1 -0
- package/dist/session/salts.d.ts +51 -0
- package/dist/session/salts.d.ts.map +1 -0
- package/dist/session/salts.js +132 -0
- package/dist/session/salts.js.map +1 -0
- package/dist/session/session-manager.d.ts +18 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +43 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +16 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/memory.d.ts +3 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +91 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/mongo.d.ts +3 -0
- package/dist/storage/mongo.d.ts.map +1 -0
- package/dist/storage/mongo.js +175 -0
- package/dist/storage/mongo.js.map +1 -0
- package/dist/storage/types.d.ts +85 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +3 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/testkit.d.ts +11 -0
- package/dist/testkit.d.ts.map +1 -0
- package/dist/testkit.js +17 -0
- package/dist/testkit.js.map +1 -0
- package/dist/tl/codec.d.ts +37 -0
- package/dist/tl/codec.d.ts.map +1 -0
- package/dist/tl/codec.js +297 -0
- package/dist/tl/codec.js.map +1 -0
- package/dist/tl/layered-registry.d.ts +29 -0
- package/dist/tl/layered-registry.d.ts.map +1 -0
- package/dist/tl/layered-registry.js +118 -0
- package/dist/tl/layered-registry.js.map +1 -0
- package/dist/tl/protocol.d.ts +126 -0
- package/dist/tl/protocol.d.ts.map +1 -0
- package/dist/tl/protocol.js +22 -0
- package/dist/tl/protocol.js.map +1 -0
- package/dist/tl/reader.d.ts +30 -0
- package/dist/tl/reader.d.ts.map +1 -0
- package/dist/tl/reader.js +87 -0
- package/dist/tl/reader.js.map +1 -0
- package/dist/tl/registry.d.ts +39 -0
- package/dist/tl/registry.d.ts.map +1 -0
- package/dist/tl/registry.js +52 -0
- package/dist/tl/registry.js.map +1 -0
- package/dist/tl/writer.d.ts +24 -0
- package/dist/tl/writer.d.ts.map +1 -0
- package/dist/tl/writer.js +95 -0
- package/dist/tl/writer.js.map +1 -0
- package/dist/transport/connection-registry.d.ts +31 -0
- package/dist/transport/connection-registry.d.ts.map +1 -0
- package/dist/transport/connection-registry.js +84 -0
- package/dist/transport/connection-registry.js.map +1 -0
- package/dist/transport/connection.d.ts +62 -0
- package/dist/transport/connection.d.ts.map +1 -0
- package/dist/transport/connection.js +77 -0
- package/dist/transport/connection.js.map +1 -0
- package/dist/transport/framing.d.ts +39 -0
- package/dist/transport/framing.d.ts.map +1 -0
- package/dist/transport/framing.js +212 -0
- package/dist/transport/framing.js.map +1 -0
- package/dist/transport/proxy-protocol.d.ts +23 -0
- package/dist/transport/proxy-protocol.d.ts.map +1 -0
- package/dist/transport/proxy-protocol.js +108 -0
- package/dist/transport/proxy-protocol.js.map +1 -0
- package/dist/transport/server-common.d.ts +27 -0
- package/dist/transport/server-common.d.ts.map +1 -0
- package/dist/transport/server-common.js +27 -0
- package/dist/transport/server-common.js.map +1 -0
- package/dist/transport/tcp-server.d.ts +26 -0
- package/dist/transport/tcp-server.d.ts.map +1 -0
- package/dist/transport/tcp-server.js +91 -0
- package/dist/transport/tcp-server.js.map +1 -0
- package/dist/transport/ws-server.d.ts +19 -0
- package/dist/transport/ws-server.d.ts.map +1 -0
- package/dist/transport/ws-server.js +78 -0
- package/dist/transport/ws-server.js.map +1 -0
- package/dist/update-publisher.d.ts +34 -0
- package/dist/update-publisher.d.ts.map +1 -0
- package/dist/update-publisher.js +29 -0
- package/dist/update-publisher.js.map +1 -0
- package/dist/updates/mongo-update-log.d.ts +13 -0
- package/dist/updates/mongo-update-log.d.ts.map +1 -0
- package/dist/updates/mongo-update-log.js +39 -0
- package/dist/updates/mongo-update-log.js.map +1 -0
- package/dist/updates/presence-binder.d.ts +29 -0
- package/dist/updates/presence-binder.d.ts.map +1 -0
- package/dist/updates/presence-binder.js +36 -0
- package/dist/updates/presence-binder.js.map +1 -0
- package/dist/updates/presence.d.ts +31 -0
- package/dist/updates/presence.d.ts.map +1 -0
- package/dist/updates/presence.js +44 -0
- package/dist/updates/presence.js.map +1 -0
- package/dist/updates/push.d.ts +25 -0
- package/dist/updates/push.d.ts.map +1 -0
- package/dist/updates/push.js +72 -0
- package/dist/updates/push.js.map +1 -0
- package/dist/updates/redis-bus.d.ts +45 -0
- package/dist/updates/redis-bus.d.ts.map +1 -0
- package/dist/updates/redis-bus.js +59 -0
- package/dist/updates/redis-bus.js.map +1 -0
- package/dist/updates/redis-presence.d.ts +43 -0
- package/dist/updates/redis-presence.d.ts.map +1 -0
- package/dist/updates/redis-presence.js +65 -0
- package/dist/updates/redis-presence.js.map +1 -0
- package/dist/updates/render.d.ts +16 -0
- package/dist/updates/render.d.ts.map +1 -0
- package/dist/updates/render.js +46 -0
- package/dist/updates/render.js.map +1 -0
- package/dist/updates/router.d.ts +27 -0
- package/dist/updates/router.d.ts.map +1 -0
- package/dist/updates/router.js +36 -0
- package/dist/updates/router.js.map +1 -0
- package/dist/updates/types.d.ts +23 -0
- package/dist/updates/types.d.ts.map +1 -0
- package/dist/updates/types.js +2 -0
- package/dist/updates/types.js.map +1 -0
- package/dist/updates/update-bus.d.ts +29 -0
- package/dist/updates/update-bus.d.ts.map +1 -0
- package/dist/updates/update-bus.js +24 -0
- package/dist/updates/update-bus.js.map +1 -0
- package/dist/util/bytes.d.ts +12 -0
- package/dist/util/bytes.d.ts.map +1 -0
- package/dist/util/bytes.js +46 -0
- package/dist/util/bytes.js.map +1 -0
- package/package.json +84 -0
- package/src/auth/handshake.ts +262 -0
- package/src/auth/nonce-store.ts +39 -0
- package/src/bootstrap.ts +114 -0
- package/src/config.ts +103 -0
- package/src/core/context.ts +94 -0
- package/src/core/errors.ts +52 -0
- package/src/core/index.ts +4 -0
- package/src/core/rpc.ts +165 -0
- package/src/core/updates.ts +69 -0
- package/src/create-server.ts +181 -0
- package/src/crypto/aes-ige.ts +57 -0
- package/src/crypto/dh.ts +101 -0
- package/src/crypto/hashes.ts +17 -0
- package/src/crypto/msg-key.ts +29 -0
- package/src/crypto/rsa.ts +70 -0
- package/src/dispatch/dispatcher.ts +586 -0
- package/src/dispatch/forwarders/in-process.ts +14 -0
- package/src/dispatch/forwarders/print.ts +22 -0
- package/src/dispatch/rpc-forwarder.ts +15 -0
- package/src/dispatch/types.ts +60 -0
- package/src/gateway.ts +214 -0
- package/src/index.ts +53 -0
- package/src/lib.ts +24 -0
- package/src/server/message-pipeline.ts +256 -0
- package/src/session/inbound-tracker.ts +221 -0
- package/src/session/message-id.ts +43 -0
- package/src/session/salts.ts +162 -0
- package/src/session/session-manager.ts +66 -0
- package/src/storage/index.ts +26 -0
- package/src/storage/memory.ts +101 -0
- package/src/storage/mongo.ts +215 -0
- package/src/storage/types.ts +92 -0
- package/src/testkit.ts +19 -0
- package/src/tl/codec.ts +292 -0
- package/src/tl/layered-registry.ts +132 -0
- package/src/tl/protocol.ts +146 -0
- package/src/tl/reader.ts +99 -0
- package/src/tl/registry.ts +78 -0
- package/src/tl/writer.ts +104 -0
- package/src/transport/connection-registry.ts +91 -0
- package/src/transport/connection.ts +113 -0
- package/src/transport/framing.ts +223 -0
- package/src/transport/proxy-protocol.ts +109 -0
- package/src/transport/server-common.ts +49 -0
- package/src/transport/tcp-server.ts +102 -0
- package/src/transport/ws-server.ts +94 -0
- package/src/update-publisher.ts +47 -0
- package/src/updates/mongo-update-log.ts +49 -0
- package/src/updates/presence-binder.ts +51 -0
- package/src/updates/presence.ts +61 -0
- package/src/updates/push.ts +90 -0
- package/src/updates/redis-bus.ts +86 -0
- package/src/updates/redis-presence.ts +87 -0
- package/src/updates/render.ts +53 -0
- package/src/updates/router.ts +52 -0
- package/src/updates/types.ts +24 -0
- package/src/updates/update-bus.ts +49 -0
- package/src/util/bytes.ts +49 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { noopLogger } from '@mt-tl/tl';
|
|
3
|
+
import { Connection } from './connection.js';
|
|
4
|
+
import { pump } from './server-common.js';
|
|
5
|
+
function toBuffer(data) {
|
|
6
|
+
if (Buffer.isBuffer(data))
|
|
7
|
+
return data;
|
|
8
|
+
if (Array.isArray(data))
|
|
9
|
+
return Buffer.concat(data);
|
|
10
|
+
return Buffer.from(data);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* WebSocket carrier for MTProto. Each binary frame is fed into the connection's
|
|
14
|
+
* transport framing; complete packets are handed to `onPacket` in arrival order.
|
|
15
|
+
*/
|
|
16
|
+
export class MtprotoWsServer {
|
|
17
|
+
options;
|
|
18
|
+
handlers;
|
|
19
|
+
wss;
|
|
20
|
+
lastId = 0;
|
|
21
|
+
constructor(options, handlers) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.handlers = handlers;
|
|
24
|
+
}
|
|
25
|
+
listen() {
|
|
26
|
+
return new Promise(resolve => {
|
|
27
|
+
this.wss = new WebSocketServer({ port: this.options.port, clientTracking: false });
|
|
28
|
+
this.wss.on('connection', (socket, req) => this.onConnection(socket, req));
|
|
29
|
+
this.wss.on('listening', () => resolve());
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Bound TCP port (useful when listening on port 0 in tests). */
|
|
33
|
+
get port() {
|
|
34
|
+
const addr = this.wss?.address();
|
|
35
|
+
return addr && typeof addr === 'object' ? addr.port : this.options.port;
|
|
36
|
+
}
|
|
37
|
+
onConnection(socket, req) {
|
|
38
|
+
const id = ++this.lastId;
|
|
39
|
+
// Only trust the (spoofable) X-Forwarded-For when an upstream proxy is declared.
|
|
40
|
+
const forwarded = this.options.trustProxy
|
|
41
|
+
? req.headers['x-forwarded-for']?.split(',')[0]?.trim()
|
|
42
|
+
: undefined;
|
|
43
|
+
const remote = forwarded ?? req.socket.remoteAddress ?? undefined;
|
|
44
|
+
const log = (this.options.logger ?? noopLogger).child({ scope: 'ws', conn: id });
|
|
45
|
+
log.info('conn.open', { remote });
|
|
46
|
+
if (log.isLevelEnabled('trace')) {
|
|
47
|
+
log.trace('ws.connect', {
|
|
48
|
+
host: req.headers.host,
|
|
49
|
+
xff: req.headers['x-forwarded-for'],
|
|
50
|
+
proto: req.headers['sec-websocket-protocol'],
|
|
51
|
+
ua: req.headers['user-agent'],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const conn = new Connection(id, bytes => socket.send(bytes, { binary: true }), () => socket.close(), remote, this.options.defaultLayer, log);
|
|
55
|
+
this.handlers.onConnect?.(conn);
|
|
56
|
+
socket.on('message', (data) => {
|
|
57
|
+
const buf = toBuffer(data);
|
|
58
|
+
if (log.isLevelEnabled('trace'))
|
|
59
|
+
log.trace('ws.recv', { bytes: buf.length, head: buf.subarray(0, 16).toString('hex') });
|
|
60
|
+
pump(conn, buf, this.handlers);
|
|
61
|
+
});
|
|
62
|
+
socket.on('close', (code, reason) => {
|
|
63
|
+
log.info('conn.close', { code });
|
|
64
|
+
if (reason?.length && log.isLevelEnabled('trace'))
|
|
65
|
+
log.trace('ws.close', { reason: reason.toString() });
|
|
66
|
+
conn.closed = true;
|
|
67
|
+
this.handlers.onClose?.(conn);
|
|
68
|
+
});
|
|
69
|
+
socket.on('error', err => {
|
|
70
|
+
log.warn('ws.error', { err: String(err) });
|
|
71
|
+
conn.close();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
close() {
|
|
75
|
+
this.wss?.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=ws-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-server.js","sourceRoot":"","sources":["../../src/transport/ws-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** A standalone publisher returned by {@link createUpdatePublisher}. */
|
|
2
|
+
export interface UpdatePublisher {
|
|
3
|
+
/** Push a TL update (`{ _: name, ... }`) to a `subject` (internal user id) —
|
|
4
|
+
* delivered to whatever node holds them. */
|
|
5
|
+
push(subject: string, update: unknown): Promise<void>;
|
|
6
|
+
/** Push to a specific auth key — including an anonymous connection (no pts). */
|
|
7
|
+
pushToAuthKey(authKeyId: string, update: unknown): Promise<void>;
|
|
8
|
+
/** Disconnect from the shared bus. */
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
/** Options for {@link createUpdatePublisher}. */
|
|
12
|
+
export interface UpdatePublisherConfig {
|
|
13
|
+
/** Redis URL of the shared pub/sub update bus (the same `REDIS_URL` your servers use). */
|
|
14
|
+
redisUrl: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Creates a server-push publisher for code running **outside** the server — a
|
|
18
|
+
* webhook receiver, a cron worker, another microservice. It drops the update on
|
|
19
|
+
* the shared Redis bus; the server fleet's router looks up presence and delivers
|
|
20
|
+
* it to whichever node holds the user (rendered for that client's layer). No
|
|
21
|
+
* client connection and no running server are needed — only the shared bus.
|
|
22
|
+
*
|
|
23
|
+
* Inside a handler, push with `ctx.push(subject, update)` instead — this is for
|
|
24
|
+
* cross-process pushes, which is why it needs the shared `redisUrl`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const updates = await createUpdatePublisher({ redisUrl: process.env.REDIS_URL! })
|
|
29
|
+
* await updates.push(subject, { _: 'updateNewMessage', message })
|
|
30
|
+
* await updates.close()
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function createUpdatePublisher(config: UpdatePublisherConfig): Promise<UpdatePublisher>;
|
|
34
|
+
//# sourceMappingURL=update-publisher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-publisher.d.ts","sourceRoot":"","sources":["../src/update-publisher.ts"],"names":[],"mappings":"AAGA,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC5B;gDAC4C;IAC5C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,gFAAgF;IAChF,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,sCAAsC;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,iDAAiD;AACjD,MAAM,WAAW,qBAAqB;IAClC,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC,CASnG"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRedisUpdateBus } from './lib.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a server-push publisher for code running **outside** the server — a
|
|
4
|
+
* webhook receiver, a cron worker, another microservice. It drops the update on
|
|
5
|
+
* the shared Redis bus; the server fleet's router looks up presence and delivers
|
|
6
|
+
* it to whichever node holds the user (rendered for that client's layer). No
|
|
7
|
+
* client connection and no running server are needed — only the shared bus.
|
|
8
|
+
*
|
|
9
|
+
* Inside a handler, push with `ctx.push(subject, update)` instead — this is for
|
|
10
|
+
* cross-process pushes, which is why it needs the shared `redisUrl`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const updates = await createUpdatePublisher({ redisUrl: process.env.REDIS_URL! })
|
|
15
|
+
* await updates.push(subject, { _: 'updateNewMessage', message })
|
|
16
|
+
* await updates.close()
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export async function createUpdatePublisher(config) {
|
|
20
|
+
if (!config.redisUrl)
|
|
21
|
+
throw new Error('createUpdatePublisher requires redisUrl');
|
|
22
|
+
const handle = await createRedisUpdateBus(config.redisUrl);
|
|
23
|
+
return {
|
|
24
|
+
push: (subject, update) => handle.bus.publishUpdate({ subject, update: update }),
|
|
25
|
+
pushToAuthKey: (authKeyId, update) => handle.bus.publishUpdate({ authKeyId, update: update }),
|
|
26
|
+
close: () => handle.close(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=update-publisher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-publisher.js","sourceRoot":"","sources":["../src/update-publisher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAoB/C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,MAA6B;IACrE,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;IAChF,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC1D,OAAO;QACH,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAmB,EAAE,CAAC;QAC7F,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CACjC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAmB,EAAE,CAAC;QACxE,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;KAC9B,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { UpdateLog } from '../core/updates.js';
|
|
2
|
+
/**
|
|
3
|
+
* Durable {@link UpdateLog} on MongoDB, for protocol-managed updates
|
|
4
|
+
* (`config.updates.managed`). `pts` is assigned atomically per subject via a
|
|
5
|
+
* counters document (`$inc`), so concurrent replicas never collide; each update
|
|
6
|
+
* is stored with its pts for `updates.getDifference`. Opens its own connection
|
|
7
|
+
* (the updates box is logically separate from auth/salt/session storage).
|
|
8
|
+
*/
|
|
9
|
+
export declare function createMongoUpdateLog(mongoUrl: string, dbName: string): Promise<{
|
|
10
|
+
log: UpdateLog;
|
|
11
|
+
close: () => Promise<void>;
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=mongo-update-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongo-update-log.d.ts","sourceRoot":"","sources":["../../src/updates/mongo-update-log.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACtC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,GAAG,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAmCzD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable {@link UpdateLog} on MongoDB, for protocol-managed updates
|
|
3
|
+
* (`config.updates.managed`). `pts` is assigned atomically per subject via a
|
|
4
|
+
* counters document (`$inc`), so concurrent replicas never collide; each update
|
|
5
|
+
* is stored with its pts for `updates.getDifference`. Opens its own connection
|
|
6
|
+
* (the updates box is logically separate from auth/salt/session storage).
|
|
7
|
+
*/
|
|
8
|
+
export async function createMongoUpdateLog(mongoUrl, dbName) {
|
|
9
|
+
const { MongoClient } = await import('mongodb');
|
|
10
|
+
const client = new MongoClient(mongoUrl);
|
|
11
|
+
await client.connect();
|
|
12
|
+
const db = client.db(dbName);
|
|
13
|
+
const counters = db.collection('update_counters');
|
|
14
|
+
const log = db.collection('updates');
|
|
15
|
+
await log.createIndex({ subject: 1, pts: 1 });
|
|
16
|
+
return {
|
|
17
|
+
log: {
|
|
18
|
+
async append(subject, update) {
|
|
19
|
+
const doc = await counters.findOneAndUpdate({ _id: subject }, { $inc: { pts: 1 } }, { upsert: true, returnDocument: 'after' });
|
|
20
|
+
const pts = doc?.pts ?? 1;
|
|
21
|
+
await log.insertOne({ subject, pts, update });
|
|
22
|
+
return { pts };
|
|
23
|
+
},
|
|
24
|
+
async since(subject, sincePts) {
|
|
25
|
+
const docs = await log
|
|
26
|
+
.find({ subject, pts: { $gt: sincePts } })
|
|
27
|
+
.sort({ pts: 1 })
|
|
28
|
+
.toArray();
|
|
29
|
+
return docs.map(d => ({ pts: d.pts, update: d.update }));
|
|
30
|
+
},
|
|
31
|
+
async currentPts(subject) {
|
|
32
|
+
const doc = await counters.findOne({ _id: subject });
|
|
33
|
+
return doc?.pts ?? 0;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
close: () => client.close(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=mongo-update-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongo-update-log.js","sourceRoot":"","sources":["../../src/updates/mongo-update-log.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACtC,QAAgB,EAChB,MAAc;IAEd,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxC,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;IACtB,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAA;IAC5B,MAAM,QAAQ,GAAG,EAAE,CAAC,UAAU,CAA+B,iBAAiB,CAAC,CAAA;IAC/E,MAAM,GAAG,GAAG,EAAE,CAAC,UAAU,CAAsD,SAAS,CAAC,CAAA;IACzF,MAAM,GAAG,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;IAE7C,OAAO;QACH,GAAG,EAAE;YACD,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM;gBACxB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,gBAAgB,CACvC,EAAE,GAAG,EAAE,OAAO,EAAE,EAChB,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EACpB,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,CAC5C,CAAA;gBACD,MAAM,GAAG,GAAG,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;gBACzB,MAAM,GAAG,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;gBAC7C,OAAO,EAAE,GAAG,EAAE,CAAA;YAClB,CAAC;YACD,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,QAAQ;gBACzB,MAAM,IAAI,GAAG,MAAM,GAAG;qBACjB,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC;qBACzC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;qBAChB,OAAO,EAAE,CAAA;gBACd,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;YAC5D,CAAC;YACD,KAAK,CAAC,UAAU,CAAC,OAAO;gBACpB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;gBACpD,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;YACxB,CAAC;SACJ;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;KAC9B,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Connection } from '../transport/connection.js';
|
|
2
|
+
import type { ConnectionRegistry } from '../transport/connection-registry.js';
|
|
3
|
+
import type { Presence } from './presence.js';
|
|
4
|
+
/**
|
|
5
|
+
* Couples a connection's authorized subject to the local registry and the global
|
|
6
|
+
* presence map. The pipeline binds on authenticated messages; the dispatcher
|
|
7
|
+
* binds/unbinds on bindUser/unbindUser effects; the carriers unbind on close.
|
|
8
|
+
*/
|
|
9
|
+
export interface PresenceBinder {
|
|
10
|
+
bind(conn: Connection, subject: string): void;
|
|
11
|
+
/** Register a connection by its auth key (enables push to anonymous connections). */
|
|
12
|
+
bindAuthKey(conn: Connection, authKeyId: string): void;
|
|
13
|
+
unbind(conn: Connection): void;
|
|
14
|
+
}
|
|
15
|
+
export declare class NoopPresenceBinder implements PresenceBinder {
|
|
16
|
+
bind(): void;
|
|
17
|
+
bindAuthKey(): void;
|
|
18
|
+
unbind(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class NodePresenceBinder implements PresenceBinder {
|
|
21
|
+
private readonly nodeId;
|
|
22
|
+
private readonly registry;
|
|
23
|
+
private readonly presence;
|
|
24
|
+
constructor(nodeId: string, registry: ConnectionRegistry, presence: Presence);
|
|
25
|
+
bind(conn: Connection, subject: string): void;
|
|
26
|
+
bindAuthKey(conn: Connection, authKeyId: string): void;
|
|
27
|
+
unbind(conn: Connection): void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=presence-binder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-binder.d.ts","sourceRoot":"","sources":["../../src/updates/presence-binder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAC5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAA;AAC7E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE7C;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC3B,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7C,qFAAqF;IACrF,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACtD,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;CACjC;AAED,qBAAa,kBAAmB,YAAW,cAAc;IACrD,IAAI,IAAI,IAAI;IACZ,WAAW,IAAI,IAAI;IACnB,MAAM,IAAI,IAAI;CACjB;AAED,qBAAa,kBAAmB,YAAW,cAAc;IAEjD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAFR,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,kBAAkB,EAC5B,QAAQ,EAAE,QAAQ;IAGvC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAM7C,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKtD,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;CAUjC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class NoopPresenceBinder {
|
|
2
|
+
bind() { }
|
|
3
|
+
bindAuthKey() { }
|
|
4
|
+
unbind() { }
|
|
5
|
+
}
|
|
6
|
+
export class NodePresenceBinder {
|
|
7
|
+
nodeId;
|
|
8
|
+
registry;
|
|
9
|
+
presence;
|
|
10
|
+
constructor(nodeId, registry, presence) {
|
|
11
|
+
this.nodeId = nodeId;
|
|
12
|
+
this.registry = registry;
|
|
13
|
+
this.presence = presence;
|
|
14
|
+
}
|
|
15
|
+
bind(conn, subject) {
|
|
16
|
+
if (this.registry.subjectOf(conn) === subject)
|
|
17
|
+
return; // already bound — cheap no-op
|
|
18
|
+
this.registry.register(subject, conn);
|
|
19
|
+
void this.presence.add(subject, this.nodeId).catch(() => { });
|
|
20
|
+
}
|
|
21
|
+
bindAuthKey(conn, authKeyId) {
|
|
22
|
+
this.registry.registerAuthKey(authKeyId, conn); // no-op if already registered
|
|
23
|
+
void this.presence.addAuthKey(authKeyId, this.nodeId).catch(() => { });
|
|
24
|
+
}
|
|
25
|
+
unbind(conn) {
|
|
26
|
+
const { subject, authKeyId } = this.registry.unregister(conn);
|
|
27
|
+
// Only drop this node's presence once no local connection remains for the key.
|
|
28
|
+
if (subject !== undefined && !this.registry.hasSubject(subject)) {
|
|
29
|
+
void this.presence.remove(subject, this.nodeId).catch(() => { });
|
|
30
|
+
}
|
|
31
|
+
if (authKeyId !== undefined && !this.registry.hasAuthKey(authKeyId)) {
|
|
32
|
+
void this.presence.removeAuthKey(authKeyId, this.nodeId).catch(() => { });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=presence-binder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-binder.js","sourceRoot":"","sources":["../../src/updates/presence-binder.ts"],"names":[],"mappings":"AAgBA,MAAM,OAAO,kBAAkB;IAC3B,IAAI,KAAU,CAAC;IACf,WAAW,KAAU,CAAC;IACtB,MAAM,KAAU,CAAC;CACpB;AAED,MAAM,OAAO,kBAAkB;IAEN;IACA;IACA;IAHrB,YACqB,MAAc,EACd,QAA4B,EAC5B,QAAkB;QAFlB,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAoB;QAC5B,aAAQ,GAAR,QAAQ,CAAU;IACpC,CAAC;IAEJ,IAAI,CAAC,IAAgB,EAAE,OAAe;QAClC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,OAAO;YAAE,OAAM,CAAC,8BAA8B;QACpF,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;QACrC,KAAK,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IAChE,CAAC;IAED,WAAW,CAAC,IAAgB,EAAE,SAAiB;QAC3C,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA,CAAC,8BAA8B;QAC7E,KAAK,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACzE,CAAC;IAED,MAAM,CAAC,IAAgB;QACnB,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QAC7D,+EAA+E;QAC/E,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9D,KAAK,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QACnE,CAAC;QACD,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAClE,KAAK,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QAC5E,CAAC;IACL,CAAC;CACJ"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global online-presence: which gateway node(s) currently hold a connection for
|
|
3
|
+
* a subject. Written by gateways, read by the Update Router to route updates only
|
|
4
|
+
* to nodes that actually hold the subject (no broadcast fan-out).
|
|
5
|
+
*
|
|
6
|
+
* Eventually-consistent is fine — a stale entry just routes to a node that drops
|
|
7
|
+
* the update, and the client recovers via pts/getDifference.
|
|
8
|
+
*/
|
|
9
|
+
export interface Presence {
|
|
10
|
+
add(subject: string, nodeId: string): Promise<void>;
|
|
11
|
+
remove(subject: string, nodeId: string): Promise<void>;
|
|
12
|
+
lookup(subject: string): Promise<string[]>;
|
|
13
|
+
/** Presence for a specific auth key (anonymous-capable delivery target). */
|
|
14
|
+
addAuthKey(authKeyId: string, nodeId: string): Promise<void>;
|
|
15
|
+
removeAuthKey(authKeyId: string, nodeId: string): Promise<void>;
|
|
16
|
+
lookupAuthKey(authKeyId: string): Promise<string[]>;
|
|
17
|
+
}
|
|
18
|
+
/** In-memory presence (single-process / tests). Use a Redis impl across nodes. */
|
|
19
|
+
export declare class InMemoryPresence implements Presence {
|
|
20
|
+
private map;
|
|
21
|
+
private addKey;
|
|
22
|
+
private removeKey;
|
|
23
|
+
private lookupKey;
|
|
24
|
+
add(subject: string, nodeId: string): Promise<void>;
|
|
25
|
+
remove(subject: string, nodeId: string): Promise<void>;
|
|
26
|
+
lookup(subject: string): Promise<string[]>;
|
|
27
|
+
addAuthKey(authKeyId: string, nodeId: string): Promise<void>;
|
|
28
|
+
removeAuthKey(authKeyId: string, nodeId: string): Promise<void>;
|
|
29
|
+
lookupAuthKey(authKeyId: string): Promise<string[]>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=presence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/updates/presence.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACrB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC1C,4EAA4E;IAC5E,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CACtD;AAED,kFAAkF;AAClF,qBAAa,gBAAiB,YAAW,QAAQ;IAE7C,OAAO,CAAC,GAAG,CAAiC;IAE5C,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,SAAS;IAKX,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAGnD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAGtD,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAG1C,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG5D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG/D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAG5D"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** In-memory presence (single-process / tests). Use a Redis impl across nodes. */
|
|
2
|
+
export class InMemoryPresence {
|
|
3
|
+
// Keyed by a prefixed target string: `u:<subject>` or `a:<authKeyId>`.
|
|
4
|
+
map = new Map();
|
|
5
|
+
addKey(key, nodeId) {
|
|
6
|
+
let set = this.map.get(key);
|
|
7
|
+
if (!set) {
|
|
8
|
+
set = new Set();
|
|
9
|
+
this.map.set(key, set);
|
|
10
|
+
}
|
|
11
|
+
set.add(nodeId);
|
|
12
|
+
}
|
|
13
|
+
removeKey(key, nodeId) {
|
|
14
|
+
const set = this.map.get(key);
|
|
15
|
+
if (!set)
|
|
16
|
+
return;
|
|
17
|
+
set.delete(nodeId);
|
|
18
|
+
if (set.size === 0)
|
|
19
|
+
this.map.delete(key);
|
|
20
|
+
}
|
|
21
|
+
lookupKey(key) {
|
|
22
|
+
const set = this.map.get(key);
|
|
23
|
+
return set ? [...set] : [];
|
|
24
|
+
}
|
|
25
|
+
async add(subject, nodeId) {
|
|
26
|
+
this.addKey(`u:${subject}`, nodeId);
|
|
27
|
+
}
|
|
28
|
+
async remove(subject, nodeId) {
|
|
29
|
+
this.removeKey(`u:${subject}`, nodeId);
|
|
30
|
+
}
|
|
31
|
+
async lookup(subject) {
|
|
32
|
+
return this.lookupKey(`u:${subject}`);
|
|
33
|
+
}
|
|
34
|
+
async addAuthKey(authKeyId, nodeId) {
|
|
35
|
+
this.addKey(`a:${authKeyId}`, nodeId);
|
|
36
|
+
}
|
|
37
|
+
async removeAuthKey(authKeyId, nodeId) {
|
|
38
|
+
this.removeKey(`a:${authKeyId}`, nodeId);
|
|
39
|
+
}
|
|
40
|
+
async lookupAuthKey(authKeyId) {
|
|
41
|
+
return this.lookupKey(`a:${authKeyId}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=presence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/updates/presence.ts"],"names":[],"mappings":"AAkBA,kFAAkF;AAClF,MAAM,OAAO,gBAAgB;IACzB,uEAAuE;IAC/D,GAAG,GAAG,IAAI,GAAG,EAAuB,CAAA;IAEpC,MAAM,CAAC,GAAW,EAAE,MAAc;QACtC,IAAI,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC3B,IAAI,CAAC,GAAG,EAAE,CAAC;YACP,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;YACf,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QAC1B,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACnB,CAAC;IACO,SAAS,CAAC,GAAW,EAAE,MAAc;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAClB,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC5C,CAAC;IACO,SAAS,CAAC,GAAW;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,OAAe,EAAE,MAAc;QACrC,IAAI,CAAC,MAAM,CAAC,KAAK,OAAO,EAAE,EAAE,MAAM,CAAC,CAAA;IACvC,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,OAAe,EAAE,MAAc;QACxC,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,EAAE,EAAE,MAAM,CAAC,CAAA;IAC1C,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,OAAe;QACxB,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,OAAO,EAAE,CAAC,CAAA;IACzC,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,MAAc;QAC9C,IAAI,CAAC,MAAM,CAAC,KAAK,SAAS,EAAE,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,SAAiB,EAAE,MAAc;QACjD,IAAI,CAAC,SAAS,CAAC,KAAK,SAAS,EAAE,EAAE,MAAM,CAAC,CAAA;IAC5C,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,SAAiB;QACjC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,SAAS,EAAE,CAAC,CAAA;IAC3C,CAAC;CACJ"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MigrationRegistry, type JsonValue, type Logger } from '@mt-tl/tl';
|
|
2
|
+
import type { ConnectionRegistry } from '../transport/connection-registry.js';
|
|
3
|
+
import type { Responder } from '../dispatch/types.js';
|
|
4
|
+
import type { LayeredRegistry } from '../tl/layered-registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* Node-side delivery of a routed update to the user's local connections, as an
|
|
7
|
+
* encrypted server notification (msg_id % 4 == 3). Best-effort: if the user has
|
|
8
|
+
* no local connection, the update is dropped (the client recovers via pts).
|
|
9
|
+
*
|
|
10
|
+
* Each connection is rendered for its own negotiated layer: types not
|
|
11
|
+
* representable there become `updateUnsupported` (pts-bearing) or are dropped.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PushService {
|
|
14
|
+
private readonly registry;
|
|
15
|
+
private readonly responder;
|
|
16
|
+
private readonly layered?;
|
|
17
|
+
private readonly migrations;
|
|
18
|
+
private readonly log;
|
|
19
|
+
constructor(registry: ConnectionRegistry, responder: Responder, layered?: LayeredRegistry | undefined, migrations?: MigrationRegistry, logger?: Logger);
|
|
20
|
+
deliver(subject: string, update: JsonValue): void;
|
|
21
|
+
/** Deliver to the connections of a specific auth key (anonymous-capable target). */
|
|
22
|
+
deliverToAuthKey(authKeyId: string, update: JsonValue): void;
|
|
23
|
+
private deliverTo;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=push.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/updates/push.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,iBAAiB,EAEjB,KAAK,SAAS,EACd,KAAK,MAAM,EAEd,MAAM,WAAW,CAAA;AAElB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAA;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAGhE;;;;;;;GAOG;AACH,qBAAa,WAAW;IAKhB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAN7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAGP,QAAQ,EAAE,kBAAkB,EAC5B,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,eAAe,YAAA,EAC1C,UAAU,CAAC,EAAE,iBAAiB,EAC9B,MAAM,CAAC,EAAE,MAAM;IAMnB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI;IAIjD,oFAAoF;IACpF,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI;IAI5D,OAAO,CAAC,SAAS;CA2CpB"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { fromJson, MigrationRegistry, noopLogger, } from '@mt-tl/tl';
|
|
2
|
+
import { renderUpdateForLayer } from './render.js';
|
|
3
|
+
/**
|
|
4
|
+
* Node-side delivery of a routed update to the user's local connections, as an
|
|
5
|
+
* encrypted server notification (msg_id % 4 == 3). Best-effort: if the user has
|
|
6
|
+
* no local connection, the update is dropped (the client recovers via pts).
|
|
7
|
+
*
|
|
8
|
+
* Each connection is rendered for its own negotiated layer: types not
|
|
9
|
+
* representable there become `updateUnsupported` (pts-bearing) or are dropped.
|
|
10
|
+
*/
|
|
11
|
+
export class PushService {
|
|
12
|
+
registry;
|
|
13
|
+
responder;
|
|
14
|
+
layered;
|
|
15
|
+
migrations;
|
|
16
|
+
log;
|
|
17
|
+
constructor(registry, responder, layered, migrations, logger) {
|
|
18
|
+
this.registry = registry;
|
|
19
|
+
this.responder = responder;
|
|
20
|
+
this.layered = layered;
|
|
21
|
+
this.migrations = migrations ?? new MigrationRegistry();
|
|
22
|
+
this.log = logger ?? noopLogger;
|
|
23
|
+
}
|
|
24
|
+
deliver(subject, update) {
|
|
25
|
+
this.deliverTo(this.registry.getBySubject(subject), update, { subject });
|
|
26
|
+
}
|
|
27
|
+
/** Deliver to the connections of a specific auth key (anonymous-capable target). */
|
|
28
|
+
deliverToAuthKey(authKeyId, update) {
|
|
29
|
+
this.deliverTo(this.registry.getByAuthKey(authKeyId), update, { authKeyId });
|
|
30
|
+
}
|
|
31
|
+
deliverTo(conns, update, target) {
|
|
32
|
+
const tl = fromJson(update);
|
|
33
|
+
if (!tl || typeof tl !== 'object' || Array.isArray(tl) || !('_' in tl))
|
|
34
|
+
return;
|
|
35
|
+
const base = tl;
|
|
36
|
+
const type = base._;
|
|
37
|
+
// Full update payload at debug — "what we're pushing out" (the canonical
|
|
38
|
+
// form, before per-connection layer rendering).
|
|
39
|
+
this.log.debug('update.data', { ...target, type, update });
|
|
40
|
+
// No local connection for the target — best-effort drop (the client recovers
|
|
41
|
+
// via pts on its next getDifference). Expected, so debug not error.
|
|
42
|
+
if (conns.length === 0) {
|
|
43
|
+
this.log.debug('update.nodest', { ...target, type });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let delivered = 0;
|
|
47
|
+
for (const conn of conns) {
|
|
48
|
+
// The client opted out of updates on this connection (invokeWithoutUpdates).
|
|
49
|
+
if (conn.ctx.noUpdates)
|
|
50
|
+
continue;
|
|
51
|
+
// Render canonical → client layer (non-additive), then per-layer representability.
|
|
52
|
+
let body = this.migrations.down(base, conn.ctx.apiLayer);
|
|
53
|
+
if (this.layered?.hasLayers()) {
|
|
54
|
+
body = renderUpdateForLayer(body, conn.ctx.apiLayer, this.layered);
|
|
55
|
+
}
|
|
56
|
+
if (!body) {
|
|
57
|
+
this.log.debug('update.skip', { ...target, type, conn: conn.id, layer: conn.ctx.apiLayer });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
this.responder.sendEncrypted(conn, body, { isNotification: true, contentRelated: false });
|
|
62
|
+
delivered++;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
this.log.error('update.fail', { ...target, type, conn: conn.id, err });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (delivered)
|
|
69
|
+
this.log.info('update.push', { ...target, type, conns: delivered });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../src/updates/push.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NodeDelivery, UpdateMessage } from './types.js';
|
|
2
|
+
import type { UpdateBus } from './update-bus.js';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Redis surfaces (for testability). Pub/sub needs TWO connections: a
|
|
5
|
+
* connection in subscriber mode cannot issue regular commands, so publishing and
|
|
6
|
+
* subscribing use separate clients.
|
|
7
|
+
*/
|
|
8
|
+
export interface RedisPubLike {
|
|
9
|
+
publish(channel: string, message: string): Promise<unknown> | unknown;
|
|
10
|
+
quit(): Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface RedisSubLike {
|
|
13
|
+
subscribe(channel: string): Promise<unknown> | unknown;
|
|
14
|
+
on(event: 'message', listener: (channel: string, message: string) => void): unknown;
|
|
15
|
+
quit(): Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Redis pub/sub {@link UpdateBus} for multi-instance server-push (infra stays
|
|
19
|
+
* Mongo + Redis). `updates.in` carries emitted updates to the router;
|
|
20
|
+
* `updates.node.{id}` carries routed deliveries to a node.
|
|
21
|
+
*
|
|
22
|
+
* Caveat: Redis pub/sub is **fan-out**, not a work queue. Per-node channels are
|
|
23
|
+
* fine (one subscriber per nodeId). But `updates.in` is delivered to *every*
|
|
24
|
+
* subscriber — run a SINGLE router with this bus (enough for the in-process-first
|
|
25
|
+
* model), or shard `updates.in` by subject across routers. For competing-consumer
|
|
26
|
+
* semantics at very large scale, switch the bus to Redis Streams.
|
|
27
|
+
*/
|
|
28
|
+
export declare class RedisUpdateBus implements UpdateBus {
|
|
29
|
+
private readonly pub;
|
|
30
|
+
private readonly sub;
|
|
31
|
+
private readonly handlers;
|
|
32
|
+
constructor(pub: RedisPubLike, sub: RedisSubLike);
|
|
33
|
+
publishUpdate(msg: UpdateMessage): Promise<void>;
|
|
34
|
+
subscribeUpdates(handler: (msg: UpdateMessage) => void): void;
|
|
35
|
+
publishToNode(nodeId: string, msg: NodeDelivery): Promise<void>;
|
|
36
|
+
subscribeNode(nodeId: string, handler: (msg: NodeDelivery) => void): void;
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export interface RedisBusHandle {
|
|
40
|
+
bus: RedisUpdateBus;
|
|
41
|
+
close: () => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** Connects two Redis clients (publish + subscribe) and builds a {@link RedisUpdateBus}. */
|
|
44
|
+
export declare function createRedisUpdateBus(url: string): Promise<RedisBusHandle>;
|
|
45
|
+
//# sourceMappingURL=redis-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-bus.d.ts","sourceRoot":"","sources":["../../src/updates/redis-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IACzB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IACrE,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAC3B;AACD,MAAM,WAAW,YAAY;IACzB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IACtD,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAA;IACnF,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAC3B;AAKD;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,YAAW,SAAS;IAIxC,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAJxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;gBAGhD,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,YAAY;IAahC,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD,gBAAgB,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI;IAKvD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrE,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI;IAKnE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,cAAc,CAAA;IACnB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAED,4FAA4F;AAC5F,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAM/E"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const CH_IN = 'updates.in';
|
|
2
|
+
const nodeChannel = (nodeId) => `updates.node.${nodeId}`;
|
|
3
|
+
/**
|
|
4
|
+
* Redis pub/sub {@link UpdateBus} for multi-instance server-push (infra stays
|
|
5
|
+
* Mongo + Redis). `updates.in` carries emitted updates to the router;
|
|
6
|
+
* `updates.node.{id}` carries routed deliveries to a node.
|
|
7
|
+
*
|
|
8
|
+
* Caveat: Redis pub/sub is **fan-out**, not a work queue. Per-node channels are
|
|
9
|
+
* fine (one subscriber per nodeId). But `updates.in` is delivered to *every*
|
|
10
|
+
* subscriber — run a SINGLE router with this bus (enough for the in-process-first
|
|
11
|
+
* model), or shard `updates.in` by subject across routers. For competing-consumer
|
|
12
|
+
* semantics at very large scale, switch the bus to Redis Streams.
|
|
13
|
+
*/
|
|
14
|
+
export class RedisUpdateBus {
|
|
15
|
+
pub;
|
|
16
|
+
sub;
|
|
17
|
+
handlers = new Map();
|
|
18
|
+
constructor(pub, sub) {
|
|
19
|
+
this.pub = pub;
|
|
20
|
+
this.sub = sub;
|
|
21
|
+
this.sub.on('message', (channel, message) => {
|
|
22
|
+
const handler = this.handlers.get(channel);
|
|
23
|
+
if (!handler)
|
|
24
|
+
return;
|
|
25
|
+
try {
|
|
26
|
+
handler(JSON.parse(message));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* drop malformed */
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async publishUpdate(msg) {
|
|
34
|
+
await this.pub.publish(CH_IN, JSON.stringify(msg));
|
|
35
|
+
}
|
|
36
|
+
subscribeUpdates(handler) {
|
|
37
|
+
this.handlers.set(CH_IN, handler);
|
|
38
|
+
void this.sub.subscribe(CH_IN);
|
|
39
|
+
}
|
|
40
|
+
async publishToNode(nodeId, msg) {
|
|
41
|
+
await this.pub.publish(nodeChannel(nodeId), JSON.stringify(msg));
|
|
42
|
+
}
|
|
43
|
+
subscribeNode(nodeId, handler) {
|
|
44
|
+
this.handlers.set(nodeChannel(nodeId), handler);
|
|
45
|
+
void this.sub.subscribe(nodeChannel(nodeId));
|
|
46
|
+
}
|
|
47
|
+
async close() {
|
|
48
|
+
await Promise.all([this.pub.quit().catch(() => { }), this.sub.quit().catch(() => { })]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Connects two Redis clients (publish + subscribe) and builds a {@link RedisUpdateBus}. */
|
|
52
|
+
export async function createRedisUpdateBus(url) {
|
|
53
|
+
const IoRedis = (await import('ioredis')).default;
|
|
54
|
+
const pub = new IoRedis(url, { lazyConnect: false });
|
|
55
|
+
const sub = new IoRedis(url, { lazyConnect: false });
|
|
56
|
+
const bus = new RedisUpdateBus(pub, sub);
|
|
57
|
+
return { bus, close: () => bus.close() };
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=redis-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-bus.js","sourceRoot":"","sources":["../../src/updates/redis-bus.ts"],"names":[],"mappings":"AAkBA,MAAM,KAAK,GAAG,YAAY,CAAA;AAC1B,MAAM,WAAW,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,gBAAgB,MAAM,EAAE,CAAA;AAEhE;;;;;;;;;;GAUG;AACH,MAAM,OAAO,cAAc;IAIF;IACA;IAJJ,QAAQ,GAAG,IAAI,GAAG,EAAkC,CAAA;IAErE,YACqB,GAAiB,EACjB,GAAiB;QADjB,QAAG,GAAH,GAAG,CAAc;QACjB,QAAG,GAAH,GAAG,CAAc;QAElC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;YACxC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAC1C,IAAI,CAAC,OAAO;gBAAE,OAAM;YACpB,IAAI,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAA;YAChC,CAAC;YAAC,MAAM,CAAC;gBACL,oBAAoB;YACxB,CAAC;QACL,CAAC,CAAC,CAAA;IACN,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,GAAkB;QAClC,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAA;IACtD,CAAC;IAED,gBAAgB,CAAC,OAAqC;QAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAiC,CAAC,CAAA;QAC3D,KAAK,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAAc,EAAE,GAAiB;QACjD,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAA;IACpE,CAAC;IAED,aAAa,CAAC,MAAc,EAAE,OAAoC;QAC9D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,OAAiC,CAAC,CAAA;QACzE,KAAK,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;IAChD,CAAC;IAED,KAAK,CAAC,KAAK;QACP,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACzF,CAAC;CACJ;AAOD,4FAA4F;AAC5F,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAW;IAClD,MAAM,OAAO,GAAG,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAA;IACjD,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAA;IACpD,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAA;IACpD,MAAM,GAAG,GAAG,IAAI,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACxC,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAA;AAC5C,CAAC"}
|