snow-connector 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 snow-connector contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # snow-connector
2
+
3
+ `snow-connector` provides a browser-based connection to a ServiceNow instance so regular users can interact programmatically from Node without requiring admin-managed service accounts or OAuth setup.
4
+
5
+ Because it is browser-based, it works with real login environments (including SSO, proxies, and browser password saving). For example, regular users can use `snow-connector` to access the ServiceNow Table API.
6
+
7
+ ## How connectivity works
8
+
9
+ A Puppeteer-controlled browser is used so users authenticate through an actual browser session.
10
+
11
+ A **worker tab** is the browser tab context `snow-connector` uses for fetches and health checks. The connector keeps this capability available using deterministic tab-selection and provisioning rules (defined in the Decision Table below), so callers can program against a known connection state.
12
+
13
+ Conceptually:
14
+
15
+ - Consumer requests are passed through browser-context fetch so they use the active user session.
16
+ - Health checks run to keep connection state known over time.
17
+ - When an eligible fetch tab is unavailable, provisioning rules create/reuse the right tab context.
18
+ - `g_ck` values are synced into the observable model for development visibility and consumer use.
19
+
20
+ ## What it provides
21
+
22
+ - **Connection** – A class that manages connection state per instance. Each instance gets a numeric `id` assigned sequentially (from the model). It applies explicit connect/health/consumer behavior rules (see Decision Table below).
23
+ - **Observable model** – Internal connector state is published through the shared model and can be observed during development in a browser-based monitor.
24
+ - **Model keys** – For each connection `id`, the model holds: `${id}_conn_status` (`'on'` / `'off'`), `${id}_conn_key`, `${id}_url`, `${id}_validationInterval`, `${id}_last_activity`, `${id}_glide_session_store` (cookie value or `null`). Global key **browser_g_cks** (domain → value) is updated by browser sync and worker-tab sync.
25
+ - **Health checker** – Runs periodic health checks using the same fetch/navigation semantics as the decision table. Key rotation is explicit and scenario-dependent.
26
+ - **Worker tab and reset** – `connection.fetch(url, options)` runs `fetch` in an eligible tab context and updates `${id}_last_activity` on success. `connection.reset()` provisions navigation to the current dynamic health path.
27
+ - **Browser sync** – Helpers to launch Chromium/Chrome or Firefox (with persistent profiles and password saving) and to sync `g_ck` into the model on each page load.
28
+
29
+ ## Usage examples
30
+
31
+ ### Declaring a connection and making requests
32
+
33
+ Assume a shared **model** (e.g. from `observable-state-model`). If snow-connector is a dependency, use `require('snow-connector/...')`; if it’s the same repo, use relative paths (e.g. `require('./providers.js')`).
34
+
35
+ #### Optional: choose a specific browser executable
36
+
37
+ This step is optional. If omitted, Snow-Connector uses Puppeteer's built-in Chromium browser or the browser path set in the `SNOW_CONNECTOR_BROWSER` environment variable.
38
+
39
+ ```javascript
40
+ const provider = require('snow-connector/providers.js').getBrowserProvider();
41
+
42
+ // Uncomment one of the below lines to override the default browser selected by snow-connector or set by the SNOW_CONNECTOR_BROWSER environment variable
43
+ // provider.setExecutablePath('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
44
+ // provider.setExecutablePath('/Applications/Firefox.app/Contents/MacOS/firefox');
45
+ // provider.setExecutablePath('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe');
46
+ // provider.setExecutablePath('C:\\Program Files\\Mozilla Firefox\\firefox.exe');
47
+ ```
48
+
49
+ #### Connection usage
50
+
51
+ ```javascript
52
+ const { Connection } = require('snow-connector/connection.js');
53
+
54
+ // 1. Declare a connection for your ServiceNow instance (id is assigned sequentially by Connection)
55
+ const instanceUrl = 'https://your-instance.service-now.com';
56
+ const connection = new Connection({
57
+ instanceUrl,
58
+ validationInterval: 60000, // optional, defaults to 15000 ms; health tab reload interval
59
+ // browserProvider: provider, // optional, only needed if provider was configured for a specific browser
60
+ });
61
+
62
+ // 2. Wait for connection startup once.
63
+ await connection.ready();
64
+
65
+ // 3. Use connection.fetch when connected
66
+ async function callInstanceApi() {
67
+ if (!connection.isOn()) return;
68
+ const res = await connection.fetch('/api/now/table/incident?sysparm_limit=1');
69
+ console.log('Fetch status:', res.status, res.body);
70
+ }
71
+ ```
72
+
73
+ Summary: create a `Connection({ instanceUrl, validationInterval?, browserProvider? })`; the connection gets an `id` assigned from the model. `startBrowserSync()` is optional if you also want global `g_ck` model sync from other tabs/domains. When the connection is on, use `connection.fetch(url, options)` for API calls (updates `${id}_last_activity`). `connection.fetch` is instance-specific: it requires relative URLs, resolves them to the configured instance, and automatically adds `X-UserToken` (from `g_ck`) when missing, which is needed for many operations. `validationInterval` defaults to 15000 ms if omitted.
74
+
75
+ ### Connection API for consumers
76
+
77
+ - `await connection.ready()` - Wait for startup initialization.
78
+ - `await connection.connect()` - Attempt to establish/confirm connectivity; resolves `true` on success.
79
+ - `connection.isOn()` - Return current connection state (`true`/`false`).
80
+ - `await connection.fetch('/relative/path', options?)` - Execute instance-scoped request. URL must be relative; connector resolves to instance base URL and auto-adds `X-UserToken` when missing. `options` follow the browser Fetch API `RequestInit` shape (for example `method`, `headers`, `body`).
81
+ - `await connection.reset()` - Re-provision navigation to the current health path (optional operational control).
82
+ - `connection.disconnect()` - Mark connection OFF and rotate conn key.
83
+
84
+ ## Decision table (source of truth)
85
+
86
+ | Scenario | Fetchable | Fetch Result | Nav Result | Conn State | Key | Description |
87
+ |---|---|---|---|---|---|---|
88
+ | Connect | Yes | Success | N/A | mark ON | ROTATE | Fetch health probe reached current success suffix, so **mark ON** and **ROTATE key**. |
89
+ | Connect | Yes | Fail | Success | mark ON | ROTATE | Fetch health probe failed, then navigation reached current success suffix, so **mark ON** and **ROTATE key**. |
90
+ | Connect | Yes | Fail | Fail | keep OFF | PRESERVE | Fetch health probe failed, and navigation never reached success suffix, so **keep OFF** and **PRESERVE key** in case of eventual future login success. |
91
+ | Connect | No | N/A | Success | mark ON | ROTATE | No fetchable tab; navigation provisioning reached current success suffix, so **mark ON** and **ROTATE key**. |
92
+ | Connect | No | N/A | Fail | keep OFF | PRESERVE | No fetchable tab; navigation provisioning never reached success suffix, so **keep OFF** and **PRESERVE key** in case of eventual future login success. |
93
+ | Health | Yes | Success | N/A | keep ON | ROTATE | Health fetch reached current success suffix, so **keep ON** and **ROTATE key**. |
94
+ | Health | Yes | Fail | N/A | mark OFF | ROTATE | Health fetch failed to reach current success suffix, so **mark OFF** and **ROTATE key**; navigation is not provisioned after failed health fetches. |
95
+ | Health | No | N/A | Success | keep ON | ROTATE | No fetchable tab; health navigation provisioning reached current success suffix, so **keep ON** and **ROTATE key**. |
96
+ | Health | No | N/A | Fail | mark OFF | ROTATE | No fetchable tab; health navigation provisioning landed off-suffix, so **mark OFF** and **ROTATE key**. |
97
+ | Consumer | Yes | N/A | N/A | (no impact) | (no impact) | Connector executes consumer fetch on selected fetch tab. Connection must first be ON, else an exception is thrown. Consumer ultimately decides if the fetch is a success or fail. |
98
+ | Consumer | No | N/A | Success | keep ON | ROTATE | No fetchable tab; connector provisions navigation to current success path. If navigation reaches suffix, **keep ON** and **ROTATE key**, then proceed with consumer fetch attempt. |
99
+ | Consumer | No | N/A | Fail | mark OFF | ROTATE | No fetchable tab; connector provisions navigation and it landed off-suffix, so **mark OFF** and **ROTATE key**, then throw exception to consumer. |
100
+
101
+ If you want to run an observable-state-model monitor listener, use the single package import:
102
+
103
+ ```javascript
104
+ const { model, ModelManager } = require('observable-state-model');
105
+ new ModelManager(3031, model).start();
106
+ ```
107
+
108
+ ## run.js – example / demo
109
+
110
+ **run.js** is an **example/demo** script. It:
111
+
112
+ - Starts a mock ServiceNow server on port 3099 (for trying the flow without a real instance).
113
+ - Starts one observable-state-model **monitor** on port 3031.
114
+ - Creates a single Connection for a configurable ServiceNow instance URL (its `id` is assigned sequentially).
115
+ - Launches a Puppeteer browser to the instance login/worker tab (health path).
116
+ - Starts browser sync so `browser_g_cks` is kept current as pages load.
117
+
118
+ You can adapt it for your own use:
119
+
120
+ 1. **Demo instance** – The demo uses `instanceUrl` for the Connection; this is dev tooling only, not part of the connector contract. Default is `https://your-instance.service-now.com`. Set the `SNOW_CONNECTOR_DEMO_INSTANCE` environment variable to use a specific instance, or edit the default in the file.
121
+ 2. **Browser** – Snow-Connector uses Puppeteer’s bundled Chromium by default, or the path in `SNOW_CONNECTOR_BROWSER` if set. To override in the demo, uncomment one of the `provider.setExecutablePath(...)` lines in `run.js` (macOS/Windows examples are in the file).
122
+
123
+ With `node run` (or `npm start`) running, open the **monitor** in your browser at **[http://localhost:3031](http://localhost:3031)**. You’ll see the shared model, including keys like `0_conn_status` (or `1_conn_status`, etc., per connection id), `browser_g_cks`, and `0_last_activity`. Log in to the ServiceNow instance in the browser tab opened by the script; connection turns **on** and last activity is set. Log out; connection turns **off**. The monitor shows how connection state changes as you log in and out.
124
+
125
+ ## Integration tests
126
+
127
+ **Integration tests require the mock ServiceNow server to be running on port 3099.** The demo **run.js** starts this server. To run the full test suite:
128
+
129
+ 1. Start the demo: `node run` (or `npm start`).
130
+ 2. In another terminal, run: `npm test`.
131
+
132
+ If the mock server is not running, the integration specs will fail with a message telling you to run `node run` first.
133
+
134
+ ## Scripts
135
+
136
+ - `npm start` – Runs `node run.js` (demo with mock, monitor, browser, and browser sync).
137
+ - `npm test` – Runs Jasmine (unit and integration). Start `node run` first for integration tests.
138
+
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Wrapper for Puppeteer-launched Firefox.
3
+ * Uses a persistent profile directory so password saving and recall work.
4
+ */
5
+
6
+ const puppeteer = require('puppeteer');
7
+ const { join } = require('path');
8
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('fs');
9
+ const os = require('os');
10
+
11
+ const PROFILE_DIR_NAME = '.snow_connector_firefox_profile';
12
+
13
+ /**
14
+ * Get the Firefox profile directory path.
15
+ * @returns {string} Path to the Firefox profile directory
16
+ */
17
+ function getFirefoxProfileDir() {
18
+ const platform = os.platform();
19
+ if (platform === 'win32') {
20
+ const localAppData = process.env.LOCALAPPDATA;
21
+ if (!localAppData) {
22
+ throw new Error('LOCALAPPDATA environment variable not found');
23
+ }
24
+ const profileDir = join(localAppData, 'snow-connector', 'firefox-profile');
25
+ if (!existsSync(profileDir)) {
26
+ mkdirSync(profileDir, { recursive: true });
27
+ }
28
+ return profileDir;
29
+ }
30
+ const profileDir = join(os.homedir(), PROFILE_DIR_NAME);
31
+ if (!existsSync(profileDir)) {
32
+ mkdirSync(profileDir, { recursive: true });
33
+ }
34
+ return profileDir;
35
+ }
36
+
37
+ /**
38
+ * Configure Firefox preferences to enable password saving.
39
+ * @param {string} profileDir - Path to the Firefox profile directory
40
+ */
41
+ function configureFirefoxPasswordSaving(profileDir) {
42
+ const userJsPath = join(profileDir, 'user.js');
43
+ let existingPrefs = '';
44
+ if (existsSync(userJsPath)) {
45
+ try {
46
+ existingPrefs = readFileSync(userJsPath, 'utf8');
47
+ } catch (error) {
48
+ // Create new
49
+ }
50
+ }
51
+
52
+ const passwordPrefs = `
53
+ user_pref("signon.rememberSignons", true);
54
+ user_pref("signon.autofillForms", true);
55
+ user_pref("signon.autofillForms.http", true);
56
+ user_pref("signon.userInputRequiredToCapture.enabled", false);
57
+ user_pref("signon.privateBrowsingCapture.enabled", true);
58
+ user_pref("signon.storeWhenAutocompleteOff", true);
59
+ `;
60
+
61
+ if (!existingPrefs.includes('signon.rememberSignons')) {
62
+ try {
63
+ writeFileSync(userJsPath, existingPrefs + passwordPrefs, 'utf8');
64
+ } catch (error) {
65
+ // Non-fatal
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * @param {Error} error
72
+ * @returns {string}
73
+ */
74
+ function formatFirefoxError(error) {
75
+ let message = `Firefox launch error: ${error.message}`;
76
+ if (error.message.includes('Code: 0') || error.message.includes('Failed to launch')) {
77
+ message += '\n\nPuppeteer cannot connect to Firefox.';
78
+ message += '\nPossible solutions: close all Firefox instances, use Chrome/Chromium, or update Firefox.';
79
+ }
80
+ return message;
81
+ }
82
+
83
+ /**
84
+ * Launch Firefox.
85
+ * @param {Object} [options] - Launch options
86
+ * @param {string} [options.executablePath] - Path to Firefox executable; omit to let Puppeteer download Firefox
87
+ * @param {string} [options.userDataDir] - Override profile directory; omit to use default snow-connector profile
88
+ * @param {string} [options.initialUrl] - URL to open after launch (optional)
89
+ * @returns {Promise<import('puppeteer').Browser>} The launched browser instance
90
+ */
91
+ async function launch(options = {}) {
92
+ const profileDir = options.userDataDir || getFirefoxProfileDir();
93
+ configureFirefoxPasswordSaving(profileDir);
94
+
95
+ const launchOptions = {
96
+ headless: false,
97
+ timeout: 60000,
98
+ browser: 'firefox',
99
+ userDataDir: profileDir,
100
+ extraPrefsFirefox: {
101
+ 'signon.rememberSignons': true,
102
+ 'signon.autofillForms': true,
103
+ 'signon.autofillForms.http': true,
104
+ 'signon.userInputRequiredToCapture.enabled': false,
105
+ 'signon.privateBrowsingCapture.enabled': true,
106
+ 'signon.storeWhenAutocompleteOff': true,
107
+ },
108
+ args: [],
109
+ dumpio: false,
110
+ defaultViewport: null,
111
+ };
112
+
113
+ if (options.executablePath) {
114
+ launchOptions.executablePath = options.executablePath;
115
+ }
116
+
117
+ const doLaunch = async (opts) => {
118
+ const browser = await puppeteer.launch(opts);
119
+ if (options.initialUrl) {
120
+ try {
121
+ const pages = await browser.pages();
122
+ const page = pages[0] || await browser.newPage();
123
+ await page.goto(options.initialUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
124
+ } catch (error) {
125
+ // Browser is running; navigation is best-effort
126
+ }
127
+ }
128
+ return browser;
129
+ };
130
+
131
+ try {
132
+ return await doLaunch(launchOptions);
133
+ } catch (error) {
134
+ if (error.message.includes('Code: 0') || error.message.includes('Failed to launch')) {
135
+ delete launchOptions.browser;
136
+ launchOptions.product = 'firefox';
137
+ try {
138
+ return await doLaunch(launchOptions);
139
+ } catch (fallbackError) {
140
+ throw new Error(formatFirefoxError(fallbackError));
141
+ }
142
+ }
143
+ throw new Error(formatFirefoxError(error));
144
+ }
145
+ }
146
+
147
+ module.exports = {
148
+ launch,
149
+ getFirefoxProfileDir,
150
+ configureFirefoxPasswordSaving,
151
+ };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Wrapper for Puppeteer-launched Chromium or Chrome.
3
+ * Uses a persistent user data directory so password saving and recall work.
4
+ */
5
+
6
+ const puppeteer = require('puppeteer');
7
+ const { join } = require('path');
8
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('fs');
9
+ const os = require('os');
10
+
11
+ const PROFILE_DIR_NAME = '.snow_connector_browser_profile';
12
+
13
+ /**
14
+ * Get the browser profile directory path for Chromium/Chrome.
15
+ * @returns {string} Path to the browser profile directory
16
+ */
17
+ function getBrowserProfileDir() {
18
+ const platform = os.platform();
19
+ if (platform === 'win32') {
20
+ const localAppData = process.env.LOCALAPPDATA;
21
+ if (!localAppData) {
22
+ throw new Error('LOCALAPPDATA environment variable not found');
23
+ }
24
+ const profileDir = join(localAppData, 'snow-connector', 'browser-profile');
25
+ if (!existsSync(profileDir)) {
26
+ mkdirSync(profileDir, { recursive: true });
27
+ }
28
+ return profileDir;
29
+ }
30
+ const profileDir = join(os.homedir(), PROFILE_DIR_NAME);
31
+ if (!existsSync(profileDir)) {
32
+ mkdirSync(profileDir, { recursive: true });
33
+ }
34
+ return profileDir;
35
+ }
36
+
37
+ /**
38
+ * Configure Chrome preferences to enable password saving.
39
+ * @param {string} userDataDir - Path to the user data directory
40
+ */
41
+ function configurePasswordSaving(userDataDir) {
42
+ const defaultProfileDir = join(userDataDir, 'Default');
43
+ if (!existsSync(defaultProfileDir)) {
44
+ mkdirSync(defaultProfileDir, { recursive: true });
45
+ }
46
+
47
+ const preferencesPath = join(defaultProfileDir, 'Preferences');
48
+ let preferences = {};
49
+
50
+ if (existsSync(preferencesPath)) {
51
+ try {
52
+ const prefsContent = readFileSync(preferencesPath, 'utf8');
53
+ preferences = JSON.parse(prefsContent);
54
+ } catch (error) {
55
+ // Create new preferences
56
+ }
57
+ }
58
+
59
+ preferences.credentials_enable_service = true;
60
+ preferences.credentials_enable_autosignin = true;
61
+ preferences.autofill = preferences.autofill || {};
62
+ preferences.autofill.profile_enabled = true;
63
+ preferences.profile = preferences.profile || {};
64
+ preferences.profile.password_manager_enabled = true;
65
+ if (!preferences.profile.info_cache) {
66
+ preferences.profile.info_cache = {};
67
+ }
68
+ preferences.password_manager = preferences.password_manager || {};
69
+ preferences.password_manager.enabled = true;
70
+
71
+ try {
72
+ writeFileSync(preferencesPath, JSON.stringify(preferences), 'utf8');
73
+ } catch (error) {
74
+ // Non-fatal
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Launch Chromium or Chrome.
80
+ * @param {Object} [options] - Launch options
81
+ * @param {string} [options.executablePath] - Path to Chrome/Chromium executable; omit to use Puppeteer's bundled Chromium
82
+ * @param {string} [options.userDataDir] - Override profile directory; omit to use default snow-connector profile
83
+ * @param {string} [options.initialUrl] - URL to open on launch (optional)
84
+ * @returns {Promise<import('puppeteer').Browser>} The launched browser instance
85
+ */
86
+ async function launch(options = {}) {
87
+ const userDataDir = options.userDataDir || getBrowserProfileDir();
88
+ configurePasswordSaving(userDataDir);
89
+
90
+ const launchOptions = {
91
+ headless: options.headless !== undefined ? options.headless : false,
92
+ timeout: 60000,
93
+ userDataDir,
94
+ ignoreDefaultArgs: ['--enable-automation'],
95
+ args: [
96
+ '--no-sandbox',
97
+ '--disable-setuid-sandbox',
98
+ '--disable-dev-shm-usage',
99
+ '--disable-accelerated-2d-canvas',
100
+ '--disable-gpu',
101
+ '--disable-background-timer-throttling',
102
+ '--disable-backgrounding-occluded-windows',
103
+ '--disable-renderer-backgrounding',
104
+ '--disable-blink-features=AutomationControlled',
105
+ '--exclude-switches=enable-automation',
106
+ '--disable-infobars',
107
+ '--no-first-run',
108
+ '--no-default-browser-check',
109
+ '--password-store=basic',
110
+ ],
111
+ };
112
+
113
+ if (options.executablePath) {
114
+ launchOptions.executablePath = options.executablePath;
115
+ }
116
+
117
+ if (options.initialUrl) {
118
+ launchOptions.args.push(options.initialUrl);
119
+ }
120
+
121
+ return puppeteer.launch(launchOptions);
122
+ }
123
+
124
+ module.exports = {
125
+ launch,
126
+ getBrowserProfileDir,
127
+ configurePasswordSaving,
128
+ };
package/browserSync.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Syncs g_ck values from Puppeteer into the model on each page load (all tabs, all domains).
3
+ * - g_ck: domain -> detected g_ck value → browser_g_cks (only set or update; never remove or nullify).
4
+ *
5
+ * No interval; updates are driven by the browser 'load' event.
6
+ */
7
+
8
+ const { getBrowserProvider, getModelProvider } = require('./providers.js');
9
+ const { extractFqdn, getGckFromPage, syncGckForPage } = require('./gckSync.js');
10
+
11
+ /**
12
+ * On a single page load: if g_ck is present, update browser_g_cks for this page's domain.
13
+ * @param {import('puppeteer').Page} page
14
+ * @param {Object} model
15
+ * @param {{ stopped: boolean }} state
16
+ */
17
+ async function handlePageLoad(page, model, state) {
18
+ if (state.stopped) return;
19
+ await syncGckForPage(page, model);
20
+ }
21
+
22
+ /**
23
+ * Start syncing browser state to the model on each page load. No interval; updates
24
+ * when any tab fires the 'load' event. Only browser_g_cks is updated.
25
+ * New tabs get the same listener when created.
26
+ * @returns {{ stop: function }} - call stop() to remove listeners and stop updates
27
+ */
28
+ function startBrowserSync() {
29
+ const browser = getBrowserProvider().getBrowser();
30
+ const model = getModelProvider().getModel();
31
+ if (!browser || !model) return { stop: () => {} };
32
+
33
+ const state = { stopped: false };
34
+
35
+ function onLoad(page) {
36
+ if (state.stopped) return;
37
+ handlePageLoad(page, model, state).catch(() => {});
38
+ }
39
+
40
+ const targetCreatedHandler = async (target) => {
41
+ if (state.stopped) return;
42
+ try {
43
+ const page = await target.page();
44
+ if (page) {
45
+ page.on('load', () => onLoad(page));
46
+ await handlePageLoad(page, model, state);
47
+ }
48
+ } catch (e) {
49
+ // ignore
50
+ }
51
+ };
52
+
53
+ browser.on('targetcreated', targetCreatedHandler);
54
+
55
+ (async () => {
56
+ const pages = await browser.pages();
57
+ for (const page of pages) {
58
+ if (state.stopped) return;
59
+ page.on('load', () => onLoad(page));
60
+ await handlePageLoad(page, model, state);
61
+ }
62
+ })();
63
+
64
+ return {
65
+ stop() {
66
+ state.stopped = true;
67
+ browser.off('targetcreated', targetCreatedHandler);
68
+ },
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ startBrowserSync,
74
+ extractFqdn,
75
+ getGckFromPage,
76
+ };