xitdb 0.1.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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE +21 -0
  4. data/README.md +131 -0
  5. data/docs/benchmarks.md +135 -0
  6. data/docs/core.md +123 -0
  7. data/docs/portable.md +99 -0
  8. data/docs/releasing.md +37 -0
  9. data/docs/ruby-profile-v1.md +226 -0
  10. data/docs/ruby.md +61 -0
  11. data/examples/indexed_users.rb +23 -0
  12. data/examples/portable.rb +62 -0
  13. data/examples/ruby.rb +56 -0
  14. data/lib/xitdb/context.rb +360 -0
  15. data/lib/xitdb/conversion.rb +222 -0
  16. data/lib/xitdb/core/array_list.rb +282 -0
  17. data/lib/xitdb/core/b_tree.rb +546 -0
  18. data/lib/xitdb/core/bytes.rb +181 -0
  19. data/lib/xitdb/core/compaction.rb +172 -0
  20. data/lib/xitdb/core/crc64.rb +22 -0
  21. data/lib/xitdb/core/cursor.rb +201 -0
  22. data/lib/xitdb/core/database.rb +359 -0
  23. data/lib/xitdb/core/file_store.rb +194 -0
  24. data/lib/xitdb/core/format.rb +148 -0
  25. data/lib/xitdb/core/hash_map.rb +371 -0
  26. data/lib/xitdb/core/hasher.rb +49 -0
  27. data/lib/xitdb/core/linked_array_list.rb +85 -0
  28. data/lib/xitdb/core/memory_store.rb +277 -0
  29. data/lib/xitdb/core/offset_map.rb +85 -0
  30. data/lib/xitdb/core/replication.rb +242 -0
  31. data/lib/xitdb/core/sorted_map.rb +361 -0
  32. data/lib/xitdb/core/transaction.rb +159 -0
  33. data/lib/xitdb/core.rb +17 -0
  34. data/lib/xitdb/database.rb +332 -0
  35. data/lib/xitdb/declarations.rb +73 -0
  36. data/lib/xitdb/errors.rb +84 -0
  37. data/lib/xitdb/history.rb +58 -0
  38. data/lib/xitdb/input_guard.rb +39 -0
  39. data/lib/xitdb/profiles/portable.rb +407 -0
  40. data/lib/xitdb/profiles/ruby.rb +173 -0
  41. data/lib/xitdb/revision.rb +42 -0
  42. data/lib/xitdb/ruby/codec.rb +123 -0
  43. data/lib/xitdb/ruby/containers.rb +495 -0
  44. data/lib/xitdb/ruby/key_encoding.rb +60 -0
  45. data/lib/xitdb/ruby/ordered_keys.rb +87 -0
  46. data/lib/xitdb/ruby/scalars.rb +296 -0
  47. data/lib/xitdb/ruby/wire.rb +146 -0
  48. data/lib/xitdb/ruby.rb +46 -0
  49. data/lib/xitdb/snapshot.rb +13 -0
  50. data/lib/xitdb/streams.rb +148 -0
  51. data/lib/xitdb/value_walker.rb +192 -0
  52. data/lib/xitdb/version.rb +5 -0
  53. data/lib/xitdb/views/array.rb +303 -0
  54. data/lib/xitdb/views/base.rb +271 -0
  55. data/lib/xitdb/views/linked_array.rb +18 -0
  56. data/lib/xitdb/views/map.rb +162 -0
  57. data/lib/xitdb/views/set.rb +41 -0
  58. data/lib/xitdb/views/sorted.rb +77 -0
  59. data/lib/xitdb.rb +20 -0
  60. data/sig/base.rbs +76 -0
  61. data/sig/core.rbs +697 -0
  62. data/sig/manifest.yaml +10 -0
  63. data/sig/native.rbs +118 -0
  64. data/sig/public.rbs +427 -0
  65. data/sig/ruby.rbs +337 -0
  66. metadata +137 -0
