forked from WebMemex/webmemex-extension
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfind-visits.js
141 lines (133 loc) · 5.39 KB
/
find-visits.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
import fromPairs from 'lodash/fp/fromPairs'
import update from 'lodash/fp/update'
import reverse from 'lodash/fp/reverse'
import unionBy from 'lodash/unionBy' // the fp version does not support >2 inputs (lodash issue #3025)
import sortBy from 'lodash/fp/sortBy'
import db, { normaliseFindResult, resultRowsById } from 'src/pouchdb'
import { convertVisitDocId, visitKeyPrefix, getTimestamp } from 'src/activity-logger'
import { getPages } from './find-pages'
// Nest the page docs into the visit docs, and return the latter.
function insertPagesIntoVisits({visitsResult, pagesResult, presorted=false}) {
// If pages are not already passed to us, get them and call ourselves again.
if (pagesResult === undefined) {
// Get the page of each visit.
const pageIds = visitsResult.rows.map(row => row.doc.page._id)
return getPages({
pageIds,
// Assume that we always want to follow redirects.
followRedirects: true,
}).then(pagesResult =>
// Invoke ourselves with the found pages.
insertPagesIntoVisits({visitsResult, pagesResult, presorted: true})
)
}
if (presorted) {
// A small optimisation if the results already match one to one.
return update('rows', rows => rows.map(
(row, i) => update('doc.page', ()=>pagesResult.rows[i].doc)(row)
))(visitsResult)
}
else {
// Read each visit's doc.page._id and replace it with the specified page.
const pagesById = resultRowsById(pagesResult)
return update('rows', rows => rows.map(
update('doc.page', page => pagesById[page._id].doc)
))(visitsResult)
}
}
// Get the most recent visits, each with the visited page already nested in it.
export function getLastVisits({
limit
}={}) {
return db.find({
selector: {
// workaround for lack of startkey/endkey support
_id: { $gte: visitKeyPrefix, $lte: `${visitKeyPrefix}\uffff`}
},
sort: [{_id: 'desc'}],
limit,
}).then(
normaliseFindResult
).then(
visitsResult => insertPagesIntoVisits({visitsResult})
)
}
// Find all visits to the given pages, return them with the pages nested inside.
// Resulting visits are sorted by time, descending.
// XXX: If pages are redirected, only visits to the source page are found.
export function findVisitsToPages({pagesResult}) {
const pageIds = pagesResult.rows.map(row => row.id)
return db.find({
// Find the visits that contain the pages
selector: {
'page._id': {$in: pageIds},
// workaround for lack of startkey/endkey support
_id: {$gte: visitKeyPrefix, $lte: `${visitKeyPrefix}\uffff`},
},
// Sort them by time, newest first
sort: [{'_id': 'desc'}],
}).then(
normaliseFindResult
).then(visitsResult =>
insertPagesIntoVisits({visitsResult, pagesResult})
)
}
// Expand the results, adding a bit of context around each visit.
// Currently context means a few preceding and succeding visits.
export function addVisitsContext({
visitsResult,
maxPrecedingVisits=2,
maxSuccedingVisits=2,
maxPrecedingTime = 1000*60*20,
maxSuccedingTime = 1000*60*20,
}) {
// For each visit, get its context.
const promises = visitsResult.rows.map(row => {
const timestamp = getTimestamp(row.doc)
// Get preceding visits
return db.allDocs({
include_docs: true,
// Subtract 1ms to exclude itself (there is no include_start option).
startkey: convertVisitDocId({timestamp: timestamp-1}),
endkey: convertVisitDocId({timestamp: timestamp-maxPrecedingTime}),
descending: true,
limit: maxPrecedingVisits,
}).then(prequelResult => {
// Get succeeding visits
return db.allDocs({
include_docs: true,
// Add 1ms to exclude itself (there is no include_start option).
startkey: convertVisitDocId({timestamp: timestamp+1}),
endkey: convertVisitDocId({timestamp: timestamp+maxSuccedingTime}),
limit: maxSuccedingVisits,
}).then(sequelResult => {
// Combine them as if they were one result.
return {
rows: prequelResult.rows.concat(reverse(sequelResult.rows))
}
})
}).then(contextResult =>
// Insert pages as usual.
insertPagesIntoVisits({visitsResult: contextResult})
).then(
// Mark each row as being a 'contextual result'.
update('rows', rows =>
rows.map(row => ({...row, isContextualResult: true}))
)
)
})
// When the context of each visit has been retrieved, merge and return them.
return Promise.all(promises).then(contextResults =>
// Insert the contexts (prequels+sequels) into the original results
update('rows', rows => {
// Concat all results and all their contexts, but remove duplicates.
const allRows = unionBy(
rows,
...contextResults.map(result => result.rows),
'id' // Use the visits' ids as the uniqueness criterion
)
// Sort them again by timestamp
return sortBy(row => -getTimestamp(row.doc))(allRows)
})(visitsResult)
)
}