fix some connectivity issue
This commit is contained in:
@@ -35,9 +35,15 @@ class FamStore {
|
||||
seasons = $state<Season[]>([]);
|
||||
initialized = $state(false);
|
||||
famId = $state('');
|
||||
// True while the realtime SSE stream is down (subscriptions exist but the
|
||||
// socket dropped, e.g. ERR_QUIC_PROTOCOL_ERROR). The SDK reconnects on its
|
||||
// own; we resync missed events on PB_CONNECT (below) + browser signals.
|
||||
connectionDown = $state(false);
|
||||
|
||||
private unsubs: (() => void)[] = [];
|
||||
private destroyed = false;
|
||||
private seenConnect = false;
|
||||
private resyncing = false;
|
||||
|
||||
memberMap(): Map<string, Member> {
|
||||
return new Map(this.members.map((m) => [m.id, m]));
|
||||
@@ -137,12 +143,115 @@ class FamStore {
|
||||
}
|
||||
|
||||
await this.subscribe();
|
||||
this.watchConnection();
|
||||
this.initPromise = null;
|
||||
})();
|
||||
|
||||
return this.initPromise!;
|
||||
}
|
||||
|
||||
// Refetch every list and replace local state. Used after a realtime
|
||||
// reconnect (the SDK re-establishes SSE itself but never replays events
|
||||
// missed during the outage) and on browser online/visible signals.
|
||||
// Acting on stale state is what produced the phantom "resource not found"
|
||||
// 400s after a tab silently disconnected.
|
||||
async resync() {
|
||||
if (!this.initialized || this.destroyed || this.resyncing) return;
|
||||
this.resyncing = true;
|
||||
try {
|
||||
const [
|
||||
membersRes,
|
||||
templatesRes,
|
||||
assignedRes,
|
||||
completionsRes,
|
||||
bonusConfigsRes,
|
||||
bonusTemplatesRes,
|
||||
rewardsRes,
|
||||
seasonsRes
|
||||
] = await Promise.all([
|
||||
pb.collection('users').getFullList({
|
||||
filter: `famId = '${this.famId}' && role = 'child'`
|
||||
}) as Promise<Member[]>,
|
||||
pb.collection('chore_templates').getFullList({ filter: `(famId = '${this.famId}' || global = true)` }) as Promise<
|
||||
ChoreTemplate[]
|
||||
>,
|
||||
pb.collection('assigned_chores').getFullList({ filter: `famId = '${this.famId}'` }) as Promise<
|
||||
AssignedChore[]
|
||||
>,
|
||||
pb.collection('completions').getFullList({ filter: `famId = '${this.famId}'` }) as Promise<
|
||||
Completion[]
|
||||
>,
|
||||
pb.collection('bonus_configs').getFullList({ filter: `famId = '${this.famId}'` }) as Promise<
|
||||
BonusConfig[]
|
||||
>,
|
||||
pb.collection('bonus_templates').getFullList({ filter: `(famId = '${this.famId}' || global = true)` }) as Promise<
|
||||
BonusTemplate[]
|
||||
>,
|
||||
pb.collection('rewards').getFullList({ filter: `famId = '${this.famId}'` }) as Promise<
|
||||
Reward[]
|
||||
>,
|
||||
pb.collection('seasons').getFullList({ filter: `famId = '${this.famId}'` }) as Promise<
|
||||
Season[]
|
||||
>
|
||||
]);
|
||||
if (this.destroyed) return;
|
||||
this.members = membersRes;
|
||||
this.templates = templatesRes;
|
||||
this.assigned = assignedRes;
|
||||
this.completions = completionsRes;
|
||||
this.bonusConfigs = bonusConfigsRes;
|
||||
this.bonusTemplates = bonusTemplatesRes;
|
||||
this.rewards = rewardsRes;
|
||||
this.seasons = seasonsRes;
|
||||
} catch (e) {
|
||||
console.error('FamStore.resync failed:', e);
|
||||
} finally {
|
||||
this.resyncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private connWatchers: (() => void)[] = [];
|
||||
|
||||
private watchConnection() {
|
||||
if (typeof window === 'undefined' || this.connWatchers.length) return;
|
||||
// SDK-level: PB_CONNECT fires on every (re)connect, including the first.
|
||||
pb.realtime
|
||||
.subscribe('PB_CONNECT', () => {
|
||||
if (this.destroyed) return;
|
||||
this.connectionDown = false;
|
||||
if (this.seenConnect) {
|
||||
// Reconnect after an outage — replay what we missed.
|
||||
this.resync().catch(() => {});
|
||||
}
|
||||
this.seenConnect = true;
|
||||
})
|
||||
.then((unsub) => {
|
||||
this.connWatchers.push(unsub);
|
||||
})
|
||||
.catch(() => {});
|
||||
const onDisc = pb.realtime.onDisconnect;
|
||||
pb.realtime.onDisconnect = (active: string[]) => {
|
||||
try {
|
||||
onDisc?.(active);
|
||||
} catch {}
|
||||
// Only flag drops with live subscriptions (not our own cleanup).
|
||||
if (active.length > 0 && !this.destroyed) this.connectionDown = true;
|
||||
};
|
||||
// Browser-level: sleep/wake and offline/online can kill SSE without the
|
||||
// SDK noticing promptly — resync when the network/tab comes back.
|
||||
const onOnline = () => {
|
||||
this.connectionDown = false;
|
||||
this.resync().catch(() => {});
|
||||
};
|
||||
const onVisible = () => {
|
||||
if (document.visibilityState === 'visible') this.resync().catch(() => {});
|
||||
};
|
||||
window.addEventListener('online', onOnline);
|
||||
document.addEventListener('visibilitychange', onVisible);
|
||||
this.connWatchers.push(() => window.removeEventListener('online', onOnline));
|
||||
this.connWatchers.push(() => document.removeEventListener('visibilitychange', onVisible));
|
||||
}
|
||||
|
||||
private async subscribe() {
|
||||
const subs: { collection: CollectionName; filter: string }[] = [
|
||||
{ collection: 'users', filter: `famId = '${this.famId}' && role = 'child'` },
|
||||
@@ -233,6 +342,10 @@ class FamStore {
|
||||
this.destroyed = true;
|
||||
for (const unsub of this.unsubs) unsub();
|
||||
this.unsubs = [];
|
||||
for (const stop of this.connWatchers) stop();
|
||||
this.connWatchers = [];
|
||||
this.seenConnect = false;
|
||||
this.connectionDown = false;
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user