@hitchy/plugin-odem-socket.io 0.2.2 → 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,33 +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
 
25
+ const txPerModel = {};
26
+
27
+ for ( const [ modelName, model ] of Object.entries( api.models ) ) {
28
+ txPerModel[modelName] = this.broadcastModelNotifications( notifications, namespace, modelName, model );
29
+ }
30
+
23
31
  namespace.on( "connection", socket => {
24
- for ( const modelName of Object.keys( api.models ) ) {
25
- const model = api.models[modelName];
32
+ const rxPerModel = {};
26
33
 
27
- this.manageModel( namespace, socket, modelName, model );
34
+ for ( const [ modelName, model ] of Object.entries( api.models ) ) {
35
+ rxPerModel[modelName] = this.handleModelRequests( crud, socket, modelName, model );
28
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
+ } );
29
47
  } );
30
- } );
31
- }
32
48
 
33
- /**
34
- * Initializes manager for controlling and monitoring named model via
35
- * provided server-side websocket.
36
- *
37
- * @param {Server} io server-side websocket
38
- * @param {WebSocket} socket socket representing established incoming connection
39
- * @param {string} modelName name of model to manage
40
- * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
41
- * @returns {void}
42
- */
43
- static manageModel( io, socket, modelName, model ) {
44
- const { crud, notifications } = api.config.socket.odem;
49
+ api.once( "close", () => {
50
+ namespace.disconnectSockets( true );
51
+
52
+ for ( const [ modelName, model ] of Object.entries( api.models ) ) {
53
+ const handlers = txPerModel[modelName];
45
54
 
46
- this.handleModelRequests( crud, socket, modelName, model );
47
- this.forwardModelNotifications( notifications, io, modelName, model );
55
+ model.notifications
56
+ .off( "created", handlers.created )
57
+ .off( "changed", handlers.changed )
58
+ .off( "removed", handlers.removed );
59
+ }
60
+ } );
61
+ } );
48
62
  }
49
63
 
50
64
  /**
@@ -55,7 +69,7 @@ module.exports = function() {
55
69
  * @param {WebSocket} socket socket representing established incoming connection
56
70
  * @param {string} modelName name of model to manage
57
71
  * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
58
- * @returns {void}
72
+ * @returns {Object<string,function>} map of registered handler functions
59
73
  */
60
74
  static handleModelRequests( enabled, socket, modelName, model ) {
61
75
  const prefix = api.utility.case.pascalToKebab( modelName );
@@ -63,153 +77,46 @@ module.exports = function() {
63
77
  if ( !enabled ) {
64
78
  logDebug( "disabling socket-based action requests for model %s (%s)", modelName, prefix );
65
79
 
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
+ };
88
+
66
89
  socket
67
- .on( `${prefix}:list`, ( _, __, ___, response ) => reject( response ) )
68
- .on( `${prefix}:find`, ( _, __, ___, ____, response ) => reject( response ) )
69
- .on( `${prefix}:create`, ( _, __, response ) => reject( response ) )
70
- .on( `${prefix}:read`, ( _, __, response ) => reject( response ) )
71
- .on( `${prefix}:update`, ( _, __, ___, response ) => reject( response ) )
72
- .on( `${prefix}:delete`, ( _, __, response ) => reject( response ) );
73
-
74
- return;
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;
75
98
  }
76
99
 
77
100
  logDebug( "enabling socket-based action requests for model %s (%s)", modelName, prefix );
78
101
 
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
- }
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
+ };
133
110
 
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
- } );
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;
213
120
  }
214
121
 
215
122
  /**
@@ -217,40 +124,254 @@ module.exports = function() {
217
124
  * them as broadcast events to connected clients.
218
125
  *
219
126
  * @param {boolean} enabled true if command handling is enabled in configuration
220
- * @param {Server} io server-side websocket
127
+ * @param {Server} namespace namespace managing a list of server-side sockets with common prefix
221
128
  * @param {string} modelName name of model to manage
222
129
  * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
223
- * @returns {void}
130
+ * @returns {Object<string,function>} map of registered handler functions
224
131
  */
225
- static forwardModelNotifications( enabled, io, modelName, model ) {
132
+ static broadcastModelNotifications( enabled, namespace, modelName, model ) {
226
133
  if ( enabled ) {
227
134
  const prefix = api.utility.case.pascalToKebab( modelName );
228
135
 
229
- model.notifications
230
- .on( "created", async( uuid, newRecord, asyncGeneratorFn ) => {
136
+ const handlers = {
137
+ created: async( uuid, newRecord, asyncGeneratorFn ) => {
231
138
  const event = {
232
139
  change: "created",
233
140
  properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
234
141
  };
235
142
 
236
- io.emit( `${prefix}:changed`, event );
237
- } )
238
- .on( "changed", async( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
143
+ namespace.emit( `${prefix}:changed`, event );
144
+ },
145
+ changed: async( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
239
146
  const event = {
240
147
  change: "updated",
241
148
  properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
242
149
  };
243
150
 
244
- io.emit( `${prefix}:changed`, event );
245
- } )
246
- .on( "removed", uuid => {
151
+ namespace.emit( `${prefix}:changed`, event );
152
+ },
153
+ removed: uuid => {
247
154
  const event = {
248
155
  change: "deleted",
249
156
  properties: { uuid: model.formatUUID( uuid ) },
250
157
  };
251
158
 
252
- io.emit( `${prefix}:changed`, event );
253
- } );
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
+ } );
254
375
  }
255
376
  }
256
377
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hitchy/plugin-odem-socket.io",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "exposing Hitchy's ODM via websocket",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,12 +30,12 @@
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.5",
38
+ "@hitchy/server-dev-tools": "^0.4.6",
39
39
  "c8": "^8.0.1",
40
40
  "eslint": "^8.47.0",
41
41
  "eslint-config-cepharum": "^1.0.13",