@auxilium/datalynk-client 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/datalynk-models.mjs +0 -0
- package/dist/api.d.ts +47 -6
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/index.cjs +338 -46
- package/dist/index.mjs +338 -46
- package/dist/service.worker.mjs +11 -6
- package/package.json +1 -1
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
|
}
|
|
@@ -3229,7 +3234,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3229
3234
|
} });
|
|
3230
3235
|
}
|
|
3231
3236
|
}
|
|
3232
|
-
const version = "1.5.
|
|
3237
|
+
const version = "1.5.2";
|
|
3233
3238
|
class WebRtc {
|
|
3234
3239
|
constructor(api) {
|
|
3235
3240
|
__publicField(this, "ice");
|
|
@@ -3776,7 +3781,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3776
3781
|
timeout: 6e4
|
|
3777
3782
|
});
|
|
3778
3783
|
__publicField(this, "authenticationInvalid", false);
|
|
3784
|
+
/** Token reached exp while the client could not reach Datalynk; keep the offline session alive until reconnect. */
|
|
3785
|
+
__publicField(this, "offlineAuthenticationExpired", false);
|
|
3779
3786
|
__publicField(this, "tokenExpiryTimeout", null);
|
|
3787
|
+
__publicField(this, "unauthorizedLogout", null);
|
|
3788
|
+
__publicField(this, "replayingPending", false);
|
|
3789
|
+
/** If replay is requested while a replay pass is already active, run another pass when it finishes. */
|
|
3790
|
+
__publicField(this, "pendingReplayRequested", false);
|
|
3791
|
+
__publicField(this, "offlineSessionOwner", null);
|
|
3780
3792
|
/** Request-specific recovery after a server response proves an API call is broken. */
|
|
3781
3793
|
__publicField(this, "recovery", null);
|
|
3782
3794
|
/** Retry the failed request twice immediately, then this long after each failed recovery response. */
|
|
@@ -3784,6 +3796,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3784
3796
|
__publicField(this, "recoveryImmediateRetries", 2);
|
|
3785
3797
|
/** LocalStorage key for persisting logins */
|
|
3786
3798
|
__publicField(this, "localStorageKey", "datalynk-token");
|
|
3799
|
+
/** Persisted owner of an offline queue so another login cannot inherit queued writes. */
|
|
3800
|
+
__publicField(this, "offlineSessionOwnerKey", "datalynk-offline-owner");
|
|
3801
|
+
__publicField(this, "tokenStorageListener", null);
|
|
3787
3802
|
/** Pending requests cache */
|
|
3788
3803
|
__publicField(this, "pending", {});
|
|
3789
3804
|
/** Helpers */
|
|
@@ -3833,12 +3848,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3833
3848
|
var _a, _b;
|
|
3834
3849
|
this.origin = origin;
|
|
3835
3850
|
this.url = `${new URL(origin).origin}/api/`;
|
|
3851
|
+
const development = this.isDevelopmentEnvironment(origin);
|
|
3836
3852
|
this.options = {
|
|
3837
3853
|
manifest: {},
|
|
3838
3854
|
name: typeof document != "undefined" ? document.title : "Datalynk",
|
|
3839
3855
|
offline: [],
|
|
3840
3856
|
origin: typeof location !== "undefined" ? location.host : "Unknown",
|
|
3841
3857
|
saveSession: true,
|
|
3858
|
+
replayOfflineQueue: true,
|
|
3859
|
+
watchTokenExpiry: true,
|
|
3842
3860
|
serviceWorker: "/service.worker.mjs",
|
|
3843
3861
|
...options,
|
|
3844
3862
|
webrtc: {
|
|
@@ -3848,14 +3866,28 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3848
3866
|
...options.webrtc || {}
|
|
3849
3867
|
}
|
|
3850
3868
|
};
|
|
3869
|
+
if (development) this.options.watchTokenExpiry = false;
|
|
3851
3870
|
if (this.options.saveSession && typeof localStorage != "undefined") {
|
|
3852
3871
|
this.token = localStorage.getItem(this.localStorageKey) || null;
|
|
3853
3872
|
this.token$.pipe(distinctUntilChanged()).subscribe((token) => {
|
|
3854
3873
|
if (token) localStorage.setItem(this.localStorageKey, token);
|
|
3855
3874
|
else localStorage.removeItem(this.localStorageKey);
|
|
3856
3875
|
});
|
|
3876
|
+
if (typeof window != "undefined") {
|
|
3877
|
+
this.tokenStorageListener = (event) => {
|
|
3878
|
+
if (event.storageArea === localStorage && event.key == null) {
|
|
3879
|
+
this.token = null;
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
if (event.key !== this.localStorageKey || event.newValue === this.token) return;
|
|
3883
|
+
if (event.newValue && this.canAdoptToken(event.newValue, this.token)) this.token = event.newValue;
|
|
3884
|
+
else if (event.newValue == null) this.token = null;
|
|
3885
|
+
};
|
|
3886
|
+
window.addEventListener("storage", this.tokenStorageListener);
|
|
3887
|
+
}
|
|
3857
3888
|
}
|
|
3858
|
-
this.
|
|
3889
|
+
if (this.options.watchTokenExpiry)
|
|
3890
|
+
this.token$.pipe(distinctUntilChanged()).subscribe((token) => this.scheduleTokenExpiry(token));
|
|
3859
3891
|
this.socket = new Socket(this, { url: options.socket });
|
|
3860
3892
|
this.gps = new Gps(this, options.gps);
|
|
3861
3893
|
this.auth = new Auth(this);
|
|
@@ -3866,21 +3898,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3866
3898
|
this.webrtc = new WebRtc(this);
|
|
3867
3899
|
if (typeof indexedDB != "undefined") {
|
|
3868
3900
|
this.database = new Database("datalynk", ["pending", ...this.options.offline || []]);
|
|
3869
|
-
this.online$.subscribe(
|
|
3870
|
-
|
|
3871
|
-
if (!online) return;
|
|
3872
|
-
const table = (_a2 = this.database) == null ? void 0 : _a2.table("pending");
|
|
3873
|
-
const keys = await (table == null ? void 0 : table.getAllKeys());
|
|
3874
|
-
await Promise.allSettled(keys.map(async (k) => {
|
|
3875
|
-
const r = await (table == null ? void 0 : table.get(k));
|
|
3876
|
-
await this.request(r).then(() => table == null ? void 0 : table.delete(k));
|
|
3877
|
-
}));
|
|
3901
|
+
if (this.options.replayOfflineQueue !== false) this.online$.subscribe((online) => {
|
|
3902
|
+
if (online) void this.replayPendingQueue();
|
|
3878
3903
|
});
|
|
3879
3904
|
}
|
|
3880
3905
|
if (typeof window !== "undefined") {
|
|
3881
3906
|
window.addEventListener("online", () => this.checkConnection());
|
|
3882
3907
|
window.addEventListener("offline", () => this.setConnectionStatus("offline"));
|
|
3883
|
-
this.online$.subscribe(() => this.offlineBanner());
|
|
3884
3908
|
this.startHeartbeat();
|
|
3885
3909
|
}
|
|
3886
3910
|
if ((_a = this.options.offline) == null ? void 0 : _a.length) {
|
|
@@ -3911,6 +3935,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3911
3935
|
var _a;
|
|
3912
3936
|
return (((_a = this.jwtPayload) == null ? void 0 : _a.exp) ?? Infinity) * 1e3 <= Date.now();
|
|
3913
3937
|
}
|
|
3938
|
+
/** Whether authentication expired while offline and must be renewed before replaying queued work. */
|
|
3939
|
+
get reauthenticationPending() {
|
|
3940
|
+
return this.offlineAuthenticationExpired;
|
|
3941
|
+
}
|
|
3914
3942
|
/** Get session info from JWT payload */
|
|
3915
3943
|
get jwtPayload() {
|
|
3916
3944
|
if (!this.token) return null;
|
|
@@ -3969,19 +3997,26 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
3969
3997
|
return this.token$.getValue();
|
|
3970
3998
|
}
|
|
3971
3999
|
set token(token) {
|
|
4000
|
+
var _a;
|
|
3972
4001
|
if (this.recovery && token !== this.recovery.token)
|
|
3973
4002
|
this.cancelRecovery(errorFromCode(401, "Session changed during API recovery"));
|
|
3974
4003
|
if (token && this.isTokenExpired(token)) {
|
|
3975
|
-
this.
|
|
3976
|
-
this.
|
|
3977
|
-
|
|
4004
|
+
if (this.adoptStoredToken(token)) return;
|
|
4005
|
+
if (this.shouldDeferLocalExpiry()) {
|
|
4006
|
+
this.deferOfflineExpiry(token);
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
this.markUnauthorized(void 0, this.hasOwnedOfflineSession(token));
|
|
3978
4010
|
return;
|
|
3979
4011
|
}
|
|
3980
4012
|
this.authenticationInvalid = false;
|
|
4013
|
+
this.offlineAuthenticationExpired = false;
|
|
3981
4014
|
this.token$.next(token);
|
|
3982
4015
|
if (token && !this.online && !this.recovery) {
|
|
3983
|
-
|
|
4016
|
+
const browserOnline = typeof navigator == "undefined" || typeof navigator.onLine == "undefined" || navigator.onLine;
|
|
4017
|
+
this.setConnectionStatus(browserOnline ? "online" : "offline");
|
|
3984
4018
|
}
|
|
4019
|
+
if (token && ((_a = this.options) == null ? void 0 : _a.replayOfflineQueue) !== false) void this.replayPendingQueue();
|
|
3985
4020
|
}
|
|
3986
4021
|
async _request(req, options = {}) {
|
|
3987
4022
|
if (this.recovery) throw errorFromCode(503, "Datalynk is unavailable");
|
|
@@ -4000,7 +4035,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4000
4035
|
async _requestOnce(req, options = {}, recoveryAttempt = false) {
|
|
4001
4036
|
const token = options.token || this.token;
|
|
4002
4037
|
if (token && this.isTokenExpired(token)) {
|
|
4003
|
-
this.
|
|
4038
|
+
if (token === this.token) this.expireToken(token);
|
|
4004
4039
|
throw errorFromCode(401, "Session token expired");
|
|
4005
4040
|
}
|
|
4006
4041
|
let resp;
|
|
@@ -4036,7 +4071,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4036
4071
|
if (!resp.ok || (data == null ? void 0 : data.error)) {
|
|
4037
4072
|
const error = Object.assign(errorFromCode(resp.status, data == null ? void 0 : data.error), data);
|
|
4038
4073
|
Object.defineProperty(error, "response", { value: resp, configurable: true });
|
|
4039
|
-
if (!recoveryAttempt && resp.status
|
|
4074
|
+
if (!recoveryAttempt && resp.status !== 401 && !this.isSqlStateError(error))
|
|
4040
4075
|
this.setConnectionStatus("online");
|
|
4041
4076
|
throw error;
|
|
4042
4077
|
}
|
|
@@ -4044,17 +4079,22 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4044
4079
|
if (!recoveryAttempt) this.setConnectionStatus("online");
|
|
4045
4080
|
return data;
|
|
4046
4081
|
}
|
|
4047
|
-
/**
|
|
4082
|
+
/**
|
|
4083
|
+
* Only failures which indicate Datalynk as a service is unavailable own global
|
|
4084
|
+
* recovery. A request-specific application failure must not take the whole
|
|
4085
|
+
* client offline: another unrelated API request may still be perfectly healthy.
|
|
4086
|
+
*/
|
|
4048
4087
|
isRecoverableResponseError(error) {
|
|
4049
4088
|
var _a;
|
|
4050
|
-
if (error instanceof UnexpectedApiResponseError) return true;
|
|
4051
4089
|
if ((error == null ? void 0 : error.code) === 401) return false;
|
|
4052
4090
|
const status = ((_a = error == null ? void 0 : error.response) == null ? void 0 : _a.status) ?? (error == null ? void 0 : error.code);
|
|
4053
|
-
|
|
4091
|
+
if (this.isGatewayUnavailableStatus(status)) return true;
|
|
4092
|
+
return this.isSqlStateError(error);
|
|
4054
4093
|
}
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4094
|
+
/** Any API error returned in PDO SQLSTATE form uses the offline/recovery path. */
|
|
4095
|
+
isSqlStateError(error) {
|
|
4096
|
+
const message = String((error == null ? void 0 : error.error) ?? (error == null ? void 0 : error.message) ?? "").trim();
|
|
4097
|
+
return /^SQLSTATE\[/i.test(message);
|
|
4058
4098
|
}
|
|
4059
4099
|
/**
|
|
4060
4100
|
* A returned server failure immediately makes the client unavailable. The exact
|
|
@@ -4147,8 +4187,22 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4147
4187
|
this.setConnectionStatus("unauthorized");
|
|
4148
4188
|
return;
|
|
4149
4189
|
}
|
|
4190
|
+
if (this.offlineAuthenticationExpired) {
|
|
4191
|
+
const controller2 = new AbortController();
|
|
4192
|
+
const timeout2 = setTimeout(() => controller2.abort(), this.heartbeat.timeout);
|
|
4193
|
+
try {
|
|
4194
|
+
const response = await fetch(this.url + this.heartbeat.target, { signal: controller2.signal });
|
|
4195
|
+
if (this.isGatewayUnavailableStatus(response.status)) this.setConnectionStatus("unavailable");
|
|
4196
|
+
else this.markUnauthorized(this.token, true);
|
|
4197
|
+
} catch {
|
|
4198
|
+
this.setConnectionStatus("offline");
|
|
4199
|
+
} finally {
|
|
4200
|
+
clearTimeout(timeout2);
|
|
4201
|
+
}
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4150
4204
|
if (this.token && this.isTokenExpired(this.token)) {
|
|
4151
|
-
this.
|
|
4205
|
+
this.expireToken(this.token);
|
|
4152
4206
|
return;
|
|
4153
4207
|
}
|
|
4154
4208
|
const controller = new AbortController();
|
|
@@ -4156,13 +4210,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4156
4210
|
try {
|
|
4157
4211
|
const response = await fetch(this.url + this.heartbeat.target, { signal: controller.signal });
|
|
4158
4212
|
if (this.recovery) return;
|
|
4159
|
-
this.setConnectionStatus(response.
|
|
4213
|
+
this.setConnectionStatus(this.isGatewayUnavailableStatus(response.status) ? "unavailable" : "online");
|
|
4160
4214
|
} catch (error) {
|
|
4161
4215
|
if (!this.recovery) this.setConnectionStatus("offline");
|
|
4162
4216
|
} finally {
|
|
4163
4217
|
clearTimeout(timeout);
|
|
4164
4218
|
}
|
|
4165
4219
|
}
|
|
4220
|
+
isGatewayUnavailableStatus(status) {
|
|
4221
|
+
return status === 502 || status === 503 || status === 504;
|
|
4222
|
+
}
|
|
4166
4223
|
isTokenExpired(token) {
|
|
4167
4224
|
var _a;
|
|
4168
4225
|
try {
|
|
@@ -4171,6 +4228,38 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4171
4228
|
return true;
|
|
4172
4229
|
}
|
|
4173
4230
|
}
|
|
4231
|
+
isDevelopmentEnvironment(origin) {
|
|
4232
|
+
var _a;
|
|
4233
|
+
const isDevHost = (hostname) => {
|
|
4234
|
+
hostname = hostname.toLowerCase();
|
|
4235
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".datalynk") || hostname.endsWith(".test");
|
|
4236
|
+
};
|
|
4237
|
+
const configuredDev = typeof window != "undefined" && ((_a = window.dojoConfig) == null ? void 0 : _a.isDev) === true;
|
|
4238
|
+
const pageHost = typeof location == "undefined" ? "" : location.hostname;
|
|
4239
|
+
return configuredDev || isDevHost(new URL(origin).hostname) || !!pageHost && isDevHost(pageHost);
|
|
4240
|
+
}
|
|
4241
|
+
canAdoptToken(candidate, rejected) {
|
|
4242
|
+
if (!candidate || candidate === rejected || this.isTokenExpired(candidate)) return false;
|
|
4243
|
+
try {
|
|
4244
|
+
const next = decodeJwt(candidate);
|
|
4245
|
+
const previous = rejected ? decodeJwt(rejected) : null;
|
|
4246
|
+
if ((previous == null ? void 0 : previous.realm) && (next == null ? void 0 : next.realm) && previous.realm !== next.realm) return false;
|
|
4247
|
+
const previousUser = (previous == null ? void 0 : previous.uid) ?? (previous == null ? void 0 : previous.sub);
|
|
4248
|
+
const nextUser = (next == null ? void 0 : next.uid) ?? (next == null ? void 0 : next.sub);
|
|
4249
|
+
return previousUser == null || String(previousUser) === String(nextUser);
|
|
4250
|
+
} catch {
|
|
4251
|
+
return false;
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
/** Adopt a newer canonical token written by another auth path before expiring this session. */
|
|
4255
|
+
adoptStoredToken(rejected) {
|
|
4256
|
+
var _a;
|
|
4257
|
+
if (!((_a = this.options) == null ? void 0 : _a.saveSession) || typeof localStorage == "undefined") return false;
|
|
4258
|
+
const stored = localStorage.getItem(this.localStorageKey);
|
|
4259
|
+
if (!stored || !this.canAdoptToken(stored, rejected)) return false;
|
|
4260
|
+
this.token = stored;
|
|
4261
|
+
return true;
|
|
4262
|
+
}
|
|
4174
4263
|
scheduleTokenExpiry(token) {
|
|
4175
4264
|
var _a, _b, _c;
|
|
4176
4265
|
if (this.tokenExpiryTimeout) clearTimeout(this.tokenExpiryTimeout);
|
|
@@ -4180,40 +4269,241 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4180
4269
|
try {
|
|
4181
4270
|
expiresAt = (((_a = decodeJwt(token)) == null ? void 0 : _a.exp) ?? 0) * 1e3;
|
|
4182
4271
|
} catch {
|
|
4183
|
-
|
|
4272
|
+
if (!this.adoptStoredToken(token)) this.expireToken(token);
|
|
4273
|
+
return;
|
|
4184
4274
|
}
|
|
4185
4275
|
const delay = expiresAt - Date.now();
|
|
4186
|
-
if (delay <= 0)
|
|
4276
|
+
if (delay <= 0) {
|
|
4277
|
+
if (!this.adoptStoredToken(token)) this.expireToken(token);
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4187
4280
|
this.tokenExpiryTimeout = setTimeout(() => {
|
|
4188
4281
|
if (delay > 2147e6) this.scheduleTokenExpiry(token);
|
|
4189
|
-
else this.
|
|
4282
|
+
else if (!this.adoptStoredToken(token)) this.expireToken(token);
|
|
4190
4283
|
}, Math.min(delay, 2147e6));
|
|
4191
4284
|
(_c = (_b = this.tokenExpiryTimeout) == null ? void 0 : _b.unref) == null ? void 0 : _c.call(_b);
|
|
4192
4285
|
}
|
|
4193
|
-
|
|
4286
|
+
/** Handle local JWT expiry without treating an offline device like a rejected online session. */
|
|
4287
|
+
expireToken(token) {
|
|
4288
|
+
if (token !== this.token) return;
|
|
4289
|
+
if (this.shouldDeferLocalExpiry()) {
|
|
4290
|
+
this.deferOfflineExpiry(token);
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
this.markUnauthorized(token, this.hasOwnedOfflineSession(token));
|
|
4294
|
+
}
|
|
4295
|
+
shouldDeferLocalExpiry() {
|
|
4296
|
+
if (this.authenticationInvalid) return false;
|
|
4297
|
+
if (typeof navigator != "undefined" && typeof navigator.onLine != "undefined" && !navigator.onLine) return true;
|
|
4298
|
+
return this.status === "offline" || this.status === "unavailable";
|
|
4299
|
+
}
|
|
4300
|
+
deferOfflineExpiry(token) {
|
|
4301
|
+
this.authenticationInvalid = false;
|
|
4302
|
+
this.offlineAuthenticationExpired = true;
|
|
4303
|
+
this.cancelRecovery(errorFromCode(401, "Session expired while offline"));
|
|
4304
|
+
this.rememberOfflineSessionOwner(token);
|
|
4305
|
+
if (this.token !== token) this.token$.next(token);
|
|
4306
|
+
if (this.status === "online") this.setConnectionStatus("offline");
|
|
4307
|
+
}
|
|
4308
|
+
markUnauthorized(token, preservePending = this.hasOwnedOfflineSession(token || this.token)) {
|
|
4194
4309
|
if (token && token !== this.token) return;
|
|
4310
|
+
if (this.adoptStoredToken(token)) return;
|
|
4311
|
+
if (this.authenticationInvalid && this.unauthorizedLogout) return;
|
|
4195
4312
|
this.authenticationInvalid = true;
|
|
4313
|
+
this.offlineAuthenticationExpired = false;
|
|
4196
4314
|
this.cancelRecovery(errorFromCode(401, "Session expired"));
|
|
4197
|
-
if (this.token
|
|
4315
|
+
if (this.token !== null) this.token$.next(null);
|
|
4198
4316
|
this.setConnectionStatus("unauthorized");
|
|
4317
|
+
this.unauthorizedLogout ?? (this.unauthorizedLogout = Promise.resolve().then(() => this.finishUnauthorizedLogout(preservePending)).finally(() => {
|
|
4318
|
+
this.unauthorizedLogout = null;
|
|
4319
|
+
}));
|
|
4320
|
+
}
|
|
4321
|
+
async finishUnauthorizedLogout(preservePending = false) {
|
|
4322
|
+
var _a;
|
|
4323
|
+
if (typeof localStorage != "undefined") {
|
|
4324
|
+
localStorage.removeItem(this.localStorageKey);
|
|
4325
|
+
localStorage.removeItem("datalynk-user");
|
|
4326
|
+
if (!preservePending) localStorage.removeItem(this.offlineSessionOwnerKey);
|
|
4327
|
+
}
|
|
4328
|
+
if (!preservePending) this.offlineSessionOwner = null;
|
|
4329
|
+
for (const slice of this.sliceCache.values()) {
|
|
4330
|
+
try {
|
|
4331
|
+
slice.sync(false);
|
|
4332
|
+
} catch {
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
await this.clearOfflineSessionCache(preservePending);
|
|
4336
|
+
if (typeof caches != "undefined") {
|
|
4337
|
+
try {
|
|
4338
|
+
await Promise.all([caches.delete("datalynk"), caches.delete("api-settings")]);
|
|
4339
|
+
} catch (error) {
|
|
4340
|
+
console.error("Unable to clear Datalynk service worker cache", error);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
if (typeof navigator != "undefined" && ((_a = navigator.serviceWorker) == null ? void 0 : _a.controller)) {
|
|
4344
|
+
navigator.serviceWorker.controller.postMessage({ token: null, clearCache: true });
|
|
4345
|
+
}
|
|
4346
|
+
if (typeof location != "undefined" && typeof location.reload == "function")
|
|
4347
|
+
location.reload();
|
|
4348
|
+
}
|
|
4349
|
+
async clearOfflineSessionCache(preservePending = false) {
|
|
4350
|
+
if (typeof indexedDB == "undefined") return;
|
|
4351
|
+
try {
|
|
4352
|
+
if (this.database) {
|
|
4353
|
+
const db = await this.database.connection;
|
|
4354
|
+
const stores = Array.from(db.objectStoreNames).filter((name) => !(preservePending && name === "pending"));
|
|
4355
|
+
if (!stores.length) return;
|
|
4356
|
+
await new Promise((resolve, reject) => {
|
|
4357
|
+
const tx = db.transaction(stores, "readwrite");
|
|
4358
|
+
stores.forEach((name) => tx.objectStore(name).clear());
|
|
4359
|
+
tx.oncomplete = () => resolve();
|
|
4360
|
+
tx.onerror = () => reject(tx.error);
|
|
4361
|
+
tx.onabort = () => reject(tx.error);
|
|
4362
|
+
});
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
await new Promise((resolve, reject) => {
|
|
4366
|
+
const request = indexedDB.open("datalynk");
|
|
4367
|
+
request.onerror = () => reject(request.error);
|
|
4368
|
+
request.onsuccess = () => {
|
|
4369
|
+
const db = request.result;
|
|
4370
|
+
const stores = Array.from(db.objectStoreNames).filter((name) => !(preservePending && name === "pending"));
|
|
4371
|
+
if (!stores.length) {
|
|
4372
|
+
db.close();
|
|
4373
|
+
resolve();
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
const tx = db.transaction(stores, "readwrite");
|
|
4377
|
+
stores.forEach((name) => tx.objectStore(name).clear());
|
|
4378
|
+
tx.oncomplete = () => {
|
|
4379
|
+
db.close();
|
|
4380
|
+
resolve();
|
|
4381
|
+
};
|
|
4382
|
+
tx.onerror = () => {
|
|
4383
|
+
const error = tx.error;
|
|
4384
|
+
db.close();
|
|
4385
|
+
reject(error);
|
|
4386
|
+
};
|
|
4387
|
+
tx.onabort = () => {
|
|
4388
|
+
const error = tx.error;
|
|
4389
|
+
db.close();
|
|
4390
|
+
reject(error);
|
|
4391
|
+
};
|
|
4392
|
+
};
|
|
4393
|
+
});
|
|
4394
|
+
} catch (error) {
|
|
4395
|
+
console.error("Unable to clear Datalynk offline session cache", error);
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
tokenIdentity(token) {
|
|
4399
|
+
if (!token) return null;
|
|
4400
|
+
try {
|
|
4401
|
+
const payload = decodeJwt(token);
|
|
4402
|
+
const user = (payload == null ? void 0 : payload.uid) ?? (payload == null ? void 0 : payload.sub);
|
|
4403
|
+
return {
|
|
4404
|
+
realm: (payload == null ? void 0 : payload.realm) == null ? void 0 : String(payload.realm),
|
|
4405
|
+
user: user == null ? void 0 : String(user)
|
|
4406
|
+
};
|
|
4407
|
+
} catch {
|
|
4408
|
+
return null;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
readOfflineSessionOwner() {
|
|
4412
|
+
if (this.offlineSessionOwner) return this.offlineSessionOwner;
|
|
4413
|
+
if (typeof localStorage == "undefined") return null;
|
|
4414
|
+
try {
|
|
4415
|
+
const value = localStorage.getItem(this.offlineSessionOwnerKey);
|
|
4416
|
+
if (!value) return null;
|
|
4417
|
+
const owner = JSON.parse(value);
|
|
4418
|
+
if (!owner || typeof owner !== "object") return null;
|
|
4419
|
+
return this.offlineSessionOwner = owner;
|
|
4420
|
+
} catch {
|
|
4421
|
+
return null;
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
ownersMatch(owner, identity2) {
|
|
4425
|
+
if (!identity2) return false;
|
|
4426
|
+
if (owner.realm != null && owner.realm !== identity2.realm) return false;
|
|
4427
|
+
if (owner.user != null && owner.user !== identity2.user) return false;
|
|
4428
|
+
return true;
|
|
4429
|
+
}
|
|
4430
|
+
rememberOfflineSessionOwner(token) {
|
|
4431
|
+
const identity2 = this.tokenIdentity(token);
|
|
4432
|
+
if (!identity2) return;
|
|
4433
|
+
const existing = this.readOfflineSessionOwner();
|
|
4434
|
+
if (existing && !this.ownersMatch(existing, identity2)) return;
|
|
4435
|
+
this.offlineSessionOwner = identity2;
|
|
4436
|
+
if (typeof localStorage != "undefined") {
|
|
4437
|
+
try {
|
|
4438
|
+
localStorage.setItem(this.offlineSessionOwnerKey, JSON.stringify(identity2));
|
|
4439
|
+
} catch {
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
ensureOfflineSessionOwner(token) {
|
|
4444
|
+
const existing = this.readOfflineSessionOwner();
|
|
4445
|
+
const identity2 = this.tokenIdentity(token);
|
|
4446
|
+
if (existing) return this.ownersMatch(existing, identity2);
|
|
4447
|
+
if (!identity2) return true;
|
|
4448
|
+
this.rememberOfflineSessionOwner(token);
|
|
4449
|
+
return true;
|
|
4450
|
+
}
|
|
4451
|
+
hasOwnedOfflineSession(token) {
|
|
4452
|
+
const owner = this.readOfflineSessionOwner();
|
|
4453
|
+
return !!owner && this.ownersMatch(owner, this.tokenIdentity(token));
|
|
4454
|
+
}
|
|
4455
|
+
clearOfflineSessionOwner() {
|
|
4456
|
+
this.offlineSessionOwner = null;
|
|
4457
|
+
if (typeof localStorage != "undefined") {
|
|
4458
|
+
try {
|
|
4459
|
+
localStorage.removeItem(this.offlineSessionOwnerKey);
|
|
4460
|
+
} catch {
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
/** Replay queued writes only after a matching authenticated session is online. */
|
|
4465
|
+
async replayPendingQueue() {
|
|
4466
|
+
if (!this.database || !this.online || this.authenticationInvalid || this.offlineAuthenticationExpired) return;
|
|
4467
|
+
if (this.replayingPending) {
|
|
4468
|
+
this.pendingReplayRequested = true;
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
const table = this.database.table("pending");
|
|
4472
|
+
const owner = this.readOfflineSessionOwner();
|
|
4473
|
+
if (owner && !this.ownersMatch(owner, this.tokenIdentity(this.token))) {
|
|
4474
|
+
if (this.token) console.warn("Datalynk offline queue is quarantined because it belongs to another authenticated user");
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
this.replayingPending = true;
|
|
4478
|
+
try {
|
|
4479
|
+
const keys = await table.getAllKeys();
|
|
4480
|
+
for (const key of keys) {
|
|
4481
|
+
if (!this.online || this.authenticationInvalid || this.offlineAuthenticationExpired) break;
|
|
4482
|
+
if (owner && !this.ownersMatch(owner, this.tokenIdentity(this.token))) break;
|
|
4483
|
+
const request = await table.get(key);
|
|
4484
|
+
if (request == null) continue;
|
|
4485
|
+
try {
|
|
4486
|
+
await this.request(request);
|
|
4487
|
+
await table.delete(key);
|
|
4488
|
+
} catch (error) {
|
|
4489
|
+
break;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
if (await table.count() === 0) this.clearOfflineSessionOwner();
|
|
4493
|
+
} finally {
|
|
4494
|
+
this.replayingPending = false;
|
|
4495
|
+
if (this.pendingReplayRequested) {
|
|
4496
|
+
this.pendingReplayRequested = false;
|
|
4497
|
+
if (this.online && !this.authenticationInvalid && !this.offlineAuthenticationExpired)
|
|
4498
|
+
void this.replayPendingQueue();
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4199
4501
|
}
|
|
4200
4502
|
setConnectionStatus(status) {
|
|
4201
4503
|
if (this.status !== status) this.status$.next(status);
|
|
4202
4504
|
const online = status === "online";
|
|
4203
4505
|
if (this.online !== online) this.online$.next(online);
|
|
4204
4506
|
}
|
|
4205
|
-
offlineBanner() {
|
|
4206
|
-
if (this.options.offlineBanner === false || typeof document == "undefined") return;
|
|
4207
|
-
if (this.status === "online") {
|
|
4208
|
-
removeBanner("datalynk-offline-banner");
|
|
4209
|
-
} else {
|
|
4210
|
-
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";
|
|
4211
|
-
createBanner(message, {
|
|
4212
|
-
id: "datalynk-offline-banner",
|
|
4213
|
-
position: this.options.offlineBanner === "top" ? "top" : "bottom"
|
|
4214
|
-
});
|
|
4215
|
-
}
|
|
4216
|
-
}
|
|
4217
4507
|
startHeartbeat() {
|
|
4218
4508
|
this.stopHeartbeat();
|
|
4219
4509
|
this.heartbeat.interval = setInterval(() => this.checkConnection(), this.heartbeat.timeout / 2);
|
|
@@ -4323,6 +4613,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4323
4613
|
if (this.offline && typeof navigator != "undefined") {
|
|
4324
4614
|
if (this.status === "unauthorized") return Promise.reject(errorFromCode(401, "Session expired"));
|
|
4325
4615
|
if (options.offline) {
|
|
4616
|
+
if (!this.ensureOfflineSessionOwner(this.token))
|
|
4617
|
+
return Promise.reject(errorFromCode(409, "Offline queue belongs to another authenticated user"));
|
|
4326
4618
|
(_b = (_a = this.database) == null ? void 0 : _a.table("pending")) == null ? void 0 : _b.add(data, key);
|
|
4327
4619
|
return Promise.resolve();
|
|
4328
4620
|
}
|