@pixpilot/chrome-lifecycle 0.9.0 → 0.10.5

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/README.md CHANGED
@@ -110,7 +110,7 @@ import { getSidePanelStateForWindow } from '@pixpilot/chrome-lifecycle';
110
110
  const state = getSidePanelStateForWindow(windowId);
111
111
  ```
112
112
 
113
- ##### `onSidePanelStateChange(listener)`
113
+ ##### `onSidePanelStateChange(listener, options?)`
114
114
 
115
115
  Listens for side panel state changes across all windows.
116
116
 
@@ -134,18 +134,41 @@ unsubscribe();
134
134
  | `previousState` | `'visible'` \| `'hidden'` \| `undefined` | State known before this one, `undefined` if new |
135
135
  | `reason` | `string` | What triggered the change |
136
136
 
137
+ **Options:**
138
+
139
+ | Property | Type | Default | Description |
140
+ | ---------------- | --------- | ------- | -------------------------------------------------------- |
141
+ | `includeRepeats` | `boolean` | `false` | Also deliver reports that repeat the state already known |
142
+
137
143
  **Returns:** Unsubscribe function
138
144
 
139
- This fires for **every** state report, including repeats of a state you already know
140
- aboutthe tracker re-reports `visible` when it reconnects to a restarted service
141
- worker, and can report `hidden` twice in a row (a visibility change followed by a
142
- port disconnect). If you want transitions rather than reports, use
143
- `onSidePanelShown` / `onSidePanelHidden` below instead of comparing states yourself.
145
+ This fires when a window's state actually **changes**. The tracker reports the same
146
+ state more than once it re-reports `visible` when it reconnects to a restarted
147
+ service worker, and a panel can report `hidden` twice in a row (a visibility change,
148
+ then a port disconnect) and those repeats are filtered out, so listeners don't
149
+ have to track the previous state themselves.
150
+
151
+ Pass `{ includeRepeats: true }` to observe the raw tracker feed instead. The option
152
+ is per listener, so one listener can watch changes while another watches everything:
153
+
154
+ ```typescript
155
+ onSidePanelStateChange(({ windowId, state }) => syncPanel(windowId, state));
156
+
157
+ onSidePanelStateChange(
158
+ ({ reason }) => {
159
+ // 'visibility-change' hidden means the panel was hidden;
160
+ // 'port-disconnected' hidden means its document went away.
161
+ if (reason === 'port-disconnected') dropDocumentCache();
162
+ },
163
+ { includeRepeats: true },
164
+ );
165
+ ```
144
166
 
145
167
  ##### `onSidePanelShown(listener)`
146
168
 
147
- Fires only when a side panel _becomes_ visible. This is the event to use for "the
148
- panel is back on screen, resync it".
169
+ `onSidePanelStateChange` narrowed to one direction, for code that only cares when a
170
+ side panel _becomes_ visible. This is the event to use for "the panel is back on
171
+ screen, resync it".
149
172
 
150
173
  ```typescript
151
174
  import { onSidePanelShown } from '@pixpilot/chrome-lifecycle';
@@ -169,9 +192,10 @@ It covers three cases, distinguishable via `reason`:
169
192
 
170
193
  ##### `onSidePanelHidden(listener)`
171
194
 
172
- Fires only when a side panel stops being visible. Nothing fires for a window that
173
- was never seen visible, so a service worker restart followed by a port disconnect
174
- stays quiet instead of reporting a close the listener never saw open.
195
+ The same narrowing for the other direction: fires only when a side panel stops being
196
+ visible. Nothing fires for a window that was never seen visible, so a service worker
197
+ restart followed by a port disconnect stays quiet instead of reporting a close the
198
+ listener never saw open.
175
199
 
176
200
  **Callback data:** same as `onSidePanelStateChange`
177
201
 
@@ -186,8 +210,8 @@ including anything stale it was showing before the switch.
186
210
 
187
211
  That surfaces here as `visibility-change`, never `document-load`. A listener that
188
212
  only refreshes on `document-load` will look correct until a user has two side panel
