@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.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, vec, option } from "@dao-xyz/borsh";
35
+ import { field, variant, option } from "@dao-xyz/borsh";
36
36
  import { Program } from "@peerbit/program";
37
- import { Documents, SearchRequest, StringMatch, StringMatchMethod, Or, IsNull, } from "@peerbit/document";
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 PQueue from "p-queue";
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
- return class IndexableFile {
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: "u32" })];
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
- if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
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
- parentId = (__runInitializers(this, _file_extraInitializers), __runInitializers(this, _parentId_initializers, void 0));
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, _parentId_extraInitializers);
131
- this.id = properties.id || sha256Base64Sync(properties.file);
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.parentId = properties.parentId;
213
+ this.hash = properties.hash || sha256Base64Sync(properties.file);
135
214
  }
136
- async getFile(_files, properties) {
137
- if (sha256Base64Sync(this.file) !== this.id) {
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
- return Promise.resolve(properties?.as == "chunks" ? [this.file] : this.file);
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
- _fileIds_decorators = [field({ type: vec("string") })];
175
- _size_decorators = [field({ type: "u32" })];
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); // hash
274
+ id = __runInitializers(this, _id_initializers, void 0);
186
275
  name = (__runInitializers(this, _id_extraInitializers), __runInitializers(this, _name_initializers, void 0));
187
- fileIds = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _fileIds_initializers, void 0));
188
- size = (__runInitializers(this, _fileIds_extraInitializers), __runInitializers(this, _size_initializers, void 0));
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, _size_extraInitializers);
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
- static async create(name, file, files, progress) {
198
- const segmetSize = TINY_FILE_SIZE_LIMIT / 10; // 10% of the small size limit
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 expectedIds = new Set(this.fileIds);
217
- const allFiles = await files.files.index.search(new SearchRequest({
218
- query: [
219
- new Or([...expectedIds].map((x) => new StringMatch({ key: "id", value: x }))),
220
- ],
221
- fetch: 0xffffffff,
222
- }));
223
- return allFiles;
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.delete(files)));
333
+ await Promise.all((await this.fetchChunks(files)).map((x) => files.files.del(x.id)));
227
334
  }
228
- async getFile(files, properties) {
229
- // Get all sub files (SmallFiles) and concatinate them in the right order (the order of this.fileIds)
230
- properties?.progress?.(0);
231
- const allChunks = await this.fetchChunks(files);
232
- const fetchQueue = new PQueue({ concurrency: 10 });
233
- let fetchError = undefined;
234
- fetchQueue.on("error", (err) => {
235
- fetchError = err;
236
- });
237
- const chunks = new Map();
238
- const expectedIds = new Set(this.fileIds);
239
- if (allChunks.length > 0) {
240
- let c = 0;
241
- for (const r of allChunks) {
242
- if (chunks.has(r.id)) {
243
- // chunk already added;
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
- if (!expectedIds.has(r.id)) {
246
- // chunk is not part of this file
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
- await fetchQueue.onIdle();
278
- if (fetchError || chunks.size !== expectedIds.size) {
279
- throw new Error(`Failed to resolve file. Recieved ${chunks.size}/${expectedIds.size} chunks`);
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
- let toPut;
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.byteLength <= TINY_FILE_SIZE_LIMIT) {
355
- toPut = new TinyFile({ name, file, parentId });
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
- else {
358
- if (parentId) {
359
- throw new Error("Unexpected that a LargeFile to have a parent");
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
- toPut = await LargeFile.create(name, file, this, progress);
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 toPut.id;
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
- throwOnMissing: true,
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
  });