@frostpillar/frostpillar-btree 0.2.5 → 0.2.6

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.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  DEFAULT_MAX_LEAF_ENTRIES,
7
7
  InMemoryBTree,
8
8
  computeAutoScaleTier
9
- } from "./chunk-CZFRT2NN.js";
9
+ } from "./chunk-OWHENPGJ.js";
10
10
 
11
11
  // src/concurrency/helpers.ts
12
12
  var DEFAULT_MAX_RETRIES = 16;
@@ -30,51 +30,79 @@ var assertNeverMutation = (mutation) => {
30
30
  `Unsupported mutation type from shared store: ${String(unknownMutation.type)}`
31
31
  );
32
32
  };
33
+ var validatePutManyEntries = (entries) => {
34
+ for (const entry of entries) {
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.");
37
+ }
38
+ }
39
+ };
40
+ var validateInitMutation = (m, expectedConfigFingerprint) => {
41
+ if (typeof m.configFingerprint !== "string") {
42
+ throw new BTreeConcurrencyError("Malformed init mutation: missing configFingerprint.");
43
+ }
44
+ if (expectedConfigFingerprint !== void 0 && m.configFingerprint !== expectedConfigFingerprint) {
45
+ throw new BTreeConcurrencyError(
46
+ "Config mismatch: store peers must share identical tree config."
47
+ );
48
+ }
49
+ };
50
+ var validateSingleMutation = (m, expectedConfigFingerprint) => {
51
+ switch (m.type) {
52
+ case "init":
53
+ validateInitMutation(m, expectedConfigFingerprint);
54
+ break;
55
+ case "put":
56
+ if (!("key" in m) || !("value" in m)) {
57
+ throw new BTreeConcurrencyError("Malformed put mutation: missing key or value.");
58
+ }
59
+ break;
60
+ case "remove":
61
+ if (!("key" in m)) {
62
+ throw new BTreeConcurrencyError("Malformed remove mutation: missing key.");
63
+ }
64
+ break;
65
+ case "removeById":
66
+ if (!("entryId" in m)) {
67
+ throw new BTreeConcurrencyError("Malformed removeById mutation: missing entryId.");
68
+ }
69
+ break;
70
+ case "updateById":
71
+ if (!("entryId" in m) || !("value" in m)) {
72
+ throw new BTreeConcurrencyError("Malformed updateById mutation: missing entryId or value.");
73
+ }
74
+ break;
75
+ case "popFirst":
76
+ case "popLast":
77
+ break;
78
+ case "putMany":
79
+ if (!("entries" in m) || !Array.isArray(m.entries)) {
80
+ throw new BTreeConcurrencyError("Malformed putMany mutation: missing entries array.");
81
+ }
82
+ validatePutManyEntries(m.entries);
83
+ break;
84
+ case "deleteRange":
85
+ if (!("startKey" in m) || !("endKey" in m)) {
86
+ throw new BTreeConcurrencyError("Malformed deleteRange mutation: missing startKey or endKey.");
87
+ }
88
+ break;
89
+ case "clear":
90
+ break;
91
+ default:
92
+ throw new BTreeConcurrencyError(
93
+ `Unsupported mutation type from shared store: ${String(m.type)}`
94
+ );
95
+ }
96
+ };
33
97
  var validateMutationBatch = (mutations, expectedConfigFingerprint) => {
34
98
  for (const mutation of mutations) {
35
99
  if (typeof mutation !== "object" || mutation === null) {
36
100
  throw new BTreeConcurrencyError("Malformed mutation: expected an object.");
37
101
  }
38
- const m = mutation;
39
- switch (m.type) {
40
- case "init":
41
- if (typeof m.configFingerprint !== "string") {
42
- throw new BTreeConcurrencyError("Malformed init mutation: missing configFingerprint.");
43
- }
44
- if (expectedConfigFingerprint !== void 0 && m.configFingerprint !== expectedConfigFingerprint) {
45
- throw new BTreeConcurrencyError(
46
- "Config mismatch: store peers must share identical tree config."
47
- );
48
- }
49
- break;
50
- case "put":
51
- if (!("key" in m) || !("value" in m)) {
52
- throw new BTreeConcurrencyError("Malformed put mutation: missing key or value.");
53
- }
54
- break;
55
- case "remove":
56
- if (!("key" in m)) {
57
- throw new BTreeConcurrencyError("Malformed remove mutation: missing key.");
58
- }
59
- break;
60
- case "removeById":
61
- if (!("entryId" in m)) {
62
- throw new BTreeConcurrencyError("Malformed removeById mutation: missing entryId.");
63
- }
64
- break;
65
- case "updateById":
66
- if (!("entryId" in m) || !("value" in m)) {
67
- throw new BTreeConcurrencyError("Malformed updateById mutation: missing entryId or value.");
68
- }
69
- break;
70
- case "popFirst":
71
- case "popLast":
72
- break;
73
- default:
74
- throw new BTreeConcurrencyError(
75
- `Unsupported mutation type from shared store: ${String(m.type)}`
76
- );
77
- }
102
+ validateSingleMutation(
103
+ mutation,
104
+ expectedConfigFingerprint
105
+ );
78
106
  }
79
107
  };
