@frostpillar/frostpillar-btree 0.2.6 → 0.2.7

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { InMemoryBTree } from './InMemoryBTree.js';
2
- export type { BTreeEntry, BTreeJSON, BTreeStats, DuplicateKeyPolicy, EntryId, InMemoryBTreeConfig, RangeBounds, } from './InMemoryBTree.js';
2
+ export type { BTreeEntry, BTreeJSON, BTreeStats, DeleteRebalancePolicy, DuplicateKeyPolicy, EntryId, InMemoryBTreeConfig, RangeBounds, } from './InMemoryBTree.js';
3
3
  export { ConcurrentInMemoryBTree } from './concurrency/index.js';
4
4
  export type { BTreeMutation, ConcurrentInMemoryBTreeConfig, ReadMode, SharedTreeLog, SharedTreeStore, } from './concurrency/index.js';
5
5
  export { BTreeConcurrencyError, BTreeInvariantError, BTreeValidationError, } from './errors.js';
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  DEFAULT_MAX_LEAF_ENTRIES,
7
7
  InMemoryBTree,
8
8
  computeAutoScaleTier
9
- } from "./chunk-OWHENPGJ.js";
9
+ } from "./chunk-UGGWGP4E.js";
10
10
 
11
11
  // src/concurrency/helpers.ts
12
12
  var DEFAULT_MAX_RETRIES = 16;
