@peerbit/please-lib 2.0.1 → 2.0.3
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/lib/esm/index.d.ts +54 -25
- package/lib/esm/index.js +409 -120
- package/lib/esm/index.js.map +1 -1
- package/package.json +15 -6
- package/src/index.ts +566 -162
package/lib/esm/index.js
CHANGED
|
@@ -32,17 +32,51 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
32
32
|
}
|
|
33
33
|
return useValue ? value : void 0;
|
|
34
34
|
};
|
|
35
|
-
import { field, variant,
|
|
35
|
+
import { field, variant, option } from "@dao-xyz/borsh";
|
|
36
36
|
import { Program } from "@peerbit/program";
|
|
37
|
-
import { Documents, SearchRequest, StringMatch, StringMatchMethod,
|
|
38
|
-
import { sha256Base64Sync, randomBytes } from "@peerbit/crypto";
|
|
37
|
+
import { Documents, SearchRequest, StringMatch, StringMatchMethod, IsNull, } from "@peerbit/document";
|
|
38
|
+
import { sha256Base64Sync, randomBytes, toBase64, toBase64URL, } from "@peerbit/crypto";
|
|
39
39
|
import { concat } from "uint8arrays";
|
|
40
40
|
import { sha256Sync } from "@peerbit/crypto";
|
|
41
41
|
import { TrustedNetwork } from "@peerbit/trusted-network";
|
|
42
|
-
import
|
|
42
|
+
import { SHA256 } from "@stablelib/sha256";
|
|
43
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
const isRetryableChunkLookupError = (error) => error instanceof Error &&
|
|
45
|
+
(error.name === "AbortError" ||
|
|
46
|
+
error.message.includes("fanout channel closed"));
|
|
43
47
|
export class AbstractFile {
|
|
48
|
+
async getFile(files, properties) {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
for await (const chunk of this.streamFile(files, properties)) {
|
|
51
|
+
chunks.push(chunk);
|
|
52
|
+
}
|
|
53
|
+
return (properties?.as === "chunks" ? chunks : concat(chunks));
|
|
54
|
+
}
|
|
55
|
+
async writeFile(files, writable, properties) {
|
|
56
|
+
try {
|
|
57
|
+
for await (const chunk of this.streamFile(files, properties)) {
|
|
58
|
+
await writable.write(chunk);
|
|
59
|
+
}
|
|
60
|
+
await writable.close?.();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (writable.abort) {
|
|
64
|
+
try {
|
|
65
|
+
await writable.abort(error);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Ignore writable cleanup failures.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
44
74
|
}
|
|
45
75
|
let IndexableFile = (() => {
|
|
76
|
+
let _classDecorators = [variant("files_indexable_file")];
|
|
77
|
+
let _classDescriptor;
|
|
78
|
+
let _classExtraInitializers = [];
|
|
79
|
+
let _classThis;
|
|
46
80
|
let _id_decorators;
|
|
47
81
|
let _id_initializers = [];
|
|
48
82
|
let _id_extraInitializers = [];
|
|
@@ -55,18 +89,22 @@ let IndexableFile = (() => {
|
|
|
55
89
|
let _parentId_decorators;
|
|
56
90
|
let _parentId_initializers = [];
|
|
57
91
|
let _parentId_extraInitializers = [];
|
|
58
|
-
|
|
92
|
+
var IndexableFile = class {
|
|
93
|
+
static { _classThis = this; }
|
|
59
94
|
static {
|
|
60
95
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
61
96
|
_id_decorators = [field({ type: "string" })];
|
|
62
97
|
_name_decorators = [field({ type: "string" })];
|
|
63
|
-
_size_decorators = [field({ type: "
|
|
98
|
+
_size_decorators = [field({ type: "u64" })];
|
|
64
99
|
_parentId_decorators = [field({ type: option("string") })];
|
|
65
100
|
__esDecorate(null, null, _id_decorators, { kind: "field", name: "id", static: false, private: false, access: { has: obj => "id" in obj, get: obj => obj.id, set: (obj, value) => { obj.id = value; } }, metadata: _metadata }, _id_initializers, _id_extraInitializers);
|
|
66
101
|
__esDecorate(null, null, _name_decorators, { kind: "field", name: "name", static: false, private: false, access: { has: obj => "name" in obj, get: obj => obj.name, set: (obj, value) => { obj.name = value; } }, metadata: _metadata }, _name_initializers, _name_extraInitializers);
|
|
67
102
|
__esDecorate(null, null, _size_decorators, { kind: "field", name: "size", static: false, private: false, access: { has: obj => "size" in obj, get: obj => obj.size, set: (obj, value) => { obj.size = value; } }, metadata: _metadata }, _size_initializers, _size_extraInitializers);
|
|
68
103
|
__esDecorate(null, null, _parentId_decorators, { kind: "field", name: "parentId", static: false, private: false, access: { has: obj => "parentId" in obj, get: obj => obj.parentId, set: (obj, value) => { obj.parentId = value; } }, metadata: _metadata }, _parentId_initializers, _parentId_extraInitializers);
|
|
69
|
-
|
|
104
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
105
|
+
IndexableFile = _classThis = _classDescriptor.value;
|
|
106
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
107
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
70
108
|
}
|
|
71
109
|
id = __runInitializers(this, _id_initializers, void 0);
|
|
72
110
|
name = (__runInitializers(this, _id_extraInitializers), __runInitializers(this, _name_initializers, void 0));
|
|
@@ -80,9 +118,32 @@ let IndexableFile = (() => {
|
|
|
80
118
|
this.parentId = file.parentId;
|
|
81
119
|
}
|
|
82
120
|
};
|
|
121
|
+
return IndexableFile = _classThis;
|
|
83
122
|
})();
|
|
84
123
|
export { IndexableFile };
|
|
85
124
|
const TINY_FILE_SIZE_LIMIT = 5 * 1e6; // 6mb
|
|
125
|
+
const LARGE_FILE_SEGMENT_SIZE = TINY_FILE_SIZE_LIMIT / 10;
|
|
126
|
+
const LARGE_FILE_TARGET_CHUNK_COUNT = 1024;
|
|
127
|
+
const CHUNK_SIZE_GRANULARITY = 64 * 1024;
|
|
128
|
+
const MAX_LARGE_FILE_SEGMENT_SIZE = TINY_FILE_SIZE_LIMIT - 256 * 1024;
|
|
129
|
+
const LARGE_FILE_CHUNK_LOOKUP_TIMEOUT_MS = 5 * 60 * 1000;
|
|
130
|
+
const TINY_FILE_SIZE_LIMIT_BIGINT = BigInt(TINY_FILE_SIZE_LIMIT);
|
|
131
|
+
const roundUpTo = (value, multiple) => Math.ceil(value / multiple) * multiple;
|
|
132
|
+
const getLargeFileSegmentSize = (size) => Math.min(MAX_LARGE_FILE_SEGMENT_SIZE, roundUpTo(Math.max(LARGE_FILE_SEGMENT_SIZE, Math.ceil(Number(size) / LARGE_FILE_TARGET_CHUNK_COUNT)), CHUNK_SIZE_GRANULARITY));
|
|
133
|
+
const getChunkStart = (index, chunkSize = LARGE_FILE_SEGMENT_SIZE) => index * chunkSize;
|
|
134
|
+
const getChunkEnd = (index, size, chunkSize = LARGE_FILE_SEGMENT_SIZE) => Math.min((index + 1) * chunkSize, Number(size));
|
|
135
|
+
const getChunkCount = (size, chunkSize = LARGE_FILE_SEGMENT_SIZE) => Math.ceil(Number(size) / chunkSize);
|
|
136
|
+
const getChunkId = (parentId, index) => `${parentId}:${index}`;
|
|
137
|
+
const createUploadId = () => toBase64URL(randomBytes(16));
|
|
138
|
+
const isBlobLike = (value) => typeof Blob !== "undefined" && value instanceof Blob;
|
|
139
|
+
const readBlobChunk = async (blob, index, chunkSize) => new Uint8Array(await blob
|
|
140
|
+
.slice(getChunkStart(index, chunkSize), getChunkEnd(index, blob.size, chunkSize))
|
|
141
|
+
.arrayBuffer());
|
|
142
|
+
const ensureSourceSize = (actual, expected) => {
|
|
143
|
+
if (actual !== expected) {
|
|
144
|
+
throw new Error(`Source size changed during upload. Expected ${expected} bytes, got ${actual}`);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
86
147
|
let TinyFile = (() => {
|
|
87
148
|
let _classDecorators = [variant(0)];
|
|
88
149
|
let _classDescriptor;
|
|
@@ -98,9 +159,15 @@ let TinyFile = (() => {
|
|
|
98
159
|
let _file_decorators;
|
|
99
160
|
let _file_initializers = [];
|
|
100
161
|
let _file_extraInitializers = [];
|
|
162
|
+
let _hash_decorators;
|
|
163
|
+
let _hash_initializers = [];
|
|
164
|
+
let _hash_extraInitializers = [];
|
|
101
165
|
let _parentId_decorators;
|
|
102
166
|
let _parentId_initializers = [];
|
|
103
167
|
let _parentId_extraInitializers = [];
|
|
168
|
+
let _index_decorators;
|
|
169
|
+
let _index_initializers = [];
|
|
170
|
+
let _index_extraInitializers = [];
|
|
104
171
|
var TinyFile = class extends _classSuper {
|
|
105
172
|
static { _classThis = this; }
|
|
106
173
|
static {
|
|
@@ -108,11 +175,15 @@ let TinyFile = (() => {
|
|
|
108
175
|
_id_decorators = [field({ type: "string" })];
|
|
109
176
|
_name_decorators = [field({ type: "string" })];
|
|
110
177
|
_file_decorators = [field({ type: Uint8Array })];
|
|
178
|
+
_hash_decorators = [field({ type: "string" })];
|
|
111
179
|
_parentId_decorators = [field({ type: option("string") })];
|
|
180
|
+
_index_decorators = [field({ type: option("u32") })];
|
|
112
181
|
__esDecorate(null, null, _id_decorators, { kind: "field", name: "id", static: false, private: false, access: { has: obj => "id" in obj, get: obj => obj.id, set: (obj, value) => { obj.id = value; } }, metadata: _metadata }, _id_initializers, _id_extraInitializers);
|
|
113
182
|
__esDecorate(null, null, _name_decorators, { kind: "field", name: "name", static: false, private: false, access: { has: obj => "name" in obj, get: obj => obj.name, set: (obj, value) => { obj.name = value; } }, metadata: _metadata }, _name_initializers, _name_extraInitializers);
|
|
114
183
|
__esDecorate(null, null, _file_decorators, { kind: "field", name: "file", static: false, private: false, access: { has: obj => "file" in obj, get: obj => obj.file, set: (obj, value) => { obj.file = value; } }, metadata: _metadata }, _file_initializers, _file_extraInitializers);
|
|
184
|
+
__esDecorate(null, null, _hash_decorators, { kind: "field", name: "hash", static: false, private: false, access: { has: obj => "hash" in obj, get: obj => obj.hash, set: (obj, value) => { obj.hash = value; } }, metadata: _metadata }, _hash_initializers, _hash_extraInitializers);
|
|
115
185
|
__esDecorate(null, null, _parentId_decorators, { kind: "field", name: "parentId", static: false, private: false, access: { has: obj => "parentId" in obj, get: obj => obj.parentId, set: (obj, value) => { obj.parentId = value; } }, metadata: _metadata }, _parentId_initializers, _parentId_extraInitializers);
|
|
186
|
+
__esDecorate(null, null, _index_decorators, { kind: "field", name: "index", static: false, private: false, access: { has: obj => "index" in obj, get: obj => obj.index, set: (obj, value) => { obj.index = value; } }, metadata: _metadata }, _index_initializers, _index_extraInitializers);
|
|
116
187
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
117
188
|
TinyFile = _classThis = _classDescriptor.value;
|
|
118
189
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
@@ -121,24 +192,32 @@ let TinyFile = (() => {
|
|
|
121
192
|
id = __runInitializers(this, _id_initializers, void 0);
|
|
122
193
|
name = (__runInitializers(this, _id_extraInitializers), __runInitializers(this, _name_initializers, void 0));
|
|
123
194
|
file = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _file_initializers, void 0)); // 10 mb imit
|
|
124
|
-
|
|
195
|
+
hash = (__runInitializers(this, _file_extraInitializers), __runInitializers(this, _hash_initializers, void 0));
|
|
196
|
+
parentId = (__runInitializers(this, _hash_extraInitializers), __runInitializers(this, _parentId_initializers, void 0));
|
|
197
|
+
index = (__runInitializers(this, _parentId_extraInitializers), __runInitializers(this, _index_initializers, void 0));
|
|
125
198
|
get size() {
|
|
126
|
-
return this.file.byteLength;
|
|
199
|
+
return BigInt(this.file.byteLength);
|
|
127
200
|
}
|
|
128
201
|
constructor(properties) {
|
|
129
202
|
super();
|
|
130
|
-
__runInitializers(this,
|
|
131
|
-
this.
|
|
203
|
+
__runInitializers(this, _index_extraInitializers);
|
|
204
|
+
this.parentId = properties.parentId;
|
|
205
|
+
this.index = properties.index;
|
|
206
|
+
this.id =
|
|
207
|
+
properties.id ||
|
|
208
|
+
(properties.parentId != null && properties.index != null
|
|
209
|
+
? `${properties.parentId}:${properties.index}`
|
|
210
|
+
: sha256Base64Sync(properties.file));
|
|
132
211
|
this.name = properties.name;
|
|
133
212
|
this.file = properties.file;
|
|
134
|
-
this.
|
|
213
|
+
this.hash = properties.hash || sha256Base64Sync(properties.file);
|
|
135
214
|
}
|
|
136
|
-
async
|
|
137
|
-
if (sha256Base64Sync(this.file) !== this.
|
|
215
|
+
async *streamFile(_files, properties) {
|
|
216
|
+
if (sha256Base64Sync(this.file) !== this.hash) {
|
|
138
217
|
throw new Error("Hash does not match the file content");
|
|
139
218
|
}
|
|
140
219
|
properties?.progress?.(1);
|
|
141
|
-
|
|
220
|
+
yield this.file;
|
|
142
221
|
}
|
|
143
222
|
async delete() {
|
|
144
223
|
// Do nothing, since no releated files where created
|
|
@@ -159,135 +238,165 @@ let LargeFile = (() => {
|
|
|
159
238
|
let _name_decorators;
|
|
160
239
|
let _name_initializers = [];
|
|
161
240
|
let _name_extraInitializers = [];
|
|
162
|
-
let _fileIds_decorators;
|
|
163
|
-
let _fileIds_initializers = [];
|
|
164
|
-
let _fileIds_extraInitializers = [];
|
|
165
241
|
let _size_decorators;
|
|
166
242
|
let _size_initializers = [];
|
|
167
243
|
let _size_extraInitializers = [];
|
|
244
|
+
let _chunkCount_decorators;
|
|
245
|
+
let _chunkCount_initializers = [];
|
|
246
|
+
let _chunkCount_extraInitializers = [];
|
|
247
|
+
let _ready_decorators;
|
|
248
|
+
let _ready_initializers = [];
|
|
249
|
+
let _ready_extraInitializers = [];
|
|
250
|
+
let _finalHash_decorators;
|
|
251
|
+
let _finalHash_initializers = [];
|
|
252
|
+
let _finalHash_extraInitializers = [];
|
|
168
253
|
var LargeFile = class extends _classSuper {
|
|
169
254
|
static { _classThis = this; }
|
|
170
255
|
static {
|
|
171
256
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
172
257
|
_id_decorators = [field({ type: "string" })];
|
|
173
258
|
_name_decorators = [field({ type: "string" })];
|
|
174
|
-
|
|
175
|
-
|
|
259
|
+
_size_decorators = [field({ type: "u64" })];
|
|
260
|
+
_chunkCount_decorators = [field({ type: "u32" })];
|
|
261
|
+
_ready_decorators = [field({ type: "bool" })];
|
|
262
|
+
_finalHash_decorators = [field({ type: option("string") })];
|
|
176
263
|
__esDecorate(null, null, _id_decorators, { kind: "field", name: "id", static: false, private: false, access: { has: obj => "id" in obj, get: obj => obj.id, set: (obj, value) => { obj.id = value; } }, metadata: _metadata }, _id_initializers, _id_extraInitializers);
|
|
177
264
|
__esDecorate(null, null, _name_decorators, { kind: "field", name: "name", static: false, private: false, access: { has: obj => "name" in obj, get: obj => obj.name, set: (obj, value) => { obj.name = value; } }, metadata: _metadata }, _name_initializers, _name_extraInitializers);
|
|
178
|
-
__esDecorate(null, null, _fileIds_decorators, { kind: "field", name: "fileIds", static: false, private: false, access: { has: obj => "fileIds" in obj, get: obj => obj.fileIds, set: (obj, value) => { obj.fileIds = value; } }, metadata: _metadata }, _fileIds_initializers, _fileIds_extraInitializers);
|
|
179
265
|
__esDecorate(null, null, _size_decorators, { kind: "field", name: "size", static: false, private: false, access: { has: obj => "size" in obj, get: obj => obj.size, set: (obj, value) => { obj.size = value; } }, metadata: _metadata }, _size_initializers, _size_extraInitializers);
|
|
266
|
+
__esDecorate(null, null, _chunkCount_decorators, { kind: "field", name: "chunkCount", static: false, private: false, access: { has: obj => "chunkCount" in obj, get: obj => obj.chunkCount, set: (obj, value) => { obj.chunkCount = value; } }, metadata: _metadata }, _chunkCount_initializers, _chunkCount_extraInitializers);
|
|
267
|
+
__esDecorate(null, null, _ready_decorators, { kind: "field", name: "ready", static: false, private: false, access: { has: obj => "ready" in obj, get: obj => obj.ready, set: (obj, value) => { obj.ready = value; } }, metadata: _metadata }, _ready_initializers, _ready_extraInitializers);
|
|
268
|
+
__esDecorate(null, null, _finalHash_decorators, { kind: "field", name: "finalHash", static: false, private: false, access: { has: obj => "finalHash" in obj, get: obj => obj.finalHash, set: (obj, value) => { obj.finalHash = value; } }, metadata: _metadata }, _finalHash_initializers, _finalHash_extraInitializers);
|
|
180
269
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
181
270
|
LargeFile = _classThis = _classDescriptor.value;
|
|
182
271
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
183
272
|
__runInitializers(_classThis, _classExtraInitializers);
|
|
184
273
|
}
|
|
185
|
-
id = __runInitializers(this, _id_initializers, void 0);
|
|
274
|
+
id = __runInitializers(this, _id_initializers, void 0);
|
|
186
275
|
name = (__runInitializers(this, _id_extraInitializers), __runInitializers(this, _name_initializers, void 0));
|
|
187
|
-
|
|
188
|
-
|
|
276
|
+
size = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _size_initializers, void 0));
|
|
277
|
+
chunkCount = (__runInitializers(this, _size_extraInitializers), __runInitializers(this, _chunkCount_initializers, void 0));
|
|
278
|
+
ready = (__runInitializers(this, _chunkCount_extraInitializers), __runInitializers(this, _ready_initializers, void 0));
|
|
279
|
+
finalHash = (__runInitializers(this, _ready_extraInitializers), __runInitializers(this, _finalHash_initializers, void 0));
|
|
189
280
|
constructor(properties) {
|
|
190
281
|
super();
|
|
191
|
-
__runInitializers(this,
|
|
192
|
-
this.id = properties.id;
|
|
282
|
+
__runInitializers(this, _finalHash_extraInitializers);
|
|
283
|
+
this.id = properties.id || createUploadId();
|
|
193
284
|
this.name = properties.name;
|
|
194
|
-
this.fileIds = properties.fileIds;
|
|
195
285
|
this.size = properties.size;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const fileIds = [];
|
|
200
|
-
const id = sha256Base64Sync(file);
|
|
201
|
-
const fileSize = file.byteLength;
|
|
202
|
-
progress?.(0);
|
|
203
|
-
const end = Math.ceil(file.byteLength / segmetSize);
|
|
204
|
-
for (let i = 0; i < end; i++) {
|
|
205
|
-
progress?.((i + 1) / end);
|
|
206
|
-
fileIds.push(await files.add(name + "/" + i, file.subarray(i * segmetSize, Math.min((i + 1) * segmetSize, file.byteLength)), id));
|
|
207
|
-
}
|
|
208
|
-
progress?.(1);
|
|
209
|
-
return new LargeFile({ id, name, fileIds: fileIds, size: fileSize });
|
|
286
|
+
this.chunkCount = properties.chunkCount;
|
|
287
|
+
this.ready = properties.ready ?? false;
|
|
288
|
+
this.finalHash = properties.finalHash;
|
|
210
289
|
}
|
|
211
290
|
get parentId() {
|
|
212
291
|
// Large file can never have a parent
|
|
213
292
|
return undefined;
|
|
214
293
|
}
|
|
215
|
-
async fetchChunks(files) {
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
294
|
+
async fetchChunks(files, properties) {
|
|
295
|
+
const chunks = new Map();
|
|
296
|
+
const totalTimeout = properties?.timeout ?? 30_000;
|
|
297
|
+
const deadline = Date.now() + totalTimeout;
|
|
298
|
+
const queryTimeout = Math.min(totalTimeout, 5_000);
|
|
299
|
+
const searchOptions = {
|
|
300
|
+
local: true,
|
|
301
|
+
remote: {
|
|
302
|
+
timeout: queryTimeout,
|
|
303
|
+
throwOnMissing: false,
|
|
304
|
+
// Chunk queries return the full TinyFile document including its
|
|
305
|
+
// bytes. Observer reads can stream that result directly, while
|
|
306
|
+
// actual replicators should still persist downloaded chunks.
|
|
307
|
+
replicate: files.persistChunkReads,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
const recordChunks = (results) => {
|
|
311
|
+
for (const chunk of results) {
|
|
312
|
+
if (chunk instanceof TinyFile &&
|
|
313
|
+
chunk.parentId === this.id &&
|
|
314
|
+
chunk.index != null &&
|
|
315
|
+
!chunks.has(chunk.index)) {
|
|
316
|
+
chunks.set(chunk.index, chunk);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
while (chunks.size < this.chunkCount && Date.now() < deadline) {
|
|
321
|
+
const before = chunks.size;
|
|
322
|
+
recordChunks(await files.files.index.search(new SearchRequest({
|
|
323
|
+
query: new StringMatch({ key: "parentId", value: this.id }),
|
|
324
|
+
fetch: 0xffffffff,
|
|
325
|
+
}), searchOptions));
|
|
326
|
+
if (chunks.size === before && chunks.size < this.chunkCount) {
|
|
327
|
+
await sleep(250);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return [...chunks.values()].sort((a, b) => (a.index || 0) - (b.index || 0));
|
|
224
331
|
}
|
|
225
332
|
async delete(files) {
|
|
226
|
-
await Promise.all((await this.fetchChunks(files)).map((x) => x.
|
|
333
|
+
await Promise.all((await this.fetchChunks(files)).map((x) => files.files.del(x.id)));
|
|
227
334
|
}
|
|
228
|
-
async
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
335
|
+
async resolveChunk(files, index, knownChunks, properties) {
|
|
336
|
+
const totalTimeout = properties?.timeout ?? LARGE_FILE_CHUNK_LOOKUP_TIMEOUT_MS;
|
|
337
|
+
const deadline = Date.now() + totalTimeout;
|
|
338
|
+
const attemptTimeout = Math.min(totalTimeout, 5_000);
|
|
339
|
+
const chunkId = getChunkId(this.id, index);
|
|
340
|
+
while (Date.now() < deadline) {
|
|
341
|
+
const cached = knownChunks.get(index);
|
|
342
|
+
if (cached) {
|
|
343
|
+
return cached;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const chunk = await files.files.index.get(chunkId, {
|
|
347
|
+
local: true,
|
|
348
|
+
waitFor: attemptTimeout,
|
|
349
|
+
remote: {
|
|
350
|
+
timeout: attemptTimeout,
|
|
351
|
+
wait: {
|
|
352
|
+
timeout: attemptTimeout,
|
|
353
|
+
behavior: "keep-open",
|
|
354
|
+
},
|
|
355
|
+
throwOnMissing: false,
|
|
356
|
+
retryMissingResponses: true,
|
|
357
|
+
replicate: files.persistChunkReads,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
if (chunk instanceof TinyFile &&
|
|
361
|
+
chunk.parentId === this.id &&
|
|
362
|
+
chunk.index === index) {
|
|
363
|
+
knownChunks.set(index, chunk);
|
|
364
|
+
return chunk;
|
|
244
365
|
}
|
|
245
|
-
|
|
246
|
-
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (!isRetryableChunkLookupError(error)) {
|
|
369
|
+
throw error;
|
|
247
370
|
}
|
|
248
|
-
fetchQueue
|
|
249
|
-
.add(async () => {
|
|
250
|
-
let lastError = undefined;
|
|
251
|
-
for (let i = 0; i < 3; i++) {
|
|
252
|
-
try {
|
|
253
|
-
const chunk = await r.getFile(files, {
|
|
254
|
-
as: "joined",
|
|
255
|
-
timeout: properties?.timeout,
|
|
256
|
-
});
|
|
257
|
-
if (!chunk) {
|
|
258
|
-
throw new Error("Failed to fetch chunk");
|
|
259
|
-
}
|
|
260
|
-
chunks.set(r.id, chunk);
|
|
261
|
-
c++;
|
|
262
|
-
properties?.progress?.(c / allChunks.length);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
// try 3 times
|
|
267
|
-
lastError = error;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
throw lastError;
|
|
271
|
-
})
|
|
272
|
-
.catch(() => {
|
|
273
|
-
fetchQueue.clear(); // Dont do anything more since we failed to fetch one block
|
|
274
|
-
});
|
|
275
371
|
}
|
|
372
|
+
await sleep(250);
|
|
276
373
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
374
|
+
throw new Error(`Failed to resolve chunk ${index + 1}/${this.chunkCount} for file ${this.id}`);
|
|
375
|
+
}
|
|
376
|
+
async *streamFile(files, properties) {
|
|
377
|
+
if (!this.ready) {
|
|
378
|
+
throw new Error("File is still uploading");
|
|
379
|
+
}
|
|
380
|
+
properties?.progress?.(0);
|
|
381
|
+
let processed = 0;
|
|
382
|
+
const hasher = this.finalHash ? new SHA256() : undefined;
|
|
383
|
+
const knownChunks = new Map();
|
|
384
|
+
for (let index = 0; index < this.chunkCount; index++) {
|
|
385
|
+
const chunkFile = await this.resolveChunk(files, index, knownChunks, {
|
|
386
|
+
timeout: properties?.timeout,
|
|
387
|
+
});
|
|
388
|
+
const chunk = await chunkFile.getFile(files, {
|
|
389
|
+
as: "joined",
|
|
390
|
+
timeout: properties?.timeout,
|
|
391
|
+
});
|
|
392
|
+
hasher?.update(chunk);
|
|
393
|
+
processed += chunk.byteLength;
|
|
394
|
+
properties?.progress?.(processed / Math.max(Number(this.size), 1));
|
|
395
|
+
yield chunk;
|
|
396
|
+
}
|
|
397
|
+
if (hasher && toBase64(hasher.digest()) !== this.finalHash) {
|
|
398
|
+
throw new Error("File hash does not match the expected content");
|
|
280
399
|
}
|
|
281
|
-
const chunkContentResolved = await Promise.all(this.fileIds.map(async (x) => {
|
|
282
|
-
const chunkValue = await chunks.get(x);
|
|
283
|
-
if (!chunkValue) {
|
|
284
|
-
throw new Error("Failed to retrieve chunk with id: " + x);
|
|
285
|
-
}
|
|
286
|
-
return chunkValue;
|
|
287
|
-
}));
|
|
288
|
-
return (properties?.as == "chunks"
|
|
289
|
-
? chunkContentResolved
|
|
290
|
-
: concat(chunkContentResolved));
|
|
291
400
|
}
|
|
292
401
|
};
|
|
293
402
|
return LargeFile = _classThis;
|
|
@@ -332,9 +441,9 @@ let Files = (() => {
|
|
|
332
441
|
name = (__runInitializers(this, _id_extraInitializers), __runInitializers(this, _name_initializers, void 0));
|
|
333
442
|
trustGraph = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _trustGraph_initializers, void 0));
|
|
334
443
|
files = (__runInitializers(this, _trustGraph_extraInitializers), __runInitializers(this, _files_initializers, void 0));
|
|
444
|
+
persistChunkReads = __runInitializers(this, _files_extraInitializers);
|
|
335
445
|
constructor(properties = {}) {
|
|
336
446
|
super();
|
|
337
|
-
__runInitializers(this, _files_extraInitializers);
|
|
338
447
|
this.id = properties.id || randomBytes(32);
|
|
339
448
|
this.name = properties.name || "";
|
|
340
449
|
this.trustGraph = properties.rootKey
|
|
@@ -347,22 +456,159 @@ let Files = (() => {
|
|
|
347
456
|
properties.rootKey?.bytes || new Uint8Array(0),
|
|
348
457
|
])),
|
|
349
458
|
});
|
|
459
|
+
this.persistChunkReads = true;
|
|
350
460
|
}
|
|
351
461
|
async add(name, file, parentId, progress) {
|
|
352
|
-
|
|
462
|
+
if (isBlobLike(file)) {
|
|
463
|
+
return this.addBlob(name, file, parentId, progress);
|
|
464
|
+
}
|
|
465
|
+
progress?.(0);
|
|
466
|
+
if (BigInt(file.byteLength) <= TINY_FILE_SIZE_LIMIT_BIGINT) {
|
|
467
|
+
const tinyFile = new TinyFile({ name, file, parentId });
|
|
468
|
+
await this.files.put(tinyFile);
|
|
469
|
+
progress?.(1);
|
|
470
|
+
return tinyFile.id;
|
|
471
|
+
}
|
|
472
|
+
const chunkSize = getLargeFileSegmentSize(file.byteLength);
|
|
473
|
+
return this.addChunkedFile(name, BigInt(file.byteLength), (index) => Promise.resolve(file.subarray(getChunkStart(index, chunkSize), getChunkEnd(index, file.byteLength, chunkSize))), chunkSize, parentId, progress);
|
|
474
|
+
}
|
|
475
|
+
async addBlob(name, file, parentId, progress) {
|
|
353
476
|
progress?.(0);
|
|
354
|
-
if (file.
|
|
355
|
-
|
|
477
|
+
if (BigInt(file.size) <= TINY_FILE_SIZE_LIMIT_BIGINT) {
|
|
478
|
+
const tinyFile = new TinyFile({
|
|
479
|
+
name,
|
|
480
|
+
file: new Uint8Array(await file.arrayBuffer()),
|
|
481
|
+
parentId,
|
|
482
|
+
});
|
|
483
|
+
await this.files.put(tinyFile);
|
|
484
|
+
progress?.(1);
|
|
485
|
+
return tinyFile.id;
|
|
356
486
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
487
|
+
const chunkSize = getLargeFileSegmentSize(file.size);
|
|
488
|
+
return this.addChunkedFile(name, BigInt(file.size), (index) => readBlobChunk(file, index, chunkSize), chunkSize, parentId, progress);
|
|
489
|
+
}
|
|
490
|
+
async addSource(name, source, parentId, progress) {
|
|
491
|
+
progress?.(0);
|
|
492
|
+
if (source.size <= TINY_FILE_SIZE_LIMIT_BIGINT) {
|
|
493
|
+
const chunks = [];
|
|
494
|
+
let processed = 0n;
|
|
495
|
+
for await (const chunk of source.readChunks(TINY_FILE_SIZE_LIMIT)) {
|
|
496
|
+
chunks.push(chunk);
|
|
497
|
+
processed += BigInt(chunk.byteLength);
|
|
498
|
+
}
|
|
499
|
+
ensureSourceSize(processed, source.size);
|
|
500
|
+
const tinyFile = new TinyFile({
|
|
501
|
+
name,
|
|
502
|
+
file: chunks.length === 0 ? new Uint8Array(0) : concat(chunks),
|
|
503
|
+
parentId,
|
|
504
|
+
});
|
|
505
|
+
await this.files.put(tinyFile);
|
|
506
|
+
progress?.(1);
|
|
507
|
+
return tinyFile.id;
|
|
508
|
+
}
|
|
509
|
+
if (parentId) {
|
|
510
|
+
throw new Error("Unexpected that a LargeFile to have a parent");
|
|
511
|
+
}
|
|
512
|
+
const size = source.size;
|
|
513
|
+
const chunkSize = getLargeFileSegmentSize(size);
|
|
514
|
+
const uploadId = createUploadId();
|
|
515
|
+
const manifest = new LargeFile({
|
|
516
|
+
id: uploadId,
|
|
517
|
+
name,
|
|
518
|
+
size,
|
|
519
|
+
chunkCount: getChunkCount(size, chunkSize),
|
|
520
|
+
ready: false,
|
|
521
|
+
});
|
|
522
|
+
await this.files.put(manifest);
|
|
523
|
+
const hasher = new SHA256();
|
|
524
|
+
try {
|
|
525
|
+
let uploadedBytes = 0n;
|
|
526
|
+
let chunkCount = 0;
|
|
527
|
+
for await (const chunkBytes of source.readChunks(chunkSize)) {
|
|
528
|
+
hasher.update(chunkBytes);
|
|
529
|
+
await this.files.put(new TinyFile({
|
|
530
|
+
name: name + "/" + chunkCount,
|
|
531
|
+
file: chunkBytes,
|
|
532
|
+
parentId: uploadId,
|
|
533
|
+
index: chunkCount,
|
|
534
|
+
}));
|
|
535
|
+
uploadedBytes += BigInt(chunkBytes.byteLength);
|
|
536
|
+
chunkCount++;
|
|
537
|
+
progress?.(Number(uploadedBytes) / Math.max(Number(size), 1));
|
|
360
538
|
}
|
|
361
|
-
|
|
539
|
+
ensureSourceSize(uploadedBytes, source.size);
|
|
540
|
+
await this.files.put(new LargeFile({
|
|
541
|
+
id: uploadId,
|
|
542
|
+
name,
|
|
543
|
+
size,
|
|
544
|
+
chunkCount,
|
|
545
|
+
ready: true,
|
|
546
|
+
finalHash: toBase64(hasher.digest()),
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
await this.cleanupChunkedUpload(uploadId).catch(() => { });
|
|
551
|
+
await this.files.del(uploadId).catch(() => { });
|
|
552
|
+
throw error;
|
|
362
553
|
}
|
|
363
|
-
await this.files.put(toPut);
|
|
364
554
|
progress?.(1);
|
|
365
|
-
return
|
|
555
|
+
return uploadId;
|
|
556
|
+
}
|
|
557
|
+
async cleanupChunkedUpload(uploadId) {
|
|
558
|
+
const chunks = await this.files.index.search(new SearchRequest({
|
|
559
|
+
query: new StringMatch({
|
|
560
|
+
key: "parentId",
|
|
561
|
+
value: uploadId,
|
|
562
|
+
}),
|
|
563
|
+
fetch: 0xffffffff,
|
|
564
|
+
}), { local: true });
|
|
565
|
+
await Promise.all(chunks.map((chunk) => this.files.del(chunk.id)));
|
|
566
|
+
}
|
|
567
|
+
async addChunkedFile(name, size, getChunk, chunkSize, parentId, progress) {
|
|
568
|
+
if (parentId) {
|
|
569
|
+
throw new Error("Unexpected that a LargeFile to have a parent");
|
|
570
|
+
}
|
|
571
|
+
const uploadId = createUploadId();
|
|
572
|
+
const chunkCount = getChunkCount(size, chunkSize);
|
|
573
|
+
const manifest = new LargeFile({
|
|
574
|
+
id: uploadId,
|
|
575
|
+
name,
|
|
576
|
+
size,
|
|
577
|
+
chunkCount,
|
|
578
|
+
ready: false,
|
|
579
|
+
});
|
|
580
|
+
await this.files.put(manifest);
|
|
581
|
+
const hasher = new SHA256();
|
|
582
|
+
try {
|
|
583
|
+
let uploadedBytes = 0;
|
|
584
|
+
for (let i = 0; i < chunkCount; i++) {
|
|
585
|
+
const chunkBytes = await getChunk(i);
|
|
586
|
+
hasher.update(chunkBytes);
|
|
587
|
+
await this.files.put(new TinyFile({
|
|
588
|
+
name: name + "/" + i,
|
|
589
|
+
file: chunkBytes,
|
|
590
|
+
parentId: uploadId,
|
|
591
|
+
index: i,
|
|
592
|
+
}));
|
|
593
|
+
uploadedBytes += chunkBytes.byteLength;
|
|
594
|
+
progress?.(uploadedBytes / Math.max(Number(size), 1));
|
|
595
|
+
}
|
|
596
|
+
await this.files.put(new LargeFile({
|
|
597
|
+
id: uploadId,
|
|
598
|
+
name,
|
|
599
|
+
size,
|
|
600
|
+
chunkCount,
|
|
601
|
+
ready: true,
|
|
602
|
+
finalHash: toBase64(hasher.digest()),
|
|
603
|
+
}));
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
await this.cleanupChunkedUpload(uploadId).catch(() => { });
|
|
607
|
+
await this.files.del(uploadId).catch(() => { });
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
progress?.(1);
|
|
611
|
+
return uploadId;
|
|
366
612
|
}
|
|
367
613
|
async removeById(id) {
|
|
368
614
|
const file = await this.files.index.get(id);
|
|
@@ -394,7 +640,10 @@ let Files = (() => {
|
|
|
394
640
|
}), {
|
|
395
641
|
local: true,
|
|
396
642
|
remote: {
|
|
397
|
-
|
|
643
|
+
// Allow partial results while the network is still forming. If we
|
|
644
|
+
// throw on missing shards here, the UI can appear "empty" until
|
|
645
|
+
// all shard roots respond, which feels broken during joins/churn.
|
|
646
|
+
throwOnMissing: false,
|
|
398
647
|
replicate: true, // sync here because this, because we might want to access it offline, even though we are not replicators
|
|
399
648
|
},
|
|
400
649
|
});
|
|
@@ -407,6 +656,45 @@ let Files = (() => {
|
|
|
407
656
|
}));
|
|
408
657
|
return count;
|
|
409
658
|
}
|
|
659
|
+
async resolveById(id, properties) {
|
|
660
|
+
return this.files.index.get(id, {
|
|
661
|
+
local: true,
|
|
662
|
+
waitFor: properties?.timeout,
|
|
663
|
+
remote: {
|
|
664
|
+
timeout: properties?.timeout ?? 10 * 1000,
|
|
665
|
+
wait: properties?.timeout
|
|
666
|
+
? {
|
|
667
|
+
timeout: properties.timeout,
|
|
668
|
+
behavior: "keep-open",
|
|
669
|
+
}
|
|
670
|
+
: undefined,
|
|
671
|
+
throwOnMissing: false,
|
|
672
|
+
retryMissingResponses: true,
|
|
673
|
+
replicate: properties?.replicate,
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
async resolveByName(name, properties) {
|
|
678
|
+
const results = await this.files.index.search(new SearchRequest({
|
|
679
|
+
query: [
|
|
680
|
+
new StringMatch({
|
|
681
|
+
key: "name",
|
|
682
|
+
value: name,
|
|
683
|
+
caseInsensitive: false,
|
|
684
|
+
method: StringMatchMethod.exact,
|
|
685
|
+
}),
|
|
686
|
+
],
|
|
687
|
+
fetch: 1,
|
|
688
|
+
}), {
|
|
689
|
+
local: true,
|
|
690
|
+
remote: {
|
|
691
|
+
timeout: properties?.timeout ?? 10 * 1000,
|
|
692
|
+
throwOnMissing: false,
|
|
693
|
+
replicate: properties?.replicate,
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
return results[0];
|
|
697
|
+
}
|
|
410
698
|
/**
|
|
411
699
|
* Get by name
|
|
412
700
|
* @param id
|
|
@@ -461,6 +749,7 @@ let Files = (() => {
|
|
|
461
749
|
}
|
|
462
750
|
// Setup lifecycle, will be invoked on 'open'
|
|
463
751
|
async open(args) {
|
|
752
|
+
this.persistChunkReads = args?.replicate !== false;
|
|
464
753
|
await this.trustGraph?.open({
|
|
465
754
|
replicate: args?.replicate,
|
|
466
755
|
});
|