@hitchy/plugin-odem-socket.io 0.2.1 → 0.2.3

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.
@@ -18,30 +18,47 @@ module.exports = function() {
18
18
  */
19
19
  static start() {
20
20
  api.once( "websocket", io => {
21
+ const { crud, notifications } = api.config.socket.odem;
22
+
21
23
  const namespace = io.of( "/hitchy/odem" );
22
24
 
23
- for ( const modelName of Object.keys( api.models ) ) {
24
- const model = api.models[modelName];
25
+ const txPerModel = {};
25
26
 
26
- this.manageModel( namespace, modelName, model );
27
+ for ( const [ modelName, model ] of Object.entries( api.models ) ) {
28
+ txPerModel[modelName] = this.broadcastModelNotifications( notifications, namespace, modelName, model );
27
29
  }
28
- } );
29
- }
30
30
 
31
- /**
32
- * Initializes manager for controlling and monitoring named model via
33
- * provided server-side websocket.
34
- *
35
- * @param {Server} io server-side websocket
36
- * @param {string} modelName name of model to manage
37
- * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
38
- * @returns {void}
39
- */
40
- static manageModel( io, modelName, model ) {
41
- const { crud, notifications } = api.config.socket.odem;
31
+ namespace.on( "connection", socket => {
32
+ const rxPerModel = {};
33
+
34
+ for ( const [ modelName, model ] of Object.entries( api.models ) ) {
35
+ rxPerModel[modelName] = this.handleModelRequests( crud, socket, modelName, model );
36
+ }
37
+
38
+ socket.once( "disconnect", () => {
39
+ for ( const [ modelName, handlers ] of Object.entries( rxPerModel ) ) {
40
+ for ( const [ eventName, handler ] of Object.entries( handlers ) ) {
41
+ const prefix = api.utility.case.pascalToKebab( modelName );
42
+
43
+ socket.off( `${prefix}:${eventName}`, handler );
44
+ }
45
+ }
46
+ } );
47
+ } );
48
+
49
+ api.once( "close", () => {
50
+ namespace.disconnectSockets( true );
42
51
 
43
- this.handleModelRequests( crud, io, modelName, model );
44
- this.forwardModelNotifications( notifications, io, modelName, model );
52
+ for ( const [ modelName, model ] of Object.entries( api.models ) ) {
53
+ const handlers = txPerModel[modelName];
54
+
55
+ model.notifications
56
+ .off( "created", handlers.created )
57
+ .off( "changed", handlers.changed )
58
+ .off( "removed", handlers.removed );
59
+ }
60
+ } );
61
+ } );
45
62
  }
46
63
 
47
64
  /**
@@ -49,168 +66,57 @@ module.exports = function() {
49
66
  * model-related actions.
50
67
  *
51
68
  * @param {boolean} enabled true if command handling is enabled in configuration
52
- * @param {Server} io server-side websocket
69
+ * @param {WebSocket} socket socket representing established incoming connection
53
70
  * @param {string} modelName name of model to manage
54
71
  * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
55
- * @returns {void}
72
+ * @returns {Object<string,function>} map of registered handler functions
56
73
  */
