dexie-cloud-addon 4.0.1-beta.46 → 4.0.1-beta.47

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.
Files changed (40) hide show
  1. package/dist/modern/dexie-cloud-addon.js +471 -313
  2. package/dist/modern/dexie-cloud-addon.js.map +1 -1
  3. package/dist/modern/dexie-cloud-addon.min.js +1 -1
  4. package/dist/modern/dexie-cloud-addon.min.js.map +1 -1
  5. package/dist/modern/service-worker.js +455 -245
  6. package/dist/modern/service-worker.js.map +1 -1
  7. package/dist/modern/service-worker.min.js +1 -1
  8. package/dist/modern/service-worker.min.js.map +1 -1
  9. package/dist/types/DexieCloudAPI.d.ts +3 -0
  10. package/dist/types/DexieCloudOptions.d.ts +1 -0
  11. package/dist/types/InvalidLicenseError.d.ts +5 -0
  12. package/dist/types/authentication/TokenErrorResponseError.d.ts +10 -0
  13. package/dist/types/authentication/authenticate.d.ts +3 -3
  14. package/dist/types/authentication/interactWithUser.d.ts +3 -0
  15. package/dist/types/authentication/logout.d.ts +5 -0
  16. package/dist/types/authentication/waitUntil.d.ts +3 -0
  17. package/dist/types/currentUserEmitter.d.ts +1 -1
  18. package/dist/types/db/entities/UserLogin.d.ts +6 -0
  19. package/dist/types/default-ui/LoginDialog.d.ts +2 -5
  20. package/dist/types/dexie-cloud-client.d.ts +2 -0
  21. package/dist/types/helpers/resolveText.d.ts +14 -0
  22. package/dist/types/isEagerSyncDisabled.d.ts +2 -0
  23. package/dist/types/middlewares/createMutationTrackingMiddleware.d.ts +1 -1
  24. package/dist/types/prodLog.d.ts +9 -0
  25. package/dist/types/sync/performGuardedJob.d.ts +2 -4
  26. package/dist/types/sync/ratelimit.d.ts +3 -0
  27. package/dist/types/sync/sync.d.ts +0 -1
  28. package/dist/types/types/DXCAlert.d.ts +1 -1
  29. package/dist/types/types/DXCUserInteraction.d.ts +40 -2
  30. package/dist/types/types/SyncState.d.ts +1 -0
  31. package/dist/umd/dexie-cloud-addon.js +470 -312
  32. package/dist/umd/dexie-cloud-addon.js.map +1 -1
  33. package/dist/umd/dexie-cloud-addon.min.js +1 -1
  34. package/dist/umd/dexie-cloud-addon.min.js.map +1 -1
  35. package/dist/umd/service-worker.js +453 -243
  36. package/dist/umd/service-worker.js.map +1 -1
  37. package/dist/umd/service-worker.min.js +1 -1
  38. package/dist/umd/service-worker.min.js.map +1 -1
  39. package/package.json +7 -6
  40. package/LICENSE +0 -202
@@ -1,6 +1,6 @@
1
1
  import Dexie, { cmp, liveQuery } from 'dexie';
2
- import { Observable, BehaviorSubject, Subject, of, fromEvent, merge, Subscription, from, throwError, combineLatest, map as map$1, share, timer } from 'rxjs';
3
- import { filter, take, switchMap, delay, distinctUntilChanged, map, tap, catchError, timeout, debounceTime, startWith, skip } from 'rxjs/operators';
2
+ import { Observable, BehaviorSubject, Subject, firstValueFrom, from, filter as filter$1, of, fromEvent, merge, Subscription, throwError, combineLatest, map as map$1, share, timer } from 'rxjs';
3
+ import { filter, take, switchMap, delay, distinctUntilChanged, map, tap, catchError, debounceTime, startWith, skip } from 'rxjs/operators';
4
4
 
5
5
  /******************************************************************************
6
6
  Copyright (c) Microsoft Corporation.
@@ -238,6 +238,7 @@ function registerPeriodicSyncEvent(db) {
238
238
 
239
239
  function triggerSync(db, purpose) {
240
240
  if (db.cloud.usingServiceWorker) {
241
+ console.debug('registering sync event');
241
242
  registerSyncEvent(db, purpose);
242
243
  }
243
244
  else {
@@ -739,14 +740,24 @@ function getTablesToSyncify(db, syncState) {
739
740
  return tablesToSyncify;
740
741
  }
741
742
 
743
+ class TokenErrorResponseError extends Error {
744
+ constructor({ title, message, messageCode, messageParams, }) {
745
+ super(message);
746
+ this.name = 'TokenErrorResponseError';
747
+ this.title = title;
748
+ this.messageCode = messageCode;
749
+ this.messageParams = messageParams;
750
+ }
751
+ }
752
+
742
753
  function interactWithUser(userInteraction, req) {
743
754
  return new Promise((resolve, reject) => {
744
- const interactionProps = Object.assign(Object.assign({}, req), { onSubmit: (res) => {
755
+ const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
745
756
  userInteraction.next(undefined);
746
757
  resolve(res);
747
758
  }, onCancel: () => {
748
759
  userInteraction.next(undefined);
749
- reject(new Dexie.AbortError("User cancelled"));
760
+ reject(new Dexie.AbortError('User cancelled'));
750
761
  } });
751
762
  userInteraction.next(interactionProps);
752
763
  // Start subscribing for external updates to db.cloud.userInteraction, and if so, cancel this request.
@@ -765,7 +776,9 @@ function alertUser(userInteraction, title, ...alerts) {
765
776
  type: 'message-alert',
766
777
  title,
767
778
  alerts,
768
- fields: {}
779
+ fields: {},
780
+ submitLabel: 'OK',
781
+ cancelLabel: null,
769
782
  });
770
783
  }
771
784
  function promptForEmail(userInteraction, title, emailHint) {
@@ -824,22 +837,48 @@ function promptForOTP(userInteraction, email, alert) {
824
837
  return otp;
825
838
  });
826
839
  }
840
+ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
841
+ return __awaiter(this, void 0, void 0, function* () {
842
+ const alerts = [
843
+ {
844
+ type: 'warning',
845
+ messageCode: 'LOGOUT_CONFIRMATION',
846
+ message: `{numUnsyncedChanges} unsynced changes will get lost!
847
+ Logout anyway?`,
848
+ messageParams: {
849
+ currentUserId,
850
+ numUnsyncedChanges: numUnsyncedChanges.toString(),
851
+ }
852
+ },
853
+ ];
854
+ return yield interactWithUser(userInteraction, {
855
+ type: 'logout-confirmation',
856
+ title: 'Confirm Logout',
857
+ alerts,
858
+ fields: {},
859
+ submitLabel: 'Confirm logout',
860
+ cancelLabel: 'Cancel'
861
+ })
862
+ .then(() => true)
863
+ .catch(() => false);
864
+ });
865
+ }
827
866
 
828
867
  function loadAccessToken(db) {
829
- var _a, _b;
868
+ var _a, _b, _c;
830
869
  return __awaiter(this, void 0, void 0, function* () {
831
870
  const currentUser = yield db.getCurrentUser();
832
871
  const { accessToken, accessTokenExpiration, refreshToken, refreshTokenExpiration, claims, } = currentUser;
833
872
  if (!accessToken)
834
- return;
873
+ return null;
835
874
  const expTime = (_a = accessTokenExpiration === null || accessTokenExpiration === void 0 ? void 0 : accessTokenExpiration.getTime()) !== null && _a !== void 0 ? _a : Infinity;
836
- if (expTime > Date.now()) {
837
- return accessToken;
875
+ if (expTime > Date.now() && (((_b = currentUser.license) === null || _b === void 0 ? void 0 : _b.status) || 'ok') === 'ok') {
876
+ return currentUser;
838
877
  }
839
878
  if (!refreshToken) {
840
879
  throw new Error(`Refresh token missing`);
841
880
  }
842
- const refreshExpTime = (_b = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _b !== void 0 ? _b : Infinity;
881
+ const refreshExpTime = (_c = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _c !== void 0 ? _c : Infinity;
843
882
  if (refreshExpTime <= Date.now()) {
844
883
  throw new Error(`Refresh token has expired`);
845
884
  }
@@ -847,8 +886,10 @@ function loadAccessToken(db) {
847
886
  yield db.table('$logins').update(claims.sub, {
848
887
  accessToken: refreshedLogin.accessToken,
849
888
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
889
+ claims: refreshedLogin.claims,
890
+ license: refreshedLogin.license,
850
891
  });
851
- return refreshedLogin.accessToken;
892
+ return refreshedLogin;
852
893
  });
853
894
  }
854
895
  function authenticate(url, context, fetchToken, userInteraction, hints) {
@@ -896,10 +937,24 @@ function refreshAccessToken(url, login) {
896
937
  if (res.status !== 200)
897
938
  throw new Error(`RefreshToken: Status ${res.status} from ${url}/token`);
898
939
  const response = yield res.json();
940
+ if (response.type === 'error') {
941
+ throw new TokenErrorResponseError(response);
942
+ }
899
943
  login.accessToken = response.accessToken;
900
944
  login.accessTokenExpiration = response.accessTokenExpiration
901
945
  ? new Date(response.accessTokenExpiration)
902
946
  : undefined;
947
+ login.claims = response.claims;
948
+ login.license = {
949
+ type: response.userType,
950
+ status: response.claims.license || 'ok',
951
+ };
952
+ if (response.evalDaysLeft != null) {
953
+ login.license.evalDaysLeft = response.evalDaysLeft;
954
+ }
955
+ if (response.userValidUntil != null) {
956
+ login.license.validUntil = new Date(response.userValidUntil);
957
+ }
903
958
  return login;
904
959
  });
905
960
  }
@@ -931,8 +986,15 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
931
986
  public_key: publicKeyPEM,
932
987
  hints,
933
988
  });
989
+ if (response2.type === 'error') {
990
+ throw new TokenErrorResponseError(response2);
991
+ }
934
992
  if (response2.type !== 'tokens')
935
993
  throw new Error(`Unexpected response type from token endpoint: ${response2.type}`);
994
+ /*const licenseStatus = response2.claims.license || 'ok';
995
+ if (licenseStatus !== 'ok') {
996
+ throw new InvalidLicenseError(licenseStatus);
997
+ }*/
936
998
  context.accessToken = response2.accessToken;
