@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/dist/src/index.js CHANGED
@@ -168,6 +168,19 @@ const nativeBackboneAppendProfileKeys = [
168
168
  "nativeBackboneRawReceiveGraphPutMs",
169
169
  "nativeBackboneRawReceiveCoordinateCommitMs",
170
170
  ];
171
+ export class NativeBackboneCoordinatePersistenceReadLimitError extends Error {
172
+ file;
173
+ maxBytes;
174
+ observedBytes;
175
+ code = "ERR_NATIVE_BACKBONE_COORDINATE_READ_LIMIT";
176
+ constructor(file, maxBytes, observedBytes) {
177
+ super(`Native backbone coordinate persistence file ${file} exceeds the ${maxBytes} byte read limit (${observedBytes.toString()} bytes)`);
178
+ this.file = file;
179
+ this.maxBytes = maxBytes;
180
+ this.observedBytes = observedBytes;
181
+ this.name = "NativeBackboneCoordinatePersistenceReadLimitError";
182
+ }
183
+ }
171
184
  const nativeBackboneCoordinatePersistenceFiles = {
172
185
  snapshot: "coordinates.bin",
173
186
  journal: "coordinates.wal",
@@ -280,6 +293,17 @@ const validateCoordinatePersistenceName = (name) => {
280
293
  }
281
294
  return name;
282
295
  };
296
+ const validateCoordinatePersistenceReadMaxBytes = (maxBytes) => {
297
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
298
+ throw new RangeError("Native backbone coordinate persistence read limit must be a non-negative safe integer");
299
+ }
300
+ return maxBytes;
301
+ };
302
+ const assertCoordinatePersistenceReadWithinLimit = (name, maxBytes, observedBytes) => {
303
+ if (BigInt(observedBytes) > BigInt(maxBytes)) {
304
+ throw new NativeBackboneCoordinatePersistenceReadLimitError(name, maxBytes, BigInt(observedBytes));
305
+ }
306
+ };
283
307
  const nativeBackboneCoordinateDropTombstoneBodyBytes = (body) => new TextEncoder().encode(JSON.stringify(body));
284
308
  const nativeBackboneCoordinateDropChecksum = (bytes) => {
285
309
  let checksum = 0xffffffff;
@@ -2961,6 +2985,15 @@ export class NativeBackboneMemoryCoordinatePersistenceStore {
2961
2985
  const file = this.files.get(validateCoordinatePersistenceName(name));
2962
2986
  return file ? copyBytes(file) : undefined;
2963
2987
  }
2988
+ async readLimited(name, maxBytes) {
2989
+ const validName = validateCoordinatePersistenceName(name);
2990
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
2991
+ const file = this.files.get(validName);
2992
+ if (file) {
2993
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, file.byteLength);
2994
+ }
2995
+ return file ? copyBytes(file) : undefined;
2996
+ }
2964
2997
  async write(name, bytes) {
2965
2998
  this.files.set(validateCoordinatePersistenceName(name), copyBytes(bytes));
2966
2999
  }
@@ -2981,6 +3014,7 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
2981
3014
  filePaths = new Map();
2982
3015
  directoryEnsured = false;
2983
3016
  appendFailure;
3017
+ readLimited;
2984
3018
  durableBarrier;
2985
3019
  constructor(directory, fs) {
2986
3020
  this.directory = directory;
@@ -2991,6 +3025,9 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
2991
3025
  if (!fs || typeof fs.open === "function") {
2992
3026
  this.durableBarrier = (name) => this.syncDurably(name);
2993
3027
  }
3028
+ if (!fs || typeof fs.openBoundedRead === "function") {
3029
+ this.readLimited = (name, maxBytes) => this.readWithinLimit(name, maxBytes);
3030
+ }
2994
3031
  }
2995
3032
  async nodeFs() {
2996
3033
  return this.fs ?? (await importNodeFs());
@@ -3043,6 +3080,79 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
3043
3080
  throw error;
3044
3081
  }
3045
3082
  }
