@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/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) => localStorage.setItem("datalynk-user", JSON.stringify(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 (((_b = this.api.jwtPayload) == null ? void 0 : _b.realm) != spoke) {
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.0";
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.token$.pipe(distinctUntilChanged()).subscribe((token) => this.scheduleTokenExpiry(token));
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(async (online) => {
3870
- var _a2;
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.authenticationInvalid = true;
3976
- this.token$.next(null);
3977
- this.setConnectionStatus("unauthorized");
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
- this.setConnectionStatus(typeof navigator == "undefined" || navigator.onLine ? "online" : "offline");
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.markUnauthorized(token);
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 < 500 && resp.status !== 401 && !this.isMysqlError(error))
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
- /** Only server-response failures own global recovery; auth and ordinary 4xx errors do not. */
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
- return status >= 500 || this.isMysqlError(error);
4091
+ if (this.isGatewayUnavailableStatus(status)) return true;
4092
+ return this.isSqlStateError(error);
4054
4093
  }
4055
- isMysqlError(error) {
4056
- const message = String((error == null ? void 0 : error.error) ?? (error == null ? void 0 : error.message) ?? "");
4057
- return /\bSQLSTATE\[[A-Z0-9]+\]/i.test(message);
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.markUnauthorized(this.token);
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.ok ? "online" : "unavailable");
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
- return this.markUnauthorized(token);
4272
+ if (!this.adoptStoredToken(token)) this.expireToken(token);
4273
+ return;
4184
4274
  }
4185
4275
  const delay = expiresAt - Date.now();
4186
- if (delay <= 0) return this.markUnauthorized(token);
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.markUnauthorized(token);
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
- markUnauthorized(token) {
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 != null) this.token$.next(null);
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
  }