57
- static handleModelRequests( enabled, io, modelName, model ) {
74
+ static handleModelRequests( enabled, socket, modelName, model ) {
58
75
  const prefix = api.utility.case.pascalToKebab( modelName );
59
76
 
60
77
  if ( !enabled ) {
61
78
  logDebug( "disabling socket-based action requests for model %s (%s)", modelName, prefix );
62
79
 
63
- io.on( "connection", socket => {
64
- socket
65
- .on( `${prefix}:list`, ( _, __, ___, response ) => reject( response ) )
66
- .on( `${prefix}:find`, ( _, __, ___, ____, response ) => reject( response ) )
67
- .on( `${prefix}:create`, ( _, __, response ) => reject( response ) )
68
- .on( `${prefix}:read`, ( _, __, response ) => reject( response ) )
69
- .on( `${prefix}:update`, ( _, __, ___, response ) => reject( response ) )
70
- .on( `${prefix}:delete`, ( _, __, response ) => reject( response ) );
71
- } );
80
+ const handlers = {
81
+ list: ( _, __, ___, response ) => reject( response ),
82
+ find: ( _, __, ___, ____, response ) => reject( response ),
83
+ create: ( _, __, response ) => reject( response ),
84
+ read: ( _, __, response ) => reject( response ),
85
+ update: ( _, __, ___, response ) => reject( response ),
86
+ delete: ( _, __, response ) => reject( response ),
87
+ };
72
88
 
73
- return;
89
+ socket
90
+ .on( `${prefix}:list`, handlers.list )
91
+ .on( `${prefix}:find`, handlers.find )
92
+ .on( `${prefix}:create`, handlers.create )
93
+ .on( `${prefix}:read`, handlers.read )
94
+ .on( `${prefix}:update`, handlers.update )
95
+ .on( `${prefix}:delete`, handlers.delete );
96
+
97
+ return handlers;
74
98
  }
75
99
 
76
100
  logDebug( "enabling socket-based action requests for model %s (%s)", modelName, prefix );
77
101
 
78
- io.on( "connection", socket => {
79
- socket
80
- .on( `${prefix}:list`, async( sessionId, queryOptions, loadRecords, response ) => {
81
- logDebug( "request for listing %s instances w/ queryOptions %j and loadRecords %j", modelName, queryOptions, Boolean( loadRecords ) );
82
-
83
- try {
84
- const meta = {};
85
- const items = await model.list( queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
86
-
87
- response( {
88
- success: true,
89
- count: meta.count,
90
- items: items.map( item => ( loadRecords ? item.toObject( { serialized: true } ) : { uuid: item.uuid } ) ),
91
- } );
92
- } catch ( error ) {
93
- logError( "listing %s instances failed: %s", modelName, error.stack );
94
-
95
- response( {
96
- error: error.message,
97
- } );
98
- }
99
- } )
100
- .on( `${prefix}:find`, async( sessionId, query, queryOptions, loadRecords, response ) => {
101
- logDebug( "request for finding %s instances matching %j w/ queryOptions %j and uuidsOnly %j", modelName, query, queryOptions, Boolean( loadRecords ) );
102
-
103
- try {
104
- const meta = {};
105
- const items = await model.find( query, queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
106
-
107
- response( {
108
- success: true,
109
- count: meta.count,
110
- items: items.map( item => ( loadRecords ? item.toObject( { serialized: true } ) : { uuid: item.uuid } ) ),
111
- } );
112
- } catch ( error ) {
113
- logError( "finding %s instances matching %j failed: %s", modelName, error.stack );
114
-
115
- response( {
116
- error: error.message,
117
- } );
118
- }
119
- } )
120
- .on( `${prefix}:create`, async( sessionId, properties, response ) => {
121
- logDebug( "request for creating %s instance with %j", modelName, properties );
122
-
123
- try {
124
- const schema = model.schema;
125
- const instance = new model; // eslint-disable-line new-cap
126
-
127
- for ( const propName of Object.keys( properties ) ) {
128
- if ( schema.props.hasOwnProperty( propName ) ||
129
- schema.computed.hasOwnProperty( propName ) ) {
130
- instance[propName] = properties[propName];
131
- }
132
- }
133
-
134
- await instance.save();
135
-
136
- response( {
137
- success: true,
138
- properties: instance.toObject( { serialized: true } ),
139
- } );
140
- } catch ( error ) {
141
- logError( "creating %s instance failed: %s", modelName, error.stack );
142
-
143
- response( {
144
- error: error.message,
145
- } );
146
- }
147
- } )
148
- .on( `${prefix}:read`, async( sessionId, uuid, response ) => {
149
- logDebug( "request for reading %s instance %s", modelName, uuid );
150
-
151
- try {
152
- const instance = new model( uuid ); // eslint-disable-line new-cap
153
- await instance.load();
154
-
155
- response( {
156
- success: true,
157
- properties: instance.toObject( { serialized: true } ),
158
- } );
159
- } catch ( error ) {
160
- logError( "reading %s instance failed: %s", modelName, error.stack );
161
-
162
- response( {
163
- error: error.message,
164
- } );
165
- }
166
- } )
167
- .on( `${prefix}:update`, async( sessionId, uuid, properties, response ) => {
168
- logDebug( "request for updating %s instance %s with %j", modelName, uuid, properties );
169
-
170
- try {
171
- const instance = new model( uuid ); // eslint-disable-line new-cap
172
- await instance.load();
173
-
174
- for ( const propName of Object.keys( properties ) ) {
175
- if ( model.schema.props.hasOwnProperty( propName ) || model.schema.computed.hasOwnProperty( propName ) ) {
176
- instance[propName] = properties[propName];
177
- }
178
- }
179
-
180
- await instance.save();
181
-
182
- response( {
183
- success: true,
184
- properties: instance.toObject( { serialized: true } ),
185
- } );
186
- } catch ( error ) {
187
- logError( "updating %s instance failed: %s", modelName, error.stack );
188
-
189
- response( {
190
- error: error.message,
191
- } );
192
- }
193
- } )
194
- .on( `${prefix}:delete`, async( sessionId, uuid, response ) => {
195
- logDebug( "request for deleting %s instance %s", modelName, uuid );
196
-
197
- try {
198
- const instance = new model( uuid ); // eslint-disable-line new-cap
199
- await instance.remove();
200
-
201
- response( {
202
- success: true,
203
- properties: { uuid },
204
- } );
205
- } catch ( error ) {
206
- logError( "deleting %s instance %s failed: %s", modelName, uuid, error.stack );
207
-
208
- response( {
209
- error: error.message,
210
- } );
211
- }
212
- } );
213
- } );
102
+ const handlers = {
103
+ list: ( ...args ) => this.handleListRequest( modelName, model, ...args ),
104
+ find: ( ...args ) => this.handleFindRequest( modelName, model, ...args ),
105
+ create: ( ...args ) => this.handleCreateRequest( modelName, model, ...args ),
106
+ read: ( ...args ) => this.handleReadRequest( modelName, model, ...args ),
107
+ update: ( ...args ) => this.handleUpdateRequest( modelName, model, ...args ),
108
+ delete: ( ...args ) => this.handleDeleteRequest( modelName, model, ...args ),
109
+ };
110
+
111
+ socket
112
+ .on( `${prefix}:list`, handlers.list )
113
+ .on( `${prefix}:find`, handlers.find )
114
+ .on( `${prefix}:create`, handlers.create )
115
+ .on( `${prefix}:read`, handlers.read )
116
+ .on( `${prefix}:update`, handlers.update )
117
+ .on( `${prefix}:delete`, handlers.delete );
118
+
119
+ return handlers;
214
120
  }
215
121
 
216
122
  /**
@@ -218,40 +124,254 @@ module.exports = function() {
218
124
  * them as broadcast events to connected clients.
219
125
  *
220
126
  * @param {boolean} enabled true if command handling is enabled in configuration
221
- * @param {Server} io server-side websocket
127
+ * @param {Server} namespace namespace managing a list of server-side sockets with common prefix
222
128
  * @param {string} modelName name of model to manage
223
129
  * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
224
- * @returns {void}
130
+ * @returns {Object<string,function>} map of registered handler functions
225
131
  */
226
- static forwardModelNotifications( enabled, io, modelName, model ) {
132
+ static broadcastModelNotifications( enabled, namespace, modelName, model ) {
227
133
  if ( enabled ) {
228
134
  const prefix = api.utility.case.pascalToKebab( modelName );
229
135
 
230
- model.notifications
231
- .on( "created", async( uuid, newRecord, asyncGeneratorFn ) => {
136
+ const handlers = {
137
+ created: async( uuid, newRecord, asyncGeneratorFn ) => {
232
138
  const event = {
233
139
  change: "created",
234
140
  properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
235
141
  };
236
142
 
237
- io.emit( `${prefix}:changed`, event );
238
- } )
239
- .on( "changed", async( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
143
+ namespace.emit( `${prefix}:changed`, event );
144
+ },
145
+ changed: async( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
240
146
  const event = {
241
147
  change: "updated",
242
148
  properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
243
149
  };
244
150
 
245
- io.emit( `${prefix}:changed`, event );
246
- } )
247
- .on( "removed", uuid => {
151
+ namespace.emit( `${prefix}:changed`, event );
152
+ },
153
+ removed: uuid => {
248
154
  const event = {
249
155
  change: "deleted",
250
156
  properties: { uuid: model.formatUUID( uuid ) },
251
157
  };
252
158
 
253
- io.emit( `${prefix}:changed`, event );
254
- } );
159
+ namespace.emit( `${prefix}:changed`, event );
160
+ },
161
+ };
162
+
163
+ model.notifications
164
+ .on( "created", handlers.created )
165
+ .on( "changed", handlers.changed )
166
+ .on( "removed", handlers.removed );
167
+
168
+ return handlers;
169
+ }
170
+
171
+ return {};
172
+ }
173
+
174
+ /**
175
+ * Handles incoming request for listing items of a model.
176
+ *
177
+ * @param {string} modelName name of model
178
+ * @param {string} model named server-side model
179
+ * @param {string} sessionId client-provided session ID
180
+ * @param {Hitchy.Odem.ListOptions} queryOptions client-provided options for listing items
181
+ * @param {boolean} loadRecords if true, whole records shall be fetched per item
182
+ * @param {function} response callback to invoke with response
183
+ * @returns {Promise<void>} promise settled on request handled
184
+ */
185
+ static async handleListRequest( modelName, model, sessionId, queryOptions, loadRecords, response ) {
186
+ logDebug( "request for listing %s instances w/ queryOptions %j and loadRecords %j", modelName, queryOptions, Boolean( loadRecords ) );
187
+
188
+ try {
189
+ const meta = {};
190
+ const items = await model.list( queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
191
+
192
+ response( {
193
+ success: true,
194
+ count: meta.count,
195
+ items: items.map( item => ( loadRecords ? item.toObject( { serialized: true } ) : { uuid: item.uuid } ) ),
196
+ } );
197
+ } catch ( error ) {
198
+ logError( "listing %s instances failed: %s", modelName, error.stack );
199
+
200
+ response( {
201
+ error: error.message,
202
+ } );
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Handles incoming request for finding items of a model matching some
208
+ * provided query.
209
+ *
210
+ * @param {string} modelName name of model
211
+ * @param {string} model named server-side model
212
+ * @param {string} sessionId client-provided session ID
213
+ * @param {Hitchy.Odem.Query} query client-provided constraints to be met by returned items
214
+ * @param {Hitchy.Odem.ListOptions} queryOptions client-provided options for listing items
215
+ * @param {boolean} loadRecords if true, whole records shall be fetched per item
216
+ * @param {function} response callback to invoke with response
217
+ * @returns {Promise<void>} promise settled on request handled
218
+ */
219
+ static async handleFindRequest( modelName, model, sessionId, query, queryOptions, loadRecords, response ) {
220
+ logDebug( "request for finding %s instances matching %j w/ queryOptions %j and uuidsOnly %j", modelName, query, queryOptions, Boolean( loadRecords ) );
221
+
222
+ try {
223
+ const meta = {};
224
+ const items = await model.find( query, queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
225
+
226
+ response( {
227
+ success: true,
228
+ count: meta.count,
229
+ items: items.map( item => ( loadRecords ? item.toObject( { serialized: true } ) : { uuid: item.uuid } ) ),
230
+ } );
231
+ } catch ( error ) {
232
+ logError( "finding %s instances matching %j failed: %s", modelName, error.stack );
233
+
234
+ response( {
235
+ error: error.message,
236
+ } );
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Handles incoming request for creating item of a model.
242
+ *
243
+ * @param {string} modelName name of model
244
+ * @param {string} model named server-side model
245
+ * @param {string} sessionId client-provided session ID
246
+ * @param {Hitchy.Odem.ListOptions} properties properties of item to create
247
+ * @param {function} response callback to invoke with response
248
+ * @returns {Promise<void>} promise settled on request handled
249
+ */
250
+ static async handleCreateRequest( modelName, model, sessionId, properties, response ) {
251
+ logDebug( "request for creating %s instance with %j", modelName, properties );
252
+
253
+ try {
254
+ const schema = model.schema;
255
+ const instance = new model; // eslint-disable-line new-cap
256
+
257
+ for ( const propName of Object.keys( properties ) ) {
258
+ if ( schema.props.hasOwnProperty( propName ) ||
259
+ schema.computed.hasOwnProperty( propName ) ) {
260
+ instance[propName] = properties[propName];
261
+ }
262
+ }
263
+
264
+ await instance.save();
265
+
266
+ response( {
267
+ success: true,
268
+ properties: instance.toObject( { serialized: true } ),
269
+ } );
270
+ } catch ( error ) {
271
+ logError( "creating %s instance failed: %s", modelName, error.stack );
272
+
273
+ response( {
274
+ error: error.message,
275
+ } );
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Handles incoming request for reading properties of a single item.
281
+ *
282
+ * @param {string} modelName name of item's model
283
+ * @param {string} model named server-side model
284
+ * @param {string} sessionId client-provided session ID
285
+ * @param {string} uuid client-provided UUID of item to read
286
+ * @param {function} response callback to invoke with response
287
+ * @returns {Promise<void>} promise settled on request handled
288
+ */
289
+ static async handleReadRequest( modelName, model, sessionId, uuid, response ) {
290
+ logDebug( "request for reading %s instance %s", modelName, uuid );
291
+
292
+ try {
293
+ const instance = new model( uuid ); // eslint-disable-line new-cap
294
+ await instance.load();
295
+
296
+ response( {
297
+ success: true,
298
+ properties: instance.toObject( { serialized: true } ),
299
+ } );
300
+ } catch ( error ) {
301
+ logError( "reading %s instance failed: %s", modelName, error.stack );
302
+
303
+ response( {
304
+ error: error.message,
305
+ } );
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Handles incoming request for adjusting properties of a single item.
311
+ *
312
+ * @param {string} modelName name of item's model
313
+ * @param {string} model named server-side model
314
+ * @param {string} sessionId client-provided session ID
315
+ * @param {string} uuid client-provided UUID of item to modify
316
+ * @param {Object} properties client-provided properties of item replacing its existing ones
317
+ * @param {function} response callback to invoke with response
318
+ * @returns {Promise<void>} promise settled on request handled
319
+ */
320
+ static async handleUpdateRequest( modelName, model, sessionId, uuid, properties, response ) {
321
+ logDebug( "request for updating %s instance %s with %j", modelName, uuid, properties );
322
+
323
+ try {
324
+ const instance = new model( uuid ); // eslint-disable-line new-cap
325
+ await instance.load();
326
+
327
+ for ( const propName of Object.keys( properties ) ) {
328
+ if ( model.schema.props.hasOwnProperty( propName ) || model.schema.computed.hasOwnProperty( propName ) ) {
329
+ instance[propName] = properties[propName];
330
+ }
331
+ }
332
+
333
+ await instance.save();
334
+
335
+ response( {
336
+ success: true,
337
+ properties: instance.toObject( { serialized: true } ),
338
+ } );
339
+ } catch ( error ) {
340
+ logError( "updating %s instance failed: %s", modelName, error.stack );
341
+
342
+ response( {
343
+ error: error.message,
344
+ } );
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Handles incoming request for removing a model's item.
350
+ *
351
+ * @param {string} modelName name of item's model
352
+ * @param {string} model named server-side model
353
+ * @param {string} sessionId client-provided session ID
354
+ * @param {string} uuid client-provided UUID of item to remove
355
+ * @param {function} response callback to invoke with response
356
+ * @returns {Promise<void>} promise settled on request handled
357
+ */
358
+ static async handleDeleteRequest( modelName, model, sessionId, uuid, response ) {
359
+ logDebug( "request for deleting %s instance %s", modelName, uuid );
360
+
361
+ try {
362
+ const instance = new model( uuid ); // eslint-disable-line new-cap
363
+ await instance.remove();
364
+
365
+ response( {
366
+ success: true,
367
+ properties: { uuid },
368
+ } );
369
+ } catch ( error ) {
370
+ logError( "deleting %s instance %s failed: %s", modelName, uuid, error.stack );
371
+
372
+ response( {
373
+ error: error.message,
374
+ } );
255
375
  }
256
376
  }
257
377
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hitchy/plugin-odem-socket.io",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "exposing Hitchy's ODM via websocket",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,18 +30,18 @@
30
30
  "index.js"
31
31
  ],
32
32
  "peerDependencies": {
33
- "@hitchy/core": ">=0.7.2",
34
- "@hitchy/plugin-odem": ">=0.7.1",
35
- "@hitchy/plugin-socket.io": ">=0.1.0"
33
+ "@hitchy/core": ">=0.8.1",
34
+ "@hitchy/plugin-odem": ">=0.7.8",
35
+ "@hitchy/plugin-socket.io": ">=0.1.1"
36
36
  },
37
37
  "devDependencies": {
38
- "@hitchy/server-dev-tools": "^0.4.3",
39
- "c8": "^7.11.2",
40
- "eslint": "^8.14.0",
41
- "eslint-config-cepharum": "^1.0.12",
42
- "eslint-plugin-promise": "^6.0.0",
43
- "mocha": "^9.2.2",
38
+ "@hitchy/server-dev-tools": "^0.4.6",
39
+ "c8": "^8.0.1",
40
+ "eslint": "^8.47.0",
41
+ "eslint-config-cepharum": "^1.0.13",
42
+ "eslint-plugin-promise": "^6.1.1",
43
+ "mocha": "^10.2.0",
44
44
  "should": "^13.2.3",
45
- "socket.io-client": "^4.5.0"
45
+ "socket.io-client": "^4.7.2"
46
46
  }
47
47
  }