Skip to content

Commit bc2afc4

Browse files
committed
feat(kv-web): implement reconnects and bucket caching
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
1 parent 711da0f commit bc2afc4

File tree

1 file changed

+69
-60
lines changed

1 file changed

+69
-60
lines changed

examples/web/ui/index.html

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
}
3333
</style>
3434
<script type="module">
35+
let conn;
36+
let buckets = new Map();
37+
3538
function toHex(v) {
3639
if (!v) return '';
3740
return v.reduce((s, x) => s + x.toString(16).padStart(2, '0'), '')
@@ -282,15 +285,14 @@
282285
}
283286

284287
async function getBucket() {
285-
if (!transport) {
286-
throw 'transport not connected';
287-
}
288+
if (!conn) throw 'WebTransport not connected';
289+
288290
const obj = getFormValues('#settings');
289291
let identifier = '';
290-
let conn = obj.connection;
291-
if (conn === 'mem') {
292+
let proto = obj.proto;
293+
if (proto === 'mem') {
292294
identifier = '';
293-
} else if (conn === 'nats') {
295+
} else if (proto === 'nats') {
294296
const addr = obj['nats-addr'];
295297
if (!addr) throw 'NATS.io server address must be set';
296298
identifier = 'wrpc+nats://' + addr;
@@ -300,55 +302,57 @@
300302

301303
const bucket = obj['nats-bucket'];
302304
if (bucket) identifier = identifier + ';' + bucket;
303-
} else if (conn === 'redis') {
305+
} else if (proto === 'redis') {
304306
const url = obj['redis-url'];
305307
if (!url) throw 'Redis URL must be set';
306308
identifier = url;
307-
} else if (conn === 'quic') {
309+
} else if (proto === 'quic') {
308310
const addr = obj['quic-addr'];
309311
if (!addr) throw 'QUIC address must be set';
310312
identifier = 'wrpc+quic://' + addr;
311313

312314
const bucket = obj['quic-bucket'];
313315
if (bucket) identifier = identifier + ';' + bucket;
314-
} else if (conn === 'tcp') {
316+
} else if (proto === 'tcp') {
315317
const addr = obj['tcp-addr'];
316318
if (!addr) throw 'TCP address must be set';
317319
identifier = 'wrpc+tcp://' + addr;
318320

319321
const bucket = obj['tcp-bucket'];
320322
if (bucket) identifier = identifier + ';' + bucket;
321-
} else if (conn === 'unix') {
323+
} else if (proto === 'unix') {
322324
const path = obj['unix-path'];
323325
if (!path) throw 'Unix Domain Socket path must be set';
324326
identifier = 'wrpc+unix://' + path;
325327

326328
const bucket = obj['unix-bucket']
327329
if (bucket) identifier = identifier + ';' + bucket;
328-
} else if (conn === 'web') {
330+
} else if (proto === 'web') {
329331
const addr = obj['web-addr'];
330332
if (!addr) throw 'WebTransport address must be set';
331333
identifier = 'wrpc+web://' + addr;
332334

333335
const bucket = obj['web-bucket'];
334336
if (bucket) identifier = identifier + ';' + bucket;
335337
} else {
336-
throw 'transport not supported yet';
338+
throw 'selected wRPC transport not supported yet';
337339
}
338340

339341
if (identifier.length > 127) throw 'this example currently does not support identifiers longer than 127 bytes - open a PR!';
340342

341-
// TODO: cache the bucket by URL (it should not persist across server restarts)
343+
const bucket = buckets.get(identifier);
344+
if (bucket) return bucket;
342345

343346
dbg.debug('creating `open` stream...');
344-
const stream = await transport.createBidirectionalStream();
347+
const stream = await conn.createBidirectionalStream();
345348
return await wasiKeyvalueStoreOpen(
346349
stream.writable.getWriter(),
347350
stream.readable.getReader(),
348351
identifier,
349352
).then(bucket => {
350353
const bucketName = uuidString(bucket);
351354
dbg.log(`opened bucket ${bucketName}`);
355+
buckets.set(identifier, bucket);
352356
return bucket;
353357
}, err => {
354358
throw 'failed to open bucket: ' + err;
@@ -360,7 +364,8 @@
360364
const getValue = document.querySelector('#get input[name="get-value"]');
361365
if (getValue) getValue.value = null;
362366

363-
if (!transport) throw 'transport not connected';
367+
if (!conn) throw 'WebTransport not connected';
368+
364369
const bucket = await getBucket();
365370
if (!bucket) throw 'failed to get bucket';
366371

@@ -371,7 +376,7 @@
371376
if (key.length > 127) throw 'this example currently does not support keys longer than 127 bytes - open a PR!';
372377

373378
dbg.debug('creating `get` stream...');
374-
const stream = await transport.createBidirectionalStream();
379+
const stream = await conn.createBidirectionalStream();
375380
await wasiKeyvalueStoreBucketGet(
376381
stream.writable.getWriter(),
377382
stream.readable.getReader(),
@@ -392,14 +397,11 @@
392397
}
393398

394399
async function handleSet() {
395-
if (!transport) {
396-
dbg.error('transport not connected')
397-
return
398-
}
400+
if (!conn) throw 'WebTransport not connected';
401+
399402
const bucket = await getBucket();
400-
if (bucket == null) {
401-
return
402-
}
403+
if (!bucket) throw 'failed to get bucket';
404+
403405
const obj = getFormValues('#set');
404406

405407
const key = obj['set-key'];
@@ -416,7 +418,7 @@
416418
if (valueBuf.length > 127) throw 'this example currently does not support values longer than 127 bytes - open a PR!';
417419

418420
dbg.debug('creating `set` stream...');
419-
const stream = await transport.createBidirectionalStream();
421+
const stream = await conn.createBidirectionalStream();
420422
await wasiKeyvalueStoreBucketSet(
421423
stream.writable.getWriter(),
422424
stream.readable.getReader(),
@@ -431,36 +433,6 @@
431433
});
432434
}
433435

434-
async function connect() {
435-
const {PORT, CERT_DIGEST} = await import('./consts.js');
436-
let transport;
437-
438-
dbg.log('connecting to wRPC over WebTransport on `' + PORT + '`...');
439-
try {
440-
transport = new WebTransport('https://localhost:' + PORT, {
441-
serverCertificateHashes: [
442-
{
443-
algorithm: 'sha-256',
444-
value: CERT_DIGEST.buffer
445-
}
446-
]
447-
});
448-
} catch (e) {
449-
dbg.error('failed to connect: ' + e);
450-
return null;
451-
}
452-
453-
dbg.debug('waiting for WebTransport readiness...');
454-
try {
455-
await transport.ready;
456-
} catch (e) {
457-
throw 'failed to await WebTransport readiness: ' + e;
458-
return null;
459-
}
460-
dbg.info('WebTransport connection established');
461-
return transport;
462-
}
463-
464436
const dbg = (() => {
465437
const output = document.querySelector('#message-output');
466438
const className = {
@@ -496,16 +468,16 @@
496468
// @ts-check
497469
function initUI() {
498470
function updateTemplate() {
499-
const option = connectionDropdown?.value ?? 'mem'
471+
const option = protoDropdown?.value ?? 'mem'
500472
const defaultTemplate = document.querySelector('.form-fields[data-option=default]');
501473
const templateOutput = document.querySelector('#template-output');
502474
const template = document.querySelector(`.form-fields[data-option=${option}]`) ?? defaultTemplate;
503475
if (templateOutput && template) templateOutput.innerHTML = template.innerHTML;
504476
}
505477

506478
/** @type {HTMLSelectElement | null} */
507-
const connectionDropdown = document.querySelector('#connection');
508-
connectionDropdown?.addEventListener('change', updateTemplate);
479+
const protoDropdown = document.querySelector('#proto');
480+
protoDropdown?.addEventListener('change', updateTemplate);
509481
updateTemplate();
510482

511483
/** @type {HTMLFormElement | null} */
@@ -538,8 +510,45 @@
538510

539511
initUI();
540512

541-
let transport = await connect().catch(err => dbg.error('failed to connect to server: ' + err));
542-
// TODO: Reconnect on failure
513+
const {PORT, CERT_DIGEST} = await import('./consts.js');
514+
for (; ;) {
515+
dbg.log('connecting to wRPC over WebTransport on `' + PORT + '`...');
516+
let c;
517+
try {
518+
c = new WebTransport('https://localhost:' + PORT, {
519+
serverCertificateHashes: [
520+
{
521+
algorithm: 'sha-256',
522+
value: CERT_DIGEST.buffer
523+
}
524+
]
525+
});
526+
} catch (err) {
527+
dbg.error('failed to establish WebTransport connection: ' + err);
528+
await new Promise(r => setTimeout(r, 1000));
529+
continue;
530+
}
531+
532+
dbg.debug('waiting for WebTransport readiness...');
533+
try {
534+
await c.ready;
535+
} catch (err) {
536+
dbg.error('failed to await WebTransport readiness: ' + err);
537+
await new Promise(r => setTimeout(r, 1000));
538+
continue;
539+
}
540+
541+
conn = c;
542+
dbg.info('WebTransport connection established');
543+
try {
544+
const {closeCode, reason} = await conn.closed;
545+
dbg.log(`WebTransport connection closed with code '${closeCode}': ${reason}`);
546+
} catch (err) {
547+
dbg.error('WebTransport connection failed: ' + err);
548+
}
549+
conn = null;
550+
}
551+
543552
</script>
544553
</head>
545554

@@ -550,7 +559,7 @@ <h1 class="title"><code>wasi:keyvalue</code></h1>
550559
</div>
551560
<div class="column is-narrow">
552561
<div class="select">
553-
<select id="connection" form="settings" name="connection">
562+
<select id="proto" form="settings" name="proto">
554563
<option value="mem">In-memory</option>
555564
<option value="redis">Redis</option>
556565
<option value="nats">wRPC/NATS.io</option>

0 commit comments

Comments
 (0)