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