hyperbee2 1.1.2 → 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,33 +1,58 @@
1
1
  const b4a = require('b4a')
2
2
  const c = require('compact-encoding')
3
- const { Block } = require('./encoding.js')
3
+ const { OP_COHORT } = require('./compression.js')
4
+ const { encodeBlock, TYPE_COMPAT, TYPE_LATEST } = require('./encoding.js')
4
5
  const {
6
+ Pointer,
7
+ KeyPointer,
8
+ ValuePointer,
5
9
  TreeNode,
6
10
  TreeNodePointer,
7
11
  MIN_KEYS,
8
- UNCHANGED,
9
- CHANGED,
12
+ INSERTED,
10
13
  NEEDS_SPLIT
11
14
  } = require('./tree.js')
12
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
+
13
20
  module.exports = class WriteBatch {
14
- constructor(tree, { length = -1, key = null, autoUpdate = true } = {}) {
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
+
15
34
  this.tree = tree
35
+ this.deltaMax = deltaMax
36
+ this.deltaMin = deltaMin
37
+ this.inlineValueSize = inlineValueSize
38
+ this.preferredBlockSize = preferredBlockSize
16
39
  this.snapshot = tree.snapshot()
17
40
  this.autoUpdate = autoUpdate
18
41
  this.length = length
19
42
  this.key = key
43
+ this.type = type
20
44
  this.closed = false
45
+ this.applied = 0
21
46
  this.root = null
22
47
  this.ops = []
23
48
  }
24
49
 
25
50
  tryPut(key, value) {
26
- this.ops.push({ put: true, key, value })
51
+ this.ops.push({ put: true, applied: false, key, value })
27
52
  }
28
53
 
29
54
  tryDelete(key) {
30
- this.ops.push({ put: false, key, value: null })
55
+ this.ops.push({ put: false, applied: false, key, value: null })
31
56
  }
32
57
 
33
58
  tryClear() {
@@ -51,7 +76,6 @@ module.exports = class WriteBatch {
51
76
 
52
77
  try {
53
78
  const ops = this.ops
54
- this.ops = []
55
79
 
56
80
  const root = await this.tree.bootstrap()
57
81
 
@@ -60,20 +84,15 @@ module.exports = class WriteBatch {
60
84
 
61
85
  const changed = length === 0
62
86
  const seq = length === 0 ? 0 : length - 1
87
+ const value = changed ? new TreeNode([], []) : null
63
88
 
64
89
  this.length = length
65
- this.root = new TreeNodePointer(
66
- context,
67
- 0,
68
- seq,
69
- 0,
70
- changed,
71
- changed ? new TreeNode([], []) : null
72
- )
90
+ this.root = new TreeNodePointer(context, 0, seq, 0, changed, value)
73
91
 
74
92
  for (const op of ops) {
75
- if (op.put) await this._put(op.key, op.value)
76
- else await this._delete(op.key)
93
+ if (op.put) op.applied = await this._put(op.key, op.value)
94
+ else op.applied = await this._delete(op.key)
95
+ if (op.applied) this.applied++
77
96
  }
78
97
 
79
98
  await this._flush()
@@ -110,15 +129,16 @@ module.exports = class WriteBatch {
110
129
 
111
130
  while (s < e) {
112
131
  const mid = (s + e) >> 1
113
- const m = v.keys[mid]
132
+ const m = v.keys.get(mid)
114
133
 
115
134
  c = b4a.compare(target, m.key)
116
135
 
117
136
  if (c === 0) {
118
- if (b4a.equals(m.value, value)) return
137
+ const existing = await this.snapshot.inflateValue(m)
138
+ if (b4a.equals(existing, value)) return false
119
139
  v.setValue(this.tree.context, mid, value)
120
140
  for (let i = 0; i < stack.length; i++) stack[i].changed = true
121
- return
141
+ return true
122
142
  }
123
143
 
124
144
  if (c < 0) e = mid
@@ -126,16 +146,21 @@ module.exports = class WriteBatch {
126
146
  }
127
147
 
128
148
  const i = c < 0 ? e : s
129
- ptr = v.children[i]
149
+ ptr = v.children.get(i)
130
150
  }
131
151
 
132
152
  const v = ptr.value ? this.snapshot.bump(ptr) : await this.snapshot.inflate(ptr)
133
- let status = v.put(this.tree.context, target, value, null)
134
-
135
- if (status === UNCHANGED) return
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
+ }
136
162
 
137
163
  ptr.changed = true
138
-
139
164
  for (let i = 0; i < stack.length; i++) stack[i].changed = true
140
165
 
141
166
  while (status === NEEDS_SPLIT) {
@@ -145,16 +170,19 @@ module.exports = class WriteBatch {
145
170
 
146
171
  if (parent) {
147
172
  const p = parent.value ? this.snapshot.bump(parent) : await this.snapshot.inflate(parent)
148
- status = p.put(this.tree.context, median.key, median.value, right)
173
+ status = p.insertNode(this.tree.context, median, right)
149
174
  ptr = parent
150
175
  } else {
151
176
  this.root = new TreeNodePointer(this.tree.context, 0, 0, 0, true, new TreeNode([], []))
152
177
  this.root.value.keys.push(median)
153
- this.root.value.children.push(ptr, right)
178
+ this.root.value.children.push(ptr)
179
+ this.root.value.children.push(right)
154
180
  this.snapshot.bump(this.root)
155
- status = UNCHANGED
181
+ status = INSERTED
156
182
  }
157
183
  }
184
+
185
+ return true
158
186
  }
159
187
 
160
188
  async _delete(key) {
@@ -172,32 +200,33 @@ module.exports = class WriteBatch {
172
200
 
173
201
  while (s < e) {
174
202
  const mid = (s + e) >> 1
175
- c = b4a.compare(key, v.keys[mid].key)
203
+ c = b4a.compare(key, v.keys.get(mid).key)
176
204
 
177
205
  if (c === 0) {
178
206
  if (v.children.length) await this._setKeyToNearestLeaf(v, mid, stack)
179
207
  else v.removeKey(mid)
180
-
181
208
  // we mark these as changed late, so we don't rewrite them if it is a 404
182
209
  for (let i = 0; i < stack.length; i++) stack[i].changed = true
183
210
  this.root = await this._rebalance(stack)
184
- return
211
+ return true
185
212
  }
186
213
 
187
214
  if (c < 0) e = mid
188
215
  else s = mid + 1
189
216
  }
190
217
 
191
- if (!v.children.length) return
218
+ if (!v.children.length) return false
192
219
 
193
220
  const i = c < 0 ? e : s
194
- ptr = v.children[i]
221
+ ptr = v.children.get(i)
195
222
  }
223
+
224
+ return false
196
225
  }
197
226
 
198
227
  async _setKeyToNearestLeaf(v, index, stack) {
199
- let left = v.children[index]
200
- let right = v.children[index + 1]
228
+ let left = v.children.get(index)
229
+ let right = v.children.get(index + 1)
201
230
 
202
231
  const [ls, rs] = await Promise.all([this._leafSize(left, false), this._leafSize(right, true)])
203
232
 
@@ -206,28 +235,28 @@ module.exports = class WriteBatch {
206
235
  stack.push(right)
207
236
  let r = right.value ? this.snapshot.bump(right) : await this.snapshot.inflate(right)
208
237
  while (r.children.length) {
209
- right = r.children[0]
238
+ right = r.children.get(0)
210
239
  stack.push(right)
211
240
  r = right.value ? this.snapshot.bump(right) : await this.snapshot.inflate(right)
212
241
  }
213
- v.keys[index] = r.keys.shift()
242
+ v.keys.set(index, r.keys.shift())
214
243
  } else {
215
244
  // if fewer leaves on the right
216
245
  stack.push(left)
217
246
  let l = left.value ? this.snapshot.bump(left) : await this.snapshot.inflate(left)
218
247
  while (l.children.length) {
219
- left = l.children[l.children.length - 1]
248
+ left = l.children.get(l.children.length - 1)
220
249
  stack.push(left)
221
250
  l = left.value ? this.snapshot.bump(left) : await this.snapshot.inflate(left)
222
251
  }
223
- v.keys[index] = l.keys.pop()
252
+ v.keys.set(index, l.keys.pop())
224
253
  }
225
254
  }
226
255
 
227
256
  async _leafSize(ptr, goLeft) {
228
257
  let v = ptr.value ? this.snapshot.bump(ptr) : await this.snapshot.inflate(ptr)
229
258
  while (v.children.length) {
230
- ptr = v.children[goLeft ? 0 : v.children.length - 1]
259
+ ptr = v.children.get(goLeft ? 0 : v.children.length - 1)
231
260
  v = ptr.value ? this.snapshot.bump(ptr) : await this.snapshot.inflate(ptr)
232
261
  }
233
262
  return v.keys.length
@@ -235,6 +264,7 @@ module.exports = class WriteBatch {
235
264
 
236
265
  async _rebalance(stack) {
237
266
  const root = stack[0]
267
+ const minKeys = this.tree.context.minKeys
238
268
 
239
269
  while (stack.length > 1) {
240
270
  const ptr = stack.pop()
@@ -251,11 +281,11 @@ module.exports = class WriteBatch {
251
281
  let l = left && (left.value ? this.snapshot.bump(left) : await this.snapshot.inflate(left))
252
282
 
253
283
  // maybe borrow from left sibling?
254
- if (l && l.keys.length > MIN_KEYS) {
284
+ if (l && l.keys.length > minKeys) {
255
285
  left.changed = true
256
- v.keys.unshift(p.keys[index - 1])
286
+ v.keys.unshift(p.keys.get(index - 1))
257
287
  if (l.children.length) v.children.unshift(l.children.pop())
258
- p.keys[index - 1] = l.keys.pop()
288
+ p.keys.set(index - 1, l.keys.pop())
259
289
  return root
260
290
  }
261
291
 
@@ -263,11 +293,11 @@ module.exports = class WriteBatch {
263
293
  right && (right.value ? this.snapshot.bump(right) : await this.snapshot.inflate(right))
264
294
 
265
295
  // maybe borrow from right sibling?
266
- if (r && r.keys.length > MIN_KEYS) {
296
+ if (r && r.keys.length > minKeys) {
267
297
  right.changed = true
268
- v.keys.push(p.keys[index])
298
+ v.keys.push(p.keys.get(index))
269
299
  if (r.children.length) v.children.push(r.children.shift())
270
- p.keys[index] = r.keys.shift()
300
+ p.keys.set(index, r.keys.shift())
271
301
  return root
272
302
  }
273
303
 
@@ -282,7 +312,7 @@ module.exports = class WriteBatch {
282
312
  }
283
313
 
284
314
  left.changed = true
285
- l.merge(r, p.keys[index])
315
+ l.merge(r, p.keys.get(index))
286
316
 
287
317
  parent.changed = true
288
318
  p.removeKey(index)
@@ -290,70 +320,101 @@ module.exports = class WriteBatch {
290
320
 
291
321
  const r = root.value ? this.snapshot.bump(root) : await this.snapshot.inflate(root)
292
322
  // check if the tree shrunk
293
- if (!r.keys.length && r.children.length) return r.children[0]
323
+ if (!r.keys.length && r.children.length) return r.children.get(0)
294
324
  return root
295
325
  }
296
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
+
297
333
  async _flush() {
298
- 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
299
342
 
300
- const update = { node: [], keys: [] }
301
343
  const batch = [update]
302
- const stack = [{ update, node: this.root }]
344
+ const stack = [this.root]
345
+ const values = []
346
+
303
347
  const context = this.tree.context.getLocalContext()
348
+ const activeRequests = this.tree.activeRequests
304
349
 
305
- await context.update(this.tree.activeRequests)
350
+ await context.update(activeRequests)
306
351
 
307
352
  while (stack.length > 0) {
308
- const { update, node } = stack.pop()
353
+ const node = stack.pop()
309
354
 
310
- node.changed = false
311
- update.node.push(node)
355
+ if (this.type !== TYPE_COMPAT && update.size >= this.preferredBlockSize) {
356
+ update = { size: 0, nodes: [], keys: [], values: [] }
357
+ batch.push(update)
358
+ }
312
359
 
313
- for (let i = 0; i < node.value.keys.length; i++) {
314
- const k = node.value.keys[i]
360
+ update.nodes.push(node)
361
+ update.size += getEstimatedNodeSize(node.value)
315
362
 
316
- if (!k.changed) {
317
- k.core = await context.getCoreOffset(k.context, k.core, this.tree.activeRequests)
318
- k.context = context
319
- continue
363
+ const keys = node.value.keys
364
+ const children = node.value.children
365
+
366
+ for (let i = 0; i < keys.entries.length; i++) {
367
+ const k = keys.entries[i]
368
+ if (!k.changed) continue
369
+
370
+ if (!this._shouldInlineValue(k)) {
371
+ values.push(k)
372
+ k.valuePointer = new ValuePointer(context, 0, 0, 0, 0)
373
+
374
+ if (minValue === -1 || minValue < k.value.byteLength) {
375
+ minValue = k.value.byteLength
376
+ }
320
377
  }
321
378
 
322
- k.changed = false
323
379
  update.keys.push(k)
380
+ update.size += getEstimatedKeySize(k)
324
381
  }
325
382
 
326
- let first = true
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)
388
+ }
389
+ }
327
390
 
328
- for (let i = 0; i < node.value.children.length; i++) {
329
- const n = node.value.children[i]
391
+ if (this.type === TYPE_COMPAT) await toCompatType(context, batch, this.ops)
330
392
 
331
- if (!n.changed) {
332
- n.core = await context.getCoreOffset(n.context, n.core, this.tree.activeRequests)
333
- n.context = context
334
- continue
335
- }
393
+ const length = context.core.length
394
+
395
+ // if noop and not genesis, bail early
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
400
+ }
401
+
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]
336
407
 
337
- if (first) {
338
- stack.push({ update, node: n })
339
- first = false
340
- } else {
341
- const update = { node: [], keys: [] }
408
+ update.size += getEstimatedValueSize(k)
409
+ update.values.push(k)
410
+
411
+ if (i === values.length - 1 || update.size >= this.preferredBlockSize) {
342
412
  batch.push(update)
343
- stack.push({ update, node: n })
413
+ update = { size: 0, nodes: [], keys: [], values: [] }
344
414
  }
345
415
  }
346
416
  }
347
417
 
348
- // if only the root was marked dirty and is === current bootstrap the batch is a noop - skip
349
- if (update.node.length === 1 && update.keys.length === 0) {
350
- const b = await this.tree.bootstrap()
351
- const n = update.node[0]
352
- if (b && b.context === n.context && b.core === n.core && b.seq === n.seq) return
353
- if (!b && n.value.isEmpty()) return
354
- }
355
-
356
- const length = context.core.length
357
418
  const blocks = new Array(batch.length)
358
419
 
359
420
  for (let i = 0; i < batch.length; i++) {
@@ -361,33 +422,63 @@ module.exports = class WriteBatch {
361
422
  const seq = length + batch.length - i - 1
362
423
 
363
424
  const block = {
364
- type: 0,
425
+ type: this.type,
365
426
  checkpoint: 0,
366
427
  batch: { start: batch.length - 1 - i, end: i },
367
428
  previous: null,
429
+ metadata: null,
368
430
  tree: null,
369
- data: null,
370
- 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
371
448
  }
372
449
 
373
450
  for (const k of update.keys) {
374
- if (block.data === null) block.data = []
451
+ if (block.keys === null) block.keys = []
375
452
 
376
453
  k.core = 0
377
454
  k.context = context
378
455
  k.seq = seq
379
- k.offset = block.data.length
380
- 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)
381
462
  }
382
463
 
383
- for (const n of update.node) {
464
+ for (const n of update.nodes) {
384
465
  if (block.tree === null) block.tree = []
385
466
 
386
467
  n.core = 0
387
468
  n.context = context
388
469
  n.seq = seq
389
470
  n.offset = block.tree.length
390
- 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)
391
482
  }
392
483
 
393
484
  blocks[seq - length] = block
@@ -396,22 +487,19 @@ module.exports = class WriteBatch {
396
487
  const buffers = new Array(blocks.length)
397
488
 
398
489
  if (blocks.length > 0 && this.length > 0) {
399
- const core = this.key
400
- ? await context.getCoreOffsetByKey(this.key, this.tree.activeRequests)
401
- : 0
490
+ const core = this.key ? await context.getCoreOffsetByKey(this.key, activeRequests) : 0
402
491
  blocks[blocks.length - 1].previous = { core, seq: this.length - 1 }
403
492
  }
404
493
 
405
494
  // TODO: make this transaction safe
406
495
  if (context.changed) {
407
- context.changed = false
408
496
  context.checkpoint = context.core.length + blocks.length
409
- blocks[blocks.length - 1].cores = context.cores
497
+ blocks[blocks.length - 1].metadata = context.flush()
410
498
  }
411
499
 
412
500
  for (let i = 0; i < blocks.length; i++) {
413
501
  blocks[i].checkpoint = context.checkpoint
414
- buffers[i] = c.encode(Block, blocks[i])
502
+ buffers[i] = encodeBlock(blocks[i])
415
503
  }
416
504
 
417
505
  if (this.closed) {
@@ -423,9 +511,90 @@ module.exports = class WriteBatch {
423
511
  for (let i = 0; i < batch.length; i++) {
424
512
  const update = batch[i]
425
513
 
426
- for (let j = 0; j < update.node.length; j++) {
427
- 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)
428
517
  }
429
518
  }
430
519
  }
431
520
  }
521
+
522
+ async function toCompatType(context, batch, ops) {
523
+ const map = new Map()
524
+ let index = 0
525
+
526
+ for (const k of batch[0].keys) {
527
+ map.set(b4a.toString(k.key, 'hex'), k)
528
+ }
529
+
530
+ for (let i = ops.length - 1; i >= 0; i--) {
531
+ const op = ops[i]
532
+ if (!op.put && !op.applied) continue
533
+
534
+ const k = map.get(b4a.toString(op.key, 'hex'))
535
+
536
+ const j = index++
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)]
539
+ }
540
+
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
600
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperbee2",
3
- "version": "1.1.2",
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
  },