@peerbit/native-backbone 0.2.8 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/native-backbone",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Native transaction backbone for Peerbit",
5
5
  "sideEffects": [
6
6
  "./dist/wasm/native_backbone.js"
package/src/index.ts CHANGED
@@ -2973,6 +2973,14 @@ export type NativeBackboneCoordinatePersistenceOptions =
2973
2973
 
2974
2974
  export type NativeBackboneCoordinatePersistenceStore = {
2975
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>;
2976
2984
  write(name: string, bytes: Uint8Array): Promise<void>;
2977
2985
  /**
2978
2986
  * Appends `bytes` to the named file. Callers hand over ownership of
@@ -2991,6 +2999,21 @@ export type NativeBackboneCoordinatePersistenceStore = {
2991
2999
  close?(options?: { flush?: boolean }): Promise<void>;
2992
3000
  };
2993
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
+
2994
3017
  export type NativeBackboneCoordinatePersistenceAdapter = {
2995
3018
  /**
2996
3019
  * Optional durable capability used by higher layers for atomic operation
@@ -3154,6 +3177,10 @@ type NativeBackboneNodeFs = {
3154
3177
  readFile(path: string): Promise<Uint8Array>;
3155
3178
  writeFile(path: string, data: Uint8Array): Promise<unknown>;
3156
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>;
3157
3184
  open?(
3158
3185
  path: string,
3159
3186
  flags: string,
@@ -3163,10 +3190,28 @@ type NativeBackboneNodeFs = {
3163
3190
 
3164
3191
  type NativeBackboneNodeAppendFileHandle = {
3165
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 }>;
3166
3200
  sync?(): Promise<unknown>;
3167
3201
  close(): Promise<unknown>;
3168
3202
  };
3169
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
+
3170
3215
  type NativeBackboneOPFSFile = {
3171
3216
  arrayBuffer(): Promise<ArrayBuffer>;
3172
3217
  size: number;
@@ -3274,6 +3319,31 @@ const validateCoordinatePersistenceName = (name: string): string => {
3274
3319
  return name;
3275
3320
  };
3276
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
+
3277
3347
  type NativeBackboneCoordinateDropTombstoneBody = {
3278
3348
  format: "peerbit-native-backbone-coordinate-drop";
3279
3349
  version: 1;
@@ -8646,6 +8716,23 @@ export class NativeBackboneMemoryCoordinatePersistenceStore
8646
8716
  return file ? copyBytes(file) : undefined;
8647
8717
  }
8648
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
+
8649
8736
  async write(name: string, bytes: Uint8Array): Promise<void> {
8650
8737
  this.files.set(validateCoordinatePersistenceName(name), copyBytes(bytes));
8651
8738
  }
@@ -8676,6 +8763,10 @@ export class NativeBackboneNodeCoordinatePersistenceStore
8676
8763
  private readonly filePaths = new Map<string, string>();
8677
8764
  private directoryEnsured = false;
8678
8765
  private appendFailure: unknown;
8766
+ readonly readLimited?: (
8767
+ name: string,
8768
+ maxBytes: number,
8769
+ ) => Promise<Uint8Array | undefined>;
8679
8770
  readonly durableBarrier?: (name?: string) => Promise<void>;
8680
8771
 
8681
8772
  constructor(
@@ -8688,6 +8779,10 @@ export class NativeBackboneNodeCoordinatePersistenceStore
8688
8779
  if (!fs || typeof fs.open === "function") {
8689
8780
  this.durableBarrier = (name) => this.syncDurably(name);
8690
8781
  }
8782
+ if (!fs || typeof fs.openBoundedRead === "function") {
8783
+ this.readLimited = (name, maxBytes) =>
8784
+ this.readWithinLimit(name, maxBytes);
8785
+ }
8691
8786
  }
8692
8787
 
8693
8788
  private async nodeFs(): Promise<NativeBackboneNodeFs> {
@@ -8749,6 +8844,120 @@ export class NativeBackboneNodeCoordinatePersistenceStore
8749
8844
  }
8750
8845
  }
8751
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
+
8752
8961
  async write(name: string, bytes: Uint8Array): Promise<void> {
8753
8962
  const fs = await this.ensureDirectory();
8754
8963
  const path = await this.filePath(name);
@@ -8949,6 +9158,38 @@ export class NativeBackboneOPFSCoordinatePersistenceStore
8949
9158
  }
8950
9159
  }
8951
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
+
8952
9193
  async write(name: string, bytes: Uint8Array): Promise<void> {
8953
9194
  const handle = await this.directory.getFileHandle(
8954
9195
  validateCoordinatePersistenceName(name),
@@ -9070,6 +9311,10 @@ export class NativeBackboneBufferedCoordinatePersistenceStore
9070
9311
  {
9071
9312
  private readonly buffers = new Map<string, Uint8Array[]>();
9072
9313
  private bufferedBytes = 0;
9314
+ readonly readLimited?: (
9315
+ name: string,
9316
+ maxBytes: number,
9317
+ ) => Promise<Uint8Array | undefined>;
9073
9318
  readonly supportsRemoval: boolean;
9074
9319
  readonly durableBarrier?: (name?: string) => Promise<void>;
9075
9320
 
@@ -9079,6 +9324,11 @@ export class NativeBackboneBufferedCoordinatePersistenceStore
9079
9324
  ) {
9080
9325
  this.supportsRemoval =
9081
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
+ }
9082
9332
  if (typeof inner.durableBarrier === "function") {
9083
9333
  this.durableBarrier = async (name) => {
9084
9334
  await this.flush(name);
@@ -9102,6 +9352,63 @@ export class NativeBackboneBufferedCoordinatePersistenceStore
9102
9352
  return this.inner.read(name);
9103
9353
  }
9104
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
+
9105
9412
  async write(name: string, bytes: Uint8Array): Promise<void> {
9106
9413
  await this.flush(name);
9107
9414
  await this.inner.write(name, bytes);