@live-change/vue3-components 0.2.4 → 0.2.5
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/index.js +2 -2
- package/logic/index.js +6 -0
- package/logic/synchronized.js +91 -0
- package/logic/synchronizedList.js +146 -0
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -19,6 +19,6 @@ import createReactiveObject from "./utils/createReactiveObject.mjs"
|
|
|
19
19
|
|
|
20
20
|
export { createReactiveObject }
|
|
21
21
|
|
|
22
|
-
import { analytics, useAnalytics, installRouterAnalytics } from "./logic"
|
|
23
|
-
export { analytics, useAnalytics, installRouterAnalytics }
|
|
22
|
+
import { analytics, useAnalytics, installRouterAnalytics, synchronized, synchronizedList } from "./logic"
|
|
23
|
+
export { analytics, useAnalytics, installRouterAnalytics, synchronized, synchronizedList }
|
|
24
24
|
|
package/logic/index.js
CHANGED
|
@@ -8,6 +8,12 @@ export { Loading, LoadingZone, WorkingZone, Observe }
|
|
|
8
8
|
import { analytics, useAnalytics, installRouterAnalytics } from "./analytics.js"
|
|
9
9
|
export { analytics, useAnalytics, installRouterAnalytics }
|
|
10
10
|
|
|
11
|
+
import { synchronized } from "./synchronized.js"
|
|
12
|
+
export { synchronized }
|
|
13
|
+
|
|
14
|
+
import { synchronizedList } from "./synchronizedList.js"
|
|
15
|
+
export { synchronizedList }
|
|
16
|
+
|
|
11
17
|
function registerLogicComponents(app) {
|
|
12
18
|
app.component("loading", Loading)
|
|
13
19
|
app.component("loading-zone", LoadingZone)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { computed, ref, watch } from "vue"
|
|
2
|
+
import { useThrottleFn } from "@vueuse/core"
|
|
3
|
+
|
|
4
|
+
function synchronized(options) {
|
|
5
|
+
const {
|
|
6
|
+
source,
|
|
7
|
+
update,
|
|
8
|
+
identifiers = {},
|
|
9
|
+
timeField = 'lastUpdate',
|
|
10
|
+
timeSource = () => (new Date()).toISOString(),
|
|
11
|
+
onChange = () => {},
|
|
12
|
+
onSave = () => {},
|
|
13
|
+
recursive = false,
|
|
14
|
+
throttle = 300,
|
|
15
|
+
autoSave = true
|
|
16
|
+
} = options
|
|
17
|
+
if(!source) throw new Error('source must be defined')
|
|
18
|
+
if(!update) throw new Error('update function must be defined')
|
|
19
|
+
if(recursive) {
|
|
20
|
+
const synchronizedValue = ref(JSON.parse(JSON.stringify(source.value || {})))
|
|
21
|
+
const synchronizedJSON = computed(() => JSON.stringify(synchronizedValue.value))
|
|
22
|
+
const lastLocalUpdate = ref(synchronized.value ? synchronizedValue.value[timeField] : '')
|
|
23
|
+
|
|
24
|
+
let lastSavedUpdate = lastLocalUpdate.value
|
|
25
|
+
|
|
26
|
+
const changed = computed(() => (JSON.stringify(source.value) != synchronizedJSON.value)
|
|
27
|
+
&& (lastLocalUpdate.value > ((source.value && source.value[timeField]) ?? '')))
|
|
28
|
+
async function save() {
|
|
29
|
+
if((JSON.stringify(source.value) == JSON.stringify(synchronizedValue.value))
|
|
30
|
+
|| (lastLocalUpdate.value <= ((source.value && source.value[timeField]) ?? ''))) {
|
|
31
|
+
return false // identical, no need to save
|
|
32
|
+
}
|
|
33
|
+
if(lastSavedUpdate == lastLocalUpdate.value) {
|
|
34
|
+
return false // duplicated save action
|
|
35
|
+
}
|
|
36
|
+
lastSavedUpdate = lastLocalUpdate.value
|
|
37
|
+
const data = JSON.parse(synchronizedJSON.value)
|
|
38
|
+
// console.log("LAST LOCAL UPDATE", lastLocalUpdate.value)
|
|
39
|
+
// console.log("LAST REMOTE UPDATE", ((source.value && source.value[timeField]) ?? ''))
|
|
40
|
+
// console.log("SOURCE JSON", JSON.stringify(source.value))
|
|
41
|
+
// console.log("SYNCHRONIZED JSON", JSON.stringify(synchronizedValue.value))
|
|
42
|
+
await update({ ...data, [timeField]: lastLocalUpdate.value, ...identifiers })
|
|
43
|
+
onSave()
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
const throttledSave = throttle ? useThrottleFn(save, throttle) : save
|
|
47
|
+
watch(() => synchronizedJSON.value, json => {
|
|
48
|
+
lastLocalUpdate.value = timeSource()
|
|
49
|
+
onChange()
|
|
50
|
+
if(autoSave) throttledSave()
|
|
51
|
+
})
|
|
52
|
+
watch(() => source.value, sourceData => {
|
|
53
|
+
if(sourceData) {
|
|
54
|
+
console.log("SRC DATA", sourceData)
|
|
55
|
+
lastLocalUpdate.value = sourceData[timeField]
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
return { value: synchronizedValue, save, changed }
|
|
59
|
+
} else {
|
|
60
|
+
const local = ref(source.value)
|
|
61
|
+
const changed = computed(() => (JSON.stringify(source.value) != JSON.stringify(local.value))
|
|
62
|
+
&& ( ((local.value && local.value[timeField]) ?? '')
|
|
63
|
+
> ((source.value && source.value[timeField]) ?? '')))
|
|
64
|
+
async function save() {
|
|
65
|
+
if((JSON.stringify(source.value) == JSON.stringify(local.value))
|
|
66
|
+
|| ( ((local.value && local.value[timeField]) ?? '')
|
|
67
|
+
<= ((source.value && source.value[timeField]) ?? ''))) return false // identical, no need to save
|
|
68
|
+
const data = JSON.parse(JSON.stringify(local.value))
|
|
69
|
+
await update({ ...data, ...identifiers })
|
|
70
|
+
onSave()
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
const throttledSave = throttle ? useThrottleFn(save, throttle) : save
|
|
74
|
+
const synchronizedComputed = computed({
|
|
75
|
+
get: () => {
|
|
76
|
+
const localTime = ((local.value && local.value[timeField]) ?? '')
|
|
77
|
+
const sourceTime = ((source.value && source.value[timeField]) ?? '')
|
|
78
|
+
return localTime > sourceTime ? local.value : source.value
|
|
79
|
+
},
|
|
80
|
+
set: newValue => {
|
|
81
|
+
local.value = { ...newValue, [timeField]: timeSource() }
|
|
82
|
+
onChange()
|
|
83
|
+
if(autoSave) throttledSave()
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
return { value: synchronizedComputed, save, changed }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { synchronized }
|
|
91
|
+
export default synchronized
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { computed, ref, watch } from "vue"
|
|
2
|
+
import { useThrottleFn } from "@vueuse/core"
|
|
3
|
+
import { synchronized } from './synchronized.js'
|
|
4
|
+
|
|
5
|
+
function sortedArraysMerge(merge, ...arrays) {
|
|
6
|
+
const result = []
|
|
7
|
+
const positions = arrays.map(a => 0)
|
|
8
|
+
let incremented = false
|
|
9
|
+
do {
|
|
10
|
+
incremented = false
|
|
11
|
+
let lowestId = '\xFF\xFF\xFF\xFF\xFF'
|
|
12
|
+
for(const arrayIndex in arrays) {
|
|
13
|
+
const position = positions[arrayIndex]
|
|
14
|
+
const array = arrays[arrayIndex]
|
|
15
|
+
if(position < array.length && array[position].id < lowestId) {
|
|
16
|
+
lowestId = array[position].id
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
let objects = new Array(arrays.length)
|
|
20
|
+
for(const arrayIndex in arrays) {
|
|
21
|
+
const position = positions[arrayIndex]
|
|
22
|
+
const array = arrays[arrayIndex]
|
|
23
|
+
if(position < array.length && array[position].id == lowestId) {
|
|
24
|
+
objects[arrayIndex] = array[position]
|
|
25
|
+
positions[arrayIndex]++
|
|
26
|
+
} else {
|
|
27
|
+
objects[arrayIndex] = null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const mergeResult = merge(...objects)
|
|
31
|
+
if(mergeResult) {
|
|
32
|
+
result.push(mergeResult)
|
|
33
|
+
}
|
|
34
|
+
} while (incremented)
|
|
35
|
+
return result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function synchronizedList(options) {
|
|
39
|
+
const {
|
|
40
|
+
source,
|
|
41
|
+
update: updateAction,
|
|
42
|
+
insert: insertAction,
|
|
43
|
+
remove: removeAction,
|
|
44
|
+
move: moveAction,
|
|
45
|
+
identifiers = {},
|
|
46
|
+
objectIdentifiers = object => ({ id: object.id }),
|
|
47
|
+
timeField = 'lastUpdate',
|
|
48
|
+
timeSource = () => (new Date()).toISOString(),
|
|
49
|
+
onChange = () => {},
|
|
50
|
+
onSave = () => {},
|
|
51
|
+
recursive = false,
|
|
52
|
+
throttle = 300,
|
|
53
|
+
autoSave = true,
|
|
54
|
+
mapper = source => synchronized({
|
|
55
|
+
source, update: updateAction, identifiers: { ...identifiers, ...objectIdentifiers(source.value) }, timeField, timeSource,
|
|
56
|
+
recursive, throttle, autoSave, onSave, onChange
|
|
57
|
+
})
|
|
58
|
+
} = options
|
|
59
|
+
if(!source) throw new Error('source must be defined')
|
|
60
|
+
const synchronizedList = ref([])
|
|
61
|
+
const locallyAdded = ref([])
|
|
62
|
+
const locallyRemoved = ref([])
|
|
63
|
+
|
|
64
|
+
function createSynchronizedElement(sourceData) {
|
|
65
|
+
const elementSource = ref(sourceData)
|
|
66
|
+
const synchronizedElement = mapper(elementSource)
|
|
67
|
+
synchronizedElement.source = elementSource
|
|
68
|
+
synchronizedElement.id = sourceData.id
|
|
69
|
+
return synchronizedElement
|
|
70
|
+
}
|
|
71
|
+
function synchronizeFromSource() {
|
|
72
|
+
let obsoleteLocallyAdded = new Set()
|
|
73
|
+
let obsoleteLocallyRemoved = new Set()
|
|
74
|
+
let newSynchronized = sortedArraysMerge(
|
|
75
|
+
(synchronizedElement, sourceElement, locallyAddedElement, locallyRemovedElement) => {
|
|
76
|
+
if(locallyAddedElement && sourceElement) {
|
|
77
|
+
obsoleteLocallyAdded.add(locallyAddedElement.id)
|
|
78
|
+
}
|
|
79
|
+
if(locallyRemovedElement && !sourceElement) {
|
|
80
|
+
obsoleteLocallyRemoved.add(locallyAddedElement.id)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if(synchronizedElement) {
|
|
84
|
+
if(locallyRemovedElement) {
|
|
85
|
+
return null // synchronized element locally
|
|
86
|
+
}
|
|
87
|
+
if(sourceElement) {
|
|
88
|
+
synchronizedElement.source.value = sourceElement
|
|
89
|
+
} else if(locallyAddedElement) {
|
|
90
|
+
synchronizedElement.source.value = locallyAddedElement
|
|
91
|
+
} else {
|
|
92
|
+
return null // synchronized element deleted
|
|
93
|
+
}
|
|
94
|
+
} else if(sourceElement) {
|
|
95
|
+
console.log("CREATE SYNCHRONIZED FROM SOURCE!")
|
|
96
|
+
return createSynchronizedElement(sourceElement)
|
|
97
|
+
} else if(locallyAddedElement) {
|
|
98
|
+
return createSynchronizedElement(locallyAddedElement)
|
|
99
|
+
}
|
|
100
|
+
}, synchronizedList.value, source.value || [], locallyAdded.value, locallyRemoved.value)
|
|
101
|
+
if(obsoleteLocallyAdded.length > 0) {
|
|
102
|
+
locallyAdded.value = locallyAdded.value.filter(
|
|
103
|
+
locallyAddedElement => obsoleteLocallyAdded.has(locallyAddedElement.id)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if(obsoleteLocallyRemoved.length > 0) {
|
|
107
|
+
locallyRemoved.value = locallyRemoved.value.filter(
|
|
108
|
+
locallyRemovedElement => obsoleteLocallyRemoved.has(locallyRemovedElement.id)
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
synchronizedList.value = newSynchronized
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sourceIds = computed(() => (source.value ?? []).map(({ id }) => id))
|
|
115
|
+
watch(() => sourceIds, () => synchronizeFromSource())
|
|
116
|
+
|
|
117
|
+
synchronizeFromSource()
|
|
118
|
+
|
|
119
|
+
const changed = computed(() => (synchronizedList.value.some(({ changed }) => changed.value))
|
|
120
|
+
|| locallyAdded.length || locallyRemoved.length)
|
|
121
|
+
|
|
122
|
+
async function save() {
|
|
123
|
+
const results = await Promise.app(synchronizedList.value.map(synchronizedElement => synchronizedElement.save()))
|
|
124
|
+
return results.some(res => res)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function insert(element) {
|
|
128
|
+
locallyAdded.push(element)
|
|
129
|
+
await insertAction({ ...element, [timeField]: timeSource(), ...identifiers })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function remove(element) {
|
|
133
|
+
locallyRemoved.push(element)
|
|
134
|
+
await removeAction({ ...element, ...identifiers })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function move(element, toId) {
|
|
138
|
+
throw new Error('not implemented yet')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const synchronizedValue = computed(() => synchronizedList.value.map(synchronizedElement => synchronizedElement.value))
|
|
142
|
+
return { value: synchronizedValue, save, changed, insert, remove, move }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default synchronizedList
|
|
146
|
+
export { synchronizedList }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/vue3-components",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Live Change Framework - vue components",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/live-change/live-change-framework-vue3",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@live-change/vue3-ssr": "^0.2.
|
|
24
|
+
"@live-change/vue3-ssr": "^0.2.5",
|
|
25
25
|
"debug": "^4.3.2",
|
|
26
26
|
"mitt": "3.0.0",
|
|
27
27
|
"vue": "^3.2.31"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "923504c224dd1e3cc5b6bb8296f0963965cd4d10"
|
|
30
30
|
}
|