forked from sheepzh/time-tracker-4-browser
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathidb.ts
More file actions
130 lines (114 loc) · 5.22 KB
/
idb.ts
File metadata and controls
130 lines (114 loc) · 5.22 KB
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
import {
BaseIDBStorage, iterateCursor, req2Promise,
type Index, type IndexResult, type Key, type Table,
} from '@db/common/indexed-storage'
import { MILL_PER_DAY, MILL_PER_SECOND } from '@util/time'
import type { TimelineCondition, TimelineDatabase } from './types'
const TIME_LIFE_CYCLE = MILL_PER_DAY * 366
// If two tick with the same host is near 1 sec, then merge them to one
const MERGE_THRESHOLD = MILL_PER_SECOND
const canMerge = (exist: timer.timeline.Tick, tick: timer.timeline.Tick) => {
const { start: existStart, host: existHost } = exist
const { start, host } = tick
return existHost === host && start >= existStart && start <= existStart + MERGE_THRESHOLD
}
const isConflict = (item: timer.timeline.Tick, tick: timer.timeline.Tick) => {
const { start: itemStart, duration: itemDuration } = item
const { start } = tick
return itemStart <= start && start < itemStart + itemDuration
}
type IndexCoverage = {
host?: boolean
start?: boolean
}
class CleanThrottle {
private lastTime = 0
private readonly interval: number = MILL_PER_DAY
tryClean(doClean: () => void): void {
const now = Date.now()
if (now - this.lastTime >= this.interval) {
this.lastTime = now
doClean()
}
}
}
export default class IDBTimelineDatabase extends BaseIDBStorage<timer.timeline.Tick> implements TimelineDatabase {
indexes: Index<timer.timeline.Tick>[] = [
'host', 'start',
]
key: Key<timer.timeline.Tick> | Key<timer.timeline.Tick>[] = ['host', 'start']
table: Table = 'timeline'
private cleanThrottle = new CleanThrottle()
batchSave(ticks: timer.timeline.Tick[]): Promise<void> {
return this.withStore(async store => {
const index = this.assertIndex(store, 'host')
const hosts = Array.from(new Set(ticks.map(tick => tick.host)))
// Fetch existing records for all hosts
const existByHost = new Map<string, timer.timeline.Tick[]>()
await Promise.all(hosts.map(async host => {
const req = index.getAll(IDBKeyRange.only(host))
const exist = await req2Promise<timer.timeline.Tick[]>(req)
exist && existByHost.set(host, exist)
}))
const toSave: timer.timeline.Tick[] = []
const toDelete: timer.timeline.Tick[] = []
ticks.forEach(tick => {
const existForHost = existByHost.get(tick.host) ?? []
// Check if there's any conflict
const anyConflict = existForHost.some(exist => isConflict(exist, tick))
if (anyConflict) return
// Find a record that can be merged
const mergeTarget = existForHost.find(exist => canMerge(exist, tick))
if (mergeTarget) {
toDelete.push(mergeTarget)
const { host, start, duration } = tick
const newStart = Math.min(mergeTarget.start, start)
const newEnd = Math.max(mergeTarget.start + mergeTarget.duration, start + duration)
const newDuration = newEnd - newStart
toSave.push({ host, start: newStart, duration: newDuration })
} else {
// No conflict and no merge, save the new tick
toSave.push(tick)
}
})
toDelete.forEach(tick => store.delete([tick.host, tick.start]))
toSave.forEach(tick => store.put(tick))
}, 'readwrite')
}
async select(cond?: TimelineCondition): Promise<timer.timeline.Tick[]> {
const rows = await this.withStore(async store => {
const { cursorReq, coverage = {} } = this.judgeIndex(store, cond)
const rows = await iterateCursor<timer.timeline.Tick>(cursorReq)
const { start: cs, host: ch } = cond ?? {}
return rows.filter(tick => {
const { host, start } = tick
if (cs && !coverage.start && start < cs) return false
if (ch && !coverage.host && host !== ch) return false
return true
})
}, 'readonly')
// Cleanup outdated ticks periodically
this.cleanThrottle.tryClean(() => this.withStore(store => {
const index = this.assertIndex(store, 'start')
const req = index.openCursor(IDBKeyRange.upperBound(Date.now() - TIME_LIFE_CYCLE, true))
iterateCursor(req, cursor => { cursor.delete() })
}, 'readwrite').catch(e => console.error('Failed to cleanup outdated ticks', e)))
return rows
}
private judgeIndex(store: IDBObjectStore, cond?: TimelineCondition): IndexResult<IndexCoverage> {
const { host, start } = cond ?? {}
if (host) {
return {
cursorReq: this.assertIndexCursor(store, 'host', IDBKeyRange.only(host)),
coverage: { host: true },
}
} else if (start !== undefined && start > 0) {
return {
cursorReq: this.assertIndexCursor(store, 'start', IDBKeyRange.lowerBound(start, false)),
coverage: { start: true },
}
} else {
return { cursorReq: store.openCursor() }
}
}
}