@auxilium/datalynk-client 1.5.1 → 1.6.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/bin/datalynk-models.mjs +0 -0
- package/dist/api.d.ts +65 -6
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/chat-entry.d.ts +3 -0
- package/dist/chat-entry.d.ts.map +1 -0
- package/dist/chat-gui.d.ts +64 -0
- package/dist/chat-gui.d.ts.map +1 -0
- package/dist/chat-gui.view.d.ts +3 -0
- package/dist/chat-gui.view.d.ts.map +1 -0
- package/dist/chat.cjs +2202 -0
- package/dist/chat.d.ts +75 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.mjs +2198 -0
- package/dist/index.cjs +514 -50
- package/dist/index.mjs +514 -50
- package/dist/service.worker.mjs +11 -6
- package/dist/socket.d.ts +14 -1
- package/dist/socket.d.ts.map +1 -1
- package/package.json +7 -2
package/dist/index.mjs
CHANGED
|
@@ -1549,7 +1549,10 @@ class Auth {
|
|
|
1549
1549
|
}
|
|
1550
1550
|
});
|
|
1551
1551
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length)
|
|
1552
|
-
this.user$.pipe(filter((u) => u !== void 0)).subscribe((u) =>
|
|
1552
|
+
this.user$.pipe(filter((u) => u !== void 0)).subscribe((u) => {
|
|
1553
|
+
if (u == null) localStorage.removeItem("datalynk-user");
|
|
1554
|
+
else localStorage.setItem("datalynk-user", JSON.stringify(u));
|
|
1555
|
+
});
|
|
1553
1556
|
}
|
|
1554
1557
|
/** Current user */
|
|
1555
1558
|
get user() {
|
|
@@ -1585,7 +1588,7 @@ class Auth {
|
|
|
1585
1588
|
* @return {Promise<void>} Login complete
|
|
1586
1589
|
*/
|
|
1587
1590
|
async handleLogin(spoke, options) {
|
|
1588
|
-
var _a, _b;
|
|
1591
|
+
var _a, _b, _c;
|
|
1589
1592
|
if (this.onlinePrompt) window.removeEventListener("online", this.onlinePrompt);
|
|
1590
1593
|
if ((_a = this.api.options.offline) == null ? void 0 : _a.length) window.removeEventListener("online", this.onlinePrompt = () => this.handleLogin(spoke, options));
|
|
1591
1594
|
const urlParams = new URLSearchParams(location.search);
|
|
@@ -1594,8 +1597,10 @@ class Auth {
|
|
|
1594
1597
|
this.api.token = urlToken;
|
|
1595
1598
|
urlParams.delete("datalynkToken");
|
|
1596
1599
|
location.href = location.pathname + (urlParams.toString() ? "?" + urlParams.toString() : "") + location.hash;
|
|
1600
|
+
} else if (this.api.reauthenticationPending && !this.api.online && ((_b = this.api.jwtPayload) == null ? void 0 : _b.realm) == spoke) {
|
|
1601
|
+
return;
|
|
1597
1602
|
} else if (this.api.token && !this.api.expired) {
|
|
1598
|
-
if (((
|
|
1603
|
+
if (((_c = this.api.jwtPayload) == null ? void 0 : _c.realm) != spoke) {
|
|
1599
1604
|
this.api.token = null;
|
|
1600
1605
|
location.reload();
|
|
1601
1606
|
}
|
|
@@ -3097,6 +3102,7 @@ class Socket {
|
|
|
3097
3102
|
// [ Callback, Re-subscribe ]
|
|
3098
3103
|
__publicField(this, "retry");
|
|
3099
3104
|
__publicField(this, "socket");
|
|
3105
|
+
__publicField(this, "connectGeneration", 0);
|
|
3100
3106
|
__publicField(this, "open", false);
|
|
3101
3107
|
this.api = api;
|
|
3102
3108
|
this.options = options;
|
|
@@ -3105,7 +3111,7 @@ class Socket {
|
|
|
3105
3111
|
this.options.url = origin.replace("http", "ws").replace(/:\d+/g, "") + `/s/`;
|
|
3106
3112
|
}
|
|
3107
3113
|
if (this.options.url !== false)
|
|
3108
|
-
api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => this.connect());
|
|
3114
|
+
api.token$.pipe(filter((u) => u !== void 0), distinctUntilChanged()).subscribe(() => void this.connect());
|
|
3109
3115
|
}
|
|
3110
3116
|
/**
|
|
3111
3117
|
* Add listener for all socket events
|
|
@@ -3127,6 +3133,7 @@ class Socket {
|
|
|
3127
3133
|
*/
|
|
3128
3134
|
close() {
|
|
3129
3135
|
var _a;
|
|
3136
|
+
if (this.options.authentication === "ticket") this.connectGeneration++;
|
|
3130
3137
|
if (this.open) console.debug("Datalynk socket: disconnected");
|
|
3131
3138
|
this.open = false;
|
|
3132
3139
|
(_a = this.socket) == null ? void 0 : _a.close();
|
|
@@ -3139,6 +3146,7 @@ class Socket {
|
|
|
3139
3146
|
* @param {number} timeout Retry to connect every x seconds
|
|
3140
3147
|
*/
|
|
3141
3148
|
connect(timeout = 30) {
|
|
3149
|
+
if (this.options.authentication === "ticket") return this.connectWithTicket(timeout);
|
|
3142
3150
|
if (this.options.url === false) return console.warn("Datalynk socket disabled");
|
|
3143
3151
|
if (this.open) this.close();
|
|
3144
3152
|
this.retry = setTimeout(() => {
|
|
@@ -3168,6 +3176,80 @@ class Socket {
|
|
|
3168
3176
|
};
|
|
3169
3177
|
}
|
|
3170
3178
|
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Ticket-authenticated connection path, used only once a chat-capable host switches
|
|
3181
|
+
* this instance to `authentication: 'ticket'` (see Api.openChat). Entirely separate
|
|
3182
|
+
* from the token path above so existing non-chat consumers are never affected.
|
|
3183
|
+
*/
|
|
3184
|
+
async connectWithTicket(timeout = 30) {
|
|
3185
|
+
var _a;
|
|
3186
|
+
if (this.options.url === false) return console.warn("Datalynk socket disabled");
|
|
3187
|
+
const generation = ++this.connectGeneration;
|
|
3188
|
+
if (this.open || this.socket) {
|
|
3189
|
+
this.open = false;
|
|
3190
|
+
(_a = this.socket) == null ? void 0 : _a.close();
|
|
3191
|
+
this.socket = void 0;
|
|
3192
|
+
}
|
|
3193
|
+
if (this.retry) clearTimeout(this.retry);
|
|
3194
|
+
this.retry = setTimeout(() => {
|
|
3195
|
+
if (this.open || generation !== this.connectGeneration) return;
|
|
3196
|
+
void this.connect(timeout);
|
|
3197
|
+
}, timeout * 1e3);
|
|
3198
|
+
if (!this.api.online || !this.api.token) return;
|
|
3199
|
+
let socketUrl = this.normalizeUrl(String(this.options.url || ""));
|
|
3200
|
+
try {
|
|
3201
|
+
const response = await this.api.request({ "$/chat/socketTicket": {} }, { noOptimize: true });
|
|
3202
|
+
if (generation !== this.connectGeneration) return;
|
|
3203
|
+
if (response == null ? void 0 : response.ticket) socketUrl += `${socketUrl.includes("?") ? "&" : "?"}ticket=${encodeURIComponent(response.ticket)}`;
|
|
3204
|
+
else throw new Error("Missing socket ticket");
|
|
3205
|
+
} catch (_) {
|
|
3206
|
+
if (generation !== this.connectGeneration) return;
|
|
3207
|
+
this.scheduleReconnect(generation, timeout);
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
if (generation !== this.connectGeneration) return;
|
|
3211
|
+
this.socket = new WebSocket(socketUrl);
|
|
3212
|
+
this.socket.onopen = () => void 0;
|
|
3213
|
+
this.socket.onclose = () => {
|
|
3214
|
+
this.open = false;
|
|
3215
|
+
this.socket = void 0;
|
|
3216
|
+
this.scheduleReconnect(generation, timeout);
|
|
3217
|
+
};
|
|
3218
|
+
this.socket.onerror = () => {
|
|
3219
|
+
};
|
|
3220
|
+
this.socket.onmessage = (message) => {
|
|
3221
|
+
var _a2;
|
|
3222
|
+
let payload;
|
|
3223
|
+
try {
|
|
3224
|
+
payload = JSON.parse(message.data);
|
|
3225
|
+
} catch {
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
if (payload.connected != void 0) {
|
|
3229
|
+
if (payload.connected) {
|
|
3230
|
+
this.open = true;
|
|
3231
|
+
if (this.retry) clearTimeout(this.retry);
|
|
3232
|
+
this.retry = null;
|
|
3233
|
+
console.debug("Datalynk socket: connected");
|
|
3234
|
+
this.listeners.forEach((l) => l[1]());
|
|
3235
|
+
} else {
|
|
3236
|
+
this.open = false;
|
|
3237
|
+
console.warn(`Datalynk socket failed: ${payload.error || "authentication failed"}`);
|
|
3238
|
+
(_a2 = this.socket) == null ? void 0 : _a2.close();
|
|
3239
|
+
}
|
|
3240
|
+
} else {
|
|
3241
|
+
this.listeners.forEach((l) => l[0](payload));
|
|
3242
|
+
}
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
scheduleReconnect(generation, timeout) {
|
|
3246
|
+
if (generation !== this.connectGeneration || this.options.url === false) return;
|
|
3247
|
+
if (this.retry) clearTimeout(this.retry);
|
|
3248
|
+
this.retry = setTimeout(() => {
|
|
3249
|
+
if (generation !== this.connectGeneration) return;
|
|
3250
|
+
void this.connect(timeout);
|
|
3251
|
+
}, timeout * 1e3);
|
|
3252
|
+
}
|
|
3171
3253
|
/**
|
|
3172
3254
|
* Send data to socket server
|
|
3173
3255
|
*
|
|
@@ -3194,6 +3276,17 @@ class Socket {
|
|
|
3194
3276
|
unsubscribe();
|
|
3195
3277
|
};
|
|
3196
3278
|
}
|
|
3279
|
+
normalizeUrl(value) {
|
|
3280
|
+
let url = value.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://");
|
|
3281
|
+
try {
|
|
3282
|
+
const parsed = new URL(url);
|
|
3283
|
+
if (parsed.pathname === "/" || parsed.pathname === "") parsed.pathname = "/s/";
|
|
3284
|
+
else if (parsed.pathname === "/s") parsed.pathname = "/s/";
|
|
3285
|
+
url = parsed.toString();
|
|
3286
|
+
} catch {
|
|
3287
|
+
}
|
|
3288
|
+
return url;
|
|
3289
|
+
}
|
|
3197
3290
|
}
|
|
3198
3291
|
class Superuser {
|
|
3199
3292
|
constructor(api) {
|
|
@@ -3225,7 +3318,7 @@ class Superuser {
|
|
|
3225
3318
|
} });
|
|
3226
3319
|
}
|
|
3227
3320
|
}
|
|
3228
|
-
const version = "1.
|
|
3321
|
+
const version = "1.6.0";
|
|
3229
3322
|
class WebRtc {
|
|
3230
3323
|
constructor(api) {
|
|
3231
3324
|
__publicField(this, "ice");
|
|
@@ -3772,7 +3865,14 @@ const _Api = class _Api {
|
|
|
3772
3865
|
timeout: 6e4
|
|
3773
3866
|
});
|
|
3774
3867
|
__publicField(this, "authenticationInvalid", false);
|
|
3868
|
+
/** Token reached exp while the client could not reach Datalynk; keep the offline session alive until reconnect. */
|
|
3869
|
+
__publicField(this, "offlineAuthenticationExpired", false);
|
|
3775
3870
|
__publicField(this, "tokenExpiryTimeout", null);
|
|
3871
|
+
__publicField(this, "unauthorizedLogout", null);
|
|
3872
|
+
__publicField(this, "replayingPending", false);
|
|
3873
|
+
/** If replay is requested while a replay pass is already active, run another pass when it finishes. */
|
|
3874
|
+
__publicField(this, "pendingReplayRequested", false);
|
|
3875
|
+
__publicField(this, "offlineSessionOwner", null);
|
|
3776
3876
|
/** Request-specific recovery after a server response proves an API call is broken. */
|
|
3777
3877
|
__publicField(this, "recovery", null);
|
|
3778
3878
|
/** Retry the failed request twice immediately, then this long after each failed recovery response. */
|
|
@@ -3780,9 +3880,15 @@ const _Api = class _Api {
|
|
|
3780
3880
|
__publicField(this, "recoveryImmediateRetries", 2);
|
|
3781
3881
|
/** LocalStorage key for persisting logins */
|
|
3782
3882
|
__publicField(this, "localStorageKey", "datalynk-token");
|
|
3883
|
+
/** Persisted owner of an offline queue so another login cannot inherit queued writes. */
|
|
3884
|
+
__publicField(this, "offlineSessionOwnerKey", "datalynk-offline-owner");
|
|
3783
3885
|
__publicField(this, "tokenStorageListener", null);
|
|
3784
3886
|
/** Pending requests cache */
|
|
3785
3887
|
__publicField(this, "pending", {});
|
|
3888
|
+
__publicField(this, "chatModulePromise", null);
|
|
3889
|
+
__publicField(this, "chatInstance", null);
|
|
3890
|
+
__publicField(this, "chatLauncherHost", null);
|
|
3891
|
+
__publicField(this, "chatLauncherButton", null);
|
|
3786
3892
|
/** Helpers */
|
|
3787
3893
|
/** Authentication */
|
|
3788
3894
|
__publicField(this, "auth");
|
|
@@ -3837,6 +3943,9 @@ const _Api = class _Api {
|
|
|
3837
3943
|
offline: [],
|
|
3838
3944
|
origin: typeof location !== "undefined" ? location.host : "Unknown",
|
|
3839
3945
|
saveSession: true,
|
|
3946
|
+
manageSession: true,
|
|
3947
|
+
chat: false,
|
|
3948
|
+
replayOfflineQueue: true,
|
|
3840
3949
|
watchTokenExpiry: true,
|
|
3841
3950
|
serviceWorker: "/service.worker.mjs",
|
|
3842
3951
|
...options,
|
|
@@ -3867,9 +3976,9 @@ const _Api = class _Api {
|
|
|
3867
3976
|
window.addEventListener("storage", this.tokenStorageListener);
|
|
3868
3977
|
}
|
|
3869
3978
|
}
|
|
3870
|
-
if (this.options.watchTokenExpiry)
|
|
3979
|
+
if (this.options.manageSession !== false && this.options.watchTokenExpiry)
|
|
3871
3980
|
this.token$.pipe(distinctUntilChanged()).subscribe((token) => this.scheduleTokenExpiry(token));
|
|
3872
|
-
this.socket = new Socket(this, { url: options.socket });
|
|
3981
|
+
this.socket = new Socket(this, { url: options.socket, authentication: options.socketAuthentication, realm: options.socketRealm });
|
|
3873
3982
|
this.gps = new Gps(this, options.gps);
|
|
3874
3983
|
this.auth = new Auth(this);
|
|
3875
3984
|
this.files = new Files(this);
|
|
@@ -3879,21 +3988,13 @@ const _Api = class _Api {
|
|
|
3879
3988
|
this.webrtc = new WebRtc(this);
|
|
3880
3989
|
if (typeof indexedDB != "undefined") {
|
|
3881
3990
|
this.database = new Database("datalynk", ["pending", ...this.options.offline || []]);
|
|
3882
|
-
this.online$.subscribe(
|
|
3883
|
-
|
|
3884
|
-
if (!online) return;
|
|
3885
|
-
const table = (_a2 = this.database) == null ? void 0 : _a2.table("pending");
|
|
3886
|
-
const keys = await (table == null ? void 0 : table.getAllKeys());
|
|
3887
|
-
await Promise.allSettled(keys.map(async (k) => {
|
|
3888
|
-
const r = await (table == null ? void 0 : table.get(k));
|
|
3889
|
-
await this.request(r).then(() => table == null ? void 0 : table.delete(k));
|
|
3890
|
-
}));
|
|
3991
|
+
if (this.options.replayOfflineQueue !== false) this.online$.subscribe((online) => {
|
|
3992
|
+
if (online) void this.replayPendingQueue();
|
|
3891
3993
|
});
|
|
3892
3994
|
}
|
|
3893
3995
|
if (typeof window !== "undefined") {
|
|
3894
3996
|
window.addEventListener("online", () => this.checkConnection());
|
|
3895
3997
|
window.addEventListener("offline", () => this.setConnectionStatus("offline"));
|
|
3896
|
-
this.online$.subscribe(() => this.offlineBanner());
|
|
3897
3998
|
this.startHeartbeat();
|
|
3898
3999
|
}
|
|
3899
4000
|
if ((_a = this.options.offline) == null ? void 0 : _a.length) {
|
|
@@ -3918,12 +4019,17 @@ const _Api = class _Api {
|
|
|
3918
4019
|
})();
|
|
3919
4020
|
}
|
|
3920
4021
|
}
|
|
4022
|
+
this.setupChatLauncher();
|
|
3921
4023
|
}
|
|
3922
4024
|
/** Is token expired */
|
|
3923
4025
|
get expired() {
|
|
3924
4026
|
var _a;
|
|
3925
4027
|
return (((_a = this.jwtPayload) == null ? void 0 : _a.exp) ?? Infinity) * 1e3 <= Date.now();
|
|
3926
4028
|
}
|
|
4029
|
+
/** Whether authentication expired while offline and must be renewed before replaying queued work. */
|
|
4030
|
+
get reauthenticationPending() {
|
|
4031
|
+
return this.offlineAuthenticationExpired;
|
|
4032
|
+
}
|
|
3927
4033
|
/** Get session info from JWT payload */
|
|
3928
4034
|
get jwtPayload() {
|
|
3929
4035
|
if (!this.token) return null;
|
|
@@ -3982,21 +4088,158 @@ const _Api = class _Api {
|
|
|
3982
4088
|
return this.token$.getValue();
|
|
3983
4089
|
}
|
|
3984
4090
|
set token(token) {
|
|
4091
|
+
var _a;
|
|
3985
4092
|
if (this.recovery && token !== this.recovery.token)
|
|
3986
4093
|
this.cancelRecovery(errorFromCode(401, "Session changed during API recovery"));
|
|
3987
|
-
if (token && this.isTokenExpired(token)) {
|
|
4094
|
+
if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
|
|
3988
4095
|
if (this.adoptStoredToken(token)) return;
|
|
3989
|
-
this.
|
|
3990
|
-
|
|
3991
|
-
|
|
4096
|
+
if (this.shouldDeferLocalExpiry()) {
|
|
4097
|
+
this.deferOfflineExpiry(token);
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
this.markUnauthorized(void 0, this.hasOwnedOfflineSession(token));
|
|
3992
4101
|
return;
|
|
3993
4102
|
}
|
|
3994
4103
|
this.authenticationInvalid = false;
|
|
4104
|
+
this.offlineAuthenticationExpired = false;
|
|
3995
4105
|
this.token$.next(token);
|
|
3996
4106
|
if (token && !this.online && !this.recovery) {
|
|
3997
4107
|
const browserOnline = typeof navigator == "undefined" || typeof navigator.onLine == "undefined" || navigator.onLine;
|
|
3998
4108
|
this.setConnectionStatus(browserOnline ? "online" : "offline");
|
|
3999
4109
|
}
|
|
4110
|
+
if (token && ((_a = this.options) == null ? void 0 : _a.replayOfflineQueue) !== false) void this.replayPendingQueue();
|
|
4111
|
+
}
|
|
4112
|
+
/** Mount the optional launcher without loading or initializing the chat bundle. */
|
|
4113
|
+
setupChatLauncher() {
|
|
4114
|
+
if (!this.options.chat || typeof document === "undefined") return;
|
|
4115
|
+
const mount = () => {
|
|
4116
|
+
if (this.chatLauncherHost || !document.body || document.querySelector("[data-datalynk-chat-launcher]")) return;
|
|
4117
|
+
const host = document.createElement("div");
|
|
4118
|
+
host.setAttribute("data-datalynk-chat-launcher", "");
|
|
4119
|
+
const root = host.attachShadow({ mode: "open" });
|
|
4120
|
+
root.innerHTML = `
|
|
4121
|
+
<style>
|
|
4122
|
+
:host {
|
|
4123
|
+
all: initial;
|
|
4124
|
+
position: fixed;
|
|
4125
|
+
right: 20px;
|
|
4126
|
+
bottom: 20px;
|
|
4127
|
+
z-index: 2147482999;
|
|
4128
|
+
pointer-events: none;
|
|
4129
|
+
}
|
|
4130
|
+
button {
|
|
4131
|
+
display: grid;
|
|
4132
|
+
place-items: center;
|
|
4133
|
+
width: 52px;
|
|
4134
|
+
height: 52px;
|
|
4135
|
+
padding: 0;
|
|
4136
|
+
border: 1px solid #2f596d;
|
|
4137
|
+
border-radius: 50%;
|
|
4138
|
+
background: #3f6f85;
|
|
4139
|
+
color: #fff;
|
|
4140
|
+
box-shadow: 0 4px 14px rgb(15 42 54 / 24%);
|
|
4141
|
+
cursor: pointer;
|
|
4142
|
+
pointer-events: auto;
|
|
4143
|
+
transition: background-color 120ms ease-out, box-shadow 120ms ease-out, transform 120ms ease-out;
|
|
4144
|
+
}
|
|
4145
|
+
button:hover { background: #315d71; box-shadow: 0 6px 18px rgb(15 42 54 / 30%); }
|
|
4146
|
+
button:active { transform: translateY(1px); }
|
|
4147
|
+
button:focus-visible { outline: 3px solid #f2c94c; outline-offset: 3px; }
|
|
4148
|
+
button:disabled { cursor: wait; opacity: .72; }
|
|
4149
|
+
.status {
|
|
4150
|
+
position: absolute;
|
|
4151
|
+
right: 62px;
|
|
4152
|
+
bottom: 7px;
|
|
4153
|
+
width: max-content;
|
|
4154
|
+
max-width: min(260px, calc(100vw - 94px));
|
|
4155
|
+
padding: 9px 11px;
|
|
4156
|
+
border: 1px solid #c6d3d9;
|
|
4157
|
+
border-radius: 6px;
|
|
4158
|
+
background: #fff;
|
|
4159
|
+
box-shadow: 0 3px 12px rgb(15 42 54 / 16%);
|
|
4160
|
+
color: #263942;
|
|
4161
|
+
font: 600 13px/1.35 "Segoe UI", Arial, sans-serif;
|
|
4162
|
+
pointer-events: none;
|
|
4163
|
+
}
|
|
4164
|
+
.status:empty { display: none; }
|
|
4165
|
+
svg { width: 25px; height: 25px; }
|
|
4166
|
+
@media (max-width: 600px) { :host { right: 12px; bottom: 12px; } }
|
|
4167
|
+
@media (prefers-reduced-motion: reduce) { button { transition: none; } }
|
|
4168
|
+
</style>
|
|
4169
|
+
<button type="button" aria-label="Open Datalynk Chat" title="Chat">
|
|
4170
|
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
4171
|
+
<path fill="currentColor" d="M4 4h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-5 3v-3a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm2.5 8.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm5.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm5.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"/>
|
|
4172
|
+
</svg>
|
|
4173
|
+
</button>
|
|
4174
|
+
<span class="status" role="status" aria-live="polite"></span>`;
|
|
4175
|
+
const button = root.querySelector("button");
|
|
4176
|
+
button.addEventListener("click", () => void this.openChatFromLauncher());
|
|
4177
|
+
document.body.appendChild(host);
|
|
4178
|
+
this.chatLauncherHost = host;
|
|
4179
|
+
this.chatLauncherButton = button;
|
|
4180
|
+
};
|
|
4181
|
+
if (document.body) mount();
|
|
4182
|
+
else document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
4183
|
+
}
|
|
4184
|
+
async openChatFromLauncher() {
|
|
4185
|
+
const button = this.chatLauncherButton;
|
|
4186
|
+
if (!button) return;
|
|
4187
|
+
const status = button.getRootNode().querySelector(".status");
|
|
4188
|
+
button.disabled = true;
|
|
4189
|
+
button.setAttribute("aria-busy", "true");
|
|
4190
|
+
status.textContent = "Opening chat…";
|
|
4191
|
+
try {
|
|
4192
|
+
await this.openChat();
|
|
4193
|
+
status.textContent = "";
|
|
4194
|
+
} catch (error) {
|
|
4195
|
+
button.disabled = false;
|
|
4196
|
+
button.removeAttribute("aria-busy");
|
|
4197
|
+
status.textContent = "Chat is unavailable for this session.";
|
|
4198
|
+
console.error("Unable to open Datalynk Chat.", error);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
restoreChatLauncher() {
|
|
4202
|
+
if (!this.chatLauncherHost || !this.chatLauncherButton) return;
|
|
4203
|
+
this.chatLauncherHost.hidden = false;
|
|
4204
|
+
this.chatLauncherButton.disabled = false;
|
|
4205
|
+
this.chatLauncherButton.removeAttribute("aria-busy");
|
|
4206
|
+
this.chatLauncherButton.focus();
|
|
4207
|
+
}
|
|
4208
|
+
/** Lazily load and construct chat. No chat code or requests run before this call. */
|
|
4209
|
+
async chat() {
|
|
4210
|
+
if (this.chatInstance) return this.chatInstance;
|
|
4211
|
+
const client = await this.loadChatModule();
|
|
4212
|
+
return this.chatInstance || (this.chatInstance = new client.Chat(this));
|
|
4213
|
+
}
|
|
4214
|
+
/** Lazily load, authorize, and open the reusable chat window. */
|
|
4215
|
+
async openChat(options = {}) {
|
|
4216
|
+
const client = await this.loadChatModule();
|
|
4217
|
+
const chat = await this.chat();
|
|
4218
|
+
if (!await chat.available()) throw new Error("Chat is disabled or this session is not authorized.");
|
|
4219
|
+
if (this.socket.options.authentication !== "ticket") {
|
|
4220
|
+
this.socket.options.authentication = "ticket";
|
|
4221
|
+
void this.socket.connect();
|
|
4222
|
+
}
|
|
4223
|
+
const onClose = options.onClose;
|
|
4224
|
+
const chatWindow = client.openChat(chat, {
|
|
4225
|
+
...options,
|
|
4226
|
+
onClose: () => {
|
|
4227
|
+
this.restoreChatLauncher();
|
|
4228
|
+
onClose == null ? void 0 : onClose();
|
|
4229
|
+
}
|
|
4230
|
+
});
|
|
4231
|
+
if (this.chatLauncherHost) this.chatLauncherHost.hidden = true;
|
|
4232
|
+
return chatWindow;
|
|
4233
|
+
}
|
|
4234
|
+
loadChatModule() {
|
|
4235
|
+
if (!this.chatModulePromise) {
|
|
4236
|
+
const moduleUrl = this.options.chatModule || "./chat.mjs";
|
|
4237
|
+
this.chatModulePromise = import(
|
|
4238
|
+
/* @vite-ignore */
|
|
4239
|
+
moduleUrl
|
|
4240
|
+
);
|
|
4241
|
+
}
|
|
4242
|
+
return this.chatModulePromise;
|
|
4000
4243
|
}
|
|
4001
4244
|
async _request(req, options = {}) {
|
|
4002
4245
|
if (this.recovery) throw errorFromCode(503, "Datalynk is unavailable");
|
|
@@ -4014,8 +4257,8 @@ const _Api = class _Api {
|
|
|
4014
4257
|
/** Execute exactly one HTTP API attempt. Recovery uses this directly to avoid recursive retry loops. */
|
|
4015
4258
|
async _requestOnce(req, options = {}, recoveryAttempt = false) {
|
|
4016
4259
|
const token = options.token || this.token;
|
|
4017
|
-
if (token && this.isTokenExpired(token)) {
|
|
4018
|
-
this.
|
|
4260
|
+
if (this.options.manageSession !== false && token && this.isTokenExpired(token)) {
|
|
4261
|
+
if (token === this.token) this.expireToken(token);
|
|
4019
4262
|
throw errorFromCode(401, "Session token expired");
|
|
4020
4263
|
}
|
|
4021
4264
|
let resp;
|
|
@@ -4051,7 +4294,7 @@ const _Api = class _Api {
|
|
|
4051
4294
|
if (!resp.ok || (data == null ? void 0 : data.error)) {
|
|
4052
4295
|
const error = Object.assign(errorFromCode(resp.status, data == null ? void 0 : data.error), data);
|
|
4053
4296
|
Object.defineProperty(error, "response", { value: resp, configurable: true });
|
|
4054
|
-
if (!recoveryAttempt && resp.status
|
|
4297
|
+
if (!recoveryAttempt && resp.status !== 401 && !this.isSqlStateError(error))
|
|
4055
4298
|
this.setConnectionStatus("online");
|
|
4056
4299
|
throw error;
|
|
4057
4300
|
}
|
|
@@ -4059,17 +4302,22 @@ const _Api = class _Api {
|
|
|
4059
4302
|
if (!recoveryAttempt) this.setConnectionStatus("online");
|
|
4060
4303
|
return data;
|
|
4061
4304
|
}
|
|
4062
|
-
/**
|
|
4305
|
+
/**
|
|
4306
|
+
* Only failures which indicate Datalynk as a service is unavailable own global
|
|
4307
|
+
* recovery. A request-specific application failure must not take the whole
|
|
4308
|
+
* client offline: another unrelated API request may still be perfectly healthy.
|
|
4309
|
+
*/
|
|
4063
4310
|
isRecoverableResponseError(error) {
|
|
4064
4311
|
var _a;
|
|
4065
|
-
if (error instanceof UnexpectedApiResponseError) return true;
|
|
4066
4312
|
if ((error == null ? void 0 : error.code) === 401) return false;
|
|
4067
4313
|
const status = ((_a = error == null ? void 0 : error.response) == null ? void 0 : _a.status) ?? (error == null ? void 0 : error.code);
|
|
4068
|
-
|
|
4314
|
+
if (this.isGatewayUnavailableStatus(status)) return true;
|
|
4315
|
+
return this.isSqlStateError(error);
|
|
4069
4316
|
}
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4317
|
+
/** Any API error returned in PDO SQLSTATE form uses the offline/recovery path. */
|
|
4318
|
+
isSqlStateError(error) {
|
|
4319
|
+
const message = String((error == null ? void 0 : error.error) ?? (error == null ? void 0 : error.message) ?? "").trim();
|
|
4320
|
+
return /^SQLSTATE\[/i.test(message);
|
|
4073
4321
|
}
|
|
4074
4322
|
/**
|
|
4075
4323
|
* A returned server failure immediately makes the client unavailable. The exact
|
|
@@ -4162,8 +4410,22 @@ const _Api = class _Api {
|
|
|
4162
4410
|
this.setConnectionStatus("unauthorized");
|
|
4163
4411
|
return;
|
|
4164
4412
|
}
|
|
4165
|
-
if (this.
|
|
4166
|
-
|
|
4413
|
+
if (this.offlineAuthenticationExpired) {
|
|
4414
|
+
const controller2 = new AbortController();
|
|
4415
|
+
const timeout2 = setTimeout(() => controller2.abort(), this.heartbeat.timeout);
|
|
4416
|
+
try {
|
|
4417
|
+
const response = await fetch(this.url + this.heartbeat.target, { signal: controller2.signal });
|
|
4418
|
+
if (this.isGatewayUnavailableStatus(response.status)) this.setConnectionStatus("unavailable");
|
|
4419
|
+
else this.markUnauthorized(this.token, true);
|
|
4420
|
+
} catch {
|
|
4421
|
+
this.setConnectionStatus("offline");
|
|
4422
|
+
} finally {
|
|
4423
|
+
clearTimeout(timeout2);
|
|
4424
|
+
}
|
|
4425
|
+
return;
|
|
4426
|
+
}
|
|
4427
|
+
if (this.options.manageSession !== false && this.token && this.isTokenExpired(this.token)) {
|
|
4428
|
+
this.expireToken(this.token);
|
|
4167
4429
|
return;
|
|
4168
4430
|
}
|
|
4169
4431
|
const controller = new AbortController();
|
|
@@ -4171,13 +4433,16 @@ const _Api = class _Api {
|
|
|
4171
4433
|
try {
|
|
4172
4434
|
const response = await fetch(this.url + this.heartbeat.target, { signal: controller.signal });
|
|
4173
4435
|
if (this.recovery) return;
|
|
4174
|
-
this.setConnectionStatus(response.
|
|
4436
|
+
this.setConnectionStatus(this.isGatewayUnavailableStatus(response.status) ? "unavailable" : "online");
|
|
4175
4437
|
} catch (error) {
|
|
4176
4438
|
if (!this.recovery) this.setConnectionStatus("offline");
|
|
4177
4439
|
} finally {
|
|
4178
4440
|
clearTimeout(timeout);
|
|
4179
4441
|
}
|
|
4180
4442
|
}
|
|
4443
|
+
isGatewayUnavailableStatus(status) {
|
|
4444
|
+
return status === 502 || status === 503 || status === 504;
|
|
4445
|
+
}
|
|
4181
4446
|
isTokenExpired(token) {
|
|
4182
4447
|
var _a;
|
|
4183
4448
|
try {
|
|
@@ -4227,45 +4492,242 @@ const _Api = class _Api {
|
|
|
4227
4492
|
try {
|
|
4228
4493
|
expiresAt = (((_a = decodeJwt(token)) == null ? void 0 : _a.exp) ?? 0) * 1e3;
|
|
4229
4494
|
} catch {
|
|
4230
|
-
if (!this.adoptStoredToken(token)) this.
|
|
4495
|
+
if (!this.adoptStoredToken(token)) this.expireToken(token);
|
|
4231
4496
|
return;
|
|
4232
4497
|
}
|
|
4233
4498
|
const delay = expiresAt - Date.now();
|
|
4234
4499
|
if (delay <= 0) {
|
|
4235
|
-
if (!this.adoptStoredToken(token)) this.
|
|
4500
|
+
if (!this.adoptStoredToken(token)) this.expireToken(token);
|
|
4236
4501
|
return;
|
|
4237
4502
|
}
|
|
4238
4503
|
this.tokenExpiryTimeout = setTimeout(() => {
|
|
4239
4504
|
if (delay > 2147e6) this.scheduleTokenExpiry(token);
|
|
4240
|
-
else if (!this.adoptStoredToken(token)) this.
|
|
4505
|
+
else if (!this.adoptStoredToken(token)) this.expireToken(token);
|
|
4241
4506
|
}, Math.min(delay, 2147e6));
|
|
4242
4507
|
(_c = (_b = this.tokenExpiryTimeout) == null ? void 0 : _b.unref) == null ? void 0 : _c.call(_b);
|
|
4243
4508
|
}
|
|
4244
|
-
|
|
4509
|
+
/** Handle local JWT expiry without treating an offline device like a rejected online session. */
|
|
4510
|
+
expireToken(token) {
|
|
4511
|
+
if (token !== this.token) return;
|
|
4512
|
+
if (this.shouldDeferLocalExpiry()) {
|
|
4513
|
+
this.deferOfflineExpiry(token);
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
this.markUnauthorized(token, this.hasOwnedOfflineSession(token));
|
|
4517
|
+
}
|
|
4518
|
+
shouldDeferLocalExpiry() {
|
|
4519
|
+
if (this.authenticationInvalid) return false;
|
|
4520
|
+
if (typeof navigator != "undefined" && typeof navigator.onLine != "undefined" && !navigator.onLine) return true;
|
|
4521
|
+
return this.status === "offline" || this.status === "unavailable";
|
|
4522
|
+
}
|
|
4523
|
+
deferOfflineExpiry(token) {
|
|
4524
|
+
this.authenticationInvalid = false;
|
|
4525
|
+
this.offlineAuthenticationExpired = true;
|
|
4526
|
+
this.cancelRecovery(errorFromCode(401, "Session expired while offline"));
|
|
4527
|
+
this.rememberOfflineSessionOwner(token);
|
|
4528
|
+
if (this.token !== token) this.token$.next(token);
|
|
4529
|
+
if (this.status === "online") this.setConnectionStatus("offline");
|
|
4530
|
+
}
|
|
4531
|
+
markUnauthorized(token, preservePending = this.hasOwnedOfflineSession(token || this.token)) {
|
|
4245
4532
|
if (token && token !== this.token) return;
|
|
4246
4533
|
if (this.adoptStoredToken(token)) return;
|
|
4534
|
+
if (this.authenticationInvalid && this.unauthorizedLogout) return;
|
|
4247
4535
|
this.authenticationInvalid = true;
|
|
4536
|
+
this.offlineAuthenticationExpired = false;
|
|
4248
4537
|
this.cancelRecovery(errorFromCode(401, "Session expired"));
|
|
4249
|
-
if (this.token
|
|
4538
|
+
if (this.token !== null) this.token$.next(null);
|
|
4250
4539
|
this.setConnectionStatus("unauthorized");
|
|
4540
|
+
this.unauthorizedLogout ?? (this.unauthorizedLogout = Promise.resolve().then(() => this.finishUnauthorizedLogout(preservePending)).finally(() => {
|
|
4541
|
+
this.unauthorizedLogout = null;
|
|
4542
|
+
}));
|
|
4543
|
+
}
|
|
4544
|
+
async finishUnauthorizedLogout(preservePending = false) {
|
|
4545
|
+
var _a;
|
|
4546
|
+
if (this.options.manageSession === false) return;
|
|
4547
|
+
if (typeof localStorage != "undefined") {
|
|
4548
|
+
localStorage.removeItem(this.localStorageKey);
|
|
4549
|
+
localStorage.removeItem("datalynk-user");
|
|
4550
|
+
if (!preservePending) localStorage.removeItem(this.offlineSessionOwnerKey);
|
|
4551
|
+
}
|
|
4552
|
+
if (!preservePending) this.offlineSessionOwner = null;
|
|
4553
|
+
for (const slice of this.sliceCache.values()) {
|
|
4554
|
+
try {
|
|
4555
|
+
slice.sync(false);
|
|
4556
|
+
} catch {
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
await this.clearOfflineSessionCache(preservePending);
|
|
4560
|
+
if (typeof caches != "undefined") {
|
|
4561
|
+
try {
|
|
4562
|
+
await Promise.all([caches.delete("datalynk"), caches.delete("api-settings")]);
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
console.error("Unable to clear Datalynk service worker cache", error);
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
if (typeof navigator != "undefined" && ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller)) {
|
|
4568
|
+
navigator.serviceWorker.controller.postMessage({ token: null, clearCache: true });
|
|
4569
|
+
}
|
|
4570
|
+
if (typeof location != "undefined" && typeof location.reload == "function")
|
|
4571
|
+
location.reload();
|
|
4572
|
+
}
|
|
4573
|
+
async clearOfflineSessionCache(preservePending = false) {
|
|
4574
|
+
if (typeof indexedDB == "undefined") return;
|
|
4575
|
+
try {
|
|
4576
|
+
if (this.database) {
|
|
4577
|
+
const db = await this.database.connection;
|
|
4578
|
+
const stores = Array.from(db.objectStoreNames).filter((name) => !(preservePending && name === "pending"));
|
|
4579
|
+
if (!stores.length) return;
|
|
4580
|
+
await new Promise((resolve, reject) => {
|
|
4581
|
+
const tx = db.transaction(stores, "readwrite");
|
|
4582
|
+
stores.forEach((name) => tx.objectStore(name).clear());
|
|
4583
|
+
tx.oncomplete = () => resolve();
|
|
4584
|
+
tx.onerror = () => reject(tx.error);
|
|
4585
|
+
tx.onabort = () => reject(tx.error);
|
|
4586
|
+
});
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
await new Promise((resolve, reject) => {
|
|
4590
|
+
const request = indexedDB.open("datalynk");
|
|
4591
|
+
request.onerror = () => reject(request.error);
|
|
4592
|
+
request.onsuccess = () => {
|
|
4593
|
+
const db = request.result;
|
|
4594
|
+
const stores = Array.from(db.objectStoreNames).filter((name) => !(preservePending && name === "pending"));
|
|
4595
|
+
if (!stores.length) {
|
|
4596
|
+
db.close();
|
|
4597
|
+
resolve();
|
|
4598
|
+
return;
|
|
4599
|
+
}
|
|
4600
|
+
const tx = db.transaction(stores, "readwrite");
|
|
4601
|
+
stores.forEach((name) => tx.objectStore(name).clear());
|
|
4602
|
+
tx.oncomplete = () => {
|
|
4603
|
+
db.close();
|
|
4604
|
+
resolve();
|
|
4605
|
+
};
|
|
4606
|
+
tx.onerror = () => {
|
|
4607
|
+
const error = tx.error;
|
|
4608
|
+
db.close();
|
|
4609
|
+
reject(error);
|
|
4610
|
+
};
|
|
4611
|
+
tx.onabort = () => {
|
|
4612
|
+
const error = tx.error;
|
|
4613
|
+
db.close();
|
|
4614
|
+
reject(error);
|
|
4615
|
+
};
|
|
4616
|
+
};
|
|
4617
|
+
});
|
|
4618
|
+
} catch (error) {
|
|
4619
|
+
console.error("Unable to clear Datalynk offline session cache", error);
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
tokenIdentity(token) {
|
|
4623
|
+
if (!token) return null;
|
|
4624
|
+
try {
|
|
4625
|
+
const payload = decodeJwt(token);
|
|
4626
|
+
const user = (payload == null ? void 0 : payload.uid) ?? (payload == null ? void 0 : payload.sub);
|
|
4627
|
+
return {
|
|
4628
|
+
realm: (payload == null ? void 0 : payload.realm) == null ? void 0 : String(payload.realm),
|
|
4629
|
+
user: user == null ? void 0 : String(user)
|
|
4630
|
+
};
|
|
4631
|
+
} catch {
|
|
4632
|
+
return null;
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
readOfflineSessionOwner() {
|
|
4636
|
+
if (this.offlineSessionOwner) return this.offlineSessionOwner;
|
|
4637
|
+
if (typeof localStorage == "undefined") return null;
|
|
4638
|
+
try {
|
|
4639
|
+
const value = localStorage.getItem(this.offlineSessionOwnerKey);
|
|
4640
|
+
if (!value) return null;
|
|
4641
|
+
const owner = JSON.parse(value);
|
|
4642
|
+
if (!owner || typeof owner !== "object") return null;
|
|
4643
|
+
return this.offlineSessionOwner = owner;
|
|
4644
|
+
} catch {
|
|
4645
|
+
return null;
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
ownersMatch(owner, identity2) {
|
|
4649
|
+
if (!identity2) return false;
|
|
4650
|
+
if (owner.realm != null && owner.realm !== identity2.realm) return false;
|
|
4651
|
+
if (owner.user != null && owner.user !== identity2.user) return false;
|
|
4652
|
+
return true;
|
|
4653
|
+
}
|
|
4654
|
+
rememberOfflineSessionOwner(token) {
|
|
4655
|
+
const identity2 = this.tokenIdentity(token);
|
|
4656
|
+
if (!identity2) return;
|
|
4657
|
+
const existing = this.readOfflineSessionOwner();
|
|
4658
|
+
if (existing && !this.ownersMatch(existing, identity2)) return;
|
|
4659
|
+
this.offlineSessionOwner = identity2;
|
|
4660
|
+
if (typeof localStorage != "undefined") {
|
|
4661
|
+
try {
|
|
4662
|
+
localStorage.setItem(this.offlineSessionOwnerKey, JSON.stringify(identity2));
|
|
4663
|
+
} catch {
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
ensureOfflineSessionOwner(token) {
|
|
4668
|
+
const existing = this.readOfflineSessionOwner();
|
|
4669
|
+
const identity2 = this.tokenIdentity(token);
|
|
4670
|
+
if (existing) return this.ownersMatch(existing, identity2);
|
|
4671
|
+
if (!identity2) return true;
|
|
4672
|
+
this.rememberOfflineSessionOwner(token);
|
|
4673
|
+
return true;
|
|
4674
|
+
}
|
|
4675
|
+
hasOwnedOfflineSession(token) {
|
|
4676
|
+
const owner = this.readOfflineSessionOwner();
|
|
4677
|
+
return !!owner && this.ownersMatch(owner, this.tokenIdentity(token));
|
|
4678
|
+
}
|
|
4679
|
+
clearOfflineSessionOwner() {
|
|
4680
|
+
this.offlineSessionOwner = null;
|
|
4681
|
+
if (typeof localStorage != "undefined") {
|
|
4682
|
+
try {
|
|
4683
|
+
localStorage.removeItem(this.offlineSessionOwnerKey);
|
|
4684
|
+
} catch {
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
/** Replay queued writes only after a matching authenticated session is online. */
|
|
4689
|
+
async replayPendingQueue() {
|
|
4690
|
+
if (!this.database || !this.online || this.authenticationInvalid || this.offlineAuthenticationExpired) return;
|
|
4691
|
+
if (this.replayingPending) {
|
|
4692
|
+
this.pendingReplayRequested = true;
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
const table = this.database.table("pending");
|
|
4696
|
+
const owner = this.readOfflineSessionOwner();
|
|
4697
|
+
if (owner && !this.ownersMatch(owner, this.tokenIdentity(this.token))) {
|
|
4698
|
+
if (this.token) console.warn("Datalynk offline queue is quarantined because it belongs to another authenticated user");
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
this.replayingPending = true;
|
|
4702
|
+
try {
|
|
4703
|
+
const keys = await table.getAllKeys();
|
|
4704
|
+
for (const key of keys) {
|
|
4705
|
+
if (!this.online || this.authenticationInvalid || this.offlineAuthenticationExpired) break;
|
|
4706
|
+
if (owner && !this.ownersMatch(owner, this.tokenIdentity(this.token))) break;
|
|
4707
|
+
const request = await table.get(key);
|
|
4708
|
+
if (request == null) continue;
|
|
4709
|
+
try {
|
|
4710
|
+
await this.request(request);
|
|
4711
|
+
await table.delete(key);
|
|
4712
|
+
} catch (error) {
|
|
4713
|
+
break;
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
if (await table.count() === 0) this.clearOfflineSessionOwner();
|
|
4717
|
+
} finally {
|
|
4718
|
+
this.replayingPending = false;
|
|
4719
|
+
if (this.pendingReplayRequested) {
|
|
4720
|
+
this.pendingReplayRequested = false;
|
|
4721
|
+
if (this.online && !this.authenticationInvalid && !this.offlineAuthenticationExpired)
|
|
4722
|
+
void this.replayPendingQueue();
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4251
4725
|
}
|
|
4252
4726
|
setConnectionStatus(status) {
|
|
4253
4727
|
if (this.status !== status) this.status$.next(status);
|
|
4254
4728
|
const online = status === "online";
|
|
4255
4729
|
if (this.online !== online) this.online$.next(online);
|
|
4256
4730
|
}
|
|
4257
|
-
offlineBanner() {
|
|
4258
|
-
if (this.options.offlineBanner === false || typeof document == "undefined") return;
|
|
4259
|
-
if (this.status === "online") {
|
|
4260
|
-
removeBanner("datalynk-offline-banner");
|
|
4261
|
-
} else {
|
|
4262
|
-
const message = this.status === "unauthorized" ? "⚠️ Your session expired, please sign in again" : this.status === "unavailable" ? "⚠️ Datalynk is currently unavailable" : "⚠️ You are offline, please reconnect to sync changes";
|
|
4263
|
-
createBanner(message, {
|
|
4264
|
-
id: "datalynk-offline-banner",
|
|
4265
|
-
position: this.options.offlineBanner === "top" ? "top" : "bottom"
|
|
4266
|
-
});
|
|
4267
|
-
}
|
|
4268
|
-
}
|
|
4269
4731
|
startHeartbeat() {
|
|
4270
4732
|
this.stopHeartbeat();
|
|
4271
4733
|
this.heartbeat.interval = setInterval(() => this.checkConnection(), this.heartbeat.timeout / 2);
|
|
@@ -4375,6 +4837,8 @@ const _Api = class _Api {
|
|
|
4375
4837
|
if (this.offline && typeof navigator != "undefined") {
|
|
4376
4838
|
if (this.status === "unauthorized") return Promise.reject(errorFromCode(401, "Session expired"));
|
|
4377
4839
|
if (options.offline) {
|
|
4840
|
+
if (!this.ensureOfflineSessionOwner(this.token))
|
|
4841
|
+
return Promise.reject(errorFromCode(409, "Offline queue belongs to another authenticated user"));
|
|
4378
4842
|
(_b = (_a = this.database) == null ? void 0 : _a.table("pending")) == null ? void 0 : _b.add(data, key);
|
|
4379
4843
|
return Promise.resolve();
|
|
4380
4844
|
}
|