3083
+ async readWithinLimit(name, maxBytes) {
3084
+ const validName = validateCoordinatePersistenceName(name);
3085
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
3086
+ const fs = await this.nodeFs();
3087
+ const path = await this.filePath(validName);
3088
+ let handle;
3089
+ try {
3090
+ if (this.fs) {
3091
+ handle = await this.fs.openBoundedRead(path);
3092
+ }
3093
+ else {
3094
+ if (!fs.open) {
3095
+ throw new Error("Default Node coordinate persistence does not expose FileHandle.open");
3096
+ }
3097
+ const opened = await fs.open(path, "r");
3098
+ if (!opened.stat || !opened.read) {
3099
+ await opened.close();
3100
+ throw new Error("Default Node coordinate persistence bounded reads require FileHandle.stat and FileHandle.read");
3101
+ }
3102
+ handle = opened;
3103
+ }
3104
+ }
3105
+ catch (error) {
3106
+ if (isNotFoundError(error)) {
3107
+ return undefined;
3108
+ }
3109
+ throw error;
3110
+ }
3111
+ try {
3112
+ const initial = await handle.stat({ bigint: true });
3113
+ if (typeof initial.size !== "bigint" || initial.size < 0n) {
3114
+ throw new Error("Node coordinate persistence returned an invalid bigint file size");
3115
+ }
3116
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, initial.size);
3117
+ if (initial.size > BigInt(Number.MAX_SAFE_INTEGER)) {
3118
+ throw new RangeError("Node coordinate persistence file is too large to materialize safely");
3119
+ }
3120
+ const byteLength = Number(initial.size);
3121
+ const bytes = new Uint8Array(byteLength);
3122
+ let offset = 0;
3123
+ while (offset < byteLength) {
3124
+ const { bytesRead } = await handle.read(bytes, offset, byteLength - offset, offset);
3125
+ if (!Number.isSafeInteger(bytesRead) ||
3126
+ bytesRead <= 0 ||
3127
+ bytesRead > byteLength - offset) {
3128
+ throw new Error("Node coordinate persistence file changed during a bounded read");
3129
+ }
3130
+ offset += bytesRead;
3131
+ }
3132
+ const growthProbe = new Uint8Array(1);
3133
+ const { bytesRead: growthBytes } = await handle.read(growthProbe, 0, 1, byteLength);
3134
+ if (!Number.isSafeInteger(growthBytes) ||
3135
+ growthBytes < 0 ||
3136
+ growthBytes > 1) {
3137
+ throw new Error("Node coordinate persistence returned invalid bounded read progress");
3138
+ }
3139
+ const final = await handle.stat({ bigint: true });
3140
+ if (typeof final.size !== "bigint" || final.size < 0n) {
3141
+ throw new Error("Node coordinate persistence returned an invalid bigint file size");
3142
+ }
3143
+ if (growthBytes !== 0 || final.size > initial.size) {
3144
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, final.size > initial.size ? final.size : initial.size + 1n);
3145
+ throw new Error("Node coordinate persistence file changed during a bounded read");
3146
+ }
3147
+ if (final.size !== initial.size) {
3148
+ throw new Error("Node coordinate persistence file changed during a bounded read");
3149
+ }
3150
+ return bytes;
3151
+ }
3152
+ finally {
3153
+ await handle.close();
3154
+ }
3155
+ }
3046
3156
  async write(name, bytes) {
3047
3157
  const fs = await this.ensureDirectory();
3048
3158
  const path = await this.filePath(name);
@@ -3214,6 +3324,31 @@ export class NativeBackboneOPFSCoordinatePersistenceStore {
3214
3324
  throw error;
3215
3325
  }
3216
3326
  }
