@rvoh/psychic-websockets 0.2.4 → 0.3.1

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.
@@ -6,7 +6,7 @@ const socketio = require("socket.io");
6
6
  const yoctocolors_1 = require("yoctocolors");
7
7
  const MissingWsRedisConnection_js_1 = require("../error/ws/MissingWsRedisConnection.js");
8
8
  const EnvInternal_js_1 = require("../helpers/EnvInternal.js");
9
- const index_js_1 = require("../psychic-application-websockets/index.js");
9
+ const index_js_1 = require("../psychic-app-websockets/index.js");
10
10
  class Cable {
11
11
  app;
12
12
  io;
@@ -17,6 +17,12 @@ class Cable {
17
17
  this.app = app;
18
18
  this.config = config;
19
19
  }
20
+ /**
21
+ * @internal
22
+ *
23
+ * creates a new http server and binds it to a new socket.io server.
24
+ * this is automatically called when you call `start`.
25
+ */
20
26
  connect() {
21
27
  if (this.io)
22
28
  return;
@@ -25,6 +31,10 @@ class Cable {
25
31
  this.httpServer = psychic_1.PsychicServer.createPsychicHttpInstance(this.app, this.config.psychicApp.sslCredentials);
26
32
  this.io = new socketio.Server(this.httpServer, { cors: this.config.psychicApp.corsOptions });
27
33
  }
34
+ /**
35
+ * builds an http server and a socket.io server, binding to redis
36
+ * to enable redis pubsub, then starts the http server.
37
+ */
28
38
  async start(port) {
29
39
  this.connect();
30
40
  for (const hook of this.config.hooks.wsStart) {
@@ -62,6 +72,9 @@ class Cable {
62
72
  port: parseInt((port || psychicAppWebsockets.psychicApp.port).toString()),
63
73
  });
64
74
  }
75
+ /**
76
+ * stops the socket.io server, closing out of all redis connections
77
+ */
65
78
  async stop() {
66
79
  try {
67
80
  await this.io?.close();
@@ -78,6 +91,11 @@ class Cable {
78
91
  }
79
92
  }
80
93
  }
94
+ /**
95
+ * @internal
96
+ *
97
+ * stops the socket.io server, closing out of all redis connections
98
+ */
81
99
  async listen({ port }) {
82
100
  return new Promise(accept => {
83
101
  this.httpServer.listen(port, () => {
@@ -93,6 +111,11 @@ class Cable {
93
111
  });
94
112
  });
95
113
  }
114
+ /**
115
+ * @internal
116
+ *
117
+ * establishes redis pubsub mechanisms
118
+ */
96
119
  bindToRedis() {
97
120
  const pubClient = this.config.websocketOptions.connection;
98
121
  const subClient = this.config.websocketOptions.subConnection;
@@ -1,18 +1,61 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.InvalidWsPathError = void 0;
4
3
  const dream_1 = require("@rvoh/dream");
5
4
  const redis_emitter_1 = require("@socket.io/redis-emitter");
6
5
  const EnvInternal_js_1 = require("../helpers/EnvInternal.js");
7
- const index_js_1 = require("../psychic-application-websockets/index.js");
6
+ const index_js_1 = require("../psychic-app-websockets/index.js");
8
7
  const redisWsKey_js_1 = require("./redisWsKey.js");
8
+ const InvalidWsPathError_js_1 = require("../error/ws/InvalidWsPathError.js");
9
9
  class Ws {
10
10
  allowedPaths;
11
+ /**
12
+ * @internal
13
+ *
14
+ * the socket.io redis emitter instance, used to emit
15
+ * messages through redis to distributed websocket clusters
16
+ */
11
17
  io;
18
+ /**
19
+ * @internal
20
+ *
21
+ * the redis client used to bind socket.io to the redis emitter
22
+ */
12
23
  redisClient;
24
+ /**
25
+ * @internal
26
+ *
27
+ * the redis client used to bind socket.io to the redis emitter
28
+ */
13
29
  booted = false;
30
+ /**
31
+ * @internal
32
+ *
33
+ * the namespace used when connecting socket.io
34
+ * this will default to '/' if it is not provided
35
+ */
14
36
  namespace;
37
+ /**
38
+ * @internal
39
+ *
40
+ * when registering your application's users with psychic-websockets,
41
+ * you need to provide the following:
42
+ * 1. an identifier for your user (i.e. user.id)
43
+ * 2. a redisKeyPrefix, which is used to prefix your id before storing it in redis
44
+ *
45
+ * this enables you to have multiple namespaces in redis to safely store ids,
46
+ * i.e.
47
+ *
48
+ * `user:1`
49
+ * `admin-user:1`
50
+ */
15
51
  redisKeyPrefix;
52
+ /**
53
+ * call this method to bind a socket to a particular identifier
54
+ *
55
+ * @param socket - the socket.io socket instance
56
+ * @param id - the identifier you wish to bind to this socket instance
57
+ * @param redisKeyPrefix - (optional) the prefix you wish to use to couple to this id (defaults to 'user')
58
+ */
16
59
  static async register(socket, id, redisKeyPrefix = 'user') {
17
60
  const psychicWebsocketsApp = index_js_1.default.getOrFail();
18
61
  const redisClient = psychicWebsocketsApp.websocketOptions.connection;
@@ -37,11 +80,34 @@ class Ws {
37
80
  message: 'Successfully connected to psychic websockets',
38
81
  });
39
82
  }
40
- constructor(allowedPaths, { namespace = '/', redisKeyPrefix = 'user', } = {}) {
83
+ constructor(allowedPaths, {
84
+ /**
85
+ * the namespace used when connecting socket.io
86
+ * this will default to '/' if it is not provided
87
+ */
88
+ namespace = '/',
89
+ /**
90
+ * when registering your application's users with psychic-websockets,
91
+ * you need to provide the following:
92
+ * 1. an identifier for your user (i.e. user.id)
93
+ * 2. a redisKeyPrefix, which is used to prefix your id before storing it in redis
94
+ *
95
+ * this enables you to have multiple namespaces in redis to safely store ids,
96
+ * i.e.
97
+ *
98
+ * `user:1`
99
+ * `admin-user:1`
100
+ */
101
+ redisKeyPrefix = 'user', } = {}) {
41
102
  this.allowedPaths = allowedPaths;
42
103
  this.namespace = namespace;
43
104
  this.redisKeyPrefix = redisKeyPrefix;
44
105
  }
106
+ /**
107
+ * @internal
108
+ *
109
+ * establishes a new socket.io-redis emitter
110
+ */
45
111
  boot() {
46
112
  if (this.booted)
47
113
  return;
@@ -50,36 +116,41 @@ class Ws {
50
116
  this.io = new redis_emitter_1.Emitter(this.redisClient).of(this.namespace);
51
117
  this.booted = true;
52
118
  }
119
+ /**
120
+ * emits data to the requested id (or dream instance) and path
121
+ *
122
+ * ```ts
123
+ * await ws.emit(123, '/ops/howyadoin', { hello: 'world' })
124
+ * ```
125
+ */
53
126
  async emit(id, path,
54
127
  // eslint-disable-next-line
55
128
  data = {}) {
56
129
  if (this.allowedPaths.length && !this.allowedPaths.includes(path))
57
- throw new InvalidWsPathError(path);
130
+ throw new InvalidWsPathError_js_1.default(path);
58
131
  this.boot();
59
132
  const socketIds = await this.findSocketIds(id?.isDreamInstance ? id.primaryKeyValue : id);
60
133
  for (const socketId of socketIds) {
61
134
  this.io.to(socketId).emit(path, data);
62
135
  }
63
136
  }
137
+ /**
138
+ * @internal
139
+ *
140
+ * used to find a redis key matching the id
141
+ */
64
142
  async findSocketIds(id) {
65
143
  this.boot();
66
144
  return (0, dream_1.uniq)(await this.redisClient.lrange(this.redisKey(id), 0, -1));
67
145
  }
146
+ /**
147
+ * @internal
148
+ *
149
+ * builds a redis key using the provided identifier and the redisKeyPrefix provided
150
+ * when this Ws instance was constructed.
151
+ */
68
152
  redisKey(userId) {
69
153
  return (0, redisWsKey_js_1.default)(userId, this.redisKeyPrefix);
70
154
  }
71
155
  }
72
156
  exports.default = Ws;
73
- class InvalidWsPathError extends Error {
74
- invalidPath;
75
- constructor(invalidPath) {
76
- super();
77
- this.invalidPath = invalidPath;
78
- }
79
- get message() {
80
- return `
81
- Invalid path passed to Ws: "${this.invalidPath}"
82
- `;
83
- }
84
- }
85
- exports.InvalidWsPathError = InvalidWsPathError;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class InvalidWsPathError extends Error {
4
+ invalidPath;
5
+ constructor(invalidPath) {
6
+ super();
7
+ this.invalidPath = invalidPath;
8
+ }
9
+ get message() {
10
+ return `
11
+ Invalid path passed to Ws: "${this.invalidPath}"
12
+ `;
13
+ }
14
+ }
15
+ exports.default = InvalidWsPathError;
@@ -11,7 +11,7 @@ In conf/app.ts, either:
11
11
  1.) disable websockets by omitting the call to psy.set('websockets', ...), OR
12
12
  2.) provide a redis connection for your websockets, as shown below:
13
13
 
14
- export default async (psy: PsychicApplication) => {
14
+ export default async (psy: PsychicApp) => {
15
15
  ...
16
16
 
17
17
  psy.set('websockets', {
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
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; } });
3
+ exports.PsychicAppWebsockets = exports.Ws = exports.Cable = void 0;
4
+ var index_js_1 = require("./cable/index.js");
5
+ Object.defineProperty(exports, "Cable", { enumerable: true, get: function () { return index_js_1.default; } });
8
6
  var ws_js_1 = require("./cable/ws.js");
9
7
  Object.defineProperty(exports, "Ws", { enumerable: true, get: function () { return ws_js_1.default; } });
8
+ var index_js_2 = require("./psychic-app-websockets/index.js");
9
+ Object.defineProperty(exports, "PsychicAppWebsockets", { enumerable: true, get: function () { return index_js_2.default; } });
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cachePsychicAppWebsockets = cachePsychicAppWebsockets;
4
+ exports.getCachedPsychicAppWebsockets = getCachedPsychicAppWebsockets;
5
+ exports.getCachedPsychicAppWebsocketsOrFail = getCachedPsychicAppWebsocketsOrFail;
6
+ let _psychicAppWebsockets = undefined;
7
+ function cachePsychicAppWebsockets(psychicAppWebsockets) {
8
+ _psychicAppWebsockets = psychicAppWebsockets;
9
+ }
10
+ function getCachedPsychicAppWebsockets() {
11
+ return _psychicAppWebsockets;
12
+ }
13
+ function getCachedPsychicAppWebsocketsOrFail() {
14
+ if (!_psychicAppWebsockets)
15
+ throw new Error('must call `cachePsychicAppWebsockets` before loading cached psychic application websockets');
16
+ return _psychicAppWebsockets;
17
+ }
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const index_js_1 = require("../cable/index.js");
4
4
  const cache_js_1 = require("./cache.js");
5
- class PsychicApplicationWebsockets {
5
+ class PsychicAppWebsockets {
6
6
  static async init(psychicApp, cb) {
7
- const psychicWsApp = new PsychicApplicationWebsockets(psychicApp);
7
+ const psychicWsApp = new PsychicAppWebsockets(psychicApp);
8
8
  await cb(psychicWsApp);
9
9
  psychicApp.on('server:shutdown', async (psychicServer) => {
10
10
  const cable = psychicServer.$attached.cable;
@@ -20,17 +20,17 @@ class PsychicApplicationWebsockets {
20
20
  psychicServer.attach('cable', cable);
21
21
  return cable.httpServer;
22
22
  });
23
- (0, cache_js_1.cachePsychicApplicationWebsockets)(psychicWsApp);
23
+ (0, cache_js_1.cachePsychicAppWebsockets)(psychicWsApp);
24
24
  return psychicWsApp;
25
25
  }
26
26
  /**
27
27
  * Returns the cached psychic application if it has been set.
28
28
  * If it has not been set, an exception is raised.
29
29
  *
30
- * The psychic application can be set by calling PsychicApplication#init
30
+ * The psychic application can be set by calling PsychicApp#init
31
31
  */
32
32
  static getOrFail() {
33
- return (0, cache_js_1.getCachedPsychicApplicationWebsocketsOrFail)();
33
+ return (0, cache_js_1.getCachedPsychicAppWebsocketsOrFail)();
34
34
  }
35
35
  psychicApp;
36
36
  static log(...args) {
@@ -61,7 +61,7 @@ class PsychicApplicationWebsockets {
61
61
  this._hooks.wsConnect.push(cb);
62
62
  break;
63
63
  default:
64
- throw new Error(`unrecognized event provided to PsychicApplicationWebsockets#on: ${hookEventType}`);
64
+ throw new Error(`unrecognized event provided to PsychicAppWebsockets#on: ${hookEventType}`);
65
65
  }
66
66
  }
67
67
  set(option, value) {
@@ -79,8 +79,8 @@ class PsychicApplicationWebsockets {
79
79
  };
80
80
  break;
81
81
  default:
82
- throw new Error(`Unhandled option type passed to PsychicApplicationWebsockets#set: ${option}`);
82
+ throw new Error(`Unhandled option type passed to PsychicAppWebsockets#set: ${option}`);
83
83
  }
84
84
  }
85
85
  }
86
- exports.default = PsychicApplicationWebsockets;
86
+ exports.default = PsychicAppWebsockets;
@@ -4,7 +4,7 @@ import * as socketio from 'socket.io';
4
4
  import colors from 'yoctocolors';
5
5
  import MissingWsRedisConnection from '../error/ws/MissingWsRedisConnection.js';
6
6
  import EnvInternal from '../helpers/EnvInternal.js';
7
- import PsychicApplicationWebsockets from '../psychic-application-websockets/index.js';
7
+ import PsychicAppWebsockets from '../psychic-app-websockets/index.js';
8
8
  export default class Cable {
9
9
  app;
10
10
  io;
@@ -15,6 +15,12 @@ export default class Cable {
15
15
  this.app = app;
16
16
  this.config = config;
17
17
  }
18
+ /**
19
+ * @internal
20
+ *
21
+ * creates a new http server and binds it to a new socket.io server.
22
+ * this is automatically called when you call `start`.
23
+ */
18
24
  connect() {
19
25
  if (this.io)
20
26
  return;
@@ -23,6 +29,10 @@ export default class Cable {
23
29
  this.httpServer = PsychicServer.createPsychicHttpInstance(this.app, this.config.psychicApp.sslCredentials);
24
30
  this.io = new socketio.Server(this.httpServer, { cors: this.config.psychicApp.corsOptions });
25
31
  }
32
+ /**
33
+ * builds an http server and a socket.io server, binding to redis
34
+ * to enable redis pubsub, then starts the http server.
35
+ */
26
36
  async start(port) {
27
37
  this.connect();
28
38
  for (const hook of this.config.hooks.wsStart) {
@@ -55,11 +65,14 @@ export default class Cable {
55
65
  }
56
66
  });
57
67
  this.bindToRedis();
58
- const psychicAppWebsockets = PsychicApplicationWebsockets.getOrFail();
68
+ const psychicAppWebsockets = PsychicAppWebsockets.getOrFail();
59
69
  await this.listen({
60
70
  port: parseInt((port || psychicAppWebsockets.psychicApp.port).toString()),
61
71
  });
62
72
  }
73
+ /**
74
+ * stops the socket.io server, closing out of all redis connections
75
+ */
63
76
  async stop() {
64
77
  try {
65
78
  await this.io?.close();
@@ -76,11 +89,16 @@ export default class Cable {
76
89
  }
77
90
  }
78
91
  }
92
+ /**
93
+ * @internal
94
+ *
95
+ * stops the socket.io server, closing out of all redis connections
96
+ */
79
97
  async listen({ port }) {
80
98
  return new Promise(accept => {
81
99
  this.httpServer.listen(port, () => {
82
100
  if (!EnvInternal.isTest) {
83
- const app = PsychicApplicationWebsockets.getOrFail().psychicApp;
101
+ const app = PsychicAppWebsockets.getOrFail().psychicApp;
84
102
  app.logger.info(PsychicServer.asciiLogo());
85
103
  app.logger.info('\n');
86
104
  app.logger.info(colors.cyan('socket server started '));
@@ -91,6 +109,11 @@ export default class Cable {
91
109
  });
92
110
  });
93
111
  }
112
+ /**
113
+ * @internal
114
+ *
115
+ * establishes redis pubsub mechanisms
116
+ */
94
117
  bindToRedis() {
95
118
  const pubClient = this.config.websocketOptions.connection;
96
119
  const subClient = this.config.websocketOptions.subConnection;
@@ -99,16 +122,16 @@ export default class Cable {
99
122
  this.redisConnections.push(pubClient);
100
123
  this.redisConnections.push(subClient);
101
124
  pubClient.on('error', error => {
102
- PsychicApplicationWebsockets.log('PUB CLIENT ERROR', error);
125
+ PsychicAppWebsockets.log('PUB CLIENT ERROR', error);
103
126
  });
104
127
  subClient.on('error', error => {
105
- PsychicApplicationWebsockets.log('sub CLIENT ERROR', error);
128
+ PsychicAppWebsockets.log('sub CLIENT ERROR', error);
106
129
  });
107
130
  try {
108
131
  this.io.adapter(createAdapter(pubClient, subClient));
109
132
  }
110
133
  catch (error) {
111
- PsychicApplicationWebsockets.log('FAILED TO ADAPT', error);
134
+ PsychicAppWebsockets.log('FAILED TO ADAPT', error);
112
135
  }
113
136
  }
114
137
  }
@@ -1,17 +1,61 @@
1
1
  import { DateTime, uniq } from '@rvoh/dream';
2
2
  import { Emitter } from '@socket.io/redis-emitter';
3
3
  import EnvInternal from '../helpers/EnvInternal.js';
4
- import PsychicApplicationWebsockets from '../psychic-application-websockets/index.js';
4
+ import PsychicAppWebsockets from '../psychic-app-websockets/index.js';
5
5
  import redisWsKey from './redisWsKey.js';
6
+ import InvalidWsPathError from '../error/ws/InvalidWsPathError.js';
6
7
  export default class Ws {
7
8
  allowedPaths;
9
+ /**
10
+ * @internal
11
+ *
12
+ * the socket.io redis emitter instance, used to emit
13
+ * messages through redis to distributed websocket clusters
14
+ */
8
15
  io;
16
+ /**
17
+ * @internal
18
+ *
19
+ * the redis client used to bind socket.io to the redis emitter
20
+ */
9
21
  redisClient;
22
+ /**
23
+ * @internal
24
+ *
25
+ * the redis client used to bind socket.io to the redis emitter
26
+ */
10
27
  booted = false;
28
+ /**
29
+ * @internal
30
+ *
31
+ * the namespace used when connecting socket.io
32
+ * this will default to '/' if it is not provided
33
+ */
11
34
  namespace;
35
+ /**
36
+ * @internal
37
+ *
38
+ * when registering your application's users with psychic-websockets,
39
+ * you need to provide the following:
40
+ * 1. an identifier for your user (i.e. user.id)
41
+ * 2. a redisKeyPrefix, which is used to prefix your id before storing it in redis
42
+ *
43
+ * this enables you to have multiple namespaces in redis to safely store ids,
44
+ * i.e.
45
+ *
46
+ * `user:1`
47
+ * `admin-user:1`
48
+ */
12
49
  redisKeyPrefix;
50
+ /**
51
+ * call this method to bind a socket to a particular identifier
52
+ *
53
+ * @param socket - the socket.io socket instance
54
+ * @param id - the identifier you wish to bind to this socket instance
55
+ * @param redisKeyPrefix - (optional) the prefix you wish to use to couple to this id (defaults to 'user')
56
+ */
13
57
  static async register(socket, id, redisKeyPrefix = 'user') {
14
- const psychicWebsocketsApp = PsychicApplicationWebsockets.getOrFail();
58
+ const psychicWebsocketsApp = PsychicAppWebsockets.getOrFail();
15
59
  const redisClient = psychicWebsocketsApp.websocketOptions.connection;
16
60
  const interpretedId = id?.isDreamInstance ? id.primaryKeyValue : id;
17
61
  const key = redisWsKey(interpretedId, redisKeyPrefix);
@@ -34,19 +78,49 @@ export default class Ws {
34
78
  message: 'Successfully connected to psychic websockets',
35
79
  });
36
80
  }
37
- constructor(allowedPaths, { namespace = '/', redisKeyPrefix = 'user', } = {}) {
81
+ constructor(allowedPaths, {
82
+ /**
83
+ * the namespace used when connecting socket.io
84
+ * this will default to '/' if it is not provided
85
+ */
86
+ namespace = '/',
87
+ /**
88
+ * when registering your application's users with psychic-websockets,
89
+ * you need to provide the following:
90
+ * 1. an identifier for your user (i.e. user.id)
91
+ * 2. a redisKeyPrefix, which is used to prefix your id before storing it in redis
92
+ *
93
+ * this enables you to have multiple namespaces in redis to safely store ids,
94
+ * i.e.
95
+ *
96
+ * `user:1`
97
+ * `admin-user:1`
98
+ */
99
+ redisKeyPrefix = 'user', } = {}) {
38
100
  this.allowedPaths = allowedPaths;
39
101
  this.namespace = namespace;
40
102
  this.redisKeyPrefix = redisKeyPrefix;
41
103
  }
104
+ /**
105
+ * @internal
106
+ *
107
+ * establishes a new socket.io-redis emitter
108
+ */
42
109
  boot() {
43
110
  if (this.booted)
44
111
  return;
45
- const psychicWebsocketsApp = PsychicApplicationWebsockets.getOrFail();
112
+ const psychicWebsocketsApp = PsychicAppWebsockets.getOrFail();
46
113
  this.redisClient = psychicWebsocketsApp.websocketOptions.connection;
47
114
  this.io = new Emitter(this.redisClient).of(this.namespace);
48
115
  this.booted = true;
49
116
  }
117
+ /**
118
+ * emits data to the requested id (or dream instance) and path
119
+ *
120
+ * ```ts
121
+ * await ws.emit(123, '/ops/howyadoin', { hello: 'world' })
122
+ * ```
123
+ */
50
124
  async emit(id, path,
51
125
  // eslint-disable-next-line
52
126
  data = {}) {
@@ -58,23 +132,22 @@ export default class Ws {
58
132
  this.io.to(socketId).emit(path, data);
59
133
  }
60
134
  }
135
+ /**
136
+ * @internal
137
+ *
138
+ * used to find a redis key matching the id
139
+ */
61
140
  async findSocketIds(id) {
62
141
  this.boot();
63
142
  return uniq(await this.redisClient.lrange(this.redisKey(id), 0, -1));
64
143
  }
144
+ /**
145
+ * @internal
146
+ *
147
+ * builds a redis key using the provided identifier and the redisKeyPrefix provided
148
+ * when this Ws instance was constructed.
149
+ */
65
150
  redisKey(userId) {
66
151
  return redisWsKey(userId, this.redisKeyPrefix);
67
152
  }
68
153
  }
69
- export class InvalidWsPathError extends Error {
70
- invalidPath;
71
- constructor(invalidPath) {
72
- super();
73
- this.invalidPath = invalidPath;
74
- }
75
- get message() {
76
- return `
77
- Invalid path passed to Ws: "${this.invalidPath}"
78
- `;
79
- }
80
- }
@@ -0,0 +1,12 @@
1
+ export default class InvalidWsPathError extends Error {
2
+ invalidPath;
3
+ constructor(invalidPath) {
4
+ super();
5
+ this.invalidPath = invalidPath;
6
+ }
7
+ get message() {
8
+ return `
9
+ Invalid path passed to Ws: "${this.invalidPath}"
10
+ `;
11
+ }
12
+ }
@@ -9,7 +9,7 @@ In conf/app.ts, either:
9
9
  1.) disable websockets by omitting the call to psy.set('websockets', ...), OR
10
10
  2.) provide a redis connection for your websockets, as shown below:
11
11
 
12
- export default async (psy: PsychicApplication) => {
12
+ export default async (psy: PsychicApp) => {
13
13
  ...
14
14
 
15
15
  psy.set('websockets', {
@@ -1,3 +1,3 @@
1
- export { default as PsychicApplicationWebsockets } from './psychic-application-websockets/index.js';
2
1
  export { default as Cable } from './cable/index.js';
3
2
  export { default as Ws } from './cable/ws.js';
3
+ export { default as PsychicAppWebsockets } from './psychic-app-websockets/index.js';
@@ -0,0 +1,12 @@
1
+ let _psychicAppWebsockets = undefined;
2
+ export function cachePsychicAppWebsockets(psychicAppWebsockets) {
3
+ _psychicAppWebsockets = psychicAppWebsockets;
4
+ }
5
+ export function getCachedPsychicAppWebsockets() {
6
+ return _psychicAppWebsockets;
7
+ }
8
+ export function getCachedPsychicAppWebsocketsOrFail() {
9
+ if (!_psychicAppWebsockets)
10
+ throw new Error('must call `cachePsychicAppWebsockets` before loading cached psychic application websockets');
11
+ return _psychicAppWebsockets;
12
+ }
@@ -1,8 +1,8 @@
1
1
  import Cable from '../cable/index.js';
2
- import { cachePsychicApplicationWebsockets, getCachedPsychicApplicationWebsocketsOrFail } from './cache.js';
3
- export default class PsychicApplicationWebsockets {
2
+ import { cachePsychicAppWebsockets, getCachedPsychicAppWebsocketsOrFail } from './cache.js';
3
+ export default class PsychicAppWebsockets {
4
4
  static async init(psychicApp, cb) {
5
- const psychicWsApp = new PsychicApplicationWebsockets(psychicApp);
5
+ const psychicWsApp = new PsychicAppWebsockets(psychicApp);
6
6
  await cb(psychicWsApp);
7
7
  psychicApp.on('server:shutdown', async (psychicServer) => {
8
8
  const cable = psychicServer.$attached.cable;
@@ -18,17 +18,17 @@ export default class PsychicApplicationWebsockets {
18
18
  psychicServer.attach('cable', cable);
19
19
  return cable.httpServer;
20
20
  });
21
- cachePsychicApplicationWebsockets(psychicWsApp);
21
+ cachePsychicAppWebsockets(psychicWsApp);
22
22
  return psychicWsApp;
23
23
  }
24
24
  /**
25
25
  * Returns the cached psychic application if it has been set.
26
26
  * If it has not been set, an exception is raised.
27
27
  *
28
- * The psychic application can be set by calling PsychicApplication#init
28
+ * The psychic application can be set by calling PsychicApp#init
29
29
  */
30
30
  static getOrFail() {
31
- return getCachedPsychicApplicationWebsocketsOrFail();
31
+ return getCachedPsychicAppWebsocketsOrFail();
32
32
  }
33
33
  psychicApp;
34
34
  static log(...args) {
@@ -59,7 +59,7 @@ export default class PsychicApplicationWebsockets {
59
59
  this._hooks.wsConnect.push(cb);
60
60
  break;
61
61
  default:
62
- throw new Error(`unrecognized event provided to PsychicApplicationWebsockets#on: ${hookEventType}`);
62
+ throw new Error(`unrecognized event provided to PsychicAppWebsockets#on: ${hookEventType}`);
63
63
  }
64
64
  }
65
65
  set(option, value) {
@@ -77,7 +77,7 @@ export default class PsychicApplicationWebsockets {
77
77
  };
78
78
  break;
79
79
  default:
80
- throw new Error(`Unhandled option type passed to PsychicApplicationWebsockets#set: ${option}`);
80
+ throw new Error(`Unhandled option type passed to PsychicAppWebsockets#set: ${option}`);
81
81
  }
82
82
  }
83
83
  }
@@ -1,19 +1,42 @@
1
- import { Application } from 'express';
1
+ import { Express } from 'express';
2
2
  import * as http from 'http';
3
3
  import * as socketio from 'socket.io';
4
- import PsychicApplicationWebsockets from '../psychic-application-websockets/index.js';
4
+ import PsychicAppWebsockets from '../psychic-app-websockets/index.js';
5
5
  export default class Cable {
6
- app: Application;
6
+ app: Express;
7
7
  io: socketio.Server | undefined;
8
8
  httpServer: http.Server;
9
9
  private config;
10
10
  private redisConnections;
11
- constructor(app: Application, config: PsychicApplicationWebsockets);
11
+ constructor(app: Express, config: PsychicAppWebsockets);
12
+ /**
13
+ * @internal
14
+ *
15
+ * creates a new http server and binds it to a new socket.io server.
16
+ * this is automatically called when you call `start`.
17
+ */
12
18
  connect(): void;
19
+ /**
20
+ * builds an http server and a socket.io server, binding to redis
21
+ * to enable redis pubsub, then starts the http server.
22
+ */
13
23
  start(port?: number): Promise<void>;
24
+ /**
25
+ * stops the socket.io server, closing out of all redis connections
26
+ */
14
27
  stop(): Promise<void>;
28
+ /**
29
+ * @internal
30
+ *
31
+ * stops the socket.io server, closing out of all redis connections
32
+ */
15
33
  listen({ port }: {
16
34
  port: number | string;
17
35
  }): Promise<unknown>;
36
+ /**
37
+ * @internal
38
+ *
39
+ * establishes redis pubsub mechanisms
40
+ */
18
41
  bindToRedis(): void;
19
42
  }
@@ -3,23 +3,102 @@ import { Emitter } from '@socket.io/redis-emitter';
3
3
  import { Socket } from 'socket.io';
4
4
  export default class Ws<AllowedPaths extends readonly string[]> {
5
5
  allowedPaths: AllowedPaths & readonly string[];
6
+ /**
7
+ * @internal
8
+ *
9
+ * the socket.io redis emitter instance, used to emit
10
+ * messages through redis to distributed websocket clusters
11
+ */
6
12
  io: Emitter;
13
+ /**
14
+ * @internal
15
+ *
16
+ * the redis client used to bind socket.io to the redis emitter
17
+ */
7
18
  private redisClient;
19
+ /**
20
+ * @internal
21
+ *
22
+ * the redis client used to bind socket.io to the redis emitter
23
+ */
8
24
  private booted;
25
+ /**
26
+ * @internal
27
+ *
28
+ * the namespace used when connecting socket.io
29
+ * this will default to '/' if it is not provided
30
+ */
9
31
  private namespace;
32
+ /**
33
+ * @internal
34
+ *
35
+ * when registering your application's users with psychic-websockets,
36
+ * you need to provide the following:
37
+ * 1. an identifier for your user (i.e. user.id)
38
+ * 2. a redisKeyPrefix, which is used to prefix your id before storing it in redis
39
+ *
40
+ * this enables you to have multiple namespaces in redis to safely store ids,
41
+ * i.e.
42
+ *
43
+ * `user:1`
44
+ * `admin-user:1`
45
+ */
10
46
  private redisKeyPrefix;
47
+ /**
48
+ * call this method to bind a socket to a particular identifier
49
+ *
50
+ * @param socket - the socket.io socket instance
51
+ * @param id - the identifier you wish to bind to this socket instance
52
+ * @param redisKeyPrefix - (optional) the prefix you wish to use to couple to this id (defaults to 'user')
53
+ */
11
54
  static register(socket: Socket, id: IdType | Dream, redisKeyPrefix?: string): Promise<void>;
12
- constructor(allowedPaths: AllowedPaths & readonly string[], { namespace, redisKeyPrefix, }?: {
55
+ constructor(allowedPaths: AllowedPaths & readonly string[], {
56
+ /**
57
+ * the namespace used when connecting socket.io
58
+ * this will default to '/' if it is not provided
59
+ */
60
+ namespace,
61
+ /**
62
+ * when registering your application's users with psychic-websockets,
63
+ * you need to provide the following:
64
+ * 1. an identifier for your user (i.e. user.id)
65
+ * 2. a redisKeyPrefix, which is used to prefix your id before storing it in redis
66
+ *
67
+ * this enables you to have multiple namespaces in redis to safely store ids,
68
+ * i.e.
69
+ *
70
+ * `user:1`
71
+ * `admin-user:1`
72
+ */
73
+ redisKeyPrefix, }?: {
13
74
  namespace?: string;
14
75
  redisKeyPrefix?: string;
15
76
  });
77
+ /**
78
+ * @internal
79
+ *
80
+ * establishes a new socket.io-redis emitter
81
+ */
16
82
  boot(): void;
83
+ /**
84
+ * emits data to the requested id (or dream instance) and path
85
+ *
86
+ * ```ts
87
+ * await ws.emit(123, '/ops/howyadoin', { hello: 'world' })
88
+ * ```
89
+ */
17
90
  emit<T extends Ws<AllowedPaths>, const P extends AllowedPaths[number]>(this: T, id: IdType | Dream, path: P, data?: any): Promise<void>;
91
+ /**
92
+ * @internal
93
+ *
94
+ * used to find a redis key matching the id
95
+ */
18
96
  findSocketIds(id: IdType): Promise<string[]>;
97
+ /**
98
+ * @internal
99
+ *
100
+ * builds a redis key using the provided identifier and the redisKeyPrefix provided
101
+ * when this Ws instance was constructed.
102
+ */
19
103
  private redisKey;
20
104
  }
21
- export declare class InvalidWsPathError extends Error {
22
- private invalidPath;
23
- constructor(invalidPath: string);
24
- get message(): string;
25
- }
@@ -0,0 +1,5 @@
1
+ export default class InvalidWsPathError extends Error {
2
+ private invalidPath;
3
+ constructor(invalidPath: string);
4
+ get message(): string;
5
+ }
@@ -1,3 +1,3 @@
1
- export { default as PsychicApplicationWebsockets } from './psychic-application-websockets/index.js';
2
1
  export { default as Cable } from './cable/index.js';
3
2
  export { default as Ws } from './cable/ws.js';
3
+ export { default as PsychicAppWebsockets } from './psychic-app-websockets/index.js';
@@ -0,0 +1,4 @@
1
+ import PsychicAppWebsockets from './index.js';
2
+ export declare function cachePsychicAppWebsockets(psychicAppWebsockets: PsychicAppWebsockets): void;
3
+ export declare function getCachedPsychicAppWebsockets(): PsychicAppWebsockets | undefined;
4
+ export declare function getCachedPsychicAppWebsocketsOrFail(): PsychicAppWebsockets;
@@ -1,33 +1,33 @@
1
- import { PsychicApplication } from '@rvoh/psychic';
1
+ import { PsychicApp } from '@rvoh/psychic';
2
2
  import { Cluster, Redis } from 'ioredis';
3
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>;
4
+ export default class PsychicAppWebsockets {
5
+ static init(psychicApp: PsychicApp, cb: (app: PsychicAppWebsockets) => void | Promise<void>): Promise<PsychicAppWebsockets>;
6
6
  /**
7
7
  * Returns the cached psychic application if it has been set.
8
8
  * If it has not been set, an exception is raised.
9
9
  *
10
- * The psychic application can be set by calling PsychicApplication#init
10
+ * The psychic application can be set by calling PsychicApp#init
11
11
  */
12
- static getOrFail(): PsychicApplicationWebsockets;
13
- psychicApp: PsychicApplication;
14
- static log(...args: Parameters<typeof PsychicApplication.log>): void;
15
- constructor(psychicApp: PsychicApplication);
12
+ static getOrFail(): PsychicAppWebsockets;
13
+ psychicApp: PsychicApp;
14
+ static log(...args: Parameters<typeof PsychicApp.log>): void;
15
+ constructor(psychicApp: PsychicApp);
16
16
  private _websocketOptions;
17
17
  get websocketOptions(): PsychicWebsocketOptions & {
18
18
  subConnection?: RedisOrRedisClusterConnection;
19
19
  };
20
20
  private _hooks;
21
- get hooks(): PsychicApplicationWebsocketsHooks;
21
+ get hooks(): PsychicAppWebsocketsHooks;
22
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;
23
+ set<Opt extends PsychicAppWebsocketsOption>(option: Opt, value: unknown): void;
24
24
  }
25
25
  interface PsychicWebsocketOptions {
26
26
  connection: Redis;
27
27
  }
28
- export type PsychicApplicationWebsocketsOption = 'websockets';
28
+ export type PsychicAppWebsocketsOption = 'websockets';
29
29
  export type PsychicWebsocketsHookEventType = 'ws:start' | 'ws:connect';
30
- export interface PsychicApplicationWebsocketsHooks {
30
+ export interface PsychicAppWebsocketsHooks {
31
31
  wsStart: ((server: SocketServer) => void | Promise<void>)[];
32
32
  wsConnect: ((socket: Socket) => void | Promise<void>)[];
33
33
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic-websockets",
4
4
  "description": "Websocket system for Psychic applications",
5
- "version": "0.2.4",
5
+ "version": "0.3.1",
6
6
  "author": "RVO Health",
7
7
  "repository": {
8
8
  "type": "git",
@@ -25,16 +25,16 @@
25
25
  "scripts": {
26
26
  "client": "yarn --cwd=./client start",
27
27
  "client:fspec": "BROWSER=none VITE_PSYCHIC_ENV=test yarn --cwd=./client start",
28
- "psy": "NODE_ENV=${NODE_ENV:-test} yarn psyts",
29
- "psyjs": "node ./dist/test-app/src/cli/index.js",
30
- "psyts": "node --experimental-specifier-resolution=node --import ./bin/esm.js ./test-app/src/cli/index.ts",
28
+ "psy": "NODE_ENV=${NODE_ENV:-test} yarn psy:ts",
29
+ "psy:js": "node ./dist/test-app/src/cli/index.js",
30
+ "psy:ts": "tsx ./test-app/src/cli/index.ts",
31
31
  "build": "echo \"building cjs...\" && rm -rf dist && npx tsc -p ./tsconfig.cjs.build.json && echo \"building esm...\" && npx tsc -p ./tsconfig.esm.build.json",
32
32
  "uspec": "vitest --config ./spec/unit/vite.config.ts",
33
33
  "fspec": "vitest run --config=./spec/features/vite.config.ts",
34
34
  "fspec:hanging": "vitest run --config=./spec/features/vite.config.ts --reporter=hanging-process",
35
35
  "format": "yarn run prettier . --write",
36
36
  "lint": "yarn run eslint --no-warn-ignored \"src/**/*.ts\" && yarn run prettier . --check",
37
- "dev": "NODE_ENV=development WORKER_COUNT=0 ts-node --transpile-only ./test-app/main.ts",
37
+ "dev": "NODE_ENV=development WORKER_COUNT=0 tsx ./test-app/main.ts",
38
38
  "prepack": "yarn build"
39
39
  },
40
40
  "peerDependencies": {
@@ -48,10 +48,10 @@
48
48
  },
49
49
  "devDependencies": {
50
50
  "@eslint/js": "=9.0.0",
51
- "@rvoh/dream": "^0.35.0",
52
- "@rvoh/dream-spec-helpers": "^0.2.0",
53
- "@rvoh/psychic": "^0.28.3",
54
- "@rvoh/psychic-spec-helpers": "^0.2.0",
51
+ "@rvoh/dream": "^0.39.0",
52
+ "@rvoh/dream-spec-helpers": "^0.2.4",
53
+ "@rvoh/psychic": "^0.31.0",
54
+ "@rvoh/psychic-spec-helpers": "^0.6.0",
55
55
  "@socket.io/redis-adapter": "^8.3.0",
56
56
  "@socket.io/redis-emitter": "^5.1.0",
57
57
  "@types/express": "^4",
@@ -72,8 +72,8 @@
72
72
  "socket.io-adapter": "^2.5.5",
73
73
  "socket.io-client": "^4.8.1",
74
74
  "supertest": "^7.0.0",
75
- "ts-node": "^10.9.2",
76
75
  "tslib": "^2.7.0",
76
+ "tsx": "^4.19.3",
77
77
  "typedoc": "^0.26.6",
78
78
  "typescript": "^5.8.2",
79
79
  "typescript-eslint": "=7.18.0",
@@ -83,4 +83,4 @@
83
83
  "dependencies": {
84
84
  "yoctocolors": "^2.1.1"
85
85
  }
86
- }
86
+ }
@@ -1,17 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
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
- }
@@ -1,4 +0,0 @@
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;