@hitchy/plugin-odem-socket.io 0.2.2 → 0.2.4

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.
@@ -7,8 +7,8 @@ module.exports = function() {
7
7
  const logError = api.log( "hitchy:odem:socket.io:error" );
8
8
 
9
9
  /**
10
- * Implements API for controlling and monitoring server-side ODM via
11
- * websocket.
10
+ * Implements API for controlling and monitoring server-side
11
+ * document-oriented database via websocket.
12
12
  */
13
13
  class OdemWebsocketProvider {
14
14
  /**
@@ -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 );
45
51
 
46
- this.handleModelRequests( crud, socket, modelName, model );
47
- 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
+ } );
48
62
  }
49
63
 
50
64
  /**
@@ -54,8 +68,8 @@ module.exports = function() {
54
68
  * @param {boolean} enabled true if command handling is enabled in configuration
55
69
  * @param {WebSocket} socket socket representing established incoming connection
56
70
  * @param {string} modelName name of model to manage
57
- * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
58
- * @returns {void}
71
+ * @param {class<Hitchy.Plugin.Odem.Model>} model model instance based on document-oriented database to manage
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,194 +77,302 @@ 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
- }
133
-
134
- await instance.save();
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
+ };
135
110
 
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
  /**
216
- * Sets up listeners for model-related notifications in ODM and forwards
217
- * them as broadcast events to connected clients.
123
+ * Sets up listeners for model-related notifications in
124
+ * document-oriented database and forwards them as broadcast events to
125
+ * connected clients.
218
126
  *
219
127
  * @param {boolean} enabled true if command handling is enabled in configuration
220
- * @param {Server} io server-side websocket
128
+ * @param {Server} namespace namespace managing a list of server-side sockets with common prefix
221
129
  * @param {string} modelName name of model to manage
222
- * @param {class<Hitchy.Plugin.Odem.Model>} model ODM-based model instance to manage
223
- * @returns {void}
130
+ * @param {class<Hitchy.Plugin.Odem.Model>} model model instance of document-oriented database to manage
131
+ * @returns {Object<string,function>} map of registered handler functions
224
132
  */
225
- static forwardModelNotifications( enabled, io, modelName, model ) {
133
+ static broadcastModelNotifications( enabled, namespace, modelName, model ) {
226
134
  if ( enabled ) {
227
135
  const prefix = api.utility.case.pascalToKebab( modelName );
228
136
 
229
- model.notifications
230
- .on( "created", async( uuid, newRecord, asyncGeneratorFn ) => {
137
+ const handlers = {
138
+ created: async( uuid, newRecord, asyncGeneratorFn ) => {
231
139
  const event = {
232
140
  change: "created",
233
141
  properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
234
142
  };
235
143
 
236
- io.emit( `${prefix}:changed`, event );
237
- } )
238
- .on( "changed", async( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
144
+ namespace.emit( `${prefix}:changed`, event );
145
+ },
146
+ changed: async( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
239
147
  const event = {
240
148
  change: "updated",
241
149
  properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
242
150
  };
243
151
 
244
- io.emit( `${prefix}:changed`, event );
245
- } )
246
- .on( "removed", uuid => {
152
+ namespace.emit( `${prefix}:changed`, event );
153
+ },
154
+ removed: uuid => {
247
155
  const event = {
248
156
  change: "deleted",
249
157
  properties: { uuid: model.formatUUID( uuid ) },
250
158
  };
251
159
 
252
- io.emit( `${prefix}:changed`, event );
253
- } );
160
+ namespace.emit( `${prefix}:changed`, event );
161
+ },
162
+ };
163
+
164
+ model.notifications
165
+ .on( "created", handlers.created )
166
+ .on( "changed", handlers.changed )
167
+ .on( "removed", handlers.removed );
168
+
169
+ return handlers;
170
+ }
171
+
172
+ return {};
173
+ }
174
+
175
+ /**
176
+ * Handles incoming request for listing items of a model.
177
+ *
178
+ * @param {string} modelName name of model
179
+ * @param {string} model named server-side model
180
+ * @param {string} sessionId client-provided session ID
181
+ * @param {Hitchy.Odem.ListOptions} queryOptions client-provided options for listing items
182
+ * @param {boolean} loadRecords if true, whole records shall be fetched per item
183
+ * @param {function} response callback to invoke with response
184
+ * @returns {Promise<void>} promise settled on request handled
185
+ */
186
+ static async handleListRequest( modelName, model, sessionId, queryOptions, loadRecords, response ) {
187
+ logDebug( "request for listing %s instances w/ queryOptions %j and loadRecords %j", modelName, queryOptions, Boolean( loadRecords ) );
188
+
189
+ try {
190
+ const meta = {};
191
+ const items = await model.list( queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
192
+
193
+ response( {
194
+ success: true,
195
+ count: meta.count,
196
+ items: items.map( item => ( loadRecords ? item.toObject( { serialized: true } ) : { uuid: item.uuid } ) ),
197
+ } );
198
+ } catch ( error ) {
199
+ logError( "listing %s instances failed: %s", modelName, error.stack );
200
+
201
+ response( {
202
+ error: error.message,
203
+ } );
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Handles incoming request for finding items of a model matching some
209
+ * provided query.
210
+ *
211
+ * @param {string} modelName name of model
212
+ * @param {string} model named server-side model
213
+ * @param {string} sessionId client-provided session ID
214
+ * @param {Hitchy.Odem.Query} query client-provided constraints to be met by returned items
215
+ * @param {Hitchy.Odem.ListOptions} queryOptions client-provided options for listing items
216
+ * @param {boolean} loadRecords if true, whole records shall be fetched per item
217
+ * @param {function} response callback to invoke with response
218
+ * @returns {Promise<void>} promise settled on request handled
219
+ */
220
+ static async handleFindRequest( modelName, model, sessionId, query, queryOptions, loadRecords, response ) {
221
+ logDebug( "request for finding %s instances matching %j w/ queryOptions %j and uuidsOnly %j", modelName, query, queryOptions, Boolean( loadRecords ) );
222
+
223
+ try {
224
+ const meta = {};
225
+ const items = await model.find( query, queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
226
+
227
+ response( {
228
+ success: true,
229
+ count: meta.count,
230
+ items: items.map( item => ( loadRecords ? item.toObject( { serialized: true } ) : { uuid: item.uuid } ) ),
231
+ } );
232
+ } catch ( error ) {
233
+ logError( "finding %s instances matching %j failed: %s", modelName, error.stack );
234
+
235
+ response( {
236
+ error: error.message,
237
+ } );
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Handles incoming request for creating item of a model.
243
+ *
244
+ * @param {string} modelName name of model
245
+ * @param {string} model named server-side model
246
+ * @param {string} sessionId client-provided session ID
247
+ * @param {Hitchy.Odem.ListOptions} properties properties of item to create
248
+ * @param {function} response callback to invoke with response
249
+ * @returns {Promise<void>} promise settled on request handled
250
+ */
251
+ static async handleCreateRequest( modelName, model, sessionId, properties, response ) {
252
+ logDebug( "request for creating %s instance with %j", modelName, properties );
253
+
254
+ try {
255
+ const schema = model.schema;
256
+ const instance = new model; // eslint-disable-line new-cap
257
+
258
+ for ( const propName of Object.keys( properties ) ) {
259
+ if ( schema.props.hasOwnProperty( propName ) ||
260
+ schema.computed.hasOwnProperty( propName ) ) {
261
+ instance[propName] = properties[propName];
262
+ }
263
+ }
264
+
265
+ await instance.save();
266
+
267
+ response( {
268
+ success: true,
269
+ properties: instance.toObject( { serialized: true } ),
270
+ } );
271
+ } catch ( error ) {
272
+ logError( "creating %s instance failed: %s", modelName, error.stack );
273
+
274
+ response( {
275
+ error: error.message,
276
+ } );
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Handles incoming request for reading properties of a single item.
282
+ *
283
+ * @param {string} modelName name of item's model
284
+ * @param {string} model named server-side model
285
+ * @param {string} sessionId client-provided session ID
286
+ * @param {string} uuid client-provided UUID of item to read
287
+ * @param {function} response callback to invoke with response
288
+ * @returns {Promise<void>} promise settled on request handled
289
+ */
290
+ static async handleReadRequest( modelName, model, sessionId, uuid, response ) {
291
+ logDebug( "request for reading %s instance %s", modelName, uuid );
292
+
293
+ try {
294
+ const instance = new model( uuid ); // eslint-disable-line new-cap
295
+ await instance.load();
296
+
297
+ response( {
298
+ success: true,
299
+ properties: instance.toObject( { serialized: true } ),
300
+ } );
301
+ } catch ( error ) {
302
+ logError( "reading %s instance failed: %s", modelName, error.stack );
303
+
304
+ response( {
305
+ error: error.message,
306
+ } );
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Handles incoming request for adjusting properties of a single item.
312
+ *
313
+ * @param {string} modelName name of item's model
314
+ * @param {string} model named server-side model
315
+ * @param {string} sessionId client-provided session ID
316
+ * @param {string} uuid client-provided UUID of item to modify
317
+ * @param {Object} properties client-provided properties of item replacing its existing ones
318
+ * @param {function} response callback to invoke with response
319
+ * @returns {Promise<void>} promise settled on request handled
320
+ */
321
+ static async handleUpdateRequest( modelName, model, sessionId, uuid, properties, response ) {
322
+ logDebug( "request for updating %s instance %s with %j", modelName, uuid, properties );
323
+
324
+ try {
325
+ const instance = new model( uuid ); // eslint-disable-line new-cap
326
+ await instance.load();
327
+
328
+ for ( const propName of Object.keys( properties ) ) {
329
+ if ( model.schema.props.hasOwnProperty( propName ) || model.schema.computed.hasOwnProperty( propName ) ) {
330
+ instance[propName] = properties[propName];
331
+ }
332
+ }
333
+
334
+ await instance.save();
335
+
336
+ response( {
337
+ success: true,
338
+ properties: instance.toObject( { serialized: true } ),
339
+ } );
340
+ } catch ( error ) {
341
+ logError( "updating %s instance failed: %s", modelName, error.stack );
342
+
343
+ response( {
344
+ error: error.message,
345
+ } );
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Handles incoming request for removing a model's item.
351
+ *
352
+ * @param {string} modelName name of item's model
353
+ * @param {string} model named server-side model
354
+ * @param {string} sessionId client-provided session ID
355
+ * @param {string} uuid client-provided UUID of item to remove
356
+ * @param {function} response callback to invoke with response
357
+ * @returns {Promise<void>} promise settled on request handled
358
+ */
359
+ static async handleDeleteRequest( modelName, model, sessionId, uuid, response ) {
360
+ logDebug( "request for deleting %s instance %s", modelName, uuid );
361
+
362
+ try {
363
+ const instance = new model( uuid ); // eslint-disable-line new-cap
364
+ await instance.remove();
365
+
366
+ response( {
367
+ success: true,
368
+ properties: { uuid },
369
+ } );
370
+ } catch ( error ) {
371
+ logError( "deleting %s instance %s failed: %s", modelName, uuid, error.stack );
372
+
373
+ response( {
374
+ error: error.message,
375
+ } );
254
376
  }
255
377
  }
256
378
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hitchy/plugin-odem-socket.io",
3
- "version": "0.2.2",
4
- "description": "exposing Hitchy's ODM via websocket",
3
+ "version": "0.2.4",
4
+ "description": "exposing Hitchy's document-oriented database via websocket",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "lint": "eslint .",
@@ -14,8 +14,7 @@
14
14
  },
15
15
  "keywords": [
16
16
  "Hitchy",
17
- "socket.io",
18
- "ODM"
17
+ "socket.io"
19
18
  ],
20
19
  "author": "cepharum GmbH",
21
20
  "license": "MIT",
@@ -30,18 +29,18 @@
30
29
  "index.js"
31
30
  ],
32
31
  "peerDependencies": {
33
- "@hitchy/core": ">=0.7.2",
34
- "@hitchy/plugin-odem": ">=0.7.1",
35
- "@hitchy/plugin-socket.io": ">=0.1.0"
32
+ "@hitchy/core": ">=0.8.1",
33
+ "@hitchy/plugin-odem": ">=0.7.8",
34
+ "@hitchy/plugin-socket.io": ">=0.1.1"
36
35
  },
37
36
  "devDependencies": {
38
- "@hitchy/server-dev-tools": "^0.4.5",
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",
37
+ "@hitchy/server-dev-tools": "^0.4.9",
38
+ "c8": "^10.1.2",
39
+ "eslint": "^8.57.0",
40
+ "eslint-config-cepharum": "^1.0.14",
41
+ "eslint-plugin-promise": "^7.1.0",
42
+ "mocha": "^10.7.3",
44
43
  "should": "^13.2.3",
45
- "socket.io-client": "^4.7.2"
44
+ "socket.io-client": "^4.7.5"
46
45
  }
47
46
  }
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hitchy/plugin-odem-socket.io
2
2
 
3
- _exposing Hitchy's ODM via websocket_
3
+ _exposing Hitchy's document-oriented database via websocket_
4
4
 
5
5
  ## License
6
6
 
@@ -29,7 +29,7 @@ npm i @hitchy/plugin-odem @hitchy/plugin-socket.io
29
29
 
30
30
  ### Basics
31
31
 
32
- This plugin is establishing an API via websocket for controlling and monitoring models of server-side ODM. Relying on particular features of [socket.io](https://socket.io/), this API is exposed in [namespace](https://socket.io/docs/v4/namespaces/) `/hitchy/odem`.
32
+ This plugin is establishing an API via websocket for controlling and monitoring models of server-side document-oriented database. Relying on particular features of [socket.io](https://socket.io/), this API is exposed in [namespace](https://socket.io/docs/v4/namespaces/) `/hitchy/odem`.
33
33
 
34
34
  For every server-side model, request listeners and/or notification broadcasters are set up. Either model's name is converted into its kebab-case variant and used as colon-separated prefix in event names emitted either by the server or by a client.
35
35
 
@@ -156,7 +156,7 @@ Last argument is a callback to be invoked with the resulting response from serve
156
156
 
157
157
  ### Notifications
158
158
 
159
- [ODM notifications](https://odem.hitchy.org/api/model.html#model-notifications) are forwarded as broadcast events named `<modelName>:changed`. Either event consists of
159
+ [notifications of the document-oriented database](https://odem.hitchy.org/api/model.html#model-notifications) are forwarded as broadcast events named `<modelName>:changed`. Either event consists of
160
160
 
161
161
  - a type of change and
162
162
  - the changed item's properties (limited to its UUID on deletion).