@live-change/dao-vue3 0.1.18 → 0.1.22

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 CHANGED
@@ -1,385 +1,19 @@
1
- import { reactive, watch, ref, computed, onUnmounted, getCurrentInstance } from 'vue'
2
- import * as lcdao from '@live-change/dao'
3
-
4
- let prefix = "$reactiveDaoPath_"
5
-
6
- const reactiveMixin = dao => ({
7
- data() {
8
- if(!this.$options.reactive) return {} // Avoid distributed fat
9
- let data = {}
10
- for (let key in this.$options.reactive) {
11
- data[key] = undefined
12
- data[key+"Error"] = undefined
13
- }
14
- return data
15
- },
16
- beforeCreate() {
17
- if(!this.$options.reactive) return; // Avoid distributed fat
18
- if (!this.$options.computed) this.$options.computed = {}
19
- for(let key in this.$options.reactive) {
20
- let path = this.$options.reactive[key]
21
- if(typeof path == 'function'){
22
- this.$options.computed[prefix + key] = path
23
- } else if(typeof path == 'string') {
24
- } else if(path.length !== undefined) {
25
- } else throw new Error("unknown reactive path "+path)
26
- }
27
- },
28
- created() {
29
- if(!this.$options.reactive) return; // Avoid distributed fat
30
- this.reactiveObservables = {}
31
- let reactiveObservables = this.reactiveObservables
32
- for(let key in this.$options.reactive) {
33
- let path = this.$options.reactive[key]
34
- if(typeof path == 'function'){
35
- let p = this[prefix + key]
36
- if(p) {
37
- reactiveObservables[key] = dao.observable(p)
38
- reactiveObservables[key].bindProperty(this, key)
39
- reactiveObservables[key].bindErrorProperty(this, key+"Error")
40
- }
41
- let oldPathJson
42
- watch(() => this[prefix + key], newPath => {
43
- const json = JSON.stringify(newPath)
44
- const match = JSON.stringify(newPath) == oldPathJson
45
- oldPathJson = json
46
- if(match) return
47
- if(reactiveObservables[key]) {
48
- this[key] = undefined
49
- this[key+"Error"] = undefined
50
- reactiveObservables[key].unbindProperty(this, key)
51
- reactiveObservables[key].unbindErrorProperty(this, key+"Error")
52
- }
53
- delete reactiveObservables[key]
54
- if(newPath) {
55
- reactiveObservables[key] = dao.observable(newPath)
56
- reactiveObservables[key].bindProperty(this, key)
57
- reactiveObservables[key].bindErrorProperty(this, key+"Error")
58
- } else {
59
- this[key] = undefined
60
- }
61
- })
62
- } else if(typeof path == 'string') {
63
- reactiveObservables[key] = dao.observable(path)
64
- reactiveObservables[key].bindProperty(this, key)
65
- reactiveObservables[key].bindErrorProperty(this, key+"Error")
66
- } else if(path.length !== undefined) {
67
- //console.log("DAO", dao)
68
- reactiveObservables[key] = dao.observable(path)
69
- reactiveObservables[key].bindProperty(this, key)
70
- reactiveObservables[key].bindErrorProperty(this, key+"Error")
71
- } else throw new Error("unknown reactive path "+path)
72
- }
73
- },
74
- beforeUnmount() {
75
- if(!this.$options.reactive) return; // Avoid distributed fat
76
- let reactiveObservables = this.reactiveObservables
77
- for(let key in reactiveObservables) {
78
- reactiveObservables[key].unbindProperty(this, key)
79
- reactiveObservables[key].unbindErrorProperty(this, key+"Error")
80
- }
81
- }
82
- })
83
-
84
- const reactivePrefetchMixin = dao => ({
85
- beforeCreate() {
86
- if(typeof window == 'undefined') return // NO REACTIVE PREFETCH ON SERVER
87
- if(!this.$options.reactivePreFetch) return
88
- if (!this.$options.computed) this.$options.computed = {}
89
- this.$options.computed[prefix+"_reactivePreFetch"] = function() {
90
- return this.$options.reactivePreFetch.call(this, this.$route, this.$router)
91
- }
92
- const optionData = this.$options.data
93
- this.$options.data = function vueReactiveDaoInjectedDataFn () {
94
- const data = (
95
- (typeof optionData === 'function')
96
- ? optionData.call(this)
97
- : optionData
98
- ) || {}
99
- data.reactivePreFetchedPaths = []
100
- data.reactivePreFetchError = null
101
- return data
102
- }
103
- },
104
- created() {
105
- if(typeof window == 'undefined') return // NO REACTIVE PREFETCH ON SERVER
106
- if(!this.$options.reactivePreFetch) return
107
- let paths = this[prefix+"_reactivePreFetch"]
108
- if(paths) {
109
- this.reactivePreFetchObservable = dao.observable({ paths })
110
- this.reactivePreFetchObservable.bindProperty(this, "reactivePreFetchedPaths")
111
- this.reactivePreFetchObservable.bindErrorProperty(this, "reactivePreFetchError")
112
- }
113
- watch(() => this[prefix + "_reactivePreFetch"], paths => {
114
- if(this.reactivePreFetchObservable) {
115
- this.reactivePreFetchObservable.unbindProperty(this, "reactivePreFetchedPaths")
116
- this.reactivePreFetchObservable.unbindErrorProperty(this, "reactivePreFetchError")
117
- }
118
- delete this.reactivePreFetchObservable
119
- if(paths) {
120
- this.reactivePreFetchObservable = dao.observable({ paths })
121
- this.reactivePreFetchObservable.bindProperty(this, "reactivePreFetchedPaths")
122
- this.reactivePreFetchObservable.bindErrorProperty(this, "reactivePreFetchError")
123
- }
124
- })
125
- },
126
- beforeUnmount() {
127
- if(typeof window == 'undefined') return; // NO REACTIVE PREFETCH ON SERVER
128
- if(!this.$options.reactivePreFetch) return; // Avoid distributed fat
129
- if(this.reactivePreFetchObservable) {
130
- this.reactivePreFetchObservable.unbindProperty(this, "reactivePreFetchedPaths")
131
- this.reactivePreFetchObservable.unbindErrorProperty(this, "reactivePreFetchError")
132
- }
133
- }
134
- })
135
-
136
- const reactiveComponent = dao => ({
137
- name: "Reactive",
138
- props: {
139
- what: {
140
- type: Object
141
- }
142
- },
143
- data() {
144
- let values = {}, errors = {}
145
- for(const key in this.what) {
146
- values[key] = undefined
147
- values[key + 'Error'] = undefined
148
- }
149
- return {
150
- values
151
- }
152
- },
153
- created() {
154
- this.observables = {}
155
- for(const name in this.what) {
156
- const what = this.what[key]
157
- const observable = dao.observable(what)
158
- this.observables[name] = observable
159
- observable.bindProperty(this.values[name])
160
- observable.bindErrorProperty(this.values[name+'Error'])
161
- }
162
- },
163
- beforeDestroy() {
164
- for(const name in this.observables) {
165
- const observable = this.observables[name]
166
- observable.unbindProperty(this, "reactivePreFetchedPaths")
167
- observable.unbindErrorProperty(this, "reactivePreFetchError")
168
- }
169
- },
170
- render(createElement) {
171
- return this.$scopedSlots.default(this.values)[0]
172
- }
173
- })
174
-
175
- class ReactiveObservableList extends lcdao.ObservableList {
176
- constructor(value, what, dispose) {
177
- super(value, what, dispose, (data) => {
178
- if(data && typeof data == 'object') {
179
- const activated = reactive(data)
180
- return activated
181
- }
182
- return data
183
- })
184
- }
185
- }
1
+ import reactiveMixin from './lib/reactiveMixin.js'
2
+ import reactivePrefetchMixin from './lib/reactivePrefetchMixin.js'
3
+ import live from './lib/live.js'
4
+ import ReactiveObservableList from './lib/ReactiveObservableList.js'
5
+ import RangeBuckets from './lib/RangeBuckets.js'
186
6
 