937
999
  context.accessTokenExpiration = new Date(response2.accessTokenExpiration);
938
1000
  context.refreshToken = response2.refreshToken;
@@ -943,6 +1005,16 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
943
1005
  context.email = response2.claims.email;
944
1006
  context.name = response2.claims.name;
945
1007
  context.claims = response2.claims;
1008
+ context.license = {
1009
+ type: response2.userType,
1010
+ status: response2.claims.license || 'ok',
1011
+ };
1012
+ if (response2.evalDaysLeft != null) {
1013
+ context.license.evalDaysLeft = response2.evalDaysLeft;
1014
+ }
1015
+ if (response2.userValidUntil != null) {
1016
+ context.license.validUntil = new Date(response2.userValidUntil);
1017
+ }
946
1018
  if (response2.alerts && response2.alerts.length > 0) {
947
1019
  yield interactWithUser(userInteraction, {
948
1020
  type: 'message-alert',
@@ -954,12 +1026,36 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
954
1026
  return context;
955
1027
  }
956
1028
  catch (error) {
957
- yield alertUser(userInteraction, 'Authentication Failed', {
958
- type: 'error',
959
- messageCode: 'GENERIC_ERROR',
960
- message: `We're having a problem authenticating right now.`,
961
- messageParams: {},
962
- }).catch(() => { });
1029
+ if (error instanceof TokenErrorResponseError) {
1030
+ yield alertUser(userInteraction, error.title, {
1031
+ type: 'error',
1032
+ messageCode: error.messageCode,
1033
+ message: error.message,
1034
+ messageParams: {},
1035
+ });
1036
+ throw error;
1037
+ }
1038
+ let message = `We're having a problem authenticating right now.`;
1039
+ console.error(`Error authenticating`, error);
1040
+ if (error instanceof TypeError) {
1041
+ const isOffline = typeof navigator !== undefined && !navigator.onLine;
1042
+ if (isOffline) {
1043
+ message = `You seem to be offline. Please connect to the internet and try again.`;
1044
+ }
1045
+ else if (Dexie.debug || (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1'))) {
1046
+ // The audience is most likely the developer. Suggest to whitelist the localhost origin:
1047
+ message = `Could not connect to server. Please verify that your origin '${location.origin}' is whitelisted using \`npx dexie-cloud whitelist\``;
1048
+ }
1049
+ else {
1050
+ message = `Could not connect to server. Please verify the connection.`;
1051
+ }
1052
+ yield alertUser(userInteraction, 'Authentication Failed', {
1053
+ type: 'error',
1054
+ messageCode: 'GENERIC_ERROR',
1055
+ message,
1056
+ messageParams: {},
1057
+ }).catch(() => { });
1058
+ }
963
1059
  throw error;
964
1060
  }
965
1061
  });
@@ -1582,6 +1678,40 @@ function cloneChange(change, rewriteValues) {
1582
1678
  : change.muts.map((m) => (Object.assign(Object.assign({}, m), { keys: m.keys.slice() }))) });
1583
1679
  }
1584
1680
 
