@muhkoo/connect 0.14.0-alpha.2 → 0.14.0-alpha.4
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/connect.d.ts
CHANGED
|
@@ -1452,6 +1452,13 @@ interface ShardClientOptions {
|
|
|
1452
1452
|
* just-uploaded file reads back offline. See {@link ShardByteCache}.
|
|
1453
1453
|
*/
|
|
1454
1454
|
cache?: ShardByteCache;
|
|
1455
|
+
/**
|
|
1456
|
+
* Coalesce concurrent origin reads into `POST {pathPrefix}/batch`. On by
|
|
1457
|
+
* default: it is what keeps opening a project from costing one request per
|
|
1458
|
+
* shard. Set false to force one-at-a-time — useful against a server without
|
|
1459
|
+
* the route, though that is detected and handled automatically.
|
|
1460
|
+
*/
|
|
1461
|
+
batchShards?: boolean;
|
|
1455
1462
|
/**
|
|
1456
1463
|
* Called when a PUT can't reach the network (offline). Lets the caller queue
|
|
1457
1464
|
* the upload for replay on reconnect; the bytes are already in {@link cache}
|
|
@@ -1486,6 +1493,11 @@ declare class ShardClient {
|
|
|
1486
1493
|
private readonly pathPrefix;
|
|
1487
1494
|
private readonly fetchFn;
|
|
1488
1495
|
private readonly cache?;
|
|
1496
|
+
/** Hashes waiting for the next batch, each with everyone awaiting it. */
|
|
1497
|
+
private pendingShards;
|
|
1498
|
+
private batchTimer;
|
|
1499
|
+
/** False once the server is known not to have the batch route. */
|
|
1500
|
+
private batchSupported;
|
|
1489
1501
|
private readonly deferUpload?;
|
|
1490
1502
|
private readonly peers?;
|
|
1491
1503
|
private readonly backgroundUpload;
|
|
@@ -1516,9 +1528,31 @@ declare class ShardClient {
|
|
|
1516
1528
|
* `null` for 404 so callers can distinguish "missing" from "errored".
|
|
1517
1529
|
*/
|
|
1518
1530
|
getShard(hash: string): Promise<Uint8Array | null>;
|
|
1531
|
+
/**
|
|
1532
|
+
* Fetch from origin, COALESCING concurrent requests into one batch.
|
|
1533
|
+
*
|
|
1534
|
+
* A shard is not a file. A project is thousands of small source modules,
|
|
1535
|
+
* each split into shards, so reading one at a time made opening a project
|
|
1536
|
+
* cost thousands of requests — 2,086 for a 519-file project. Latency, not
|
|
1537
|
+
* bytes, is what that spends: Chromium took 18.4s over those requests and
|
|
1538
|
+
* Brave, which filters every one, took 37.5s for identical work. Batching
|
|
1539
|
+
* collapses whatever is in flight at the same moment into a single POST,
|
|
1540
|
+
* which is why it helps most exactly where it hurt most.
|
|
1541
|
+
*
|
|
1542
|
+
* Callers are unchanged: `getShard` still asks for one shard and gets one
|
|
1543
|
+
* shard. The window is a few milliseconds — long enough to gather the reads
|
|
1544
|
+
* a concurrent walk issues together, short enough not to be felt by a lone
|
|
1545
|
+
* read.
|
|
1546
|
+
*
|
|
1547
|
+
* Falls back permanently to one-at-a-time if the server has no batch route,
|
|
1548
|
+
* so a new SDK keeps working against an older deployment.
|
|
1549
|
+
*/
|
|
1550
|
+
private fetchFromOrigin;
|
|
1551
|
+
/** Send one batch of whatever has accumulated, and settle those callers. */
|
|
1552
|
+
private flushShardBatch;
|
|
1519
1553
|
/** Fetch a shard from origin. Returns `null` for 404 (or offline when a
|
|
1520
1554
|
* cache is present, so RS recovery can proceed); throws otherwise. */
|
|
1521
|
-
private
|
|
1555
|
+
private fetchOneFromOrigin;
|
|
1522
1556
|
/**
|
|
1523
1557
|
* Check whether a shard exists. Lets the caller skip a PUT they'd otherwise
|
|
1524
1558
|
* dedup-rewrite. The server can implement this as a HEAD or return a small
|
package/dist/server/index.js
CHANGED
|
@@ -3235,7 +3235,7 @@ function base64UrlDecode(s) {
|
|
|
3235
3235
|
* expect (and isn't serving a stale cached bundle). Keep this in sync with
|
|
3236
3236
|
* `package.json` `version`.
|
|
3237
3237
|
*/
|
|
3238
|
-
const VERSION = "0.14.0-alpha.
|
|
3238
|
+
const VERSION = "0.14.0-alpha.4";
|
|
3239
3239
|
|
|
3240
3240
|
/**
|
|
3241
3241
|
* `HttpClient` — the one place the unified {@link Client} talks to the
|
|
@@ -7207,6 +7207,23 @@ class ReedSolomonCodec {
|
|
|
7207
7207
|
* skip a PUT they'd otherwise dedup-rewrite. The
|
|
7208
7208
|
* server can ignore the optimization if it wants.
|
|
7209
7209
|
*/
|
|
7210
|
+
/**
|
|
7211
|
+
* How long concurrent shard reads are gathered before one batch is sent.
|
|
7212
|
+
*
|
|
7213
|
+
* Long enough to collect what a concurrent directory walk issues at the same
|
|
7214
|
+
* moment, short enough that a single read does not feel it.
|
|
7215
|
+
*/
|
|
7216
|
+
const SHARD_BATCH_WINDOW_MS = 6;
|
|
7217
|
+
/** Hashes per batch. Must not exceed what the server will accept. */
|
|
7218
|
+
const SHARD_BATCH_MAX = 256;
|
|
7219
|
+
/** Decode base64 without `atob`'s per-character loop cost on large shards. */
|
|
7220
|
+
function bytesFromBase64(b64) {
|
|
7221
|
+
const binary = atob(b64);
|
|
7222
|
+
const out = new Uint8Array(binary.length);
|
|
7223
|
+
for (let i = 0; i < binary.length; i++)
|
|
7224
|
+
out[i] = binary.charCodeAt(i);
|
|
7225
|
+
return out;
|
|
7226
|
+
}
|
|
7210
7227
|
/** Peer head-start (ms) before origin joins the race in {@link ShardClient.getShard}. */
|
|
7211
7228
|
const HEDGE_MS = 500;
|
|
7212
7229
|
/**
|
|
@@ -7226,6 +7243,11 @@ class ShardClient {
|
|
|
7226
7243
|
pathPrefix;
|
|
7227
7244
|
fetchFn;
|
|
7228
7245
|
cache;
|
|
7246
|
+
/** Hashes waiting for the next batch, each with everyone awaiting it. */
|
|
7247
|
+
pendingShards;
|
|
7248
|
+
batchTimer;
|
|
7249
|
+
/** False once the server is known not to have the batch route. */
|
|
7250
|
+
batchSupported;
|
|
7229
7251
|
deferUpload;
|
|
7230
7252
|
peers;
|
|
7231
7253
|
backgroundUpload;
|
|
@@ -7245,6 +7267,11 @@ class ShardClient {
|
|
|
7245
7267
|
}
|
|
7246
7268
|
this.fetchFn = f.bind(globalThis);
|
|
7247
7269
|
this.cache = opts.cache;
|
|
7270
|
+
// Coalescing state. `batchSupported` latches false against a deployment
|
|
7271
|
+
// without the batch route, so we degrade once rather than per request.
|
|
7272
|
+
this.pendingShards = new Map();
|
|
7273
|
+
this.batchTimer = null;
|
|
7274
|
+
this.batchSupported = opts.batchShards !== false;
|
|
7248
7275
|
this.deferUpload = opts.deferUpload;
|
|
7249
7276
|
this.peers = opts.peers;
|
|
7250
7277
|
// Background upload needs a local cache to hold the bytes until the PUT
|
|
@@ -7421,9 +7448,124 @@ class ShardClient {
|
|
|
7421
7448
|
}, () => startOrigin());
|
|
7422
7449
|
});
|
|
7423
7450
|
}
|
|
7451
|
+
/**
|
|
7452
|
+
* Fetch from origin, COALESCING concurrent requests into one batch.
|
|
7453
|
+
*
|
|
7454
|
+
* A shard is not a file. A project is thousands of small source modules,
|
|
7455
|
+
* each split into shards, so reading one at a time made opening a project
|
|
7456
|
+
* cost thousands of requests — 2,086 for a 519-file project. Latency, not
|
|
7457
|
+
* bytes, is what that spends: Chromium took 18.4s over those requests and
|
|
7458
|
+
* Brave, which filters every one, took 37.5s for identical work. Batching
|
|
7459
|
+
* collapses whatever is in flight at the same moment into a single POST,
|
|
7460
|
+
* which is why it helps most exactly where it hurt most.
|
|
7461
|
+
*
|
|
7462
|
+
* Callers are unchanged: `getShard` still asks for one shard and gets one
|
|
7463
|
+
* shard. The window is a few milliseconds — long enough to gather the reads
|
|
7464
|
+
* a concurrent walk issues together, short enough not to be felt by a lone
|
|
7465
|
+
* read.
|
|
7466
|
+
*
|
|
7467
|
+
* Falls back permanently to one-at-a-time if the server has no batch route,
|
|
7468
|
+
* so a new SDK keeps working against an older deployment.
|
|
7469
|
+
*/
|
|
7470
|
+
fetchFromOrigin(hash) {
|
|
7471
|
+
if (!this.batchSupported)
|
|
7472
|
+
return this.fetchOneFromOrigin(hash);
|
|
7473
|
+
return new Promise((resolve, reject) => {
|
|
7474
|
+
const waiting = this.pendingShards.get(hash);
|
|
7475
|
+
if (waiting) {
|
|
7476
|
+
waiting.push({ resolve, reject });
|
|
7477
|
+
return;
|
|
7478
|
+
}
|
|
7479
|
+
this.pendingShards.set(hash, [{ resolve, reject }]);
|
|
7480
|
+
if (this.batchTimer === null) {
|
|
7481
|
+
this.batchTimer = setTimeout(() => { void this.flushShardBatch(); }, SHARD_BATCH_WINDOW_MS);
|
|
7482
|
+
}
|
|
7483
|
+
});
|
|
7484
|
+
}
|
|
7485
|
+
/** Send one batch of whatever has accumulated, and settle those callers. */
|
|
7486
|
+
async flushShardBatch() {
|
|
7487
|
+
this.batchTimer = null;
|
|
7488
|
+
if (this.pendingShards.size === 0)
|
|
7489
|
+
return;
|
|
7490
|
+
const hashes = [...this.pendingShards.keys()].slice(0, SHARD_BATCH_MAX);
|
|
7491
|
+
const waiters = hashes.map((h) => {
|
|
7492
|
+
const w = this.pendingShards.get(h);
|
|
7493
|
+
this.pendingShards.delete(h);
|
|
7494
|
+
return [h, w];
|
|
7495
|
+
});
|
|
7496
|
+
// Anything over the cap waits for the next window rather than being
|
|
7497
|
+
// dropped or silently truncated.
|
|
7498
|
+
if (this.pendingShards.size > 0 && this.batchTimer === null) {
|
|
7499
|
+
this.batchTimer = setTimeout(() => { void this.flushShardBatch(); }, SHARD_BATCH_WINDOW_MS);
|
|
7500
|
+
}
|
|
7501
|
+
let shards;
|
|
7502
|
+
try {
|
|
7503
|
+
const res = await this.fetchFn(`${this.baseUrl}${this.pathPrefix}/batch`, {
|
|
7504
|
+
method: "POST",
|
|
7505
|
+
headers: { "Content-Type": "application/json" },
|
|
7506
|
+
body: JSON.stringify({ hashes }),
|
|
7507
|
+
});
|
|
7508
|
+
if (res.status === 404 || res.status === 405) {
|
|
7509
|
+
// No batch route on this deployment. Stop trying, and serve
|
|
7510
|
+
// everyone individually — including the batch we just gathered.
|
|
7511
|
+
this.batchSupported = false;
|
|
7512
|
+
for (const [h, ws] of waiters) {
|
|
7513
|
+
this.fetchOneFromOrigin(h).then((b) => ws.forEach((w) => w.resolve(b)), (e) => ws.forEach((w) => w.reject(e)));
|
|
7514
|
+
}
|
|
7515
|
+
return;
|
|
7516
|
+
}
|
|
7517
|
+
if (!res.ok)
|
|
7518
|
+
throw new Error(`ShardClient batch failed (${res.status}): ${await safeText(res)}`);
|
|
7519
|
+
const parsed = await res.json().catch(() => null);
|
|
7520
|
+
// A 200 is not proof the batch route exists. A catch-all handler, a
|
|
7521
|
+
// proxy, or an older deployment can answer this POST with something
|
|
7522
|
+
// else entirely, and trusting the shape would resolve every hash to
|
|
7523
|
+
// `null` — reads silently returning "missing" for shards that are
|
|
7524
|
+
// right there. Anything not shaped like a batch reply is treated as
|
|
7525
|
+
// "no batch route", exactly like a 404.
|
|
7526
|
+
const candidate = parsed?.shards;
|
|
7527
|
+
if (!candidate || typeof candidate !== "object") {
|
|
7528
|
+
this.batchSupported = false;
|
|
7529
|
+
for (const [h, ws] of waiters) {
|
|
7530
|
+
this.fetchOneFromOrigin(h).then((b) => ws.forEach((w) => w.resolve(b)), (e) => ws.forEach((w) => w.reject(e)));
|
|
7531
|
+
}
|
|
7532
|
+
return;
|
|
7533
|
+
}
|
|
7534
|
+
shards = candidate;
|
|
7535
|
+
}
|
|
7536
|
+
catch (err) {
|
|
7537
|
+
// Offline with a cache behaves as it does for a single fetch: report
|
|
7538
|
+
// absence so Reed-Solomon recovery can still be attempted.
|
|
7539
|
+
for (const [, ws] of waiters) {
|
|
7540
|
+
if (this.cache)
|
|
7541
|
+
ws.forEach((w) => w.resolve(null));
|
|
7542
|
+
else
|
|
7543
|
+
ws.forEach((w) => w.reject(err));
|
|
7544
|
+
}
|
|
7545
|
+
return;
|
|
7546
|
+
}
|
|
7547
|
+
for (const [hash, ws] of waiters) {
|
|
7548
|
+
const b64 = shards[hash];
|
|
7549
|
+
if (typeof b64 !== "string") {
|
|
7550
|
+
ws.forEach((w) => w.resolve(null));
|
|
7551
|
+
continue;
|
|
7552
|
+
}
|
|
7553
|
+
let bytes;
|
|
7554
|
+
try {
|
|
7555
|
+
bytes = bytesFromBase64(b64);
|
|
7556
|
+
}
|
|
7557
|
+
catch (e) {
|
|
7558
|
+
ws.forEach((w) => w.reject(e));
|
|
7559
|
+
continue;
|
|
7560
|
+
}
|
|
7561
|
+
if (this.cache)
|
|
7562
|
+
void this.cache.put(hash, bytes);
|
|
7563
|
+
ws.forEach((w) => w.resolve(bytes));
|
|
7564
|
+
}
|
|
7565
|
+
}
|
|
7424
7566
|
/** Fetch a shard from origin. Returns `null` for 404 (or offline when a
|
|
7425
7567
|
* cache is present, so RS recovery can proceed); throws otherwise. */
|
|
7426
|
-
async
|
|
7568
|
+
async fetchOneFromOrigin(hash) {
|
|
7427
7569
|
const url = this.urlForHash(hash);
|
|
7428
7570
|
let response;
|
|
7429
7571
|
try {
|