dexie-cloud-addon 4.3.5 → 4.3.7
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/TODO-SOCIALAUTH.md +1 -1
- package/dist/modern/DexieCloudAPI.d.ts +2 -0
- package/dist/modern/authentication/handleOAuthCallback.d.ts +0 -4
- package/dist/modern/default-ui/OptionButton.d.ts +0 -4
- package/dist/modern/dexie-cloud-addon.js +68 -122
- package/dist/modern/dexie-cloud-addon.js.map +1 -1
- package/dist/modern/dexie-cloud-addon.min.js +1 -1
- package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
- package/dist/modern/service-worker.js +68 -122
- package/dist/modern/service-worker.js.map +1 -1
- package/dist/modern/service-worker.min.js +1 -1
- package/dist/modern/service-worker.min.js.map +1 -1
- package/dist/modern/types/DXCUserInteraction.d.ts +1 -3
- package/dist/umd/dexie-cloud-addon.js +69 -123
- package/dist/umd/dexie-cloud-addon.js.map +1 -1
- package/dist/umd/dexie-cloud-addon.min.js +1 -1
- package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
- package/dist/umd/service-worker.js +69 -123
- package/dist/umd/service-worker.js.map +1 -1
- package/dist/umd/service-worker.min.js +1 -1
- package/dist/umd/service-worker.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ==========================================================================
|
|
10
10
|
*
|
|
11
|
-
* Version 4.3.
|
|
11
|
+
* Version 4.3.7, Wed Jan 28 2026
|
|
12
12
|
*
|
|
13
13
|
* https://dexie.org
|
|
14
14
|
*
|
|
@@ -2445,73 +2445,19 @@
|
|
|
2445
2445
|
}
|
|
2446
2446
|
}
|
|
2447
2447
|
|
|
2448
|
-
/**
|
|
2449
|
-
const
|
|
2450
|
-
/** Default SVG icons for built-in OAuth providers */
|
|
2451
|
-
const ProviderIcons = {
|
|
2452
|
-
google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
|
|
2453
|
-
github: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>`,
|
|
2454
|
-
microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`,
|
|
2455
|
-
apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
|
|
2456
|
-
};
|
|
2457
|
-
/** Email/envelope icon for OTP option */
|
|
2458
|
-
const EmailIcon = `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>`;
|
|
2459
|
-
/**
|
|
2460
|
-
* Fetches SVG content from a URL and caches it.
|
|
2461
|
-
* Returns the SVG string or null if fetch fails.
|
|
2462
|
-
*/
|
|
2463
|
-
function fetchSvgIcon(url) {
|
|
2464
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
2465
|
-
if (svgCache[url]) {
|
|
2466
|
-
return svgCache[url];
|
|
2467
|
-
}
|
|
2468
|
-
try {
|
|
2469
|
-
const res = yield fetch(url);
|
|
2470
|
-
if (res.ok) {
|
|
2471
|
-
const svg = yield res.text();
|
|
2472
|
-
// Validate it looks like SVG
|
|
2473
|
-
if (svg.includes('<svg')) {
|
|
2474
|
-
svgCache[url] = svg;
|
|
2475
|
-
return svg;
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
catch (_a) {
|
|
2480
|
-
// Silently fail - will show no icon
|
|
2481
|
-
}
|
|
2482
|
-
return null;
|
|
2483
|
-
});
|
|
2484
|
-
}
|
|
2448
|
+
/** Email/envelope icon data URL for OTP option */
|
|
2449
|
+
const EmailIcon = `data:image/svg+xml;base64,${btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#666666" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 6L12 13 2 6"/></svg>')}`;
|
|
2485
2450
|
/**
|
|
2486
2451
|
* Converts an OAuthProviderInfo to a generic DXCOption.
|
|
2487
|
-
* Fetches SVG icons from URLs if needed.
|
|
2488
2452
|
*/
|
|
2489
2453
|
function providerToOption(provider) {
|
|
2490
|
-
return
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
// If provider has iconUrl pointing to SVG, fetch and inline it
|
|
2498
|
-
else if ((_a = provider.iconUrl) === null || _a === void 0 ? void 0 : _a.toLowerCase().endsWith('.svg')) {
|
|
2499
|
-
const fetched = yield fetchSvgIcon(provider.iconUrl);
|
|
2500
|
-
if (fetched) {
|
|
2501
|
-
iconSvg = fetched;
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
return {
|
|
2505
|
-
name: 'provider',
|
|
2506
|
-
value: provider.name,
|
|
2507
|
-
displayName: `Continue with ${provider.displayName}`,
|
|
2508
|
-
iconSvg,
|
|
2509
|
-
// If iconUrl is not SVG, pass it through for img tag rendering
|
|
2510
|
-
iconUrl: (!iconSvg && provider.iconUrl) ? provider.iconUrl : undefined,
|
|
2511
|
-
// Use provider type as style hint for branding
|
|
2512
|
-
styleHint: provider.type,
|
|
2513
|
-
};
|
|
2514
|
-
});
|
|
2454
|
+
return {
|
|
2455
|
+
name: 'provider',
|
|
2456
|
+
value: provider.name,
|
|
2457
|
+
displayName: `Continue with ${provider.displayName}`,
|
|
2458
|
+
iconUrl: provider.iconUrl,
|
|
2459
|
+
styleHint: provider.type,
|
|
2460
|
+
};
|
|
2515
2461
|
}
|
|
2516
2462
|
function interactWithUser(userInteraction, req) {
|
|
2517
2463
|
return new Promise((resolve, reject) => {
|
|
@@ -2662,8 +2608,8 @@
|
|
|
2662
2608
|
*/
|
|
2663
2609
|
function promptForProvider(userInteraction_1, providers_1, otpEnabled_1) {
|
|
2664
2610
|
return __awaiter(this, arguments, void 0, function* (userInteraction, providers, otpEnabled, title = 'Choose login method', alerts = []) {
|
|
2665
|
-
// Convert providers to generic options
|
|
2666
|
-
const providerOptions =
|
|
2611
|
+
// Convert providers to generic options
|
|
2612
|
+
const providerOptions = providers.map(providerToOption);
|
|
2667
2613
|
// Build the options array
|
|
2668
2614
|
const options = [...providerOptions];
|
|
2669
2615
|
// Add OTP option if enabled
|
|
@@ -2672,7 +2618,7 @@
|
|
|
2672
2618
|
name: 'otp',
|
|
2673
2619
|
value: 'email',
|
|
2674
2620
|
displayName: 'Continue with email',
|
|
2675
|
-
|
|
2621
|
+
iconUrl: EmailIcon,
|
|
2676
2622
|
styleHint: 'otp',
|
|
2677
2623
|
});
|
|
2678
2624
|
}
|
|
@@ -13261,7 +13207,7 @@
|
|
|
13261
13207
|
*
|
|
13262
13208
|
* ==========================================================================
|
|
13263
13209
|
*
|
|
13264
|
-
* Version 4.2.2,
|
|
13210
|
+
* Version 4.2.2, Wed Jan 28 2026
|
|
13265
13211
|
*
|
|
13266
13212
|
* https://dexie.org
|
|
13267
13213
|
*
|
|
@@ -14708,10 +14654,12 @@
|
|
|
14708
14654
|
mode: 'cors',
|
|
14709
14655
|
});
|
|
14710
14656
|
if (!res.ok) {
|
|
14657
|
+
// Read body once as text to avoid stream consumption issues
|
|
14658
|
+
const bodyText = yield res.text().catch(() => res.statusText);
|
|
14711
14659
|
if (res.status === 400 || res.status === 401) {
|
|
14712
|
-
// Try to parse error response
|
|
14660
|
+
// Try to parse error response as JSON
|
|
14713
14661
|
try {
|
|
14714
|
-
const errorResponse =
|
|
14662
|
+
const errorResponse = JSON.parse(bodyText);
|
|
14715
14663
|
if (errorResponse.type === 'error') {
|
|
14716
14664
|
// Check for specific error codes
|
|
14717
14665
|
if (errorResponse.messageCode === 'INVALID_OTP') {
|
|
@@ -14728,8 +14676,7 @@
|
|
|
14728
14676
|
// Fall through to generic error
|
|
14729
14677
|
}
|
|
14730
14678
|
}
|
|
14731
|
-
|
|
14732
|
-
throw new OAuthError('provider_error', undefined, `Token exchange failed: ${res.status} ${errorText}`);
|
|
14679
|
+
throw new OAuthError('provider_error', undefined, `Token exchange failed: ${res.status} ${bodyText}`);
|
|
14733
14680
|
}
|
|
14734
14681
|
const response = yield res.json();
|
|
14735
14682
|
if (response.type === 'error') {
|
|
@@ -14835,9 +14782,8 @@
|
|
|
14835
14782
|
* ```
|
|
14836
14783
|
*/
|
|
14837
14784
|
function startOAuthRedirect(options) {
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
sessionStorage.setItem('dexie-cloud-oauth-provider', options.provider);
|
|
14785
|
+
if (typeof window === 'undefined') {
|
|
14786
|
+
throw new Error('OAuth redirect requires a browser environment');
|
|
14841
14787
|
}
|
|
14842
14788
|
const loginUrl = buildOAuthLoginUrl(options);
|
|
14843
14789
|
window.location.href = loginUrl;
|
|
@@ -14863,7 +14809,21 @@
|
|
|
14863
14809
|
}
|
|
14864
14810
|
// Handle OAuth provider login via redirect
|
|
14865
14811
|
if (hints === null || hints === void 0 ? void 0 : hints.provider) {
|
|
14866
|
-
|
|
14812
|
+
let resolvedRedirectUri = undefined;
|
|
14813
|
+
if (hints.redirectPath) {
|
|
14814
|
+
// If redirectPath is absolute, use as is. If relative, resolve against current location
|
|
14815
|
+
if (/^https?:\/\//i.test(hints.redirectPath)) {
|
|
14816
|
+
resolvedRedirectUri = hints.redirectPath;
|
|
14817
|
+
}
|
|
14818
|
+
else if (typeof window !== 'undefined' && window.location) {
|
|
14819
|
+
// Use URL constructor to resolve relative path
|
|
14820
|
+
resolvedRedirectUri = new URL(hints.redirectPath, window.location.href).toString();
|
|
14821
|
+
}
|
|
14822
|
+
else if (typeof location !== 'undefined' && location.href) {
|
|
14823
|
+
resolvedRedirectUri = new URL(hints.redirectPath, location.href).toString();
|
|
14824
|
+
}
|
|
14825
|
+
}
|
|
14826
|
+
initiateOAuthRedirect(db, hints.provider, resolvedRedirectUri);
|
|
14867
14827
|
// This function never returns - page navigates away
|
|
14868
14828
|
throw new OAuthRedirectError(hints.provider);
|
|
14869
14829
|
}
|
|
@@ -14943,7 +14903,8 @@
|
|
|
14943
14903
|
const res1 = yield fetch(`${url}/token`, {
|
|
14944
14904
|
body: JSON.stringify(tokenRequest),
|
|
14945
14905
|
method: 'post',
|
|
14946
|
-
headers: { 'Content-Type': 'application/json'
|
|
14906
|
+
headers: { 'Content-Type': 'application/json' },
|
|
14907
|
+
mode: 'cors',
|
|
14947
14908
|
});
|
|
14948
14909
|
if (res1.status !== 200) {
|
|
14949
14910
|
const errMsg = yield res1.text();
|
|
@@ -15007,13 +14968,18 @@
|
|
|
15007
14968
|
* the user is redirected back with a dxc-auth query parameter that is
|
|
15008
14969
|
* automatically detected by db.cloud.configure().
|
|
15009
14970
|
*/
|
|
15010
|
-
function initiateOAuthRedirect(db, provider) {
|
|
14971
|
+
function initiateOAuthRedirect(db, provider, redirectUriOverride) {
|
|
15011
14972
|
var _a, _b;
|
|
15012
14973
|
const url = (_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl;
|
|
15013
14974
|
if (!url)
|
|
15014
14975
|
throw new Error(`No database URL given.`);
|
|
15015
|
-
const redirectUri =
|
|
15016
|
-
(
|
|
14976
|
+
const redirectUri = redirectUriOverride ||
|
|
14977
|
+
((_b = db.cloud.options) === null || _b === void 0 ? void 0 : _b.oauthRedirectUri) ||
|
|
14978
|
+
(typeof location !== 'undefined' ? location.href : undefined);
|
|
14979
|
+
// CodeRabbit suggested to fail fast here, but the only situation where
|
|
14980
|
+
// redirectUri would be undefined is in non-browser environments, and
|
|
14981
|
+
// in those environments OAuth redirect does not make sense anyway
|
|
14982
|
+
// and will fail fast in startOAuthRedirect().
|
|
15017
14983
|
// Start OAuth redirect flow - page navigates away
|
|
15018
14984
|
startOAuthRedirect({
|
|
15019
14985
|
databaseUrl: url,
|
|
@@ -17066,13 +17032,13 @@
|
|
|
17066
17032
|
color: "#3c4043"
|
|
17067
17033
|
},
|
|
17068
17034
|
ProviderGitHub: {
|
|
17069
|
-
backgroundColor: "#
|
|
17070
|
-
border: "1px solid #
|
|
17071
|
-
color: "#
|
|
17035
|
+
backgroundColor: "#ffffff",
|
|
17036
|
+
border: "1px solid #dadce0",
|
|
17037
|
+
color: "#181717"
|
|
17072
17038
|
},
|
|
17073
17039
|
ProviderMicrosoft: {
|
|
17074
17040
|
backgroundColor: "#ffffff",
|
|
17075
|
-
border: "1px solid #
|
|
17041
|
+
border: "1px solid #dadce0",
|
|
17076
17042
|
color: "#5e5e5e"
|
|
17077
17043
|
},
|
|
17078
17044
|
ProviderApple: {
|
|
@@ -17081,9 +17047,9 @@
|
|
|
17081
17047
|
color: "#ffffff"
|
|
17082
17048
|
},
|
|
17083
17049
|
ProviderCustom: {
|
|
17084
|
-
backgroundColor: "#
|
|
17085
|
-
border: "1px solid #
|
|
17086
|
-
color: "#
|
|
17050
|
+
backgroundColor: "#ffffff",
|
|
17051
|
+
border: "1px solid #dadce0",
|
|
17052
|
+
color: "#181717"
|
|
17087
17053
|
},
|
|
17088
17054
|
// Divider styles
|
|
17089
17055
|
Divider: {
|
|
@@ -17174,39 +17140,13 @@
|
|
|
17174
17140
|
* Generic button component for selectable options.
|
|
17175
17141
|
* Displays the option's icon and display name.
|
|
17176
17142
|
*
|
|
17177
|
-
* The icon can be:
|
|
17178
|
-
* - Inline SVG (iconSvg) - rendered directly with dangerouslySetInnerHTML
|
|
17179
|
-
* - Image URL (iconUrl) - rendered as an img tag
|
|
17180
|
-
*
|
|
17181
17143
|
* Style is determined by the styleHint property for branding purposes.
|
|
17182
17144
|
*/
|
|
17183
17145
|
function OptionButton({ option, onClick }) {
|
|
17184
|
-
const { displayName, iconUrl,
|
|
17146
|
+
const { displayName, iconUrl, styleHint } = option;
|
|
17185
17147
|
const style = getOptionStyle(styleHint);
|
|
17186
|
-
// Get the text color from the button style for SVG fill processing
|
|
17187
|
-
const textColor = style.color || '#000000';
|
|
17188
|
-
// Process SVG to replace currentColor with actual text color
|
|
17189
|
-
const processedSvg = iconSvg
|
|
17190
|
-
? iconSvg
|
|
17191
|
-
.replace(/fill="currentColor"/gi, `fill="${textColor}"`)
|
|
17192
|
-
.replace(/fill='currentColor'/gi, `fill='${textColor}'`)
|
|
17193
|
-
.replace(/stroke="currentColor"/gi, `stroke="${textColor}"`)
|
|
17194
|
-
.replace(/stroke='currentColor'/gi, `stroke='${textColor}'`)
|
|
17195
|
-
: null;
|
|
17196
|
-
// Render the appropriate icon
|
|
17197
|
-
const renderIcon = () => {
|
|
17198
|
-
// Inline SVG
|
|
17199
|
-
if (processedSvg) {
|
|
17200
|
-
return (_$1("span", { style: Styles.ProviderButtonIcon, "aria-hidden": "true", dangerouslySetInnerHTML: { __html: processedSvg } }));
|
|
17201
|
-
}
|
|
17202
|
-
// Image URL
|
|
17203
|
-
if (iconUrl) {
|
|
17204
|
-
return (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" }));
|
|
17205
|
-
}
|
|
17206
|
-
return null;
|
|
17207
|
-
};
|
|
17208
17148
|
return (_$1("button", { type: "button", style: style, onClick: onClick, class: `dxc-option-btn${styleHint ? ` dxc-option-${styleHint}` : ''}`, "aria-label": displayName },
|
|
17209
|
-
|
|
17149
|
+
iconUrl && (_$1("img", { src: iconUrl, alt: "", style: Styles.ProviderButtonIcon, "aria-hidden": "true" })),
|
|
17210
17150
|
_$1("span", { style: Styles.ProviderButtonText }, displayName)));
|
|
17211
17151
|
}
|
|
17212
17152
|
/**
|
|
@@ -17993,7 +17933,7 @@
|
|
|
17993
17933
|
const syncComplete = new rxjs.Subject();
|
|
17994
17934
|
dexie.cloud = {
|
|
17995
17935
|
// @ts-ignore
|
|
17996
|
-
version: "4.3.
|
|
17936
|
+
version: "4.3.7",
|
|
17997
17937
|
options: Object.assign({}, DEFAULT_OPTIONS),
|
|
17998
17938
|
schema: null,
|
|
17999
17939
|
get currentUserId() {
|
|
@@ -18263,12 +18203,18 @@
|
|
|
18263
18203
|
pendingOAuthError = null; // Clear pending error
|
|
18264
18204
|
console.debug('[dexie-cloud] Showing OAuth error:', error.message);
|
|
18265
18205
|
// Show alert to user about the OAuth error
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18206
|
+
// Guard so UI errors don't abort initialization
|
|
18207
|
+
try {
|
|
18208
|
+
yield alertUser(db.cloud.userInteraction, 'Authentication Error', {
|
|
18209
|
+
type: 'error',
|
|
18210
|
+
messageCode: 'GENERIC_ERROR',
|
|
18211
|
+
message: error.message,
|
|
18212
|
+
messageParams: { provider: error.provider || 'unknown' }
|
|
18213
|
+
});
|
|
18214
|
+
}
|
|
18215
|
+
catch (uiError) {
|
|
18216
|
+
console.error('[dexie-cloud] Failed to show OAuth error alert:', uiError);
|
|
18217
|
+
}
|
|
18272
18218
|
}
|
|
18273
18219
|
// Process pending OAuth callback if present (from dxc-auth redirect)
|
|
18274
18220
|
if (pendingOAuthCode && !db.cloud.isServiceWorkerDB) {
|
|
@@ -18372,7 +18318,7 @@
|
|
|
18372
18318
|
}
|
|
18373
18319
|
}
|
|
18374
18320
|
// @ts-ignore
|
|
18375
|
-
dexieCloud.version = "4.3.
|
|
18321
|
+
dexieCloud.version = "4.3.7";
|
|
18376
18322
|
Dexie.Cloud = dexieCloud;
|
|
18377
18323
|
|
|
18378
18324
|
// In case the SW lives for a while, let it reuse already opened connections:
|