187
7
  const ReactiveDaoVue = {
188
8
  install(Vue, options) {
189
9
  if(!options || !options.dao) throw new Error("dao option required")
190
10
  const dao = options.dao
191
-
192
11
  Vue.mixin(reactiveMixin(dao))
193
-
194
12
  Vue.mixin(reactivePrefetchMixin(dao))
195
-
196
- Vue.component('reactive', reactiveComponent(dao))
197
13
  }
198
14
  }
199
15
 
200
16
  //// TODO: rename reactive to live
201
- export { ReactiveDaoVue, reactiveMixin, reactivePrefetchMixin, reactiveComponent, ReactiveObservableList }
202
-
203
- const liveSymbol = Symbol('live')
204
-
205
- async function live(api, path, onUnmountedCb) {
206
- if(!onUnmountedCb && typeof window != 'undefined') {
207
- if(getCurrentInstance()) {
208
- onUnmountedCb = onUnmounted
209
- } else {
210
- onUnmountedCb = () => {
211
- console.error("live fetch outside component instance - possible memory leak")
212
- }
213
- }
214
- }
215
-
216
- if(Array.isArray(path)) path = { what: path }
217
- const paths = [ path ]
218
- if(typeof window == 'undefined') {
219
- const preFetchPaths = await api.get({ paths })
220
- console.log("PRE FETCH DATA", preFetchPaths)
221
- const preFetchMap = new Map(preFetchPaths.map((res) => [JSON.stringify(res.what), res] ))
222
- function createObject(what, more) {
223
- const res = preFetchMap.get(JSON.stringify(what))
224
- if(res.error) throw new Error(res.error)
225
- const data = res.data
226
- if(more) {
227
- if(Array.isArray(data)) {
228
- for(let i = 0; i < data.length; i ++) {
229
- for(const moreElement of more) {
230
- if(moreElement.to) {
231
- console.log("COLLECT POINTERS FROM", data[i], "SC", moreElement.schema)
232
- const pointers = lcdao.collectPointers(data[i], moreElement.schema ,
233
- (what) => preFetchMap.get(JSON.stringify(what)))
234
- console.log("POINTERS COLLECTED", pointers)
235
- const values = pointers.map(pointer => createObject(pointer, moreElement.more))
236
- console.log("VALUES", values)
237
- console.log("MANY", pointers.many)
238
- if(pointers.many) {
239
- data[i][moreElement.to] = values
240
- } else {
241
- data[i][moreElement.to] = values[0] || null
242
- }
243
- }
244
- }
245
- }
246
- } else {
247
- for(const moreElement of more) {
248
- if(moreElement.to) {
249
- const pointers = lcdao.collectPointers(data, moreElement.schema,
250
- (what) => preFetchMap.get(JSON.stringify(what)))
251
- const values = pointers.map(pointer => createObject(pointer, moreElement.more))
252
- if(pointers.many) {
253
- data[moreElement.to] = values
254
- } else {
255
- data[moreElement.to] = values[0] || null
256
- }
257
- }
258
- }
259
- }
260
- }
261
- return data
262
- }
263
- return createObject(path.what, path.more)
264
- } else {
265
- const preFetchPaths = api.observable({ paths })
266
- const observables = []
267
- function bindResult(what, more, object, property) {
268
- if(!what) throw new Error("what parameter required!")
269
- const observable = api.observable(what)
270
- if(more && more.some(m => m.to)) {
271
- const extendedObservable = new lcdao.ExtendedObservableList(observable,
272
- newElement => {
273
- if(!newElement) return newElement
274
- const extendedElement = { ...newElement }
275
- const props = {}
276
- for(const moreElement of more) {
277
- if(moreElement.to) {
278
- const prop = {
279
- bounds: [],
280
- sources: []
281
- }
282
- props[moreElement.to] = prop
283
- let requiredSrcs = []
284
- const srcs = new Map()
285
- function getSource(ptr) {
286
- const exists = srcs.get(ptr)
287
- if(exists !== undefined) return exists.list
288
- requiredSrcs.push(exists)
289
- return undefined
290
- }
291
- function computePointers() {
292
- while(true) {
293
- const pointers = lcdao.collectPointers(newElement, moreElement.schema, getSource)
294
- if(requiredSrcs.length == 0) return pointers
295
- for(const requiredSrc of requiredSrcs) {
296
- const observable = api.observable(requiredSrc)
297
- const observer = () => {
298
- bindPointers(computePointers())
299
- }
300
- srcs.set(JSON.stringify(requiredSrc), observable)
301
- prop.sources.push({ observable, observer })
302
- observable.observe(observer)
303
- }
304
- }
305
- }
306
- function bindPointers(pointers) {
307
- if(pointers.many) {
308
- const oldBound = prop.bounds.slice()
309
- const newArray = new Array(pointers.length)
310
- const newBounds = new Array(pointers.length)
311
- for(let i = 0; i < pointers.length; i++) {
312
- newBounds[i] = bindResult(pointers[i], moreElements.more, newArray, i)
313
- }
314
- prop.bounds = newBounds
315
- oldBound.forEach(b => b.dispose())
316
- extendedElement[moreElement.to] = newArray
317
- } else if(pointers.length > 0) {
318
- const oldBound = prop.bounds
319
- if(!oldBound || oldBound.length == 0 ||
320
- JSON.stringify(oldBound[0].what) != JSON.stringify(pointers[0])) {
321
- if(oldBound) {
322
- prop.bounds.forEach(b => b.dispose())
323
- }
324
- if(pointers[0]) {
325
- prop.bounds = [
326
- bindResult(pointers[0], moreElement.more, extendedElement, moreElement.to)
327
- ]
328
- }
329
- }
330
- }
331
- }
332
- bindPointers(computePointers())
333
- }
334
- }
335
- extendedElement[liveSymbol] = props
336
- return extendedElement
337
- },
338
- disposedElement => {
339
- if(!disposedElement) return
340
- const boundProps = disposedElement[liveSymbol]
341
- for(const propName in boundProps) {
342
- const prop = boundProps[propName]
343
- const propBounds = prop.bounds
344
- for(const propBound of propBounds) {
345
- //console.log("PROP BOUND DISPOSE", propBound)
346
- propBound.dispose()
347
- }
348
- const propSources = prop.sources
349
- for(const propSource of propSources) {
350
- //console.log("PROP SOURCE DISPOSE", propSource)
351
- propSource.observable.unobserve(propSource.observer)
352
- }
353
- }
354
- }
355
- )
356
- extendedObservable.bindProperty(object, property)
357
- return {
358
- what,
359
- property,
360
- dispose() {
361
- extendedObservable.unbindProperty(object, property)
362
- }
363
- }
364
- } else {
365
- observable.bindProperty(object, property)
366
- return {
367
- what, property,
368
- dispose() {
369
- observable.unbindProperty(object, property)
370
- }
371
- }
372
- }
373
- }
374
- const resultRef = ref()
375
- bindResult(path.what, path.more, resultRef, 'value')
376
- /// TODO: unobserve on unmounted
377
- await preFetchPaths.wait()
378
- return resultRef
379
- }
380
- }
381
-
382
- export { live }
383
-
17
+ export { ReactiveDaoVue, reactiveMixin, reactivePrefetchMixin, ReactiveObservableList, RangeBuckets, live }
384
18
 
