@terreno/api 0.13.2 → 0.14.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.
- package/dist/__tests__/versionCheckPlugin.test.js +53 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.asyncHandler.test.d.ts +1 -0
- package/dist/api.asyncHandler.test.js +236 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +17 -14
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +248 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +106 -10
- package/dist/errors.test.js +16 -1
- package/dist/example.js +16 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +53 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +65 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +720 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +2158 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +241 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +37 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.asyncHandler.test.ts +177 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +46 -19
- package/src/config.test.ts +255 -0
- package/src/config.ts +206 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +94 -20
- package/src/example.ts +46 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +50 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +59 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +568 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +1755 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +196 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type http from "node:http";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import { Server } from "socket.io";
|
|
4
|
+
import type { TerrenoPlugin } from "../terrenoPlugin";
|
|
5
|
+
import { type SocketWithDecodedToken } from "./socketUser";
|
|
6
|
+
import type { RealtimeAppOptions } from "./types";
|
|
7
|
+
/**
|
|
8
|
+
* Caps on per-socket subscriptions. Prevents a malicious or buggy client from
|
|
9
|
+
* exhausting server memory by opening unbounded subscriptions.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_MODEL_SUBSCRIPTIONS = 50;
|
|
12
|
+
export declare const MAX_DOCUMENT_SUBSCRIPTIONS = 500;
|
|
13
|
+
export declare const MAX_QUERY_SUBSCRIPTIONS = 100;
|
|
14
|
+
/**
|
|
15
|
+
* Replace any userinfo (`user:password@`) component in a URL with `***@` so the
|
|
16
|
+
* credentials are not written to logs. Falls back to a regex when the string
|
|
17
|
+
* isn't a valid URL the standard `URL` parser can read.
|
|
18
|
+
*
|
|
19
|
+
* Exported for testing.
|
|
20
|
+
*/
|
|
21
|
+
export declare const redactCredentials: (url: string) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Minimal shape this module requires from a Socket.io socket. Lets tests pass a
|
|
24
|
+
* mock without standing up a real server.
|
|
25
|
+
*/
|
|
26
|
+
export interface RealtimeSocketLike extends SocketWithDecodedToken {
|
|
27
|
+
id: string;
|
|
28
|
+
join: (room: string) => Promise<void> | void;
|
|
29
|
+
leave: (room: string) => Promise<void> | void;
|
|
30
|
+
emit: (event: string, payload: unknown) => void;
|
|
31
|
+
on: (event: string, handler: (...args: any[]) => any) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Install the realtime subscription handlers on a single socket. Extracted from the
|
|
35
|
+
* RealtimeApp connection handler so this logic can be unit-tested with a mock socket
|
|
36
|
+
* (no real Socket.io / HTTP server / JWT handshake required).
|
|
37
|
+
*
|
|
38
|
+
* Enforces:
|
|
39
|
+
* - per-socket subscription caps (DoS protection)
|
|
40
|
+
* - registry membership (only realtime-enabled collections can be subscribed)
|
|
41
|
+
* - owner-strategy isolation (non-admin users cannot subscribe to other users' rooms)
|
|
42
|
+
* - server-side queryId computation (clients can't hijack queries by colliding ids)
|
|
43
|
+
*/
|
|
44
|
+
export declare const installRealtimeSocketHandlers: (socket: RealtimeSocketLike, options?: {
|
|
45
|
+
logInfo?: (msg: string) => void;
|
|
46
|
+
}) => void;
|
|
47
|
+
/**
|
|
48
|
+
* TerrenoPlugin that provides real-time sync via Socket.io and MongoDB change streams.
|
|
49
|
+
*
|
|
50
|
+
* Attaches a Socket.io server to the HTTP server created by TerrenoApp.start(),
|
|
51
|
+
* sets up JWT authentication for socket connections, manages room subscriptions
|
|
52
|
+
* (model, document, and query rooms), and starts a change stream watcher that
|
|
53
|
+
* emits events to connected clients.
|
|
54
|
+
*
|
|
55
|
+
* ## Subscription types
|
|
56
|
+
*
|
|
57
|
+
* - **Model rooms**: `subscribe:model` / `unsubscribe:model` — receive all events for a collection
|
|
58
|
+
* - **Document rooms**: `subscribe:document` / `unsubscribe:document` — receive events for a single document
|
|
59
|
+
* - **Query rooms**: `subscribe:query` / `unsubscribe:query` — receive events matching a MongoDB query
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const app = new TerrenoApp({
|
|
64
|
+
* userModel: User,
|
|
65
|
+
* realtime: { debug: true },
|
|
66
|
+
* })
|
|
67
|
+
* .register(todoRouter) // todoRouter has realtime config
|
|
68
|
+
* .start();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare class RealtimeApp implements TerrenoPlugin {
|
|
72
|
+
private io;
|
|
73
|
+
private config;
|
|
74
|
+
constructor(config?: RealtimeAppOptions);
|
|
75
|
+
/**
|
|
76
|
+
* Register routes and middleware. Adds a /realtime/health endpoint.
|
|
77
|
+
*/
|
|
78
|
+
register(app: express.Application): void;
|
|
79
|
+
/**
|
|
80
|
+
* Called after the HTTP server is created. Sets up Socket.io, auth, rooms,
|
|
81
|
+
* and starts the change stream watcher.
|
|
82
|
+
*/
|
|
83
|
+
onServerCreated(server: http.Server): void;
|
|
84
|
+
/**
|
|
85
|
+
* Get the Socket.io server instance.
|
|
86
|
+
*/
|
|
87
|
+
getIo(): Server | null;
|
|
88
|
+
/**
|
|
89
|
+
* Gracefully shut down the real-time server.
|
|
90
|
+
*/
|
|
91
|
+
close(): Promise<void>;
|
|
92
|
+
private setupAdapter;
|
|
93
|
+
}
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
47
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
48
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
49
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
50
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
51
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
52
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
56
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
57
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
58
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
59
|
+
function step(op) {
|
|
60
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
61
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
62
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
63
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
64
|
+
switch (op[0]) {
|
|
65
|
+
case 0: case 1: t = op; break;
|
|
66
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
67
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
68
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
69
|
+
default:
|
|
70
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
71
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
72
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
73
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
74
|
+
if (t[2]) _.ops.pop();
|
|
75
|
+
_.trys.pop(); continue;
|
|
76
|
+
}
|
|
77
|
+
op = body.call(thisArg, _);
|
|
78
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
79
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
83
|
+
exports.RealtimeApp = exports.installRealtimeSocketHandlers = exports.redactCredentials = exports.MAX_QUERY_SUBSCRIPTIONS = exports.MAX_DOCUMENT_SUBSCRIPTIONS = exports.MAX_MODEL_SUBSCRIPTIONS = void 0;
|
|
84
|
+
var Sentry = __importStar(require("@sentry/bun"));
|
|
85
|
+
var socketio_jwt_1 = require("@thream/socketio-jwt");
|
|
86
|
+
var socket_io_1 = require("socket.io");
|
|
87
|
+
var logger_1 = require("../logger");
|
|
88
|
+
var permissions_1 = require("../permissions");
|
|
89
|
+
var changeStreamWatcher_1 = require("./changeStreamWatcher");
|
|
90
|
+
var queryStore_1 = require("./queryStore");
|
|
91
|
+
var registry_1 = require("./registry");
|
|
92
|
+
var socketUser_1 = require("./socketUser");
|
|
93
|
+
/**
|
|
94
|
+
* Caps on per-socket subscriptions. Prevents a malicious or buggy client from
|
|
95
|
+
* exhausting server memory by opening unbounded subscriptions.
|
|
96
|
+
*/
|
|
97
|
+
exports.MAX_MODEL_SUBSCRIPTIONS = 50;
|
|
98
|
+
exports.MAX_DOCUMENT_SUBSCRIPTIONS = 500;
|
|
99
|
+
exports.MAX_QUERY_SUBSCRIPTIONS = 100;
|
|
100
|
+
/**
|
|
101
|
+
* Replace any userinfo (`user:password@`) component in a URL with `***@` so the
|
|
102
|
+
* credentials are not written to logs. Falls back to a regex when the string
|
|
103
|
+
* isn't a valid URL the standard `URL` parser can read.
|
|
104
|
+
*
|
|
105
|
+
* Exported for testing.
|
|
106
|
+
*/
|
|
107
|
+
var redactCredentials = function (url) {
|
|
108
|
+
try {
|
|
109
|
+
var parsed = new URL(url);
|
|
110
|
+
if (parsed.username || parsed.password) {
|
|
111
|
+
parsed.username = "";
|
|
112
|
+
parsed.password = "";
|
|
113
|
+
// URL serialization drops the empty userinfo; reinsert a sentinel so logs make it
|
|
114
|
+
// obvious credentials were stripped rather than silently absent from the source URL.
|
|
115
|
+
return parsed.toString().replace("".concat(parsed.protocol, "//"), "".concat(parsed.protocol, "//***@"));
|
|
116
|
+
}
|
|
117
|
+
return parsed.toString();
|
|
118
|
+
}
|
|
119
|
+
catch (_a) {
|
|
120
|
+
return url.replace(/^(\w+):\/\/[^@/]*@/, "$1://***@");
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
exports.redactCredentials = redactCredentials;
|
|
124
|
+
var canSubscribe = function (entry, method, user) { return __awaiter(void 0, void 0, void 0, function () {
|
|
125
|
+
var permissions;
|
|
126
|
+
return __generator(this, function (_a) {
|
|
127
|
+
permissions = entry.options.permissions[method];
|
|
128
|
+
return [2 /*return*/, (0, permissions_1.checkPermissions)(method, permissions, user)];
|
|
129
|
+
});
|
|
130
|
+
}); };
|
|
131
|
+
var getAuthorizedQuery = function (entry, query, user) { return __awaiter(void 0, void 0, void 0, function () {
|
|
132
|
+
var filteredQuery, error_1;
|
|
133
|
+
return __generator(this, function (_a) {
|
|
134
|
+
switch (_a.label) {
|
|
135
|
+
case 0: return [4 /*yield*/, canSubscribe(entry, "list", user)];
|
|
136
|
+
case 1:
|
|
137
|
+
if (!(_a.sent())) {
|
|
138
|
+
return [2 /*return*/, null];
|
|
139
|
+
}
|
|
140
|
+
if (!entry.options.queryFilter) {
|
|
141
|
+
return [2 /*return*/, query];
|
|
142
|
+
}
|
|
143
|
+
_a.label = 2;
|
|
144
|
+
case 2:
|
|
145
|
+
_a.trys.push([2, 4, , 5]);
|
|
146
|
+
return [4 /*yield*/, entry.options.queryFilter(user, query)];
|
|
147
|
+
case 3:
|
|
148
|
+
filteredQuery = _a.sent();
|
|
149
|
+
return [3 /*break*/, 5];
|
|
150
|
+
case 4:
|
|
151
|
+
error_1 = _a.sent();
|
|
152
|
+
logger_1.logger.error("[realtime] queryFilter threw for ".concat(entry.modelName, "/list: ").concat(error_1, ". Denying query subscription."));
|
|
153
|
+
Sentry.captureException(error_1);
|
|
154
|
+
return [2 /*return*/, null];
|
|
155
|
+
case 5:
|
|
156
|
+
if (filteredQuery === null) {
|
|
157
|
+
return [2 /*return*/, null];
|
|
158
|
+
}
|
|
159
|
+
return [2 /*return*/, __assign(__assign({}, query), filteredQuery)];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}); };
|
|
163
|
+
/**
|
|
164
|
+
* Install the realtime subscription handlers on a single socket. Extracted from the
|
|
165
|
+
* RealtimeApp connection handler so this logic can be unit-tested with a mock socket
|
|
166
|
+
* (no real Socket.io / HTTP server / JWT handshake required).
|
|
167
|
+
*
|
|
168
|
+
* Enforces:
|
|
169
|
+
* - per-socket subscription caps (DoS protection)
|
|
170
|
+
* - registry membership (only realtime-enabled collections can be subscribed)
|
|
171
|
+
* - owner-strategy isolation (non-admin users cannot subscribe to other users' rooms)
|
|
172
|
+
* - server-side queryId computation (clients can't hijack queries by colliding ids)
|
|
173
|
+
*/
|
|
174
|
+
var installRealtimeSocketHandlers = function (socket, options) {
|
|
175
|
+
var _a, _b, _c;
|
|
176
|
+
if (options === void 0) { options = {}; }
|
|
177
|
+
var logInfo = (_a = options.logInfo) !== null && _a !== void 0 ? _a : (function () { });
|
|
178
|
+
var userId = (_b = socket.decodedToken) === null || _b === void 0 ? void 0 : _b.id;
|
|
179
|
+
var isAdmin = ((_c = socket.decodedToken) === null || _c === void 0 ? void 0 : _c.admin) === true;
|
|
180
|
+
var user = (0, socketUser_1.getSocketUser)(socket);
|
|
181
|
+
var counts = { document: 0, model: 0, query: 0 };
|
|
182
|
+
var joinUserRooms = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
183
|
+
return __generator(this, function (_a) {
|
|
184
|
+
switch (_a.label) {
|
|
185
|
+
case 0:
|
|
186
|
+
if (!userId) return [3 /*break*/, 3];
|
|
187
|
+
return [4 /*yield*/, socket.join("user:".concat(userId))];
|
|
188
|
+
case 1:
|
|
189
|
+
_a.sent();
|
|
190
|
+
return [4 /*yield*/, socket.join("authenticated")];
|
|
191
|
+
case 2:
|
|
192
|
+
_a.sent();
|
|
193
|
+
logInfo("[realtime] User ".concat(userId, " connected"));
|
|
194
|
+
_a.label = 3;
|
|
195
|
+
case 3:
|
|
196
|
+
if (!isAdmin) return [3 /*break*/, 5];
|
|
197
|
+
return [4 /*yield*/, socket.join("admin")];
|
|
198
|
+
case 4:
|
|
199
|
+
_a.sent();
|
|
200
|
+
logInfo("[realtime] Admin user ".concat(userId, " joined admin room"));
|
|
201
|
+
_a.label = 5;
|
|
202
|
+
case 5: return [2 /*return*/];
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}); };
|
|
206
|
+
// Fire-and-forget — there is nothing useful for the caller to await.
|
|
207
|
+
void joinUserRooms();
|
|
208
|
+
socket.on("subscribe:model", function (modelName) { return __awaiter(void 0, void 0, void 0, function () {
|
|
209
|
+
var entry;
|
|
210
|
+
return __generator(this, function (_a) {
|
|
211
|
+
switch (_a.label) {
|
|
212
|
+
case 0:
|
|
213
|
+
if (typeof modelName !== "string" || modelName.length === 0) {
|
|
214
|
+
return [2 /*return*/];
|
|
215
|
+
}
|
|
216
|
+
if (counts.model >= exports.MAX_MODEL_SUBSCRIPTIONS) {
|
|
217
|
+
logInfo("[realtime] User ".concat(userId, " hit model subscription limit"));
|
|
218
|
+
return [2 /*return*/];
|
|
219
|
+
}
|
|
220
|
+
entry = (0, registry_1.findRegistryEntryByRoutePath)(modelName);
|
|
221
|
+
if (!entry) {
|
|
222
|
+
logInfo("[realtime] User ".concat(userId, " denied model subscription: collection \"").concat(modelName, "\" not registered"));
|
|
223
|
+
return [2 /*return*/];
|
|
224
|
+
}
|
|
225
|
+
// Owner-strategy models fan out via user:{ownerId} — there is no shared model room
|
|
226
|
+
// that should be open to all users. Owners receive events through their user room
|
|
227
|
+
// automatically; admins can use the admin room to see everything.
|
|
228
|
+
if (entry.config.roomStrategy === "owner" && !isAdmin) {
|
|
229
|
+
logInfo("[realtime] User ".concat(userId, " denied model subscription for ").concat(modelName, ": ") +
|
|
230
|
+
"owner strategy restricts model room to admins");
|
|
231
|
+
return [2 /*return*/];
|
|
232
|
+
}
|
|
233
|
+
return [4 /*yield*/, canSubscribe(entry, "list", user)];
|
|
234
|
+
case 1:
|
|
235
|
+
if (!(_a.sent())) {
|
|
236
|
+
logInfo("[realtime] User ".concat(userId, " denied model subscription for ").concat(modelName, ": list permission denied"));
|
|
237
|
+
return [2 /*return*/];
|
|
238
|
+
}
|
|
239
|
+
counts.model += 1;
|
|
240
|
+
return [4 /*yield*/, socket.join("model:".concat(modelName))];
|
|
241
|
+
case 2:
|
|
242
|
+
_a.sent();
|
|
243
|
+
logInfo("[realtime] User ".concat(userId, " subscribed to model:").concat(modelName));
|
|
244
|
+
return [2 /*return*/];
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}); });
|
|
248
|
+
socket.on("unsubscribe:model", function (modelName) { return __awaiter(void 0, void 0, void 0, function () {
|
|
249
|
+
return __generator(this, function (_a) {
|
|
250
|
+
switch (_a.label) {
|
|
251
|
+
case 0:
|
|
252
|
+
if (!(typeof modelName === "string" && modelName.length > 0)) return [3 /*break*/, 2];
|
|
253
|
+
return [4 /*yield*/, socket.leave("model:".concat(modelName))];
|
|
254
|
+
case 1:
|
|
255
|
+
_a.sent();
|
|
256
|
+
counts.model = Math.max(0, counts.model - 1);
|
|
257
|
+
logInfo("[realtime] User ".concat(userId, " unsubscribed from model:").concat(modelName));
|
|
258
|
+
_a.label = 2;
|
|
259
|
+
case 2: return [2 /*return*/];
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}); });
|
|
263
|
+
socket.on("subscribe:document", function (payload) { return __awaiter(void 0, void 0, void 0, function () {
|
|
264
|
+
var entry, room;
|
|
265
|
+
return __generator(this, function (_a) {
|
|
266
|
+
switch (_a.label) {
|
|
267
|
+
case 0:
|
|
268
|
+
if (!(payload === null || payload === void 0 ? void 0 : payload.collection) ||
|
|
269
|
+
!(payload === null || payload === void 0 ? void 0 : payload.id) ||
|
|
270
|
+
typeof payload.collection !== "string" ||
|
|
271
|
+
typeof payload.id !== "string") {
|
|
272
|
+
return [2 /*return*/];
|
|
273
|
+
}
|
|
274
|
+
if (counts.document >= exports.MAX_DOCUMENT_SUBSCRIPTIONS) {
|
|
275
|
+
logInfo("[realtime] User ".concat(userId, " hit document subscription limit"));
|
|
276
|
+
return [2 /*return*/];
|
|
277
|
+
}
|
|
278
|
+
entry = (0, registry_1.findRegistryEntryByRoutePath)(payload.collection);
|
|
279
|
+
if (!entry) {
|
|
280
|
+
logInfo("[realtime] User ".concat(userId, " denied document subscription: ") +
|
|
281
|
+
"collection \"".concat(payload.collection, "\" not registered"));
|
|
282
|
+
return [2 /*return*/];
|
|
283
|
+
}
|
|
284
|
+
return [4 /*yield*/, canSubscribe(entry, "read", user)];
|
|
285
|
+
case 1:
|
|
286
|
+
if (!(_a.sent())) {
|
|
287
|
+
logInfo("[realtime] User ".concat(userId, " denied document subscription for ") +
|
|
288
|
+
"".concat(payload.collection, "/").concat(payload.id, ": read permission denied"));
|
|
289
|
+
return [2 /*return*/];
|
|
290
|
+
}
|
|
291
|
+
if (entry.config.roomStrategy === "owner" && !isAdmin) {
|
|
292
|
+
logInfo("[realtime] User ".concat(userId, " denied document subscription for ") +
|
|
293
|
+
"".concat(payload.collection, "/").concat(payload.id, ": owner strategy requires admin"));
|
|
294
|
+
return [2 /*return*/];
|
|
295
|
+
}
|
|
296
|
+
counts.document += 1;
|
|
297
|
+
room = "document:".concat(payload.collection, ":").concat(payload.id);
|
|
298
|
+
return [4 /*yield*/, socket.join(room)];
|
|
299
|
+
case 2:
|
|
300
|
+
_a.sent();
|
|
301
|
+
logInfo("[realtime] User ".concat(userId, " subscribed to ").concat(room));
|
|
302
|
+
return [2 /*return*/];
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}); });
|
|
306
|
+
socket.on("unsubscribe:document", function (payload) { return __awaiter(void 0, void 0, void 0, function () {
|
|
307
|
+
var room;
|
|
308
|
+
return __generator(this, function (_a) {
|
|
309
|
+
switch (_a.label) {
|
|
310
|
+
case 0:
|
|
311
|
+
if (!((payload === null || payload === void 0 ? void 0 : payload.collection) && (payload === null || payload === void 0 ? void 0 : payload.id))) return [3 /*break*/, 2];
|
|
312
|
+
room = "document:".concat(payload.collection, ":").concat(payload.id);
|
|
313
|
+
return [4 /*yield*/, socket.leave(room)];
|
|
314
|
+
case 1:
|
|
315
|
+
_a.sent();
|
|
316
|
+
counts.document = Math.max(0, counts.document - 1);
|
|
317
|
+
logInfo("[realtime] User ".concat(userId, " unsubscribed from ").concat(room));
|
|
318
|
+
_a.label = 2;
|
|
319
|
+
case 2: return [2 /*return*/];
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}); });
|
|
323
|
+
socket.on("subscribe:query", function (payload) { return __awaiter(void 0, void 0, void 0, function () {
|
|
324
|
+
var entry, query, queryId;
|
|
325
|
+
return __generator(this, function (_a) {
|
|
326
|
+
switch (_a.label) {
|
|
327
|
+
case 0:
|
|
328
|
+
if (!(payload === null || payload === void 0 ? void 0 : payload.collection) ||
|
|
329
|
+
!(payload === null || payload === void 0 ? void 0 : payload.query) ||
|
|
330
|
+
typeof payload.collection !== "string" ||
|
|
331
|
+
typeof payload.query !== "object" ||
|
|
332
|
+
Array.isArray(payload.query)) {
|
|
333
|
+
return [2 /*return*/];
|
|
334
|
+
}
|
|
335
|
+
if (counts.query >= exports.MAX_QUERY_SUBSCRIPTIONS) {
|
|
336
|
+
logInfo("[realtime] User ".concat(userId, " hit query subscription limit"));
|
|
337
|
+
return [2 /*return*/];
|
|
338
|
+
}
|
|
339
|
+
entry = (0, registry_1.findRegistryEntryByRoutePath)(payload.collection);
|
|
340
|
+
if (!entry) {
|
|
341
|
+
logInfo("[realtime] User ".concat(userId, " denied query subscription: ") +
|
|
342
|
+
"collection \"".concat(payload.collection, "\" not registered"));
|
|
343
|
+
return [2 /*return*/];
|
|
344
|
+
}
|
|
345
|
+
return [4 /*yield*/, getAuthorizedQuery(entry, __assign({}, payload.query), user)];
|
|
346
|
+
case 1:
|
|
347
|
+
query = _a.sent();
|
|
348
|
+
if (!query) {
|
|
349
|
+
logInfo("[realtime] User ".concat(userId, " denied query subscription for ").concat(payload.collection, ": ") +
|
|
350
|
+
"list permission or query filter denied");
|
|
351
|
+
return [2 /*return*/];
|
|
352
|
+
}
|
|
353
|
+
if (entry.config.roomStrategy === "owner" && !isAdmin) {
|
|
354
|
+
if (!userId) {
|
|
355
|
+
return [2 /*return*/];
|
|
356
|
+
}
|
|
357
|
+
query = __assign(__assign({}, query), { ownerId: userId });
|
|
358
|
+
}
|
|
359
|
+
queryId = (0, queryStore_1.computeQueryId)(payload.collection, query);
|
|
360
|
+
(0, queryStore_1.addQuerySubscription)(socket.id, payload.collection, query, queryId);
|
|
361
|
+
counts.query += 1;
|
|
362
|
+
return [4 /*yield*/, socket.join("query:".concat(queryId))];
|
|
363
|
+
case 2:
|
|
364
|
+
_a.sent();
|
|
365
|
+
socket.emit("query:subscribed", {
|
|
366
|
+
clientQueryId: payload.queryId,
|
|
367
|
+
collection: payload.collection,
|
|
368
|
+
queryId: queryId,
|
|
369
|
+
});
|
|
370
|
+
logInfo("[realtime] User ".concat(userId, " subscribed to query:").concat(queryId, " on ").concat(payload.collection));
|
|
371
|
+
return [2 /*return*/];
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}); });
|
|
375
|
+
socket.on("unsubscribe:query", function (payload) { return __awaiter(void 0, void 0, void 0, function () {
|
|
376
|
+
return __generator(this, function (_a) {
|
|
377
|
+
switch (_a.label) {
|
|
378
|
+
case 0:
|
|
379
|
+
if (!(payload === null || payload === void 0 ? void 0 : payload.queryId)) return [3 /*break*/, 2];
|
|
380
|
+
(0, queryStore_1.removeQuerySubscription)(socket.id, payload.queryId);
|
|
381
|
+
return [4 /*yield*/, socket.leave("query:".concat(payload.queryId))];
|
|
382
|
+
case 1:
|
|
383
|
+
_a.sent();
|
|
384
|
+
counts.query = Math.max(0, counts.query - 1);
|
|
385
|
+
logInfo("[realtime] User ".concat(userId, " unsubscribed from query:").concat(payload.queryId));
|
|
386
|
+
_a.label = 2;
|
|
387
|
+
case 2: return [2 /*return*/];
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}); });
|
|
391
|
+
socket.on("disconnect", function () {
|
|
392
|
+
(0, queryStore_1.removeAllSocketQueries)(socket.id);
|
|
393
|
+
logInfo("[realtime] User ".concat(userId, " disconnected"));
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
exports.installRealtimeSocketHandlers = installRealtimeSocketHandlers;
|
|
397
|
+
/**
|
|
398
|
+
* TerrenoPlugin that provides real-time sync via Socket.io and MongoDB change streams.
|
|
399
|
+
*
|
|
400
|
+
* Attaches a Socket.io server to the HTTP server created by TerrenoApp.start(),
|
|
401
|
+
* sets up JWT authentication for socket connections, manages room subscriptions
|
|
402
|
+
* (model, document, and query rooms), and starts a change stream watcher that
|
|
403
|
+
* emits events to connected clients.
|
|
404
|
+
*
|
|
405
|
+
* ## Subscription types
|
|
406
|
+
*
|
|
407
|
+
* - **Model rooms**: `subscribe:model` / `unsubscribe:model` — receive all events for a collection
|
|
408
|
+
* - **Document rooms**: `subscribe:document` / `unsubscribe:document` — receive events for a single document
|
|
409
|
+
* - **Query rooms**: `subscribe:query` / `unsubscribe:query` — receive events matching a MongoDB query
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```typescript
|
|
413
|
+
* const app = new TerrenoApp({
|
|
414
|
+
* userModel: User,
|
|
415
|
+
* realtime: { debug: true },
|
|
416
|
+
* })
|
|
417
|
+
* .register(todoRouter) // todoRouter has realtime config
|
|
418
|
+
* .start();
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
var RealtimeApp = /** @class */ (function () {
|
|
422
|
+
function RealtimeApp(config) {
|
|
423
|
+
if (config === void 0) { config = {}; }
|
|
424
|
+
this.io = null;
|
|
425
|
+
this.config = config;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Register routes and middleware. Adds a /realtime/health endpoint.
|
|
429
|
+
*/
|
|
430
|
+
RealtimeApp.prototype.register = function (app) {
|
|
431
|
+
var _this = this;
|
|
432
|
+
app.get("/realtime/health", function (_req, res) {
|
|
433
|
+
var _a, _b, _c, _d;
|
|
434
|
+
var connected = (_c = (_b = (_a = _this.io) === null || _a === void 0 ? void 0 : _a.engine) === null || _b === void 0 ? void 0 : _b.clientsCount) !== null && _c !== void 0 ? _c : 0;
|
|
435
|
+
res.json({
|
|
436
|
+
clients: connected,
|
|
437
|
+
debug: (_d = _this.config.debug) !== null && _d !== void 0 ? _d : false,
|
|
438
|
+
status: _this.io ? "running" : "not_started",
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* Called after the HTTP server is created. Sets up Socket.io, auth, rooms,
|
|
444
|
+
* and starts the change stream watcher.
|
|
445
|
+
*/
|
|
446
|
+
RealtimeApp.prototype.onServerCreated = function (server) {
|
|
447
|
+
var _a, _b, _c;
|
|
448
|
+
var debug = (_a = this.config.debug) !== null && _a !== void 0 ? _a : false;
|
|
449
|
+
var logInfo = function (message) {
|
|
450
|
+
if (debug) {
|
|
451
|
+
logger_1.logger.info(message);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
logInfo("[realtime] Setting up Socket.io server...");
|
|
456
|
+
this.io = new socket_io_1.Server(server, {
|
|
457
|
+
cors: (_b = this.config.cors) !== null && _b !== void 0 ? _b : {
|
|
458
|
+
methods: ["GET", "POST"],
|
|
459
|
+
origin: "*",
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
// JWT authentication middleware
|
|
463
|
+
var tokenSecret = (_c = this.config.tokenSecret) !== null && _c !== void 0 ? _c : process.env.TOKEN_SECRET;
|
|
464
|
+
if (!tokenSecret) {
|
|
465
|
+
throw new Error("[realtime] TOKEN_SECRET is required for socket authentication. " +
|
|
466
|
+
"Set process.env.TOKEN_SECRET or pass tokenSecret in RealtimeAppOptions.");
|
|
467
|
+
}
|
|
468
|
+
this.io.use((0, socketio_jwt_1.authorize)({
|
|
469
|
+
secret: tokenSecret,
|
|
470
|
+
}));
|
|
471
|
+
logInfo("[realtime] JWT authorization middleware added");
|
|
472
|
+
// Configure adapter for multi-instance deployments
|
|
473
|
+
this.setupAdapter(logInfo);
|
|
474
|
+
// Connection handling
|
|
475
|
+
this.io.on("connection", function (socket) {
|
|
476
|
+
try {
|
|
477
|
+
// socketio-jwt's authorize middleware adds `decodedToken` at runtime; cast through
|
|
478
|
+
// RealtimeSocketLike (a structural subset) to keep socket handler logic testable.
|
|
479
|
+
(0, exports.installRealtimeSocketHandlers)(socket, { logInfo: logInfo });
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
logger_1.logger.error("[realtime] Error handling connection: ".concat(error));
|
|
483
|
+
Sentry.captureException(error);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
this.io.on("connect_error", function (error) {
|
|
487
|
+
logger_1.logger.error("[realtime] Connection error: ".concat(error.message));
|
|
488
|
+
Sentry.captureException(error);
|
|
489
|
+
});
|
|
490
|
+
// Start the change stream watcher
|
|
491
|
+
(0, changeStreamWatcher_1.startChangeStreamWatcher)(this.io, this.config.changeStream, debug);
|
|
492
|
+
logInfo("[realtime] Socket.io server setup complete");
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
logger_1.logger.error("[realtime] Failed to set up Socket.io: ".concat(error));
|
|
496
|
+
Sentry.captureException(error);
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
/**
|
|
501
|
+
* Get the Socket.io server instance.
|
|
502
|
+
*/
|
|
503
|
+
RealtimeApp.prototype.getIo = function () {
|
|
504
|
+
return this.io;
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* Gracefully shut down the real-time server.
|
|
508
|
+
*/
|
|
509
|
+
RealtimeApp.prototype.close = function () {
|
|
510
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
511
|
+
var error_2;
|
|
512
|
+
return __generator(this, function (_a) {
|
|
513
|
+
switch (_a.label) {
|
|
514
|
+
case 0:
|
|
515
|
+
_a.trys.push([0, 4, , 5]);
|
|
516
|
+
return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
|
|
517
|
+
case 1:
|
|
518
|
+
_a.sent();
|
|
519
|
+
if (!this.io) return [3 /*break*/, 3];
|
|
520
|
+
return [4 /*yield*/, this.io.close()];
|
|
521
|
+
case 2:
|
|
522
|
+
_a.sent();
|
|
523
|
+
this.io = null;
|
|
524
|
+
_a.label = 3;
|
|
525
|
+
case 3: return [3 /*break*/, 5];
|
|
526
|
+
case 4:
|
|
527
|
+
error_2 = _a.sent();
|
|
528
|
+
logger_1.logger.error("[realtime] Error closing: ".concat(error_2));
|
|
529
|
+
return [3 /*break*/, 5];
|
|
530
|
+
case 5: return [2 /*return*/];
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
RealtimeApp.prototype.setupAdapter = function (logInfo) {
|
|
536
|
+
var _a, _b, _c;
|
|
537
|
+
if (!this.io) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
var adapter = (_a = this.config.adapter) !== null && _a !== void 0 ? _a : "none";
|
|
541
|
+
if (adapter === "redis") {
|
|
542
|
+
var redisUrl = (_c = (_b = this.config.redisUrl) !== null && _b !== void 0 ? _b : process.env.VALKEY_URL) !== null && _c !== void 0 ? _c : process.env.REDIS_URL;
|
|
543
|
+
if (redisUrl) {
|
|
544
|
+
logInfo("[realtime] Redis adapter configured with URL: ".concat((0, exports.redactCredentials)(redisUrl)));
|
|
545
|
+
// Redis adapter must be configured externally by the consuming app
|
|
546
|
+
// since @socket.io/redis-adapter and ioredis are optional peer dependencies.
|
|
547
|
+
// Use realtimeApp.getIo() to access the Socket.io instance and call
|
|
548
|
+
// io.adapter(createRedisAdapter(pubClient, subClient))
|
|
549
|
+
logger_1.logger.info("[realtime] To enable Redis adapter, configure it after server creation via getIo(). " +
|
|
550
|
+
"See @socket.io/redis-adapter docs.");
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
logger_1.logger.warn("[realtime] Redis adapter requested but no VALKEY_URL or REDIS_URL found");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// 'none' — no adapter, single instance mode
|
|
557
|
+
};
|
|
558
|
+
return RealtimeApp;
|
|
559
|
+
}());
|
|
560
|
+
exports.RealtimeApp = RealtimeApp;
|