@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.d.ts +42 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +178 -0
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +307 -0
package/package.json
CHANGED
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);
|