1 | import { DB, BatchDBOp, ENCODING_OPTS } from './db'
|
2 |
|
3 | import type { LevelUp } from 'levelup'
|
4 |
|
5 | export type Checkpoint = {
|
6 |
|
7 |
|
8 | keyValueMap: Map<string, Buffer | null>
|
9 | root: Buffer
|
10 | }
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | export class CheckpointDB extends DB {
|
17 | public checkpoints: Checkpoint[]
|
18 |
|
19 | |
20 |
|
21 |
|
22 |
|
23 |
|
24 | constructor(leveldb?: LevelUp) {
|
25 | super(leveldb)
|
26 |
|
27 | this.checkpoints = []
|
28 | }
|
29 |
|
30 | |
31 |
|
32 |
|
33 | get isCheckpoint() {
|
34 | return this.checkpoints.length > 0
|
35 | }
|
36 |
|
37 | |
38 |
|
39 |
|
40 |
|
41 | checkpoint(root: Buffer) {
|
42 | this.checkpoints.push({ keyValueMap: new Map<string, Buffer>(), root })
|
43 | }
|
44 |
|
45 | |
46 |
|
47 |
|
48 | async commit() {
|
49 | const { keyValueMap } = this.checkpoints.pop()!
|
50 | if (!this.isCheckpoint) {
|
51 |
|
52 | const batchOp: BatchDBOp[] = []
|
53 | keyValueMap.forEach(function (value, key) {
|
54 | if (value === null) {
|
55 | batchOp.push({
|
56 | type: 'del',
|
57 | key: Buffer.from(key, 'binary'),
|
58 | })
|
59 | } else {
|
60 | batchOp.push({
|
61 | type: 'put',
|
62 | key: Buffer.from(key, 'binary'),
|
63 | value,
|
64 | })
|
65 | }
|
66 | })
|
67 | await this.batch(batchOp)
|
68 | } else {
|
69 |
|
70 | const currentKeyValueMap = this.checkpoints[this.checkpoints.length - 1].keyValueMap
|
71 | keyValueMap.forEach((value, key) => currentKeyValueMap.set(key, value))
|
72 | }
|
73 | }
|
74 |
|
75 | |
76 |
|
77 |
|
78 | async revert() {
|
79 | const { root } = this.checkpoints.pop()!
|
80 | return root
|
81 | }
|
82 |
|
83 | |
84 |
|
85 |
|
86 |
|
87 |
|
88 | async get(key: Buffer): Promise<Buffer | null> {
|
89 |
|
90 | for (let index = this.checkpoints.length - 1; index >= 0; index--) {
|
91 | const value = this.checkpoints[index].keyValueMap.get(key.toString('binary'))
|
92 | if (value !== undefined) {
|
93 | return value
|
94 | }
|
95 | }
|
96 |
|
97 |
|
98 | const value = await super.get(key)
|
99 | if (this.isCheckpoint) {
|
100 |
|
101 | this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), value)
|
102 | }
|
103 |
|
104 | return value
|
105 | }
|
106 |
|
107 | |
108 |
|
109 |
|
110 |
|
111 |
|
112 | async put(key: Buffer, val: Buffer): Promise<void> {
|
113 | if (this.isCheckpoint) {
|
114 |
|
115 | this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), val)
|
116 | } else {
|
117 | await super.put(key, val)
|
118 | }
|
119 | }
|
120 |
|
121 | |
122 |
|
123 |
|
124 |
|
125 | async del(key: Buffer): Promise<void> {
|
126 | if (this.isCheckpoint) {
|
127 |
|
128 | this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), null)
|
129 | } else {
|
130 |
|
131 | await this._leveldb.del(key, ENCODING_OPTS)
|
132 | }
|
133 | }
|
134 |
|
135 | |
136 |
|
137 |
|
138 |
|
139 | async batch(opStack: BatchDBOp[]): Promise<void> {
|
140 | if (this.isCheckpoint) {
|
141 | for (const op of opStack) {
|
142 | if (op.type === 'put') {
|
143 | await this.put(op.key, op.value)
|
144 | } else if (op.type === 'del') {
|
145 | await this.del(op.key)
|
146 | }
|
147 | }
|
148 | } else {
|
149 | await super.batch(opStack)
|
150 | }
|
151 | }
|
152 | }
|