1681
+ // If we get Ratelimit-Limit and Ratelimit-Remaining where Ratelimit-Remaining is below
1682
+ // (Ratelimit-Limit / 2), we should delay the next sync by (Ratelimit-Reset / Ratelimit-Remaining)
1683
+ // seconds (given that there is a Ratelimit-Reset header).
1684
+ let syncRatelimitDelays = new WeakMap();
1685
+ function checkSyncRateLimitDelay(db) {
1686
+ var _a, _b;
1687
+ return __awaiter(this, void 0, void 0, function* () {
1688
+ const delatMilliseconds = ((_b = (_a = syncRatelimitDelays.get(db)) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : 0) - Date.now();
1689
+ if (delatMilliseconds > 0) {
1690
+ console.debug(`Stalling sync request ${delatMilliseconds} ms to spare ratelimits`);
1691
+ yield new Promise(resolve => setTimeout(resolve, delatMilliseconds));
1692
+ }
1693
+ });
1694
+ }
1695
+ function updateSyncRateLimitDelays(db, res) {
1696
+ const limit = res.headers.get('Ratelimit-Limit');
1697
+ const remaining = res.headers.get('Ratelimit-Remaining');
1698
+ const reset = res.headers.get('Ratelimit-Reset');
1699
+ if (limit && remaining && reset) {
1700
+ const limitNum = Number(limit);
1701
+ const remainingNum = Math.max(0, Number(remaining));
1702
+ const willResetInSeconds = Number(reset);
1703
+ if (remainingNum < limitNum / 2) {
1704
+ const delay = Math.ceil(willResetInSeconds / (remainingNum + 1));
1705
+ syncRatelimitDelays.set(db, new Date(Date.now() + delay * 1000));
1706
+ console.debug(`Sync ratelimit delay set to ${delay} seconds`);
1707
+ }
1708
+ else {
1709
+ syncRatelimitDelays.delete(db);
1710
+ console.debug(`Sync ratelimit delay cleared`);
1711
+ }
1712
+ }
1713
+ }
1714
+
1585
1715
  //import {BisonWebStreamReader} from "dreambase-library/dist/typeson-simplified/BisonWebStreamReader";
1586
1716
  function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser) {
1587
1717
  return __awaiter(this, void 0, void 0, function* () {
@@ -1590,9 +1720,20 @@ function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, c
1590
1720
  //
1591
1721
  const headers = {
1592
1722
  Accept: 'application/json, application/x-bison, application/x-bison-stream',
1593
- 'Content-Type': 'application/tson'
1723
+ 'Content-Type': 'application/tson',
1594
1724
  };
1595
- const accessToken = yield loadAccessToken(db);
1725
+ const updatedUser = yield loadAccessToken(db);
1726
+ /*
1727
+ if (updatedUser?.license && changes.length > 0) {
1728
+ if (updatedUser.license.status === 'expired') {
1729
+ throw new Error(`License has expired`);
1730
+ }
1731
+ if (updatedUser.license.status === 'deactivated') {
1732
+ throw new Error(`License deactivated`);
1733
+ }
1734
+ }
1735
+ */
1736
+ const accessToken = updatedUser === null || updatedUser === void 0 ? void 0 : updatedUser.accessToken;
1596
1737
  if (accessToken) {
1597
1738
  headers.Authorization = `Bearer ${accessToken}`;
1598
1739
  }
@@ -1601,27 +1742,31 @@ function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, c
1601
1742
  dbID: syncState === null || syncState === void 0 ? void 0 : syncState.remoteDbId,
1602
1743
  clientIdentity,
1603
1744
  schema: schema || {},
1604
- lastPull: syncState ? {
1605
- serverRevision: syncState.serverRevision,
1606
- realms: syncState.realms,
1607
- inviteRealms: syncState.inviteRealms
1608
- } : undefined,
1745
+ lastPull: syncState
1746
+ ? {
1747
+ serverRevision: syncState.serverRevision,
1748
+ realms: syncState.realms,
1749
+ inviteRealms: syncState.inviteRealms,
1750
+ }
1751
+ : undefined,
1609
1752
  baseRevs,
1610
- changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes)
1753
+ changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
1611
1754
  };
1612
- console.debug("Sync request", syncRequest);
1755
+ console.debug('Sync request', syncRequest);
1613
1756
  db.syncStateChangedEvent.next({
1614
1757
  phase: 'pushing',
1615
1758
  });
1616
1759
  const res = yield fetch(`${databaseUrl}/sync`, {
1617
1760
  method: 'post',
1618
1761
  headers,
1619
- body: TSON.stringify(syncRequest)
1762
+ credentials: 'include',
1763
+ body: TSON.stringify(syncRequest),
1620
1764
  });
1621
1765
  //const contentLength = Number(res.headers.get('content-length'));
1622
1766
  db.syncStateChangedEvent.next({
1623
- phase: 'pulling'
1767
+ phase: 'pulling',
1624
1768
  });
1769
+ updateSyncRateLimitDelays(db, res);
1625
1770
  if (!res.ok) {
1626
1771
  throw new HttpError(res);
1627
1772
  }
@@ -1823,12 +1968,13 @@ const CURRENT_SYNC_WORKER = 'currentSyncWorker';
1823
1968
  function sync(db, options, schema, syncOptions) {
1824
1969
  return _sync
1825
1970
  .apply(this, arguments)
1826
- .then(() => {
1971
+ .then((result) => {
1827
1972
  if (!(syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)) { // && syncOptions?.purpose !== 'push') {
1828
1973
  db.syncStateChangedEvent.next({
1829
1974
  phase: 'in-sync',
1830
1975
  });
1831
1976
  }
1977
+ return result;
1832
1978
  })
1833
1979
  .catch((error) => __awaiter(this, void 0, void 0, function* () {
1834
1980
  if (syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)
@@ -2043,6 +2189,7 @@ function _sync(db, options, schema, { isInitialSync, cancelToken, justCheckIfNee
2043
2189
  }));
2044
2190
  if (!done) {
2045
2191
  console.debug('MORE SYNC NEEDED. Go for it again!');
2192
+ yield checkSyncRateLimitDelay(db);
2046
2193
  return yield _sync(db, options, schema, { isInitialSync, cancelToken });
2047
2194
  }
2048
2195
  console.debug('SYNC DONE', { isInitialSync });
@@ -2179,6 +2326,8 @@ function MessagesFromServerConsumer(db) {
2179
2326
  yield db.table('$logins').update(user.userId, {
2180
2327
  accessToken: refreshedLogin.accessToken,
2181
2328
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
2329
+ claims: refreshedLogin.claims,
2330
+ license: refreshedLogin.license,
2182
2331
  });
2183
2332
  // Updating $logins will trigger emission of db.cloud.currentUser observable, which
2184
2333
  // in turn will lead to that connectWebSocket.ts will reconnect the socket with the
@@ -2449,6 +2598,61 @@ class AuthPersistedContext {
2449
2598
  }
2450
2599
  }
2451
2600
 
2601
+ function waitUntil(o, // Works with Dexie's liveQuery observables if we'd need that
2602
+ predicate) {
2603
+ return firstValueFrom(from(o).pipe(filter$1(predicate)));
2604
+ }
2605
+
2606
+ function logout(db) {
2607
+ return __awaiter(this, void 0, void 0, function* () {
2608
+ const numUnsyncedChanges = yield _logout(db);
2609
+ if (numUnsyncedChanges) {
2610
+ if (yield confirmLogout(db.cloud.userInteraction, db.cloud.currentUserId, numUnsyncedChanges)) {
2611
+ yield _logout(db, { deleteUnsyncedData: true });
2612
+ }
2613
+ else {
2614
+ throw new Error(`User cancelled logout due to unsynced changes`);
2615
+ }
2616
+ }
2617
+ });
2618
+ }
2619
+ function _logout(db, { deleteUnsyncedData = false } = {}) {
2620
+ return __awaiter(this, void 0, void 0, function* () {
2621
+ // Clear the database without emptying configuration options.
2622
+ const [numUnsynced, loggedOut] = yield db.dx.transaction('rw', db.dx.tables, (tx) => __awaiter(this, void 0, void 0, function* () {
2623
+ // @ts-ignore
2624
+ const idbtrans = tx.idbtrans;
2625
+ idbtrans.disableChangeTracking = true;
2626
+ idbtrans.disableAccessControl = true;
2627
+ const mutationTables = tx.storeNames.filter((tableName) => tableName.endsWith('_mutations'));
2628
+ // Count unsynced changes
2629
+ const unsyncCounts = yield Promise.all(mutationTables.map((mutationTable) => tx.table(mutationTable).count()));
2630
+ const sumUnSynced = unsyncCounts.reduce((a, b) => a + b, 0);
2631
+ if (sumUnSynced > 0 && !deleteUnsyncedData) {
2632
+ // Let caller ask user if they want to delete unsynced data.
2633
+ return [sumUnSynced, false];
2634
+ }
2635
+ // Either there are no unsynched changes, or caller provided flag deleteUnsynchedData = true.
2636
+ // Clear all tables except $jobs and $syncState (except the persisted sync state which is
2637
+ // also cleared because we're going to rebuild it using a fresh sync).
2638
+ db.$syncState.delete('syncState');
2639
+ for (const table of db.dx.tables) {
2640
+ if (table.name !== '$jobs' && table.name !== '$syncState') {
2641
+ table.clear();
2642
+ }
2643
+ }
2644
+ return [sumUnSynced, true];
2645
+ }));
2646
+ if (loggedOut) {
2647
+ // Wait for currentUser observable to emit UNAUTHORIZED_USER
2648
+ yield waitUntil(db.cloud.currentUser, (user) => user.userId === UNAUTHORIZED_USER.userId);
2649
+ // Then perform an initial sync
2650
+ yield db.cloud.sync({ purpose: 'pull', wait: true });
2651
+ }
2652
+ return numUnsynced;
2653
+ });
2654
+ }
2655
+
2452
2656
  function otpFetchTokenCallback(db) {
2453
2657
  const { userInteraction } = db.cloud;
2454
2658
  return function otpAuthenticate({ public_key, hints }) {
@@ -2492,8 +2696,9 @@ function otpFetchTokenCallback(db) {
2492
2696
  throw new HttpError(res1, errMsg);
2493
2697
  }
2494
2698
  const response = yield res1.json();
2495
- if (response.type === 'tokens') {
2699
+ if (response.type === 'tokens' || response.type === 'error') {
2496
2700
  // Demo user request can get a "tokens" response right away
2701
+ // Error can also be returned right away.
2497
2702
  return response;
2498
2703
  }
2499
2704
  else if (tokenRequest.grant_type === 'otp') {
@@ -2525,12 +2730,6 @@ function otpFetchTokenCallback(db) {
2525
2730
  }
2526
2731
  if (res2.status !== 200) {
2527
2732
  const errMsg = yield res2.text();
2528
- yield alertUser(userInteraction, "OTP Authentication Failed", {
2529
- type: 'error',
2530
- messageCode: 'GENERIC_ERROR',
2531
- message: errMsg,
2532
- messageParams: {}
2533
- }).catch(() => { });
2534
2733
  throw new HttpError(res2, errMsg);
2535
2734
  }
2536
2735
  const response2 = yield res2.json();
@@ -2543,6 +2742,18 @@ function otpFetchTokenCallback(db) {
2543
2742
  };
2544
2743
  }
2545
2744
 
2745
+ /** A way to log to console in production without terser stripping out
2746
+ * it from the release bundle.
2747
+ * This should be used very rarely and only in places where it's
2748
+ * absolutely necessary to log something in production.
2749
+ *
2750
+ * @param level
2751
+ * @param args
2752
+ */
2753
+ function prodLog(level, ...args) {
2754
+ globalThis["con" + "sole"][level](...args);
2755
+ }
2756
+
2546
2757
  /** This function changes or sets the current user as requested.
2547
2758
  *
2548
2759
  * Use cases:
@@ -2569,85 +2780,73 @@ function setCurrentUser(db, user) {
2569
2780
  }));
2570
2781
  user.isLoggedIn = true;
2571
2782
  user.lastLogin = new Date();
2572
- yield user.save();
2573
- console.debug('Saved new user', user.email);
2574
- }));
2575
- yield new Promise((resolve) => {
2576
- if (db.cloud.currentUserId === user.userId) {
2577
- resolve(null);
2783
+ try {
2784
+ yield user.save();
2578
2785
  }
2579
- else {
2580
- const subscription = db.cloud.currentUser.subscribe((currentUser) => {
2581
- if (currentUser.userId === user.userId) {
2582
- subscription.unsubscribe();
2583
- resolve(null);
2786
+ catch (e) {
2787
+ try {
2788
+ if (e.name === 'DataCloneError') {
2789
+ // We've seen this buggy behavior in some browsers and in case it happens
2790
+ // again we really need to collect the details to understand what's going on.
2791
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2792
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2793
+ prodLog('debug', `Login context:`, user);
2794
+ prodLog('debug', `Login context JSON:`, JSON.stringify(user));
2584
2795
  }
2585
- });
2796
+ }
2797
+ catch (_a) { }
2798
+ throw e;
2586
2799
  }
2587
- });
2588
- // TANKAR!!!!
2589
- // V: Service workern kommer inte ha tillgång till currentUserObservable om den inte istället härrör från ett liveQuery.
2590
- // V: Samma med andra windows.
2591
- // V: Så kanske göra om den till att häröra från liveQuery som läser $logins.orderBy('lastLogin').last().
2592
- // V: Då bara vara medveten om:
2593
- // V: En sån observable börjar hämta data vid första subscribe
2594
- // V: Vi har inget "inital value" men kan emulera det till att vara ANONYMOUS_USER
2595
- // V: Om requireAuth är true, så borde db.on(ready) hålla databasen stängd för alla utom denna observable.
2596
- // V: Om inte så behöver den inte blocka.
2597
- // Andra tankar:
2598
- // * Man kan inte byta användare när man är offline. Skulle gå att flytta realms till undanstuff-tabell vid user-change.
2599
- // men troligen inte värt det.
2600
- // * Istället: sälj inte inte switch-user funktionalitet utan tala enbart om inloggat vs icke inloggat läge.
2601
- // * populate $logins med ANONYMOUS så att en påbörjad inloggning inte räknas, alternativt ha en boolean prop!
2602
- // Kanske bäst ha en boolean prop!
2603
- // * Alternativ switch-user funktionalitet:
2604
- // * DBCore gömmer data från realms man inte har tillgång till.
2605
- // * Cursor impl behövs också då.
2606
- // * Då blir det snabba user switch.
2607
- // * claims-settet som skickas till servern blir summan av alla claims. Då måste servern stödja multipla tokens eller
2608
- // att ens token är ett samlad.
2800
+ console.debug('Saved new user', user.email);
2801
+ }));
2802
+ yield waitUntil(db.cloud.currentUser, (currentUser) => currentUser.userId === user.userId);
2609
2803
  });
2610
2804
  }
2611
2805
 
2612
2806
  function login(db, hints) {
2807
+ var _a;
2613
2808
  return __awaiter(this, void 0, void 0, function* () {
2614
2809
  const currentUser = yield db.getCurrentUser();
2615
- if (currentUser.isLoggedIn) {
2616
- if (hints) {
2617
- if (hints.email && db.cloud.currentUser.value.email !== hints.email) {
2618
- throw new Error(`Must logout before changing user`);
2619
- }
2620
- if (hints.userId && db.cloud.currentUserId !== hints.userId) {
2621
- throw new Error(`Must logout before changing user`);
2622
- }
2810
+ const origUserId = currentUser.userId;
2811
+ if (currentUser.isLoggedIn && (!hints || (!hints.email && !hints.userId))) {
2812
+ const licenseStatus = ((_a = currentUser.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
2813
+ if (licenseStatus === 'ok' && currentUser.accessToken && (!currentUser.accessTokenExpiration || currentUser.accessTokenExpiration.getTime() > Date.now())) {
2814
+ // Already authenticated according to given hints. And license is valid.
2815
+ return false;
2623
2816
  }
2624
- // Already authenticated according to given hints.
2625
- return false;
2817
+ if (currentUser.refreshToken && (!currentUser.refreshTokenExpiration || currentUser.refreshTokenExpiration.getTime() > Date.now())) {
2818
+ // Refresh the token
2819
+ yield loadAccessToken(db);
2820
+ return false;
2821
+ }
2822
+ // No refresh token - must re-authenticate:
2626
2823
  }
2627
2824
  const context = new AuthPersistedContext(db, {
2628
2825
  claims: {},
2629
2826
  lastLogin: new Date(0),
2630
2827
  });
2631
2828
  yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
2632
- try {
2633
- yield context.save();
2829
+ if (origUserId !== UNAUTHORIZED_USER.userId && context.userId !== origUserId) {
2830
+ // User was logged in before, but now logged in as another user.
2831
+ yield logout(db);
2634
2832
  }
2635
- catch (e) {
2636
- try {
2637
- if (e.name === 'DataCloneError') {
2638
- console.debug(`Login context property names:`, Object.keys(context));
2639
- console.debug(`Login context:`, context);
2640
- console.debug(`Login context JSON:`, JSON.stringify(context));
2641
- }
2833
+ /*try {
2834
+ await context.save();
2835
+ } catch (e) {
2836
+ try {
2837
+ if (e.name === 'DataCloneError') {
2838
+ console.debug(`Login context property names:`, Object.keys(context));
2839
+ console.debug(`Login context:`, context);
2840
+ console.debug(`Login context JSON:`, JSON.stringify(context));
2642
2841
  }
2643
- catch (_a) { }
2644
- throw e;
2645
- }
2842
+ } catch {}
2843
+ throw e;
2844
+ }*/
2646
2845
  yield setCurrentUser(db, context);
2647
2846
  // Make sure to resync as the new login will be authorized
2648
2847
  // for new realms.
2649
2848
  triggerSync(db, "pull");
2650
- return true;
2849
+ return context.userId !== origUserId;
2651
2850
  });
2652
2851
  }
2653
2852
 
@@ -2976,6 +3175,13 @@ function writeLock(fn, prop) {
2976
3175
 
2977
3176
  const outstandingTransactions = new BehaviorSubject(new Set());
2978
3177
 
3178
+ function isEagerSyncDisabled(db) {
3179
+ var _a, _b, _c, _d;
3180
+ return (((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.disableEagerSync) ||
3181
+ ((_c = (_b = db.cloud.currentUser.value) === null || _b === void 0 ? void 0 : _b.license) === null || _c === void 0 ? void 0 : _c.status) !== 'ok' ||
3182
+ !((_d = db.cloud.options) === null || _d === void 0 ? void 0 : _d.databaseUrl));
3183
+ }
3184
+
2979
3185
  /** Tracks all mutations in the same transaction as the mutations -
2980
3186
  * so it is guaranteed that no mutation goes untracked - and if transaction
2981
3187
  * aborts, the mutations won't be tracked.
@@ -2984,7 +3190,7 @@ const outstandingTransactions = new BehaviorSubject(new Set());
2984
3190
  * changes to server and cleanup the tracked mutations once the server has
2985
3191
  * ackowledged that it got them.
2986
3192
  */
2987
- function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3193
+ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
2988
3194
  return {
2989
3195
  stack: 'dbcore',
2990
3196
  name: 'MutationTrackingMiddleware',
@@ -2995,7 +3201,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
2995
3201
  try {
2996
3202
  mutTableMap = new Map(ordinaryTables.map((tbl) => [
2997
3203
  tbl.name,
2998
- core.table(`$${tbl.name}_mutations`)
3204
+ core.table(`$${tbl.name}_mutations`),
2999
3205
  ]));
3000
3206
  }
3001
3207
  catch (_a) {
@@ -3029,15 +3235,9 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3029
3235
  outstandingTransactions.next(outstandingTransactions.value);
3030
3236
  };
3031
3237
  const txComplete = () => {
3032
- var _a;
3033
- if (tx.mutationsAdded && ((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl)) {
3034
- if (db.cloud.usingServiceWorker) {
3035
- console.debug('registering sync event');
3036
- registerSyncEvent(db, "push");
3037
- }
3038
- else {
3039
- db.localSyncEvent.next({ purpose: "push" });
3040
- }
3238
+ if (tx.mutationsAdded &&
3239
+ !isEagerSyncDisabled(db)) {
3240
+ triggerSync(db, 'push');
3041
3241
  }
3042
3242
  removeTransaction();
3043
3243
  };
@@ -3104,7 +3304,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3104
3304
  .query({
3105
3305
  query: { range: req.range, index: schema.primaryKey },
3106
3306
  trans: req.trans,
3107
- values: false
3307
+ values: false,
3108
3308
  })
3109
3309
  // Do a delete request instead, but keep the criteria info for the server to execute
3110
3310
  .then((res) => {
@@ -3112,7 +3312,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3112
3312
  type: 'delete',
3113
3313
  keys: res.result,
3114
3314
  trans: req.trans,
3115
- criteria: { index: null, range: req.range }
3315
+ criteria: { index: null, range: req.range },
3116
3316
  });
3117
3317
  })
3118
3318
  : mutateAndLog(req);
@@ -3120,7 +3320,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3120
3320
  function mutateAndLog(req) {
3121
3321
  const trans = req.trans;
3122
3322
  trans.mutationsAdded = true;
3123
- const { txid, currentUser: { userId } } = trans;
3323
+ const { txid, currentUser: { userId }, } = trans;
3124
3324
  const { type } = req;
3125
3325
  const opNo = ++trans.opCount;
3126
3326
  return table.mutate(req).then((res) => {
@@ -3141,7 +3341,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3141
3341
  keys,
3142
3342
  criteria: req.criteria,
3143
3343
  txid,
3144
- userId
3344
+ userId,
3145
3345
  }
3146
3346
  : req.type === 'add'
3147
3347
  ? {
@@ -3151,7 +3351,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3151
3351
  keys,
3152
3352
  txid,
3153
3353
  userId,
3154
- values
3354
+ values,
3155
3355
  }
3156
3356
  : req.criteria && req.changeSpec
3157
3357
  ? {
@@ -3163,7 +3363,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3163
3363
  criteria: req.criteria,
3164
3364
  changeSpec: req.changeSpec,
3165
3365
  txid,
3166
- userId
3366
+ userId,
3167
3367
  }
3168
3368
  : updates
3169
3369
  ? {
@@ -3174,7 +3374,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3174
3374
  keys: updates.keys,
3175
3375
  changeSpecs: updates.changeSpecs,
3176
3376
  txid,
3177
- userId
3377
+ userId,
3178
3378
  }
3179
3379
  : {
3180
3380
  type: 'upsert',
@@ -3183,7 +3383,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3183
3383
  keys,
3184
3384
  values,
3185
3385
  txid,
3186
- userId
3386
+ userId,
3187
3387
  };
3188
3388
  return keys.length > 0 || ('criteria' in req && req.criteria)
3189
3389
  ? mutsTable
@@ -3193,7 +3393,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3193
3393
  });
3194
3394
  }
3195
3395
  } });
3196
- }
3396
+ },
3197
3397
  };
3198
3398
  }
3199
3399
 
@@ -3552,6 +3752,20 @@ class WSConnection extends Subscription {
3552
3752
  }
3553
3753
  }
