Repository navigation
fix(wework): preserve unread state across restarts #2743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,15 +39,22 @@ const RUNTIME_TASK_LIFECYCLE_READ_METHODS = new Set<PropertyKey>([ | |||||||||||||||||||||||||||||||||||||||||||||
| export class RuntimeTaskLifecycleStore { | ||||||||||||||||||||||||||||||||||||||||||||||
| private readonly machines = new Map<string, RuntimeTaskMachine>() | ||||||||||||||||||||||||||||||||||||||||||||||
| private readonly deviceAliases = new Map<string, string>() | ||||||||||||||||||||||||||||||||||||||||||||||
| private readonly previousRunningTaskKeys: Set<string> | ||||||||||||||||||||||||||||||||||||||||||||||
| private readonly listeners = new Set<Listener>() | ||||||||||||||||||||||||||||||||||||||||||||||
| private readonly unreadStorageKey: string | ||||||||||||||||||||||||||||||||||||||||||||||
| private readonly runningStorageKey: string | ||||||||||||||||||||||||||||||||||||||||||||||
| private currentTaskKey: string | null = null | ||||||||||||||||||||||||||||||||||||||||||||||
| private persistedUnreadSerialized: string | null = null | ||||||||||||||||||||||||||||||||||||||||||||||
| private persistedRunningSerialized: string | null = null | ||||||||||||||||||||||||||||||||||||||||||||||
| private version = 0 | ||||||||||||||||||||||||||||||||||||||||||||||
| private snapshot = EMPTY_STORE_SNAPSHOT | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| constructor(userId: number | string | null | undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||
| this.unreadStorageKey = `wework.runtimeTaskLifecycle.${userId ?? 'anonymous'}.unread.v1` | ||||||||||||||||||||||||||||||||||||||||||||||
| const storageScope = `wework.runtimeTaskLifecycle.${userId ?? 'anonymous'}` | ||||||||||||||||||||||||||||||||||||||||||||||
| this.unreadStorageKey = `${storageScope}.unread` | ||||||||||||||||||||||||||||||||||||||||||||||
| this.runningStorageKey = `${storageScope}.running` | ||||||||||||||||||||||||||||||||||||||||||||||
| this.previousRunningTaskKeys = readStoredTaskKeys(this.runningStorageKey) | ||||||||||||||||||||||||||||||||||||||||||||||
| this.persistedRunningSerialized = serializeTaskKeys(this.previousRunningTaskKeys) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| subscribe = (listener: Listener): (() => void) => { | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -259,7 +266,9 @@ export class RuntimeTaskLifecycleStore { | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| remove(address: RuntimeTaskAddress): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| const canonicalAddress = this.canonicalizeAddress(address) | ||||||||||||||||||||||||||||||||||||||||||||||
| const deleted = this.machines.delete(getRuntimeTaskLifecycleKey(canonicalAddress)) | ||||||||||||||||||||||||||||||||||||||||||||||
| const key = getRuntimeTaskLifecycleKey(canonicalAddress) | ||||||||||||||||||||||||||||||||||||||||||||||
| const deleted = this.machines.delete(key) | ||||||||||||||||||||||||||||||||||||||||||||||
| this.previousRunningTaskKeys.delete(key) | ||||||||||||||||||||||||||||||||||||||||||||||
| if (deleted) this.publish() | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+269
to
272
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Persist a removed restored running key. If no machine exists after restart, Persist the changed running-key set when Proposed fix const key = getRuntimeTaskLifecycleKey(canonicalAddress)
const deleted = this.machines.delete(key)
- this.previousRunningTaskKeys.delete(key)
+ const removedRunningKey = this.previousRunningTaskKeys.delete(key)
+ if (!deleted && removedRunningKey) this.persistRunningKeys()
if (deleted) this.publish()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -286,6 +295,8 @@ export class RuntimeTaskLifecycleStore { | |||||||||||||||||||||||||||||||||||||||||||||
| event.type === 'executor_snapshot_received' ? { ...event, address: canonicalAddress } : event | ||||||||||||||||||||||||||||||||||||||||||||||
| const machine = this.ensureMachine(canonicalAddress) | ||||||||||||||||||||||||||||||||||||||||||||||
| const previous = machine.getSnapshot() | ||||||||||||||||||||||||||||||||||||||||||||||
| const key = previous.key | ||||||||||||||||||||||||||||||||||||||||||||||
| const wasRunning = previous.derived.isRunning || this.previousRunningTaskKeys.has(key) | ||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||
| canonicalEvent.type === 'executor_snapshot_received' && | ||||||||||||||||||||||||||||||||||||||||||||||
| previous.task && | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -297,14 +308,16 @@ export class RuntimeTaskLifecycleStore { | |||||||||||||||||||||||||||||||||||||||||||||
| let changed = eventChanged | ||||||||||||||||||||||||||||||||||||||||||||||
| const next = machine.getSnapshot() | ||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||
| previous.derived.isRunning && | ||||||||||||||||||||||||||||||||||||||||||||||
| wasRunning && | ||||||||||||||||||||||||||||||||||||||||||||||
| !next.derived.isRunning && | ||||||||||||||||||||||||||||||||||||||||||||||
| !next.derived.isQueued && | ||||||||||||||||||||||||||||||||||||||||||||||
| next.goalStatus !== 'active' && | ||||||||||||||||||||||||||||||||||||||||||||||
| next.key !== this.currentTaskKey | ||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||
| changed = machine.dispatch({ type: 'marked_unread' }) || changed | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if (next.derived.isRunning) this.previousRunningTaskKeys.add(key) | ||||||||||||||||||||||||||||||||||||||||||||||
| else this.previousRunningTaskKeys.delete(key) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
310
to
+320
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Do not suppress unread state for a settled active Goal. Lines 311-317 skip Remove the Proposed fix wasRunning &&
!next.derived.isRunning &&
!next.derived.isQueued &&
- next.goalStatus !== 'active' &&
next.key !== this.currentTaskKey📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| if (next.derived.isRunning || next.key === this.currentTaskKey) { | ||||||||||||||||||||||||||||||||||||||||||||||
| changed = machine.dispatch({ type: 'marked_read' }) || changed | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -408,6 +421,9 @@ export class RuntimeTaskLifecycleStore { | |||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if (previousState.unread) nextMachine.dispatch({ type: 'marked_unread' }) | ||||||||||||||||||||||||||||||||||||||||||||||
| if (this.previousRunningTaskKeys.delete(previousKey)) { | ||||||||||||||||||||||||||||||||||||||||||||||
| this.previousRunningTaskKeys.add(nextKey) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| this.machines.delete(previousKey) | ||||||||||||||||||||||||||||||||||||||||||||||
| if (this.currentTaskKey === previousKey) this.currentTaskKey = nextKey | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -433,26 +449,19 @@ export class RuntimeTaskLifecycleStore { | |||||||||||||||||||||||||||||||||||||||||||||
| unreadTaskKeys, | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| this.persistUnreadKeys(unreadTaskKeys) | ||||||||||||||||||||||||||||||||||||||||||||||
| this.persistRunningKeys() | ||||||||||||||||||||||||||||||||||||||||||||||
| this.listeners.forEach(listener => listener()) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private readUnreadKeys(): Set<string> { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined') return new Set() | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const value = JSON.parse(window.localStorage.getItem(this.unreadStorageKey) ?? '[]') | ||||||||||||||||||||||||||||||||||||||||||||||
| const keys = new Set<string>( | ||||||||||||||||||||||||||||||||||||||||||||||
| Array.isArray(value) ? value.filter(item => typeof item === 'string') : [] | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| this.persistedUnreadSerialized = serializeUnreadKeys(keys) | ||||||||||||||||||||||||||||||||||||||||||||||
| return keys | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| return new Set() | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| const keys = readStoredTaskKeys(this.unreadStorageKey) | ||||||||||||||||||||||||||||||||||||||||||||||
| this.persistedUnreadSerialized = serializeTaskKeys(keys) | ||||||||||||||||||||||||||||||||||||||||||||||
| return keys | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private persistUnreadKeys(keys: ReadonlySet<string>): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined') return | ||||||||||||||||||||||||||||||||||||||||||||||
| const serialized = serializeUnreadKeys(keys) | ||||||||||||||||||||||||||||||||||||||||||||||
| const serialized = serializeTaskKeys(keys) | ||||||||||||||||||||||||||||||||||||||||||||||
| if (serialized === this.persistedUnreadSerialized) return | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| window.localStorage.setItem(this.unreadStorageKey, serialized) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -461,6 +470,18 @@ export class RuntimeTaskLifecycleStore { | |||||||||||||||||||||||||||||||||||||||||||||
| console.warn('Failed to persist runtime task unread state', error) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| private persistRunningKeys(): void { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined') return | ||||||||||||||||||||||||||||||||||||||||||||||
| const serialized = serializeTaskKeys(this.previousRunningTaskKeys) | ||||||||||||||||||||||||||||||||||||||||||||||
| if (serialized === this.persistedRunningSerialized) return | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| window.localStorage.setItem(this.runningStorageKey, serialized) | ||||||||||||||||||||||||||||||||||||||||||||||
| this.persistedRunningSerialized = serialized | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||
| console.warn('Failed to persist runtime task running state', error) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export function createRuntimeTaskLifecycleOwnershipView( | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -570,6 +591,16 @@ function emptyRuntimeTaskSummary(address: RuntimeTaskAddress): RuntimeTaskSummar | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function serializeUnreadKeys(keys: ReadonlySet<string>): string { | ||||||||||||||||||||||||||||||||||||||||||||||
| function serializeTaskKeys(keys: ReadonlySet<string>): string { | ||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.stringify([...keys].slice(-200)) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function readStoredTaskKeys(storageKey: string): Set<string> { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined') return new Set() | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const value = JSON.parse(window.localStorage.getItem(storageKey) ?? '[]') | ||||||||||||||||||||||||||||||||||||||||||||||
| return new Set(Array.isArray(value) ? value.filter(item => typeof item === 'string') : []) | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| return new Set() | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Add real-Tauri regression coverage for restart unread state.
These tests verify store state in Vitest only. This change alters desktop unread behavior across restart. Add a CI-invoked E2E scenario that uses real backend requests. Run isolated real-Tauri verification with
scripts/ai-verify.mjs.As per coding guidelines: “Any Wework UI, Tauri command, local-runtime, IPC, or desktop integration behavior change requires isolated real-Tauri verification in addition to unit and E2E tests.”
🤖 Prompt for AI Agents
Source: Coding guidelines