189
- extensions installed, and then silently serve stale content. `onSidePanelShown`
190
- handles both.
213
+ extensions installed, and then silently serve stale content. `onSidePanelShown` — or
214
+ `onSidePanelStateChange` checking for `state === 'visible'` — covers both.
191
215
 
192
216
  There is no Chrome API to ask whether your panel is the one currently on screen, so
193
217
  `document.hidden` in the panel document is the only available signal. Leave
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- const e=new Map;let t=!1;function n(){t||(t=!0,chrome.windows.onRemoved.addListener(t=>{let n=e.get(t);n&&(n.forEach(e=>{e()}),e.delete(t))}))}function r(t,r){n();let i=e.get(t);return i||(i=new Set,e.set(t,i)),i.add(r),()=>{let n=e.get(t);n&&(n.delete(r),n.size===0&&e.delete(t))}}const i=250,a=5e3,o=2;let s=!1;function c(e={}){if(s)return console.info(`Side panel state tracker already initialized`),()=>{};s=!0;let{trackDocumentVisibility:t=!0}=e,n,r,i=0,a,o=!1;function c(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(o)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let s=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(o||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**i,5e3);i+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:s,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!o){c();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,i=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(o=!0,c(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(a=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,a)),f(`document-load`)}),()=>{o=!0,c(),a&&document.removeEventListener(`visibilitychange`,a);let e=n;n=void 0,e?.disconnect(),s=!1}}const l=new Map,u=new Map,d=new Set;let f=!1;function p(){f||(f=!0,chrome.action.onClicked.addListener(e=>{let t=l.get(e.windowId);t&&t.state===`visible`?t.port&&(l.delete(e.windowId),t.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:e.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(e=>{e.name===chrome.runtime.id&&(e.onMessage.addListener(t=>{t.type===`side-panel-state-tracker`&&t.state&&g({port:e,state:t.state,reason:t.reason??`unknown`,windowId:t.windowId,type:t.type})}),e.onDisconnect.addListener(e=>{Array.from(l.entries()).forEach(([t,n])=>{n.port&&n.port===e&&g({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(e=>{l.delete(e),u.delete(e)}))}function m(){f||=(p(),!0)}function h(e,t){if(e.type!==`side-panel-state-tracker`)return;let n={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t};d.forEach(e=>{try{e(n)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function g(e){let{windowId:t,state:n}=e,r=u.get(t);if(e.type===`side-panel-state-tracker`&&u.set(t,n),n===`hidden`){l.delete(t),h(e,r);return}l.set(t,e),h(e,r)}function _(e){return m(),l.get(e)?.state}function v(e){return m(),_(e)===`visible`}function y(e){return m(),d.add(e),()=>{d.delete(e)}}function b(e){return y(t=>{t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function x(e){return y(t=>{t.state===`hidden`&&t.previousState===`visible`&&e(t)})}exports.getSidePanelStateForWindow=_,exports.initSidePanelStateManager=p,exports.initializeSidePanelStateTracker=c,exports.isWindowSidePanelVisible=v,exports.onSidePanelHidden=x,exports.onSidePanelShown=b,exports.onSidePanelStateChange=y,exports.onWindowClose=r;
1
+ const e=new Map;let t=!1;function n(){t||(t=!0,chrome.windows.onRemoved.addListener(t=>{let n=e.get(t);n&&(n.forEach(e=>{e()}),e.delete(t))}))}function r(t,r){n();let i=e.get(t);return i||(i=new Set,e.set(t,i)),i.add(r),()=>{let n=e.get(t);n&&(n.delete(r),n.size===0&&e.delete(t))}}const i=250,a=5e3,o=2;let s=!1;function c(e={}){if(s)return console.info(`Side panel state tracker already initialized`),()=>{};s=!0;let{trackDocumentVisibility:t=!0}=e,n,r,i=0,a,o=!1;function c(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(o)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let s=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(o||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**i,5e3);i+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:s,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!o){c();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,i=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(o=!0,c(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(a=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,a)),f(`document-load`)}),()=>{o=!0,c(),a&&document.removeEventListener(`visibilitychange`,a);let e=n;n=void 0,e?.disconnect(),s=!1}}const l=new Map,u=new Map,d=new Set;let f=!1;function p(){f||(f=!0,chrome.action.onClicked.addListener(e=>{let t=l.get(e.windowId);t&&t.state===`visible`?t.port&&(l.delete(e.windowId),t.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:e.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(e=>{e.name===chrome.runtime.id&&(e.onMessage.addListener(t=>{t.type===`side-panel-state-tracker`&&t.state&&g({port:e,state:t.state,reason:t.reason??`unknown`,windowId:t.windowId,type:t.type})}),e.onDisconnect.addListener(e=>{Array.from(l.entries()).forEach(([t,n])=>{n.port&&n.port===e&&g({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(e=>{l.delete(e),u.delete(e)}))}function m(){f||=(p(),!0)}function h(e,t){if(e.type!==`side-panel-state-tracker`)return;let n={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t},r=e.state===t;d.forEach(({listener:e,includeRepeats:t})=>{if(!(r&&!t))try{e(n)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function g(e){let{windowId:t,state:n}=e,r=u.get(t);if(e.type===`side-panel-state-tracker`&&u.set(t,n),n===`hidden`){l.delete(t),h(e,r);return}l.set(t,e),h(e,r)}function _(e){return m(),l.get(e)?.state}function v(e){return m(),_(e)===`visible`}function y(e,t={}){m();let n={listener:e,includeRepeats:t.includeRepeats===!0};return d.add(n),()=>{d.delete(n)}}function b(e){return y(t=>{t.reason!==`reconnected`&&t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function x(e){return y(t=>{t.reason!==`reconnected`&&t.state===`hidden`&&t.previousState===`visible`&&e(t)})}exports.getSidePanelStateForWindow=_,exports.initSidePanelStateManager=p,exports.initializeSidePanelStateTracker=c,exports.isWindowSidePanelVisible=v,exports.onSidePanelHidden=x,exports.onSidePanelShown=b,exports.onSidePanelStateChange=y,exports.onWindowClose=r;
package/dist/index.d.cts CHANGED
@@ -60,6 +60,20 @@ interface SidePanelClientMessage {
60
60
  //#endregion
61
61
  //#region src/sidepanel-state-manager.d.ts
62
62
  type SidePanelStateListener = (data: SidePanelStateChangeData) => void;
63
+ interface SidePanelStateChangeOptions {
64
+ /**
65
+ * Also deliver reports that repeat a state already known for that window — a
66
+ * reconnect while still visible, or a `port-disconnected` after the panel
67
+ * already reported itself hidden.
68
+ *
69
+ * Off by default, so listeners see state *changes*. Turn it on to observe the
70
+ * raw tracker feed, e.g. to tell "the document died" apart from "the document
71
+ * was hidden" when both arrive as `hidden`.
72
+ *
73
+ * @default false
74
+ */
75
+ includeRepeats?: boolean;
76
+ }
63
77
  /**
64
78
  * Initializes the side panel state manager.
65
79
  * Sets up Chrome event listeners for action clicks and runtime connections.
@@ -71,38 +85,53 @@ declare function getSidePanelStateForWindow(windowId: number): SidePanelState |
71
85
  declare function isWindowSidePanelVisible(windowId: number): boolean;
72
86
  /**
73
87
  * Adds a listener for side panel state changes.
74
- * The listener will be called whenever the side panel state changes (visible/hidden).
88
+ *
89
+ * The listener is called when a window's side panel state actually changes. The
90
+ * tracker reports the same state more than once — it re-reports `visible` when it
91
+ * reconnects to a restarted service worker, and a panel can report `hidden` twice
92
+ * (a visibility change, then a port disconnect) — and those repeats are filtered
93
+ * out unless {@link SidePanelStateChangeOptions.includeRepeats} is set.
94
+ *
75
95
  * Note: Heartbeat messages do not trigger listeners, and timestamp is excluded from the data.
76
96
  *
77
97
  * @param listener - Callback function that receives state change data
98
+ * @param options - Delivery options
78
99
  * @returns Unsubscribe function to remove the listener
79
100
  */
80
- declare function onSidePanelStateChange(listener: SidePanelStateListener): () => void;
101
+ declare function onSidePanelStateChange(listener: SidePanelStateListener, options?: SidePanelStateChangeOptions): () => void;
81
102
  /**
82
- * Adds a listener that fires only when a side panel *becomes* visible, skipping
83
- * repeats of a state you already knew about.
103
+ * Adds a listener that fires only when a side panel *becomes* visible.
104
+ *
105
+ * A narrowed {@link onSidePanelStateChange} for code that only cares about one
106
+ * direction. This is the event to use for "the panel is back on screen, resync
107
+ * it". It covers a freshly loaded document (`reason: 'document-load'`) and a panel
108
+ * Chrome had cached while another extension's side panel took over the slot
109
+ * (`reason: 'visibility-change'`).
84
110
  *
85
- * This is the event to use for "the panel is back on screen, resync it". It covers
86
- * a freshly loaded document (`reason: 'document-load'`), a panel Chrome had cached
87
- * while another extension's side panel took over the slot
88
- * (`reason: 'visibility-change'`), and the first report after a service worker
89
- * restart (`reason: 'reconnected'`).
111
+ * Reconnects (`reason: 'reconnected'`) are not shows and never fire this. The
112
+ * tracker reconnects when the service worker is torn down and restarted, and the
113
+ * panel document lives through that untouched nothing appeared, so nothing needs
114
+ * resyncing. It has to be excluded explicitly because a restarted worker has no
115
+ * record of the window, leaving `previousState` empty, which would otherwise make
116
+ * every recycle look like a fresh show.
90
117
  *
91
118
  * @param listener - Callback function that receives state change data
92
119
  * @returns Unsubscribe function to remove the listener
93
120
  */
94
121
  declare function onSidePanelShown(listener: SidePanelStateListener): () => void;
95
122
  /**
96
- * Adds a listener that fires only when a side panel *stops* being visible, skipping
97
- * repeats of a state you already knew about.
123
+ * Adds a listener that fires only when a side panel *stops* being visible.
98
124
  *
99
- * Nothing fires for a window that was never seen visible, so a service worker
125
+ * A narrowed {@link onSidePanelStateChange} for code that only cares about one
126
+ * direction. Nothing fires for a window that was never seen visible, so a service worker
100
127
  * restart followed by a port disconnect stays quiet instead of reporting a close
101
- * that the listener never saw open.
128
+ * that the listener never saw open. Reconnects are excluded for the same reason
129
+ * they are in {@link onSidePanelShown}: they report the state the panel is already
130
+ * in, not a move into it.
102
131
  *
103
132
  * @param listener - Callback function that receives state change data
104
133
  * @returns Unsubscribe function to remove the listener
105
134
  */
106
135
  declare function onSidePanelHidden(listener: SidePanelStateListener): () => void;
107
136
  //#endregion
108
- export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateData, SidePanelStateListener, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
137
+ export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateChangeOptions, SidePanelStateData, SidePanelStateListener, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
package/dist/index.d.ts CHANGED
@@ -60,6 +60,20 @@ interface SidePanelClientMessage {
60
60
  //#endregion
61
61
  //#region src/sidepanel-state-manager.d.ts
62
62
  type SidePanelStateListener = (data: SidePanelStateChangeData) => void;
63
+ interface SidePanelStateChangeOptions {
64
+ /**
65
+ * Also deliver reports that repeat a state already known for that window — a
66
+ * reconnect while still visible, or a `port-disconnected` after the panel
67
+ * already reported itself hidden.
68
+ *
69
+ * Off by default, so listeners see state *changes*. Turn it on to observe the
70
+ * raw tracker feed, e.g. to tell "the document died" apart from "the document
71
+ * was hidden" when both arrive as `hidden`.
72
+ *
73
+ * @default false
74
+ */
75
+ includeRepeats?: boolean;
76
+ }
63
77
  /**
64
78
  * Initializes the side panel state manager.
65
79
  * Sets up Chrome event listeners for action clicks and runtime connections.
@@ -71,38 +85,53 @@ declare function getSidePanelStateForWindow(windowId: number): SidePanelState |
71
85
  declare function isWindowSidePanelVisible(windowId: number): boolean;
72
86
  /**
73
87
  * Adds a listener for side panel state changes.
74
- * The listener will be called whenever the side panel state changes (visible/hidden).
88
+ *
89
+ * The listener is called when a window's side panel state actually changes. The
90
+ * tracker reports the same state more than once — it re-reports `visible` when it
91
+ * reconnects to a restarted service worker, and a panel can report `hidden` twice
92
+ * (a visibility change, then a port disconnect) — and those repeats are filtered
93
+ * out unless {@link SidePanelStateChangeOptions.includeRepeats} is set.
94
+ *
75
95
  * Note: Heartbeat messages do not trigger listeners, and timestamp is excluded from the data.
76
96
  *
77
97
  * @param listener - Callback function that receives state change data
98
+ * @param options - Delivery options
78
99
  * @returns Unsubscribe function to remove the listener
79
100
  */
80
- declare function onSidePanelStateChange(listener: SidePanelStateListener): () => void;
101
+ declare function onSidePanelStateChange(listener: SidePanelStateListener, options?: SidePanelStateChangeOptions): () => void;
81
102
  /**
82
- * Adds a listener that fires only when a side panel *becomes* visible, skipping
83
- * repeats of a state you already knew about.
103
+ * Adds a listener that fires only when a side panel *becomes* visible.
104
+ *
105
+ * A narrowed {@link onSidePanelStateChange} for code that only cares about one
106
+ * direction. This is the event to use for "the panel is back on screen, resync
107
+ * it". It covers a freshly loaded document (`reason: 'document-load'`) and a panel
108
+ * Chrome had cached while another extension's side panel took over the slot
109
+ * (`reason: 'visibility-change'`).
84
110
  *
85
- * This is the event to use for "the panel is back on screen, resync it". It covers
86
- * a freshly loaded document (`reason: 'document-load'`), a panel Chrome had cached
87
- * while another extension's side panel took over the slot
88
- * (`reason: 'visibility-change'`), and the first report after a service worker
89
- * restart (`reason: 'reconnected'`).
111
+ * Reconnects (`reason: 'reconnected'`) are not shows and never fire this. The
112
+ * tracker reconnects when the service worker is torn down and restarted, and the
113
+ * panel document lives through that untouched nothing appeared, so nothing needs
114
+ * resyncing. It has to be excluded explicitly because a restarted worker has no
115
+ * record of the window, leaving `previousState` empty, which would otherwise make
116
+ * every recycle look like a fresh show.
90
117
  *
91
118
  * @param listener - Callback function that receives state change data
92
119
  * @returns Unsubscribe function to remove the listener
93
120
  */
94
121
  declare function onSidePanelShown(listener: SidePanelStateListener): () => void;
95
122
  /**
96
- * Adds a listener that fires only when a side panel *stops* being visible, skipping
97
- * repeats of a state you already knew about.
123
+ * Adds a listener that fires only when a side panel *stops* being visible.
98
124
  *
99
- * Nothing fires for a window that was never seen visible, so a service worker
125
+ * A narrowed {@link onSidePanelStateChange} for code that only cares about one
126
+ * direction. Nothing fires for a window that was never seen visible, so a service worker
100
127
  * restart followed by a port disconnect stays quiet instead of reporting a close
101
- * that the listener never saw open.
128
+ * that the listener never saw open. Reconnects are excluded for the same reason
129
+ * they are in {@link onSidePanelShown}: they report the state the panel is already
130
+ * in, not a move into it.
102
131
  *
103
132
  * @param listener - Callback function that receives state change data
104
133
  * @returns Unsubscribe function to remove the listener
105
134
  */
106
135
  declare function onSidePanelHidden(listener: SidePanelStateListener): () => void;
107
136
  //#endregion
108
- export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateData, SidePanelStateListener, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
137
+ export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateChangeOptions, SidePanelStateData, SidePanelStateListener, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- const e=new Map;let t=!1;function n(){t||(t=!0,chrome.windows.onRemoved.addListener(t=>{let n=e.get(t);n&&(n.forEach(e=>{e()}),e.delete(t))}))}function r(t,r){n();let i=e.get(t);return i||(i=new Set,e.set(t,i)),i.add(r),()=>{let n=e.get(t);n&&(n.delete(r),n.size===0&&e.delete(t))}}let i=!1;function a(e={}){if(i)return console.info(`Side panel state tracker already initialized`),()=>{};i=!0;let{trackDocumentVisibility:t=!0}=e,n,r,a=0,o,s=!1;function c(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(s)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let i=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(s||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**a,5e3);a+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:i,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!s){c();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,a=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(s=!0,c(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(o=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,o)),f(`document-load`)}),()=>{s=!0,c(),o&&document.removeEventListener(`visibilitychange`,o);let e=n;n=void 0,e?.disconnect(),i=!1}}const o=new Map,s=new Map,c=new Set;let l=!1;function u(){l||(l=!0,chrome.action.onClicked.addListener(e=>{let t=o.get(e.windowId);t&&t.state===`visible`?t.port&&(o.delete(e.windowId),t.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:e.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(e=>{e.name===chrome.runtime.id&&(e.onMessage.addListener(t=>{t.type===`side-panel-state-tracker`&&t.state&&p({port:e,state:t.state,reason:t.reason??`unknown`,windowId:t.windowId,type:t.type})}),e.onDisconnect.addListener(e=>{Array.from(o.entries()).forEach(([t,n])=>{n.port&&n.port===e&&p({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(e=>{o.delete(e),s.delete(e)}))}function d(){l||=(u(),!0)}function f(e,t){if(e.type!==`side-panel-state-tracker`)return;let n={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t};c.forEach(e=>{try{e(n)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function p(e){let{windowId:t,state:n}=e,r=s.get(t);if(e.type===`side-panel-state-tracker`&&s.set(t,n),n===`hidden`){o.delete(t),f(e,r);return}o.set(t,e),f(e,r)}function m(e){return d(),o.get(e)?.state}function h(e){return d(),m(e)===`visible`}function g(e){return d(),c.add(e),()=>{c.delete(e)}}function _(e){return g(t=>{t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function v(e){return g(t=>{t.state===`hidden`&&t.previousState===`visible`&&e(t)})}export{m as getSidePanelStateForWindow,u as initSidePanelStateManager,a as initializeSidePanelStateTracker,h as isWindowSidePanelVisible,v as onSidePanelHidden,_ as onSidePanelShown,g as onSidePanelStateChange,r as onWindowClose};
1
+ const e=new Map;let t=!1;function n(){t||(t=!0,chrome.windows.onRemoved.addListener(t=>{let n=e.get(t);n&&(n.forEach(e=>{e()}),e.delete(t))}))}function r(t,r){n();let i=e.get(t);return i||(i=new Set,e.set(t,i)),i.add(r),()=>{let n=e.get(t);n&&(n.delete(r),n.size===0&&e.delete(t))}}let i=!1;function a(e={}){if(i)return console.info(`Side panel state tracker already initialized`),()=>{};i=!0;let{trackDocumentVisibility:t=!0}=e,n,r,a=0,o,s=!1;function c(){r&&=(clearTimeout(r),void 0)}return chrome.windows.getCurrent(e=>{if(s)return;if(e.id==null){console.error(`[side-panel-state-tracker] Could not get window ID`);return}let i=e.id;function l(){return t&&document.hidden?`hidden`:`visible`}function u(){if(s||r)return;n=void 0,console.info(`[side-panel-state-tracker] Connection to background lost, scheduling reconnect...`);let e=Math.min(250*2**a,5e3);a+=1,r=setTimeout(()=>{r=void 0,f(`reconnected`)},e)}function d(e){let t=n;if(!t){u();return}try{t.postMessage({...e,windowId:i,type:`side-panel-state-tracker`,timestamp:Date.now()})}catch(e){console.error(`[side-panel-state-tracker] Failed to send message:`,e),u()}}function f(e){if(!s){c();try{let t=chrome.runtime.connect({name:chrome.runtime.id});n=t,a=0,t.onDisconnect.addListener(()=>{n===t&&(console.info(`[side-panel-state-tracker] Background connection lost.`),u())}),t.onMessage.addListener(e=>{e.type===`close-side-panel`&&(s=!0,c(),window.close())}),d({state:e===`document-load`?`visible`:l(),reason:e})}catch(e){console.error(`[side-panel-state-tracker] Failed to connect:`,e),u()}}}t&&(o=()=>{d({state:l(),reason:`visibility-change`})},document.addEventListener(`visibilitychange`,o)),f(`document-load`)}),()=>{s=!0,c(),o&&document.removeEventListener(`visibilitychange`,o);let e=n;n=void 0,e?.disconnect(),i=!1}}const o=new Map,s=new Map,c=new Set;let l=!1;function u(){l||(l=!0,chrome.action.onClicked.addListener(e=>{let t=o.get(e.windowId);t&&t.state===`visible`?t.port&&(o.delete(e.windowId),t.port.postMessage({type:`close-side-panel`})):chrome.sidePanel.open({windowId:e.windowId}).catch(console.error)}),chrome.runtime.onConnect.addListener(e=>{e.name===chrome.runtime.id&&(e.onMessage.addListener(t=>{t.type===`side-panel-state-tracker`&&t.state&&p({port:e,state:t.state,reason:t.reason??`unknown`,windowId:t.windowId,type:t.type})}),e.onDisconnect.addListener(e=>{Array.from(o.entries()).forEach(([t,n])=>{n.port&&n.port===e&&p({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}),chrome.windows.onRemoved.addListener(e=>{o.delete(e),s.delete(e)}))}function d(){l||=(u(),!0)}function f(e,t){if(e.type!==`side-panel-state-tracker`)return;let n={state:e.state,reason:e.reason,windowId:e.windowId,previousState:t},r=e.state===t;c.forEach(({listener:e,includeRepeats:t})=>{if(!(r&&!t))try{e(n)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function p(e){let{windowId:t,state:n}=e,r=s.get(t);if(e.type===`side-panel-state-tracker`&&s.set(t,n),n===`hidden`){o.delete(t),f(e,r);return}o.set(t,e),f(e,r)}function m(e){return d(),o.get(e)?.state}function h(e){return d(),m(e)===`visible`}function g(e,t={}){d();let n={listener:e,includeRepeats:t.includeRepeats===!0};return c.add(n),()=>{c.delete(n)}}function _(e){return g(t=>{t.reason!==`reconnected`&&t.state===`visible`&&t.previousState!==`visible`&&e(t)})}function v(e){return g(t=>{t.reason!==`reconnected`&&t.state===`hidden`&&t.previousState===`visible`&&e(t)})}export{m as getSidePanelStateForWindow,u as initSidePanelStateManager,a as initializeSidePanelStateTracker,h as isWindowSidePanelVisible,v as onSidePanelHidden,_ as onSidePanelShown,g as onSidePanelStateChange,r as onWindowClose};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pixpilot/chrome-lifecycle",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "0.10.5",
5
5
  "description": "Lifecycle management utilities for Chrome extensions.",
6
6
  "author": "m.doaie <m.doaie@hotmail.com>",
7
7
  "license": "MIT",
@@ -30,10 +30,10 @@
30
30
  "tsdown": "^0.15.9",
31
31
  "typescript": "^5.9.3",
32
32
  "@internal/eslint-config": "0.3.0",
33
- "@internal/tsconfig": "0.1.0",
34
33
  "@internal/prettier-config": "0.1.0",
35
- "@internal/vitest-config": "0.1.0",
36
- "@internal/tsdown-config": "0.1.0"
34
+ "@internal/tsconfig": "0.1.0",
35
+ "@internal/tsdown-config": "0.1.0",
36
+ "@internal/vitest-config": "0.1.0"
37
37
  },
38
38
  "prettier": "@internal/prettier-config",
39
39
  "scripts": {