@@ -0,0 +1,546 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XitDB
4
+ module Core
5
+ # A counted B-tree for indexed collections.
6
+ class BTree
7
+ HEADER_SIZE = 16
8
+ CAPACITY = 16
9
+ SPLIT_COUNT = 8
10
+ LEAF_SIZE = 146
11
+ BRANCH_SIZE = 274
12
+ # A mutable node used while copying a B-tree path.
13
+ class Node
14
+ attr_accessor :kind, :slots, :counts
15
+
16
+ def initialize(kind, slots, counts)
17
+ @kind = kind
18
+ @slots = slots
19
+ @counts = counts
20
+ end
21
+
22
+ def size
23
+ kind.zero? ? slots.size : counts.sum
24
+ end
25
+ end
26
+ private_constant :Node
27
+
28
+ attr_reader :cursor
29
+
30
+ def initialize(cursor, tag: :linked_array_list)
31
+ @cursor = cursor
32
+ @tag = tag
33
+ unless [:none, @tag].include?(cursor.slot.tag)
34
+ raise InvalidValueError, "slot does not contain a linked array list"
35
+ end
36
+
37
+ return unless cursor.slot.tag == :none && cursor.transaction
38
+
39
+ mutate do |transaction|
40
+ pointer = write_node(transaction, Node.new(0, [], []))
41
+ publish(transaction, 0, pointer)
42
+ end
43
+ end
44
+
45
+ def size
46
+ header.first
47
+ end
48
+
49
+ def at(index)
50
+ count, pointer = header
51
+ rank = resolve_index(index, count)
52
+ return nil unless rank
53
+
54
+ _path, node, rank = descend(pointer, rank, count)
55
+ node.slots.fetch(rank)
56
+ end
57
+
58
+ def insert(index, slot)
59
+ validate_slot(slot)
60
+ count, pointer = header
61
+ integer_index(index)
62
+ raise IndexError, "array index outside list" unless index.between?(0, count)
63
+ raise RangeError, "array size exceeds unsigned 64-bit range" if count == Format::UINT64_MAX
64
+
65
+ mutate do |transaction|
66
+ path, node, rank = descend(pointer, index, count, insertion: true)
67
+ node.slots.insert(rank, slot)
68
+ result = write_split(transaction, node, pointer_for(path, pointer))
69
+ path.reverse_each do |parent_pointer, parent, child_index|
70
+ parent.slots[child_index, 1] = result.map { |ptr, _| Slot.new(tag: :index, value: ptr) }
71
+ parent.counts[child_index, 1] = result.map(&:last)
72
+ result = write_split(transaction, parent, parent_pointer)
73
+ end
74
+ root_pointer = if result.size == 1
75
+ result.first.first
76
+ else
77
+ write_node(transaction, Node.new(1, result.map do |ptr, _|
78
+ Slot.new(tag: :index, value: ptr)
79
+ end, result.map(&:last)))
80
+ end
81
+ publish(transaction, count + 1, root_pointer)
82
+ cursor.invalidate_children(from: index)
83
+ end
84
+ self
85
+ end
86
+
87
+ def replace(index, slot, owner: nil, collection: false, propagate: true)
88
+ validate_slot(slot)
89
+ count, pointer = header
90
+ rank = resolve_index(index, count)
91
+ raise IndexError, "array index outside list" unless rank
92
+
93
+ mutate do |transaction|
94
+ path, node, rank = descend(pointer, rank, count)
95
+ node.slots[rank] = slot
96
+ child_pointer = write_node(transaction, node, pointer_for(path, pointer))
97
+ path.reverse_each do |parent_pointer, parent, child_index|
98
+ parent.slots[child_index] = Slot.new(tag: :index, value: child_pointer)
99
+ child_pointer = write_node(transaction, parent, parent_pointer)
100
+ end
101
+ publish(transaction, count, child_pointer, propagate: propagate)
102
+ cursor.invalidate_children(index: resolve_index(index, count), except: owner) unless collection
103
+ end
104
+ self
105
+ end
106
+
107
+ def delete_at(index)
108
+ count, pointer = header
109
+ rank = resolve_index(index, count)
110
+ return nil unless rank
111
+
112
+ mutate do |transaction|
113
+ path, node, offset = descend(pointer, rank, count)
114
+ removed = node.slots.delete_at(offset)
115
+ old_pointer = pointer_for(path, pointer)
116
+ path.reverse_each do |parent_pointer, parent, child_index|
117
+ rebalance_child(transaction, node, parent, child_index, old_pointer)
118
+ node = parent
119
+ old_pointer = parent_pointer
120
+ end
121
+ root_pointer = collapse_root(transaction, node, old_pointer, count - 1)
122
+ publish(transaction, count - 1, root_pointer)
123
+ cursor.invalidate_children(from: rank)
124
+ removed
125
+ end
126
+ end
127
+
128
+ def split(index)
129
+ count, pointer = header
130
+ integer_index(index)
131
+ index += count if index.negative?
132
+ raise IndexError, "array split outside list" unless index.between?(0, count)
133
+
134
+ mutate do |transaction|
135
+ split_root(transaction, [pointer, count, cursor.bound], index).map { |ref| detached(transaction, ref) }
136
+ end
137
+ end
138
+
139
+ def slice(start, length)
140
+ count, pointer = header
141
+ integer_index(start)
142
+ integer_index(length)
143
+ start += count if start.negative?
144
+ unless start.between?(0, count) && length >= 0 && length <= count - start
145
+ raise IndexError, "array slice outside list"
146
+ end
147
+
148
+ mutate do |transaction|
149
+ _left, tail = split_root(transaction, [pointer, count, cursor.bound], start)
150
+ selected, _right = split_root(transaction, tail, length)
151
+ detached(transaction, selected)
152
+ end
153
+ end
154
+
155
+ def concat(other)
156
+ raise TypeError, "expected BTree" unless other.is_a?(BTree)
157
+ unless cursor.database.equal?(other.cursor.database)
158
+ raise ForeignValueError, "cannot share linked arrays across databases"
159
+ end
160
+
161
+ count, pointer = header
162
+ other_count, other_pointer = other.send(:header)
163
+ raise RangeError, "array size exceeds unsigned 64-bit range" if count + other_count > Format::UINT64_MAX
164
+
165
+ mutate do |transaction|
166
+ ref = join_roots(transaction, [pointer, count, cursor.bound],
167
+ [other_pointer, other_count, other.cursor.bound])
168
+ detached(transaction, ref)
169
+ end
170
+ end
171
+
172
+ def each_from_index(index, &block)
173
+ return enum_for(:each_from_index, index) unless block_given?
174
+
175
+ count, pointer = header
176
+ integer_index(index)
177
+ index += count if index.negative?
178
+ return self unless index >= 0 && index < count
179
+
180
+ path, node, rank = descend(pointer, index, count)
181
+ loop do
182
+ node.slots.drop(rank).each(&block)
183
+ # @type var frame: [Integer, node, Integer]?
184
+ frame = next_frame(path)
185
+ break unless frame
186
+
187
+ parent_pointer, parent, child_index = frame
188
+ child_index += 1
189
+ path << [parent_pointer, parent, child_index]
190
+ subpath, node, rank = descend(parent.slots[child_index].value, 0, parent.counts[child_index],
191
+ ancestors: path.map(&:first))
192
+ path.concat(subpath)
193
+ end
194
+ self
195
+ end
196
+
197
+ private
198
+
199
+ def next_frame(path)
200
+ until path.empty?
201
+ candidate = path.pop || raise(CorruptionError, "missing B-tree path")
202
+ return candidate if candidate[2] + 1 < candidate[1].slots.size
203
+ end
204
+ nil
205
+ end
206
+
207
+ def rebalance_child(transaction, node, parent, index, old_pointer)
208
+ if node.slots.empty?
209
+ parent.slots.delete_at(index)
210
+ parent.counts.delete_at(index)
211
+ else
212
+ node, index = merge_neighbor(node, parent, index) if node.slots.size < SPLIT_COUNT && parent.slots.size > 1
213
+ parent.slots[index] = Slot.new(tag: :index, value: write_node(transaction, node, old_pointer))
214
+ parent.counts[index] = node.size
215
+ end
216
+ end
217
+
218
+ def merge_neighbor(node, parent, index)
219
+ first = index + 1 < parent.slots.size ? index : index - 1
220
+ neighbor_index = first == index ? first + 1 : first
221
+ neighbor = read_node(parent.slots[neighbor_index].value, parent.counts[neighbor_index])
222
+ raise CorruptionError, "unequal B-tree child heights" unless neighbor.kind == node.kind
223
+ return [node, index] if node.slots.size + neighbor.slots.size > CAPACITY
224
+
225
+ node = first == index ? merge_nodes(node, neighbor) : merge_nodes(neighbor, node)
226
+ parent.slots.delete_at(first + 1)
227
+ parent.counts.delete_at(first + 1)
228
+ [node, first]
229
+ end
230
+
231
+ def collapse_root(transaction, node, old_pointer, count)
232
+ unless node.kind == 1 && node.slots.size == 1
233
+ node = Node.new(0, [], []) if node.slots.empty?
234
+ return write_node(transaction, node, old_pointer)
235
+ end
236
+
237
+ pointer = node.slots.first.value
238
+ # @type var seen: Hash[Integer, bool]
239
+ seen = {}
240
+ loop do
241
+ raise CorruptionError, "cyclic B-tree branch" if seen[pointer]
242
+
243
+ seen[pointer] = true
244
+ child = read_node(pointer, count)
245
+ return pointer unless child.kind == 1 && child.slots.size == 1
246
+
247
+ pointer = child.slots.first.value
248
+ end
249
+ end
250
+
251
+ def integer_index(index)
252
+ raise TypeError, "array index must be an Integer" unless index.is_a?(Integer)
253
+ end
254
+
255
+ def resolve_index(index, count)
256
+ integer_index(index)
257
+ index += count if index.negative?
258
+ index if index >= 0 && index < count
259
+ end
260
+
261
+ def read(offset, length, bound: cursor.bound)
262
+ cursor.database.read_at(offset, length, bound: bound)
263
+ end
264
+
265
+ def header
266
+ slot = cursor.slot
267
+ return [0, nil] if slot.tag == :none
268
+ raise InvalidValueError, "slot does not contain the expected B-tree type" unless slot.tag == @tag
269
+
270
+ Format.pair(read(slot.value, HEADER_SIZE), "Q>Q>")
271
+ end
272
+
273
+ def read_node(pointer, expected, bound: cursor.bound)
274
+ kind, num = read_node_header(pointer, bound)
275
+ bytes = read(pointer, kind.zero? ? LEAF_SIZE : BRANCH_SIZE, bound: bound)
276
+ slots = node_slots(bytes, 2, CAPACITY).first(num)
277
+ # @type var counts: Array[Integer]
278
+ counts = kind.zero? ? [] : Format.integers(Format.slice(bytes, LEAF_SIZE, 128), "Q>*").first(num)
279
+ node = Node.new(kind, slots, counts)
280
+ validate_node(node, expected)
281
+ node
282
+ end
283
+
284
+ def read_node_header(pointer, bound)
285
+ kind, num = Format.pair(read(pointer, 2, bound: bound), "CC")
286
+ unless [0, 1].include?(kind) && num <= CAPACITY && (kind.zero? || num.positive?)
287
+ raise CorruptionError, "invalid B-tree node header"
288
+ end
289
+
290
+ [kind, num]
291
+ end
292
+
293
+ def node_slots(bytes, start, count)
294
+ Array.new(count) do |index|
295
+ slot = Slot.parse(Format.slice(bytes, start + (index * Slot::SIZE), Slot::SIZE))
296
+ raise CorruptionError, "unknown B-tree slot tag" unless Slot::TAGS.include?(slot.tag)
297
+
298
+ slot
299
+ end
300
+ end
301
+
302
+ def validate_node(node, expected)
303
+ case node.kind
304
+ when 1 then validate_branch(node)
305
+ when 0 then validate_leaf(node)
306
+ end
307
+
308
+ raise CorruptionError, "invalid B-tree subtree count" unless node.size == expected
309
+ end
310
+
311
+ def validate_branch(node)
312
+ return unless node.slots.any? { |slot| slot.tag != :index } || node.counts.any?(&:zero?)
313
+
314
+ raise CorruptionError, "invalid B-tree branch"
315
+ end
316
+
317
+ def validate_leaf(node)
318
+ raise CorruptionError, "index slot in B-tree leaf" if node.slots.any? { |slot| slot.tag == :index }
319
+ end
320
+
321
+ def descend(pointer, rank, expected, insertion: false, ancestors: [])
322
+ return [[], Node.new(0, [], []), 0] unless pointer
323
+
324
+ # @type var path: path
325
+ path = []
326
+ seen = ancestors.to_h { |ptr| [ptr, true] }
327
+ loop do
328
+ raise CorruptionError, "cyclic B-tree branch" if seen[pointer]
329
+
330
+ seen[pointer] = true
331
+ node = read_node(pointer, expected)
332
+ return [path, node, rank] if node.kind.zero?
333
+
334
+ i = 0
335
+ while i + 1 < node.slots.size && (insertion ? rank > node.counts[i] : rank >= node.counts[i])
336
+ rank -= node.counts[i]
337
+ i += 1
338
+ end
339
+ path << [pointer, node, i]
340
+ pointer = node.slots[i].value
341
+ expected = node.counts[i]
342
+ end
343
+ end
344
+
345
+ def pointer_for(path, root_pointer)
346
+ path.empty? ? root_pointer : path.last[1].slots[path.last[2]].value
347
+ end
348
+
349
+ def validate_slot(slot)
350
+ raise TypeError, "expected Slot" unless slot.is_a?(Slot)
351
+ raise CorruptionError, "unknown slot tag" unless Slot::TAGS.include?(slot.tag)
352
+ raise InvalidValueError, "index slot is internal to B-tree branches" if slot.tag == :index
353
+ end
354
+
355
+ def mutate
356
+ cursor.slot
357
+ transaction = cursor.transaction
358
+ raise ReadOnlyError, "linked array list is read-only" unless transaction
359
+
360
+ checkpoint = transaction.checkpoint
361
+ begin
362
+ result = yield transaction
363
+ rescue Exception # rubocop:disable Lint/RescueException -- Cleanup must also preserve nonstandard exceptions.
364
+ transaction.restore(checkpoint)
365
+ raise
366
+ else
367
+ transaction.release_checkpoint(checkpoint)
368
+ result
369
+ end
370
+ end
371
+
372
+ # References retain each source cursor's read boundary while nodes are shared.
373
+ def node_for(ref)
374
+ pointer = ref[0]
375
+ raise CorruptionError, "missing B-tree pointer" unless pointer
376
+
377
+ read_node(pointer, ref[1], bound: ref[2] || cursor.bound)
378
+ end
379
+
380
+ def child_ref(ref, node, index)
381
+ [node.slots[index].value, node.counts[index], ref[2]]
382
+ end
383
+
384
+ def merge_nodes(left, right)
385
+ raise CorruptionError, "unequal B-tree node kinds" unless left.kind == right.kind
386
+
387
+ Node.new(left.kind, left.slots + right.slots, left.counts + right.counts)
388
+ end
389
+
390
+ def assemble(transaction, node)
391
+ if node.slots.size <= CAPACITY
392
+ [[write_node(transaction, node), node.size, nil]]
393
+ else
394
+ middle = node.slots.size / 2
395
+ [Node.new(node.kind, node.slots.first(middle), node.counts.first(middle)),
396
+ Node.new(node.kind, node.slots.drop(middle), node.counts.drop(middle))].map do |half|
397
+ [write_node(transaction, half), half.size, nil]
398
+ end
399
+ end
400
+ end
401
+
402
+ def branch(refs)
403
+ slots = refs.map do |ref|
404
+ pointer = ref[0]
405
+ raise CorruptionError, "missing B-tree pointer" unless pointer
406
+
407
+ Slot.new(tag: :index, value: pointer)
408
+ end
409
+ Node.new(1, slots, refs.map { |ref| ref[1] })
410
+ end
411
+
412
+ def height(ref)
413
+ levels = 0
414
+ # @type var seen: Hash[Integer?, bool]
415
+ seen = {}
416
+ loop do
417
+ raise CorruptionError, "cyclic B-tree branch" if seen[ref[0]]
418
+
419
+ seen[ref[0]] = true
420
+ node = node_for(ref)
421
+ return levels if node.kind.zero?
422
+
423
+ ref = child_ref(ref, node, 0)
424
+ levels += 1
425
+ end
426
+ end
427
+
428
+ def join_roots(transaction, left, right)
429
+ return right if left[1].zero?
430
+ return left if right[1].zero?
431
+
432
+ left_height = height(left)
433
+ right_height = height(right)
434
+ # @type var node: node
435
+ # @type var path: Array[[ref, node, Integer]]
436
+ path = []
437
+ while left_height > right_height
438
+ node = node_for(left)
439
+ index = node.slots.size - 1
440
+ path << [left, node, index]
441
+ left = child_ref(left, node, index)
442
+ left_height -= 1
443
+ end
444
+ while right_height > left_height
445
+ node = node_for(right)
446
+ path << [right, node, 0]
447
+ right = child_ref(right, node, 0)
448
+ right_height -= 1
449
+ end
450
+ result = assemble(transaction, merge_nodes(node_for(left), node_for(right)))
451
+ path.reverse_each do |_ref, node, index|
452
+ node.slots[index, 1] = result.map do |ref|
453
+ pointer = ref[0]
454
+ raise CorruptionError, "missing B-tree pointer" unless pointer
455
+
456
+ Slot.new(tag: :index, value: pointer)
457
+ end
458
+ node.counts[index, 1] = result.map { |ref| ref[1] }
459
+ result = assemble(transaction, node)
460
+ end
461
+ result.size == 1 ? result.first : assemble(transaction, branch(result)).first
462
+ end
463
+
464
+ def sibling_root(transaction, ref, node, range)
465
+ indices = node.slots.each_index.to_a[range] || []
466
+ refs = indices.map { |index| child_ref(ref, node, index) }
467
+ return [nil, 0, nil] if refs.empty?
468
+ return refs.first if refs.size == 1
469
+
470
+ assemble(transaction, branch(refs)).first
471
+ end
472
+
473
+ def split_root(transaction, ref, rank)
474
+ return [[nil, 0, nil], ref] if rank.zero?
475
+ return [ref, [nil, 0, nil]] if rank == ref[1]
476
+
477
+ # @type var path: Array[[ref, node, Integer]]
478
+ path = []
479
+ # @type var seen: Hash[Integer?, bool]
480
+ seen = {}
481
+ node = node_for(ref)
482
+ loop do
483
+ raise CorruptionError, "cyclic B-tree branch" if seen[ref[0]]
484
+
485
+ seen[ref[0]] = true
486
+ node = node_for(ref)
487
+ break if node.kind.zero?
488
+
489
+ index = 0
490
+ while index + 1 < node.slots.size && rank > node.counts[index]
491
+ rank -= node.counts[index]
492
+ index += 1
493
+ end
494
+ path << [ref, node, index]
495
+ ref = child_ref(ref, node, index)
496
+ end
497
+ left = assemble(transaction, Node.new(0, node.slots.first(rank), [])).first
498
+ right = assemble(transaction, Node.new(0, node.slots.drop(rank), [])).first
499
+ path.reverse_each do |parent_ref, parent, index|
500
+ left = join_roots(transaction, sibling_root(transaction, parent_ref, parent, 0...index), left)
501
+ right = join_roots(transaction, right,
502
+ sibling_root(transaction, parent_ref, parent, (index + 1)...parent.slots.size))
503
+ end
504
+ [left, right]
505
+ end
506
+
507
+ def detached(transaction, ref)
508
+ pointer = ref[0] || write_node(transaction, Node.new(0, [], []))
509
+ position = transaction.append([ref[1], pointer].pack("Q>Q>"))
510
+ self.class.new(transaction.cursor(slot: Slot.new(tag: :linked_array_list, value: position)))
511
+ end
512
+
513
+ def write_node(transaction, node, old_pointer = nil)
514
+ bytes = [node.kind, node.slots.size].pack("CC")
515
+ bytes << node.slots.map(&:to_bytes).join << ("\0".b * ((CAPACITY - node.slots.size) * Slot::SIZE))
516
+ bytes << (node.counts + ([0] * (CAPACITY - node.counts.size))).pack("Q>*") unless node.kind.zero?
517
+ if old_pointer && old_pointer >= transaction.mutable_start
518
+ transaction.write_at(old_pointer, bytes)
519
+ old_pointer
520
+ else
521
+ transaction.append(bytes)
522
+ end
523
+ end
524
+
525
+ def write_split(transaction, node, old_pointer)
526
+ return [[write_node(transaction, node, old_pointer), node.size]] if node.slots.size <= CAPACITY
527
+
528
+ left = Node.new(node.kind, node.slots.first(SPLIT_COUNT), node.counts.first(SPLIT_COUNT))
529
+ right = Node.new(node.kind, node.slots.drop(SPLIT_COUNT), node.counts.drop(SPLIT_COUNT))
530
+ [[write_node(transaction, left, old_pointer), left.size], [write_node(transaction, right), right.size]]
531
+ end
532
+
533
+ def publish(transaction, count, pointer, propagate: true)
534
+ bytes = [count, pointer].pack("Q>Q>")
535
+ old = cursor.slot
536
+ position = if old.tag == @tag && old.value >= transaction.mutable_start
537
+ transaction.write_at(old.value, bytes)
538
+ old.value
539
+ else
540
+ transaction.append(bytes)
541
+ end
542
+ cursor.update_collection(Slot.new(tag: @tag, value: position), propagate: propagate)
543
+ end
544
+ end
545
+ end
546
+ end