3554
3754
 
3755
+ class InvalidLicenseError extends Error {
3756
+ constructor(license) {
3757
+ super(license === 'expired'
3758
+ ? `License expired`
3759
+ : license === 'deactivated'
3760
+ ? `User deactivated`
3761
+ : 'Invalid license');
3762
+ this.name = 'InvalidLicenseError';
3763
+ if (license) {
3764
+ this.license = license;
3765
+ }
3766
+ }
3767
+ }
3768
+
3555
3769
  function sleep$1(ms) {
3556
3770
  return new Promise((resolve) => setTimeout(resolve, ms));
3557
3771
  }
@@ -3585,7 +3799,12 @@ function connectWebSocket(db) {
3585
3799
  function createObservable() {
3586
3800
  return db.cloud.persistedSyncState.pipe(filter((syncState) => syncState === null || syncState === void 0 ? void 0 : syncState.serverRevision), // Don't connect before there's no initial sync performed.
3587
3801
  take(1), // Don't continue waking up whenever syncState change
3588
- switchMap((syncState) => db.cloud.currentUser.pipe(map((userLogin) => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]))), switchMap(([userLogin, syncState]) => {
3802
+ switchMap((syncState) => db.cloud.currentUser.pipe(map((userLogin) => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => {
3803
+ /*if (userLogin.license?.status && userLogin.license.status !== 'ok') {
3804
+ throw new InvalidLicenseError();
3805
+ }*/
3806
+ return userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]));
3807
+ }), switchMap(([userLogin, syncState]) => {
3589
3808
  if ((userLogin === null || userLogin === void 0 ? void 0 : userLogin.isLoggedIn) && !(syncState === null || syncState === void 0 ? void 0 : syncState.realms.includes(userLogin.userId))) {
3590
3809
  // We're in an in-between state when user is logged in but the user's realms are not yet synced.
3591
3810
  // Don't make this change reconnect the websocket just yet. Wait till syncState is updated
@@ -3614,14 +3833,20 @@ function connectWebSocket(db) {
3614
3833
  yield db.table('$logins').update(user.userId, {
3615
3834
  accessToken: refreshedLogin.accessToken,
3616
3835
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
3836
+ claims: refreshedLogin.claims,
3837
+ license: refreshedLogin.license,
3617
3838
  });
3618
3839
  })), switchMap(() => createObservable()));
3619
3840
  }
3620
3841
  else {
3621
- return throwError(error);
3842
+ return throwError(() => error);
3622
3843
  }
3623
3844
  }), catchError((error) => {
3624
3845
  db.cloud.webSocketStatus.next("error");
3846
+ if (error instanceof InvalidLicenseError) {
3847
+ // Don't retry. Just throw and don't try connect again.
3848
+ return throwError(() => error);
3849
+ }
3625
3850
  return from(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
3626
3851
  }));
