@pixpilot/chrome-lifecycle 0.8.0 → 0.10.0

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
 
@@ -127,10 +127,92 @@ unsubscribe();
127
127
 
128
128
  **Callback data:**
129
129
 
130
- | Property | Type | Description |
131
- | ---------- | ------------------------- | ------------------------- |
132
- | `windowId` | `number` | Chrome window ID |
133
- | `state` | `'visible'` \| `'hidden'` | Current side panel state |
134
- | `reason` | `string` | What triggered the change |
130
+ | Property | Type | Description |
131
+ | --------------- | ---------------------------------------- | ----------------------------------------------- |
132
+ | `windowId` | `number` | Chrome window ID |
133
+ | `state` | `'visible'` \| `'hidden'` | Current side panel state |
134
+ | `previousState` | `'visible'` \| `'hidden'` \| `undefined` | State known before this one, `undefined` if new |
135
+ | `reason` | `string` | What triggered the change |
136
+
137
+ **Options:**
138
+
139
+ | Property | Type | Default | Description |
140
+ | ---------------- | --------- | ------- | -------------------------------------------------------- |
141
+ | `includeRepeats` | `boolean` | `false` | Also deliver reports that repeat the state already known |
142
+
143
+ **Returns:** Unsubscribe function
144
+
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
+ ```
166
+
167
+ ##### `onSidePanelShown(listener)`
168
+
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".
172
+
173
+ ```typescript
174
+ import { onSidePanelShown } from '@pixpilot/chrome-lifecycle';
175
+
176
+ const unsubscribe = onSidePanelShown(({ windowId, reason }) => {
177
+ refreshPanelContents(windowId);
178
+ });
179
+ ```
180
+
181
+ It covers three cases, distinguishable via `reason`:
182
+
183
+ | `reason` | When |
184
+ | ------------------- | ---------------------------------------------------------------------------------- |
185
+ | `document-load` | A freshly loaded panel document |
186
+ | `visibility-change` | A cached document Chrome is showing again — see the note on other extensions below |
187
+ | `reconnected` | First report after the service worker restarted, so your background state was lost |
188
+
189
+ **Callback data:** same as `onSidePanelStateChange`
190
+
191
+ **Returns:** Unsubscribe function
192
+
193
+ ##### `onSidePanelHidden(listener)`
194
+
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.
199
+
200
+ **Callback data:** same as `onSidePanelStateChange`
135
201
 
136
202
  **Returns:** Unsubscribe function
203
+
204
+ #### When another extension's side panel takes over
205
+
206
+ Chrome gives all extensions one side panel slot per window. When the user opens a
207
+ different extension's panel, yours does **not** get torn down — Chrome keeps the
208
+ document alive and hides it, so it comes back with all of its state intact,
209
+ including anything stale it was showing before the switch.
210
+
211
+ That surfaces here as `visibility-change`, never `document-load`. A listener that
212
+ only refreshes on `document-load` will look correct until a user has two side panel
213
+ extensions installed, and then silently serve stale content. `onSidePanelShown` — or
214
+ `onSidePanelStateChange` checking for `state === 'visible'` — covers both.
215
+
216
+ There is no Chrome API to ask whether your panel is the one currently on screen, so
217
+ `document.hidden` in the panel document is the only available signal. Leave
218
+ `trackDocumentVisibility` enabled if you rely on this.
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 Set;let d=!1;function f(){d||(d=!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&&h({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&&h({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}))}function p(){d||=(f(),!0)}function m(e){if(e.type!==`side-panel-state-tracker`)return;let t={state:e.state,reason:e.reason,windowId:e.windowId};u.forEach(e=>{try{e(t)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function h(e){let{windowId:t,state:n}=e;if(n===`hidden`){l.delete(t),m(e);return}l.set(t,e),m(e)}function g(e){return p(),l.get(e)?.state}function _(e){return p(),g(e)===`visible`}function v(e){return p(),u.add(e),()=>{u.delete(e)}}exports.getSidePanelStateForWindow=g,exports.initSidePanelStateManager=f,exports.initializeSidePanelStateTracker=c,exports.isWindowSidePanelVisible=_,exports.onSidePanelStateChange=v,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.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;
package/dist/index.d.cts CHANGED
@@ -41,13 +41,39 @@ interface SidePanelStateData extends BaseSidePanelMessage {
41
41
  state: SidePanelState;
42
42
  reason: string;
43
43
  }
44
- type SidePanelStateChangeData = Omit<SidePanelStateData, 'timestamp' | 'type'>;
44
+ interface SidePanelStateChangeData extends Omit<SidePanelStateData, 'timestamp' | 'type'> {
45
+ /**
46
+ * The state recorded for this window before this change.
47
+ *
48
+ * `undefined` means nothing was recorded yet: the first event for a window, or
49
+ * the first event after the service worker restarted and lost its state. Use it
50
+ * to tell a real transition apart from a repeat of the state you already knew —
51
+ * the tracker reports the same state more than once (a reconnect while still
52
+ * visible, consecutive visibility changes), so plain state events are not
53
+ * transitions.
54
+ */
55
+ previousState?: SidePanelState;
56
+ }
45
57
  interface SidePanelClientMessage {
46
58
  type: 'close-side-panel';
47
59
  }
48
60
  //#endregion
49
61
  //#region src/sidepanel-state-manager.d.ts
50
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
+ }
51
77
  /**
52
78
  * Initializes the side panel state manager.
53
79
  * Sets up Chrome event listeners for action clicks and runtime connections.
@@ -59,12 +85,46 @@ declare function getSidePanelStateForWindow(windowId: number): SidePanelState |
59
85
  declare function isWindowSidePanelVisible(windowId: number): boolean;
60
86
  /**
61
87
  * Adds a listener for side panel state changes.
62
- * 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
+ *
63
95
  * Note: Heartbeat messages do not trigger listeners, and timestamp is excluded from the data.
64
96
  *
65
97
  * @param listener - Callback function that receives state change data
98
+ * @param options - Delivery options
99
+ * @returns Unsubscribe function to remove the listener
100
+ */
101
+ declare function onSidePanelStateChange(listener: SidePanelStateListener, options?: SidePanelStateChangeOptions): () => void;
102
+ /**
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
108
+ * a freshly loaded document (`reason: 'document-load'`), a panel Chrome had cached
109
+ * while another extension's side panel took over the slot
110
+ * (`reason: 'visibility-change'`), and the first report after a service worker
111
+ * restart (`reason: 'reconnected'`).
112
+ *
113
+ * @param listener - Callback function that receives state change data
114
+ * @returns Unsubscribe function to remove the listener
115
+ */
116
+ declare function onSidePanelShown(listener: SidePanelStateListener): () => void;
117
+ /**
118
+ * Adds a listener that fires only when a side panel *stops* being visible.
119
+ *
120
+ * A narrowed {@link onSidePanelStateChange} for code that only cares about one
121
+ * direction. Nothing fires for a window that was never seen visible, so a service worker
122
+ * restart followed by a port disconnect stays quiet instead of reporting a close
123
+ * that the listener never saw open.
124
+ *
125
+ * @param listener - Callback function that receives state change data
66
126
  * @returns Unsubscribe function to remove the listener
67
127
  */
68
- declare function onSidePanelStateChange(listener: SidePanelStateListener): () => void;
128
+ declare function onSidePanelHidden(listener: SidePanelStateListener): () => void;
69
129
  //#endregion
70
- export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateData, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelStateChange, onWindowClose };
130
+ export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateChangeOptions, SidePanelStateData, SidePanelStateListener, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelHidden, onSidePanelShown, onSidePanelStateChange, onWindowClose };
package/dist/index.d.ts CHANGED
@@ -41,13 +41,39 @@ interface SidePanelStateData extends BaseSidePanelMessage {
41
41
  state: SidePanelState;
42
42
  reason: string;
43
43
  }
44
- type SidePanelStateChangeData = Omit<SidePanelStateData, 'timestamp' | 'type'>;
44
+ interface SidePanelStateChangeData extends Omit<SidePanelStateData, 'timestamp' | 'type'> {
45
+ /**
46
+ * The state recorded for this window before this change.
47
+ *
48
+ * `undefined` means nothing was recorded yet: the first event for a window, or
49
+ * the first event after the service worker restarted and lost its state. Use it
50
+ * to tell a real transition apart from a repeat of the state you already knew —
51
+ * the tracker reports the same state more than once (a reconnect while still
52
+ * visible, consecutive visibility changes), so plain state events are not
53
+ * transitions.
54
+ */
55
+ previousState?: SidePanelState;
56
+ }
45
57
  interface SidePanelClientMessage {
46
58
  type: 'close-side-panel';
47
59
  }
48
60
  //#endregion
49
61
  //#region src/sidepanel-state-manager.d.ts
50
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
+ }
51
77
  /**
52
78
  * Initializes the side panel state manager.
53
79
  * Sets up Chrome event listeners for action clicks and runtime connections.
@@ -59,12 +85,46 @@ declare function getSidePanelStateForWindow(windowId: number): SidePanelState |
59
85
  declare function isWindowSidePanelVisible(windowId: number): boolean;
60
86
  /**
61
87
  * Adds a listener for side panel state changes.
62
- * 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
+ *
63
95
  * Note: Heartbeat messages do not trigger listeners, and timestamp is excluded from the data.
64
96
  *
65
97
  * @param listener - Callback function that receives state change data
98
+ * @param options - Delivery options
99
+ * @returns Unsubscribe function to remove the listener
100
+ */
101
+ declare function onSidePanelStateChange(listener: SidePanelStateListener, options?: SidePanelStateChangeOptions): () => void;
102
+ /**
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
108
+ * a freshly loaded document (`reason: 'document-load'`), a panel Chrome had cached
109
+ * while another extension's side panel took over the slot
110
+ * (`reason: 'visibility-change'`), and the first report after a service worker
111
+ * restart (`reason: 'reconnected'`).
112
+ *
113
+ * @param listener - Callback function that receives state change data
114
+ * @returns Unsubscribe function to remove the listener
115
+ */
116
+ declare function onSidePanelShown(listener: SidePanelStateListener): () => void;
117
+ /**
118
+ * Adds a listener that fires only when a side panel *stops* being visible.
119
+ *
120
+ * A narrowed {@link onSidePanelStateChange} for code that only cares about one
121
+ * direction. Nothing fires for a window that was never seen visible, so a service worker
122
+ * restart followed by a port disconnect stays quiet instead of reporting a close
123
+ * that the listener never saw open.
124
+ *
125
+ * @param listener - Callback function that receives state change data
66
126
  * @returns Unsubscribe function to remove the listener
67
127
  */
68
- declare function onSidePanelStateChange(listener: SidePanelStateListener): () => void;
128
+ declare function onSidePanelHidden(listener: SidePanelStateListener): () => void;
69
129
  //#endregion
70
- export { InitializeSidePanelStateTrackerOptions, SidePanelClientMessage, SidePanelState, SidePanelStateChangeData, SidePanelStateData, getSidePanelStateForWindow, initSidePanelStateManager, initializeSidePanelStateTracker, isWindowSidePanelVisible, onSidePanelStateChange, onWindowClose };
130
+ 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 Set;let c=!1;function l(){c||(c=!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&&f({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&&f({port:void 0,state:`hidden`,reason:`port-disconnected`,windowId:t,type:`side-panel-state-tracker`})})}))}))}function u(){c||=(l(),!0)}function d(e){if(e.type!==`side-panel-state-tracker`)return;let t={state:e.state,reason:e.reason,windowId:e.windowId};s.forEach(e=>{try{e(t)}catch(e){console.error(`Error in side panel state listener:`,e)}})}function f(e){let{windowId:t,state:n}=e;if(n===`hidden`){o.delete(t),d(e);return}o.set(t,e),d(e)}function p(e){return u(),o.get(e)?.state}function m(e){return u(),p(e)===`visible`}function h(e){return u(),s.add(e),()=>{s.delete(e)}}export{p as getSidePanelStateForWindow,l as initSidePanelStateManager,a as initializeSidePanelStateTracker,m as isWindowSidePanelVisible,h 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.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};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pixpilot/chrome-lifecycle",
3
3
  "type": "module",
4
- "version": "0.8.0",
4
+ "version": "0.10.0",
5
5
  "description": "Lifecycle management utilities for Chrome extensions.",
6
6
  "author": "m.doaie <m.doaie@hotmail.com>",
7
7
  "license": "MIT",
@@ -29,11 +29,11 @@
29
29
  "eslint": "^9.38.0",
30
30
  "tsdown": "^0.15.9",
31
31
  "typescript": "^5.9.3",
32
- "@internal/eslint-config": "0.3.0",
33
32
  "@internal/prettier-config": "0.1.0",
34
- "@internal/vitest-config": "0.1.0",
35
33
  "@internal/tsconfig": "0.1.0",
36
- "@internal/tsdown-config": "0.1.0"
34
+ "@internal/tsdown-config": "0.1.0",
35
+ "@internal/vitest-config": "0.1.0",
36
+ "@internal/eslint-config": "0.3.0"
37
37
  },
38
38
  "prettier": "@internal/prettier-config",
39
39
  "scripts": {