Skip to content

Commit be14909

Browse files
committed
feat(refs): keeps data of refs when updating the document
this prevents unsubscribing and subscribing again to ref when a document is partially updated. It works by using old data to populate data with existing values
1 parent 6cbdee7 commit be14909

File tree

4 files changed

+67
-42
lines changed

4 files changed

+67
-42
lines changed

src/index.js

+34-25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSnapshot, extractRefs, callOnceWithArg, deepGetSplit } from './utils'
1+
import { createSnapshot, extractRefs, callOnceWithArg, walkGet, walkSet } from './utils'
22

33
// NOTE not convinced by the naming of subscribeToRefs and subscribeToDocument
44
// first one is calling the other on every ref and subscribeToDocument may call
@@ -7,7 +7,7 @@ function subscribeToRefs ({
77
subs,
88
refs,
99
target,
10-
key,
10+
path,
1111
data,
1212
depth,
1313
resolve
@@ -30,22 +30,24 @@ function subscribeToRefs ({
3030
const sub = subs[refKey]
3131
const ref = refs[refKey]
3232

33-
// documents are fully replaced so it's necessary to bind things again
34-
// TODO it would be nice to prevent unnecessary unbind
35-
if (sub) sub.unbind()
33+
if (sub) {
34+
if (sub.path !== ref.path) sub.unbind()
35+
// if has already be bound and as we always walk the objects, it will work
36+
else return
37+
}
3638

3739
// maybe wrap the unbind function to call unbind on every child
38-
const [innerObj, innerKey] = deepGetSplit(target[key], refKey)
39-
if (!innerObj) {
40-
console.log('=== ERROR ===')
41-
console.log(data, refKey, key, innerObj, innerKey)
42-
console.log('===')
43-
}
40+
// const [innerObj, innerKey] = deepGetSplit(target[key], refKey)
41+
// if (!innerObj) {
42+
// console.log('=== ERROR ===')
43+
// console.log(data, refKey, key, innerObj, innerKey)
44+
// console.log('===')
45+
// }
4446
subs[refKey] = {
4547
unbind: subscribeToDocument({
4648
ref,
47-
target: innerObj,
48-
key: innerKey,
49+
target,
50+
path: `${path}.${refKey}`,
4951
depth,
5052
resolve
5153
}),
@@ -62,6 +64,7 @@ function bindCollection ({
6264
reject
6365
}) {
6466
// TODO wait to get all data
67+
// XXX support pathes? nested.obj.list
6568
const array = vm[key] = []
6669
const originalResolve = resolve
6770
let isResolved
@@ -82,24 +85,24 @@ function bindCollection ({
8285
refs,
8386
subs,
8487
target: array,
85-
key: newIndex,
88+
path: newIndex,
8689
depth: 0,
8790
resolve: resolve.bind(null, doc)
8891
})
8992
},
9093
modified: ({ oldIndex, newIndex, doc }) => {
9194
const subs = arraySubs.splice(oldIndex, 1)[0]
9295
arraySubs.splice(newIndex, 0, subs)
93-
array.splice(oldIndex, 1)
96+
const oldData = array.splice(oldIndex, 1)[0]
9497
const snapshot = createSnapshot(doc)
95-
const [data, refs] = extractRefs(snapshot)
98+
const [data, refs] = extractRefs(snapshot, oldData)
9699
array.splice(newIndex, 0, data)
97100
subscribeToRefs({
98101
data,
99102
refs,
100103
subs,
101104
target: array,
102-
key: newIndex,
105+
path: newIndex,
103106
depth: 0,
104107
resolve
105108
})
@@ -131,6 +134,7 @@ function bindCollection ({
131134
resolve = ({ id }) => {
132135
if (id in validDocs) {
133136
if (++count >= expectedItems) {
137+
// TODO use array instead?
134138
originalResolve(vm[key])
135139
// reset resolve to noop
136140
resolve = _ => {}
@@ -157,21 +161,23 @@ function bindCollection ({
157161
}
158162
}
159163

160-
function updateDataFromDocumentSnapshot ({ snapshot, target, key, subs, depth = 0, resolve }) {
161-
const [data, refs] = extractRefs(snapshot)
162-
target[key] = data
164+
function updateDataFromDocumentSnapshot ({ snapshot, target, path, subs, depth = 0, resolve }) {
165+
const [data, refs] = extractRefs(snapshot, walkGet(target, path))
166+
// TODO use walkSet?
167+
walkSet(target, path, data)
168+
// target[key] = data
163169
subscribeToRefs({
164170
data,
165171
subs,
166172
refs,
167173
target,
168-
key,
174+
path,
169175
depth,
170176
resolve
171177
})
172178
}
173179

174-
function subscribeToDocument ({ ref, target, key, depth, resolve }) {
180+
function subscribeToDocument ({ ref, target, path, depth, resolve }) {
175181
const subs = Object.create(null)
176182
// console.log('subDoc(1)', key)
177183
// const lol = key
@@ -181,13 +187,15 @@ function subscribeToDocument ({ ref, target, key, depth, resolve }) {
181187
updateDataFromDocumentSnapshot({
182188
snapshot: createSnapshot(doc),
183189
target,
184-
key,
190+
path,
185191
subs,
186192
depth,
187193
resolve
188194
})
189195
} else {
190-
target[key] = null
196+
// TODO use deep set
197+
walkSet(target, path, null)
198+
// target[path] = null
191199
resolve()
192200
}
193201
})
@@ -214,13 +222,14 @@ function bindDocument ({
214222
const subs = Object.create(null)
215223
// bind here the function so it can be resolved anywhere
216224
// this is specially useful for refs
225+
// TODO use walkGet?
217226
resolve = callOnceWithArg(resolve, () => vm[key])
218227
const unbind = document.onSnapshot(doc => {
219228
if (doc.exists) {
220229
updateDataFromDocumentSnapshot({
221230
snapshot: createSnapshot(doc),
222231
target: vm,
223-
key,
232+
path: key,
224233
subs,
225234
resolve
226235
})

src/utils.js

+15-14
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ function isObject (o) {
99
return o && typeof o === 'object'
1010
}
1111

12-
export function extractRefs (doc, path = '', result = [{}, {}]) {
12+
export function extractRefs (doc, oldDoc, path = '', result = [{}, {}]) {
13+
// must be set here because walkGet can return null or undefined
14+
oldDoc = oldDoc || {}
1315
const idDescriptor = Object.getOwnPropertyDescriptor(doc, 'id')
1416
if (idDescriptor && !idDescriptor.enumerable) {
1517
Object.defineProperty(result[0], 'id', idDescriptor)
@@ -18,16 +20,16 @@ export function extractRefs (doc, path = '', result = [{}, {}]) {
1820
const ref = doc[key]
1921
// if it's a ref
2022
if (typeof ref.isEqual === 'function') {
21-
tot[0][key] = ref.path
23+
tot[0][key] = oldDoc[key] || ref.path
2224
// TODO handle subpathes?
2325
tot[1][path + key] = ref
2426
} else if (Array.isArray(ref)) {
2527
// TODO handle array
2628
tot[0][key] = Array(ref.length).fill(null)
27-
extractRefs(ref, path + key + '.', [tot[0][key], tot[1]])
29+
extractRefs(ref, oldDoc[key] || {}, path + key + '.', [tot[0][key], tot[1]])
2830
} else if (isObject(ref)) {
2931
tot[0][key] = {}
30-
extractRefs(ref, path + key + '.', [tot[0][key], tot[1]])
32+
extractRefs(ref, oldDoc[key] || {}, path + key + '.', [tot[0][key], tot[1]])
3133
} else {
3234
tot[0][key] = ref
3335
}
@@ -45,15 +47,14 @@ export function callOnceWithArg (fn, argFn) {
4547
}
4648
}
4749

48-
export function deepGetSplit (obj, path) {
49-
const keys = path.split('.')
50-
// We want the containing obj and the last key
51-
// key is the one we're going to bind to
50+
export function walkGet (obj, path) {
51+
return path.split('.').reduce((target, key) => target[key], obj)
52+
}
53+
54+
export function walkSet (obj, path, value) {
55+
// path can be a number
56+
const keys = ('' + path).split('.')
5257
const key = keys.pop()
53-
return [
54-
keys.reduce((res, key) => {
55-
return res[key]
56-
}, obj),
57-
key
58-
]
58+
const target = keys.reduce((target, key) => target[key], obj)
59+
return (target[key] = value)
5960
}

test/refs-collections.spec.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,10 @@ test('updates when modifying an item', async () => {
138138
])
139139
})
140140

141-
test('removes the DocumentReference when modifying an item', async () => {
141+
test('keeps old data of refs when modifying an item', async () => {
142142
await vm.$bind('items', collection)
143143
await first.update({ newThing: true })
144144

145-
expect(typeof vm.items[0].ref).toBe('string')
146-
await delay(5)
147145
expect(vm.items[0]).toEqual({
148146
ref: { isA: true },
149147
newThing: true

test/refs-documents.spec.js

+17
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,23 @@ test('unbinds previously bound document when overwriting a bound', async () => {
154154
spy.mockRestore()
155155
})
156156

157+
test('does not rebind if it is the same ref', async () => {
158+
const c = collection.doc()
159+
160+
const spy = spyOnSnapshot(c)
161+
await c.update({ baz: 'baz' })
162+
await d.update({ ref: c })
163+
// NOTE see #1
164+
await delay(5)
165+
expect(spy).toHaveBeenCalledTimes(1)
166+
167+
await d.update({ ref: c })
168+
await delay(5)
169+
170+
expect(spy).toHaveBeenCalledTimes(1)
171+
spy.mockRestore()
172+
})
173+
157174
test('resolves the promise when refs are resolved in a document', async () => {
158175
await a.update({ a: true })
159176
await b.update({ ref: a })

0 commit comments

Comments
 (0)