@rvoh/psychic-websockets 0.2.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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/cjs/src/cable/index.js +121 -0
- package/dist/cjs/src/cable/redisWsKey.js +6 -0
- package/dist/cjs/src/cable/ws.js +86 -0
- package/dist/cjs/src/error/ws/MissingWsRedisConnection.js +32 -0
- package/dist/cjs/src/helpers/EnvInternal.js +5 -0
- package/dist/cjs/src/index.js +9 -0
- package/dist/cjs/src/psychic-application-websockets/cache.js +17 -0
- package/dist/cjs/src/psychic-application-websockets/index.js +79 -0
- package/dist/esm/src/cable/index.js +118 -0
- package/dist/esm/src/cable/redisWsKey.js +3 -0
- package/dist/esm/src/cable/ws.js +81 -0
- package/dist/esm/src/error/ws/MissingWsRedisConnection.js +29 -0
- package/dist/esm/src/helpers/EnvInternal.js +3 -0
- package/dist/esm/src/index.js +3 -0
- package/dist/esm/src/psychic-application-websockets/cache.js +12 -0
- package/dist/esm/src/psychic-application-websockets/index.js +76 -0
- package/dist/types/src/cable/index.d.ts +24 -0
- package/dist/types/src/cable/redisWsKey.d.ts +2 -0
- package/dist/types/src/cable/ws.d.ts +25 -0
- package/dist/types/src/error/ws/MissingWsRedisConnection.d.ts +3 -0
- package/dist/types/src/helpers/EnvInternal.d.ts +6 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/psychic-application-websockets/cache.d.ts +4 -0
- package/dist/types/src/psychic-application-websockets/index.d.ts +35 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 RVO Health
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
> ATTENTION: we are currently in the process of releasing this code to the world, as of the afternoon of March 10th, 2025. This notice will be removed, and the version of this repo will be bumped to 1.0.0, once all of the repos have been migrated to the new spaces and we can verify that it is all working. This is anticipated to take 1 day.
|
|
2
|
+
|
|
3
|
+
# Psychic websockets
|
|
4
|
+
|
|
5
|
+
Documentation for this repo can be found at [https://psychic-docs.netlify.app/docs/plugins/websockets/overview](https://psychic-docs.netlify.app/docs/plugins/websockets/overview)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const psychic_1 = require("@rvoh/psychic");
|
|
4
|
+
const redis_adapter_1 = require("@socket.io/redis-adapter");
|
|
5
|
+
const colors = require("colorette");
|
|
6
|
+
const socketio = require("socket.io");
|
|
7
|
+
const MissingWsRedisConnection_js_1 = require("../error/ws/MissingWsRedisConnection.js");
|
|
8
|
+
const EnvInternal_js_1 = require("../helpers/EnvInternal.js");
|
|
9
|
+
const index_js_1 = require("../psychic-application-websockets/index.js");
|
|
10
|
+
class Cable {
|
|
11
|
+
app;
|
|
12
|
+
io;
|
|
13
|
+
httpServer;
|
|
14
|
+
config;
|
|
15
|
+
redisConnections = [];
|
|
16
|
+
constructor(app, config) {
|
|
17
|
+
this.app = app;
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
connect() {
|
|
21
|
+
if (this.io)
|
|
22
|
+
return;
|
|
23
|
+
// for socket.io, we have to circumvent the normal process for starting a
|
|
24
|
+
// psychic server so that we can bind socket.io to the http instance.
|
|
25
|
+
this.httpServer = psychic_1.PsychicServer.createPsychicHttpInstance(this.app, this.config.psychicApp.sslCredentials);
|
|
26
|
+
this.io = new socketio.Server(this.httpServer, { cors: this.config.psychicApp.corsOptions });
|
|
27
|
+
}
|
|
28
|
+
async start(port, { withFrontEndClient = false, frontEndPort = 3000, } = {}) {
|
|
29
|
+
this.connect();
|
|
30
|
+
for (const hook of this.config.hooks.wsStart) {
|
|
31
|
+
await hook(this.io);
|
|
32
|
+
}
|
|
33
|
+
this.io.on('connect', async (socket) => {
|
|
34
|
+
try {
|
|
35
|
+
for (const hook of this.config.hooks.wsConnect) {
|
|
36
|
+
await hook(socket);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (EnvInternal_js_1.default.boolean('PSYCHIC_DANGEROUSLY_PERMIT_WS_EXCEPTIONS'))
|
|
41
|
+
throw error;
|
|
42
|
+
else {
|
|
43
|
+
;
|
|
44
|
+
this.config.psychicApp.constructor.logWithLevel('error', `
|
|
45
|
+
An exception was caught in your websocket thread.
|
|
46
|
+
To prevent your server from crashing, we are rescuing this error here for you.
|
|
47
|
+
If you would like us to raise this exception, make sure to set
|
|
48
|
+
|
|
49
|
+
PSYCHIC_DANGEROUSLY_PERMIT_WS_EXCEPTIONS=1
|
|
50
|
+
|
|
51
|
+
the error received is:
|
|
52
|
+
|
|
53
|
+
${error.message}
|
|
54
|
+
`);
|
|
55
|
+
console.trace();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
this.bindToRedis();
|
|
60
|
+
const psychicAppWebsockets = index_js_1.default.getOrFail();
|
|
61
|
+
await this.listen({
|
|
62
|
+
port: parseInt((port || psychicAppWebsockets.psychicApp.port).toString()),
|
|
63
|
+
withFrontEndClient,
|
|
64
|
+
frontEndPort,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async stop() {
|
|
68
|
+
try {
|
|
69
|
+
await this.io?.close();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// noop
|
|
73
|
+
}
|
|
74
|
+
for (const connection of this.redisConnections) {
|
|
75
|
+
try {
|
|
76
|
+
connection.disconnect();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// noop
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async listen({ port, withFrontEndClient, frontEndPort, }) {
|
|
84
|
+
return new Promise(accept => {
|
|
85
|
+
this.httpServer.listen(port, () => {
|
|
86
|
+
if (!EnvInternal_js_1.default.isTest) {
|
|
87
|
+
const app = index_js_1.default.getOrFail().psychicApp;
|
|
88
|
+
app.logger.info(psychic_1.PsychicServer.asciiLogo());
|
|
89
|
+
app.logger.info('\n');
|
|
90
|
+
app.logger.info(colors.cyan('socket server started '));
|
|
91
|
+
app.logger.info(colors.cyan(`psychic dev server started at port ${colors.bgBlueBright(colors.green(port))}`));
|
|
92
|
+
if (withFrontEndClient)
|
|
93
|
+
app.logger.info(`client server started at port ${colors.cyan(frontEndPort)}`);
|
|
94
|
+
app.logger.info('\n');
|
|
95
|
+
}
|
|
96
|
+
accept(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
bindToRedis() {
|
|
101
|
+
const pubClient = this.config.websocketOptions.connection;
|
|
102
|
+
const subClient = this.config.websocketOptions.subConnection;
|
|
103
|
+
if (!pubClient || !subClient)
|
|
104
|
+
throw new MissingWsRedisConnection_js_1.default();
|
|
105
|
+
this.redisConnections.push(pubClient);
|
|
106
|
+
this.redisConnections.push(subClient);
|
|
107
|
+
pubClient.on('error', error => {
|
|
108
|
+
index_js_1.default.log('PUB CLIENT ERROR', error);
|
|
109
|
+
});
|
|
110
|
+
subClient.on('error', error => {
|
|
111
|
+
index_js_1.default.log('sub CLIENT ERROR', error);
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
this.io.adapter((0, redis_adapter_1.createAdapter)(pubClient, subClient));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
index_js_1.default.log('FAILED TO ADAPT', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.default = Cable;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvalidWsPathError = void 0;
|
|
4
|
+
const dream_1 = require("@rvoh/dream");
|
|
5
|
+
const redis_emitter_1 = require("@socket.io/redis-emitter");
|
|
6
|
+
const luxon_1 = require("luxon");
|
|
7
|
+
const EnvInternal_js_1 = require("../helpers/EnvInternal.js");
|
|
8
|
+
const redisWsKey_js_1 = require("./redisWsKey.js");
|
|
9
|
+
const index_js_1 = require("../psychic-application-websockets/index.js");
|
|
10
|
+
class Ws {
|
|
11
|
+
allowedPaths;
|
|
12
|
+
io;
|
|
13
|
+
redisClient;
|
|
14
|
+
booted = false;
|
|
15
|
+
namespace;
|
|
16
|
+
redisKeyPrefix;
|
|
17
|
+
static async register(socket, id, redisKeyPrefix = 'user') {
|
|
18
|
+
const psychicWebsocketsApp = index_js_1.default.getOrFail();
|
|
19
|
+
const redisClient = psychicWebsocketsApp.websocketOptions.connection;
|
|
20
|
+
const interpretedId = id?.isDreamInstance ? id.primaryKeyValue : id;
|
|
21
|
+
const key = (0, redisWsKey_js_1.default)(interpretedId, redisKeyPrefix);
|
|
22
|
+
const socketIdsToKeep = await redisClient.lrange((0, redisWsKey_js_1.default)(interpretedId, redisKeyPrefix), -2, -1);
|
|
23
|
+
await redisClient
|
|
24
|
+
.multi()
|
|
25
|
+
.del(key)
|
|
26
|
+
.rpush(key, ...socketIdsToKeep, socket.id)
|
|
27
|
+
.expireat(key,
|
|
28
|
+
// TODO: make this configurable in non-test environments
|
|
29
|
+
luxon_1.DateTime.now()
|
|
30
|
+
.plus(EnvInternal_js_1.default.isTest ? { seconds: 15 } : { day: 1 })
|
|
31
|
+
.toSeconds())
|
|
32
|
+
.exec();
|
|
33
|
+
socket.on('disconnect', async () => {
|
|
34
|
+
await redisClient.lrem(key, 1, socket.id);
|
|
35
|
+
});
|
|
36
|
+
const ws = new Ws(['/ops/connection-success']);
|
|
37
|
+
await ws.emit(interpretedId, '/ops/connection-success', {
|
|
38
|
+
message: 'Successfully connected to psychic websockets',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
constructor(allowedPaths, { namespace = '/', redisKeyPrefix = 'user', } = {}) {
|
|
42
|
+
this.allowedPaths = allowedPaths;
|
|
43
|
+
this.namespace = namespace;
|
|
44
|
+
this.redisKeyPrefix = redisKeyPrefix;
|
|
45
|
+
}
|
|
46
|
+
boot() {
|
|
47
|
+
if (this.booted)
|
|
48
|
+
return;
|
|
49
|
+
const psychicWebsocketsApp = index_js_1.default.getOrFail();
|
|
50
|
+
this.redisClient = psychicWebsocketsApp.websocketOptions.connection;
|
|
51
|
+
this.io = new redis_emitter_1.Emitter(this.redisClient).of(this.namespace);
|
|
52
|
+
this.booted = true;
|
|
53
|
+
}
|
|
54
|
+
async emit(id, path,
|
|
55
|
+
// eslint-disable-next-line
|
|
56
|
+
data = {}) {
|
|
57
|
+
if (this.allowedPaths.length && !this.allowedPaths.includes(path))
|
|
58
|
+
throw new InvalidWsPathError(path);
|
|
59
|
+
this.boot();
|
|
60
|
+
const socketIds = await this.findSocketIds(id?.isDreamInstance ? id.primaryKeyValue : id);
|
|
61
|
+
for (const socketId of socketIds) {
|
|
62
|
+
this.io.to(socketId).emit(path, data);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async findSocketIds(id) {
|
|
66
|
+
this.boot();
|
|
67
|
+
return (0, dream_1.uniq)(await this.redisClient.lrange(this.redisKey(id), 0, -1));
|
|
68
|
+
}
|
|
69
|
+
redisKey(userId) {
|
|
70
|
+
return (0, redisWsKey_js_1.default)(userId, this.redisKeyPrefix);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.default = Ws;
|
|
74
|
+
class InvalidWsPathError extends Error {
|
|
75
|
+
invalidPath;
|
|
76
|
+
constructor(invalidPath) {
|
|
77
|
+
super();
|
|
78
|
+
this.invalidPath = invalidPath;
|
|
79
|
+
}
|
|
80
|
+
get message() {
|
|
81
|
+
return `
|
|
82
|
+
Invalid path passed to Ws: "${this.invalidPath}"
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.InvalidWsPathError = InvalidWsPathError;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class MissingWsRedisConnection extends Error {
|
|
4
|
+
get message() {
|
|
5
|
+
return `
|
|
6
|
+
No websocket redis connection was found, even though
|
|
7
|
+
the application is configured to establish websockets.
|
|
8
|
+
|
|
9
|
+
In conf/app.ts, either:
|
|
10
|
+
|
|
11
|
+
1.) disable websockets by omitting the call to psy.set('websockets', ...), OR
|
|
12
|
+
2.) provide a redis connection for your websockets, as shown below:
|
|
13
|
+
|
|
14
|
+
export default async (psy: PsychicApplication) => {
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
psy.set('websockets', {
|
|
18
|
+
connection: new Redis({
|
|
19
|
+
host: AppEnv.string('WS_REDIS_HOST'),
|
|
20
|
+
port: AppEnv.integer('WS_REDIS_PORT'),
|
|
21
|
+
username: AppEnv.string('WS_REDIS_USERNAME', { optional: true }),
|
|
22
|
+
password: AppEnv.string('WS_REDIS_PASSWORD', { optional: true }),
|
|
23
|
+
tls: AppEnv.isProduction ? {} : undefined,
|
|
24
|
+
maxRetriesPerRequest: null,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.default = MissingWsRedisConnection;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Ws = exports.Cable = exports.PsychicApplicationWebsockets = void 0;
|
|
4
|
+
var index_js_1 = require("./psychic-application-websockets/index.js");
|
|
5
|
+
Object.defineProperty(exports, "PsychicApplicationWebsockets", { enumerable: true, get: function () { return index_js_1.default; } });
|
|
6
|
+
var index_js_2 = require("./cable/index.js");
|
|
7
|
+
Object.defineProperty(exports, "Cable", { enumerable: true, get: function () { return index_js_2.default; } });
|
|
8
|
+
var ws_js_1 = require("./cable/ws.js");
|
|
9
|
+
Object.defineProperty(exports, "Ws", { enumerable: true, get: function () { return ws_js_1.default; } });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cachePsychicApplicationWebsockets = cachePsychicApplicationWebsockets;
|
|
4
|
+
exports.getCachedPsychicApplicationWebsockets = getCachedPsychicApplicationWebsockets;
|
|
5
|
+
exports.getCachedPsychicApplicationWebsocketsOrFail = getCachedPsychicApplicationWebsocketsOrFail;
|
|
6
|
+
let _psychicAppWebsockets = undefined;
|
|
7
|
+
function cachePsychicApplicationWebsockets(psychicAppWebsockets) {
|
|
8
|
+
_psychicAppWebsockets = psychicAppWebsockets;
|
|
9
|
+
}
|
|
10
|
+
function getCachedPsychicApplicationWebsockets() {
|
|
11
|
+
return _psychicAppWebsockets;
|
|
12
|
+
}
|
|
13
|
+
function getCachedPsychicApplicationWebsocketsOrFail() {
|
|
14
|
+
if (!_psychicAppWebsockets)
|
|
15
|
+
throw new Error('must call `cachePsychicApplicationWebsockets` before loading cached psychic application websockets');
|
|
16
|
+
return _psychicAppWebsockets;
|
|
17
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_js_1 = require("../cable/index.js");
|
|
4
|
+
const cache_js_1 = require("./cache.js");
|
|
5
|
+
class PsychicApplicationWebsockets {
|
|
6
|
+
static async init(psychicApp, cb) {
|
|
7
|
+
const psychicWsApp = new PsychicApplicationWebsockets(psychicApp);
|
|
8
|
+
await cb(psychicWsApp);
|
|
9
|
+
psychicApp.on('server:shutdown', async (psychicServer) => {
|
|
10
|
+
const cable = psychicServer.$attached.cable;
|
|
11
|
+
await cable?.stop();
|
|
12
|
+
});
|
|
13
|
+
psychicApp.override('server:start', async (psychicServer, { port, withFrontEndClient, frontEndPort }) => {
|
|
14
|
+
const cable = new index_js_1.default(psychicServer.expressApp, psychicWsApp);
|
|
15
|
+
await cable.start(port, {
|
|
16
|
+
withFrontEndClient,
|
|
17
|
+
frontEndPort,
|
|
18
|
+
});
|
|
19
|
+
psychicServer.attach('cable', cable);
|
|
20
|
+
return cable.httpServer;
|
|
21
|
+
});
|
|
22
|
+
(0, cache_js_1.cachePsychicApplicationWebsockets)(psychicWsApp);
|
|
23
|
+
return psychicWsApp;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns the cached psychic application if it has been set.
|
|
27
|
+
* If it has not been set, an exception is raised.
|
|
28
|
+
*
|
|
29
|
+
* The psychic application can be set by calling PsychicApplication#init
|
|
30
|
+
*/
|
|
31
|
+
static getOrFail() {
|
|
32
|
+
return (0, cache_js_1.getCachedPsychicApplicationWebsocketsOrFail)();
|
|
33
|
+
}
|
|
34
|
+
psychicApp;
|
|
35
|
+
static log(...args) {
|
|
36
|
+
const psychicWebsocketsApp = this.getOrFail();
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
38
|
+
return psychicWebsocketsApp.psychicApp.constructor.log(...args);
|
|
39
|
+
}
|
|
40
|
+
constructor(psychicApp) {
|
|
41
|
+
this.psychicApp = psychicApp;
|
|
42
|
+
}
|
|
43
|
+
_websocketOptions;
|
|
44
|
+
get websocketOptions() {
|
|
45
|
+
return this._websocketOptions;
|
|
46
|
+
}
|
|
47
|
+
_hooks = {
|
|
48
|
+
wsStart: [],
|
|
49
|
+
wsConnect: [],
|
|
50
|
+
};
|
|
51
|
+
get hooks() {
|
|
52
|
+
return this._hooks;
|
|
53
|
+
}
|
|
54
|
+
on(hookEventType, cb) {
|
|
55
|
+
switch (hookEventType) {
|
|
56
|
+
case 'ws:start':
|
|
57
|
+
this._hooks.wsStart.push(cb);
|
|
58
|
+
break;
|
|
59
|
+
case 'ws:connect':
|
|
60
|
+
this._hooks.wsConnect.push(cb);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
throw new Error(`unrecognized event provided to PsychicApplicationWebsockets#on: ${hookEventType}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
set(option, value) {
|
|
67
|
+
switch (option) {
|
|
68
|
+
case 'websockets':
|
|
69
|
+
this._websocketOptions = {
|
|
70
|
+
...value,
|
|
71
|
+
subConnection: value?.connection?.duplicate(),
|
|
72
|
+
};
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`Unhandled option type passed to PsychicApplicationWebsockets#set: ${option}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.default = PsychicApplicationWebsockets;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { PsychicServer } from '@rvoh/psychic';
|
|
2
|
+
import { createAdapter } from '@socket.io/redis-adapter';
|
|
3
|
+
import * as colors from 'colorette';
|
|
4
|
+
import * as socketio from 'socket.io';
|
|
5
|
+
import MissingWsRedisConnection from '../error/ws/MissingWsRedisConnection.js';
|
|
6
|
+
import EnvInternal from '../helpers/EnvInternal.js';
|
|
7
|
+
import PsychicApplicationWebsockets from '../psychic-application-websockets/index.js';
|
|
8
|
+
export default class Cable {
|
|
9
|
+
app;
|
|
10
|
+
io;
|
|
11
|
+
httpServer;
|
|
12
|
+
config;
|
|
13
|
+
redisConnections = [];
|
|
14
|
+
constructor(app, config) {
|
|
15
|
+
this.app = app;
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
connect() {
|
|
19
|
+
if (this.io)
|
|
20
|
+
return;
|
|
21
|
+
// for socket.io, we have to circumvent the normal process for starting a
|
|
22
|
+
// psychic server so that we can bind socket.io to the http instance.
|
|
23
|
+
this.httpServer = PsychicServer.createPsychicHttpInstance(this.app, this.config.psychicApp.sslCredentials);
|
|
24
|
+
this.io = new socketio.Server(this.httpServer, { cors: this.config.psychicApp.corsOptions });
|
|
25
|
+
}
|
|
26
|
+
async start(port, { withFrontEndClient = false, frontEndPort = 3000, } = {}) {
|
|
27
|
+
this.connect();
|
|
28
|
+
for (const hook of this.config.hooks.wsStart) {
|
|
29
|
+
await hook(this.io);
|
|
30
|
+
}
|
|
31
|
+
this.io.on('connect', async (socket) => {
|
|
32
|
+
try {
|
|
33
|
+
for (const hook of this.config.hooks.wsConnect) {
|
|
34
|
+
await hook(socket);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (EnvInternal.boolean('PSYCHIC_DANGEROUSLY_PERMIT_WS_EXCEPTIONS'))
|
|
39
|
+
throw error;
|
|
40
|
+
else {
|
|
41
|
+
;
|
|
42
|
+
this.config.psychicApp.constructor.logWithLevel('error', `
|
|
43
|
+
An exception was caught in your websocket thread.
|
|
44
|
+
To prevent your server from crashing, we are rescuing this error here for you.
|
|
45
|
+
If you would like us to raise this exception, make sure to set
|
|
46
|
+
|
|
47
|
+
PSYCHIC_DANGEROUSLY_PERMIT_WS_EXCEPTIONS=1
|
|
48
|
+
|
|
49
|
+
the error received is:
|
|
50
|
+
|
|
51
|
+
${error.message}
|
|
52
|
+
`);
|
|
53
|
+
console.trace();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.bindToRedis();
|
|
58
|
+
const psychicAppWebsockets = PsychicApplicationWebsockets.getOrFail();
|
|
59
|
+
await this.listen({
|
|
60
|
+
port: parseInt((port || psychicAppWebsockets.psychicApp.port).toString()),
|
|
61
|
+
withFrontEndClient,
|
|
62
|
+
frontEndPort,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async stop() {
|
|
66
|
+
try {
|
|
67
|
+
await this.io?.close();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// noop
|
|
71
|
+
}
|
|
72
|
+
for (const connection of this.redisConnections) {
|
|
73
|
+
try {
|
|
74
|
+
connection.disconnect();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// noop
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async listen({ port, withFrontEndClient, frontEndPort, }) {
|
|
82
|
+
return new Promise(accept => {
|
|
83
|
+
this.httpServer.listen(port, () => {
|
|
84
|
+
if (!EnvInternal.isTest) {
|
|
85
|
+
const app = PsychicApplicationWebsockets.getOrFail().psychicApp;
|
|
86
|
+
app.logger.info(PsychicServer.asciiLogo());
|
|
87
|
+
app.logger.info('\n');
|
|
88
|
+
app.logger.info(colors.cyan('socket server started '));
|
|
89
|
+
app.logger.info(colors.cyan(`psychic dev server started at port ${colors.bgBlueBright(colors.green(port))}`));
|
|
90
|
+
if (withFrontEndClient)
|
|
91
|
+
app.logger.info(`client server started at port ${colors.cyan(frontEndPort)}`);
|
|
92
|
+
app.logger.info('\n');
|
|
93
|
+
}
|
|
94
|
+
accept(true);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
bindToRedis() {
|
|
99
|
+
const pubClient = this.config.websocketOptions.connection;
|
|
100
|
+
const subClient = this.config.websocketOptions.subConnection;
|
|
101
|
+
if (!pubClient || !subClient)
|
|
102
|
+
throw new MissingWsRedisConnection();
|
|
103
|
+
this.redisConnections.push(pubClient);
|
|
104
|
+
this.redisConnections.push(subClient);
|
|
105
|
+
pubClient.on('error', error => {
|
|
106
|
+
PsychicApplicationWebsockets.log('PUB CLIENT ERROR', error);
|
|
107
|
+
});
|
|
108
|
+
subClient.on('error', error => {
|
|
109
|
+
PsychicApplicationWebsockets.log('sub CLIENT ERROR', error);
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
this.io.adapter(createAdapter(pubClient, subClient));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
PsychicApplicationWebsockets.log('FAILED TO ADAPT', error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { uniq } from '@rvoh/dream';
|
|
2
|
+
import { Emitter } from '@socket.io/redis-emitter';
|
|
3
|
+
import { DateTime } from 'luxon';
|
|
4
|
+
import EnvInternal from '../helpers/EnvInternal.js';
|
|
5
|
+
import redisWsKey from './redisWsKey.js';
|
|
6
|
+
import PsychicApplicationWebsockets from '../psychic-application-websockets/index.js';
|
|
7
|
+
export default class Ws {
|
|
8
|
+
allowedPaths;
|
|
9
|
+
io;
|
|
10
|
+
redisClient;
|
|
11
|
+
booted = false;
|
|
12
|
+
namespace;
|
|
13
|
+
redisKeyPrefix;
|
|
14
|
+
static async register(socket, id, redisKeyPrefix = 'user') {
|
|
15
|
+
const psychicWebsocketsApp = PsychicApplicationWebsockets.getOrFail();
|
|
16
|
+
const redisClient = psychicWebsocketsApp.websocketOptions.connection;
|
|
17
|
+
const interpretedId = id?.isDreamInstance ? id.primaryKeyValue : id;
|
|
18
|
+
const key = redisWsKey(interpretedId, redisKeyPrefix);
|
|
19
|
+
const socketIdsToKeep = await redisClient.lrange(redisWsKey(interpretedId, redisKeyPrefix), -2, -1);
|
|
20
|
+
await redisClient
|
|
21
|
+
.multi()
|
|
22
|
+
.del(key)
|
|
23
|
+
.rpush(key, ...socketIdsToKeep, socket.id)
|
|
24
|
+
.expireat(key,
|
|
25
|
+
// TODO: make this configurable in non-test environments
|
|
26
|
+
DateTime.now()
|
|
27
|
+
.plus(EnvInternal.isTest ? { seconds: 15 } : { day: 1 })
|
|
28
|
+
.toSeconds())
|
|
29
|
+
.exec();
|
|
30
|
+
socket.on('disconnect', async () => {
|
|
31
|
+
await redisClient.lrem(key, 1, socket.id);
|
|
32
|
+
});
|
|
33
|
+
const ws = new Ws(['/ops/connection-success']);
|
|
34
|
+
await ws.emit(interpretedId, '/ops/connection-success', {
|
|
35
|
+
message: 'Successfully connected to psychic websockets',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
constructor(allowedPaths, { namespace = '/', redisKeyPrefix = 'user', } = {}) {
|
|
39
|
+
this.allowedPaths = allowedPaths;
|
|
40
|
+
this.namespace = namespace;
|
|
41
|
+
this.redisKeyPrefix = redisKeyPrefix;
|
|
42
|
+
}
|
|
43
|
+
boot() {
|
|
44
|
+
if (this.booted)
|
|
45
|
+
return;
|
|
46
|
+
const psychicWebsocketsApp = PsychicApplicationWebsockets.getOrFail();
|
|
47
|
+
this.redisClient = psychicWebsocketsApp.websocketOptions.connection;
|
|
48
|
+
this.io = new Emitter(this.redisClient).of(this.namespace);
|
|
49
|
+
this.booted = true;
|
|
50
|
+
}
|
|
51
|
+
async emit(id, path,
|
|
52
|
+
// eslint-disable-next-line
|
|
53
|
+
data = {}) {
|
|
54
|
+
if (this.allowedPaths.length && !this.allowedPaths.includes(path))
|
|
55
|
+
throw new InvalidWsPathError(path);
|
|
56
|
+
this.boot();
|
|
57
|
+
const socketIds = await this.findSocketIds(id?.isDreamInstance ? id.primaryKeyValue : id);
|
|
58
|
+
for (const socketId of socketIds) {
|
|
59
|
+
this.io.to(socketId).emit(path, data);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async findSocketIds(id) {
|
|
63
|
+
this.boot();
|
|
64
|
+
return uniq(await this.redisClient.lrange(this.redisKey(id), 0, -1));
|
|
65
|
+
}
|
|
66
|
+
redisKey(userId) {
|
|
67
|
+
return redisWsKey(userId, this.redisKeyPrefix);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export class InvalidWsPathError extends Error {
|
|
71
|
+
invalidPath;
|
|
72
|
+
constructor(invalidPath) {
|
|
73
|
+
super();
|
|
74
|
+
this.invalidPath = invalidPath;
|
|
75
|
+
}
|
|
76
|
+
get message() {
|
|
77
|
+
return `
|
|
78
|
+
Invalid path passed to Ws: "${this.invalidPath}"
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export default class MissingWsRedisConnection extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
return `
|
|
4
|
+
No websocket redis connection was found, even though
|
|
5
|
+
the application is configured to establish websockets.
|
|
6
|
+
|
|
7
|
+
In conf/app.ts, either:
|
|
8
|
+
|
|
9
|
+
1.) disable websockets by omitting the call to psy.set('websockets', ...), OR
|
|
10
|
+
2.) provide a redis connection for your websockets, as shown below:
|
|
11
|
+
|
|
12
|
+
export default async (psy: PsychicApplication) => {
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
psy.set('websockets', {
|
|
16
|
+
connection: new Redis({
|
|
17
|
+
host: AppEnv.string('WS_REDIS_HOST'),
|
|
18
|
+
port: AppEnv.integer('WS_REDIS_PORT'),
|
|
19
|
+
username: AppEnv.string('WS_REDIS_USERNAME', { optional: true }),
|
|
20
|
+
password: AppEnv.string('WS_REDIS_PASSWORD', { optional: true }),
|
|
21
|
+
tls: AppEnv.isProduction ? {} : undefined,
|
|
22
|
+
maxRetriesPerRequest: null,
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
let _psychicAppWebsockets = undefined;
|
|
2
|
+
export function cachePsychicApplicationWebsockets(psychicAppWebsockets) {
|
|
3
|
+
_psychicAppWebsockets = psychicAppWebsockets;
|
|
4
|
+
}
|
|
5
|
+
export function getCachedPsychicApplicationWebsockets() {
|
|
6
|
+
return _psychicAppWebsockets;
|
|
7
|
+
}
|
|
8
|
+
export function getCachedPsychicApplicationWebsocketsOrFail() {
|
|
9
|
+
if (!_psychicAppWebsockets)
|
|
10
|
+
throw new Error('must call `cachePsychicApplicationWebsockets` before loading cached psychic application websockets');
|
|
11
|
+
return _psychicAppWebsockets;
|
|
12
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Cable from '../cable/index.js';
|
|
2
|
+
import { cachePsychicApplicationWebsockets, getCachedPsychicApplicationWebsocketsOrFail } from './cache.js';
|
|
3
|
+
export default class PsychicApplicationWebsockets {
|
|
4
|
+
static async init(psychicApp, cb) {
|
|
5
|
+
const psychicWsApp = new PsychicApplicationWebsockets(psychicApp);
|
|
6
|
+
await cb(psychicWsApp);
|
|
7
|
+
psychicApp.on('server:shutdown', async (psychicServer) => {
|
|
8
|
+
const cable = psychicServer.$attached.cable;
|
|
9
|
+
await cable?.stop();
|
|
10
|
+
});
|
|
11
|
+
psychicApp.override('server:start', async (psychicServer, { port, withFrontEndClient, frontEndPort }) => {
|
|
12
|
+
const cable = new Cable(psychicServer.expressApp, psychicWsApp);
|
|
13
|
+
await cable.start(port, {
|
|
14
|
+
withFrontEndClient,
|
|
15
|
+
frontEndPort,
|
|
16
|
+
});
|
|
17
|
+
psychicServer.attach('cable', cable);
|
|
18
|
+
return cable.httpServer;
|
|
19
|
+
});
|
|
20
|
+
cachePsychicApplicationWebsockets(psychicWsApp);
|
|
21
|
+
return psychicWsApp;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns the cached psychic application if it has been set.
|
|
25
|
+
* If it has not been set, an exception is raised.
|
|
26
|
+
*
|
|
27
|
+
* The psychic application can be set by calling PsychicApplication#init
|
|
28
|
+
*/
|
|
29
|
+
static getOrFail() {
|
|
30
|
+
return getCachedPsychicApplicationWebsocketsOrFail();
|
|
31
|
+
}
|
|
32
|
+
psychicApp;
|
|
33
|
+
static log(...args) {
|
|
34
|
+
const psychicWebsocketsApp = this.getOrFail();
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
36
|
+
return psychicWebsocketsApp.psychicApp.constructor.log(...args);
|
|
37
|
+
}
|
|
38
|
+
constructor(psychicApp) {
|
|
39
|
+
this.psychicApp = psychicApp;
|
|
40
|
+
}
|
|
41
|
+
_websocketOptions;
|
|
42
|
+
get websocketOptions() {
|
|
43
|
+
return this._websocketOptions;
|
|
44
|
+
}
|
|
45
|
+
_hooks = {
|
|
46
|
+
wsStart: [],
|
|
47
|
+
wsConnect: [],
|
|
48
|
+
};
|
|
49
|
+
get hooks() {
|
|
50
|
+
return this._hooks;
|
|
51
|
+
}
|
|
52
|
+
on(hookEventType, cb) {
|
|
53
|
+
switch (hookEventType) {
|
|
54
|
+
case 'ws:start':
|
|
55
|
+
this._hooks.wsStart.push(cb);
|
|
56
|
+
break;
|
|
57
|
+
case 'ws:connect':
|
|
58
|
+
this._hooks.wsConnect.push(cb);
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`unrecognized event provided to PsychicApplicationWebsockets#on: ${hookEventType}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
set(option, value) {
|
|
65
|
+
switch (option) {
|
|
66
|
+
case 'websockets':
|
|
67
|
+
this._websocketOptions = {
|
|
68
|
+
...value,
|
|
69
|
+
subConnection: value?.connection?.duplicate(),
|
|
70
|
+
};
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Unhandled option type passed to PsychicApplicationWebsockets#set: ${option}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import * as http from 'http';
|
|
3
|
+
import * as socketio from 'socket.io';
|
|
4
|
+
import PsychicApplicationWebsockets from '../psychic-application-websockets/index.js';
|
|
5
|
+
export default class Cable {
|
|
6
|
+
app: Application;
|
|
7
|
+
io: socketio.Server | undefined;
|
|
8
|
+
httpServer: http.Server;
|
|
9
|
+
private config;
|
|
10
|
+
private redisConnections;
|
|
11
|
+
constructor(app: Application, config: PsychicApplicationWebsockets);
|
|
12
|
+
connect(): void;
|
|
13
|
+
start(port?: number, { withFrontEndClient, frontEndPort, }?: {
|
|
14
|
+
withFrontEndClient?: boolean;
|
|
15
|
+
frontEndPort?: number;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
stop(): Promise<void>;
|
|
18
|
+
listen({ port, withFrontEndClient, frontEndPort, }: {
|
|
19
|
+
port: number | string;
|
|
20
|
+
withFrontEndClient: boolean;
|
|
21
|
+
frontEndPort: number;
|
|
22
|
+
}): Promise<unknown>;
|
|
23
|
+
bindToRedis(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Dream, IdType } from '@rvoh/dream';
|
|
2
|
+
import { Emitter } from '@socket.io/redis-emitter';
|
|
3
|
+
import { Socket } from 'socket.io';
|
|
4
|
+
export default class Ws<AllowedPaths extends readonly string[]> {
|
|
5
|
+
allowedPaths: AllowedPaths & readonly string[];
|
|
6
|
+
io: Emitter;
|
|
7
|
+
private redisClient;
|
|
8
|
+
private booted;
|
|
9
|
+
private namespace;
|
|
10
|
+
private redisKeyPrefix;
|
|
11
|
+
static register(socket: Socket, id: IdType | Dream, redisKeyPrefix?: string): Promise<void>;
|
|
12
|
+
constructor(allowedPaths: AllowedPaths & readonly string[], { namespace, redisKeyPrefix, }?: {
|
|
13
|
+
namespace?: string;
|
|
14
|
+
redisKeyPrefix?: string;
|
|
15
|
+
});
|
|
16
|
+
boot(): void;
|
|
17
|
+
emit<T extends Ws<AllowedPaths>, const P extends AllowedPaths[number]>(this: T, id: IdType | Dream, path: P, data?: any): Promise<void>;
|
|
18
|
+
findSocketIds(id: IdType): Promise<string[]>;
|
|
19
|
+
private redisKey;
|
|
20
|
+
}
|
|
21
|
+
export declare class InvalidWsPathError extends Error {
|
|
22
|
+
private invalidPath;
|
|
23
|
+
constructor(invalidPath: string);
|
|
24
|
+
get message(): string;
|
|
25
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Env } from '@rvoh/dream';
|
|
2
|
+
declare const EnvInternal: Env<{
|
|
3
|
+
string: "NODE_ENV" | "PSYCHIC_CORE_DEVELOPMENT";
|
|
4
|
+
boolean: "DEBUG" | "PSYCHIC_CORE_DEVELOPMENT" | "PSYCHIC_DANGEROUSLY_PERMIT_WS_EXCEPTIONS";
|
|
5
|
+
}, "NODE_ENV" | "PSYCHIC_CORE_DEVELOPMENT", never, "PSYCHIC_CORE_DEVELOPMENT" | "DEBUG" | "PSYCHIC_DANGEROUSLY_PERMIT_WS_EXCEPTIONS">;
|
|
6
|
+
export default EnvInternal;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import PsychicApplicationWebsockets from './index.js';
|
|
2
|
+
export declare function cachePsychicApplicationWebsockets(psychicAppWebsockets: PsychicApplicationWebsockets): void;
|
|
3
|
+
export declare function getCachedPsychicApplicationWebsockets(): PsychicApplicationWebsockets | undefined;
|
|
4
|
+
export declare function getCachedPsychicApplicationWebsocketsOrFail(): PsychicApplicationWebsockets;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PsychicApplication } from '@rvoh/psychic';
|
|
2
|
+
import { Cluster, Redis } from 'ioredis';
|
|
3
|
+
import { Socket, Server as SocketServer } from 'socket.io';
|
|
4
|
+
export default class PsychicApplicationWebsockets {
|
|
5
|
+
static init(psychicApp: PsychicApplication, cb: (app: PsychicApplicationWebsockets) => void | Promise<void>): Promise<PsychicApplicationWebsockets>;
|
|
6
|
+
/**
|
|
7
|
+
* Returns the cached psychic application if it has been set.
|
|
8
|
+
* If it has not been set, an exception is raised.
|
|
9
|
+
*
|
|
10
|
+
* The psychic application can be set by calling PsychicApplication#init
|
|
11
|
+
*/
|
|
12
|
+
static getOrFail(): PsychicApplicationWebsockets;
|
|
13
|
+
psychicApp: PsychicApplication;
|
|
14
|
+
static log(...args: Parameters<typeof PsychicApplication.log>): void;
|
|
15
|
+
constructor(psychicApp: PsychicApplication);
|
|
16
|
+
private _websocketOptions;
|
|
17
|
+
get websocketOptions(): PsychicWebsocketOptions & {
|
|
18
|
+
subConnection?: RedisOrRedisClusterConnection;
|
|
19
|
+
};
|
|
20
|
+
private _hooks;
|
|
21
|
+
get hooks(): PsychicApplicationWebsocketsHooks;
|
|
22
|
+
on<T extends PsychicWebsocketsHookEventType>(hookEventType: T, cb: T extends 'ws:start' ? (server: SocketServer) => void | Promise<void> : T extends 'ws:connect' ? (socket: Socket) => void | Promise<void> : never): void;
|
|
23
|
+
set<Opt extends PsychicApplicationWebsocketsOption>(option: Opt, value: unknown): void;
|
|
24
|
+
}
|
|
25
|
+
interface PsychicWebsocketOptions {
|
|
26
|
+
connection: Redis;
|
|
27
|
+
}
|
|
28
|
+
export type PsychicApplicationWebsocketsOption = 'websockets';
|
|
29
|
+
export type PsychicWebsocketsHookEventType = 'ws:start' | 'ws:connect';
|
|
30
|
+
export interface PsychicApplicationWebsocketsHooks {
|
|
31
|
+
wsStart: ((server: SocketServer) => void | Promise<void>)[];
|
|
32
|
+
wsConnect: ((socket: Socket) => void | Promise<void>)[];
|
|
33
|
+
}
|
|
34
|
+
export type RedisOrRedisClusterConnection = Redis | Cluster;
|
|
35
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@rvoh/psychic-websockets",
|
|
4
|
+
"description": "Websocket system for Psychic applications",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"author": "RVOHealth",
|
|
7
|
+
"repository": "https://github.com/rvohealth/psychic-websockets.git",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"main": "./dist/cjs/src/index.js",
|
|
10
|
+
"module": "./dist/esm/src/index.js",
|
|
11
|
+
"types": "./dist/types/src/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/types/src/index.d.ts",
|
|
15
|
+
"import": "./dist/esm/src/index.js",
|
|
16
|
+
"require": "./dist/cjs/src/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/**/*"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"client": "yarn --cwd=./client start",
|
|
24
|
+
"psy": "yarn psyts",
|
|
25
|
+
"psyjs": "node ./dist/test-app/src/cli/index.js",
|
|
26
|
+
"psyts": "node --experimental-specifier-resolution=node --import ./bin/esm.js ./test-app/src/cli/index.ts",
|
|
27
|
+
"build": "echo \"building cjs...\" && rm -rf dist && npx tsc -p ./tsconfig.cjs.build.json && echo \"building esm...\" && npx tsc -p ./tsconfig.esm.build.json",
|
|
28
|
+
"uspec": "vitest --config ./spec/unit/vite.config.ts",
|
|
29
|
+
"fspec": "vitest run --config=./spec/features/vite.config.ts",
|
|
30
|
+
"format": "yarn run prettier . --write",
|
|
31
|
+
"lint": "yarn run eslint --no-warn-ignored \"src/**/*.ts\" && yarn run prettier . --check",
|
|
32
|
+
"dev": "NODE_ENV=development WORKER_COUNT=0 ts-node --transpile-only ./test-app/main.ts",
|
|
33
|
+
"prepack": "yarn build"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@rvoh/dream": "*",
|
|
37
|
+
"@rvoh/psychic": "*",
|
|
38
|
+
"@socket.io/redis-adapter": "*",
|
|
39
|
+
"@socket.io/redis-emitter": "*",
|
|
40
|
+
"ioredis": "*",
|
|
41
|
+
"socket.io": "*",
|
|
42
|
+
"socket.io-adapter": "*"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@eslint/js": "=9.0.0",
|
|
46
|
+
"@rvoh/dream": "^0.29.1",
|
|
47
|
+
"@rvoh/dream-spec-helpers": "^0.1.0",
|
|
48
|
+
"@rvoh/psychic": "^0.24.0",
|
|
49
|
+
"@rvoh/psychic-spec-helpers": "^0.2.0",
|
|
50
|
+
"@socket.io/redis-adapter": "^8.3.0",
|
|
51
|
+
"@socket.io/redis-emitter": "^5.1.0",
|
|
52
|
+
"@types/express": "^4",
|
|
53
|
+
"@types/luxon": "^3.4.2",
|
|
54
|
+
"@types/node": "^22.5.1",
|
|
55
|
+
"@types/pg": "^8",
|
|
56
|
+
"@types/supertest": "^6.0.2",
|
|
57
|
+
"bullmq": "^5.12.12",
|
|
58
|
+
"eslint": "^9.9.1",
|
|
59
|
+
"express": "^4.21.2",
|
|
60
|
+
"ioredis": "^5.4.1",
|
|
61
|
+
"kysely": "^0.27.5",
|
|
62
|
+
"kysely-codegen": "^0.17.0",
|
|
63
|
+
"luxon": "^3.5.0",
|
|
64
|
+
"luxon-jest-matchers": "^0.1.14",
|
|
65
|
+
"pg": "^8.13.1",
|
|
66
|
+
"prettier": "^3.3.3",
|
|
67
|
+
"puppeteer": "^24.4.0",
|
|
68
|
+
"socket.io": "^4.8.1",
|
|
69
|
+
"socket.io-adapter": "^2.5.5",
|
|
70
|
+
"socket.io-client": "^4.8.1",
|
|
71
|
+
"supertest": "^7.0.0",
|
|
72
|
+
"ts-node": "^10.9.2",
|
|
73
|
+
"tslib": "^2.7.0",
|
|
74
|
+
"typedoc": "^0.26.6",
|
|
75
|
+
"typescript": "^5.5.4",
|
|
76
|
+
"typescript-eslint": "=7.18.0",
|
|
77
|
+
"vitest": "^3.0.8"
|
|
78
|
+
},
|
|
79
|
+
"packageManager": "yarn@4.4.1"
|
|
80
|
+
}
|