@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 +37 -13
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +43 -14
- package/dist/index.d.ts +43 -14
- package/dist/index.js +1 -1
- package/package.json +4 -4
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
|
|
140
|
-
|
|
141
|
-
worker, and can report `hidden` twice in a row (a visibility change
|
|
142
|
-
port disconnect)
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
panel is
|
|
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
|
-
|
|
173
|
-
was never seen visible, so a service worker
|
|
174
|
-
stays quiet instead of reporting a close the
|
|
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
|
-
|
|
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){
|
|
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
|
-
*
|
|
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
|
|
83
|
-
*
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
83
|
-
*
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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){
|
|
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.
|
|
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/
|
|
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": {
|