@frostpillar/frostpillar-btree 0.2.4 → 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-ZA3EQNDI.js";
9
+ } from "./chunk-OWHENPGJ.js";
10
10
 
11
11
  // src/concurrency/helpers.ts
12
12
  var DEFAULT_MAX_RETRIES = 16;
@@ -30,6 +30,81 @@ 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
+ };
97
+ var validateMutationBatch = (mutations, expectedConfigFingerprint) => {
98
+ for (const mutation of mutations) {
99
+ if (typeof mutation !== "object" || mutation === null) {
100
+ throw new BTreeConcurrencyError("Malformed mutation: expected an object.");
101
+ }
102
+ validateSingleMutation(
103
+ mutation,
104
+ expectedConfigFingerprint
105
+ );
106
+ }
107
+ };
33
108
  var normalizeMaxRetries = (value) => {
34
109
  if (value === void 0) {
35
110
  return DEFAULT_MAX_RETRIES;
@@ -88,10 +163,110 @@ function assertAppendVersionContract(expectedVersion, appendResult) {
88
163
  }
89
164
  }
90
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
+
91
265
  // src/concurrency/ConcurrentInMemoryBTree.ts
92
266
  var ConcurrentInMemoryBTree = class {
93
267
  constructor(config) {
94
268
  this.store = config.store;
269
+ this.compareKeys = config.compareKeys;
95
270
  this.maxRetries = normalizeMaxRetries(config.maxRetries);
96
271
  this.maxSyncMutationsPerBatch = normalizeMaxSyncMutationsPerBatch(
97
272
  config.maxSyncMutationsPerBatch
@@ -110,6 +285,7 @@ var ConcurrentInMemoryBTree = class {
110
285
  this.currentVersion = 0n;
111
286
  this.operationQueue = Promise.resolve();
112
287
  this.initSeen = false;
288
+ this.corrupted = false;
113
289
  }
114
290
  async sync() {
115
291
  await this.runExclusive(async () => {
@@ -132,39 +308,31 @@ var ConcurrentInMemoryBTree = class {
132
308
  if (log.version <= this.currentVersion) {
133
309
  return;
134
310
  }
135
- for (const mutation of log.mutations) {
136
- this.applyMutationLocal(mutation);
137
- }
138
- this.currentVersion = log.version;
139
- }
140
- applyMutationLocal(mutation) {
141
- switch (mutation.type) {
142
- case "init":
143
- if (mutation.configFingerprint !== this.configFingerprint) {
144
- throw new BTreeConcurrencyError(
145
- "Config mismatch: store peers must share identical tree config."
146
- );
147
- }
148
- this.initSeen = true;
149
- return null;
150
- case "put":
151
- return this.tree.put(mutation.key, mutation.value);
152
- case "remove":
153
- return this.tree.remove(mutation.key);
154
- case "removeById":
155
- return this.tree.removeById(mutation.entryId);
156
- case "updateById":
157
- return this.tree.updateById(mutation.entryId, mutation.value);
158
- case "popFirst":
159
- return this.tree.popFirst();
160
- case "popLast":
161
- return this.tree.popLast();
162
- default:
163
- return assertNeverMutation(mutation);
311
+ validateMutationBatch(log.mutations, this.configFingerprint);
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
+ );
164
325
  }
165
326
  }
166
327
  runExclusive(operation) {
167
- 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
+ };
168
336
  const result = this.operationQueue.then(run, run);
169
337
  this.operationQueue = result.then(
170
338
  () => void 0,
@@ -192,13 +360,29 @@ var ConcurrentInMemoryBTree = class {
192
360
  const appendResult = await this.store.append(expectedVersion, mutations);
193
361
  assertAppendVersionContract(expectedVersion, appendResult);
194
362
  if (appendResult.applied) {
195
- for (const m of mutations) {
196
- if (m === mutation) break;
197
- 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
+ );
198
385
  }
199
- const localResult = this.applyMutationLocal(mutation);
200
- this.currentVersion = appendResult.version;
201
- return localResult;
202
386
  }
203
387
  }
204
388
  throw new BTreeConcurrencyError(
@@ -208,47 +392,56 @@ var ConcurrentInMemoryBTree = class {
208
392
  async put(key, value) {
209
393
  return this.runExclusive(async () => {
210
394
  return this.appendMutationAndApplyUnlocked(
211
- (tree) => {
212
- if (this.duplicateKeys === "reject" && tree.hasKey(key)) {
213
- throw new BTreeValidationError("Duplicate key rejected.");
214
- }
215
- return { type: "put", key, value };
216
- }
395
+ createPutEvaluator(this.duplicateKeys, key, value)
217
396
  );
218
397
  });
219
398
  }
220
399
  async remove(key) {
221
400
  return this.runExclusive(async () => {
222
- return this.appendMutationAndApplyUnlocked(
223
- (tree) => {
224
- return tree.hasKey(key) ? { type: "remove", key } : null;
225
- }
226
- );
401
+ return this.appendMutationAndApplyUnlocked(createRemoveEvaluator(key));
227
402
  });
228
403
  }
229
404
  async removeById(entryId) {
230
405
  return this.runExclusive(async () => {
231
- return this.appendMutationAndApplyUnlocked(
232
- (tree) => {
233
- return tree.peekById(entryId) !== null ? { type: "removeById", entryId } : null;
234
- }
235
- );
406
+ return this.appendMutationAndApplyUnlocked(createRemoveByIdEvaluator(entryId));
236
407
  });
237
408
  }
238
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
+ }
239
428
  return this.runExclusive(async () => {
240
429
  return this.appendMutationAndApplyUnlocked(
241
- (tree) => {
242
- return tree.peekById(entryId) !== null ? { type: "updateById", entryId, value } : null;
243
- }
430
+ createPutManyEvaluator(entries, this.duplicateKeys, this.compareKeys)
244
431
  );
245
432
  });
246
433
  }
247
- async popFirst() {
434
+ async deleteRange(startKey, endKey, options) {
248
435
  return this.runExclusive(async () => {
249
- return this.appendMutationAndApplyUnlocked((tree) => {
250
- return tree.peekFirst() !== null ? { type: "popFirst" } : null;
251
- });
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());
252
445
  });
253
446
  }
254
447
  async get(key) {
@@ -284,13 +477,6 @@ var ConcurrentInMemoryBTree = class {
284
477
  async peekLast() {
285
478
  return this.readOp((tree) => tree.peekLast());
286
479
  }
287
- async popLast() {
288
- return this.runExclusive(async () => {
289
- return this.appendMutationAndApplyUnlocked((tree) => {
290
- return tree.peekLast() !== null ? { type: "popLast" } : null;
291
- });
292
- });
293
- }
294
480
  async peekById(entryId) {
295
481
  return this.readOp((tree) => tree.peekById(entryId));
296
482
  }
@@ -306,6 +492,38 @@ var ConcurrentInMemoryBTree = class {
306
492
  async getPairOrNextLower(key) {
307
493
  return this.readOp((tree) => tree.getPairOrNextLower(key));
308
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
+ }
309
527
  };
310
528
  export {
311
529
  BTreeConcurrencyError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frostpillar/frostpillar-btree",
3
- "version": "0.2.4",
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",