3327
+ async readLimited(name, maxBytes) {
3328
+ const validName = validateCoordinatePersistenceName(name);
3329
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
3330
+ try {
3331
+ const handle = await this.directory.getFileHandle(validName, {
3332
+ create: false,
3333
+ });
3334
+ const file = await handle.getFile();
3335
+ if (!Number.isSafeInteger(file.size) || file.size < 0) {
3336
+ throw new Error("OPFS coordinate persistence returned an invalid file size");
3337
+ }
3338
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, file.size);
3339
+ const buffer = await file.arrayBuffer();
3340
+ if (buffer.byteLength !== file.size) {
3341
+ throw new Error("OPFS coordinate persistence file changed during a bounded read");
3342
+ }
3343
+ return new Uint8Array(buffer);
3344
+ }
3345
+ catch (error) {
3346
+ if (isNotFoundError(error)) {
3347
+ return undefined;
3348
+ }
3349
+ throw error;
3350
+ }
3351
+ }
3217
3352
  async write(name, bytes) {
3218
3353
  const handle = await this.directory.getFileHandle(validateCoordinatePersistenceName(name), { create: true });
3219
3354
  const writable = await handle.createWritable();
@@ -3320,6 +3455,7 @@ export class NativeBackboneBufferedCoordinatePersistenceStore {
3320
3455
  options;
3321
3456
  buffers = new Map();
3322
3457
  bufferedBytes = 0;
3458
+ readLimited;
3323
3459
  supportsRemoval;
3324
3460
  durableBarrier;
3325
3461
  constructor(inner, options = {}) {
@@ -3327,6 +3463,10 @@ export class NativeBackboneBufferedCoordinatePersistenceStore {
3327
3463
  this.options = options;
3328
3464
  this.supportsRemoval =
3329
3465
  inner.supportsRemoval ?? typeof inner.remove === "function";
3466
+ if (typeof inner.readLimited === "function") {
3467
+ const innerReadLimited = inner.readLimited.bind(inner);
3468
+ this.readLimited = (name, maxBytes) => this.readWithinLimit(name, maxBytes, innerReadLimited);
3469
+ }
3330
3470
  if (typeof inner.durableBarrier === "function") {
3331
3471
  this.durableBarrier = async (name) => {
3332
3472
  await this.flush(name);
@@ -3347,6 +3487,44 @@ export class NativeBackboneBufferedCoordinatePersistenceStore {
3347
3487
  await this.flush(name);
3348
3488
  return this.inner.read(name);
3349
3489
  }
3490
+ async readWithinLimit(name, maxBytes, innerReadLimited) {
3491
+ const validName = validateCoordinatePersistenceName(name);
3492
+ const limit = validateCoordinatePersistenceReadMaxBytes(maxBytes);
3493
+ const pending = this.buffers.get(validName);
3494
+ if (!pending || pending.length === 0) {
3495
+ return innerReadLimited(validName, limit);
3496
+ }
3497
+ const pendingLength = pending.length;
3498
+ let pendingBytes = 0n;
3499
+ for (const chunk of pending) {
3500
+ pendingBytes += BigInt(chunk.byteLength);
3501
+ }
3502
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, pendingBytes);
3503
+ const remaining = limit - Number(pendingBytes);
3504
+ let existingBytes;
3505
+ try {
3506
+ existingBytes = (await innerReadLimited(validName, remaining))
3507
+ ?.byteLength;
3508
+ }
3509
+ catch (error) {
3510
+ if (error instanceof NativeBackboneCoordinatePersistenceReadLimitError) {
3511
+ throw new NativeBackboneCoordinatePersistenceReadLimitError(validName, limit, pendingBytes + error.observedBytes);
3512
+ }
3513
+ throw error;
3514
+ }
3515
+ let confirmedPendingBytes = 0n;
3516
+ for (const chunk of pending) {
3517
+ confirmedPendingBytes += BigInt(chunk.byteLength);
3518
+ }
3519
+ if (this.buffers.get(validName) !== pending ||
3520
+ pending.length !== pendingLength ||
3521
+ confirmedPendingBytes !== pendingBytes) {
3522
+ throw new Error("Native backbone coordinate persistence pending bytes changed during a bounded read");
3523
+ }
3524
+ assertCoordinatePersistenceReadWithinLimit(validName, limit, pendingBytes + BigInt(existingBytes ?? 0));
3525
+ await this.flush(validName);
3526
+ return innerReadLimited(validName, limit);
3527
+ }
3350
3528
  async write(name, bytes) {
3351
3529
  await this.flush(name);
3352
3530
  await this.inner.write(name, bytes);