mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(notifications): share remote alert eligibility
This commit is contained in:
@@ -3985,12 +3985,10 @@ function loadInitialWebSessionTabs(
|
||||
}
|
||||
},
|
||||
freshSnapshots,
|
||||
notificationBaseline.allowsInitialSnapshot
|
||||
notificationBaseline.allowsSnapshot
|
||||
)
|
||||
for (const snapshot of freshSnapshots) {
|
||||
if (isWebSessionTabsWorktreeRemovalFrame(snapshot)) {
|
||||
notificationBaseline.recordSnapshot(snapshot)
|
||||
}
|
||||
notificationBaseline.recordSnapshot(snapshot)
|
||||
}
|
||||
} finally {
|
||||
for (const finishRecovery of finishRecoveries) {
|
||||
@@ -4020,31 +4018,55 @@ function loadInitialWebSessionTabs(
|
||||
}
|
||||
|
||||
type WebSessionTabsNotificationBaseline = {
|
||||
allowsInitialSnapshot: (snapshot: RuntimeMobileSessionTabsResult) => boolean
|
||||
allowsSnapshot: (snapshot: RuntimeMobileSessionTabsResult) => boolean
|
||||
recordSnapshot: (snapshot: RuntimeMobileSessionTabsResult) => void
|
||||
completeInitialSubscription: () => void
|
||||
}
|
||||
|
||||
type WebSessionTabsNotificationBaselineOwner = {
|
||||
pairingRevision: number | undefined
|
||||
baseline: WebSessionTabsNotificationBaseline
|
||||
}
|
||||
|
||||
function createWebSessionTabsNotificationBaseline(
|
||||
environmentId: string
|
||||
): WebSessionTabsNotificationBaseline {
|
||||
const knownAtEffectStart = new Set(
|
||||
const knownAtOwnerStart = new Set(
|
||||
getTrackedWebSessionTabsWorktrees(environmentId).map(({ worktree }) => worktree)
|
||||
)
|
||||
const knownWorktrees = new Set(knownAtEffectStart)
|
||||
const knownWorktrees = new Set(knownAtOwnerStart)
|
||||
let awaitingInitialSubscription = true
|
||||
return {
|
||||
allowsInitialSnapshot: (snapshot) => knownAtEffectStart.has(snapshot.worktree),
|
||||
allowsSnapshot: (snapshot) => knownWorktrees.has(snapshot.worktree),
|
||||
allowsSnapshot: (snapshot) =>
|
||||
(awaitingInitialSubscription ? knownAtOwnerStart : knownWorktrees).has(snapshot.worktree),
|
||||
recordSnapshot: (snapshot) => {
|
||||
if (isWebSessionTabsWorktreeRemovalFrame(snapshot)) {
|
||||
knownAtOwnerStart.delete(snapshot.worktree)
|
||||
knownWorktrees.delete(snapshot.worktree)
|
||||
} else {
|
||||
knownWorktrees.add(snapshot.worktree)
|
||||
}
|
||||
},
|
||||
completeInitialSubscription: () => {
|
||||
awaitingInitialSubscription = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWebSessionTabsNotificationBaseline(
|
||||
baselines: Map<string, WebSessionTabsNotificationBaselineOwner>,
|
||||
environmentId: string,
|
||||
pairingRevision: number | undefined
|
||||
): WebSessionTabsNotificationBaseline {
|
||||
const existing = baselines.get(environmentId)
|
||||
if (existing && existing.pairingRevision === pairingRevision) {
|
||||
return existing.baseline
|
||||
}
|
||||
const baseline = createWebSessionTabsNotificationBaseline(environmentId)
|
||||
baselines.set(environmentId, { pairingRevision, baseline })
|
||||
return baseline
|
||||
}
|
||||
|
||||
export function useWebSessionTabsSync(): void {
|
||||
const recordVisibilityResumeSnapshotRef = useRef<
|
||||
(environmentId: string, snapshot: RuntimeMobileSessionTabsResult, receivedFrame: number) => void
|
||||
@@ -4060,7 +4082,9 @@ export function useWebSessionTabsSync(): void {
|
||||
) => boolean
|
||||
>(() => true)
|
||||
const visibilityResumeOmissionsByKeyRef = useRef(new Map<string, VisibilityResumeOmission>())
|
||||
const visibilityResumeNotificationBaselinesRef = useRef(new Map<string, ReadonlySet<string>>())
|
||||
const notificationBaselinesByEnvironmentRef = useRef(
|
||||
new Map<string, WebSessionTabsNotificationBaselineOwner>()
|
||||
)
|
||||
const mirroredSessionTabsOwnerRevisionByEnvironmentRef = useRef(
|
||||
new Map<string, number | undefined>()
|
||||
)
|
||||
@@ -4109,7 +4133,7 @@ export function useWebSessionTabsSync(): void {
|
||||
}
|
||||
mirroredSessionTabsOwnerRevisionByEnvironmentRef.current.clear()
|
||||
visibilityResumeOmissionsByKeyRef.current.clear()
|
||||
visibilityResumeNotificationBaselinesRef.current.clear()
|
||||
notificationBaselinesByEnvironmentRef.current.clear()
|
||||
},
|
||||
[]
|
||||
)
|
||||
@@ -4142,7 +4166,7 @@ export function useWebSessionTabsSync(): void {
|
||||
mirroredEnvironmentOwnerRevisions.get(environmentId) !== previousRevision
|
||||
) {
|
||||
clearWebSessionTabsTrackingForEnvironment(environmentId)
|
||||
visibilityResumeNotificationBaselinesRef.current.delete(environmentId)
|
||||
notificationBaselinesByEnvironmentRef.current.delete(environmentId)
|
||||
}
|
||||
}
|
||||
mirroredSessionTabsOwnerRevisionByEnvironmentRef.current = mirroredEnvironmentOwnerRevisions
|
||||
@@ -4162,10 +4186,17 @@ export function useWebSessionTabsSync(): void {
|
||||
return
|
||||
}
|
||||
const notificationBaselinesByEnvironment = new Map(
|
||||
environments.map(({ environmentId }) => [
|
||||
environmentId,
|
||||
createWebSessionTabsNotificationBaseline(environmentId)
|
||||
])
|
||||
environments.map(
|
||||
({ environmentId, expectedEnvironmentPairingRevision }) =>
|
||||
[
|
||||
environmentId,
|
||||
getWebSessionTabsNotificationBaseline(
|
||||
notificationBaselinesByEnvironmentRef.current,
|
||||
environmentId,
|
||||
expectedEnvironmentPairingRevision
|
||||
)
|
||||
] as const
|
||||
)
|
||||
)
|
||||
|
||||
type VisibilityResumeEnvironment = {
|
||||
@@ -4508,11 +4539,11 @@ export function useWebSessionTabsSync(): void {
|
||||
}
|
||||
}
|
||||
const resumedEnvironments = new Map<string, VisibilityResumeEnvironment>()
|
||||
const notificationBaselines = new Map<string, ReadonlySet<string>>()
|
||||
const trackedWorktreeIds = new Set<string>()
|
||||
for (const index of restartingSpecIndexes) {
|
||||
const environmentId = environmentIdBySubscriptionSpec[index]
|
||||
if (environmentId) {
|
||||
notificationBaselinesByEnvironment.get(environmentId)?.completeInitialSubscription()
|
||||
const trackedWorktrees = getTrackedWebSessionTabsWorktrees(environmentId)
|
||||
if (trackedWorktrees.length === 0) {
|
||||
continue
|
||||
@@ -4520,10 +4551,6 @@ export function useWebSessionTabsSync(): void {
|
||||
for (const { worktree } of trackedWorktrees) {
|
||||
trackedWorktreeIds.add(worktree)
|
||||
}
|
||||
notificationBaselines.set(
|
||||
environmentId,
|
||||
new Set(trackedWorktrees.map(({ worktree }) => worktree))
|
||||
)
|
||||
resumedEnvironments.set(environmentId, {
|
||||
trackedWorktrees,
|
||||
inventoryReceived: false,
|
||||
@@ -4532,7 +4559,6 @@ export function useWebSessionTabsSync(): void {
|
||||
})
|
||||
}
|
||||
}
|
||||
visibilityResumeNotificationBaselinesRef.current = notificationBaselines
|
||||
visibilityResumeBatch =
|
||||
resumedEnvironments.size > 0
|
||||
? {
|
||||
@@ -4594,6 +4620,7 @@ export function useWebSessionTabsSync(): void {
|
||||
return
|
||||
}
|
||||
if (response.ok === false) {
|
||||
notificationBaseline.completeInitialSubscription()
|
||||
console.warn(
|
||||
'[web-session-tabs-sync] global subscription failed:',
|
||||
response.error.message
|
||||
@@ -4712,6 +4739,11 @@ export function useWebSessionTabsSync(): void {
|
||||
for (const snapshot of freshSnapshots) {
|
||||
notificationBaseline.recordSnapshot(snapshot)
|
||||
}
|
||||
for (const { snapshot } of applicable) {
|
||||
if (!isWebSessionTabsWorktreeRemovalFrame(snapshot)) {
|
||||
notificationBaseline.recordSnapshot(snapshot)
|
||||
}
|
||||
}
|
||||
const freshSnapshotSet = new Set(freshSnapshots)
|
||||
for (const { index, snapshot } of applicable) {
|
||||
if (unchangedVisibilityResumeSnapshots[index]) {
|
||||
@@ -4742,6 +4774,9 @@ export function useWebSessionTabsSync(): void {
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (isCurrent()) {
|
||||
notificationBaseline.completeInitialSubscription()
|
||||
}
|
||||
for (const finishRecovery of finishRecoveries) {
|
||||
finishRecovery?.()
|
||||
}
|
||||
@@ -4759,6 +4794,7 @@ export function useWebSessionTabsSync(): void {
|
||||
// talking has not reported a single PTY dead.
|
||||
return
|
||||
}
|
||||
notificationBaseline.completeInitialSubscription()
|
||||
const receivedFrame = recordReceivedWebSessionTabsSnapshot(environmentId, event)
|
||||
recordVisibilityResumeSnapshotReceipt(environmentId, event, receivedFrame)
|
||||
const finishRecovery = beginWebSessionTabsSnapshotRecovery(
|
||||
@@ -4824,6 +4860,7 @@ export function useWebSessionTabsSync(): void {
|
||||
},
|
||||
onError: (error) => {
|
||||
if (isCurrent()) {
|
||||
notificationBaseline.completeInitialSubscription()
|
||||
console.warn('[web-session-tabs-sync] global subscription error:', error.message)
|
||||
}
|
||||
}
|
||||
@@ -4831,6 +4868,7 @@ export function useWebSessionTabsSync(): void {
|
||||
)
|
||||
},
|
||||
onSubscribeError: (error) => {
|
||||
notificationBaseline.completeInitialSubscription()
|
||||
console.warn(
|
||||
'[web-session-tabs-sync] failed to subscribe globally:',
|
||||
error instanceof Error ? error.message : String(error)
|
||||
@@ -4883,7 +4921,11 @@ export function useWebSessionTabsSync(): void {
|
||||
|
||||
let requestedInitialTerminal = false
|
||||
let requestedRespawnAfterWake = false
|
||||
const notificationBaseline = createWebSessionTabsNotificationBaseline(environmentId)
|
||||
const notificationBaseline = getWebSessionTabsNotificationBaseline(
|
||||
notificationBaselinesByEnvironmentRef.current,
|
||||
environmentId,
|
||||
expectedEnvironmentPairingRevision
|
||||
)
|
||||
/** Resolves the settle receipt of the evidence this frame put (or already
|
||||
* had) in the store, or null when the frame was discarded undecided. */
|
||||
const applyActiveSnapshot = async (
|
||||
@@ -5029,10 +5071,7 @@ export function useWebSessionTabsSync(): void {
|
||||
response,
|
||||
isCurrent,
|
||||
receivedFrame,
|
||||
notificationBaseline.allowsSnapshot(event) ||
|
||||
visibilityResumeNotificationBaselinesRef.current
|
||||
.get(environmentId)
|
||||
?.has(activeWorktreeId) === true
|
||||
notificationBaseline.allowsSnapshot(event)
|
||||
)
|
||||
.catch((error) => {
|
||||
if (isCurrent()) {
|
||||
|
||||
@@ -370,6 +370,71 @@ describe('paired session-tab visibility-resume notifications', () => {
|
||||
hook.unmount()
|
||||
})
|
||||
|
||||
it.each(['done', 'blocked', 'waiting'] as const)(
|
||||
'arms later %s resumes after duplicate cold listAll and subscription inventories',
|
||||
async (state) => {
|
||||
runtimeCall.mockResolvedValueOnce({
|
||||
id: 'list-all',
|
||||
ok: true,
|
||||
result: { snapshots: [snapshot(1, 'working', NOW)] },
|
||||
_meta: { runtimeId: RUNTIME_ID }
|
||||
})
|
||||
const hook = renderHook(() => useWebSessionTabsSync())
|
||||
await act(settle)
|
||||
await publish(findSubscription('session.tabs.subscribeAll'), {
|
||||
type: 'snapshots',
|
||||
snapshots: [snapshot(1, 'working', NOW)]
|
||||
})
|
||||
notificationDispatch.mockClear()
|
||||
|
||||
await parkAndReveal()
|
||||
await publish(findSubscription('session.tabs.subscribeAll', 1), {
|
||||
type: 'snapshots',
|
||||
snapshots: [snapshot(2, state, NOW + 1_000)]
|
||||
})
|
||||
vi.advanceTimersByTime(1_500)
|
||||
|
||||
expect(notificationDispatch).toHaveBeenCalledTimes(1)
|
||||
expect(badgeCount()).toBe(1)
|
||||
hook.unmount()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['done', 'blocked', 'waiting'] as const)(
|
||||
'recovers %s after listAll succeeds and the initial subscription fails',
|
||||
async (state) => {
|
||||
runtimeCall.mockResolvedValueOnce({
|
||||
id: 'list-all',
|
||||
ok: true,
|
||||
result: { snapshots: [snapshot(1, 'working', NOW)] },
|
||||
_meta: { runtimeId: RUNTIME_ID }
|
||||
})
|
||||
const hook = renderHook(() => useWebSessionTabsSync())
|
||||
await act(settle)
|
||||
await act(async () => {
|
||||
findSubscription('session.tabs.subscribeAll').callbacks.onResponse({
|
||||
id: 'subscription-error',
|
||||
ok: false,
|
||||
error: { code: 'disconnected', message: 'subscription unavailable' },
|
||||
_meta: { runtimeId: RUNTIME_ID }
|
||||
})
|
||||
await settle()
|
||||
})
|
||||
notificationDispatch.mockClear()
|
||||
|
||||
await parkAndReveal()
|
||||
await publish(findSubscription('session.tabs.subscribeAll', 1), {
|
||||
type: 'snapshots',
|
||||
snapshots: [snapshot(2, state, NOW + 1_000)]
|
||||
})
|
||||
vi.advanceTimersByTime(1_500)
|
||||
|
||||
expect(notificationDispatch).toHaveBeenCalledTimes(1)
|
||||
expect(badgeCount()).toBe(1)
|
||||
hook.unmount()
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['done', 'blocked'] as const)(
|
||||
'keeps cold %s inventory silent when first mounted hidden',
|
||||
async (state) => {
|
||||
@@ -510,6 +575,45 @@ describe('paired session-tab visibility-resume notifications', () => {
|
||||
hook.unmount()
|
||||
})
|
||||
|
||||
it('forgets active-stream eligibility after a visibility resume and removal', async () => {
|
||||
seedRemoteMirrorState(true)
|
||||
mocks.getExplicitRuntimeEnvironmentIdForWorktree.mockReturnValue(ENVIRONMENT_ID)
|
||||
const hook = renderHook(() => useWebSessionTabsSync())
|
||||
await act(settle)
|
||||
await publish(findSubscription('session.tabs.subscribeAll'), {
|
||||
type: 'updated',
|
||||
...snapshot(1, 'working', NOW)
|
||||
})
|
||||
await publish(findSubscription('session.tabs.subscribe'), {
|
||||
type: 'updated',
|
||||
...snapshot(1, 'working', NOW)
|
||||
})
|
||||
notificationDispatch.mockClear()
|
||||
|
||||
await parkAndReveal()
|
||||
const active = findSubscription('session.tabs.subscribe', 1)
|
||||
await publish(active, {
|
||||
type: 'updated',
|
||||
worktree: WORKTREE_ID,
|
||||
publicationEpoch: 'epoch-1',
|
||||
snapshotVersion: 2,
|
||||
removed: true,
|
||||
activeGroupId: null,
|
||||
activeTabId: null,
|
||||
activeTabType: null,
|
||||
tabs: []
|
||||
})
|
||||
await publish(active, {
|
||||
type: 'snapshot',
|
||||
...snapshot(1, 'done', NOW + 1_000, NOW + 1_000, undefined, 'epoch-2')
|
||||
})
|
||||
vi.advanceTimersByTime(1_500)
|
||||
|
||||
expect(notificationDispatch).not.toHaveBeenCalled()
|
||||
expect(badgeCount()).toBe(0)
|
||||
hook.unmount()
|
||||
})
|
||||
|
||||
it.each(['done', 'blocked'] as const)(
|
||||
'keeps a metadata-only %s inventory refresh silent',
|
||||
async (state) => {
|
||||
|
||||
Reference in New Issue
Block a user