80
108
  var normalizeMaxRetries = (value) => {
@@ -135,10 +163,110 @@ function assertAppendVersionContract(expectedVersion, appendResult) {
135
163
  }
136
164
  }
137
165
 
166
+ // src/concurrency/writeOps.ts
167
+ var applyMutationLocal = (tree, mutation, onInit) => {
168
+ switch (mutation.type) {
169
+ case "init":
170
+ onInit();
171
+ return null;
172
+ case "put":
173
+ return tree.put(mutation.key, mutation.value);
174
+ case "putMany":
175
+ return tree.putMany(mutation.entries);
176
+ case "remove":
177
+ return tree.remove(mutation.key);
178
+ case "removeById":
179
+ return tree.removeById(mutation.entryId);
180
+ case "updateById":
181
+ return tree.updateById(mutation.entryId, mutation.value);
182
+ case "popFirst":
183
+ return tree.popFirst();
184
+ case "popLast":
185
+ return tree.popLast();
186
+ case "deleteRange":
187
+ return tree.deleteRange(mutation.startKey, mutation.endKey, mutation.options);
188
+ case "clear":
189
+ tree.clear();
190
+ return null;
191
+ default:
192
+ return assertNeverMutation(mutation);
193
+ }
194
+ };
195
+ var createPutEvaluator = (duplicateKeys, key, value) => {
196
+ return (tree) => {
197
+ if (duplicateKeys === "reject" && tree.hasKey(key)) {
198
+ throw new BTreeValidationError("Duplicate key rejected.");
199
+ }
200
+ return { type: "put", key, value };
201
+ };
202
+ };
203
+ var createRemoveEvaluator = (key) => {
204
+ return (tree) => {
205
+ return tree.hasKey(key) ? { type: "remove", key } : null;
206
+ };
207
+ };
208
+ var createRemoveByIdEvaluator = (entryId) => {
209
+ return (tree) => {
210
+ return tree.peekById(entryId) !== null ? { type: "removeById", entryId } : null;
211
+ };
212
+ };
213
+ var createUpdateByIdEvaluator = (entryId, value) => {
214
+ return (tree) => {
215
+ return tree.peekById(entryId) !== null ? { type: "updateById", entryId, value } : null;
216
+ };
217
+ };
218
+ var createPopFirstEvaluator = () => {
219
+ return (tree) => {
220
+ return tree.peekFirst() !== null ? { type: "popFirst" } : null;
221
+ };
222
+ };
223
+ var createPopLastEvaluator = () => {
224
+ return (tree) => {
225
+ return tree.peekLast() !== null ? { type: "popLast" } : null;
226
+ };
227
+ };
228
+ var createPutManyEvaluator = (entries, duplicateKeys, compareKeys) => {
229
+ return (tree) => {
230
+ const strictlyAscending = duplicateKeys !== "allow";
231
+ for (let i = 1; i < entries.length; i += 1) {
232
+ const cmp = compareKeys(entries[i - 1].key, entries[i].key);
233
+ if (cmp > 0) {
234
+ throw new BTreeValidationError("putMany: entries not in ascending order.");
235
+ }
236
+ if (strictlyAscending && cmp === 0) {
237
+ throw new BTreeValidationError(
238
+ duplicateKeys === "reject" ? "putMany: duplicate key rejected." : "putMany: equal keys not allowed in strict mode."
239
+ );
240
+ }
241
+ }
242
+ if (duplicateKeys === "reject") {
243
+ for (const entry of entries) {
244
+ if (tree.hasKey(entry.key)) {
245
+ throw new BTreeValidationError("Duplicate key rejected.");
246
+ }
247
+ }
248
+ }
249
+ return { type: "putMany", entries };
250
+ };
251
+ };
252
+ var createDeleteRangeEvaluator = (startKey, endKey, options) => {
253
+ return (tree) => {
254
+ const count = tree.count(startKey, endKey, options);
255
+ if (count === 0) {
256
+ return null;
257
+ }
258
+ return { type: "deleteRange", startKey, endKey, options };
259
+ };
260
+ };
261
+ var createClearEvaluator = () => {
262
+ return () => ({ type: "clear" });
263
+ };
264
+
138
265
  // src/concurrency/ConcurrentInMemoryBTree.ts
