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 +13 -1
- package/package.json +5 -2
- package/src/index.js +1 -1
- package/src/model.js +36 -18
- package/src/rpc.js +1 -1
- package/src/type-cache.js +7 -11
- package/src/websocket.js +31 -20
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
|
-
|
|
341
|
+
Jan 31, 2026
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dmx-api",
|
|
3
|
-
"version": "
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/src/type-cache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
Vue.delete(state.topicTypes, uri)
|
|
287
|
+
delete state.topicTypes[uri]
|
|
290
288
|
}
|
|
291
289
|
|
|
292
290
|
function removeAssocType (uri) {
|
|
293
|
-
|
|
294
|
-
Vue.delete(state.assocTypes, uri)
|
|
291
|
+
delete state.assocTypes[uri]
|
|
295
292
|
}
|
|
296
293
|
|
|
297
294
|
function removeRoleType (uri) {
|
|
298
|
-
|
|
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
|
|
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]
|
|
51
|
+
DEV && console.log('[DMX] Connecting', this.url)
|
|
40
52
|
this.ws = new WebSocket(this.url)
|
|
41
53
|
this.ws.onopen = e => {
|
|
42
|
-
this.
|
|
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
|
|
51
|
-
this.
|
|
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
|
-
|
|
60
|
-
this.
|
|
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
|
-
|
|
64
|
-
|
|
78
|
+
_ping () {
|
|
79
|
+
DEV && console.log('[DMX] WebSocket ping')
|
|
80
|
+
this.send({type: 'ping'})
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.send({type: 'idle'})
|
|
83
|
+
_close () {
|
|
84
|
+
this.ws.close()
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
}
|