From 63a431f533d970752dc4099d32106e4c809a3996 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sat, 3 Oct 2026 13:29:38 -0400 Subject: [PATCH] Renew presence from the socket's current state; scope open dialogs to their page From Greptile on #243: - A renewal awaits its pass check, then saved the socket's state from before the wait: a clear (or cursor move) that arrived meanwhile was undone, and newcomers saw a stale "editing". It now renews what the socket holds after the wait, and nothing for a socket that closed. - A lineup dialog edits the groups of the page it opened on. Left open across a page change, it no longer counts as editing whatever shares its item ids on the new page. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../collab/lineup_editing_presence_provider.dart | 13 ++++++++++++- presence/src/room.ts | 13 +++++++++---- .../lineup_editing_presence_provider_test.dart | 9 ++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/providers/collab/lineup_editing_presence_provider.dart b/lib/providers/collab/lineup_editing_presence_provider.dart index 7c268b143..2bb34d075 100644 --- a/lib/providers/collab/lineup_editing_presence_provider.dart +++ b/lib/providers/collab/lineup_editing_presence_provider.dart @@ -16,17 +16,26 @@ final openLineUpItemsProvider = ); class OpenLineUpItemsNotifier extends Notifier>> { + /// The page each dialog opened on: its items are that page's, and stay + /// so if the page on screen changes while it is open. + final Map _pageOf = {}; + @override Map> build() => const {}; void open(Object owner, Set itemIds) { + _pageOf[owner] = ref.read(strategyPageSessionProvider).activePageId; state = {...state, owner: itemIds}; } void close(Object owner) { + _pageOf.remove(owner); if (!state.containsKey(owner)) return; state = {...state}..remove(owner); } + + /// The page [owner] opened on. + String? pageOf(Object owner) => _pageOf[owner]; } /// The lineup groups this user is editing on the page on screen: those of @@ -39,6 +48,7 @@ final myLineupEditingProvider = Provider((ref) { final held = ref.watch(editorHeldEntitiesProvider) ?? const {}; final placement = ref.watch(lineUpProvider.select((s) => s.placement)); final open = ref.watch(openLineUpItemsProvider); + final openNotifier = ref.read(openLineUpItemsProvider.notifier); ref.watch(lineupGroupMemoryRevisionProvider); final liveSync = ref.read(activePageLiveSyncProvider.notifier); final groupIds = { @@ -46,7 +56,8 @@ final myLineupEditingProvider = Provider((ref) { ...held, if (placement?.pinnedOriginId case final id?) id, if (placement?.pinnedLandingId case final id?) id, - for (final ids in open.values) ...ids, + for (final MapEntry(key: owner, value: ids) in open.entries) + if (openNotifier.pageOf(owner) == pageId) ...ids, }) if (liveSync.lineupGroupOf(pageId, id) case final group?) group, }; diff --git a/presence/src/room.ts b/presence/src/room.ts index 717a193a2..39b1a67ba 100644 --- a/presence/src/room.ts +++ b/presence/src/room.ts @@ -192,8 +192,13 @@ export class PresenceRoom extends DurableObject { this.drop(ws, self, CLOSE_PASS_EXPIRED, "Invalid renewal"); return; } + // Verifying awaited: the cursor or what is being edited may have + // changed meanwhile, or the socket closed. Renew what it holds now, + // never the state from before the wait. + const current = attachmentOf(ws); + if (current === null) return; const renewed: Attachment = { - ...self, + ...current, lastRenewAt: now, exp: claims.exp, name: claims.name, @@ -205,9 +210,9 @@ export class PresenceRoom extends DurableObject { // Name, avatar, or role may have changed since the last pass; a join // for a known sid replaces that peer. if ( - renewed.name !== self.name || - renewed.avatar !== self.avatar || - renewed.role !== self.role + renewed.name !== current.name || + renewed.avatar !== current.avatar || + renewed.role !== current.role ) { this.broadcast({ t: "join", peer: toPeer(renewed) }, ws); } diff --git a/test/providers/lineup_editing_presence_provider_test.dart b/test/providers/lineup_editing_presence_provider_test.dart index b6436dcdf..9d59d826b 100644 --- a/test/providers/lineup_editing_presence_provider_test.dart +++ b/test/providers/lineup_editing_presence_provider_test.dart @@ -164,7 +164,7 @@ void main() { ); }); - test('groups are those of the page on screen', () { + test('a dialog edits the groups of the page it opened on, only there', () { container .read(openLineUpItemsProvider.notifier) .open(Object(), {'landing-a', 'landing-b'}); @@ -172,6 +172,13 @@ void main() { session.setStateForTest( container.read(strategyPageSessionProvider).copyWith(activePageId: 'p2'), ); + // Page 2 has lineups under the same ids, in other groups; the dialog + // still shows page 1's, so it edits nothing here. + expect(editing(), isNull); + + container + .read(openLineUpItemsProvider.notifier) + .open(Object(), {'landing-a'}); expect( editing(), const PresenceEditing(pageId: 'p2', groupIds: {'group-a2'}),