139
266
  var ConcurrentInMemoryBTree = class {
140
267
  constructor(config) {
141
268
  this.store = config.store;
269
+ this.compareKeys = config.compareKeys;
142
270
  this.maxRetries = normalizeMaxRetries(config.maxRetries);
143
271
  this.maxSyncMutationsPerBatch = normalizeMaxSyncMutationsPerBatch(
144
272
  config.maxSyncMutationsPerBatch
@@ -157,6 +285,7 @@ var ConcurrentInMemoryBTree = class {
157
285
  this.currentVersion = 0n;
158
286
  this.operationQueue = Promise.resolve();
159
287
  this.initSeen = false;
288
+ this.corrupted = false;
160
289
  }
161
290
  async sync() {
162
291
  await this.runExclusive(async () => {
@@ -180,34 +309,30 @@ var ConcurrentInMemoryBTree = class {
180
309
  return;
181
310
  }
182
311
  validateMutationBatch(log.mutations, this.configFingerprint);
183
- for (const mutation of log.mutations) {
184
- this.applyMutationLocal(mutation);
185
- }
186
- this.currentVersion = log.version;
187
- }
188
- applyMutationLocal(mutation) {
189
- switch (mutation.type) {
190
- case "init":
191
- this.initSeen = true;
192
- return null;
193
- case "put":
194
- return this.tree.put(mutation.key, mutation.value);
195
- case "remove":
196
- return this.tree.remove(mutation.key);
197
- case "removeById":
198
- return this.tree.removeById(mutation.entryId);
199
- case "updateById":
200
- return this.tree.updateById(mutation.entryId, mutation.value);
201
- case "popFirst":
202
- return this.tree.popFirst();
203
- case "popLast":
204
- return this.tree.popLast();
205
- default:
206
- return assertNeverMutation(mutation);
312
+ try {
313
+ for (const mutation of log.mutations) {
314
+ applyMutationLocal(this.tree, mutation, () => {
315
+ this.initSeen = true;
316
+ });
317
+ }
318
+ this.currentVersion = log.version;
319
+ } catch (error) {
320
+ this.corrupted = true;
321
+ const cause = error instanceof Error ? error.message : String(error);
322
+ throw new BTreeConcurrencyError(
323
+ `Replay failure: instance is permanently corrupted. Discard and create a new instance. Cause: ${cause}`
324
+ );
207
325
  }
208
326
  }
209
327
  runExclusive(operation) {
210
- const run = async () => operation();
328
+ const run = async () => {
329
+ if (this.corrupted) {
330
+ throw new BTreeConcurrencyError(
331
+ "Instance is permanently corrupted. Discard and create a new instance."
332
+ );
333
+ }
334
+ return operation();
335
+ };
211
336
  const result = this.operationQueue.then(run, run);
212
337
  this.operationQueue = result.then(
213
338
  () => void 0,
@@ -235,13 +360,29 @@ var ConcurrentInMemoryBTree = class {
235
360
  const appendResult = await this.store.append(expectedVersion, mutations);
236
361
  assertAppendVersionContract(expectedVersion, appendResult);
237
362
  if (appendResult.applied) {
238
- for (const m of mutations) {
239
- if (m === mutation) break;
240
- this.applyMutationLocal(m);
363
+ try {
364
+ for (const m of mutations) {
365
+ if (m === mutation) break;
366
+ applyMutationLocal(this.tree, m, () => {
367
+ this.initSeen = true;
368
+ });
369
+ }
370
+ const localResult = applyMutationLocal(
371
+ this.tree,
372
+ mutation,
373
+ () => {
374
+ this.initSeen = true;
375
+ }
376
+ );
377
+ this.currentVersion = appendResult.version;
378
+ return localResult;
379
+ } catch (error) {
380
+ this.corrupted = true;
381
+ const cause = error instanceof Error ? error.message : String(error);
382
+ throw new BTreeConcurrencyError(
383
+ `Local apply failure after successful append: instance is permanently corrupted. Discard and create a new instance. Cause: ${cause}`
384
+ );
241
385
  }
242
- const localResult = this.applyMutationLocal(mutation);
243
- this.currentVersion = appendResult.version;
244
- return localResult;
245
386
  }
246
387
  }
247
388
  throw new BTreeConcurrencyError(
@@ -251,47 +392,56 @@ var ConcurrentInMemoryBTree = class {
251
392
  async put(key, value) {
252
393
  return this.runExclusive(async () => {
253
394
  return this.appendMutationAndApplyUnlocked(
254
- (tree) => {
255
- if (this.duplicateKeys === "reject" && tree.hasKey(key)) {
256
- throw new BTreeValidationError("Duplicate key rejected.");
257
- }
258
- return { type: "put", key, value };
259
- }
395
+ createPutEvaluator(this.duplicateKeys, key, value)
260
396
  );
261
397
  });
262
398
  }
263
399
  async remove(key) {
264
400
  return this.runExclusive(async () => {
265
- return this.appendMutationAndApplyUnlocked(
266
- (tree) => {
267
- return tree.hasKey(key) ? { type: "remove", key } : null;
268
- }
269
- );
401
+ return this.appendMutationAndApplyUnlocked(createRemoveEvaluator(key));
270
402
  });
271
403
  }
272
404
  async removeById(entryId) {
273
405
  return this.runExclusive(async () => {
274
- return this.appendMutationAndApplyUnlocked(
275
- (tree) => {
276
- return tree.peekById(entryId) !== null ? { type: "removeById", entryId } : null;
277
- }
278
- );
406
+ return this.appendMutationAndApplyUnlocked(createRemoveByIdEvaluator(entryId));
279
407
  });
280
408
  }
281
409
  async updateById(entryId, value) {
410
+ return this.runExclusive(async () => {
411
+ return this.appendMutationAndApplyUnlocked(createUpdateByIdEvaluator(entryId, value));
412
+ });
413
+ }
414
+ async popFirst() {
415
+ return this.runExclusive(async () => {
416
+ return this.appendMutationAndApplyUnlocked(createPopFirstEvaluator());
417
+ });
418
+ }
419
+ async popLast() {
420
+ return this.runExclusive(async () => {
421
+ return this.appendMutationAndApplyUnlocked(createPopLastEvaluator());
422
+ });
423
+ }
424
+ async putMany(entries) {
425
+ if (entries.length === 0) {
426
+ return [];
427
+ }
282
428
  return this.runExclusive(async () => {
283
429
  return this.appendMutationAndApplyUnlocked(
284
- (tree) => {
285
- return tree.peekById(entryId) !== null ? { type: "updateById", entryId, value } : null;
286
- }
430
+ createPutManyEvaluator(entries, this.duplicateKeys, this.compareKeys)
287
431
  );
288
432
  });
289
433
  }
290
- async popFirst() {
434
+ async deleteRange(startKey, endKey, options) {
291
435
  return this.runExclusive(async () => {
292
- return this.appendMutationAndApplyUnlocked((tree) => {
293
- return tree.peekFirst() !== null ? { type: "popFirst" } : null;
294
- });
436
+ const result = await this.appendMutationAndApplyUnlocked(
437
+ createDeleteRangeEvaluator(startKey, endKey, options)
438
+ );
439
+ return result ?? 0;
440
+ });
441
+ }
442
+ async clear() {
443
+ await this.runExclusive(async () => {
444
+ await this.appendMutationAndApplyUnlocked(createClearEvaluator());
295
445
  });
296
446
  }
297
447
  async get(key) {
@@ -327,13 +477,6 @@ var ConcurrentInMemoryBTree = class {
327
477
  async peekLast() {
328
478
  return this.readOp((tree) => tree.peekLast());
329
479
  }
330
- async popLast() {
331
- return this.runExclusive(async () => {
332
- return this.appendMutationAndApplyUnlocked((tree) => {
333
- return tree.peekLast() !== null ? { type: "popLast" } : null;
334
- });
335
- });
336
- }
337
480
  async peekById(entryId) {
338
481
  return this.readOp((tree) => tree.peekById(entryId));
339
482
  }
@@ -349,6 +492,38 @@ var ConcurrentInMemoryBTree = class {
349
492
  async getPairOrNextLower(key) {
350
493
  return this.readOp((tree) => tree.getPairOrNextLower(key));
351
494
  }
495
+ async entries() {
496
+ return this.readOp((tree) => Array.from(tree.entries()));
497
+ }
498
+ async entriesReversed() {
499
+ return this.readOp((tree) => Array.from(tree.entriesReversed()));
500
+ }
501
+ async keys() {
502
+ return this.readOp((tree) => Array.from(tree.keys()));
503
+ }
504
+ async values() {
505
+ return this.readOp((tree) => Array.from(tree.values()));
506
+ }
507
+ async forEach(callback) {
508
+ await this.readOp((tree) => {
509
+ tree.forEach(callback);
510
+ });
511
+ }
512
+ async *[Symbol.asyncIterator]() {
513
+ const all = await this.entries();
514
+ for (const entry of all) {
515
+ yield entry;
516
+ }
517
+ }
518
+ async clone() {
519
+ return this.readOp((tree) => tree.clone());
520
+ }
521
+ async toJSON() {
522
+ return this.readOp((tree) => tree.toJSON());
523
+ }
524
+ static fromJSON(json, compareKeys) {
525
+ return InMemoryBTree.fromJSON(json, compareKeys);
526
+ }
352
527
  };
353
528
  export {
354
529
  BTreeConcurrencyError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frostpillar/frostpillar-btree",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
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",