@peerbit/native-backbone 0.2.7 → 0.2.9

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/src/index.ts CHANGED
@@ -61,6 +61,20 @@ type NativePeerbitBackboneHandle = {
61
61
  start2: string,
62
62
  end2: string,
63
63
  ) => BigUint64Array;
64
+ entry_hash_numbers_in_range_limited?: (
65
+ start1: string,
66
+ end1: string,
67
+ start2: string,
68
+ end2: string,
69
+ limit: number,
70
+ ) => unknown[];
71
+ entry_hash_numbers_in_range_u64_limited?: (
72
+ start1: string,
73
+ end1: string,
74
+ start2: string,
75
+ end2: string,
76
+ limit: number,
77
+ ) => BigUint64Array;
64
78
  count_entry_coordinates_in_ranges: (
65
79
  start1: string[],
66
80
  end1: string[],
@@ -2959,6 +2973,14 @@ export type NativeBackboneCoordinatePersistenceOptions =
2959
2973
 
2960
2974
  export type NativeBackboneCoordinatePersistenceStore = {
2961
2975
  read(name: string): Promise<Uint8Array | undefined>;
2976
+ /**
2977
+ * Read a named file without materializing more than `maxBytes`.
2978
+ *
2979
+ * The optional method is an explicit capability: callers that require a
2980
+ * pre-allocation bound must fail closed when it is absent and must not fall
2981
+ * back to `read`.
2982
+ */
2983
+ readLimited?(name: string, maxBytes: number): Promise<Uint8Array | undefined>;
2962
2984
  write(name: string, bytes: Uint8Array): Promise<void>;
2963
2985
  /**
2964
2986
  * Appends `bytes` to the named file. Callers hand over ownership of
@@ -2977,6 +2999,21 @@ export type NativeBackboneCoordinatePersistenceStore = {
2977
2999
  close?(options?: { flush?: boolean }): Promise<void>;
2978
3000
  };
2979
3001
 
3002
+ export class NativeBackboneCoordinatePersistenceReadLimitError extends Error {
3003
+ readonly code = "ERR_NATIVE_BACKBONE_COORDINATE_READ_LIMIT";
3004
+
3005
+ constructor(
3006
+ readonly file: string,
3007
+ readonly maxBytes: number,
3008
+ readonly observedBytes: bigint,
3009
+ ) {
3010
+ super(
3011
+ `Native backbone coordinate persistence file ${file} exceeds the ${maxBytes} byte read limit (${observedBytes.toString()} bytes)`,
3012
+ );
3013
+ this.name = "NativeBackboneCoordinatePersistenceReadLimitError";
3014
+ }
3015
+ }
3016
+
2980
3017
  export type NativeBackboneCoordinatePersistenceAdapter = {
2981
3018
  /**
2982
3019
  * Optional durable capability used by higher layers for atomic operation
@@ -3140,6 +3177,10 @@ type NativeBackboneNodeFs = {
3140
3177
  readFile(path: string): Promise<Uint8Array>;
3141
3178
  writeFile(path: string, data: Uint8Array): Promise<unknown>;
3142
3179
  appendFile(path: string, data: Uint8Array): Promise<unknown>;
3180
+ /** Explicit capability for pre-allocation-bounded positional reads. */
3181
+ openBoundedRead?(
3182
+ path: string,
3183
+ ): Promise<NativeBackboneNodeBoundedReadFileHandle>;
3143
3184
  open?(
3144
3185
  path: string,
3145
3186
  flags: string,
@@ -3149,10 +3190,28 @@ type NativeBackboneNodeFs = {
3149
3190
 
3150
3191
  type NativeBackboneNodeAppendFileHandle = {
3151
3192
  write(data: Uint8Array): Promise<{ bytesWritten: number }>;
3193
+ read?(
3194
+ buffer: Uint8Array,
3195
+ offset: number,
3196
+ length: number,
3197
+ position: number,
3198
+ ): Promise<{ bytesRead: number }>;
3199
+ stat?(options: { bigint: true }): Promise<{ size: bigint }>;
3152
3200
  sync?(): Promise<unknown>;
3153
3201
  close(): Promise<unknown>;
3154
3202
  };
3155
3203
 
3204
+ type NativeBackboneNodeBoundedReadFileHandle = {
3205
+ read(
3206
+ buffer: Uint8Array,
3207
+ offset: number,
3208
+ length: number,
3209
+ position: number,
3210
+ ): Promise<{ bytesRead: number }>;
3211
+ stat(options: { bigint: true }): Promise<{ size: bigint }>;
3212
+ close(): Promise<unknown>;
3213
+ };
3214
+
3156
3215
  type NativeBackboneOPFSFile = {
3157
3216
  arrayBuffer(): Promise<ArrayBuffer>;
3158
3217
  size: number;
@@ -3260,6 +3319,31 @@ const validateCoordinatePersistenceName = (name: string): string => {
3260
3319
  return name;
3261
3320
  };
3262
3321
 
3322
+ const validateCoordinatePersistenceReadMaxBytes = (
3323
+ maxBytes: number,
3324
+ ): number => {
3325
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
3326
+ throw new RangeError(
3327
+ "Native backbone coordinate persistence read limit must be a non-negative safe integer",
3328
+ );
3329
+ }
3330
+ return maxBytes;
3331
+ };
3332
+
3333
+ const assertCoordinatePersistenceReadWithinLimit = (
3334
+ name: string,
3335
+ maxBytes: number,
3336
+ observedBytes: number | bigint,
3337
+ ): void => {
3338
+ if (BigInt(observedBytes) > BigInt(maxBytes)) {
3339
+ throw new NativeBackboneCoordinatePersistenceReadLimitError(
3340
+ name,
3341
+ maxBytes,
3342
+ BigInt(observedBytes),
3343
+ );
3344
+ }
3345
+ };
3346
+
3263
3347
  type NativeBackboneCoordinateDropTombstoneBody = {
3264
3348
  format: "peerbit-native-backbone-coordinate-drop";
3265
3349
  version: 1;
@@ -6364,6 +6448,52 @@ export class NativePeerbitBackbone {
6364
6448
  );
6365
6449
  }
6366
6450
 
6451
+ getEntryHashNumbersInRangeLimited(range: {
6452
+ start1: bigint | number | string;
6453
+ end1: bigint | number | string;
6454
+ start2: bigint | number | string;
6455
+ end2: bigint | number | string;
6456
+ limit: number;
6457
+ }): bigint[] | undefined {
6458
+ if (
6459
+ typeof this.native.entry_hash_numbers_in_range_limited !== "function"
6460
+ ) {
6461
+ return undefined;
6462
+ }
6463
+ return rowsToNumbers(
6464
+ "u64",
6465
+ this.native.entry_hash_numbers_in_range_limited(
6466
+ integerString(range.start1),
6467
+ integerString(range.end1),
6468
+ integerString(range.start2),
6469
+ integerString(range.end2),
6470
+ range.limit,
6471
+ ),
6472
+ ) as bigint[];
6473
+ }
6474
+
6475
+ getEntryHashNumbersInRangeU64Limited(range: {
6476
+ start1: bigint | number | string;
6477
+ end1: bigint | number | string;
6478
+ start2: bigint | number | string;
6479
+ end2: bigint | number | string;
6480
+ limit: number;
6481
+ }): BigUint64Array | undefined {
6482
+ if (
6483
+ typeof BigUint64Array === "undefined" ||
6484
+ typeof this.native.entry_hash_numbers_in_range_u64_limited !== "function"
6485
+ ) {
6486
+ return undefined;
6487
+ }
6488
+ return this.native.entry_hash_numbers_in_range_u64_limited(
6489
+ integerString(range.start1),
6490
+ integerString(range.end1),
6491
+ integerString(range.start2),
6492
+ integerString(range.end2),
6493
+ range.limit,
6494
+ );
6495
+ }
6496
+
6367
6497
  countEntryCoordinatesInRanges(
6368
6498
  ranges: Iterable<{
6369
6499
  start1: bigint | number | string;
@@ -8586,6 +8716,23 @@ export class NativeBackboneMemoryCoordinatePersistenceStore
8586
8716
  return file ? copyBytes(file) : undefined;
8587
8717
  }
8588
8718
 
8719
+ async readLimited(
8720
+ name: string,
8721
+ maxBytes: number,
8722
+ ): Promise<Uint8Array | undefined> {
8723
+ const validName = validateCoordinatePersistenceName(name);
8724
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
8725
+ const file = this.files.get(validName);
8726
+ if (file) {
8727
+ assertCoordinatePersistenceReadWithinLimit(
8728
+ validName,
8729
+ limit,
8730
+ file.byteLength,
8731
+ );
8732
+ }
8733
+ return file ? copyBytes(file) : undefined;
8734
+ }
8735
+
8589
8736
  async write(name: string, bytes: Uint8Array): Promise<void> {
8590
8737
  this.files.set(validateCoordinatePersistenceName(name), copyBytes(bytes));
8591
8738
  }
@@ -8616,6 +8763,10 @@ export class NativeBackboneNodeCoordinatePersistenceStore
8616
8763
  private readonly filePaths = new Map<string, string>();
8617
8764
  private directoryEnsured = false;
8618
8765
  private appendFailure: unknown;
8766
+ readonly readLimited?: (
8767
+ name: string,
8768
+ maxBytes: number,
8769
+ ) => Promise<Uint8Array | undefined>;
8619
8770
  readonly durableBarrier?: (name?: string) => Promise<void>;
8620
8771
 
8621
8772
  constructor(
@@ -8628,6 +8779,10 @@ export class NativeBackboneNodeCoordinatePersistenceStore
8628
8779
  if (!fs || typeof fs.open === "function") {
8629
8780
  this.durableBarrier = (name) => this.syncDurably(name);
8630
8781
  }
8782
+ if (!fs || typeof fs.openBoundedRead === "function") {
8783
+ this.readLimited = (name, maxBytes) =>
8784
+ this.readWithinLimit(name, maxBytes);
8785
+ }
8631
8786
  }
8632
8787
 
8633
8788
  private async nodeFs(): Promise<NativeBackboneNodeFs> {
@@ -8689,6 +8844,120 @@ export class NativeBackboneNodeCoordinatePersistenceStore
8689
8844
  }
8690
8845
  }
8691
8846
 
8847
+ private async readWithinLimit(
8848
+ name: string,
8849
+ maxBytes: number,
8850
+ ): Promise<Uint8Array | undefined> {
8851
+ const validName = validateCoordinatePersistenceName(name);
8852
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
8853
+ const fs = await this.nodeFs();
8854
+ const path = await this.filePath(validName);
8855
+ let handle: NativeBackboneNodeBoundedReadFileHandle | undefined;
8856
+ try {
8857
+ if (this.fs) {
8858
+ handle = await this.fs.openBoundedRead!(path);
8859
+ } else {
8860
+ if (!fs.open) {
8861
+ throw new Error(
8862
+ "Default Node coordinate persistence does not expose FileHandle.open",
8863
+ );
8864
+ }
8865
+ const opened = await fs.open(path, "r");
8866
+ if (!opened.stat || !opened.read) {
8867
+ await opened.close();
8868
+ throw new Error(
8869
+ "Default Node coordinate persistence bounded reads require FileHandle.stat and FileHandle.read",
8870
+ );
8871
+ }
8872
+ handle = opened as NativeBackboneNodeBoundedReadFileHandle;
8873
+ }
8874
+ } catch (error) {
8875
+ if (isNotFoundError(error)) {
8876
+ return undefined;
8877
+ }
8878
+ throw error;
8879
+ }
8880
+ try {
8881
+ const initial = await handle.stat({ bigint: true });
8882
+ if (typeof initial.size !== "bigint" || initial.size < 0n) {
8883
+ throw new Error(
8884
+ "Node coordinate persistence returned an invalid bigint file size",
8885
+ );
8886
+ }
8887
+ assertCoordinatePersistenceReadWithinLimit(
8888
+ validName,
8889
+ limit,
8890
+ initial.size,
8891
+ );
8892
+ if (initial.size > BigInt(Number.MAX_SAFE_INTEGER)) {
8893
+ throw new RangeError(
8894
+ "Node coordinate persistence file is too large to materialize safely",
8895
+ );
8896
+ }
8897
+ const byteLength = Number(initial.size);
8898
+ const bytes = new Uint8Array(byteLength);
8899
+ let offset = 0;
8900
+ while (offset < byteLength) {
8901
+ const { bytesRead } = await handle.read(
8902
+ bytes,
8903
+ offset,
8904
+ byteLength - offset,
8905
+ offset,
8906
+ );
8907
+ if (
8908
+ !Number.isSafeInteger(bytesRead) ||
8909
+ bytesRead <= 0 ||
8910
+ bytesRead > byteLength - offset
8911
+ ) {
8912
+ throw new Error(
8913
+ "Node coordinate persistence file changed during a bounded read",
8914
+ );
8915
+ }
8916
+ offset += bytesRead;
8917
+ }
8918
+ const growthProbe = new Uint8Array(1);
8919
+ const { bytesRead: growthBytes } = await handle.read(
8920
+ growthProbe,
8921
+ 0,
8922
+ 1,
8923
+ byteLength,
8924
+ );
8925
+ if (
8926
+ !Number.isSafeInteger(growthBytes) ||
8927
+ growthBytes < 0 ||
8928
+ growthBytes > 1
8929
+ ) {
8930
+ throw new Error(
8931
+ "Node coordinate persistence returned invalid bounded read progress",
8932
+ );
8933
+ }
8934
+ const final = await handle.stat({ bigint: true });
8935
+ if (typeof final.size !== "bigint" || final.size < 0n) {
8936
+ throw new Error(
8937
+ "Node coordinate persistence returned an invalid bigint file size",
8938
+ );
8939
+ }
8940
+ if (growthBytes !== 0 || final.size > initial.size) {
8941
+ assertCoordinatePersistenceReadWithinLimit(
8942
+ validName,
8943
+ limit,
8944
+ final.size > initial.size ? final.size : initial.size + 1n,
8945
+ );
8946
+ throw new Error(
8947
+ "Node coordinate persistence file changed during a bounded read",
8948
+ );
8949
+ }
8950
+ if (final.size !== initial.size) {
8951
+ throw new Error(
8952
+ "Node coordinate persistence file changed during a bounded read",
8953
+ );
8954
+ }
8955
+ return bytes;
8956
+ } finally {
8957
+ await handle.close();
8958
+ }
8959
+ }
8960
+
8692
8961
  async write(name: string, bytes: Uint8Array): Promise<void> {
8693
8962
  const fs = await this.ensureDirectory();
8694
8963
  const path = await this.filePath(name);
@@ -8889,6 +9158,38 @@ export class NativeBackboneOPFSCoordinatePersistenceStore
8889
9158
  }
8890
9159
  }
8891
9160
 
9161
+ async readLimited(
9162
+ name: string,
9163
+ maxBytes: number,
9164
+ ): Promise<Uint8Array | undefined> {
9165
+ const validName = validateCoordinatePersistenceName(name);
9166
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
9167
+ try {
9168
+ const handle = await this.directory.getFileHandle(validName, {
9169
+ create: false,
9170
+ });
9171
+ const file = await handle.getFile();
9172
+ if (!Number.isSafeInteger(file.size) || file.size < 0) {
9173
+ throw new Error(
9174
+ "OPFS coordinate persistence returned an invalid file size",
9175
+ );
9176
+ }
9177
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, file.size);
9178
+ const buffer = await file.arrayBuffer();
9179
+ if (buffer.byteLength !== file.size) {
9180
+ throw new Error(
9181
+ "OPFS coordinate persistence file changed during a bounded read",
9182
+ );
9183
+ }
9184
+ return new Uint8Array(buffer);
9185
+ } catch (error) {
9186
+ if (isNotFoundError(error)) {
9187
+ return undefined;
9188
+ }
9189
+ throw error;
9190
+ }
9191
+ }
9192
+
8892
9193
  async write(name: string, bytes: Uint8Array): Promise<void> {
8893
9194
  const handle = await this.directory.getFileHandle(
8894
9195
  validateCoordinatePersistenceName(name),
@@ -9010,6 +9311,10 @@ export class NativeBackboneBufferedCoordinatePersistenceStore
9010
9311
  {
9011
9312
  private readonly buffers = new Map<string, Uint8Array[]>();
9012
9313
  private bufferedBytes = 0;
9314
+ readonly readLimited?: (
9315
+ name: string,
9316
+ maxBytes: number,
9317
+ ) => Promise<Uint8Array | undefined>;
9013
9318
  readonly supportsRemoval: boolean;
9014
9319
  readonly durableBarrier?: (name?: string) => Promise<void>;
9015
9320
 
@@ -9019,6 +9324,11 @@ export class NativeBackboneBufferedCoordinatePersistenceStore
9019
9324
  ) {
9020
9325
  this.supportsRemoval =
9021
9326
  inner.supportsRemoval ?? typeof inner.remove === "function";
9327
+ if (typeof inner.readLimited === "function") {
9328
+ const innerReadLimited = inner.readLimited.bind(inner);
9329
+ this.readLimited = (name, maxBytes) =>
9330
+ this.readWithinLimit(name, maxBytes, innerReadLimited);
9331
+ }
9022
9332
  if (typeof inner.durableBarrier === "function") {
9023
9333
  this.durableBarrier = async (name) => {
9024
9334
  await this.flush(name);
@@ -9042,6 +9352,63 @@ export class NativeBackboneBufferedCoordinatePersistenceStore
9042
9352
  return this.inner.read(name);
9043
9353
  }
9044
9354
 
9355
+ private async readWithinLimit(
9356
+ name: string,
9357
+ maxBytes: number,
9358
+ innerReadLimited: (
9359
+ name: string,
9360
+ maxBytes: number,
9361
+ ) => Promise<Uint8Array | undefined>,
9362
+ ): Promise<Uint8Array | undefined> {
9363
+ const validName = validateCoordinatePersistenceName(name);
9364
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
9365
+ const pending = this.buffers.get(validName);
9366
+ if (!pending || pending.length === 0) {
9367
+ return innerReadLimited(validName, limit);
9368
+ }
9369
+ const pendingLength = pending.length;
9370
+ let pendingBytes = 0n;
9371
+ for (const chunk of pending) {
9372
+ pendingBytes += BigInt(chunk.byteLength);
9373
+ }
9374
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, pendingBytes);
9375
+ const remaining = limit - Number(pendingBytes);
9376
+ let existingBytes: number | undefined;
9377
+ try {
9378
+ existingBytes = (await innerReadLimited(validName, remaining))
9379
+ ?.byteLength;
9380
+ } catch (error) {
9381
+ if (error instanceof NativeBackboneCoordinatePersistenceReadLimitError) {
9382
+ throw new NativeBackboneCoordinatePersistenceReadLimitError(
9383
+ validName,
9384
+ limit,
9385
+ pendingBytes + error.observedBytes,
9386
+ );
9387
+ }
9388
+ throw error;
9389
+ }
9390
+ let confirmedPendingBytes = 0n;
9391
+ for (const chunk of pending) {
9392
+ confirmedPendingBytes += BigInt(chunk.byteLength);
9393
+ }
9394
+ if (
9395
+ this.buffers.get(validName) !== pending ||
9396
+ pending.length !== pendingLength ||
9397
+ confirmedPendingBytes !== pendingBytes
9398
+ ) {
9399
+ throw new Error(
9400
+ "Native backbone coordinate persistence pending bytes changed during a bounded read",
9401
+ );
9402
+ }
9403
+ assertCoordinatePersistenceReadWithinLimit(
9404
+ validName,
9405
+ limit,
9406
+ pendingBytes + BigInt(existingBytes ?? 0),
9407
+ );
9408
+ await this.flush(validName);
9409
+ return innerReadLimited(validName, limit);
9410
+ }
9411
+
9045
9412
  async write(name: string, bytes: Uint8Array): Promise<void> {
9046
9413
  await this.flush(name);
9047
9414
  await this.inner.write(name, bytes);
@@ -326,6 +326,30 @@ impl NativePeerbitBackbone {
326
326
  .entry_hash_numbers_in_range_u64(start1, end1, start2, end2)
327
327
  }
328
328
 
329
+ pub fn entry_hash_numbers_in_range_limited(
330
+ &self,
331
+ start1: String,
332
+ end1: String,
333
+ start2: String,
334
+ end2: String,
335
+ limit: u32,
336
+ ) -> Result<Array, JsValue> {
337
+ self.shared_log
338
+ .entry_hash_numbers_in_range_limited(start1, end1, start2, end2, limit)
339
+ }
340
+
341
+ pub fn entry_hash_numbers_in_range_u64_limited(
342
+ &self,
343
+ start1: String,
344
+ end1: String,
345
+ start2: String,
346
+ end2: String,
347
+ limit: u32,
348
+ ) -> Result<BigUint64Array, JsValue> {
349
+ self.shared_log
350
+ .entry_hash_numbers_in_range_u64_limited(start1, end1, start2, end2, limit)
351
+ }
352
+
329
353
  pub fn count_entry_coordinates_in_ranges(
330
354
  &self,
331
355
  start1: Array,