UNPKG

4.49 kBPlain TextView Raw
1import { DB, BatchDBOp, ENCODING_OPTS } from './db'
2// eslint-disable-next-line implicit-dependencies/no-implicit
3import type { LevelUp } from 'levelup'
4
5export type Checkpoint = {
6 // We cannot use a Buffer => Buffer map directly. If you create two Buffers with the same internal value,
7 // then when setting a value on the Map, it actually creates two indices.
8 keyValueMap: Map<string, Buffer | null>
9 root: Buffer
10}
11
12/**
13 * DB is a thin wrapper around the underlying levelup db,
14 * which validates inputs and sets encoding type.
15 */
16export class CheckpointDB extends DB {
17 public checkpoints: Checkpoint[]
18
19 /**
20 * Initialize a DB instance. If `leveldb` is not provided, DB
21 * defaults to an [in-memory store](https://github.com/Level/memdown).
22 * @param leveldb - An abstract-leveldown compliant store
23 */
24 constructor(leveldb?: LevelUp) {
25 super(leveldb)
26 // Roots of trie at the moment of checkpoint
27 this.checkpoints = []
28 }
29
30 /**
31 * Is the DB during a checkpoint phase?
32 */
33 get isCheckpoint() {
34 return this.checkpoints.length > 0
35 }
36
37 /**
38 * Adds a new checkpoint to the stack
39 * @param root
40 */
41 checkpoint(root: Buffer) {
42 this.checkpoints.push({ keyValueMap: new Map<string, Buffer>(), root })
43 }
44
45 /**
46 * Commits the latest checkpoint
47 */
48 async commit() {
49 const { keyValueMap } = this.checkpoints.pop()!
50 if (!this.isCheckpoint) {
51 // This was the final checkpoint, we should now commit and flush everything to disk
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 // dump everything into the current (higher level) cache
70 const currentKeyValueMap = this.checkpoints[this.checkpoints.length - 1].keyValueMap
71 keyValueMap.forEach((value, key) => currentKeyValueMap.set(key, value))
72 }
73 }
74
75 /**
76 * Reverts the latest checkpoint
77 */
78 async revert() {
79 const { root } = this.checkpoints.pop()!
80 return root
81 }
82
83 /**
84 * Retrieves a raw value from leveldb.
85 * @param key
86 * @returns A Promise that resolves to `Buffer` if a value is found or `null` if no value is found.
87 */
88 async get(key: Buffer): Promise<Buffer | null> {
89 // Lookup the value in our cache. We return the latest checkpointed value (which should be the value on disk)
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 // Nothing has been found in cache, look up from disk
97
98 const value = await super.get(key)
99 if (this.isCheckpoint) {
100 // Since we are a checkpoint, put this value in cache, so future `get` calls will not look the key up again from disk.
101 this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), value)
102 }
103
104 return value
105 }
106
107 /**
108 * Writes a value directly to leveldb.
109 * @param key The key as a `Buffer`
110 * @param value The value to be stored
111 */
112 async put(key: Buffer, val: Buffer): Promise<void> {
113 if (this.isCheckpoint) {
114 // put value in cache
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 * Removes a raw value in the underlying leveldb.
123 * @param keys
124 */
125 async del(key: Buffer): Promise<void> {
126 if (this.isCheckpoint) {
127 // delete the value in the current cache
128 this.checkpoints[this.checkpoints.length - 1].keyValueMap.set(key.toString('binary'), null)
129 } else {
130 // delete the value on disk
131 await this._leveldb.del(key, ENCODING_OPTS)
132 }
133 }
134
135 /**
136 * Performs a batch operation on db.
137 * @param opStack A stack of levelup operations
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}