@muhkoo/connect 0.15.0-alpha.1 → 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 +147 -28
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +30 -10
- package/dist/server/index.js +147 -28
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/connect.d.ts
CHANGED
|
@@ -2463,7 +2463,7 @@ declare class ShardClient {
|
|
|
2463
2463
|
private readonly pathPrefix;
|
|
2464
2464
|
private readonly fetchFn;
|
|
2465
2465
|
private readonly cache?;
|
|
2466
|
-
/** Hashes waiting for the next batch, each with everyone awaiting it. */
|
|
2466
|
+
/** Hashes waiting for the next batch, each with everyone awaiting it and its size if a caller gave it. */
|
|
2467
2467
|
private pendingShards;
|
|
2468
2468
|
private batchTimer;
|
|
2469
2469
|
/** False once the server is known not to have the batch route. */
|
|
@@ -2496,8 +2496,13 @@ declare class ShardClient {
|
|
|
2496
2496
|
/**
|
|
2497
2497
|
* Fetch a shard by hash. Throws if the server returns a non-200; returns
|
|
2498
2498
|
* `null` for 404 so callers can distinguish "missing" from "errored".
|
|
2499
|
+
*
|
|
2500
|
+
* `size`, the shard's byte length when known (a manifest's `shardSize`),
|
|
2501
|
+
* lets concurrent reads be batched within what the server will send.
|
|
2499
2502
|
*/
|
|
2500
|
-
getShard(hash: string
|
|
2503
|
+
getShard(hash: string, opts?: {
|
|
2504
|
+
size?: number;
|
|
2505
|
+
}): Promise<Uint8Array | null>;
|
|
2501
2506
|
/**
|
|
2502
2507
|
* Fetch from origin, COALESCING concurrent requests into one batch.
|
|
2503
2508
|
*
|
|
@@ -2520,6 +2525,8 @@ declare class ShardClient {
|
|
|
2520
2525
|
private fetchFromOrigin;
|
|
2521
2526
|
/** Send one batch of whatever has accumulated, and settle those callers. */
|
|
2522
2527
|
private flushShardBatch;
|
|
2528
|
+
/** POST one batch and settle everyone waiting on it. Never throws. */
|
|
2529
|
+
private sendShardBatch;
|
|
2523
2530
|
/** Fetch a shard from origin. Returns `null` for 404 (or offline when a
|
|
2524
2531
|
* cache is present, so RS recovery can proceed); throws otherwise. */
|
|
2525
2532
|
private fetchOneFromOrigin;
|
|
@@ -6737,11 +6744,19 @@ declare class VfsLimitError extends Error {
|
|
|
6737
6744
|
* Every mutation is a read-modify-write of exactly ONE directory record, so a
|
|
6738
6745
|
* write cannot leave the tree half-updated: either the parent names the new
|
|
6739
6746
|
* child or it does not. Two tabs writing different directories never conflict;
|
|
6740
|
-
* two tabs writing the SAME directory
|
|
6741
|
-
*
|
|
6742
|
-
*
|
|
6743
|
-
*
|
|
6747
|
+
* two tabs (or devices) writing the SAME directory are last-writer-wins. A VFS
|
|
6748
|
+
* write sends no HLC and the personal space has no compare-and-swap: it applies
|
|
6749
|
+
* puts of one key in the order they arrive, so whichever arrives last is kept,
|
|
6750
|
+
* and the other tab's edit is lost. Orphaned records (a directory whose parent
|
|
6751
|
+
* entry lost the race) are unreachable rather than corrupting — they cost
|
|
6752
|
+
* storage until a future sweep, which is the trade this shape makes in
|
|
6744
6753
|
* exchange for single-record writes.
|
|
6754
|
+
*
|
|
6755
|
+
* Within ONE tab, writes of a record are queued and sent one at a time, in the
|
|
6756
|
+
* order they were made, and a read that has to fetch a record waits for its
|
|
6757
|
+
* write in flight (see `writing`). That ordering is per namespace instance
|
|
6758
|
+
* only; nothing orders writes across tabs or devices - that would take a
|
|
6759
|
+
* compare-and-swap at the server.
|
|
6745
6760
|
*/
|
|
6746
6761
|
|
|
6747
6762
|
/**
|
|
@@ -6828,7 +6843,9 @@ declare class VfsNamespace {
|
|
|
6828
6843
|
* that is often the one sent first. Then the earlier write, sealed before
|
|
6829
6844
|
* the later edit was made, undoes it. That is how a file deleted forever
|
|
6830
6845
|
* from the Trash, while the move that put it there was still being
|
|
6831
|
-
* written, came back. So each write waits for the one before it
|
|
6846
|
+
* written, came back. So each write waits for the one before it, and a
|
|
6847
|
+
* read that has to fetch the record waits for its write too: see
|
|
6848
|
+
* `openDirById`.
|
|
6832
6849
|
*/
|
|
6833
6850
|
private readonly writing;
|
|
6834
6851
|
private readonly historyLimit;
|
|
@@ -7052,8 +7069,11 @@ declare class VfsNamespace {
|
|
|
7052
7069
|
* Delete metadata records no longer reachable from the root.
|
|
7053
7070
|
*
|
|
7054
7071
|
* These accumulate because every write touches exactly one record: if two
|
|
7055
|
-
* tabs edit the same directory,
|
|
7056
|
-
*
|
|
7072
|
+
* tabs or devices edit the same directory, the personal space keeps
|
|
7073
|
+
* whichever put arrives last (a VFS write sends no HLC, and the space
|
|
7074
|
+
* applies puts of one key in arrival order), which can leave a child whose
|
|
7075
|
+
* parent entry lost the race. The per-record write queue (`writing`) only
|
|
7076
|
+
* orders writes within one tab. Unreachable is harmless —
|
|
7057
7077
|
* it is invisible and undecryptable-by-nobody — but it costs storage, so
|
|
7058
7078
|
* this reclaims it.
|
|
7059
7079
|
*
|
|
@@ -8998,7 +9018,7 @@ declare function getId(row: {
|
|
|
8998
9018
|
* expect (and isn't serving a stale cached bundle). Keep this in sync with
|
|
8999
9019
|
* `package.json` `version`.
|
|
9000
9020
|
*/
|
|
9001
|
-
declare const VERSION = "0.15.0-alpha.
|
|
9021
|
+
declare const VERSION = "0.15.0-alpha.2";
|
|
9002
9022
|
|
|
9003
9023
|
/**
|
|
9004
9024
|
* The SDK's process-global logger.
|
package/dist/server/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) {
|
|
@@ -15467,7 +15539,9 @@ class VfsNamespace {
|
|
|
15467
15539
|
* that is often the one sent first. Then the earlier write, sealed before
|
|
15468
15540
|
* the later edit was made, undoes it. That is how a file deleted forever
|
|
15469
15541
|
* from the Trash, while the move that put it there was still being
|
|
15470
|
-
* written, came back. So each write waits for the one before it
|
|
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`.
|
|
15471
15545
|
*/
|
|
15472
15546
|
writing = new Map();
|
|
15473
15547
|
historyLimit;
|
|
@@ -16220,8 +16294,11 @@ class VfsNamespace {
|
|
|
16220
16294
|
* Delete metadata records no longer reachable from the root.
|
|
16221
16295
|
*
|
|
16222
16296
|
* These accumulate because every write touches exactly one record: if two
|
|
16223
|
-
* tabs edit the same directory,
|
|
16224
|
-
*
|
|
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 —
|
|
16225
16302
|
* it is invisible and undecryptable-by-nobody — but it costs storage, so
|
|
16226
16303
|
* this reclaims it.
|
|
16227
16304
|
*
|
|
@@ -16379,7 +16456,41 @@ class VfsNamespace {
|
|
|
16379
16456
|
// every concurrent reader to miss and fire its own request for the same
|
|
16380
16457
|
// record — which is how loading one project turned into thousands of
|
|
16381
16458
|
// identical `vfs/d/<id>` fetches, most of them in flight at once.
|
|
16382
|
-
|
|
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
|
+
}
|
|
16383
16494
|
const raw = await this.deps.store.get(dirKey(id));
|
|
16384
16495
|
// An absent record is an EMPTY directory, not an error: the root
|
|
16385
16496
|
// exists before anything has been written to it, and a directory is
|
|
@@ -16405,7 +16516,8 @@ class VfsNamespace {
|
|
|
16405
16516
|
throw err;
|
|
16406
16517
|
}
|
|
16407
16518
|
return this.readRecord(id, key, value, path);
|
|
16408
|
-
}
|
|
16519
|
+
};
|
|
16520
|
+
const pending = load().catch((err) => {
|
|
16409
16521
|
// A failed read must not be remembered, or one blip poisons the
|
|
16410
16522
|
// directory for the life of the session.
|
|
16411
16523
|
if (this.cache.get(id) === pending)
|
|
@@ -16506,7 +16618,14 @@ class VfsNamespace {
|
|
|
16506
16618
|
// change the store never took - a deleted file gone from every
|
|
16507
16619
|
// listing while it is still stored, and a retry told it does not
|
|
16508
16620
|
// exist. Drop it, so the next read asks the store.
|
|
16509
|
-
|
|
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);
|
|
16510
16629
|
throw err;
|
|
16511
16630
|
}
|
|
16512
16631
|
finally {
|