dmx-api 3.1.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Version History
4
4
 
5
+ **4.0.1** -- Jan 31, 2026
6
+
7
+ * Improvement:
8
+ - No alert box appears when WebSocket connection is dropped. Reopens when browser is "foregrounded".
9
+ * Fix:
10
+ - Topic positioning for `Topicmap` instances hold as reactive Vue 3 state.
11
+
12
+ **4.0** -- Oct 16, 2024
13
+
14
+ * BREAKING CHANGE
15
+ - requires Vue 3 (instead Vue 2)
16
+
5
17
  **3.1** -- Oct 16, 2024
6
18
 
7
19
  * Adapted to DMX 5.3.5 (account management)
@@ -326,4 +338,4 @@ library's `init()` function.
326
338
 
327
339
  ------------
328
340
  Jörg Richter
329
- Oct 16, 2024
341
+ Jan 31, 2026
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "dmx-api",
3
- "version": "3.1.0",
3
+ "version": "4.0.1",
4
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",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/dmx-systems/dmx-api.git"
10
+ "url": "git+https://github.com/dmx-systems/dmx-api.git"
11
11
  },
12
12
  "dependencies": {
13
13
  "axios": "0.18.1",
14
14
  "clone": "2.1.2",
15
15
  "debounce": "1.2.0",
16
16
  "font-awesome": "4.7.0"
17
+ },
18
+ "peerDependencies": {
19
+ "vue": "3.x"
17
20
  }
18
21
  }
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import icons from './icons'
6
6
  import DMXWebSocket from './websocket'
7
7
  import {default as typeCache, init as initTypeCache, storeModule} from './type-cache'
8
8
 
9
- console.log('[DMX-API] 3.1')
9
+ console.log('[DMX-API] 2026/01/28')
10
10
 
11
11
  let adminWorkspaceId // promise
12
12
 
package/src/model.js CHANGED
@@ -2,7 +2,6 @@ import rpc from './rpc'
2
2
  import typeCache from './type-cache'
3
3
  import permCache from './permission-cache'
4
4
  import utils from './utils'
5
- import Vue from 'vue'
6
5
 
7
6
  // TODO: inject or factor out
8
7
  const DEFAULT_TOPIC_ICON = '\uf111' // fa-circle
@@ -734,6 +733,9 @@ class Topicmap extends Topic {
734
733
  this._assocs = utils.mapById(utils.instantiateMany(topicmap.assocs, ViewAssoc)) // map: ID -> dmx.ViewAssoc
735
734
  }
736
735
 
