-
Notifications
You must be signed in to change notification settings - Fork 5
/
client-connector.js
149 lines (136 loc) · 4.74 KB
/
client-connector.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/* global Meteor */
import React, { createContext, useState, useContext, useRef } from 'react'
import { EJSON } from 'meteor/ejson'
import { useTracker } from 'meteor/react-meteor-data'
import { makePagedRun, makeSingleRun, makeDataMethod, makePruneMethod } from './both'
import { getCollectionByName } from './client-collection'
const ConnectorContext = createContext(false)
export const DataHydrationProvider = ({ handle, children }) => {
// :TODO: Add some development mode checks to make sure user sets handle.isHydrating correctly
/**
* Handle should be passed by user in the shape of:
* { isHydrating: true|false }
*/
return <ConnectorContext.Provider value={handle}>
{children}
</ConnectorContext.Provider>
}
let requestCounter = 0
export const createConnector = ({ name, collection, validate, query, single = false }) => {
const run = single
? makeSingleRun(collection, query)
: makePagedRun(collection, query)
const dataMethod = makeDataMethod(name, validate, run, query)
const pruneMethod = makePruneMethod(name, collection, validate, query)
return (args = {}, onLoad = null) => {
const hydrationContext = useContext(ConnectorContext)
const [isLoading, setIsLoading] = useState(false)
const { current: refs } = useRef({
requestId: requestCounter,
onLoad: null,
lastArgValues: null
})
// If onLoad is defined inline in the user (likely) it's ref will change with each render pass,
// so we need to make sure we always have the latest one in the effect callback - but we don't
// want to re-invoke the effect every time the reference changes, which is every time.
refs.onLoad = onLoad
// We only want to refetch data if `args` changes. We also need this to start synchronously,
// so that we can correctly ascertain whether react is currently hydrating.
const argValues = Object.values(args)
// We don't need to load data, but we need to call onLoad with the correct documents from offline storage.
// Data should already have been hydrated.
if (hydrationContext.isHydrating) {
if (refs.onLoad) {
validate(args)
const docs = run(args)
refs.onLoad(docs)
}
} else if (!isLoading && !isArgsEqual(argValues, refs.lastArgValues)) {
setIsLoading(true)
deferPrune(pruneMethod, collection, query, args)
// Capture requestId in scope, and compare to make sure a new request hasn't started before we're done
const requestId = refs.requestId = requestCounter++
dataMethod.call(args, (err, res) => {
let docs
if (err) {
console.error(err)
docs = []
} else {
docs = res
if (single) {
collection.upsert(docs._id, docs)
} else {
for (const doc of docs) {
collection.upsert(doc._id, doc)
}
}
}
if (requestId === refs.requestId) {
Meteor.defer(() => {
if (requestId !== refs.requestId) {
return
}
setIsLoading(false)
if (refs.onLoad) {
refs.onLoad(docs)
}
})
}
})
}
refs.lastArgValues = argValues
return useTracker(() => {
validate(args)
return [run(args), isLoading]
}, [isLoading, ...Object.values(args)])
}
}
const deferPrune = (pruneMethod, collection, query, args) => Meteor.defer(() => {
const allLocal = collection.find(query(args)).fetch()
const IDs = allLocal.map(doc => doc._id)
pruneMethod.call({ ...args, IDs }, (err, res) => {
if (err) {
console.error(err)
return
}
for (const id of res) {
collection.remove(id)
}
})
})
// :TODO: use a prune method - probably store a lookup table like getCollectionByName
export const hydrateData = (id = '__NPCollectionCaptureData__') => {
const collectionDataNode = document.getElementById(id)
if (collectionDataNode) {
const data = EJSON.parse(collectionDataNode.innerText)
updateCollections(data)
collectionDataNode.parentNode.removeChild(collectionDataNode)
}
}
export const updateCollections = (data) => {
const cols = []
for (const collectionData of data) {
const col = getCollectionByName(collectionData.name)
for (const doc of collectionData.docs) {
col.upsert(doc._id, doc)
}
cols.push(col)
}
}
const is = (x, y) =>
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
const isArgsEqual = (nextDeps, prevDeps) => {
if (!nextDeps || !prevDeps) {
return false
}
const len = nextDeps.length
if (prevDeps.length !== len) {
return false
}
for (let i = 0; i < len; i++) {
if (!is(nextDeps[i], prevDeps[i])) {
return false
}
}
return true
}