@@ -33,13 +33,17 @@ var assertNeverMutation = (mutation) => {
33
33
  var validatePutManyEntries = (entries) => {
34
34
  for (const entry of entries) {
35
35
  if (typeof entry !== "object" || entry === null || !("key" in entry) || !("value" in entry)) {
36
- throw new BTreeConcurrencyError("Malformed putMany mutation: each entry must have key and value.");
36
+ throw new BTreeConcurrencyError(
37
+ "Malformed putMany mutation: each entry must have key and value."
38
+ );
37
39
  }
38
40
  }
39
41
  };
40
42
  var validateInitMutation = (m, expectedConfigFingerprint) => {
41
43
  if (typeof m.configFingerprint !== "string") {
42
- throw new BTreeConcurrencyError("Malformed init mutation: missing configFingerprint.");
44
+ throw new BTreeConcurrencyError(
45
+ "Malformed init mutation: missing configFingerprint."
46
+ );
43
47
  }
44
48
  if (expectedConfigFingerprint !== void 0 && m.configFingerprint !== expectedConfigFingerprint) {
45
49
  throw new BTreeConcurrencyError(
@@ -47,45 +51,70 @@ var validateInitMutation = (m, expectedConfigFingerprint) => {
47
51
  );
48
52
  }
49
53
  };
50
- var validateSingleMutation = (m, expectedConfigFingerprint) => {
54
+ var validateMutationFields = (m) => {
51
55
  switch (m.type) {
52
- case "init":
53
- validateInitMutation(m, expectedConfigFingerprint);
54
- break;
55
56
  case "put":
56
57
  if (!("key" in m) || !("value" in m)) {
57
- throw new BTreeConcurrencyError("Malformed put mutation: missing key or value.");
58
+ throw new BTreeConcurrencyError(
59
+ "Malformed put mutation: missing key or value."
60
+ );
58
61
  }
59
62
  break;
60
63
  case "remove":
61
64
  if (!("key" in m)) {
62
- throw new BTreeConcurrencyError("Malformed remove mutation: missing key.");
65
+ throw new BTreeConcurrencyError(
66
+ "Malformed remove mutation: missing key."
67
+ );
63
68
  }
64
69
  break;
65
70
  case "removeById":
66
71
  if (!("entryId" in m)) {
67
- throw new BTreeConcurrencyError("Malformed removeById mutation: missing entryId.");
72
+ throw new BTreeConcurrencyError(
73
+ "Malformed removeById mutation: missing entryId."
74
+ );
68
75
  }
69
76
  break;
70
77
  case "updateById":
71
78
  if (!("entryId" in m) || !("value" in m)) {
72
- throw new BTreeConcurrencyError("Malformed updateById mutation: missing entryId or value.");
79
+ throw new BTreeConcurrencyError(
80
+ "Malformed updateById mutation: missing entryId or value."
81
+ );
73
82
  }
74
83
  break;
75
- case "popFirst":
76
- case "popLast":
77
- break;
78
84
  case "putMany":
79
85
  if (!("entries" in m) || !Array.isArray(m.entries)) {
80
- throw new BTreeConcurrencyError("Malformed putMany mutation: missing entries array.");
86
+ throw new BTreeConcurrencyError(
87
+ "Malformed putMany mutation: missing entries array."
88
+ );
81
89
  }
82
90
  validatePutManyEntries(m.entries);
83
91
  break;
84
92
  case "deleteRange":
85
93
  if (!("startKey" in m) || !("endKey" in m)) {
86
- throw new BTreeConcurrencyError("Malformed deleteRange mutation: missing startKey or endKey.");
94
+ throw new BTreeConcurrencyError(
95
+ "Malformed deleteRange mutation: missing startKey or endKey."
96
+ );
87
97
  }
88
98
  break;
99
+ default:
100
+ break;
101
+ }
102
+ };
103
+ var validateSingleMutation = (m, expectedConfigFingerprint) => {
104
+ switch (m.type) {
105
+ case "init":
106
+ validateInitMutation(m, expectedConfigFingerprint);
107
+ break;
108
+ case "put":
109
+ case "remove":
110
+ case "removeById":
111
+ case "updateById":
112
+ case "putMany":
113
+ case "deleteRange":
114
+ validateMutationFields(m);
115
+ break;
116
+ case "popFirst":
117
+ case "popLast":
89
118
  case "clear":
90
119
  break;
91
120
  default:
@@ -97,7 +126,9 @@ var validateSingleMutation = (m, expectedConfigFingerprint) => {
97
126
  var validateMutationBatch = (mutations, expectedConfigFingerprint) => {
98
127
  for (const mutation of mutations) {
99
128
  if (typeof mutation !== "object" || mutation === null) {
100
- throw new BTreeConcurrencyError("Malformed mutation: expected an object.");
129
+ throw new BTreeConcurrencyError(
130
+ "Malformed mutation: expected an object."
131
+ );
101
132
  }
102
133
  validateSingleMutation(
103
134
  mutation,
@@ -132,9 +163,7 @@ var normalizeReadMode = (value) => {
132
163
  return "strong";
133
164
  }
134
165
  if (value !== "strong" && value !== "local") {
135
- throw new BTreeConcurrencyError(
136
- `readMode: must be 'strong' or 'local'.`
137
- );
166
+ throw new BTreeConcurrencyError(`readMode: must be 'strong' or 'local'.`);
138
167
  }
139
168
  return value;
140
169
  };
@@ -184,7 +213,11 @@ var applyMutationLocal = (tree, mutation, onInit) => {
184
213
  case "popLast":
185
214
  return tree.popLast();
186
215
  case "deleteRange":
187
- return tree.deleteRange(mutation.startKey, mutation.endKey, mutation.options);
216
+ return tree.deleteRange(
217
+ mutation.startKey,
218
+ mutation.endKey,
219
+ mutation.options
220
+ );
188
221
  case "clear":
189
222
  tree.clear();
190
223
  return null;
@@ -231,7 +264,9 @@ var createPutManyEvaluator = (entries, duplicateKeys, compareKeys) => {
231
264
  for (let i = 1; i < entries.length; i += 1) {
232
265
  const cmp = compareKeys(entries[i - 1].key, entries[i].key);
233
266
  if (cmp > 0) {
234
- throw new BTreeValidationError("putMany: entries not in ascending order.");
267
+ throw new BTreeValidationError(
268
+ "putMany: entries not in ascending order."
269
+ );
235
270
  }
236
271
  if (strictlyAscending && cmp === 0) {
237
272
  throw new BTreeValidationError(
@@ -262,59 +297,48 @@ var createClearEvaluator = () => {
262
297
  return () => ({ type: "clear" });
263
298
  };
264
299
 
265
- // src/concurrency/ConcurrentInMemoryBTree.ts
266
- var ConcurrentInMemoryBTree = class {
267
- constructor(config) {
268
- this.store = config.store;
269
- this.compareKeys = config.compareKeys;
270
- this.maxRetries = normalizeMaxRetries(config.maxRetries);
271
- this.maxSyncMutationsPerBatch = normalizeMaxSyncMutationsPerBatch(
272
- config.maxSyncMutationsPerBatch
300
+ // src/concurrency/syncLogValidation.ts
301
+ var validateSyncLog = (log, maxSyncMutationsPerBatch) => {
302
+ if (typeof log.version !== "bigint") {
303
+ throw new BTreeConcurrencyError("Store contract: version must be bigint.");
304
+ }
305
+ if (!Array.isArray(log.mutations)) {
306
+ throw new BTreeConcurrencyError(
307
+ "Store contract: mutations must be an array."
273
308
  );
274
- this.duplicateKeys = config.duplicateKeys ?? "replace";
275
- this.readMode = normalizeReadMode(config.readMode);
276
- this.configFingerprint = computeConfigFingerprint(config);
277
- this.tree = new InMemoryBTree({
278
- compareKeys: config.compareKeys,
279
- maxLeafEntries: config.maxLeafEntries,
280
- maxBranchChildren: config.maxBranchChildren,
281
- duplicateKeys: config.duplicateKeys,
282
- enableEntryIdLookup: config.enableEntryIdLookup,
283
- autoScale: config.autoScale
284
- });
309
+ }
310
+ if (log.mutations.length > maxSyncMutationsPerBatch) {
311
+ throw new BTreeConcurrencyError(
312
+ `Sync batch exceeded limit (${String(maxSyncMutationsPerBatch)}).`
313
+ );
314
+ }
315
+ };
316
+
317
+ // src/concurrency/coordinator.ts
318
+ var Coordinator = class {
319
+ constructor(tree, store, maxRetries, maxSyncMutationsPerBatch, configFingerprint, readMode) {
320
+ this.tree = tree;
321
+ this.store = store;
322
+ this.maxRetries = maxRetries;
323
+ this.maxSyncMutationsPerBatch = maxSyncMutationsPerBatch;
324
+ this.configFingerprint = configFingerprint;
325
+ this.readMode = readMode;
285
326
  this.currentVersion = 0n;
286
327
  this.operationQueue = Promise.resolve();
287
328
  this.initSeen = false;
288
329
  this.corrupted = false;
289
330
  }
290
- async sync() {
291
- await this.runExclusive(async () => {
292
- await this.syncUnlocked();
293
- });
294
- }
295
331
  async syncUnlocked() {
296
332
  const log = await this.store.getLogEntriesSince(this.currentVersion);
297
- if (typeof log.version !== "bigint") {
298
- throw new BTreeConcurrencyError("Store contract: version must be bigint.");
299
- }
300
- if (!Array.isArray(log.mutations)) {
301
- throw new BTreeConcurrencyError("Store contract: mutations must be an array.");
302
- }
303
- if (log.mutations.length > this.maxSyncMutationsPerBatch) {
304
- throw new BTreeConcurrencyError(
305
- `Sync batch exceeded limit (${String(this.maxSyncMutationsPerBatch)}).`
306
- );
307
- }
308
- if (log.version <= this.currentVersion) {
309
- return;
310
- }
333
+ validateSyncLog(log, this.maxSyncMutationsPerBatch);
334
+ if (log.version <= this.currentVersion) return;
311
335
  validateMutationBatch(log.mutations, this.configFingerprint);
312
336
  try {
313
- for (const mutation of log.mutations) {
314
- applyMutationLocal(this.tree, mutation, () => {
315
- this.initSeen = true;
316
- });
317
- }
337
+ const markInit = () => {
338
+ this.initSeen = true;
339
+ };
340
+ for (const mutation of log.mutations)
341
+ applyMutationLocal(this.tree, mutation, markInit);
318
342
  this.currentVersion = log.version;
319
343
  } catch (error) {
320
344
  this.corrupted = true;
@@ -326,11 +350,10 @@ var ConcurrentInMemoryBTree = class {
326
350
  }
327
351
  runExclusive(operation) {
328
352
  const run = async () => {
329
- if (this.corrupted) {
353
+ if (this.corrupted)
330
354
  throw new BTreeConcurrencyError(
331
355
  "Instance is permanently corrupted. Discard and create a new instance."
332
356
  );
333
- }
334
357
  return operation();
335
358
  };
336
359
  const result = this.operationQueue.then(run, run);
@@ -342,40 +365,38 @@ var ConcurrentInMemoryBTree = class {
342
365
  }
343
366
  readOp(fn) {
344
367
  return this.runExclusive(async () => {
345
- if (this.readMode === "strong") {
346
- await this.syncUnlocked();
347
- }
368
+ if (this.readMode === "strong") await this.syncUnlocked();
348
369
  return fn(this.tree);
349
370
  });
350
371
  }
351
- async appendMutationAndApplyUnlocked(evaluate) {
372
+ async appendAndApply(evaluate) {
352
373
  for (let attempt = 0; attempt < this.maxRetries; attempt += 1) {
353
374
  await this.syncUnlocked();
354
375
  const mutation = evaluate(this.tree);
355
- if (mutation === null) {
356
- return null;
357
- }
376
+ if (mutation === null) return null;
358
377
  const expectedVersion = this.currentVersion;
359
- const mutations = this.initSeen ? [mutation] : [{ type: "init", configFingerprint: this.configFingerprint }, mutation];
378
+ const mutations = this.initSeen ? [mutation] : [
379
+ { type: "init", configFingerprint: this.configFingerprint },
380
+ mutation
381
+ ];
360
382
  const appendResult = await this.store.append(expectedVersion, mutations);
361
383
  assertAppendVersionContract(expectedVersion, appendResult);
362
384
  if (appendResult.applied) {
363
385
  try {
386
+ const markInit = () => {
387
+ this.initSeen = true;
388
+ };
364
389
  for (const m of mutations) {
365
390
  if (m === mutation) break;
366
- applyMutationLocal(this.tree, m, () => {
367
- this.initSeen = true;
368
- });
391
+ applyMutationLocal(this.tree, m, markInit);
369
392
  }
370
- const localResult = applyMutationLocal(
393
+ const result = applyMutationLocal(
371
394
  this.tree,
372
395
  mutation,
373
- () => {
374
- this.initSeen = true;
375
- }
396
+ markInit
376
397
  );
377
398
  this.currentVersion = appendResult.version;
378
- return localResult;
399
+ return result;
379
400
  } catch (error) {
380
401
  this.corrupted = true;
381
402
  const cause = error instanceof Error ? error.message : String(error);
@@ -389,137 +410,159 @@ var ConcurrentInMemoryBTree = class {
389
410
  `Mutation failed after ${String(this.maxRetries)} retries.`
390
411
  );
391
412
  }
392
- async put(key, value) {
393
- return this.runExclusive(async () => {
394
- return this.appendMutationAndApplyUnlocked(
395
- createPutEvaluator(this.duplicateKeys, key, value)
396
- );
413
+ writeOp(evaluator) {
414
+ return this.runExclusive(async () => this.appendAndApply(evaluator));
415
+ }
416
+ };
417
+
418
+ // src/concurrency/ConcurrentInMemoryBTree.ts
419
+ var ConcurrentInMemoryBTree = class {
420
+ constructor(config) {
421
+ this.compareKeys = config.compareKeys;
422
+ this.duplicateKeys = config.duplicateKeys ?? "replace";
423
+ const tree = new InMemoryBTree({
424
+ compareKeys: config.compareKeys,
425
+ maxLeafEntries: config.maxLeafEntries,
426
+ maxBranchChildren: config.maxBranchChildren,
427
+ duplicateKeys: config.duplicateKeys,
428
+ enableEntryIdLookup: config.enableEntryIdLookup,
429
+ autoScale: config.autoScale,
430
+ deleteRebalancePolicy: config.deleteRebalancePolicy
397
431
  });
432
+ this.coord = new Coordinator(
433
+ tree,
434
+ config.store,
435
+ normalizeMaxRetries(config.maxRetries),
436
+ normalizeMaxSyncMutationsPerBatch(config.maxSyncMutationsPerBatch),
437
+ computeConfigFingerprint(config),
438
+ normalizeReadMode(config.readMode)
439
+ );
398
440
  }
399
- async remove(key) {
400
- return this.runExclusive(async () => {
401
- return this.appendMutationAndApplyUnlocked(createRemoveEvaluator(key));
441
+ async sync() {
442
+ await this.coord.runExclusive(async () => {
443
+ await this.coord.syncUnlocked();
402
444
  });
403
445
  }
404
- async removeById(entryId) {
405
- return this.runExclusive(async () => {
406
- return this.appendMutationAndApplyUnlocked(createRemoveByIdEvaluator(entryId));
446
+ async syncThenRead(fn) {
447
+ return this.coord.runExclusive(async () => {
448
+ await this.coord.syncUnlocked();
449
+ return fn(this.coord.tree);
407
450
  });
408
451
  }
452
+ async put(key, value) {
453
+ return this.coord.writeOp(
454
+ createPutEvaluator(this.duplicateKeys, key, value)
455
+ );
456
+ }
457
+ async remove(key) {
458
+ return this.coord.writeOp(createRemoveEvaluator(key));
459
+ }
460
+ async removeById(entryId) {
461
+ return this.coord.writeOp(createRemoveByIdEvaluator(entryId));
462
+ }
409
463
  async updateById(entryId, value) {
410
- return this.runExclusive(async () => {
411
- return this.appendMutationAndApplyUnlocked(createUpdateByIdEvaluator(entryId, value));
412
- });
464
+ return this.coord.writeOp(createUpdateByIdEvaluator(entryId, value));
413
465
  }
414
466
  async popFirst() {
415
- return this.runExclusive(async () => {
416
- return this.appendMutationAndApplyUnlocked(createPopFirstEvaluator());
417
- });
467
+ return this.coord.writeOp(createPopFirstEvaluator());
418
468
  }
419
469
  async popLast() {
420
- return this.runExclusive(async () => {
421
- return this.appendMutationAndApplyUnlocked(createPopLastEvaluator());
422
- });
470
+ return this.coord.writeOp(createPopLastEvaluator());
423
471
  }
424
472
  async putMany(entries) {
425
- if (entries.length === 0) {
426
- return [];
427
- }
428
- return this.runExclusive(async () => {
429
- return this.appendMutationAndApplyUnlocked(
430
- createPutManyEvaluator(entries, this.duplicateKeys, this.compareKeys)
431
- );
432
- });
473
+ if (entries.length === 0) return [];
474
+ return this.coord.writeOp(
475
+ createPutManyEvaluator(entries, this.duplicateKeys, this.compareKeys)
476
+ );
433
477
  }
434
478
  async deleteRange(startKey, endKey, options) {
435
- return this.runExclusive(async () => {
436
- const result = await this.appendMutationAndApplyUnlocked(
437
- createDeleteRangeEvaluator(startKey, endKey, options)
438
- );
439
- return result ?? 0;
440
- });
479
+ const result = await this.coord.writeOp(
480
+ createDeleteRangeEvaluator(startKey, endKey, options)
481
+ );
482
+ return result ?? 0;
441
483
  }
442
484
  async clear() {
443
- await this.runExclusive(async () => {
444
- await this.appendMutationAndApplyUnlocked(createClearEvaluator());
445
- });
485
+ await this.coord.writeOp(createClearEvaluator());
446
486
  }
447
487
  async get(key) {
448
- return this.readOp((tree) => tree.get(key));
488
+ return this.coord.readOp((t) => t.get(key));
449
489
  }
450
490
  async hasKey(key) {
451
- return this.readOp((tree) => tree.hasKey(key));
491
+ return this.coord.readOp((t) => t.hasKey(key));
452
492
  }
453
493
  async findFirst(key) {
454
- return this.readOp((tree) => tree.findFirst(key));
494
+ return this.coord.readOp((t) => t.findFirst(key));
455
495
  }
456
496
  async findLast(key) {
457
- return this.readOp((tree) => tree.findLast(key));
497
+ return this.coord.readOp((t) => t.findLast(key));
458
498
  }
459
499
  async range(startKey, endKey, options) {
460
- return this.readOp((tree) => tree.range(startKey, endKey, options));
500
+ return this.coord.readOp((t) => t.range(startKey, endKey, options));
461
501
  }
462
502
  async snapshot() {
463
- return this.readOp((tree) => tree.snapshot());
503
+ return this.coord.readOp((t) => t.snapshot());
464
504
  }
465
505
  async size() {
466
- return this.readOp((tree) => tree.size());
506
+ return this.coord.readOp((t) => t.size());
467
507
  }
468
508
  async assertInvariants() {
469
- await this.readOp((tree) => tree.assertInvariants());
509
+ await this.coord.readOp((t) => t.assertInvariants());
470
510
  }
471
511
  async getStats() {
472
- return this.readOp((tree) => tree.getStats());
512
+ return this.coord.readOp((t) => t.getStats());
473
513
  }
474
514
  async peekFirst() {
475
- return this.readOp((tree) => tree.peekFirst());
515
+ return this.coord.readOp((t) => t.peekFirst());
476
516
  }
477
517
  async peekLast() {
478
- return this.readOp((tree) => tree.peekLast());
518
+ return this.coord.readOp((t) => t.peekLast());
479
519
  }
480
520
  async peekById(entryId) {
481
- return this.readOp((tree) => tree.peekById(entryId));
521
+ return this.coord.readOp((t) => t.peekById(entryId));
482
522
  }
483
523
  async count(startKey, endKey, options) {
484
- return this.readOp((tree) => tree.count(startKey, endKey, options));
524
+ return this.coord.readOp((t) => t.count(startKey, endKey, options));
485
525
  }
486
526
  async nextHigherKey(key) {
487
- return this.readOp((tree) => tree.nextHigherKey(key));
527
+ return this.coord.readOp((t) => t.nextHigherKey(key));
488
528
  }
489
529
  async nextLowerKey(key) {
490
- return this.readOp((tree) => tree.nextLowerKey(key));
530
+ return this.coord.readOp((t) => t.nextLowerKey(key));
491
531
  }
492
532
  async getPairOrNextLower(key) {
493
- return this.readOp((tree) => tree.getPairOrNextLower(key));
533
+ return this.coord.readOp((t) => t.getPairOrNextLower(key));
494
534
  }
495
535
  async entries() {
496
- return this.readOp((tree) => Array.from(tree.entries()));
536
+ return this.coord.readOp((t) => Array.from(t.entries()));
497
537
  }
498
538
  async entriesReversed() {
499
- return this.readOp((tree) => Array.from(tree.entriesReversed()));
539
+ return this.coord.readOp((t) => Array.from(t.entriesReversed()));
500
540
  }
501
541
  async keys() {
502
- return this.readOp((tree) => Array.from(tree.keys()));
542
+ return this.coord.readOp((t) => Array.from(t.keys()));
503
543
  }
504
544
  async values() {
505
- return this.readOp((tree) => Array.from(tree.values()));
545
+ return this.coord.readOp((t) => Array.from(t.values()));
506
546
  }
507
547
  async forEach(callback) {
508
- await this.readOp((tree) => {
509
- tree.forEach(callback);
548
+ await this.coord.readOp((t) => {
549
+ t.forEach(callback);
550
+ });
551
+ }
552
+ async forEachRange(startKey, endKey, callback, options) {
553
+ await this.coord.readOp((t) => {
554
+ t.forEachRange(startKey, endKey, callback, options);
510
555
  });
511
556
  }
512
557
  async *[Symbol.asyncIterator]() {
513
558
  const all = await this.entries();
514
- for (const entry of all) {
515
- yield entry;
516
- }
559
+ for (const entry of all) yield entry;
517
560
  }
518
561
  async clone() {
519
- return this.readOp((tree) => tree.clone());
562
+ return this.coord.readOp((t) => t.clone());
520
563
  }
521
564
  async toJSON() {
522
- return this.readOp((tree) => tree.toJSON());
565
+ return this.coord.readOp((t) => t.toJSON());
523
566
  }
524
567
  static fromJSON(json, compareKeys) {
525
568
  return InMemoryBTree.fromJSON(json, compareKeys);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frostpillar/frostpillar-btree",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "A tiny, zero-dependency in-memory B+ tree for TypeScript, Node.js, and browser JavaScript.",
5
5
  "type": "module",
6
6
  "author": "Hajime Sano",