@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 +88 -6
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +64 -4
- package/dist/index.d.ts +64 -4
- 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
|
|
|
@@ -127,10 +127,92 @@ unsubscribe();
|
|
|
127
127
|
|
|
128
128
|
**Callback data:**
|
|
129
129
|
|
|
130
|
-
| Property
|
|
131
|
-
|
|
|
132
|
-
| `windowId`
|
|
133
|
-
| `state`
|
|
134
|
-
| `
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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": {
|