@muhkoo/connect 0.15.0-alpha.0 → 0.15.0-alpha.2
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/browser/blockEngine.worker.js.map +1 -1
- package/dist/browser/index.js +198 -50
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +52 -9
- package/dist/server/index.js +198 -50
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/browser/index.js
CHANGED
|
@@ -5862,6 +5862,34 @@ class ReedSolomonCodec {
|
|
|
5862
5862
|
const SHARD_BATCH_WINDOW_MS = 6;
|
|
5863
5863
|
/** Hashes per batch. Must not exceed what the server will accept. */
|
|
5864
5864
|
const SHARD_BATCH_MAX = 256;
|
|
5865
|
+
/**
|
|
5866
|
+
* Expected response bytes per batch, which a batch is split to stay under.
|
|
5867
|
+
*
|
|
5868
|
+
* The server refuses a batch above 24 MiB of shard bytes with a 413, and the
|
|
5869
|
+
* reads in a refused batch fail. Counting hashes alone, about 25 one-MiB
|
|
5870
|
+
* shards in one window - a few large files read at once - drew it. This
|
|
5871
|
+
* leaves the server's ceiling well clear.
|
|
5872
|
+
*/
|
|
5873
|
+
const SHARD_BATCH_BUDGET_BYTES = 16 * 1024 * 1024;
|
|
5874
|
+
/** An entry's JSON around its base64: the quoted hash, a colon, quotes and a comma, rounded up. */
|
|
5875
|
+
const SHARD_BATCH_ENTRY_BYTES = 128;
|
|
5876
|
+
/**
|
|
5877
|
+
* What a shard adds to a batch's response: base64 is 4 bytes for every 3.
|
|
5878
|
+
*
|
|
5879
|
+
* A shard whose size the caller did not give counts as its entry alone, so
|
|
5880
|
+
* reads without sizes batch by count, as they always have — kept for the
|
|
5881
|
+
* pinned "many concurrent reads → ONE request" test
|
|
5882
|
+
* (`shard-batching.test.ts`). No SDK read path is size-less today:
|
|
5883
|
+
* `FileStorage.readChunk` always passes `shardSize`. The server reads and
|
|
5884
|
+
* base64-encodes every shard before it checks the total
|
|
5885
|
+
* (`ShardsController.ts`), so a very large size-less batch can fail with a
|
|
5886
|
+
* 5xx before the client ever sees a 413. The split in `sendShardBatch` is a
|
|
5887
|
+
* best-effort net for that case, not a guarantee.
|
|
5888
|
+
*/
|
|
5889
|
+
function batchCost(size) {
|
|
5890
|
+
const bytes = typeof size === "number" && Number.isFinite(size) && size > 0 ? size : 0;
|
|
5891
|
+
return Math.ceil(bytes / 3) * 4 + SHARD_BATCH_ENTRY_BYTES;
|
|
5892
|
+
}
|
|
5865
5893
|
/** Decode base64 without `atob`'s per-character loop cost on large shards. */
|
|
5866
5894
|
function bytesFromBase64(b64) {
|
|
5867
5895
|
const binary = atob(b64);
|
|
@@ -5889,7 +5917,7 @@ class ShardClient {
|
|
|
5889
5917
|
pathPrefix;
|
|
5890
5918
|
fetchFn;
|
|
5891
5919
|
cache;
|
|
5892
|
-
/** Hashes waiting for the next batch, each with everyone awaiting it. */
|
|
5920
|
+
/** Hashes waiting for the next batch, each with everyone awaiting it and its size if a caller gave it. */
|
|
5893
5921
|
pendingShards;
|
|
5894
5922
|
batchTimer;
|
|
5895
5923
|
/** False once the server is known not to have the batch route. */
|
|
@@ -6038,8 +6066,11 @@ class ShardClient {
|
|
|
6038
6066
|
/**
|
|
6039
6067
|
* Fetch a shard by hash. Throws if the server returns a non-200; returns
|
|
6040
6068
|
* `null` for 404 so callers can distinguish "missing" from "errored".
|
|
6069
|
+
*
|
|
6070
|
+
* `size`, the shard's byte length when known (a manifest's `shardSize`),
|
|
6071
|
+
* lets concurrent reads be batched within what the server will send.
|
|
6041
6072
|
*/
|
|
6042
|
-
async getShard(hash) {
|
|
6073
|
+
async getShard(hash, opts = {}) {
|
|
6043
6074
|
// Cache-first: a hit is content-verified by construction (key == hash).
|
|
6044
6075
|
if (this.cache) {
|
|
6045
6076
|
const cached = await this.cache.get(hash);
|
|
@@ -6048,7 +6079,7 @@ class ShardClient {
|
|
|
6048
6079
|
}
|
|
6049
6080
|
// No peers → straight to origin.
|
|
6050
6081
|
if (!this.peers)
|
|
6051
|
-
return this.fetchFromOrigin(hash);
|
|
6082
|
+
return this.fetchFromOrigin(hash, opts.size);
|
|
6052
6083
|
// HEDGED RACE: prefer a peer (saves origin egress) but never let a slow
|
|
6053
6084
|
// or missing peer stall delivery — give the peer a short head start, then
|
|
6054
6085
|
// start origin in parallel and take whichever returns the block first.
|
|
@@ -6079,7 +6110,7 @@ class ShardClient {
|
|
|
6079
6110
|
clearTimeout(timer);
|
|
6080
6111
|
timer = null;
|
|
6081
6112
|
}
|
|
6082
|
-
this.fetchFromOrigin(hash).then(win, fail);
|
|
6113
|
+
this.fetchFromOrigin(hash, opts.size).then(win, fail);
|
|
6083
6114
|
};
|
|
6084
6115
|
timer = setTimeout(startOrigin, HEDGE_MS);
|
|
6085
6116
|
peers.getBlock(hash).then((b) => {
|
|
@@ -6113,16 +6144,17 @@ class ShardClient {
|
|
|
6113
6144
|
* Falls back permanently to one-at-a-time if the server has no batch route,
|
|
6114
6145
|
* so a new SDK keeps working against an older deployment.
|
|
6115
6146
|
*/
|
|
6116
|
-
fetchFromOrigin(hash) {
|
|
6147
|
+
fetchFromOrigin(hash, size) {
|
|
6117
6148
|
if (!this.batchSupported)
|
|
6118
6149
|
return this.fetchOneFromOrigin(hash);
|
|
6119
6150
|
return new Promise((resolve, reject) => {
|
|
6120
|
-
const
|
|
6121
|
-
if (
|
|
6122
|
-
|
|
6151
|
+
const pending = this.pendingShards.get(hash);
|
|
6152
|
+
if (pending) {
|
|
6153
|
+
pending.waiters.push({ resolve, reject });
|
|
6154
|
+
pending.size ??= size;
|
|
6123
6155
|
return;
|
|
6124
6156
|
}
|
|
6125
|
-
this.pendingShards.set(hash, [{ resolve, reject }]);
|
|
6157
|
+
this.pendingShards.set(hash, { size, waiters: [{ resolve, reject }] });
|
|
6126
6158
|
if (this.batchTimer === null) {
|
|
6127
6159
|
this.batchTimer = setTimeout(() => { void this.flushShardBatch(); }, SHARD_BATCH_WINDOW_MS);
|
|
6128
6160
|
}
|
|
@@ -6133,17 +6165,33 @@ class ShardClient {
|
|
|
6133
6165
|
this.batchTimer = null;
|
|
6134
6166
|
if (this.pendingShards.size === 0)
|
|
6135
6167
|
return;
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6168
|
+
// As many as fit: at most SHARD_BATCH_MAX hashes, and a response
|
|
6169
|
+
// expected to stay within SHARD_BATCH_BUDGET_BYTES. One that does not
|
|
6170
|
+
// fit is skipped for a smaller one behind it. The first always goes,
|
|
6171
|
+
// so a shard over the budget by itself is fetched alone.
|
|
6172
|
+
const waiters = [];
|
|
6173
|
+
let cost = 0;
|
|
6174
|
+
for (const [h, pending] of this.pendingShards) {
|
|
6175
|
+
if (waiters.length === SHARD_BATCH_MAX)
|
|
6176
|
+
break;
|
|
6177
|
+
const next = batchCost(pending.size);
|
|
6178
|
+
if (waiters.length > 0 && cost + next > SHARD_BATCH_BUDGET_BYTES)
|
|
6179
|
+
continue;
|
|
6180
|
+
waiters.push([h, pending.waiters]);
|
|
6181
|
+
cost += next;
|
|
6182
|
+
}
|
|
6183
|
+
for (const [h] of waiters)
|
|
6139
6184
|
this.pendingShards.delete(h);
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
// Anything over the cap waits for the next window rather than being
|
|
6143
|
-
// dropped or silently truncated.
|
|
6185
|
+
// Anything over the cap or the budget waits for the next window
|
|
6186
|
+
// rather than being dropped or silently truncated.
|
|
6144
6187
|
if (this.pendingShards.size > 0 && this.batchTimer === null) {
|
|
6145
6188
|
this.batchTimer = setTimeout(() => { void this.flushShardBatch(); }, SHARD_BATCH_WINDOW_MS);
|
|
6146
6189
|
}
|
|
6190
|
+
await this.sendShardBatch(waiters);
|
|
6191
|
+
}
|
|
6192
|
+
/** POST one batch and settle everyone waiting on it. Never throws. */
|
|
6193
|
+
async sendShardBatch(waiters) {
|
|
6194
|
+
const hashes = waiters.map(([h]) => h);
|
|
6147
6195
|
let shards;
|
|
6148
6196
|
try {
|
|
6149
6197
|
const res = await this.fetchFn(`${this.baseUrl}${this.pathPrefix}/batch`, {
|
|
@@ -6151,6 +6199,21 @@ class ShardClient {
|
|
|
6151
6199
|
headers: { "Content-Type": "application/json" },
|
|
6152
6200
|
body: JSON.stringify({ hashes }),
|
|
6153
6201
|
});
|
|
6202
|
+
if (res.status === 413 && waiters.length > 1) {
|
|
6203
|
+
// Too large for the server after all: a size was not given,
|
|
6204
|
+
// or was wrong. Ask for each half on its own. Halving reaches
|
|
6205
|
+
// one hash per batch within log2(n) levels, and one hash the
|
|
6206
|
+
// server refuses is an error like any other, below.
|
|
6207
|
+
// Release the body first: under undici (the CLI, Node), an
|
|
6208
|
+
// unread body holds the connection until garbage collection.
|
|
6209
|
+
await res.body?.cancel().catch(() => { });
|
|
6210
|
+
const half = Math.ceil(waiters.length / 2);
|
|
6211
|
+
await Promise.all([
|
|
6212
|
+
this.sendShardBatch(waiters.slice(0, half)),
|
|
6213
|
+
this.sendShardBatch(waiters.slice(half)),
|
|
6214
|
+
]);
|
|
6215
|
+
return;
|
|
6216
|
+
}
|
|
6154
6217
|
if (res.status === 404 || res.status === 405) {
|
|
6155
6218
|
// No batch route on this deployment. Stop trying, and serve
|
|
6156
6219
|
// everyone individually — including the batch we just gathered.
|
|
@@ -6526,7 +6589,8 @@ class FileStorage {
|
|
|
6526
6589
|
const fetched = new Array(total).fill(null);
|
|
6527
6590
|
const fetchInto = async (indexes) => {
|
|
6528
6591
|
await this.runWithConcurrency(indexes.map((i) => async () => {
|
|
6529
|
-
|
|
6592
|
+
// The size keeps a batch of concurrent reads within what the server will send.
|
|
6593
|
+
fetched[i] = await this.shards.getShard(manifest.shardHashes[i], { size: manifest.shardSize });
|
|
6530
6594
|
}));
|
|
6531
6595
|
};
|
|
6532
6596
|
await fetchInto(Array.from({ length: dataCount }, (_, i) => i));
|
|
@@ -6675,7 +6739,7 @@ async function parseOrThrow(response, op) {
|
|
|
6675
6739
|
* expect (and isn't serving a stale cached bundle). Keep this in sync with
|
|
6676
6740
|
* `package.json` `version`.
|
|
6677
6741
|
*/
|
|
6678
|
-
const VERSION = "0.15.0-alpha.
|
|
6742
|
+
const VERSION = "0.15.0-alpha.2";
|
|
6679
6743
|
|
|
6680
6744
|
/**
|
|
6681
6745
|
* `HttpClient` — the one place the unified {@link Client} talks to the
|
|
@@ -15082,11 +15146,19 @@ function nextTurn() {
|
|
|
15082
15146
|
* Every mutation is a read-modify-write of exactly ONE directory record, so a
|
|
15083
15147
|
* write cannot leave the tree half-updated: either the parent names the new
|
|
15084
15148
|
* child or it does not. Two tabs writing different directories never conflict;
|
|
15085
|
-
* two tabs writing the SAME directory
|
|
15086
|
-
*
|
|
15087
|
-
*
|
|
15088
|
-
*
|
|
15149
|
+
* two tabs (or devices) writing the SAME directory are last-writer-wins. A VFS
|
|
15150
|
+
* write sends no HLC and the personal space has no compare-and-swap: it applies
|
|
15151
|
+
* puts of one key in the order they arrive, so whichever arrives last is kept,
|
|
15152
|
+
* and the other tab's edit is lost. Orphaned records (a directory whose parent
|
|
15153
|
+
* entry lost the race) are unreachable rather than corrupting — they cost
|
|
15154
|
+
* storage until a future sweep, which is the trade this shape makes in
|
|
15089
15155
|
* exchange for single-record writes.
|
|
15156
|
+
*
|
|
15157
|
+
* Within ONE tab, writes of a record are queued and sent one at a time, in the
|
|
15158
|
+
* order they were made, and a read that has to fetch a record waits for its
|
|
15159
|
+
* write in flight (see `writing`). That ordering is per namespace instance
|
|
15160
|
+
* only; nothing orders writes across tabs or devices - that would take a
|
|
15161
|
+
* compare-and-swap at the server.
|
|
15090
15162
|
*/
|
|
15091
15163
|
/** Refuse a directory at `path` if it would be deeper than MAX_DEPTH from the root. */
|
|
15092
15164
|
function assertWritableDepth(path) {
|
|
@@ -15458,6 +15530,20 @@ class VfsNamespace {
|
|
|
15458
15530
|
* than by TTL — a stale directory listing shows files that are not there.
|
|
15459
15531
|
*/
|
|
15460
15532
|
cache = new Map();
|
|
15533
|
+
/**
|
|
15534
|
+
* The write of each directory record still in flight, by id.
|
|
15535
|
+
*
|
|
15536
|
+
* Two writes of one record must land in the order they were made. The
|
|
15537
|
+
* personal space has no compare-and-swap: of two writes of one key that
|
|
15538
|
+
* are in flight together it keeps whichever request finishes last, and
|
|
15539
|
+
* that is often the one sent first. Then the earlier write, sealed before
|
|
15540
|
+
* the later edit was made, undoes it. That is how a file deleted forever
|
|
15541
|
+
* from the Trash, while the move that put it there was still being
|
|
15542
|
+
* written, came back. So each write waits for the one before it, and a
|
|
15543
|
+
* read that has to fetch the record waits for its write too: see
|
|
15544
|
+
* `openDirById`.
|
|
15545
|
+
*/
|
|
15546
|
+
writing = new Map();
|
|
15461
15547
|
historyLimit;
|
|
15462
15548
|
maxEntries;
|
|
15463
15549
|
maxPathChars;
|
|
@@ -15921,10 +16007,7 @@ class VfsNamespace {
|
|
|
15921
16007
|
const parentPath = dirname(target);
|
|
15922
16008
|
await this.mkdir(parentPath, { recursive: true });
|
|
15923
16009
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
15924
|
-
const { manifest, size } = await this.deps.content.write(bytes, {
|
|
15925
|
-
name,
|
|
15926
|
-
type: opts.type ?? contentTypeFor(name),
|
|
15927
|
-
});
|
|
16010
|
+
const { manifest, size } = await this.deps.content.write(bytes, { name, type: opts.type ?? contentTypeFor(name) }, { onProgress: opts.onProgress });
|
|
15928
16011
|
const parent = await this.openDir(parentPath);
|
|
15929
16012
|
const prior = entryIn(parent.node, name);
|
|
15930
16013
|
if (prior?.kind === "dir")
|
|
@@ -16211,8 +16294,11 @@ class VfsNamespace {
|
|
|
16211
16294
|
* Delete metadata records no longer reachable from the root.
|
|
16212
16295
|
*
|
|
16213
16296
|
* These accumulate because every write touches exactly one record: if two
|
|
16214
|
-
* tabs edit the same directory,
|
|
16215
|
-
*
|
|
16297
|
+
* tabs or devices edit the same directory, the personal space keeps
|
|
16298
|
+
* whichever put arrives last (a VFS write sends no HLC, and the space
|
|
16299
|
+
* applies puts of one key in arrival order), which can leave a child whose
|
|
16300
|
+
* parent entry lost the race. The per-record write queue (`writing`) only
|
|
16301
|
+
* orders writes within one tab. Unreachable is harmless —
|
|
16216
16302
|
* it is invisible and undecryptable-by-nobody — but it costs storage, so
|
|
16217
16303
|
* this reclaims it.
|
|
16218
16304
|
*
|
|
@@ -16370,7 +16456,41 @@ class VfsNamespace {
|
|
|
16370
16456
|
// every concurrent reader to miss and fire its own request for the same
|
|
16371
16457
|
// record — which is how loading one project turned into thousands of
|
|
16372
16458
|
// identical `vfs/d/<id>` fetches, most of them in flight at once.
|
|
16373
|
-
|
|
16459
|
+
// Reads `pending` only after its first `await`: before that, the const is not yet initialised.
|
|
16460
|
+
const load = async () => {
|
|
16461
|
+
// Not while a write of this record is still in flight: see
|
|
16462
|
+
// `writing`. Our own writes echo back over the personal-space
|
|
16463
|
+
// socket, and `watch` drops the record each one names, so a read
|
|
16464
|
+
// can miss with the write still on its way - and the store would
|
|
16465
|
+
// answer with the copy from BEFORE it. An edit made to that copy
|
|
16466
|
+
// is queued behind the write, and then stored over it. So wait
|
|
16467
|
+
// for the latest write queued, which waits on every one before it,
|
|
16468
|
+
// to land or fail - and then for any queued while this read
|
|
16469
|
+
// waited, since the one it waited for may have failed with that
|
|
16470
|
+
// one still on its way. A record with no write in flight is
|
|
16471
|
+
// fetched at once.
|
|
16472
|
+
let awaited;
|
|
16473
|
+
for (let inFlight = this.writing.get(id); inFlight && inFlight !== awaited; inFlight = this.writing.get(id)) {
|
|
16474
|
+
awaited = inFlight;
|
|
16475
|
+
await inFlight.catch(() => { });
|
|
16476
|
+
}
|
|
16477
|
+
if (awaited) {
|
|
16478
|
+
// A write that landed put the record it wrote back in the
|
|
16479
|
+
// cache, and the next edit of this directory is made to that.
|
|
16480
|
+
// Answer with it rather than fetch a second copy: two copies
|
|
16481
|
+
// of one record, each edited and written, undo each other's
|
|
16482
|
+
// edits. When the cache holds nothing else - the write failed,
|
|
16483
|
+
// or a change since has dropped the record - ask the store.
|
|
16484
|
+
const now = this.cache.get(id);
|
|
16485
|
+
if (now && now !== pending)
|
|
16486
|
+
return now;
|
|
16487
|
+
// Empty, when the write failed: it dropped what the cache held,
|
|
16488
|
+
// and after an echo that was this very read. Put it back before
|
|
16489
|
+
// asking the store, so a read that comes in meanwhile shares
|
|
16490
|
+
// this copy instead of fetching a second one.
|
|
16491
|
+
if (!now)
|
|
16492
|
+
this.cache.set(id, pending);
|
|
16493
|
+
}
|
|
16374
16494
|
const raw = await this.deps.store.get(dirKey(id));
|
|
16375
16495
|
// An absent record is an EMPTY directory, not an error: the root
|
|
16376
16496
|
// exists before anything has been written to it, and a directory is
|
|
@@ -16396,7 +16516,8 @@ class VfsNamespace {
|
|
|
16396
16516
|
throw err;
|
|
16397
16517
|
}
|
|
16398
16518
|
return this.readRecord(id, key, value, path);
|
|
16399
|
-
}
|
|
16519
|
+
};
|
|
16520
|
+
const pending = load().catch((err) => {
|
|
16400
16521
|
// A failed read must not be remembered, or one blip poisons the
|
|
16401
16522
|
// directory for the life of the session.
|
|
16402
16523
|
if (this.cache.get(id) === pending)
|
|
@@ -16482,24 +16603,14 @@ class VfsNamespace {
|
|
|
16482
16603
|
if (Object.hasOwn(dir.node.entries, name))
|
|
16483
16604
|
delete dir.hidden[name];
|
|
16484
16605
|
}
|
|
16485
|
-
|
|
16486
|
-
|
|
16487
|
-
|
|
16606
|
+
// Not sent until any earlier write of this record has landed: see
|
|
16607
|
+
// `writing`. Whether that one worked or not, this one still goes.
|
|
16608
|
+
const before = this.writing.get(dir.id);
|
|
16609
|
+
const send = () => this.writeRecord(dir);
|
|
16610
|
+
const write = before ? before.then(send, send) : send();
|
|
16611
|
+
this.writing.set(dir.id, write);
|
|
16488
16612
|
try {
|
|
16489
|
-
|
|
16490
|
-
try {
|
|
16491
|
-
sealed = await seal(dir.key, stored);
|
|
16492
|
-
}
|
|
16493
|
-
catch (err) {
|
|
16494
|
-
// JSON.stringify's own failures: a value too deeply nested to
|
|
16495
|
-
// write. Reading drops those from a stored record, so this is
|
|
16496
|
-
// what the write itself was given.
|
|
16497
|
-
if (!(err instanceof RangeError || err instanceof TypeError))
|
|
16498
|
-
throw err;
|
|
16499
|
-
const where = dir.path ?? `the directory stored as record ${printable(dir.id, 40)}`;
|
|
16500
|
-
throw new VfsConflictError(`cannot write to ${where}: it would hold a value nested too deeply to store`);
|
|
16501
|
-
}
|
|
16502
|
-
await this.deps.store.put(dirKey(dir.id), sealed);
|
|
16613
|
+
await write;
|
|
16503
16614
|
}
|
|
16504
16615
|
catch (err) {
|
|
16505
16616
|
// The edit above was made in place, on the very object the cache
|
|
@@ -16507,11 +16618,48 @@ class VfsNamespace {
|
|
|
16507
16618
|
// change the store never took - a deleted file gone from every
|
|
16508
16619
|
// listing while it is still stored, and a retry told it does not
|
|
16509
16620
|
// exist. Drop it, so the next read asks the store.
|
|
16510
|
-
|
|
16621
|
+
//
|
|
16622
|
+
// Unless a later write of this record is already queued. It
|
|
16623
|
+
// seals the record as it stands when it is sent, this edit
|
|
16624
|
+
// included, so the cache shows what is about to be stored, and
|
|
16625
|
+
// dropping it would only send readers to the store for an older
|
|
16626
|
+
// copy. If that write fails too, it drops the cache itself.
|
|
16627
|
+
if (this.writing.get(dir.id) === write)
|
|
16628
|
+
this.cache.delete(dir.id);
|
|
16511
16629
|
throw err;
|
|
16512
16630
|
}
|
|
16631
|
+
finally {
|
|
16632
|
+
if (this.writing.get(dir.id) === write)
|
|
16633
|
+
this.writing.delete(dir.id);
|
|
16634
|
+
}
|
|
16513
16635
|
this.cache.set(dir.id, Promise.resolve(dir));
|
|
16514
16636
|
}
|
|
16637
|
+
/**
|
|
16638
|
+
* Seal a directory record as it stands NOW and put it in the store.
|
|
16639
|
+
*
|
|
16640
|
+
* Read at the moment of sending, not when the edit was made, so a write
|
|
16641
|
+
* that waited behind another carries every edit made to the record by
|
|
16642
|
+
* then, its own included.
|
|
16643
|
+
*/
|
|
16644
|
+
async writeRecord(dir) {
|
|
16645
|
+
const stored = Object.keys(dir.hidden).length
|
|
16646
|
+
? { ...dir.node, entries: Object.assign(Object.create(null), dir.hidden, dir.node.entries) }
|
|
16647
|
+
: dir.node;
|
|
16648
|
+
let sealed;
|
|
16649
|
+
try {
|
|
16650
|
+
sealed = await seal(dir.key, stored);
|
|
16651
|
+
}
|
|
16652
|
+
catch (err) {
|
|
16653
|
+
// JSON.stringify's own failures: a value too deeply nested to
|
|
16654
|
+
// write. Reading drops those from a stored record, so this is
|
|
16655
|
+
// what the write itself was given.
|
|
16656
|
+
if (!(err instanceof RangeError || err instanceof TypeError))
|
|
16657
|
+
throw err;
|
|
16658
|
+
const where = dir.path ?? `the directory stored as record ${printable(dir.id, 40)}`;
|
|
16659
|
+
throw new VfsConflictError(`cannot write to ${where}: it would hold a value nested too deeply to store`);
|
|
16660
|
+
}
|
|
16661
|
+
await this.deps.store.put(dirKey(dir.id), sealed);
|
|
16662
|
+
}
|
|
16515
16663
|
async createDir(parentPath, name) {
|
|
16516
16664
|
assertValidName(name);
|
|
16517
16665
|
const parent = await this.openDir(parentPath);
|
|
@@ -19488,9 +19636,9 @@ class Client {
|
|
|
19488
19636
|
// GC. `storage.writeFile` would additionally index the manifest
|
|
19489
19637
|
// in the space — a second, diverging source of truth for a file
|
|
19490
19638
|
// whose handle the VFS already holds.
|
|
19491
|
-
write: async (data, meta) => {
|
|
19639
|
+
write: async (data, meta, opts) => {
|
|
19492
19640
|
const space = await resolveContentSpace();
|
|
19493
|
-
const { manifest, stat } = await space.putFile(data, { name: meta.name, type: meta.type });
|
|
19641
|
+
const { manifest, stat } = await space.putFile(data, { name: meta.name, type: meta.type }, { onProgress: opts?.onProgress });
|
|
19494
19642
|
return { manifest, size: stat.size };
|
|
19495
19643
|
},
|
|
19496
19644
|
// Reads go through the manifest alone — no space needed. That is
|