@@ -192,25 +192,25 @@ export async function persistTabChatToSession(storageArea, key, html, warn = con
192192 }
193193}
194194
195- export const TAB_CHAT_HANDOFF_SETTLE_MS = 25 ;
195+ export const TAB_CHAT_HANDOFF_PREFIX = 'tabChatHandoff:' ;
196196
197197/**
198198 * Serialize tab-chat reads and writes in the background's shared JavaScript
199- * realm. The short settle window lets the outgoing panel enqueue its final
200- * visibility snapshot even if the newly visible panel's message is delivered
201- * first.
199+ * realm. Coordinated loads explicitly request and acknowledge the outgoing
200+ * document's final snapshot before assigning a new handoff generation.
202201 *
203202 * @param {chrome.storage.StorageArea | browser.storage.StorageArea } storageArea
204203 * @param {{
205204 * persist?: typeof persistTabChatToSession,
206- * settleHandoff ?: () => Promise<void >,
205+ * requestHandoff ?: (tabId: number, handoff: { ownerId: string, generation: number } ) => Promise<object | null >,
207206 * }} options
208207 */
209208export function createTabChatHandoffCoordinator ( storageArea , {
210209 persist = persistTabChatToSession ,
211- settleHandoff = ( ) => new Promise ( resolve => setTimeout ( resolve , TAB_CHAT_HANDOFF_SETTLE_MS ) ) ,
210+ requestHandoff = async ( ) => null ,
212211} = { } ) {
213212 const operations = new Map ( ) ;
213+ const handoffOperations = new Map ( ) ;
214214 const latestHtml = new Map ( ) ;
215215
216216 function normalizeTabId ( tabId ) {
@@ -232,34 +232,110 @@ export function createTabChatHandoffCoordinator(storageArea, {
232232 return operation ;
233233 }
234234
235- function save ( tabId , html ) {
235+ function enqueueHandoff ( tabId , fn ) {
236+ const previous = handoffOperations . get ( tabId ) || Promise . resolve ( ) ;
237+ const operation = previous . catch ( ( ) => { } ) . then ( fn ) ;
238+ handoffOperations . set ( tabId , operation ) ;
239+ operation . finally ( ( ) => {
240+ if ( handoffOperations . get ( tabId ) === operation ) handoffOperations . delete ( tabId ) ;
241+ } ) . catch ( ( ) => { } ) ;
242+ return operation ;
243+ }
244+
245+ async function readHandoffState ( tabId ) {
246+ const key = TAB_CHAT_HANDOFF_PREFIX + tabId ;
247+ const stored = await storageArea . get ( key ) ;
248+ const state = stored ?. [ key ] ;
249+ const ownerId = String ( state ?. ownerId || '' ) ;
250+ const generation = Number ( state ?. generation ) ;
251+ return ownerId && Number . isFinite ( generation ) && generation > 0
252+ ? { ownerId, generation }
253+ : null ;
254+ }
255+
256+ async function readLatest ( tabId ) {
257+ if ( latestHtml . has ( tabId ) ) {
258+ return { ok : true , found : true , html : latestHtml . get ( tabId ) } ;
259+ }
260+ const key = TAB_CHAT_PREFIX + tabId ;
261+ const stored = await storageArea . get ( key ) ;
262+ const html = stored ?. [ key ] ;
263+ if ( typeof html === 'string' ) {
264+ latestHtml . set ( tabId , html ) ;
265+ return { ok : true , found : true , html } ;
266+ }
267+ return { ok : true , found : false , html : null } ;
268+ }
269+
270+ function save ( tabId , html , { ownerId = '' , handoffGeneration = null } = { } ) {
236271 const numericTabId = normalizeTabId ( tabId ) ;
237272 if ( numericTabId == null ) return Promise . resolve ( { ok : false , error : 'No tab ID' } ) ;
238273 const source = String ( html || '' ) ;
239274 return enqueue ( numericTabId , async ( queuedTabId ) => {
275+ const normalizedOwnerId = String ( ownerId || '' ) ;
276+ const normalizedGeneration = Number ( handoffGeneration ) ;
277+ if ( normalizedOwnerId && Number . isFinite ( normalizedGeneration ) && normalizedGeneration > 0 ) {
278+ const state = await readHandoffState ( queuedTabId ) ;
279+ if ( ! state
280+ || state . ownerId !== normalizedOwnerId
281+ || state . generation !== normalizedGeneration ) {
282+ return { ok : true , skipped : true , reason : 'stale-handoff' } ;
283+ }
284+ }
240285 // Retain the lossless copy even if persistence has to compact the
241286 // storage.session value for quota recovery.
242287 latestHtml . set ( queuedTabId , source ) ;
243288 return persist ( storageArea , TAB_CHAT_PREFIX + queuedTabId , source ) ;
244289 } ) ;
245290 }
246291
247- async function load ( tabId , { waitForHandoff = false } = { } ) {
292+ async function load ( tabId , { waitForHandoff = false , claimantId = '' } = { } ) {
248293 const numericTabId = normalizeTabId ( tabId ) ;
249294 if ( numericTabId == null ) return { ok : false , found : false , error : 'No tab ID' } ;
250- if ( waitForHandoff ) await settleHandoff ( ) ;
251- return enqueue ( numericTabId , async ( queuedTabId ) => {
252- if ( latestHtml . has ( queuedTabId ) ) {
253- return { ok : true , found : true , html : latestHtml . get ( queuedTabId ) } ;
254- }
255- const key = TAB_CHAT_PREFIX + queuedTabId ;
256- const stored = await storageArea . get ( key ) ;
257- const html = stored ?. [ key ] ;
258- if ( typeof html === 'string' ) {
259- latestHtml . set ( queuedTabId , html ) ;
260- return { ok : true , found : true , html } ;
295+ if ( ! waitForHandoff ) {
296+ return enqueue ( numericTabId , queuedTabId => readLatest ( queuedTabId ) ) ;
297+ }
298+
299+ return enqueueHandoff ( numericTabId , async ( ) => {
300+ const outgoing = await enqueue ( numericTabId , queuedTabId => readHandoffState ( queuedTabId ) ) ;
301+ if ( outgoing ) {
302+ let acknowledgement = null ;
303+ try {
304+ acknowledgement = await requestHandoff ( numericTabId , outgoing ) ;
305+ } catch { /* an unavailable outgoing document has no snapshot to acknowledge */ }
306+ if ( acknowledgement ?. ok
307+ && String ( acknowledgement . ownerId || '' ) === outgoing . ownerId
308+ && Number ( acknowledgement . generation ) === outgoing . generation
309+ && typeof acknowledgement . html === 'string' ) {
310+ await save ( numericTabId , acknowledgement . html , {
311+ ownerId : outgoing . ownerId ,
312+ handoffGeneration : outgoing . generation ,
313+ } ) ;
314+ }
261315 }
262- return { ok : true , found : false , html : null } ;
316+
317+ return enqueue ( numericTabId , async ( queuedTabId ) => {
318+ const current = await readHandoffState ( queuedTabId ) ;
319+ const generation = Math . max (
320+ Number ( outgoing ?. generation || 0 ) ,
321+ Number ( current ?. generation || 0 ) ,
322+ ) + 1 ;
323+ const normalizedClaimantId = String ( claimantId || '' ) ;
324+ if ( normalizedClaimantId ) {
325+ await storageArea . set ( {
326+ [ TAB_CHAT_HANDOFF_PREFIX + queuedTabId ] : {
327+ ownerId : normalizedClaimantId ,
328+ generation,
329+ } ,
330+ } ) ;
331+ }
332+ const result = await readLatest ( queuedTabId ) ;
333+ return {
334+ ...result ,
335+ handoffOwnerId : normalizedClaimantId || null ,
336+ handoffGeneration : normalizedClaimantId ? generation : null ,
337+ } ;
338+ } ) ;
263339 } ) ;
264340 }
265341
@@ -268,7 +344,10 @@ export function createTabChatHandoffCoordinator(storageArea, {
268344 if ( numericTabId == null ) return Promise . resolve ( { ok : false , error : 'No tab ID' } ) ;
269345 return enqueue ( numericTabId , async ( queuedTabId ) => {
270346 latestHtml . delete ( queuedTabId ) ;
271- await storageArea . remove ( TAB_CHAT_PREFIX + queuedTabId ) ;
347+ await storageArea . remove ( [
348+ TAB_CHAT_PREFIX + queuedTabId ,
349+ TAB_CHAT_HANDOFF_PREFIX + queuedTabId ,
350+ ] ) ;
272351 return { ok : true } ;
273352 } ) ;
274353 }
0 commit comments