hyperbee2 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/write.js CHANGED
@@ -1,24 +1,46 @@
1
1
  const b4a = require('b4a')
2
2
  const c = require('compact-encoding')
3
- const { encodeBlock } = require('./encoding.js')
3
+ const { OP_COHORT } = require('./compression.js')
4
+ const { encodeBlock, TYPE_COMPAT, TYPE_LATEST } = require('./encoding.js')
4
5
  const {
5
- DataPointer,
6
+ Pointer,
7
+ KeyPointer,
8
+ ValuePointer,
6
9
  TreeNode,
7
10
  TreeNodePointer,
8
11
  MIN_KEYS,
9
- UNCHANGED,
10
- CHANGED,
12
+ INSERTED,
11
13
  NEEDS_SPLIT
12
14
  } = require('./tree.js')
13
15
 
16
+ const PREFERRED_BLOCK_SIZE = 4096
17
+ const INLINE_VALUE_SIZE = 4096
18
+ const ESTIMATED_POINTER_SIZE = 12 // conversative, seq=8b, offset=2b, core=2b
19
+
14
20
  module.exports = class WriteBatch {
15
- constructor(tree, { length = -1, key = null, autoUpdate = true, blockFormat = 3 } = {}) {
21
+ constructor(tree, opts = {}) {
22
+ const {
23
+ length = -1,
24
+ key = null,
25
+ autoUpdate = true,
26
+ compat = false,
27
+ type = compat ? TYPE_COMPAT : TYPE_LATEST,
28
+ deltaMax = supportsCompression(type) ? 16 : 0,
29
+ deltaMin = supportsCompression(type) ? 1 : Infinity,
30
+ inlineValueSize = INLINE_VALUE_SIZE,
31
+ preferredBlockSize = PREFERRED_BLOCK_SIZE
32
+ } = opts
33
+
16
34
  this.tree = tree
35
+ this.deltaMax = deltaMax
36
+ this.deltaMin = deltaMin
37
+ this.inlineValueSize = inlineValueSize
38
+ this.preferredBlockSize = preferredBlockSize
17
39
  this.snapshot = tree.snapshot()
18
40
  this.autoUpdate = autoUpdate
19
41
  this.length = length
20
42
  this.key = key
21
- this.blockFormat = blockFormat
43
+ this.type = type
22
44
  this.closed = false
23
45
  this.applied = 0
24
46
  this.root = null
@@ -62,16 +84,10 @@ module.exports = class WriteBatch {
62
84
 
63
85
  const changed = length === 0
64
86
  const seq = length === 0 ? 0 : length - 1
87
+ const value = changed ? new TreeNode([], []) : null
65
88
 
66
89
  this.length = length
67
- this.root = new TreeNodePointer(
68
- context,
69
- 0,
70
- seq,
71
- 0,
72
- changed,
73
- changed ? new TreeNode([], []) : null
74
- )
90
+ this.root = new TreeNodePointer(context, 0, seq, 0, changed, value)
75
91
 
76
92
  for (const op of ops) {
77
93
  if (op.put) op.applied = await this._put(op.key, op.value)
@@ -113,12 +129,13 @@ module.exports = class WriteBatch {
113
129
 
114
130
  while (s < e) {
115
131
  const mid = (s + e) >> 1
116
- const m = v.keys[mid]
132
+ const m = v.keys.get(mid)
117
133
 
118
134
  c = b4a.compare(target, m.key)
119
135
 
120
136
  if (c === 0) {
121
- if (b4a.equals(m.value, value)) return false
137
+ const existing = await this.snapshot.inflateValue(m)
138
+ if (b4a.equals(existing, value)) return false
122
139
  v.setValue(this.tree.context, mid, value)
123
140
  for (let i = 0; i < stack.length; i++) stack[i].changed = true
124
141
  return true
@@ -129,16 +146,21 @@ module.exports = class WriteBatch {
129
146
  }
130
147
 
131
148
  const i = c < 0 ? e : s
132
- ptr = v.children[i]
149
+ ptr = v.children.get(i)
133
150
  }
134
151
 
135
152
  const v = ptr.value ? this.snapshot.bump(ptr) : await this.snapshot.inflate(ptr)
136
- let status = v.put(this.tree.context, target, value, null)
137
-
138
- if (status === UNCHANGED) return false
153
+ let status = v.insertLeaf(this.tree.context, target, value)
154
+
155
+ if (status >= 0) {
156
+ // already exists, upsert if changed
157
+ const m = v.keys.get(status)
158
+ const existing = await this.snapshot.inflateValue(m)
159
+ if (b4a.equals(existing, value)) return false
160
+ v.setValue(this.tree.context, status, value)
161
+ }
139
162
 
140
163
  ptr.changed = true
141
-
142
164
  for (let i = 0; i < stack.length; i++) stack[i].changed = true
143
165
 
144
166
  while (status === NEEDS_SPLIT) {
@@ -148,14 +170,15 @@ module.exports = class WriteBatch {
148
170
 
149
171
  if (parent) {
150
172
  const p = parent.value ? this.snapshot.bump(parent) : await this.snapshot.inflate(parent)
151
- status = p.put(this.tree.context, median.key, median.value, right)
173
+ status = p.insertNode(this.tree.context, median, right)
152
174
  ptr = parent
153
175
  } else {
154
176
  this.root = new TreeNodePointer(this.tree.context, 0, 0, 0, true, new TreeNode([], []))
155
177
  this.root.value.keys.push(median)
156
- this.root.value.children.push(ptr, right)
178
+ this.root.value.children.push(ptr)
179
+ this.root.value.children.push(right)
157
180
  this.snapshot.bump(this.root)
158
- status = UNCHANGED
181
+ status = INSERTED
159
182
  }
160
183
  }
161
184
 
@@ -177,12 +200,11 @@ module.exports = class WriteBatch {
177
200
 
178
201
  while (s < e) {
179
202
  const mid = (s + e) >> 1
180
- c = b4a.compare(key, v.keys[mid].key)
203
+ c = b4a.compare(key, v.keys.get(mid).key)
181
204
 
182
205
  if (c === 0) {
183
206
  if (v.children.length) await this._setKeyToNearestLeaf(v, mid, stack)
184
207
  else v.removeKey(mid)
185
-
186
208
  // we mark these as changed late, so we don't rewrite them if it is a 404
187
209
  for (let i = 0; i < stack.length; i++) stack[i].changed = true
188
210
  this.root = await this._rebalance(stack)
@@ -196,15 +218,15 @@ module.exports = class WriteBatch {
196
218
  if (!v.children.length) return false
197
219
 
198
220
  const i = c < 0 ? e : s
199
- ptr = v.children[i]
221
+ ptr = v.children.get(i)
200
222
  }
201
223
 
202
224
  return false
203
225
  }
204
226
 
205
227
  async _setKeyToNearestLeaf(v, index, stack) {
206
- let left = v.children[index]
207
- let right = v.children[index + 1]
228
+ let left = v.children.get(index)
229
+ let right = v.children.get(index + 1)
208
230
 
209
231
  const [ls, rs] = await Promise.all([this._leafSize(left, false), this._leafSize(right, true)])
210
232
 
@@ -213,28 +235,28 @@ module.exports = class WriteBatch {
213
235
  stack.push(right)
214
236
  let r = right.value ? this.snapshot.bump(right) : await this.snapshot.inflate(right)
215
237
  while (r.children.length) {
216
- right = r.children[0]
238
+ right = r.children.get(0)
217
239
  stack.push(right)
218
240
  r = right.value ? this.snapshot.bump(right) : await this.snapshot.inflate(right)
219
241
  }
220
- v.keys[index] = r.keys.shift()
242
+ v.keys.set(index, r.keys.shift())
221
243
  } else {
222
244
  // if fewer leaves on the right
223
245
  stack.push(left)
224
246
  let l = left.value ? this.snapshot.bump(left) : await this.snapshot.inflate(left)
225
247
  while (l.children.length) {
226
- left = l.children[l.children.length - 1]
248
+ left = l.children.get(l.children.length - 1)
227
249
  stack.push(left)
228
250
  l = left.value ? this.snapshot.bump(left) : await this.snapshot.inflate(left)
229
251
  }
230
- v.keys[index] = l.keys.pop()
252
+ v.keys.set(index, l.keys.pop())
231
253
  }
232
254
  }
233
255
 
234
256
  async _leafSize(ptr, goLeft) {
235
257
  let v = ptr.value ? this.snapshot.bump(ptr) : await this.snapshot.inflate(ptr)
236
258
  while (v.children.length) {
237
- ptr = v.children[goLeft ? 0 : v.children.length - 1]
259
+ ptr = v.children.get(goLeft ? 0 : v.children.length - 1)
238
260
  v = ptr.value ? this.snapshot.bump(ptr) : await this.snapshot.inflate(ptr)
239
261
  }
240
262
  return v.keys.length
@@ -242,6 +264,7 @@ module.exports = class WriteBatch {
242
264
 
243
265
  async _rebalance(stack) {
244
266
  const root = stack[0]
267
+ const minKeys = this.tree.context.minKeys
245
268
 
246
269
  while (stack.length > 1) {
247
270
  const ptr = stack.pop()
@@ -258,11 +281,11 @@ module.exports = class WriteBatch {
258
281
  let l = left && (left.value ? this.snapshot.bump(left) : await this.snapshot.inflate(left))
259
282
 
260
283
  // maybe borrow from left sibling?
261
- if (l && l.keys.length > MIN_KEYS) {
284
+ if (l && l.keys.length > minKeys) {
262
285
  left.changed = true
263
- v.keys.unshift(p.keys[index - 1])
286
+ v.keys.unshift(p.keys.get(index - 1))
264
287
  if (l.children.length) v.children.unshift(l.children.pop())
265
- p.keys[index - 1] = l.keys.pop()
288
+ p.keys.set(index - 1, l.keys.pop())
266
289
  return root
267
290
  }
268
291
 
@@ -270,11 +293,11 @@ module.exports = class WriteBatch {
270
293
  right && (right.value ? this.snapshot.bump(right) : await this.snapshot.inflate(right))
271
294
 
272
295
  // maybe borrow from right sibling?
273
- if (r && r.keys.length > MIN_KEYS) {
296
+ if (r && r.keys.length > minKeys) {
274
297
  right.changed = true
275
- v.keys.push(p.keys[index])
298
+ v.keys.push(p.keys.get(index))
276
299
  if (r.children.length) v.children.push(r.children.shift())
277
- p.keys[index] = r.keys.shift()
300
+ p.keys.set(index, r.keys.shift())
278
301
  return root
279
302
  }
280
303
 
@@ -289,7 +312,7 @@ module.exports = class WriteBatch {
289
312
  }
290
313
 
291
314
  left.changed = true
292
- l.merge(r, p.keys[index])
315
+ l.merge(r, p.keys.get(index))
293
316
 
294
317
  parent.changed = true
295
318
  p.removeKey(index)
@@ -297,69 +320,100 @@ module.exports = class WriteBatch {
297
320
 
298
321
  const r = root.value ? this.snapshot.bump(root) : await this.snapshot.inflate(root)
299
322
  // check if the tree shrunk
300
- if (!r.keys.length && r.children.length) return r.children[0]
323
+ if (!r.keys.length && r.children.length) return r.children.get(0)
301
324
  return root
302
325
  }
303
326
 
327
+ _shouldInlineValue(k) {
328
+ if (!k.value || supportsCompression(this.type)) return true
329
+ if (k.valuePointer) return true
330
+ return k.value.byteLength <= this.inlineValueSize
331
+ }
332
+
304
333
  async _flush() {
305
- if (!this.root || !this.root.changed) return
334
+ if (!this.root || !this.root.changed) {
335
+ return
336
+ }
337
+
338
+ if (!this.root.value) await this.snapshot.inflate(this.root)
339
+
340
+ let update = { size: 0, nodes: [], keys: [], values: [] }
341
+ let minValue = -1
306
342
 
307
- const update = { node: [], keys: [] }
308
343
  const batch = [update]
309
- const stack = [{ update, node: this.root }]
344
+ const stack = [this.root]
345
+ const values = []
346
+
310
347
  const context = this.tree.context.getLocalContext()
348
+ const activeRequests = this.tree.activeRequests
311
349
 
312
- await context.update(this.tree.activeRequests)
350
+ await context.update(activeRequests)
313
351
 
314
352
  while (stack.length > 0) {
315
- const { update, node } = stack.pop()
353
+ const node = stack.pop()
316
354
 
317
- node.changed = false
318
- update.node.push(node)
319
-
320
- for (let i = 0; i < node.value.keys.length; i++) {
321
- const k = node.value.keys[i]
355
+ if (this.type !== TYPE_COMPAT && update.size >= this.preferredBlockSize) {
356
+ update = { size: 0, nodes: [], keys: [], values: [] }
357
+ batch.push(update)
358
+ }
322
359
 
323
- if (!k.changed) {
324
- k.core = await context.getCoreOffset(k.context, k.core, this.tree.activeRequests)
325
- k.context = context
326
- continue
327
- }
360
+ update.nodes.push(node)
361
+ update.size += getEstimatedNodeSize(node.value)
328
362
 
329
- k.changed = false
330
- update.keys.push(k)
331
- }
363
+ const keys = node.value.keys
364
+ const children = node.value.children
332
365
 
333
- let first = true
366
+ for (let i = 0; i < keys.entries.length; i++) {
367
+ const k = keys.entries[i]
368
+ if (!k.changed) continue
334
369
 
335
- for (let i = 0; i < node.value.children.length; i++) {
336
- const n = node.value.children[i]
370
+ if (!this._shouldInlineValue(k)) {
371
+ values.push(k)
372
+ k.valuePointer = new ValuePointer(context, 0, 0, 0, 0)
337
373
 
338
- if (!n.changed) {
339
- n.core = await context.getCoreOffset(n.context, n.core, this.tree.activeRequests)
340
- n.context = context
341
- continue
374
+ if (minValue === -1 || minValue < k.value.byteLength) {
375
+ minValue = k.value.byteLength
376
+ }
342
377
  }
343
378
 
344
- if (first || this.blockFormat === 2) {
345
- stack.push({ update, node: n })
346
- first = false
347
- } else {
348
- const update = { node: [], keys: [] }
349
- batch.push(update)
350
- stack.push({ update, node: n })
351
- }
379
+ update.keys.push(k)
380
+ update.size += getEstimatedKeySize(k)
381
+ }
382
+
383
+ for (let i = 0; i < children.entries.length; i++) {
384
+ const c = children.entries[i]
385
+ if (!c.value || !c.changed) continue
386
+ children.touch(i)
387
+ stack.push(c)
352
388
  }
353
389
  }
354
390
 
391
+ if (this.type === TYPE_COMPAT) await toCompatType(context, batch, this.ops)
392
+
355
393
  const length = context.core.length
356
394
 
357
395
  // if noop and not genesis, bail early
358
- if (this.applied === 0 && length > 0) {
359
- return
396
+ if (this.applied === 0 && length > 0) return
397
+
398
+ if (minValue > -1 && minValue + update.size < this.preferredBlockSize) {
399
+ // TODO: repack the value into the block
360
400
  }
361
401
 
362
- if (this.blockFormat === 2) toBlockFormat2(context, batch, this.ops)
402
+ if (values.length) {
403
+ update = { size: 0, nodes: [], keys: [], values: [] }
404
+
405
+ for (let i = 0; i < values.length; i++) {
406
+ const k = values[i]
407
+
408
+ update.size += getEstimatedValueSize(k)
409
+ update.values.push(k)
410
+
411
+ if (i === values.length - 1 || update.size >= this.preferredBlockSize) {
412
+ batch.push(update)
413
+ update = { size: 0, nodes: [], keys: [], values: [] }
414
+ }
415
+ }
416
+ }
363
417
 
364
418
  const blocks = new Array(batch.length)
365
419
 
@@ -368,33 +422,63 @@ module.exports = class WriteBatch {
368
422
  const seq = length + batch.length - i - 1
369
423
 
370
424
  const block = {
371
- type: 0,
425
+ type: this.type,
372
426
  checkpoint: 0,
373
427
  batch: { start: batch.length - 1 - i, end: i },
374
428
  previous: null,
429
+ metadata: null,
375
430
  tree: null,
376
- data: null,
377
- cores: null
431
+ keys: null,
432
+ values: null,
433
+ cohorts: null
434
+ }
435
+
436
+ for (const k of update.values) {
437
+ if (block.values === null) block.values = []
438
+ const ptr = k.valuePointer
439
+
440
+ ptr.core = 0
441
+ ptr.context = context
442
+ ptr.seq = seq
443
+ ptr.offset = block.values.length
444
+ ptr.split = 0
445
+
446
+ block.values.push(k.value)
447
+ k.value = null // unlinked
378
448
  }
379
449
 
380
450
  for (const k of update.keys) {
381
- if (block.data === null) block.data = []
451
+ if (block.keys === null) block.keys = []
382
452
 
383
453
  k.core = 0
384
454
  k.context = context
385
455
  k.seq = seq
386
- k.offset = block.data.length
387
- block.data.push(k)
456
+ k.offset = block.keys.length
457
+ k.changed = false
458
+
459
+ if (k.valuePointer) updateValuePointerContext(k.valuePointer, context)
460
+
461
+ block.keys.push(k)
388
462
  }
389
463
 
390
- for (const n of update.node) {
464
+ for (const n of update.nodes) {
391
465
  if (block.tree === null) block.tree = []
392
466
 
393
467
  n.core = 0
394
468
  n.context = context
395
469
  n.seq = seq
396
470
  n.offset = block.tree.length
397
- block.tree.push(n.value)
471
+ n.changed = false
472
+
473
+ const treeDelta = {
474
+ keys: n.value.keys.flush(this.deltaMax, this.deltaMin),
475
+ children: n.value.children.flush(this.deltaMax, this.deltaMin)
476
+ }
477
+
478
+ prepareCohorts(context, block, seq, treeDelta.keys, supportsCompression)
479
+ prepareCohorts(context, block, seq, treeDelta.children, false)
480
+
481
+ block.tree.push(treeDelta)
398
482
  }
399
483
 
400
484
  blocks[seq - length] = block
@@ -403,22 +487,19 @@ module.exports = class WriteBatch {
403
487
  const buffers = new Array(blocks.length)
404
488
 
405
489
  if (blocks.length > 0 && this.length > 0) {
406
- const core = this.key
407
- ? await context.getCoreOffsetByKey(this.key, this.tree.activeRequests)
408
- : 0
490
+ const core = this.key ? await context.getCoreOffsetByKey(this.key, activeRequests) : 0
409
491
  blocks[blocks.length - 1].previous = { core, seq: this.length - 1 }
410
492
  }
411
493
 
412
494
  // TODO: make this transaction safe
413
495
  if (context.changed) {
414
- context.changed = false
415
496
  context.checkpoint = context.core.length + blocks.length
416
- blocks[blocks.length - 1].cores = context.cores
497
+ blocks[blocks.length - 1].metadata = context.flush()
417
498
  }
418
499
 
419
500
  for (let i = 0; i < blocks.length; i++) {
420
501
  blocks[i].checkpoint = context.checkpoint
421
- buffers[i] = encodeBlock(blocks[i], this.blockFormat)
502
+ buffers[i] = encodeBlock(blocks[i])
422
503
  }
423
504
 
424
505
  if (this.closed) {
@@ -430,14 +511,15 @@ module.exports = class WriteBatch {
430
511
  for (let i = 0; i < batch.length; i++) {
431
512
  const update = batch[i]
432
513
 
433
- for (let j = 0; j < update.node.length; j++) {
434
- this.snapshot.bump(update.node)
514
+ for (let j = 0; j < update.nodes.length; j++) {
515
+ const node = update.nodes[j]
516
+ this.snapshot.bump(node)
435
517
  }
436
518
  }
437
519
  }
438
520
  }
439
521
 
440
- function toBlockFormat2(context, batch, ops) {
522
+ async function toCompatType(context, batch, ops) {
441
523
  const map = new Map()
442
524
  let index = 0
443
525
 
@@ -450,10 +532,69 @@ function toBlockFormat2(context, batch, ops) {
450
532
  if (!op.put && !op.applied) continue
451
533
 
452
534
  const k = map.get(b4a.toString(op.key, 'hex'))
535
+
453
536
  const j = index++
454
- if (j === batch.length) batch.push({ node: [], keys: [] })
455
- batch[j].keys = [k || new DataPointer(context, 0, 0, 0, false, op.key, op.value)]
537
+ if (j === batch.length) batch.push({ size: 0, nodes: [], keys: [], values: [] })
538
+ batch[j].keys = [k || new KeyPointer(context, 0, 0, 0, false, op.key, op.value, null)]
456
539
  }
457
540
 
458
- return batch
541
+ // compat doesnt support block 0
542
+ if (context.core.length > 0) return
543
+
544
+ const header = b4a.from('0a086879706572626565', 'hex')
545
+ await context.core.append(header)
546
+ }
547
+
548
+ function updateValuePointerContext(valuePointer, context) {
549
+ valuePointer.core = context.getCoreOffsetLocal(valuePointer.context, valuePointer.core)
550
+ valuePointer.context = context
551
+ }
552
+
553
+ // TODO: this isnt right anymore as we compress the delta post flush, but prob ok...
554
+ function getEstimatedNodeSize(n) {
555
+ return (
556
+ n.keys.delta.length * ESTIMATED_POINTER_SIZE + n.children.delta.length * ESTIMATED_POINTER_SIZE
557
+ )
558
+ }
559
+
560
+ function getEstimatedKeySize(k) {
561
+ return k.key.byteLength + (k.valuePointer ? ESTIMATED_POINTER_SIZE : 0)
562
+ }
563
+
564
+ function getEstimatedValueSize(k) {
565
+ return k.value.byteLength
566
+ }
567
+
568
+ function prepareCohorts(context, block, seq, deltas, keys) {
569
+ for (const d of deltas) {
570
+ // same below but the delta might be a noop (ie add and the delete) - handle that
571
+ if (keys && d.changed && d.pointer && d.pointer.changed) d.pointer = null
572
+
573
+ if (d.pointer && d.pointer.context !== context) {
574
+ const p = d.pointer
575
+ p.core = context.getCoreOffsetLocal(p.context, p.core)
576
+ p.context = context
577
+ }
578
+
579
+ if (d.type !== OP_COHORT || !d.changed) continue
580
+ if (block.cohorts === null) block.cohorts = []
581
+
582
+ d.changed = false
583
+ d.pointer = new Pointer(context, 0, seq, block.cohorts.length)
584
+
585
+ for (const dd of d.deltas) {
586
+ if (keys && dd.changed && dd.pointer && dd.pointer.changed) dd.pointer = null
587
+ dd.changed = false
588
+ const p = dd.pointer
589
+ if (!p) continue
590
+ p.core = context.getCoreOffsetLocal(p.context, p.core)
591
+ p.context = context
592
+ }
593
+
594
+ block.cohorts.push(d.deltas)
595
+ }
596
+ }
597
+
598
+ function supportsCompression(type) {
599
+ return type !== TYPE_COMPAT && type !== 0
459
600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperbee2",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "btree",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -10,7 +10,8 @@
10
10
  ],
11
11
  "scripts": {
12
12
  "format": "prettier . --write",
13
- "test": "prettier . --check && node test/all.js",
13
+ "lint": "lunte && prettier . --check",
14
+ "test": "node test/all.js",
14
15
  "test:bare": "bare test/all.js",
15
16
  "test:generate": "brittle -r test/all.js test/*.js"
16
17
  },
@@ -26,6 +27,7 @@
26
27
  "devDependencies": {
27
28
  "brittle": "^3.18.0",
28
29
  "corestore": "^7.4.5",
30
+ "lunte": "^1.4.0",
29
31
  "prettier": "^3.6.2",
30
32
  "prettier-config-holepunch": "^2.0.0"
31
33
  },