@linxin666/dsh-client-ui-task-board 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core/store.ts CHANGED
@@ -25,11 +25,30 @@ export interface TaskStore {
25
25
  save(tasks: readonly TaskRecord[]): void
26
26
  /** Drop the persisted ledger (leaves the in-memory state alone). */
27
27
  clear(): void
28
+ /**
29
+ * Subscribe to ledger changes written by ANOTHER tab of the same origin
30
+ * (browser storage events). The board controller reloads the ledger on
31
+ * such a change, so a task deleted in one tab cannot keep firing (or be
32
+ * written back) from the stale in-memory copy of another tab. No-op when
33
+ * the backend has no cross-instance channel (in-memory store).
34
+ */
35
+ subscribeExternal?(listener: () => void): () => void
28
36
  }
29
37
 
30
38
  /** Storage key for the task ledger document. */
31
39
  export const DEFAULT_STORAGE_KEY = 'dsh.taskBoard.v1'
32
40
 
41
+ /** Structural shape of the storage event fired in sibling tabs (DOM-free). */
42
+ export interface StorageChangeEvent {
43
+ key: string | null
44
+ }
45
+
46
+ /** The event-target face the store needs for cross-tab notifications. */
47
+ export interface StorageEvents {
48
+ addEventListener(type: 'storage', listener: (event: StorageChangeEvent) => void): void
49
+ removeEventListener(type: 'storage', listener: (event: StorageChangeEvent) => void): void
50
+ }
51
+
33
52
  /**
34
53
  * Structural row check with the status left unvalidated (see {@link parseLedger}).
35
54
  * The `schedule` field is deliberately NOT checked here: a malformed schedule
@@ -127,10 +146,16 @@ export class LocalStorageTaskStore implements TaskStore {
127
146
  /**
128
147
  * @param key - storage key for the ledger document.
129
148
  * @param storage - storage backend (defaults to the global localStorage; tests inject fakes).
149
+ * @param events - storage-event target for cross-tab notifications (defaults
150
+ * to the browser global; undefined in non-browser runtimes, where the
151
+ * subscription becomes a no-op).
130
152
  */
131
153
  constructor(
132
154
  private readonly key: string = DEFAULT_STORAGE_KEY,
133
155
  private readonly storage: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | undefined = globalThis.localStorage,
156
+ private readonly events: StorageEvents | undefined = typeof (globalThis as { addEventListener?: unknown }).addEventListener === 'function'
157
+ ? (globalThis as unknown as StorageEvents)
158
+ : undefined,
134
159
  ) {}
135
160
 
136
161
  load(): TaskRecord[] {
@@ -163,6 +188,22 @@ export class LocalStorageTaskStore implements TaskStore {
163
188
  console.error('[dsh-task-board] task ledger clear failed', error)
164
189
  }
165
190
  }
191
+
192
+ /**
193
+ * Cross-tab change subscription (see {@link TaskStore.subscribeExternal}).
194
+ * The browser fires the storage event in every OTHER tab of the same origin
195
+ * when one tab writes; a null key means the whole storage was cleared. Both
196
+ * cases reload the ledger here; unrelated keys are ignored.
197
+ */
198
+ subscribeExternal(listener: () => void): () => void {
199
+ if (this.events === undefined) return () => {}
200
+ const onStorage = (event: StorageChangeEvent): void => {
201
+ if (event.key !== null && event.key !== this.key) return
202
+ listener()
203
+ }
204
+ this.events.addEventListener('storage', onStorage)
205
+ return () => { this.events?.removeEventListener('storage', onStorage) }
206
+ }
166
207
  }
167
208
 
168
209
  /** In-memory backend (tests, and a fallback when storage is unavailable). */