The template banks a browser's seats in a cookie the server sets, and gives a lost seat back by name to the address it was last used from
A seat lived in the browser's storage alone, and Safari clears that after a week away, or at a word from the person on another device. Now every seat taken is banked as well in an HttpOnly cookie the server writes, which such clearing leaves alone, and the hall takes its seats back from the bank on arrival. For a browser that lost even the cookie, the ledger notes the address each seat was last used from, as a salted hash, and a name given in the hall or at the table gets its seats back from the same address, with a new token; where the address differs, or two people at the table share it, the table is named so the host can mint a phrase. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
1 parent
6a9dc5a496
commit
dcb18b2b7e
7 files changed
+318
-8
No files matched your search
@@ -117,12 +117,45 @@
|
||||
room = await Room.join(roomId, name.trim());
|
||||
joining = false;
|
||||
} catch (err) {
|
||||
// A seat already under this name may be this person's own, lost with the browser's storage: it is looked for first.
|
||||
if (err instanceof ServerError && err.status === 409 && (await sitBackDown())) return;
|
||||
error = err instanceof Error ? err.message : 'The seat could not be taken.';
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The seat under this name at this table, back again if it was last used from this address; true when it was. */
|
||||
async function sitBackDown(): Promise<boolean> {
|
||||
const r = await api.findSeats(name.trim(), roomId).catch(() => null);
|
||||
const mine = r?.seated.find((s) => s.roomId === roomId);
|
||||
if (!mine) {
|
||||
if (r?.elsewhere.length) error = `A seat here is under that name, last used from another address. Ask the host for a transfer phrase, or write to the keeper.`;
|
||||
return false;
|
||||
}
|
||||
rememberSeat(roomId, { seat: mine.seat, token: mine.token });
|
||||
trustSeat(roomId);
|
||||
seatLost = '';
|
||||
room = await Room.resume(roomId, { seat: mine.seat, token: mine.token });
|
||||
joining = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** From the gallery of a game begun: the seat under this name, if it was last used from here. */
|
||||
async function findMySeat(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
busy = true;
|
||||
error = '';
|
||||
try {
|
||||
rememberName(name);
|
||||
if (!(await sitBackDown()) && !error) error = 'No seat here is under that name.';
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'The seat could not be found.';
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(link);
|
||||
@@ -247,7 +280,11 @@
|
||||
<div class="notice muted">
|
||||
{#if seatLost}<p>{seatLost} Claim it here, or <a href="mailto:eric@ericwagoner.com?subject={encodeURIComponent(`My seat in room ${roomId}`)}">write to the keeper</a>.</p>
|
||||
{:else}{#if room.view.expected.length > 0}<p>{room.nameOf(room.view.host)} began the game before {room.view.expected.join(' and ')} arrived, and the seat held for {room.view.expected.length === 1 ? 'them' : 'each'} went with it.</p>{/if}
|
||||
<p>Had a seat at this table? Ask the host for a transfer phrase, or make one on the device that still holds the seat, and claim it here. Or <a href="mailto:eric@ericwagoner.com?subject={encodeURIComponent(`My seat in room ${roomId}`)}">write to the keeper</a> for a link that brings it back.</p>{/if}
|
||||
<p>Had a seat at this table? Give your name, and if the seat was last used from here it is yours again. Otherwise ask the host for a transfer phrase, or make one on the device that still holds the seat, and claim it here. Or <a href="mailto:eric@ericwagoner.com?subject={encodeURIComponent(`My seat in room ${roomId}`)}">write to the keeper</a> for a link that brings it back.</p>{/if}
|
||||
<form class="sit" onsubmit={findMySeat}>
|
||||
<input type="text" bind:value={name} maxlength={NAME_MAX} placeholder="your name at this table" autocomplete="nickname" aria-label="your name at this table" />
|
||||
<button type="submit" class="quiet" disabled={busy || !name.trim()}>Sit back down</button>
|
||||
</form>
|
||||
<form class="sit" onsubmit={claim}>
|
||||
<input type="text" bind:value={claimPhrase} placeholder="the four-word phrase" autocomplete="off" aria-label="transfer phrase" />
|
||||
<button type="submit" class="quiet" disabled={claiming || claimPhrase.trim().split(/[\s-]+/).length < 4}>Claim the seat</button>
|
||||
|
||||
Reference in new issue
Block a user