3627
3852
  }
@@ -3650,97 +3875,12 @@ function isSyncNeeded(db) {
3650
3875
  });
3651
3876
  }
3652
3877
 
3653
- const SECONDS = 1000;
3654
- const MINUTES = 60 * SECONDS;
3655
-
3656
- const myId = randomString$1(16);
3657
-
3658
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
3659
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
3660
- function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
3661
- return __awaiter(this, void 0, void 0, function* () {
3662
- // Start working.
3663
- //
3664
- // Check if someone else is working on this already.
3665
- //
3666
- const jobsTable = db.table(jobsTableName);
3667
- function aquireLock() {
3668
- return __awaiter(this, void 0, void 0, function* () {
3669
- const gotTheLock = yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
3670
- const currentWork = yield jobsTable.get(jobName);
3671
- if (!currentWork) {
3672
- // No one else is working. Let's record that we are.
3673
- yield jobsTable.add({
3674
- nodeId: myId,
3675
- started: new Date(),
3676
- heartbeat: new Date()
3677
- }, jobName);
3678
- return true;
3679
- }
3680
- else if (currentWork.heartbeat.getTime() <
3681
- Date.now() - GUARDED_JOB_TIMEOUT) {
3682
- console.warn(`Latest ${jobName} worker seem to have died.\n`, `The dead job started:`, currentWork.started, `\n`, `Last heart beat was:`, currentWork.heartbeat, '\n', `We're now taking over!`);
3683
- // Now, take over!
3684
- yield jobsTable.put({
3685
- nodeId: myId,
3686
- started: new Date(),
3687
- heartbeat: new Date()
3688
- }, jobName);
3689
- return true;
3690
- }
3691
- return false;
3692
- }));
3693
- if (gotTheLock)
3694
- return true;
3695
- // Someone else took the job.
3696
- if (awaitRemoteJob) {
3697
- try {
3698
- const jobDoneObservable = from(liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
3699
- yield jobDoneObservable.toPromise();
3700
- return false;
3701
- }
3702
- catch (err) {
3703
- if (err.name !== 'TimeoutError') {
3704
- throw err;
3705
- }
3706
- // Timeout stopped us! Try aquire the lock now.
3707
- // It will likely succeed this time unless
3708
- // another client took it.
3709
- return yield aquireLock();
3710
- }
3711
- }
3712
- return false;
3713
- });
3714
- }
3715
- if (yield aquireLock()) {
3716
- // We own the lock entry and can do our job undisturbed.
3717
- // We're not within a transaction, but these type of locks
3718
- // spans over transactions.
3719
- // Start our heart beat during the job.
3720
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
3721
- const heartbeat = setInterval(() => {
3722
- jobsTable.update(jobName, (job) => {
3723
- if (job.nodeId === myId) {
3724
- job.heartbeat = new Date();
3725
- }
3726
- });
3727
- }, GUARDED_JOB_HEARTBEAT);
3728
- try {
3729
- return yield job();
3730
- }
3731
- finally {
3732
- // Stop heartbeat
3733
- clearInterval(heartbeat);
3734
- // Remove the persisted job state:
3735
- yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
3736
- const currentWork = yield jobsTable.get(jobName);
3737
- if (currentWork && currentWork.nodeId === myId) {
3738
- yield jobsTable.delete(jobName);
3739
- }
3740
- }));
3741
- }
3742
- }
3743
- });
3878
+ function performGuardedJob(db, jobName, job) {
3879
+ if (typeof navigator === 'undefined' || !navigator.locks) {
3880
+ // No support for guarding jobs. IE11, node.js, etc.
3881
+ return job();
3882
+ }
3883
+ return navigator.locks.request(db.name + '|' + jobName, () => job());
3744
3884
  }
