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