Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
539 changes: 270 additions & 269 deletions CLAUDE-index.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4263,6 +4263,20 @@ sub-entry with its line: grep the index for a word, then read this file from tha
roster-length check shows a phantom peer while pending. `$peers` ticks
(`peers.update`) on every open/close, so derive from `openedPeers` (#14 CN; the
Users peers-trigger had this bug too).
- **`peer.id` does NOT go null on a signaling drop any more — and "is signaling up" is
`peer.open`, never `peer.id`** (net-drop-1). peerjs's `Peer.disconnect()`, which runs on every
drop of the signaling socket, sets `id` to null until the socket re-registers; everything
that asked "who am I" during the gap read null. The cloud roles plugin
(`getPeers().peer.id`) counted itself ALONE and wiped its role map, so a joiner came back
from a healed drop a VIEWER whose edits never left it (net-drop B -> A red on every
deployed build and green on a dev server, which loads no plugin). Core's own readers
(own avatar, own locks, shared-library owner, chat, `atscene`) were wrong for the same
seconds. `peerDrop.keepPeerId` puts an own getter on the one Peer core builds (live id,
else the id it was created with); peerjs's `_id`/`_lastServerId`/`open`/`disconnected`
are untouched, and `reconnectDial` checks `peer.disconnected` itself. Suite
`net-drop` (own id at the cut; roles unchanged by the heal under
`E2E_CLOUD_PLUGIN=https://theprototype.pages.dev/cloud-plugin.js`, the way to run any
two-peer suite in the deployed condition on a dev server).
- A fixed overlay anchored under the Connect pill can't use a `position:fixed`
click-catcher sized to the viewport — `.connect-wrap`'s `translateX(-50%)` makes it
the containing block for fixed descendants (the transform gotcha). Close the
Expand Down
27 changes: 27 additions & 0 deletions src/lib/peerDrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,30 @@ export function quietPeerError(type, { lastLeaveAt, reconnecting, now }) {
export function unavailablePeerId(message) {
return String(message ?? '').match(/peer\s+([0-9a-z][0-9a-z_-]{2,})\s*$/i)?.[1] ?? '';
}

/**
* net-drop-1: OUR ID OUTLIVES A SIGNALING DROP. peerjs's `Peer.disconnect()` — run on every
* drop of the signaling socket — sets `peer.id` to null until the socket re-registers, while every
* data conn, lock, roster row and plugin keys on that id, and it comes back unchanged (`reconnect()`
* re-registers the id it remembered; a rebuild reuses `PeerConnection.myId`). Everything asking
* "who am I" during the gap read null: the cloud roles plugin counted itself ALONE, wiped its role
* map, and a joiner came back from a healed drop as a VIEWER whose every edit stayed local (the
* net-drop B -> A loss on deployed builds); in core an own avatar, own locks and own shared-library
* rows read as somebody else's. An own getter on THIS Peer answers the live id while there is one,
* else the id it was created with; peerjs's own bookkeeping (`_id`, `_lastServerId`, `open`,
* `disconnected`) is untouched, so "is the signaling link up" is still `open`, never `id`.
* @template T @param {T} peer @param {string} id @returns {T}
*/
export function keepPeerId(peer, id) {
if (!peer || !id) return peer;
/** @type {(() => any) | undefined} */
let live;
for (let o = Object.getPrototypeOf(peer); o && !live; o = Object.getPrototypeOf(o)) live = Object.getOwnPropertyDescriptor(o, 'id')?.get;
Object.defineProperty(peer, 'id', {
configurable: true,
get() {
return (live ? live.call(this) : null) ?? id;
}
});
return peer;
}
8 changes: 5 additions & 3 deletions src/lib/peerHandler.svelte.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { registerDebugHook } from '$lib/debugHooks';
import { routePerfLive, perfLivePeerGone } from '$lib/perf/liveWire.js'; // 34 PF: the live perf stream (an import-free leaf)
import { noteWireError } from '$lib/wireErrors';
import { composeAdmitNotice, shortPeer } from '$lib/admitNotice';
import { ICE_DROP_GRACE_MS, LINK_POLL_MS, strikesNeeded, quietPeerError, unavailablePeerId, consentCounters, linkSilence } from '$lib/peerDrop';
import { ICE_DROP_GRACE_MS, LINK_POLL_MS, strikesNeeded, quietPeerError, unavailablePeerId, consentCounters, linkSilence, keepPeerId } from '$lib/peerDrop';
import { createDialBackQueue } from '$lib/dialBackQueue'; // scatter-sync: room edits held for an opening approval dial-back
import { noteWire } from '$lib/sceneBudget';
import { applyAnnotation, applyAnnotationsSnapshot, sendAnnotations } from '$lib/annotationsHandler';
Expand Down Expand Up @@ -380,7 +380,8 @@ export class PeerConnection {
// 36-export: an exported game is single-player and opens NO signaling socket — a stub
// Peer that never opens (`connect()` answers undefined, which every dial site already
// guards, the signaling-down rule), so `$peers.peer.id` and every send stay valid
this.peer = exportMode ? /** @type {Peer} */ (offlinePeer(this.myId)) : new Peer(this.myId, options);
// net-drop-1: the id stays readable through a signaling drop (peerDrop.keepPeerId)
this.peer = exportMode ? /** @type {Peer} */ (offlinePeer(this.myId)) : keepPeerId(new Peer(this.myId, options), this.myId);
};

// 27-F (audit H2): THE recreate ritual, in one place. Four callers had their own
Expand Down Expand Up @@ -2156,7 +2157,8 @@ export class PeerConnection {
* Called from the window's timers and when the signaling link comes back.
* @param {string} peerId */
reconnectDial(peerId) {
if (this.connections[peerId] || !(this.peer.id < peerId)) return;
// a Peer off the signaling server cannot dial (its id still reads, keepPeerId): the `open` handler redials
if (this.connections[peerId] || this.peer.disconnected || !(this.peer.id < peerId)) return;
const conn = this.peer.connect(peerId, dialOptions());
if (!conn) return; // the signaling link is down: the next tick (or its return) redials
/** @type {any} */ (conn).__dialedAt = Date.now();
Expand Down
Loading
Loading