736
+ /**
737
+ * @param id Number or String
738
+ */
737
739
  getTopic (id) {
738
740
  const topic = this.getTopicIfExists(id)
739
741
  if (!topic) {
@@ -742,6 +744,9 @@ class Topicmap extends Topic {
742
744
  return topic
743
745
  }
744
746
 
747
+ /**
748
+ * @param id Number or String
749
+ */
745
750
  getAssoc (id) {
746
751
  const assoc = this.getAssocIfExists(id)
747
752
  if (!assoc) {
@@ -761,26 +766,44 @@ class Topicmap extends Topic {
761
766
  return o
762
767
  }
763
768
 
769
+ /**
770
+ * @param id Number or String
771
+ */
764
772
  getTopicIfExists (id) {
765
773
  return this._topics[id]
766
774
  }
767
775
 
776
+ /**
777
+ * @param id Number or String
778
+ */
768
779
  getAssocIfExists (id) {
769
780
  return this._assocs[id]
770
781
  }
771
782
 
783
+ /**
784
+ * @param id Number or String
785
+ */
772
786
  hasTopic (id) {
773
787
  return this.getTopicIfExists(id)
774
788
  }
775
789
 
790
+ /**
791
+ * @param id Number or String
792
+ */
776
793
  hasAssoc (id) {
777
794
  return this.getAssocIfExists(id)
778
795
  }
779
796
 
797
+ /**
798
+ * @param id Number or String
799
+ */
780
800
  hasObject (id) {
781
801
  return this.hasTopic(id) || this.hasAssoc(id)
782
802
  }
783
803
 
804
+ /**
805
+ * @param id Number or String
806
+ */
784
807
  hasVisibleObject (id) {
785
808
  const o = this.getTopicIfExists(id) || this.getAssocIfExists(id)
786
809
  return o && o.isVisible()
@@ -831,8 +854,7 @@ class Topicmap extends Topic {
831
854
  if (!(topic instanceof ViewTopic)) {
832
855
  throw Error(`addTopic() expects ViewTopic, got ${topic.constructor.name}`)
833
856
  }
834
- // reactivity is required to trigger "visibleTopicIds" getter (module dmx-cytoscape-renderer)
835
- Vue.set(this._topics, topic.id, topic)
857
+ this._topics[topic.id] = topic
836
858
  }
837
859
 
838
860
  /**
@@ -845,8 +867,7 @@ class Topicmap extends Topic {
845
867
  if (!(assoc instanceof ViewAssoc)) {
846
868
  throw Error(`addAssoc() expects ViewAssoc, got ${assoc.constructor.name}`)
847
869
  }
848
- // reactivity is required to trigger "visibleAssocIds" getter (module dmx-cytoscape-renderer)
849
- Vue.set(this._assocs, assoc.id, assoc)
870
+ this._assocs[assoc.id] = assoc
850
871
  }
851
872
 
852
873
  /**
@@ -880,7 +901,11 @@ class Topicmap extends Topic {
880
901
  viewTopic.children = topic.children // ### TODO, see comment in newViewTopic() above
881
902
  this.addTopic(viewTopic)
882
903
  op.type = 'add'
883
- op.viewTopic = viewTopic
904
+ op.viewTopic = this.getTopic(topic.id)
905
+ // Note: we don't assign plain "viewTopic" to op.viewTopic but must re-access it by this.getTopic(). In case of a
906
+ // Vue 3 application both are not the same as addTopic() implicitly wraps viewTopic into a JS Proxy object
907
+ // (provided this Topicmap instance is reactive state). All subsequent modifications of viewTopic (in particular
908
+ // the initialization of its position) must be applied to the Proxy object then, not the original viewTopic.
884
909
  } else {
885
910
  if (!viewTopic.isVisible()) {
886
911
  viewTopic.setVisibility(true)
@@ -919,11 +944,8 @@ class Topicmap extends Topic {
919
944
 
920
945
  updateTopic (topic) {
921
946
  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
- )
947
+ const uri = 'dmx.base.url#dmx.topicmaps.background_image'
948
+ this.children[uri] = topic.children[uri]
927
949
  }
928
950
  }
929
951
 
@@ -934,16 +956,14 @@ class Topicmap extends Topic {
934
956
  * Note: if the topic is not in this topicmap nothing is performed.
935
957
  */
936
958
  removeTopic (id) {
937
- // reactivity is required to trigger "visibleTopicIds" getter (module dmx-cytoscape-renderer)
938
- Vue.delete(this._topics, id)
959
+ delete this._topics[id]
939
960
  }
940
961
 
941
962
  /**
942
963
  * Note: if the assoc is not in this topicmap nothing is performed.
943
964
  */
944
965
  removeAssoc (id) {
945
- // reactivity is required to trigger "visibleAssocIds" getter (module dmx-cytoscape-renderer)
946
- Vue.delete(this._assocs, id)
966
+ delete this._assocs[id]
947
967
  }
948
968
 
949
969
  // Associations
@@ -1042,9 +1062,7 @@ const viewPropsMixin = Base => class extends Base {
1042
1062
  }
1043
1063
 
1044
1064
  setViewProp (propUri, value) {
1045
- // Note: some view props must be reactive, e.g. 'dmx.topicmaps.pinned' reflects pin button state.
1046
- // Test it with topics/assocs which don't have a 'dmx.topicmaps.pinned' setting yet. ### FIXDOC
1047
- Vue.set(this.viewProps, propUri, value)
1065
+ this.viewProps[propUri] = value
1048
1066
  }
1049
1067
  }
1050
1068
 
package/src/rpc.js CHANGED
@@ -536,7 +536,7 @@ export default {
536
536
  },
537
537
 
538
538
  /**
539
- * @param password expected to be SHA256 encoded
539
+ * @param password plain text
540
540
  *
541
541
  * @return a promise for a Username topic
542
542
  */
package/src/type-cache.js CHANGED
@@ -1,7 +1,7 @@
1
- import {Topic, TopicType, AssocType, RoleType} from './model'
1
+ import { nextTick } from 'vue'
2
+ import { Topic, TopicType, AssocType, RoleType } from './model'
2
3
  import rpc from './rpc'
3
4
  import utils from './utils'
4
- import Vue from 'vue'
5
5
 
6
6
  const typeP = {} // intermediate type promises
7
7
 
@@ -87,7 +87,7 @@ const actions = {
87
87
  }
88
88
  // Note: role types are never updated as they are simple values and thus immutable
89
89
  })
90
- Vue.nextTick(() => {
90
+ nextTick(() => {
91
91
  // console.log(`Type-cache: processing ${directives.length} directives (DELETE_TYPE)`)
92
92
  directives.forEach(dir => {
93
93
  switch (dir.type) {
@@ -278,25 +278,21 @@ function _putType (type, typeClass, prop) {
278
278
  if (!(type instanceof typeClass)) {
279
279
  throw Error(`can't cache "${type.constructor.name}", expected is "${typeClass.name}", ${JSON.stringify(type)}`)
280
280
  }
281
- // Note: type cache must be reactive
282
- Vue.set(state[prop], type.uri, type)
281
+ state[prop][type.uri] = type
283
282
  }
284
283
 
285
284
  // ---
286
285
 
287
286
  function removeTopicType (uri) {
288
- // Note: type cache must be reactive
289
- Vue.delete(state.topicTypes, uri)
287
+ delete state.topicTypes[uri]
290
288
  }
291
289
 
292
290
  function removeAssocType (uri) {
293
- // Note: type cache must be reactive
294
- Vue.delete(state.assocTypes, uri)
291
+ delete state.assocTypes[uri]
295
292
  }
296
293
 
297
294
  function removeRoleType (uri) {
298
- // Note: type cache must be reactive
299
- Vue.delete(state.roleTypes, uri)
295
+ delete state.roleTypes[uri]
300
296
  }
301
297
 
302
298
  // ---
package/src/websocket.js CHANGED
@@ -1,4 +1,4 @@
1
- const IDLE_INTERVAL = 60 * 1000 // 60s
1
+ const HEARTBEAT_INTERVAL = 25 * 1000 // 25s
2
2
 
3
3
  /**
4
4
  * A WebSocket connection to the DMX server.
@@ -13,6 +13,8 @@ const IDLE_INTERVAL = 60 * 1000 // 60s
13
13
  export default class DMXWebSocket {
14
14
 
15
15
  /**
16
+ * @param config
17
+ * a promise for an object having a `dmx.websockets.url` property.
16
18
  * @param messageHandler
17
19
  * the function that processes incoming messages.
18
20
  * One argument is passed: the message pushed by the server (a deserialzed JSON object).
@@ -20,8 +22,8 @@ export default class DMXWebSocket {
20
22
  constructor (config, messageHandler) {
21
23
  this.messageHandler = messageHandler
22
24
  config.then(config => {
25
+ document.addEventListener('visibilitychange', this._handleVisibilityChange.bind(this))
23
26
  this.url = config['dmx.websockets.url']
24
- // DEV && console.log('[DMX] CONFIG: WebSocket server is reachable at', this.url)
25
27
  this._connect()
26
28
  })
27
29
  }
@@ -35,11 +37,21 @@ export default class DMXWebSocket {
35
37
  this.ws.send(JSON.stringify(message))
36
38
  }
37
39
 
40
+ _handleVisibilityChange () {
41
+ DEV && console.log('[DMX] Document visibility:', document.visibilityState, new Date())
42
+ if (document.visibilityState === "hidden") {
43
+ // mobile browsers often kill background sockets anyway
44
+ this._close()
45
+ } else {
46
+ this._connect()
47
+ }
48
+ }
49
+
38
50
  _connect () {
39
- DEV && console.log('[DMX] Opening WebSocket connection to', this.url)
51
+ DEV && console.log('[DMX] Connecting', this.url)
40
52
  this.ws = new WebSocket(this.url)
41
53
  this.ws.onopen = e => {
42
- this._startIdling()
54
+ this._startHeartbeat()
43
55
  }
44
56
  this.ws.onmessage = e => {
45
57
  const message = JSON.parse(e.data)
@@ -47,33 +59,32 @@ export default class DMXWebSocket {
47
59
  this.messageHandler(message)
48
60
  }
49
61
  this.ws.onclose = e => {
50
- DEV && console.log('[DMX] WebSocket connection closed (' + e.reason + ')')
51
- this._stopIdling()
52
- this._reload() // a closed ws connection is regarded an (backend/network) error which requires page reloading
62
+ DEV && console.log('[DMX] WebSocket closed', e.reason)
63
+ this._cleanup()
53
64
  }
54
65
  this.ws.onerror = e => {
55
66
  DEV && console.warn('[DMX] WebSocket error')
56
67
  }
57
68
  }
58
69
 
59
- _startIdling () {
60
- this.idleId = setInterval(this._idle.bind(this), IDLE_INTERVAL)
70
+ _startHeartbeat () {
71
+ this.heartbeatTimer = setInterval(this._ping.bind(this), HEARTBEAT_INTERVAL)
72
+ }
73
+
74
+ _stopHeartbeat () {
75
+ clearInterval(this.heartbeatTimer)
61
76
  }
62
77
 
63
- _stopIdling () {
64
- clearInterval(this.idleId)
78
+ _ping () {
79
+ DEV && console.log('[DMX] WebSocket ping')
80
+ this.send({type: 'ping'})
65
81
  }
66
82
 
67
- _idle () {
68
- DEV && console.log('[DMX] WebSocket connection idle')
69
- this.send({type: 'idle'})
83
+ _close () {
84
+ this.ws.close()
70
85
  }
71
86
 
72
- _reload () {
73
- setTimeout(() => {
74
- alert('There is a problem with the server or network.\n\nPlease press OK to reload page.\n' +
75
- 'If it fails try manual page reload.')
76
- location.reload()
77
- }, 1000) // timeout to not interfere with interactive page reload (which also closes websocket connection)
87
+ _cleanup () {
88
+ this._stopHeartbeat()
78
89
  }
79
90
  }