385
19
  export default ReactiveDaoVue
@@ -0,0 +1,236 @@
1
+ import live from "./live.js"
2
+ import { computed, reactive, ref, unref, watch } from "vue"
3
+
4
+ class Bucket {
5
+
6
+ constructor(id, api, range, pathFunction, bucketSize, waitForComponents, hardClose) {
7
+ this.id = id
8
+ this.api = () => api
9
+ this.range = reactive(range)
10
+ this.pathFunction = pathFunction
11
+ this.bucketSize = bucketSize
12
+ this.waitForComponents = waitForComponents
13
+ this.hardClose = hardClose
14
+
15
+ this.disposed = false
16
+ this.onDispose = []
17
+ this.domElements = []
18
+
19
+ this.data = computed(() => {
20
+ const ldv = this.liveData.value
21
+ if(!ldv) return []
22
+ let source = unref(ldv)
23
+ if(this.range.reverse) {
24
+ source = source.slice()
25
+ source.reverse()
26
+ }
27
+ if(this.hardClose) {
28
+ return source
29
+ }
30
+ return source.filter(element => (
31
+ (!this.range.gt || element.id > this.range.gt ) &&
32
+ (!this.range.gte || element.id >= this.range.gte) &&
33
+ (!this.range.lt || element.id < this.range.lt ) &&
34
+ (!this.range.lte || element.id <= this.range.lte)
35
+ ))
36
+ })
37
+ this.liveData = ref(null)
38
+
39
+ this.load()
40
+
41
+ this.promise = waitForComponents ? this.createPromise() : this.dataPromise
42
+ }
43
+
44
+ createPromise() {
45
+ const promise = new Promise((r1, r2) => { this.resolve = r1; this.reject = r2 })
46
+ }
47
+
48
+ async load() {
49
+ this.path = this.pathFunction(this.range)
50
+ this.dataPromise = live(this.api(), this.path, fun => onDispose.push(fun))
51
+ this.dataPromise.then(data => {
52
+ this.liveData.value = data
53
+ })
54
+ return this.dataPromise
55
+ }
56
+
57
+ isTopClosed() {
58
+ return !!(this.range && (this.range.gt || this.range.gte))
59
+ }
60
+
61
+ isBottomClosed() {
62
+ return !!(this.range && (this.range.lt || this.range.lte))
63
+ }
64
+
65
+ canClose() {
66
+ const data = unref(this.data)
67
+ return data && data.length == this.bucketSize
68
+ }
69
+
70
+ async closeTop() {
71
+ const data = unref(await this.dataPromise)
72
+ if(!this.data) throw new Error("can't close - bucket not loaded!")
73
+ if(this.data.length < this.bucketSize) throw new Error("can't close - bucket not full")
74
+ this.range.gte = data[this.range.reverse ? data.length - 1 : 0].id
75
+ if(this.hardClose) await this.load()
76
+ }
77
+
78
+ async closeBottom() {
79
+ const data = unref(await this.dataPromise)
80
+ if(data.length < this.bucketSize) throw new Error("can't close - bucket not full")
81
+ this.range.lte = data[this.range.reverse ? 0 : data.length - 1].id
82
+ if(this.hardClose) await this.load()
83
+ }
84
+
85
+ dispose() {
86
+ this.disposed = true
87
+ for(const disposeFunction of this.onDispose) {
88
+ disposeFunction()
89
+ }
90
+ }
91
+ }
92
+
93
+ class RangeBuckets {
94
+ constructor(api, pathFunction, options) {
95
+ this.api = api
96
+ this.pathFunction = pathFunction
97
+ this.bucketSize = options?.bucketSize || 3
98
+ this.position = options?.position
99
+ this.softClose = options?.softClose
100
+ this.waitForComponents = options?.waitForComponents
101
+
102
+ this.lastBucketId = 0
103
+ this.buckets = reactive([])
104
+
105
+ this.canLoadTop = computed(() => this.isTopLoadPossible())
106
+ this.canLoadBottom = computed(() => this.isBottomLoadPossible())
107
+
108
+ this.loadFirstBucket()
109
+ }
110
+
111
+ isTopLoadPossible() {
112
+ if(this.buckets.length == 0) return false
113
+ const firstBucket = this.buckets[0]
114
+ return firstBucket.isTopClosed() || firstBucket.canClose()
115
+ }
116
+
117
+ isBottomLoadPossible() {
118
+ if(this.buckets.length == 0) return false
119
+ const lastBucket = this.buckets[this.buckets.length - 1]
120
+ return lastBucket.isBottomClosed() || lastBucket.canClose()
121
+ }
122
+
123
+ loadFirstBucket() {
124
+ const firstBucket = this.createBucket({
125
+ gte: this.position,
126
+ limit: this.bucketSize
127
+ })
128
+ this.buckets.push(firstBucket)
129
+ }
130
+
131
+ createBucket(range) {
132
+ return new Bucket(++this.lastBucketId, this.api, range, this.pathFunction,
133
+ this.bucketSize, this.waitForComponents, !this.softClose)
134
+ }
135
+
136
+ async wait() {
137
+ //console.log("WAIT FOR BUCKETS", this.buckets.length)
138
+ await Promise.all(this.buckets.map(bucket => bucket.promise)).then(loaded => this)
139
+ }
140
+
141
+ async loadTop() {
142
+ //console.log("LOAD TOP!", this.isTopLoadPossible())
143
+ if(this.buckets.length == 0) return this.loadFirstBucket()
144
+ const firstBucket = this.buckets[0]
145
+ await firstBucket.promise
146
+ if(!this.isTopLoadPossible()) return
147
+ if(firstBucket != this.buckets[0]) {
148
+ return this.buckets[0].promise
149
+ }
150
+ let range = { limit: this.bucketSize, reverse: true }
151
+ if(!firstBucket.isTopClosed()) {
152
+ if(firstBucket.canClose()) {
153
+ await firstBucket.closeTop()
154
+ if(!firstBucket.isTopClosed()) throw new Error('top not closed!!!')
155
+ if(!this.isTopLoadPossible()) return
156
+ return this.loadTop()
157
+ } else {
158
+ console.log("FBD", unref(firstBucket.data))
159
+ throw new Error('impossible to read before bucket that is not closeable')
160
+ }
161
+ }
162
+ if(firstBucket.range.gte) {
163
+ range.lt = firstBucket.range.gte
164
+ } else if(firstBucket.range.gt) {
165
+ range.lte = firstBucket.range.gt
166
+ } else {
167
+ throw new Error('imposible to read before bucket '+ JSON.stringify(firstBucket.range))
168
+ }
169
+ const bucket = this.createBucket(range)
170
+ this.buckets.unshift(bucket)
171
+ return bucket.promise
172
+ }
173
+
174
+ async loadBottom() {
175
+ //console.log("LOAD BOTTOM!", this.isBottomLoadPossible())
176
+ if(this.buckets.length == 0) return this.loadFirstBucket()
177
+ const lastBucket = this.buckets[this.buckets.length - 1]
178
+ await lastBucket.promise
179
+ if(!this.isBottomLoadPossible()) return
180
+ if(lastBucket != this.buckets[this.buckets.length - 1]) {
181
+ return this.buckets[this.buckets.length - 1].promise
182
+ }
183
+ let range = { limit: this.bucketSize }
184
+ if(!lastBucket.isBottomClosed()) {
185
+ if(lastBucket.canClose()) {
186
+ await lastBucket.closeBottom()
187
+ if(!lastBucket.isBottomClosed()) throw new Error('bottom not closed!!!')
188
+ if(!this.isBottomLoadPossible()) return
189
+ return this.loadBottom()
190
+ } else {
191
+ throw new Error('impossible to read after bucket that is not closeable')
192
+ }
193
+ }
194
+ if(lastBucket.range.lte) {
195
+ range.gt = lastBucket.range.lte
196
+ } else if(lastBucket.range.lt) {
197
+ range.lte = lastBucket.range.lt
198
+ } else {
199
+ throw new Error('imposible to read after bucket '+ JSON.stringify(lastBucket.range))
200
+ }
201
+ const bucket = this.createBucket(range)
202
+ this.buckets.push(bucket)
203
+ return bucket.promise
204
+ }
205
+
206
+ dropTop() {
207
+ if(this.buckets.length == 0) throw new Error('impossible to drop from empty')
208
+ //console.log("DROP TOP!")
209
+ const droppedBucket = this.buckets[0]
210
+ const height = droppedBucket.domElements.reduce((acc, el) => acc + (el?.offsetHeight || 0), 0)
211
+ //console.log("DOM ELEMENTS", droppedBucket.domElements.map(el => el?.offsetHeight || 0), height)
212
+ this.buckets.shift()
213
+ droppedBucket.dispose()
214
+ return height
215
+ }
216
+
217
+ dropBottom() {
218
+ //console.log("DROP BOTTOM!")
219
+ if(this.buckets.length == 0) throw new Error('impossible to drop from empty')
220
+ const droppedBucket = this.buckets[this.buckets.length - 1]
221
+ const height = droppedBucket.domElements.reduce((acc, el) => acc + el?.offsetHeight, 0)
222
+ this.buckets.pop()
223
+ droppedBucket.dispose()
224
+ return height
225
+ }
226
+
227
+ dispose() {
228
+ for(const bucket of this.buckets) {
229
+ bucket.dispose()
230
+ }
231
+ this.buckets = []
232
+ }
233
+
234
+ }
235
+
236
+ export default RangeBuckets
@@ -0,0 +1,16 @@
1
+ import { reactive } from 'vue'
2
+ import { ObservableList } from '@live-change/dao'
3
+
4
+ class ReactiveObservableList extends ObservableList {
5
+ constructor(value, what, dispose) {
6
+ super(value, what, dispose, (data) => {
7
+ if(data && typeof data == 'object') {
8
+ const activated = reactive(data)
9
+ return activated
10
+ }
11
+ return data
12
+ })
13
+ }
14
+ }
15
+
16
+ export default ReactiveObservableList
package/lib/live.js ADDED
@@ -0,0 +1,193 @@
1
+ import { ref, onUnmounted, getCurrentInstance, unref, reactive } from 'vue'
2
+ import { collectPointers, ExtendedObservableList } from '@live-change/dao'
3
+
4
+ const liveSymbol = Symbol('live')
5
+
6
+ async function live(api, path, onUnmountedCb) {
7
+ if(!onUnmountedCb && typeof window != 'undefined') {
8
+ if(getCurrentInstance()) {
9
+ onUnmountedCb = onUnmounted
10
+ } else {
11
+ onUnmountedCb = () => {
12
+ console.error("live fetch outside component instance - possible memory leak")
13
+ }
14
+ }
15
+ }
16
+
17
+ if(Array.isArray(path)) path = { what: path }
18
+ const paths = [ path ]
19
+ if(typeof window == 'undefined') {
20
+ const preFetchPaths = await api.get({ paths })
21
+ console.log("PRE FETCH DATA", preFetchPaths)
22
+ const preFetchMap = new Map(preFetchPaths.map((res) => [JSON.stringify(res.what), res] ))
23
+ function createObject(what, more) {
24
+ const res = preFetchMap.get(JSON.stringify(what))
25
+ if(res.error) throw new Error(res.error)
26
+ const data = res.data
27
+ if(more) {
28
+ if(Array.isArray(data)) {
29
+ for(let i = 0; i < data.length; i ++) {
30
+ for(const moreElement of more) {
31
+ if(moreElement.to) {
32
+ console.log("COLLECT POINTERS FROM", data[i], "SC", moreElement.schema)
33
+ const pointers = collectPointers(data[i], moreElement.schema ,
34
+ (what) => preFetchMap.get(JSON.stringify(what)))
35
+ console.log("POINTERS COLLECTED", pointers)
36
+ const values = pointers.map(pointer => createObject(pointer, moreElement.more))
37
+ console.log("VALUES", values)
38
+ console.log("MANY", pointers.many)
39
+ if(pointers.many) {
40
+ data[i][moreElement.to] = values
41
+ } else {
42
+ data[i][moreElement.to] = values[0] || null
43
+ }
44
+ }
45
+ }
46
+ }
47
+ } else {
48
+ for(const moreElement of more) {
49
+ if(moreElement.to) {
50
+ const pointers = collectPointers(data, moreElement.schema,
51
+ (what) => preFetchMap.get(JSON.stringify(what)))
52
+ const values = pointers.map(pointer => createObject(pointer, moreElement.more))
53
+ if(pointers.many) {
54
+ data[moreElement.to] = values
55
+ } else {
56
+ data[moreElement.to] = values[0] || null
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ return data
63
+ }
64
+ return createObject(path.what, path.more)
65
+ } else {
66
+ const preFetchPaths = api.observable({ paths })
67
+ const observables = []
68
+ function bindResult(what, more, object, property) {
69
+ if(!what) throw new Error("what parameter required!")
70
+ const observable = api.observable(what)
71
+ if(more && more.some(m => m.to)) {
72
+ const extendedObservable = new ExtendedObservableList(observable,
73
+ newElement => {
74
+ if(!newElement) return newElement
75
+ const extendedElement = reactive({ ...newElement })
76
+ const props = {}
77
+ for(const moreElement of more) {
78
+ if(moreElement.to) {
79
+ const prop = {
80
+ bounds: [],
81
+ sources: []
82
+ }
83
+ props[moreElement.to] = prop
84
+ let requiredSrcs = []
85
+ const srcs = new Map()
86
+ function getSource(ptr) {
87
+ const exists = srcs.get(ptr)
88
+ if(exists !== undefined) return exists.list
89
+ requiredSrcs.push(exists)
90
+ return undefined
91
+ }
92
+ function computePointers() {
93
+ while(true) {
94
+ const pointers = collectPointers(newElement, moreElement.schema, getSource)
95
+ if(requiredSrcs.length == 0) return pointers
96
+ for(const requiredSrc of requiredSrcs) {
97
+ const observable = api.observable(requiredSrc)
98
+ const observer = () => {
99
+ bindPointers(computePointers())
100
+ }
101
+ srcs.set(JSON.stringify(requiredSrc), observable)
102
+ prop.sources.push({ observable, observer })
103
+ observable.observe(observer)
104
+ }
105
+ }
106
+ }
107
+ function bindPointers(pointers) {
108
+ if(pointers.many) {
109
+ const oldBound = prop.bounds.slice()
110
+ const newArray = new Array(pointers.length)
111
+ const newBounds = new Array(pointers.length)
112
+ for(let i = 0; i < pointers.length; i++) {
113
+ newBounds[i] = bindResult(pointers[i], moreElements.more, newArray, i)
114
+ }
115
+ prop.bounds = newBounds
116
+ oldBound.forEach(b => b.dispose())
117
+ extendedElement[moreElement.to] = newArray
118
+ } else if(pointers.length > 0) {
119
+ const oldBound = prop.bounds
120
+ if(!oldBound || oldBound.length == 0 ||
121
+ JSON.stringify(oldBound[0].what) != JSON.stringify(pointers[0])) {
122
+ if(oldBound) {
123
+ prop.bounds.forEach(b => b.dispose())
124
+ }
125
+ if(pointers[0]) {
126
+ prop.bounds = [
127
+ bindResult(pointers[0], moreElement.more, extendedElement, moreElement.to)
128
+ ]
129
+ }
130
+ }
131
+ }
132
+ }
133
+ bindPointers(computePointers())
134
+ }
135
+ }
136
+ extendedElement[liveSymbol] = props
137
+ return extendedElement
138
+ },
139
+ disposedElement => {
140
+ if(!disposedElement) return
141
+ const boundProps = disposedElement[liveSymbol]
142
+ for(const propName in boundProps) {
143
+ const prop = boundProps[propName]
144
+ const propBounds = prop.bounds
145
+ for(const propBound of propBounds) {
146
+ //console.log("PROP BOUND DISPOSE", propBound)
147
+ propBound.dispose()
148
+ }
149
+ const propSources = prop.sources
150
+ for(const propSource of propSources) {
151
+ //console.log("PROP SOURCE DISPOSE", propSource)
152
+ propSource.observable.unobserve(propSource.observer)
153
+ }
154
+ }
155
+ },
156
+ (data) => {
157
+ if(data && typeof data == 'object') {
158
+ const activated = reactive(data)
159
+ return activated
160
+ }
161
+ return data
162
+ }
163
+ )
164
+ extendedObservable.bindProperty(object, property)
165
+ return {
166
+ what,
167
+ property,
168
+ dispose() {
169
+ extendedObservable.unbindProperty(object, property)
170
+ }
171
+ }
172
+ } else {
173
+ observable.bindProperty(object, property)
174
+ return {
175
+ what, property,
176
+ dispose() {
177
+ observable.unbindProperty(object, property)
178
+ }
179
+ }
180
+ }
181
+ }
182
+ const resultRef = ref()
183
+ bindResult(path.what, path.more, resultRef, 'value')
184
+ /// TODO: unobserve on unmounted
185
+ await preFetchPaths.wait()
186
+ while(unref(resultRef) === undefined) { // wait for next tick
187
+ await new Promise((resolve) => setTimeout(resolve, 0))
188
+ }
189
+ return resultRef
190
+ }
191
+ }
192
+
193
+ export default live
@@ -0,0 +1,83 @@
1
+ import { watch } from 'vue'
2
+
3
+ const prefix = "$lcDaoPath_"
4
+
5
+ const reactiveMixin = dao => ({
6
+ data() {
7
+ if(!this.$options.reactive) return {} // Avoid distributed fat
8
+ let data = {}
9
+ for (let key in this.$options.reactive) {
10
+ data[key] = undefined
11
+ data[key+"Error"] = undefined
12
+ }
13
+ return data
14
+ },
15
+ beforeCreate() {
16
+ if(!this.$options.reactive) return; // Avoid distributed fat
17
+ if (!this.$options.computed) this.$options.computed = {}
18
+ for(let key in this.$options.reactive) {
19
+ let path = this.$options.reactive[key]
20
+ if(typeof path == 'function'){
21
+ this.$options.computed[prefix + key] = path
22
+ } else if(typeof path == 'string') {
23
+ } else if(path.length !== undefined) {
24
+ } else throw new Error("unknown reactive path "+path)
25
+ }
26
+ },
27
+ created() {
28
+ if(!this.$options.reactive) return; // Avoid distributed fat
29
+ this.reactiveObservables = {}
30
+ let reactiveObservables = this.reactiveObservables
31
+ for(let key in this.$options.reactive) {
32
+ let path = this.$options.reactive[key]
33
+ if(typeof path == 'function'){
34
+ let p = this[prefix + key]
35
+ if(p) {
36
+ reactiveObservables[key] = dao.observable(p)
37
+ reactiveObservables[key].bindProperty(this, key)
38
+ reactiveObservables[key].bindErrorProperty(this, key+"Error")
39
+ }
40
+ let oldPathJson
41
+ watch(() => this[prefix + key], newPath => {
42
+ const json = JSON.stringify(newPath)
43
+ const match = JSON.stringify(newPath) == oldPathJson
44
+ oldPathJson = json
45
+ if(match) return
46
+ if(reactiveObservables[key]) {
47
+ this[key] = undefined
48
+ this[key+"Error"] = undefined
49
+ reactiveObservables[key].unbindProperty(this, key)
50
+ reactiveObservables[key].unbindErrorProperty(this, key+"Error")
51
+ }
52
+ delete reactiveObservables[key]
53
+ if(newPath) {
54
+ reactiveObservables[key] = dao.observable(newPath)
55
+ reactiveObservables[key].bindProperty(this, key)
56
+ reactiveObservables[key].bindErrorProperty(this, key+"Error")
57
+ } else {
58
+ this[key] = undefined
59
+ }
60
+ })
61
+ } else if(typeof path == 'string') {
62
+ reactiveObservables[key] = dao.observable(path)
63
+ reactiveObservables[key].bindProperty(this, key)
64
+ reactiveObservables[key].bindErrorProperty(this, key+"Error")
65
+ } else if(path.length !== undefined) {
66
+ //console.log("DAO", dao)
67
+ reactiveObservables[key] = dao.observable(path)
68
+ reactiveObservables[key].bindProperty(this, key)
69
+ reactiveObservables[key].bindErrorProperty(this, key+"Error")
70
+ } else throw new Error("unknown reactive path "+path)
71
+ }
72
+ },
73
+ beforeUnmount() {
74
+ if(!this.$options.reactive) return; // Avoid distributed fat
75
+ let reactiveObservables = this.reactiveObservables
76
+ for(let key in reactiveObservables) {
77
+ reactiveObservables[key].unbindProperty(this, key)
78
+ reactiveObservables[key].unbindErrorProperty(this, key+"Error")
79
+ }
80
+ }
81
+ })
82
+
83
+ export default reactiveMixin
@@ -0,0 +1,57 @@
1
+ import { watch } from 'vue'
2
+
3
+ let prefix = "$lcDaoPath_"
4
+
5
+ const reactivePrefetchMixin = dao => ({
6
+ beforeCreate() {
7
+ if(typeof window == 'undefined') return // NO REACTIVE PREFETCH ON SERVER
8
+ if(!this.$options.reactivePreFetch) return
9
+ if (!this.$options.computed) this.$options.computed = {}
10
+ this.$options.computed[prefix + "_reactivePreFetch"] = function() {
11
+ return this.$options.reactivePreFetch.call(this, this.$route, this.$router)
12
+ }
13
+ const optionData = this.$options.data
14
+ this.$options.data = function vueReactiveDaoInjectedDataFn () {
15
+ const data = (
16
+ (typeof optionData === 'function')
17
+ ? optionData.call(this)
18
+ : optionData
19
+ ) || {}
20
+ data.reactivePreFetchedPaths = []
21
+ data.reactivePreFetchError = null
22
+ return data
23
+ }
24
+ },
25
+ created() {
26
+ if(typeof window == 'undefined') return // NO REACTIVE PREFETCH ON SERVER
27
+ if(!this.$options.reactivePreFetch) return
28
+ let paths = this[prefix + "_reactivePreFetch"]
29
+ if(paths) {
30
+ this.reactivePreFetchObservable = dao.observable({ paths })
31
+ this.reactivePreFetchObservable.bindProperty(this, "reactivePreFetchedPaths")
32
+ this.reactivePreFetchObservable.bindErrorProperty(this, "reactivePreFetchError")
33
+ }
34
+ watch(() => this[prefix + "_reactivePreFetch"], paths => {
35
+ if(this.reactivePreFetchObservable) {
36
+ this.reactivePreFetchObservable.unbindProperty(this, "reactivePreFetchedPaths")
37
+ this.reactivePreFetchObservable.unbindErrorProperty(this, "reactivePreFetchError")
38
+ }
39
+ delete this.reactivePreFetchObservable
40
+ if(paths) {
41
+ this.reactivePreFetchObservable = dao.observable({ paths })
42
+ this.reactivePreFetchObservable.bindProperty(this, "reactivePreFetchedPaths")
43
+ this.reactivePreFetchObservable.bindErrorProperty(this, "reactivePreFetchError")
44
+ }
45
+ })
46
+ },
47
+ beforeUnmount() {
48
+ if(typeof window == 'undefined') return; // NO REACTIVE PREFETCH ON SERVER
49
+ if(!this.$options.reactivePreFetch) return; // Avoid distributed fat
50
+ if(this.reactivePreFetchObservable) {
51
+ this.reactivePreFetchObservable.unbindProperty(this, "reactivePreFetchedPaths")
52
+ this.reactivePreFetchObservable.unbindErrorProperty(this, "reactivePreFetchError")
53
+ }
54
+ }
55
+ })
56
+
57
+ export default reactivePrefetchMixin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/dao-vue3",
3
- "version": "0.1.18",
3
+ "version": "0.1.22",
4
4
  "author": {
5
5
  "email": "m8@em8.pl",
6
6
  "name": "Michał Łaszczewski",
@@ -10,7 +10,7 @@
10
10
  "url": "https://github.com/live-change/dao-vue3/issues"
11
11
  },
12
12
  "dependencies": {
13
- "@live-change/dao": "^0.2.41"
13
+ "@live-change/dao": "^0.3.9"
14
14
  },
15
15
  "devDependencies": {},
16
16
  "description": "Vue.js integration for live-change dao",