dmx-api 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  ## Version History
4
4
 
5
+ **3.0** -- May 19, 2023
6
+
7
+ Version 3.0 of the `dmx-api` library extends/modifies the API in order to support a wider variety of frontend
8
+ applications. Additionally, depending on application type, the launch time is reduced as less data is transferred from
9
+ server ([#497](https://git.dmx.systems/dmx-platform/dmx-platform/-/issues/497),
10
+ [#501](https://git.dmx.systems/dmx-platform/dmx-platform/-/issues/501)). This required some breaking changes in the
11
+ library's `init()` function.
12
+
13
+ * BREAKING CHANGES
14
+ - changed behavior of the library's `init()` function:
15
+ - The client-side type cache is not fully pre-populated by default anymore. Instead the application pass
16
+ `topicTypes` config to pre-populate selectively, or pass `all`. Depending on application type this results in
17
+ less data transfer at application launch.
18
+ - The SVG utility for FontAwesome icons is not initialized by default anymore. Instead an application can
19
+ initialize it on-demand (by calling `dmx.icons.init()`). Applications who don't need it launch quicker as
20
+ downloading the FontAwesome SVG data (450K) is omitted.
21
+ - rename class `Type` -> `DMXType`
22
+ - remove `getPosition()` from `ViewTopic`. Use the new `pos` getter instead.
23
+ * Improvement:
24
+ - The library's `init()` function optionally opens the WebSocket for client-synchronization (if `messageHandler`
25
+ config is passed). The application no longer depends on
26
+ [dmx-websocket](https://github.com/dmx-systems/dmx-websocket) module then.
27
+ - Vanilla [axios](https://github.com/axios/axios) http instance (without error interceptor set by application) is
28
+ exported as `rpc._http`.
29
+ * Model:
30
+ - change `Type`'s `newFormModel()`:
31
+ - `object` parameter is now optional
32
+ - add `allChildren` parameter (optional)
33
+ - add `panX`, `panY`, `zoom`, `bgImageUrl` getters to `Topicmap`
34
+ - add `updateTopic()`, `updateAssoc()` to `Topicmap` (part of
35
+ [dmx-topicmap-panel](https://github.com/dmx-systems/dmx-topicmap-panel) protocol)
36
+ - add `pos` getter to `ViewTopic`
37
+ * RPC:
38
+ - add `getTopicType()` and `getAssocType()`
39
+ - add `includeChildren` parameter to `getTopicmap()`
40
+ - add `includeChildren` and `includeAssocChildren` parameters to `getAssignedWorkspace()`
41
+ - add `getMemberships()` to get the members of a workspace
42
+ - add `bulkUpdateUserMemberships()` and `bulkUpdateWorkspaceMemberships()`
43
+ - add `deleteWorkspace()`
44
+ - change `updateTopic()`: returns `Topic` plus directives (formerly just directives)
45
+ - change `login()`: returned promise resolves with username (formerly undefined)
46
+ * Utils:
47
+ - add `stripHtml()`
48
+ * Fix:
49
+ - fixed a bug where nested entities loose their child values while update request
50
+
5
51
  **2.1** -- Jun 13, 2021
6
52
 
7
53
  * Model:
@@ -219,7 +265,8 @@
219
265
 
220
266
  **0.13** -- Mar 25, 2018
221
267
 
222
- * Model: `Topicmap.revealTopic()`'s `pos` param is optional. If not given it's up to the topicmap renderer to position the topic.
268
+ * Model: `Topicmap.revealTopic()`'s `pos` param is optional. If not given it's up to the topicmap renderer to position
269
+ the topic.
223
270
  * Utils: `clone()` for deep-cloning arbitrary objects
224
271
  * Depends on module `clone` instead `lodash.clonedeep`
225
272
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dmx-api",
3
- "version": "2.1.0",
4
- "description": "DMX 5 base types and API",
3
+ "version": "3.0.0",
4
+ "description": "API and utilities for DMX 5 based frontends",
5
5
  "author": "Jörg Richter <jri@dmx.berlin>",
6
6
  "license": "AGPL-3.0",
7
7
  "main": "src/index.js",
package/src/icons.js CHANGED
@@ -6,11 +6,17 @@ import rpc from './rpc'
6
6
  let faFont // Font Awesome SVG <font> element
7
7
  let faDefaultWidth // Default icon width
8
8
 
9
- // a promise resolved once the Font Awesome SVG is loaded
10
- const svgReady = rpc.getXML(fa).then(svg => {
11
- faFont = svg.querySelector('font')
12
- faDefaultWidth = faFont.getAttribute('horiz-adv-x')
13
- })
9
+ /**
10
+ * Initializes the Icons-Toolkit.
11
+ *
12
+ * @returns a promise resolved once the toolkit is ready for `faGlyph()` calls (Font Awesome SVG is loaded).
13
+ */
14
+ function init () {
15
+ return rpc.getXML(fa).then(svg => {
16
+ faFont = svg.querySelector('font')
17
+ faDefaultWidth = faFont.getAttribute('horiz-adv-x')
18
+ })
19
+ }
14
20
 
15
21
  function faGlyph (unicode) {
16
22
  try {
@@ -25,6 +31,6 @@ function faGlyph (unicode) {
25
31
  }
26
32
 
27
33
  export default {
28
- ready: svgReady,
34
+ init,
29
35
  faGlyph
30
36
  }
package/src/index.js CHANGED
@@ -1,22 +1,23 @@
1
- import {
2
- DMXObject, Topic, Assoc, Player, RelatedTopic, Type, TopicType, AssocType, Topicmap, ViewTopic, ViewAssoc,
3
- setIconRenderers
4
- } from './model'
5
- import rpc from './rpc'
6
- import typeCache from './type-cache'
7
- import permCache from './permission-cache'
8
- import utils from './utils'
9
- import icons from './icons'
10
-
11
- console.log('[DMX-API] 2021/06/13')
1
+ import * as model from './model'
2
+ import rpc from './rpc'
3
+ import permCache from './permission-cache'
4
+ import utils from './utils'
5
+ import icons from './icons'
6
+ import DMXWebSocket from './websocket'
7
+ import {default as typeCache, init as initTypeCache, storeModule} from './type-cache'
8
+
9
+ console.log('[DMX-API] 3.0')
12
10
 
13
11
  let adminWorkspaceId // promise
14
12
 
15
- export default {
13
+ const clientId = newClientId()
14
+ updateClientIdCookie()
16
15
 
17
- /* eslint object-property-newline: "off" */
18
- DMXObject, Topic, Assoc, Player, RelatedTopic, Type, TopicType, AssocType, Topicmap, ViewTopic, ViewAssoc,
16
+ window.addEventListener('focus', updateClientIdCookie)
19
17
 
18
+ export default {
19
+
20
+ ...model,
20
21
  rpc,
21
22
  typeCache,
22
23
  permCache,
@@ -24,10 +25,12 @@ export default {
24
25
  icons,
25
26
 
26
27
  init (config) {
27
- config.iconRenderers && setIconRenderers(config.iconRenderers)
28
- config.onHttpError && rpc.setErrorHandler(config.onHttpError)
29
28
  adminWorkspaceId = rpc.getAdminWorkspaceId()
30
- return typeCache.init(config.store)
29
+ config.store.registerModule('typeCache', storeModule)
30
+ config.messageHandler && new DMXWebSocket(rpc.getWebsocketConfig(), config.messageHandler)
31
+ config.onHttpError && rpc.setErrorHandler(config.onHttpError)
32
+ config.iconRenderers && model.setIconRenderers(config.iconRenderers)
33
+ return initTypeCache(config.topicTypes)
31
34
  },
32
35
 
33
36
  /**
@@ -37,3 +40,12 @@ export default {
37
40
  return adminWorkspaceId.then(id => permCache.isWritable(id))
38
41
  }
39
42
  }
43
+
44
+ function updateClientIdCookie () {
45
+ // DEV && console.log('dmx_client_id', clientId)
46
+ utils.setCookie('dmx_client_id', clientId)
47
+ }
48
+
49
+ function newClientId () {
50
+ return Math.floor(Number.MAX_SAFE_INTEGER * Math.random())
51
+ }
package/src/model.js CHANGED
@@ -17,7 +17,7 @@ class DMXObject {
17
17
 
18
18
  constructor (object) {
19
19
  if (!object) {
20
- throw Error(`invalid object passed to DMXObject constructor: ${object}`)
20
+ throw Error(`invalid object passed to DMXObject constructor: ${JSON.stringify(object)}`)
21
21
  } else if (object.constructor.name !== 'Object') {
22
22
  throw Error(`DMXObject constructor expects Object, got ${object.constructor.name} ${JSON.stringify(object)}`)
23
23
  }
@@ -186,7 +186,6 @@ class Topic extends DMXObject {
186
186
  }
187
187
 
188
188
  update () {
189
- // console.log('update', this)
190
189
  return rpc.updateTopic(this)
191
190
  }
192
191
 
@@ -315,7 +314,6 @@ class Assoc extends DMXObject {
315
314
  }
316
315
 
317
316
  update () {
318
- console.log('update', this)
319
317
  return rpc.updateAssoc(this)
320
318
  }
321
319
 
@@ -415,8 +413,7 @@ class RelatedTopic extends Topic {
415
413
  }
416
414
  }
417
415
 
418
- // TODO: name it "DMXType"
419
- class Type extends Topic {
416
+ class DMXType extends Topic {
420
417
 
421
418
  constructor (type) {
422
419
  super(type)
@@ -481,15 +478,25 @@ class Type extends Topic {
481
478
  /**
482
479
  * Creates a form model for this type.
483
480
  *
484
- * @param object optional, if given its values are filled in. ### FIXDOC
485
- * The object is expected to be of *this* type.
481
+ * @param object optional, if given 1) its values are filled in, and 2) is manipulated in-place, that
482
+ * includes a) adding empty fields, and b) removing fields when "Reduced Details" is
483
+ * switched on (the default, see "allChildren" parameter).
484
+ * The given object is expected to be of *this* type.
485
+ * @param allChildren optional, if true all children are included in the returned form model, that is
486
+ * "Reduced Details" is switched off. Default is false.
486
487
  *
487
- * @return the created form model. ### FIXDOC
488
+ * @return the created form model. If "object" is given the (in-place manipulated) object is returned.
489
+ * Otherwise a newly created form model with empty values is returned.
488
490
  */
489
- newFormModel (object) {
490
- const o = this._newFormModel(object)
491
- object.children = utils.instantiateChildren(o.children)
492
- return object
491
+ newFormModel (object, allChildren) {
492
+ const o = this._newFormModel(object, allChildren)
493
+ if (object) {
494
+ object.children = utils.instantiateChildren(o.children)
495
+ // FIXME: o.assoc is not used
496
+ return object
497
+ } else {
498
+ return o
499
+ }
493
500
  }
494
501
 
495
502
  /**
@@ -498,14 +505,14 @@ class Type extends Topic {
498
505
  *
499
506
  * @return a newly constructed plain object
500
507
  */
501
- _newFormModel (object) {
508
+ _newFormModel (object, allChildren, level = 0, compDef) {
502
509
 
503
510
  function _newFormModel (object, type, level, compDef) {
504
511
  const o = type._newInstance(object)
505
512
  if (type.isComposite) {
506
513
  type.compDefs.forEach(compDef => {
507
514
  // Reduced details: at deeper levels for entity types only their identity attributes are included
508
- if (level === 0 || type.isValue || compDef.isIdentityAttr) {
515
+ if (allChildren || level === 0 || type.isValue || compDef.isIdentityAttr) {
509
516
  const compDefUri = compDef.compDefUri
510
517
  const childType = compDef.childType
511
518
  const child = object && object.children[compDefUri]
@@ -528,23 +535,21 @@ class Type extends Topic {
528
535
  return o
529
536
  }
530
537
 
531
- return _newFormModel(object, this, 0)
538
+ return _newFormModel(object, this, level, compDef)
532
539
  }
533
540
 
534
541
  _newInstance (object) {
535
- // console.log('_newInstance', this, object)
536
- const o = {
542
+ return {
537
543
  id: object ? object.id : -1,
538
544
  uri: object ? object.uri : '',
539
545
  typeUri: object ? object.typeUri : this.uri,
540
546
  value: object ? object.value : '',
541
547
  children: {}
542
548
  }
543
- return o
544
549
  }
545
550
  }
546
551
 
547
- class TopicType extends Type {
552
+ class TopicType extends DMXType {
548
553
 
549
554
  /**
550
555
  * Creates a form model for this topic type, and fills in the given value.
@@ -589,7 +594,7 @@ class TopicType extends Type {
589
594
  }
590
595
  }
591
596
 
592
- class AssocType extends Type {
597
+ class AssocType extends DMXType {
593
598
 
594
599
  get isTopicType () {
595
600
  return false
@@ -674,7 +679,7 @@ class CompDef extends Assoc {
674
679
  return topic && topic.value
675
680
  }
676
681
 
677
- // ### TODO: principal copy in Type
682
+ // ### TODO: principal copy in DMXType
678
683
  _getViewConfig (childTypeUri) {
679
684
  // TODO: don't hardcode config type URI
680
685
  const configTopic = this.viewConfig['dmx.webclient.view_config']
@@ -694,8 +699,8 @@ class CompDef extends Assoc {
694
699
  return 'dmx.core.composition'
695
700
  }
696
701
 
697
- emptyChildInstance () {
698
- const topic = this.childType._newFormModel()
702
+ emptyChildInstance (level) {
703
+ const topic = this.childType._newFormModel(undefined, false, level, this)
699
704
  topic.assoc = this.instanceLevelAssocType._newFormModel()
700
705
  return new Topic(topic)
701
706
  }
@@ -796,17 +801,16 @@ class Topicmap extends Topic {
796
801
  }
797
802
 
798
803
  /**
799
- * Returns the position of the given topic/assoc.
800
- *
801
- * Note: ViewTopic has getPosition() too but ViewAssoc has not
802
- * as a ViewAssoc doesn't know the Topicmap it belongs to.
804
+ * Returns the position of the given topic/assoc of this topicmap.
803
805
  *
804
806
  * @param id a topic ID or an assoc ID
805
807
  */
806
808
  getPosition (id) {
809
+ // Note: a ViewAssoc can't have a `pos` getter (like ViewTopic) as assoc pos is calculated from the player objects
810
+ // and the assoc only has its IDs and no reference to the Topicmap. So we make `getPosition()` a Topicmap method.
807
811
  const o = this.getObject(id)
808
812
  if (o.isTopic) {
809
- return o.getPosition()
813
+ return o.pos
810
814
  } else {
811
815
  const pos1 = this.getPosition(o.player1.id)
812
816
  const pos2 = this.getPosition(o.player2.id)
@@ -818,22 +822,28 @@ class Topicmap extends Topic {
818
822
  }
819
823
 
820
824
  /**
825
+ * Adds the given topic to this topicmap.
826
+ * If the topic is contained in the topicmap already (ID-check) it is replaced.
827
+ *
821
828
  * @param topic a dmx.ViewTopic
822
829
  */
823
830
  addTopic (topic) {
824
831
  if (!(topic instanceof ViewTopic)) {
825
- throw Error(`addTopic() expects a ViewTopic, got ${topic.constructor.name}`)
832
+ throw Error(`addTopic() expects ViewTopic, got ${topic.constructor.name}`)
826
833
  }
827
834
  // reactivity is required to trigger "visibleTopicIds" getter (module dmx-cytoscape-renderer)
828
835
  Vue.set(this._topics, topic.id, topic)
829
836
  }
830
837
 
831
838
  /**
839
+ * Adds the given assoc to this topicmap.
840
+ * If the assoc is contained in the topicmap already (ID-check) it is replaced.
841
+ *
832
842
  * @param assoc a dmx.ViewAssoc
833
843
  */
834
844
  addAssoc (assoc) {
835
845
  if (!(assoc instanceof ViewAssoc)) {
836
- throw Error(`addAssoc() expects a ViewAssoc, got ${assoc.constructor.name}`)
846
+ throw Error(`addAssoc() expects ViewAssoc, got ${assoc.constructor.name}`)
837
847
  }
838
848
  // reactivity is required to trigger "visibleAssocIds" getter (module dmx-cytoscape-renderer)
839
849
  Vue.set(this._assocs, assoc.id, assoc)
@@ -905,6 +915,21 @@ class Topicmap extends Topic {
905
915
  return op
906
916
  }
907
917
 
918
+ // the following 4 methods are part of the Topicmap Panel protocol
919
+
920
+ updateTopic (topic) {
921
+ if (this.id === topic.id) {
922
+ Vue.set(
923
+ this.children,
924
+ 'dmx.base.url#dmx.topicmaps.background_image',
925
+ topic.children['dmx.base.url#dmx.topicmaps.background_image']
926
+ )
927
+ }
928
+ }
929
+
930
+ updateAssoc (assoc) {
931
+ }
932
+
908
933
  /**
909
934
  * Note: if the topic is not in this topicmap nothing is performed.
910
935
  */
@@ -965,6 +990,23 @@ class Topicmap extends Topic {
965
990
 
966
991
  // Topicmap
967
992
 
993
+ get panX () {
994
+ return this.viewProps['dmx.topicmaps.pan_x']
995
+ }
996
+
997
+ get panY () {
998
+ return this.viewProps['dmx.topicmaps.pan_y']
999
+ }
1000
+
1001
+ get zoom () {
1002
+ return this.viewProps['dmx.topicmaps.zoom']
1003
+ }
1004
+
1005
+ get bgImageUrl () {
1006
+ const url = this.children['dmx.base.url#dmx.topicmaps.background_image']
1007
+ return url && url.value
1008
+ }
1009
+
968
1010
  setViewport (pan, zoom) {
969
1011
  this.viewProps['dmx.topicmaps.pan_x'] = pan.x
970
1012
  this.viewProps['dmx.topicmaps.pan_y'] = pan.y
@@ -1020,8 +1062,7 @@ class ViewTopic extends viewPropsMixin(Topic) {
1020
1062
  return rpc.getTopic(this.id, true, true)
1021
1063
  }
1022
1064
 
1023
- // TODO: make it a "pos" getter?
1024
- getPosition () {
1065
+ get pos () {
1025
1066
  return {
1026
1067
  x: this.getViewProp('dmx.topicmaps.x'),
1027
1068
  y: this.getViewProp('dmx.topicmaps.y')
@@ -1059,7 +1100,7 @@ export {
1059
1100
  Assoc,
1060
1101
  Player,
1061
1102
  RelatedTopic,
1062
- Type,
1103
+ DMXType,
1063
1104
  TopicType,
1064
1105
  AssocType,
1065
1106
  RoleType,
package/src/rpc.js CHANGED
@@ -108,9 +108,11 @@ export default {
108
108
  },
109
109
 
110
110
  updateTopic (topicModel) {
111
- return http.put(`/core/topic/${topicModel.id}`, topicModel).then(response =>
112
- response.data
113
- )
111
+ return http.put(`/core/topic/${topicModel.id}`, topicModel).then(response => {
112
+ const topic = new Topic(response.data)
113
+ topic.directives = response.data.directives
114
+ return topic
115
+ })
114
116
  },
115
117
 
116
118
  deleteTopic (id) {
@@ -204,6 +206,12 @@ export default {
204
206
 
205
207
  // Topic Types
206
208
 
209
+ getTopicType (topicTypeUri) {
210
+ return http.get(`/core/topic-type/${topicTypeUri}`).then(response =>
211
+ new TopicType(response.data)
212
+ )
213
+ },
214
+
207
215
  getTopicTypeImplicitly (topicId) {
208
216
  return http.get(`/core/topic-type/topic/${topicId}`).then(response =>
209
217
  new TopicType(response.data)
@@ -230,6 +238,12 @@ export default {
230
238
 
231
239
  // Association Types
232
240
 
241
+ getAssocType (assocTypeUri) {
242
+ return http.get(`/core/assoc-type/${assocTypeUri}`).then(response =>
243
+ new AssocType(response.data)
244
+ )
245
+ },
246
+
233
247
  getAssocTypeImplicitly (assocId) {
234
248
  return http.get(`/core/assoc-type/assoc/${assocId}`).then(response =>
235
249
  new AssocType(response.data)
@@ -300,8 +314,12 @@ export default {
300
314
  )
301
315
  },
302
316
 
303
- getTopicmap (topicmapId) {
304
- return http.get(`/topicmaps/${topicmapId}`).then(response =>
317
+ getTopicmap (topicmapId, includeChildren) {
318
+ return http.get(`/topicmaps/${topicmapId}`, {
319
+ params: {
320
+ children: includeChildren,
321
+ }
322
+ }).then(response =>
305
323
  new Topicmap(response.data)
306
324
  )
307
325
  },
@@ -368,7 +386,6 @@ export default {
368
386
  },
369
387
 
370
388
  setTopicmapViewport: utils.debounce((topicmapId, pan, zoom) => {
371
- // console.log('setTopicmapViewport')
372
389
  roundPos(pan, 'x', 'y')
373
390
  http.put(`/topicmaps/${topicmapId}/pan/${pan.x}/${pan.y}/zoom/${zoom}`)
374
391
  }, 3000),
@@ -386,6 +403,10 @@ export default {
386
403
  )
387
404
  },
388
405
 
406
+ deleteWorkspace (workspaceId) {
407
+ http.delete(`/workspaces/${workspaceId}`)
408
+ },
409
+
389
410
  getAssignedTopics (workspaceId, topicTypeUri, includeChildren, includeAssocChildren) {
390
411
  return http.get(`/workspaces/${workspaceId}/topics/${topicTypeUri}`, {
391
412
  params: {
@@ -400,8 +421,13 @@ export default {
400
421
  /**
401
422
  * @return the workspace topic, or empty string if no workspace is assigned
402
423
  */
403
- getAssignedWorkspace (objectId) {
404
- return http.get(`/workspaces/object/${objectId}`).then(response =>
424
+ getAssignedWorkspace (objectId, includeChildren, includeAssocChildren) {
425
+ return http.get(`/workspaces/object/${objectId}`, {
426
+ params: {
427
+ children: includeChildren,
428
+ assocChildren: includeAssocChildren
429
+ }
430
+ }).then(response =>
405
431
  // Note: if no workspace is assigned the response is 204 No Content; "data" is the empty string then
406
432
  response.data && new Topic(response.data)
407
433
  )
@@ -422,7 +448,10 @@ export default {
422
448
  headers: {
423
449
  Authorization: authMethod + ' ' + btoa(credentials.username + ':' + credentials.password)
424
450
  }
425
- }).then(() => permCache.clear())
451
+ }).then(() => {
452
+ permCache.clear()
453
+ return credentials.username
454
+ })
426
455
  },
427
456
 
428
457
  logout () {
@@ -448,6 +477,34 @@ export default {
448
477
  )
449
478
  },
450
479
 
480
+ getMemberships (id) {
481
+ return http.get(`/access-control/workspace/${id}/memberships`).then(response =>
482
+ utils.instantiateMany(response.data, RelatedTopic)
483
+ )
484
+ },
485
+
486
+ bulkUpdateUserMemberships (username, addWorkspaceIds, removeWorkspaceIds) {
487
+ return http.put(`/access-control/user/${username}`, undefined, {
488
+ params: {
489
+ addWorkspaceIds: addWorkspaceIds.join(','),
490
+ removeWorkspaceIds: removeWorkspaceIds.join(',')
491
+ }
492
+ }).then(response =>
493
+ utils.instantiateMany(response.data, RelatedTopic)
494
+ )
495
+ },
496
+
497
+ bulkUpdateWorkspaceMemberships (workspaceId, addUserIds, removeUserIds) {
498
+ return http.put(`/access-control/workspace/${workspaceId}`, undefined, {
499
+ params: {
500
+ addUserIds: addUserIds.join(','),
501
+ removeUserIds: removeUserIds.join(',')
502
+ }
503
+ }).then(response =>
504
+ utils.instantiateMany(response.data, RelatedTopic)
505
+ )
506
+ },
507
+
451
508
  getAdminWorkspaceId () {
452
509
  return http.get('/access-control/workspace/admin/id').then(response =>
453
510
  response.data
@@ -548,7 +605,9 @@ export default {
548
605
  return Promise.reject(error)
549
606
  }
550
607
  )
551
- }
608
+ },
609
+
610
+ _http
552
611
  }
553
612
 
554
613
  function toPath (idLists) {
package/src/type-cache.js CHANGED
@@ -3,14 +3,15 @@ import rpc from './rpc'
3
3
  import utils from './utils'
4
4
  import Vue from 'vue'
5
5
 
6
+ const typeP = {} // intermediate type promises
7
+
6
8
  // Note: the type cache is reactive state. E.g. new topic types appear in the Search Widget's
7
9
  // type menu automatically (see "createTopicTypes" getter in search.js of module dmx-search).
8
-
9
10
  const state = {
10
- topicTypes: undefined, // object: topic type URI (string) -> TopicType
11
- assocTypes: undefined, // object: assoc type URI (string) -> AssocType
12
- roleTypes: undefined, // object: role type URI (string) -> RoleType
13
- dataTypes: undefined // object: data type URI (string) -> data type (Topic)
11
+ topicTypes: {}, // object: topic type URI (string) -> TopicType
12
+ assocTypes: {}, // object: assoc type URI (string) -> AssocType
13
+ roleTypes: {}, // object: role type URI (string) -> RoleType
14
+ dataTypes: {} // object: data type URI (string) -> data type (Topic)
14
15
  }
15
16
 
16
17
  const actions = {
@@ -27,8 +28,11 @@ const actions = {
27
28
  _putRoleType(roleType)
28
29
  },
29
30
 
31
+ /**
32
+ * Re-populates the type cache with *all* types readable by current user.
33
+ */
30
34
  initTypeCache () {
31
- return initTypes()
35
+ return initAllTypes()
32
36
  },
33
37
 
34
38
  // WebSocket messages
@@ -104,30 +108,17 @@ const actions = {
104
108
  }
105
109
  }
106
110
 
107
- function init (store) {
108
- store.registerModule('typeCache', {state, actions})
109
- return initTypes()
110
- }
111
-
112
- function initTypes () {
113
- return Promise.all([
114
- // init state
115
- rpc.getAllTopicTypes().then(topicTypes => {
116
- state.topicTypes = utils.mapByUri(topicTypes)
117
- _putTopicType(bootstrapType())
118
- }),
119
- rpc.getAllAssocTypes().then(assocTypes => {
120
- state.assocTypes = utils.mapByUri(assocTypes)
121
- }),
122
- rpc.getAllRoleTypes().then(roleTypes => {
123
- state.roleTypes = utils.mapByUri(roleTypes)
124
- }),
125
- rpc.getTopicsByType('dmx.core.data_type').then(dataTypes => {
126
- state.dataTypes = utils.mapByUri(dataTypes)
127
- })
128
- ]).then(() => {
129
- // console.log('### Type cache ready!')
130
- })
111
+ function init (topicTypes) {
112
+ if (topicTypes) {
113
+ let p
114
+ if (topicTypes === 'all') {
115
+ p = initAllTypes()
116
+ } else {
117
+ topicTypes.forEach(_initTopicType)
118
+ // TODO: return promise
119
+ }
120
+ return p
121
+ }
131
122
  }
132
123
 
133
124
  // ---
@@ -149,9 +140,9 @@ function getRoleType (uri) {
149
140
  }
150
141
 
151
142
  function getType (uri, className, prop) {
152
- const type = state[prop] && state[prop][uri]
143
+ const type = _getType(uri, prop)
153
144
  if (!type) {
154
- throw Error(`unknown ${className} "${uri}"`)
145
+ throw Error(`${className} "${uri}" not in type cache`)
155
146
  }
156
147
  return type
157
148
  }
@@ -169,7 +160,7 @@ function getTypeById (id) {
169
160
 
170
161
  // TODO: the following 4 functions return async data so they should return promise
171
162
 
172
- // IMPORTANT: call these methods only from a reactive context, otherwise you might get undefined
163
+ // IMPORTANT: calling these methods must be synced, otherwise you might get undefined
173
164
 
174
165
  function getAllTopicTypes () {
175
166
  return getAllTypes('topicTypes')
@@ -196,6 +187,67 @@ function getAllTypes (prop) {
196
187
 
197
188
  // ---
198
189
 
190
+ function initAllTypes () {
191
+ return Promise.all([
192
+ // init state
193
+ rpc.getAllTopicTypes().then(topicTypes => {
194
+ state.topicTypes = utils.mapByUri(topicTypes)
195
+ _putTopicType(bootstrapType())
196
+ }),
197
+ rpc.getAllAssocTypes().then(assocTypes => {
198
+ state.assocTypes = utils.mapByUri(assocTypes)
199
+ }),
200
+ rpc.getAllRoleTypes().then(roleTypes => {
201
+ state.roleTypes = utils.mapByUri(roleTypes)
202
+ }),
203
+ rpc.getTopicsByType('dmx.core.data_type').then(dataTypes => {
204
+ state.dataTypes = utils.mapByUri(dataTypes)
205
+ })
206
+ ])
207
+ }
208
+
209
+ function _initTopicType (uri) {
210
+ _initType(uri, 'topicTypes', TopicType, rpc.getTopicType).then(topicType => {
211
+ topicType.compDefs.forEach(compDef => {
212
+ _initTopicType(compDef.childTypeUri)
213
+ _initAssocType(compDef.instanceLevelAssocTypeUri)
214
+ })
215
+ })
216
+ }
217
+
218
+ function _initAssocType (uri) {
219
+ _initType(uri, 'assocTypes', AssocType, rpc.getAssocType).then(assocType => {
220
+ assocType.compDefs.forEach(compDef => {
221
+ _initTopicType(compDef.childTypeUri)
222
+ // TODO: call _initAssocType()?
223
+ })
224
+ })
225
+ }
226
+
227
+ function _initType (uri, prop, typeClass, fetchFunc) {
228
+ const type = _getType(uri, prop)
229
+ if (type) {
230
+ return Promise.resolve(type)
231
+ } else {
232
+ let p = typeP[uri]
233
+ if (!p) {
234
+ p = fetchFunc(uri).then(type => {
235
+ _putType(type, typeClass, prop)
236
+ delete typeP[uri]
237
+ return type
238
+ })
239
+ typeP[uri] = p
240
+ }
241
+ return p
242
+ }
243
+ }
244
+
245
+ function _getType (uri, prop) {
246
+ return state[prop] && state[prop][uri]
247
+ }
248
+
249
+ // ---
250
+
199
251
  function putTopicType (topicType) {
200
252
  _putTopicType(new TopicType(topicType))
201
253
  }
@@ -260,8 +312,9 @@ function bootstrapType () {
260
312
  })
261
313
  }
262
314
 
315
+ // public API
316
+
263
317
  export default {
264
- init,
265
318
  getTopicType,
266
319
  getAssocType,
267
320
  getDataType,
@@ -272,3 +325,8 @@ export default {
272
325
  getAllDataTypes,
273
326
  getAllRoleTypes
274
327
  }
328
+
329
+ // module internal API
330
+
331
+ export {init}
332
+ export const storeModule = {state, actions}
package/src/utils.js CHANGED
@@ -184,6 +184,10 @@ function round (val, decimals) {
184
184
  return Math.round(factor * val) / factor
185
185
  }
186
186
 
187
+ function stripHtml (html) {
188
+ return html.replace(/<.*?>/g, '') // *? is the reluctant version of the * quantifier (which is greedy)
189
+ }
190
+
187
191
  // ---
188
192
 
189
193
  export default {
@@ -202,5 +206,6 @@ export default {
202
206
  deleteCookie,
203
207
  fulltextQuery,
204
208
  formatFileSize,
205
- round
209
+ round,
210
+ stripHtml
206
211
  }
@@ -0,0 +1,77 @@
1
+ const IDLE_INTERVAL = 60 * 1000 // 60s
2
+ const RECONNECT_DELAY = 5 * 1000 // 5s
3
+
4
+ /**
5
+ * A WebSocket connection to the DMX server.
6
+ *
7
+ * The URL to connect to is determined automatically, based on the server-side `dmx.websockets.url` config property.
8
+ * WebSocket messages are expected to be JSON. Serialization/Deserialization performs automatically.
9
+ *
10
+ * Properties:
11
+ * `url` - url of the WebSocket server
12
+ * `ws` - the native WebSocket object
13
+ */
14
+ export default class DMXWebSocket {
15
+
16
+ /**
17
+ * @param messageHandler
18
+ * the function that processes incoming messages.
19
+ * One argument is passed: the message pushed by the server (a deserialzed JSON object).
20
+ */
21
+ constructor (config, messageHandler) {
22
+ this.messageHandler = messageHandler
23
+ config.then(config => {
24
+ this.url = config['dmx.websockets.url']
25
+ // DEV && console.log('[DMX] CONFIG: WebSocket server is reachable at', this.url)
26
+ this._connect()
27
+ })
28
+ }
29
+
30
+ /**
31
+ * Sends a message to the server.
32
+ *
33
+ * @param message the message to be sent (arbitrary type). Will be serialized as JSON.
34
+ */
35
+ send (message) {
36
+ this.ws.send(JSON.stringify(message))
37
+ }
38
+
39
+ _connect () {
40
+ DEV && console.log('[DMX] Opening WebSocket connection to', this.url)
41
+ this.ws = new WebSocket(this.url)
42
+ this.ws.onopen = e => {
43
+ this._startIdling()
44
+ }
45
+ this.ws.onmessage = e => {
46
+ const message = JSON.parse(e.data)
47
+ DEV && console.log('[DMX] Receiving message', message)
48
+ this.messageHandler(message)
49
+ }
50
+ this.ws.onclose = e => {
51
+ DEV && console.log('[DMX] Closing WebSocket connection (' + e.reason + '), try reconnect in ' +
52
+ RECONNECT_DELAY / 1000 + ' seconds')
53
+ this._stopIdling()
54
+ this._reconnect()
55
+ }
56
+ this.ws.onerror = e => {
57
+ DEV && console.warn('[DMX] WebSocket error')
58
+ }
59
+ }
60
+
61
+ _reconnect () {
62
+ setTimeout(this._connect.bind(this), RECONNECT_DELAY)
63
+ }
64
+
65
+ _startIdling () {
66
+ this.idleId = setInterval(this._idle.bind(this), IDLE_INTERVAL)
67
+ }
68
+
69
+ _stopIdling () {
70
+ clearInterval(this.idleId)
71
+ }
72
+
73
+ _idle () {
74
+ DEV && console.log('[DMX] WebSocket connection idle')
75
+ this.send({type: 'idle'})
76
+ }
77
+ }