fix(canvas): add auth headers to API fetches + deduplicate sync events

- Add Authorization header to fetchTripData, fetchTripDetail, and
  fetchNotesData to prevent 401 errors on private spaces
- Add #initialSyncFired flag to CommunitySync so the "synced" event
  only fires once per connection cycle instead of on every debounce gap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-22 16:28:58 -07:00
parent 29c4f48634
commit 2c0fbb76ac
2 changed files with 16 additions and 4 deletions

View File

@ -161,6 +161,7 @@ export class CommunitySync extends EventTarget {
#offlineStore: OfflineStore | null = null;
#saveDebounceTimer: ReturnType<typeof setTimeout> | null = null;
#syncedDebounceTimer: ReturnType<typeof setTimeout> | null = null;
#initialSyncFired = false;
#wsUrl: string | null = null;
// ── Undo/Redo state ──
@ -292,6 +293,7 @@ export class CommunitySync extends EventTarget {
this.#ws.onclose = () => {
console.log(`[CommunitySync] Disconnected from ${this.#communitySlug}`);
this.#initialSyncFired = false;
this.dispatchEvent(new CustomEvent("disconnected"));
if (!this.#disconnectedIntentionally) {
@ -844,10 +846,12 @@ export class CommunitySync extends EventTarget {
// Debounce the synced event — during initial sync negotiation, #applyDocToDOM()
// is called for every Automerge sync message (100+ round-trips). Debounce to
// fire once after the burst settles.
// fire once after the burst settles. Only fires once per connection cycle.
if (this.#initialSyncFired) return;
if (this.#syncedDebounceTimer) clearTimeout(this.#syncedDebounceTimer);
this.#syncedDebounceTimer = setTimeout(() => {
this.#syncedDebounceTimer = null;
this.#initialSyncFired = true;
this.dispatchEvent(new CustomEvent("synced", { detail: { shapes } }));
}, 300);
}

View File

@ -3088,10 +3088,18 @@
let _tripCache = null; // { trips: [], detail: null, fetchedAt: 0 }
const TRIP_CACHE_TTL = 60000; // 1 minute
function _authHeaders() {
try {
const s = JSON.parse(localStorage.getItem('encryptid_session') || '{}');
if (s?.accessToken) return { 'Authorization': 'Bearer ' + s.accessToken };
} catch {}
return {};
}
async function fetchTripData() {
if (_tripCache && Date.now() - _tripCache.fetchedAt < TRIP_CACHE_TTL) return _tripCache;
try {
const res = await fetch(`/${communitySlug}/rtrips/api/trips`);
const res = await fetch(`/${communitySlug}/rtrips/api/trips`, { headers: _authHeaders() });
if (!res.ok) return { trips: [], detail: null, fetchedAt: Date.now() };
const trips = await res.json();
_tripCache = { trips, detail: null, fetchedAt: Date.now() };
@ -3101,7 +3109,7 @@
async function fetchTripDetail(tripId) {
try {
const res = await fetch(`/${communitySlug}/rtrips/api/trips/${tripId}`);
const res = await fetch(`/${communitySlug}/rtrips/api/trips/${tripId}`, { headers: _authHeaders() });
if (!res.ok) return null;
return await res.json();
} catch { return null; }
@ -3145,7 +3153,7 @@
async function fetchNotesData() {
if (_notesCache && Date.now() - _notesCache.fetchedAt < TRIP_CACHE_TTL) return _notesCache;
try {
const res = await fetch(`/${communitySlug}/rnotes/api/notes?limit=50`);
const res = await fetch(`/${communitySlug}/rnotes/api/notes?limit=50`, { headers: _authHeaders() });
if (!res.ok) return { notes: [], fetchedAt: Date.now() };
const data = await res.json();
_notesCache = { notes: data.notes || [], fetchedAt: Date.now() };