3745
3885
 
3746
3886
  const ongoingSyncs = new WeakMap();
@@ -3794,6 +3934,9 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
3794
3934
  function _syncIfPossible() {
3795
3935
  return __awaiter(this, void 0, void 0, function* () {
3796
3936
  try {
3937
+ // Check if should delay sync due to ratelimit:
3938
+ yield checkSyncRateLimitDelay(db);
3939
+ // Check if we need to lock the sync job. Not needed if we are the service worker.
3797
3940
  if (db.cloud.isServiceWorkerDB) {
3798
3941
  // We are the dedicated sync SW:
3799
3942
  yield sync(db, cloudOptions, cloudSchema, options);
@@ -3801,7 +3944,7 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
3801
3944
  else if (!db.cloud.usingServiceWorker) {
3802
3945
  // We use a flow that is better suited for the case when multiple workers want to
3803
3946
  // do the same thing.
3804
- yield performGuardedJob(db, CURRENT_SYNC_WORKER, '$jobs', () => sync(db, cloudOptions, cloudSchema, options));
3947
+ yield performGuardedJob(db, CURRENT_SYNC_WORKER, () => sync(db, cloudOptions, cloudSchema, options));
3805
3948
  }
3806
3949
  else {
3807
3950
  assert(false);
@@ -3823,19 +3966,29 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
3823
3966
  }
3824
3967
  }
3825
3968
 
3969
+ const SECONDS = 1000;
3970
+ const MINUTES = 60 * SECONDS;
3971
+
3826
3972
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
3827
3973
  let localSyncEventSubscription = null;
3828
3974
  //let syncHandler: ((event: Event) => void) | null = null;
3829
3975
  //let periodicSyncHandler: ((event: Event) => void) | null = null;
3830
3976
  let cancelToken = { cancelled: false };
3977
+ let retryHandle = null;
3978
+ let retryPurpose = null; // "pull" is superset of "push"
3831
3979
  function syncAndRetry(purpose, retryNum = 1) {
3832
3980
  // Use setTimeout() to get onto a clean stack and
3833
3981
  // break free from possible active transaction:
3834
3982
  setTimeout(() => {
3983
+ if (retryHandle)
3984
+ clearTimeout(retryHandle);
3985
+ const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
3986
+ retryHandle = null;
3987
+ retryPurpose = null;
3835
3988
  syncIfPossible(db, cloudOptions, cloudSchema, {
3836
3989
  cancelToken,
3837
3990
  retryImmediatelyOnFetchError: true,
3838
- purpose,
3991
+ purpose: combPurpose,
3839
3992
  }).catch((e) => {
3840
3993
  console.error('error in syncIfPossible()', e);
3841
3994
  if (cancelToken.cancelled) {
@@ -3845,7 +3998,13 @@ function LocalSyncWorker(db, cloudOptions, cloudSchema) {
3845
3998
  // Mimic service worker sync event: retry 3 times
3846
3999
  // * first retry after 5 minutes
3847
4000
  // * second retry 15 minutes later
3848
- setTimeout(() => syncAndRetry(purpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
4001
+ const combinedPurpose = retryPurpose && retryPurpose === 'pull' ? 'pull' : purpose;
4002
+ const handle = setTimeout(() => syncAndRetry(combinedPurpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
4003
+ // Cancel the previous retryHandle if it exists to avoid scheduling loads of retries.
4004
+ if (retryHandle)
4005
+ clearTimeout(retryHandle);
4006
+ retryHandle = handle;
4007
+ retryPurpose = combinedPurpose;
3849
4008
  }
3850
4009
  });
3851
4010
  }, 0);
@@ -3912,10 +4071,12 @@ const Styles = {
3912
4071
  },
3913
4072
  Alert: {
3914
4073
  error: {
3915
- color: "red"
4074
+ color: "red",
4075
+ fontWeight: "bold"
3916
4076
  },
3917
4077
  warning: {
3918
- color: "yellow"
4078
+ color: "#f80",
4079
+ fontWeight: "bold"
3919
4080
  },
3920
4081
  info: {
3921
4082
  color: "black"
@@ -3956,7 +4117,8 @@ const Styles = {
3956
4117
  border: "3px solid #3d3d5d",
3957
4118
  borderRadius: "8px",
3958
4119
  boxShadow: "0 0 80px 10px #666",
3959
- width: "auto"
4120
+ width: "auto",
4121
+ fontFamily: "sans-serif",
3960
4122
  },
3961
4123
  Input: {
3962
4124
  height: "35px",
@@ -3977,11 +4139,26 @@ function Dialog({ children, className }) {
3977
4139
 
3978
4140
  var t,r,u,i,o=0,c=[],f=[],e=l$1.__b,a=l$1.__r,v=l$1.diffed,l=l$1.__c,m=l$1.unmount;function d(t,u){l$1.__h&&l$1.__h(r,t,o||u),o=0;var i=r.__H||(r.__H={__:[],__h:[]});return t>=i.__.length&&i.__.push({__V:f}),i.__[t]}function p(n){return o=1,y(z,n)}function y(n,u,i){var o=d(t++,2);if(o.t=n,!o.__c&&(o.__=[i?i(u):z(void 0,u),function(n){var t=o.__N?o.__N[0]:o.__[0],r=o.t(t,n);t!==r&&(o.__N=[r,o.__[1]],o.__c.setState({}));}],o.__c=r,!r.u)){r.u=!0;var c=r.shouldComponentUpdate;r.shouldComponentUpdate=function(n,t,r){if(!o.__c.__H)return !0;var u=o.__c.__H.__.filter(function(n){return n.__c});if(u.every(function(n){return !n.__N}))return !c||c.call(this,n,t,r);var i=!1;return u.forEach(function(n){if(n.__N){var t=n.__[0];n.__=n.__N,n.__N=void 0,t!==n.__[0]&&(i=!0);}}),!!i&&(!c||c.call(this,n,t,r))};}return o.__N||o.__}function s(u,i){var o=d(t++,4);!l$1.__s&&w(o.__H,i)&&(o.__=u,o.i=i,r.__h.push(o));}function _(n){return o=5,F(function(){return {current:n}},[])}function F(n,r){var u=d(t++,7);return w(u.__H,r)?(u.__V=n(),u.i=r,u.__h=n,u.__V):u.__}function b(){for(var t;t=c.shift();)if(t.__P&&t.__H)try{t.__H.__h.forEach(j),t.__H.__h.forEach(k),t.__H.__h=[];}catch(r){t.__H.__h=[],l$1.__e(r,t.__v);}}l$1.__b=function(n){r=null,e&&e(n);},l$1.__r=function(n){a&&a(n),t=0;var i=(r=n.__c).__H;i&&(u===r?(i.__h=[],r.__h=[],i.__.forEach(function(n){n.__N&&(n.__=n.__N),n.__V=f,n.__N=n.i=void 0;})):(i.__h.forEach(j),i.__h.forEach(k),i.__h=[])),u=r;},l$1.diffed=function(t){v&&v(t);var o=t.__c;o&&o.__H&&(o.__H.__h.length&&(1!==c.push(o)&&i===l$1.requestAnimationFrame||((i=l$1.requestAnimationFrame)||function(n){var t,r=function(){clearTimeout(u),g&&cancelAnimationFrame(t),setTimeout(n);},u=setTimeout(r,100);g&&(t=requestAnimationFrame(r));})(b)),o.__H.__.forEach(function(n){n.i&&(n.__H=n.i),n.__V!==f&&(n.__=n.__V),n.i=void 0,n.__V=f;})),u=r=null;},l$1.__c=function(t,r){r.some(function(t){try{t.__h.forEach(j),t.__h=t.__h.filter(function(n){return !n.__||k(n)});}catch(u){r.some(function(n){n.__h&&(n.__h=[]);}),r=[],l$1.__e(u,t.__v);}}),l&&l(t,r);},l$1.unmount=function(t){m&&m(t);var r,u=t.__c;u&&u.__H&&(u.__H.__.forEach(function(n){try{j(n);}catch(n){r=n;}}),r&&l$1.__e(r,u.__v));};var g="function"==typeof requestAnimationFrame;function j(n){var t=r,u=n.__c;"function"==typeof u&&(n.__c=void 0,u()),r=t;}function k(n){var t=r;n.__c=n.__(),r=t;}function w(n,t){return !n||n.length!==t.length||t.some(function(t,r){return t!==n[r]})}function z(n,t){return "function"==typeof t?t(n):t}
3979
4141
 
4142
+ /** Resolve a message template with parameters.
4143
+ *
4144
+ * Example:
4145
+ * resolveText({
4146
+ * message: "Hello {name}!",
4147
+ * messageCode: "HELLO",
4148
+ * messageParams: {name: "David"}
4149
+ * }) => "Hello David!"
4150
+ *
4151
+ * @param message Template message with {vars} in it.
4152
+ * @param messageCode Unique code for the message. Can be used for translation.
4153
+ * @param messageParams Parameters to be used in the message.
4154
+ * @returns A final message where parameters have been replaced with values.
4155
+ */
3980
4156
  function resolveText({ message, messageCode, messageParams }) {
3981
- return message.replace(/\{\w+\}/ig, n => messageParams[n.substr(1, n.length - 2)]);
4157
+ return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
3982
4158
  }
3983
4159
 
3984
- function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
4160
+ const OTP_LENGTH = 8;
4161
+ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
3985
4162
  const [params, setParams] = p({});
3986
4163
  const firstFieldRef = _(null);
3987
4164
  s(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
@@ -3989,21 +4166,34 @@ function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
3989
4166
  h(p$1, null,
3990
4167
  h("h3", { style: Styles.WindowHeader }, title),
3991
4168
  alerts.map((alert) => (h("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
3992
- h("form", { onSubmit: ev => {
4169
+ h("form", { onSubmit: (ev) => {
3993
4170
  ev.preventDefault();
3994
4171
  onSubmit(params);
3995
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label },
4172
+ } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label, key: idx },
3996
4173
  label ? `${label}: ` : '',
3997
- h("input", { ref: idx === 0 ? firstFieldRef : undefined, type: type, name: fieldName, autoComplete: "on", style: Styles.Input, autoFocus: true, placeholder: placeholder, value: params[fieldName] || '', onInput: (ev) => { var _a; return setParams(Object.assign(Object.assign({}, params), { [fieldName]: valueTransformer(type, (_a = ev.target) === null || _a === void 0 ? void 0 : _a['value']) })); } })))))),
4174
+ h("input", { ref: idx === 0 ? firstFieldRef : undefined, type: type, name: fieldName, autoComplete: "on", style: Styles.Input, autoFocus: true, placeholder: placeholder, value: params[fieldName] || '', onInput: (ev) => {
4175
+ var _a;
4176
+ const value = valueTransformer(type, (_a = ev.target) === null || _a === void 0 ? void 0 : _a['value']);
4177
+ let updatedParams = Object.assign(Object.assign({}, params), { [fieldName]: value });
4178
+ setParams(updatedParams);
4179
+ if (type === 'otp' && (value === null || value === void 0 ? void 0 : value.trim().length) === OTP_LENGTH) {
4180
+ // Auto-submit when OTP is filled in.
4181
+ onSubmit(updatedParams);
4182
+ }
4183
+ } })))))),
3998
4184
  h("div", { style: Styles.ButtonsDiv },
3999
- h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, "Submit"),
4000
- h("button", { style: Styles.Button, onClick: onCancel }, "Cancel"))));
4185
+ h(p$1, null,
4186
+ h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, submitLabel),
4187
+ cancelLabel && (h("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
4001
4188
  }
4002
4189
  function valueTransformer(type, value) {
4003
4190
  switch (type) {
4004
- case "email": return value.toLowerCase();
4005
- case "otp": return value.toUpperCase();
4006
- default: return value;
4191
+ case 'email':
4192
+ return value.toLowerCase();
4193
+ case 'otp':
4194
+ return value.toUpperCase();
4195
+ default:
4196
+ return value;
4007
4197
  }
4008
4198
  }
4009
4199
 
@@ -4057,11 +4247,20 @@ function setupDefaultGUI(db) {
4057
4247
  }
4058
4248
  };
4059
4249
  }
4060
- // TODO:
4061
- /*
4062
- * Gjort klart allt kring user interaction förutom att mounta default-ui på ett element.
4063
- * Också att kolla först om nån annan subscribar och i så fall inte göra nåt.
4064
- */
4250
+
4251
+ function associate(factory) {
4252
+ const wm = new WeakMap();
4253
+ return (x) => {
4254
+ let rv = wm.get(x);
4255
+ if (!rv) {
4256
+ rv = factory(x);
4257
+ wm.set(x, rv);
4258
+ }
4259
+ return rv;
4260
+ };
4261
+ }
4262
+
4263
+ const getCurrentUserEmitter = associate((db) => new BehaviorSubject(UNAUTHORIZED_USER));
4065
4264
 
4066
4265
  function computeSyncState(db) {
4067
4266
  let _prevStatus = db.cloud.webSocketStatus.value;
@@ -4089,8 +4288,17 @@ function computeSyncState(db) {
4089
4288
  return combineLatest([
4090
4289
  lazyWebSocketStatus,
4091
4290
  db.syncStateChangedEvent.pipe(startWith({ phase: 'initial' })),
4291
+ getCurrentUserEmitter(db.dx._novip),
4092
4292
  userIsReallyActive
4093
- ]).pipe(map(([status, syncState, userIsActive]) => {
4293
+ ]).pipe(map(([status, syncState, user, userIsActive]) => {
4294
+ var _a;
4295
+ if (((_a = user.license) === null || _a === void 0 ? void 0 : _a.status) && user.license.status !== 'ok') {
4296
+ return {
4297
+ phase: 'offline',
4298
+ status: 'offline',
4299
+ license: user.license.status
4300
+ };
4301
+ }
4094
4302
  let { phase, error, progress } = syncState;
4095
4303
  let adjustedStatus = status;
4096
4304
  if (phase === 'error') {
@@ -4123,23 +4331,12 @@ function computeSyncState(db) {
4123
4331
  error,
4124
4332
  progress,
4125
4333
  status: isOnline ? adjustedStatus : 'offline',
4334
+ license: 'ok'
4126
4335
  };
4127
4336
  return retState;
4128
4337
  }));
4129
4338
  }
4130
4339
 
4131
- function associate(factory) {
4132
- const wm = new WeakMap();
4133
- return (x) => {
4134
- let rv = wm.get(x);
4135
- if (!rv) {
4136
- rv = factory(x);
4137
- wm.set(x, rv);
4138
- }
4139
- return rv;
4140
- };
4141
- }
4142
-
4143
4340
  function createSharedValueObservable(o, defaultValue) {
4144
4341
  let currentValue = defaultValue;
4145
4342
  let shared = from(o).pipe(map$1((x) => (currentValue = x)), share({ resetOnRefCountZero: () => timer(1000) }));
@@ -4181,8 +4378,6 @@ const getGlobalRolesObservable = associate((db) => {
4181
4378
  })), {});
4182
4379
  });
4183
4380
 
4184
- const getCurrentUserEmitter = associate((db) => new BehaviorSubject(UNAUTHORIZED_USER));
4185
-
4186
4381
  const getInternalAccessControlObservable = associate((db) => {
4187
4382
  return createSharedValueObservable(getCurrentUserEmitter(db._novip).pipe(switchMap((currentUser) => liveQuery(() => db.transaction('r', 'realms', 'members', () => Promise.all([
4188
4383
  db.members.where({ userId: currentUser.userId }).toArray(),
@@ -4465,7 +4660,7 @@ function dexieCloud(dexie) {
4465
4660
  });
4466
4661
  const syncComplete = new Subject();
4467
4662
  dexie.cloud = {
4468
- version: '4.0.1-beta.46',
4663
+ version: '4.0.1-beta.47',
4469
4664
  options: Object.assign({}, DEFAULT_OPTIONS),
4470
4665
  schema: null,
4471
4666
  get currentUserId() {
@@ -4501,11 +4696,24 @@ function dexieCloud(dexie) {
4501
4696
  }
4502
4697
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
4503
4698
  },
4699
+ logout({ force } = {}) {
4700
+ return __awaiter(this, void 0, void 0, function* () {
4701
+ force
4702
+ ? yield _logout(DexieCloudDB(dexie), { deleteUnsyncedData: true })
4703
+ : yield logout(DexieCloudDB(dexie));
4704
+ });
4705
+ },
4504
4706
  sync({ wait, purpose } = { wait: true, purpose: 'push' }) {
4707
+ var _a;
4505
4708
  return __awaiter(this, void 0, void 0, function* () {
4506
4709
  if (wait === undefined)
4507
4710
  wait = true;
4508
4711
  const db = DexieCloudDB(dexie);
4712
+ const licenseStatus = ((_a = db.cloud.currentUser.value.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
4713
+ if (licenseStatus !== 'ok') {
4714
+ // Refresh access token to check for updated license
4715
+ yield loadAccessToken(db);
4716
+ }
4509
4717
  if (purpose === 'pull') {
4510
4718
  const syncState = db.cloud.persistedSyncState.value;
4511
4719
  triggerSync(db, purpose);
@@ -4709,7 +4917,9 @@ function dexieCloud(dexie) {
4709
4917
  db.syncStateChangedEvent.next({
4710
4918
  phase: 'not-in-sync',
4711
4919
  });
4712
- triggerSync(db, 'push');
4920
+ if (!isEagerSyncDisabled(db)) {
4921
+ triggerSync(db, 'push');
4922
+ }
4713
4923
  }), fromEvent(self, 'offline').subscribe(() => {
4714
4924
  console.debug('offline!');
4715
4925
  db.syncStateChangedEvent.next({
@@ -4726,7 +4936,7 @@ function dexieCloud(dexie) {
4726
4936
  });
4727
4937
  }
4728
4938
  }
4729
- dexieCloud.version = '4.0.1-beta.46';
4939
+ dexieCloud.version = '4.0.1-beta.47';
4730
4940
  Dexie.Cloud = dexieCloud;
4731
4941
 
4732
4942
  // In case the SW lives for a while, let it reuse already opened connections: