dexie-cloud-addon 4.0.1-beta.45 → 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 +485 -317
  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 +469 -249
  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 +484 -316
  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 +467 -247
  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
@@ -244,6 +244,7 @@
244
244
 
245
245
  function triggerSync(db, purpose) {
246
246
  if (db.cloud.usingServiceWorker) {
247
+ console.debug('registering sync event');
247
248
  registerSyncEvent(db, purpose);
248
249
  }
249
250
  else {
@@ -745,14 +746,24 @@
745
746
  return tablesToSyncify;
746
747
  }
747
748
 
749
+ class TokenErrorResponseError extends Error {
750
+ constructor({ title, message, messageCode, messageParams, }) {
751
+ super(message);
752
+ this.name = 'TokenErrorResponseError';
753
+ this.title = title;
754
+ this.messageCode = messageCode;
755
+ this.messageParams = messageParams;
756
+ }
757
+ }
758
+
748
759
  function interactWithUser(userInteraction, req) {
749
760
  return new Promise((resolve, reject) => {
750
- const interactionProps = Object.assign(Object.assign({}, req), { onSubmit: (res) => {
761
+ const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
751
762
  userInteraction.next(undefined);
752
763
  resolve(res);
753
764
  }, onCancel: () => {
754
765
  userInteraction.next(undefined);
755
- reject(new Dexie__default["default"].AbortError("User cancelled"));
766
+ reject(new Dexie__default["default"].AbortError('User cancelled'));
756
767
  } });
757
768
  userInteraction.next(interactionProps);
758
769
  // Start subscribing for external updates to db.cloud.userInteraction, and if so, cancel this request.
@@ -771,7 +782,9 @@
771
782
  type: 'message-alert',
772
783
  title,
773
784
  alerts,
774
- fields: {}
785
+ fields: {},
786
+ submitLabel: 'OK',
787
+ cancelLabel: null,
775
788
  });
776
789
  }
777
790
  function promptForEmail(userInteraction, title, emailHint) {
@@ -830,22 +843,48 @@
830
843
  return otp;
831
844
  });
832
845
  }
846
+ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
847
+ return __awaiter(this, void 0, void 0, function* () {
848
+ const alerts = [
849
+ {
850
+ type: 'warning',
851
+ messageCode: 'LOGOUT_CONFIRMATION',
852
+ message: `{numUnsyncedChanges} unsynced changes will get lost!
853
+ Logout anyway?`,
854
+ messageParams: {
855
+ currentUserId,
856
+ numUnsyncedChanges: numUnsyncedChanges.toString(),
857
+ }
858
+ },
859
+ ];
860
+ return yield interactWithUser(userInteraction, {
861
+ type: 'logout-confirmation',
862
+ title: 'Confirm Logout',
863
+ alerts,
864
+ fields: {},
865
+ submitLabel: 'Confirm logout',
866
+ cancelLabel: 'Cancel'
867
+ })
868
+ .then(() => true)
869
+ .catch(() => false);
870
+ });
871
+ }
833
872
 
834
873
  function loadAccessToken(db) {
835
- var _a, _b;
874
+ var _a, _b, _c;
836
875
  return __awaiter(this, void 0, void 0, function* () {
837
876
  const currentUser = yield db.getCurrentUser();
838
877
  const { accessToken, accessTokenExpiration, refreshToken, refreshTokenExpiration, claims, } = currentUser;
839
878
  if (!accessToken)
840
- return;
879
+ return null;
841
880
  const expTime = (_a = accessTokenExpiration === null || accessTokenExpiration === void 0 ? void 0 : accessTokenExpiration.getTime()) !== null && _a !== void 0 ? _a : Infinity;
842
- if (expTime > Date.now()) {
843
- return accessToken;
881
+ if (expTime > Date.now() && (((_b = currentUser.license) === null || _b === void 0 ? void 0 : _b.status) || 'ok') === 'ok') {
882
+ return currentUser;
844
883
  }
845
884
  if (!refreshToken) {
846
885
  throw new Error(`Refresh token missing`);
847
886
  }
848
- const refreshExpTime = (_b = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _b !== void 0 ? _b : Infinity;
887
+ const refreshExpTime = (_c = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _c !== void 0 ? _c : Infinity;
849
888
  if (refreshExpTime <= Date.now()) {
850
889
  throw new Error(`Refresh token has expired`);
851
890
  }
@@ -853,8 +892,10 @@
853
892
  yield db.table('$logins').update(claims.sub, {
854
893
  accessToken: refreshedLogin.accessToken,
855
894
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
895
+ claims: refreshedLogin.claims,
896
+ license: refreshedLogin.license,
856
897
  });
857
- return refreshedLogin.accessToken;
898
+ return refreshedLogin;
858
899
  });
859
900
  }
860
901
  function authenticate(url, context, fetchToken, userInteraction, hints) {
@@ -902,10 +943,24 @@
902
943
  if (res.status !== 200)
903
944
  throw new Error(`RefreshToken: Status ${res.status} from ${url}/token`);
904
945
  const response = yield res.json();
946
+ if (response.type === 'error') {
947
+ throw new TokenErrorResponseError(response);
948
+ }
905
949
  login.accessToken = response.accessToken;
906
950
  login.accessTokenExpiration = response.accessTokenExpiration
907
951
  ? new Date(response.accessTokenExpiration)
908
952
  : undefined;
953
+ login.claims = response.claims;
954
+ login.license = {
955
+ type: response.userType,
956
+ status: response.claims.license || 'ok',
957
+ };
958
+ if (response.evalDaysLeft != null) {
959
+ login.license.evalDaysLeft = response.evalDaysLeft;
960
+ }
961
+ if (response.userValidUntil != null) {
962
+ login.license.validUntil = new Date(response.userValidUntil);
963
+ }
909
964
  return login;
910
965
  });
911
966
  }
@@ -937,8 +992,15 @@
937
992
  public_key: publicKeyPEM,
938
993
  hints,
939
994
  });
995
+ if (response2.type === 'error') {
996
+ throw new TokenErrorResponseError(response2);
997
+ }
940
998
  if (response2.type !== 'tokens')
941
999
  throw new Error(`Unexpected response type from token endpoint: ${response2.type}`);
1000
+ /*const licenseStatus = response2.claims.license || 'ok';
1001
+ if (licenseStatus !== 'ok') {
1002
+ throw new InvalidLicenseError(licenseStatus);
1003
+ }*/
942
1004
  context.accessToken = response2.accessToken;
943
1005
  context.accessTokenExpiration = new Date(response2.accessTokenExpiration);
944
1006
  context.refreshToken = response2.refreshToken;
@@ -949,6 +1011,16 @@
949
1011
  context.email = response2.claims.email;
950
1012
  context.name = response2.claims.name;
951
1013
  context.claims = response2.claims;
1014
+ context.license = {
1015
+ type: response2.userType,
1016
+ status: response2.claims.license || 'ok',
1017
+ };
1018
+ if (response2.evalDaysLeft != null) {
1019
+ context.license.evalDaysLeft = response2.evalDaysLeft;
1020
+ }
1021
+ if (response2.userValidUntil != null) {
1022
+ context.license.validUntil = new Date(response2.userValidUntil);
1023
+ }
952
1024
  if (response2.alerts && response2.alerts.length > 0) {
953
1025
  yield interactWithUser(userInteraction, {
954
1026
  type: 'message-alert',
@@ -960,12 +1032,36 @@
960
1032
  return context;
961
1033
  }
962
1034
  catch (error) {
963
- yield alertUser(userInteraction, 'Authentication Failed', {
964
- type: 'error',
965
- messageCode: 'GENERIC_ERROR',
966
- message: `We're having a problem authenticating right now.`,
967
- messageParams: {},
968
- }).catch(() => { });
1035
+ if (error instanceof TokenErrorResponseError) {
1036
+ yield alertUser(userInteraction, error.title, {
1037
+ type: 'error',
1038
+ messageCode: error.messageCode,
1039
+ message: error.message,
1040
+ messageParams: {},
1041
+ });
1042
+ throw error;
1043
+ }
1044
+ let message = `We're having a problem authenticating right now.`;
1045
+ console.error(`Error authenticating`, error);
1046
+ if (error instanceof TypeError) {
1047
+ const isOffline = typeof navigator !== undefined && !navigator.onLine;
1048
+ if (isOffline) {
1049
+ message = `You seem to be offline. Please connect to the internet and try again.`;
1050
+ }
1051
+ else if (Dexie__default["default"].debug || (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1'))) {
1052
+ // The audience is most likely the developer. Suggest to whitelist the localhost origin:
1053
+ message = `Could not connect to server. Please verify that your origin '${location.origin}' is whitelisted using \`npx dexie-cloud whitelist\``;
1054
+ }
1055
+ else {
1056
+ message = `Could not connect to server. Please verify the connection.`;
1057
+ }
1058
+ yield alertUser(userInteraction, 'Authentication Failed', {
1059
+ type: 'error',
1060
+ messageCode: 'GENERIC_ERROR',
1061
+ message,
1062
+ messageParams: {},
1063
+ }).catch(() => { });
1064
+ }
969
1065
  throw error;
970
1066
  }
971
1067
  });
@@ -1545,9 +1641,19 @@
1545
1641
  const mutClone = changeClone.muts[mutIndex];
1546
1642
  const rewrittenKey = JSON.stringify(key);
1547
1643
  mutClone.keys[keyIndex] = rewrittenKey;
1548
- if (rewriteValues) {
1549
- Dexie__default["default"].setByKeyPath(mutClone.values[keyIndex], primaryKey.keyPath, rewrittenKey);
1550
- }
1644
+ /* Bug (#1777)
1645
+ We should not rewrite values. It will fail because the key is array and the value is string.
1646
+ Only the keys should be rewritten and it's already done on the server.
1647
+ We should take another round of revieweing how key transformations are being done between
1648
+ client and server and let the server do the key transformations entirely instead now that
1649
+ we have the primary key schema on the server making it possible to do so.
1650
+ if (rewriteValues) {
1651
+ Dexie.setByKeyPath(
1652
+ (mutClone as DBInsertOperation).values[keyIndex],
1653
+ primaryKey.keyPath!,
1654
+ rewrittenKey
1655
+ );
1656
+ }*/
1551
1657
  }
1552
1658
  else if (key[0] === '#') {
1553
1659
  // Private ID - translate!
@@ -1578,6 +1684,40 @@
1578
1684
  : change.muts.map((m) => (Object.assign(Object.assign({}, m), { keys: m.keys.slice() }))) });
1579
1685
  }
1580
1686
 
1687
+ // If we get Ratelimit-Limit and Ratelimit-Remaining where Ratelimit-Remaining is below
1688
+ // (Ratelimit-Limit / 2), we should delay the next sync by (Ratelimit-Reset / Ratelimit-Remaining)
1689
+ // seconds (given that there is a Ratelimit-Reset header).
1690
+ let syncRatelimitDelays = new WeakMap();
1691
+ function checkSyncRateLimitDelay(db) {
1692
+ var _a, _b;
1693
+ return __awaiter(this, void 0, void 0, function* () {
1694
+ const delatMilliseconds = ((_b = (_a = syncRatelimitDelays.get(db)) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : 0) - Date.now();
1695
+ if (delatMilliseconds > 0) {
1696
+ console.debug(`Stalling sync request ${delatMilliseconds} ms to spare ratelimits`);
1697
+ yield new Promise(resolve => setTimeout(resolve, delatMilliseconds));
1698
+ }
1699
+ });
1700
+ }
1701
+ function updateSyncRateLimitDelays(db, res) {
1702
+ const limit = res.headers.get('Ratelimit-Limit');
1703
+ const remaining = res.headers.get('Ratelimit-Remaining');
1704
+ const reset = res.headers.get('Ratelimit-Reset');
1705
+ if (limit && remaining && reset) {
1706
+ const limitNum = Number(limit);
1707
+ const remainingNum = Math.max(0, Number(remaining));
1708
+ const willResetInSeconds = Number(reset);
1709
+ if (remainingNum < limitNum / 2) {
1710
+ const delay = Math.ceil(willResetInSeconds / (remainingNum + 1));
1711
+ syncRatelimitDelays.set(db, new Date(Date.now() + delay * 1000));
1712
+ console.debug(`Sync ratelimit delay set to ${delay} seconds`);
1713
+ }
1714
+ else {
1715
+ syncRatelimitDelays.delete(db);
1716
+ console.debug(`Sync ratelimit delay cleared`);
1717
+ }
1718
+ }
1719
+ }
1720
+
1581
1721
  //import {BisonWebStreamReader} from "dreambase-library/dist/typeson-simplified/BisonWebStreamReader";
1582
1722
  function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser) {
1583
1723
  return __awaiter(this, void 0, void 0, function* () {
@@ -1586,9 +1726,20 @@
1586
1726
  //
1587
1727
  const headers = {
1588
1728
  Accept: 'application/json, application/x-bison, application/x-bison-stream',
1589
- 'Content-Type': 'application/tson'
1729
+ 'Content-Type': 'application/tson',
1590
1730
  };
1591
- const accessToken = yield loadAccessToken(db);
1731
+ const updatedUser = yield loadAccessToken(db);
1732
+ /*
1733
+ if (updatedUser?.license && changes.length > 0) {
1734
+ if (updatedUser.license.status === 'expired') {
1735
+ throw new Error(`License has expired`);
1736
+ }
1737
+ if (updatedUser.license.status === 'deactivated') {
1738
+ throw new Error(`License deactivated`);
1739
+ }
1740
+ }
1741
+ */
1742
+ const accessToken = updatedUser === null || updatedUser === void 0 ? void 0 : updatedUser.accessToken;
1592
1743
  if (accessToken) {
1593
1744
  headers.Authorization = `Bearer ${accessToken}`;
1594
1745
  }
@@ -1597,27 +1748,31 @@
1597
1748
  dbID: syncState === null || syncState === void 0 ? void 0 : syncState.remoteDbId,
1598
1749
  clientIdentity,
1599
1750
  schema: schema || {},
1600
- lastPull: syncState ? {
1601
- serverRevision: syncState.serverRevision,
1602
- realms: syncState.realms,
1603
- inviteRealms: syncState.inviteRealms
1604
- } : undefined,
1751
+ lastPull: syncState
1752
+ ? {
1753
+ serverRevision: syncState.serverRevision,
1754
+ realms: syncState.realms,
1755
+ inviteRealms: syncState.inviteRealms,
1756
+ }
1757
+ : undefined,
1605
1758
  baseRevs,
1606
- changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes)
1759
+ changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
1607
1760
  };
1608
- console.debug("Sync request", syncRequest);
1761
+ console.debug('Sync request', syncRequest);
1609
1762
  db.syncStateChangedEvent.next({
1610
1763
  phase: 'pushing',
1611
1764
  });
1612
1765
  const res = yield fetch(`${databaseUrl}/sync`, {
1613
1766
  method: 'post',
1614
1767
  headers,
1615
- body: TSON.stringify(syncRequest)
1768
+ credentials: 'include',
1769
+ body: TSON.stringify(syncRequest),
1616
1770
  });
1617
1771
  //const contentLength = Number(res.headers.get('content-length'));
1618
1772
  db.syncStateChangedEvent.next({
1619
- phase: 'pulling'
1773
+ phase: 'pulling',
1620
1774
  });
1775
+ updateSyncRateLimitDelays(db, res);
1621
1776
  if (!res.ok) {
1622
1777
  throw new HttpError(res);
1623
1778
  }
@@ -1819,12 +1974,13 @@
1819
1974
  function sync(db, options, schema, syncOptions) {
1820
1975
  return _sync
1821
1976
  .apply(this, arguments)
1822
- .then(() => {
1977
+ .then((result) => {
1823
1978
  if (!(syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)) { // && syncOptions?.purpose !== 'push') {
1824
1979
  db.syncStateChangedEvent.next({
1825
1980
  phase: 'in-sync',
1826
1981
  });
1827
1982
  }
1983
+ return result;
1828
1984
  })
1829
1985
  .catch((error) => __awaiter(this, void 0, void 0, function* () {
1830
1986
  if (syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)
@@ -1854,7 +2010,7 @@
1854
2010
  });
1855
2011
  db.syncStateChangedEvent.next({
1856
2012
  phase: isOnline ? 'error' : 'offline',
1857
- error,
2013
+ error: new Error('' + (error === null || error === void 0 ? void 0 : error.message) || error),
1858
2014
  });
1859
2015
  return Promise.reject(error);
1860
2016
  }));
@@ -2039,6 +2195,7 @@
2039
2195
  }));
2040
2196
  if (!done) {
2041
2197
  console.debug('MORE SYNC NEEDED. Go for it again!');
2198
+ yield checkSyncRateLimitDelay(db);
2042
2199
  return yield _sync(db, options, schema, { isInitialSync, cancelToken });
2043
2200
  }
2044
2201
  console.debug('SYNC DONE', { isInitialSync });
@@ -2175,6 +2332,8 @@
2175
2332
  yield db.table('$logins').update(user.userId, {
2176
2333
  accessToken: refreshedLogin.accessToken,
2177
2334
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
2335
+ claims: refreshedLogin.claims,
2336
+ license: refreshedLogin.license,
2178
2337
  });
2179
2338
  // Updating $logins will trigger emission of db.cloud.currentUser observable, which
2180
2339
  // in turn will lead to that connectWebSocket.ts will reconnect the socket with the
@@ -2445,6 +2604,61 @@
2445
2604
  }
2446
2605
  }
2447
2606
 
2607
+ function waitUntil(o, // Works with Dexie's liveQuery observables if we'd need that
2608
+ predicate) {
2609
+ return rxjs.firstValueFrom(rxjs.from(o).pipe(rxjs.filter(predicate)));
2610
+ }
2611
+
2612
+ function logout(db) {
2613
+ return __awaiter(this, void 0, void 0, function* () {
2614
+ const numUnsyncedChanges = yield _logout(db);
2615
+ if (numUnsyncedChanges) {
2616
+ if (yield confirmLogout(db.cloud.userInteraction, db.cloud.currentUserId, numUnsyncedChanges)) {
2617
+ yield _logout(db, { deleteUnsyncedData: true });
2618
+ }
2619
+ else {
2620
+ throw new Error(`User cancelled logout due to unsynced changes`);
2621
+ }
2622
+ }
2623
+ });
2624
+ }
2625
+ function _logout(db, { deleteUnsyncedData = false } = {}) {
2626
+ return __awaiter(this, void 0, void 0, function* () {
2627
+ // Clear the database without emptying configuration options.
2628
+ const [numUnsynced, loggedOut] = yield db.dx.transaction('rw', db.dx.tables, (tx) => __awaiter(this, void 0, void 0, function* () {
2629
+ // @ts-ignore
2630
+ const idbtrans = tx.idbtrans;
2631
+ idbtrans.disableChangeTracking = true;
2632
+ idbtrans.disableAccessControl = true;
2633
+ const mutationTables = tx.storeNames.filter((tableName) => tableName.endsWith('_mutations'));
2634
+ // Count unsynced changes
2635
+ const unsyncCounts = yield Promise.all(mutationTables.map((mutationTable) => tx.table(mutationTable).count()));
2636
+ const sumUnSynced = unsyncCounts.reduce((a, b) => a + b, 0);
2637
+ if (sumUnSynced > 0 && !deleteUnsyncedData) {
2638
+ // Let caller ask user if they want to delete unsynced data.
2639
+ return [sumUnSynced, false];
2640
+ }
2641
+ // Either there are no unsynched changes, or caller provided flag deleteUnsynchedData = true.
2642
+ // Clear all tables except $jobs and $syncState (except the persisted sync state which is
2643
+ // also cleared because we're going to rebuild it using a fresh sync).
2644
+ db.$syncState.delete('syncState');
2645
+ for (const table of db.dx.tables) {
2646
+ if (table.name !== '$jobs' && table.name !== '$syncState') {
2647
+ table.clear();
2648
+ }
2649
+ }
2650
+ return [sumUnSynced, true];
2651
+ }));
2652
+ if (loggedOut) {
2653
+ // Wait for currentUser observable to emit UNAUTHORIZED_USER
2654
+ yield waitUntil(db.cloud.currentUser, (user) => user.userId === UNAUTHORIZED_USER.userId);
2655
+ // Then perform an initial sync
2656
+ yield db.cloud.sync({ purpose: 'pull', wait: true });
2657
+ }
2658
+ return numUnsynced;
2659
+ });
2660
+ }
2661
+
2448
2662
  function otpFetchTokenCallback(db) {
2449
2663
  const { userInteraction } = db.cloud;
2450
2664
  return function otpAuthenticate({ public_key, hints }) {
@@ -2488,8 +2702,9 @@
2488
2702
  throw new HttpError(res1, errMsg);
2489
2703
  }
2490
2704
  const response = yield res1.json();
2491
- if (response.type === 'tokens') {
2705
+ if (response.type === 'tokens' || response.type === 'error') {
2492
2706
  // Demo user request can get a "tokens" response right away
2707
+ // Error can also be returned right away.
2493
2708
  return response;
2494
2709
  }
2495
2710
  else if (tokenRequest.grant_type === 'otp') {
@@ -2521,12 +2736,6 @@
2521
2736
  }
2522
2737
  if (res2.status !== 200) {
2523
2738
  const errMsg = yield res2.text();
2524
- yield alertUser(userInteraction, "OTP Authentication Failed", {
2525
- type: 'error',
2526
- messageCode: 'GENERIC_ERROR',
2527
- message: errMsg,
2528
- messageParams: {}
2529
- }).catch(() => { });
2530
2739
  throw new HttpError(res2, errMsg);
2531
2740
  }
2532
2741
  const response2 = yield res2.json();
@@ -2539,6 +2748,18 @@
2539
2748
  };
2540
2749
  }
2541
2750
 
2751
+ /** A way to log to console in production without terser stripping out
2752
+ * it from the release bundle.
2753
+ * This should be used very rarely and only in places where it's
2754
+ * absolutely necessary to log something in production.
2755
+ *
2756
+ * @param level
2757
+ * @param args
2758
+ */
2759
+ function prodLog(level, ...args) {
2760
+ globalThis["con" + "sole"][level](...args);
2761
+ }
2762
+
2542
2763
  /** This function changes or sets the current user as requested.
2543
2764
  *
2544
2765
  * Use cases:
@@ -2565,85 +2786,73 @@
2565
2786
  }));
2566
2787
  user.isLoggedIn = true;
2567
2788
  user.lastLogin = new Date();
2568
- yield user.save();
2569
- console.debug('Saved new user', user.email);
2570
- }));
2571
- yield new Promise((resolve) => {
2572
- if (db.cloud.currentUserId === user.userId) {
2573
- resolve(null);
2789
+ try {
2790
+ yield user.save();
2574
2791
  }
2575
- else {
2576
- const subscription = db.cloud.currentUser.subscribe((currentUser) => {
2577
- if (currentUser.userId === user.userId) {
2578
- subscription.unsubscribe();
2579
- resolve(null);
2792
+ catch (e) {
2793
+ try {
2794
+ if (e.name === 'DataCloneError') {
2795
+ // We've seen this buggy behavior in some browsers and in case it happens
2796
+ // again we really need to collect the details to understand what's going on.
2797
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2798
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2799
+ prodLog('debug', `Login context:`, user);
2800
+ prodLog('debug', `Login context JSON:`, JSON.stringify(user));
2580
2801
  }
2581
- });
2802
+ }
2803
+ catch (_a) { }
2804
+ throw e;
2582
2805
  }
2583
- });
2584
- // TANKAR!!!!
2585
- // V: Service workern kommer inte ha tillgång till currentUserObservable om den inte istället härrör från ett liveQuery.
2586
- // V: Samma med andra windows.
2587
- // V: Så kanske göra om den till att häröra från liveQuery som läser $logins.orderBy('lastLogin').last().
2588
- // V: Då bara vara medveten om:
2589
- // V: En sån observable börjar hämta data vid första subscribe
2590
- // V: Vi har inget "inital value" men kan emulera det till att vara ANONYMOUS_USER
2591
- // V: Om requireAuth är true, så borde db.on(ready) hålla databasen stängd för alla utom denna observable.
2592
- // V: Om inte så behöver den inte blocka.
2593
- // Andra tankar:
2594
- // * Man kan inte byta användare när man är offline. Skulle gå att flytta realms till undanstuff-tabell vid user-change.
2595
- // men troligen inte värt det.
2596
- // * Istället: sälj inte inte switch-user funktionalitet utan tala enbart om inloggat vs icke inloggat läge.
2597
- // * populate $logins med ANONYMOUS så att en påbörjad inloggning inte räknas, alternativt ha en boolean prop!
2598
- // Kanske bäst ha en boolean prop!
2599
- // * Alternativ switch-user funktionalitet:
2600
- // * DBCore gömmer data från realms man inte har tillgång till.
2601
- // * Cursor impl behövs också då.
2602
- // * Då blir det snabba user switch.
2603
- // * claims-settet som skickas till servern blir summan av alla claims. Då måste servern stödja multipla tokens eller
2604
- // att ens token är ett samlad.
2806
+ console.debug('Saved new user', user.email);
2807
+ }));
2808
+ yield waitUntil(db.cloud.currentUser, (currentUser) => currentUser.userId === user.userId);
2605
2809
  });
2606
2810
  }
2607
2811
 
2608
2812
  function login(db, hints) {
2813
+ var _a;
2609
2814
  return __awaiter(this, void 0, void 0, function* () {
2610
2815
  const currentUser = yield db.getCurrentUser();
2611
- if (currentUser.isLoggedIn) {
2612
- if (hints) {
2613
- if (hints.email && db.cloud.currentUser.value.email !== hints.email) {
2614
- throw new Error(`Must logout before changing user`);
2615
- }
2616
- if (hints.userId && db.cloud.currentUserId !== hints.userId) {
2617
- throw new Error(`Must logout before changing user`);
2618
- }
2816
+ const origUserId = currentUser.userId;
2817
+ if (currentUser.isLoggedIn && (!hints || (!hints.email && !hints.userId))) {
2818
+ const licenseStatus = ((_a = currentUser.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
2819
+ if (licenseStatus === 'ok' && currentUser.accessToken && (!currentUser.accessTokenExpiration || currentUser.accessTokenExpiration.getTime() > Date.now())) {
2820
+ // Already authenticated according to given hints. And license is valid.
2821
+ return false;
2619
2822
  }
2620
- // Already authenticated according to given hints.
2621
- return false;
2823
+ if (currentUser.refreshToken && (!currentUser.refreshTokenExpiration || currentUser.refreshTokenExpiration.getTime() > Date.now())) {
2824
+ // Refresh the token
2825
+ yield loadAccessToken(db);
2826
+ return false;
2827
+ }
2828
+ // No refresh token - must re-authenticate:
2622
2829
  }
2623
2830
  const context = new AuthPersistedContext(db, {
2624
2831
  claims: {},
2625
2832
  lastLogin: new Date(0),
2626
2833
  });
2627
2834
  yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
2628
- try {
2629
- yield context.save();
2835
+ if (origUserId !== UNAUTHORIZED_USER.userId && context.userId !== origUserId) {
2836
+ // User was logged in before, but now logged in as another user.
2837
+ yield logout(db);
2630
2838
  }
2631
- catch (e) {
2632
- try {
2633
- if (e.name === 'DataCloneError') {
2634
- console.debug(`Login context property names:`, Object.keys(context));
2635
- console.debug(`Login context:`, context);
2636
- console.debug(`Login context JSON:`, JSON.stringify(context));
2637
- }
2839
+ /*try {
2840
+ await context.save();
2841
+ } catch (e) {
2842
+ try {
2843
+ if (e.name === 'DataCloneError') {
2844
+ console.debug(`Login context property names:`, Object.keys(context));
2845
+ console.debug(`Login context:`, context);
2846
+ console.debug(`Login context JSON:`, JSON.stringify(context));
2638
2847
  }
2639
- catch (_a) { }
2640
- throw e;
2641
- }
2848
+ } catch {}
2849
+ throw e;
2850
+ }*/
2642
2851
  yield setCurrentUser(db, context);
2643
2852
  // Make sure to resync as the new login will be authorized
2644
2853
  // for new realms.
2645
2854
  triggerSync(db, "pull");
2646
- return true;
2855
+ return context.userId !== origUserId;
2647
2856
  });
2648
2857
  }
2649
2858
 
@@ -2972,6 +3181,13 @@
2972
3181
 
2973
3182
  const outstandingTransactions = new rxjs.BehaviorSubject(new Set());
2974
3183
 
3184
+ function isEagerSyncDisabled(db) {
3185
+ var _a, _b, _c, _d;
3186
+ return (((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.disableEagerSync) ||
3187
+ ((_c = (_b = db.cloud.currentUser.value) === null || _b === void 0 ? void 0 : _b.license) === null || _c === void 0 ? void 0 : _c.status) !== 'ok' ||
3188
+ !((_d = db.cloud.options) === null || _d === void 0 ? void 0 : _d.databaseUrl));
3189
+ }
3190
+
2975
3191
  /** Tracks all mutations in the same transaction as the mutations -
2976
3192
  * so it is guaranteed that no mutation goes untracked - and if transaction
2977
3193
  * aborts, the mutations won't be tracked.
@@ -2980,7 +3196,7 @@
2980
3196
  * changes to server and cleanup the tracked mutations once the server has
2981
3197
  * ackowledged that it got them.
2982
3198
  */
2983
- function createMutationTrackingMiddleware({ currentUserObservable, db }) {
3199
+ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
2984
3200
  return {
2985
3201
  stack: 'dbcore',
2986
3202
  name: 'MutationTrackingMiddleware',
@@ -2991,7 +3207,7 @@
2991
3207
  try {
2992
3208
  mutTableMap = new Map(ordinaryTables.map((tbl) => [
2993
3209
  tbl.name,
2994
- core.table(`$${tbl.name}_mutations`)
3210
+ core.table(`$${tbl.name}_mutations`),
2995
3211
  ]));
2996
3212
  }
2997
3213
  catch (_a) {
@@ -3025,15 +3241,9 @@
3025
3241
  outstandingTransactions.next(outstandingTransactions.value);
3026
3242
  };
3027
3243
  const txComplete = () => {
3028
- var _a;
3029
- if (tx.mutationsAdded && ((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl)) {
3030
- if (db.cloud.usingServiceWorker) {
3031
- console.debug('registering sync event');
3032
- registerSyncEvent(db, "push");
3033
- }
3034
- else {
3035
- db.localSyncEvent.next({ purpose: "push" });
3036
- }
3244
+ if (tx.mutationsAdded &&
3245
+ !isEagerSyncDisabled(db)) {
3246
+ triggerSync(db, 'push');
3037
3247
  }
3038
3248
  removeTransaction();
3039
3249
  };
@@ -3100,7 +3310,7 @@
3100
3310
  .query({
3101
3311
  query: { range: req.range, index: schema.primaryKey },
3102
3312
  trans: req.trans,
3103
- values: false
3313
+ values: false,
3104
3314
  })
3105
3315
  // Do a delete request instead, but keep the criteria info for the server to execute
3106
3316
  .then((res) => {
@@ -3108,7 +3318,7 @@
3108
3318
  type: 'delete',
3109
3319
  keys: res.result,
3110
3320
  trans: req.trans,
3111
- criteria: { index: null, range: req.range }
3321
+ criteria: { index: null, range: req.range },
3112
3322
  });
3113
3323
  })
3114
3324
  : mutateAndLog(req);
@@ -3116,7 +3326,7 @@
3116
3326
  function mutateAndLog(req) {
3117
3327
  const trans = req.trans;
3118
3328
  trans.mutationsAdded = true;
3119
- const { txid, currentUser: { userId } } = trans;
3329
+ const { txid, currentUser: { userId }, } = trans;
3120
3330
  const { type } = req;
3121
3331
  const opNo = ++trans.opCount;
3122
3332
  return table.mutate(req).then((res) => {
@@ -3137,7 +3347,7 @@
3137
3347
  keys,
3138
3348
  criteria: req.criteria,
3139
3349
  txid,
3140
- userId
3350
+ userId,
3141
3351
  }
3142
3352
  : req.type === 'add'
3143
3353
  ? {
@@ -3147,7 +3357,7 @@
3147
3357
  keys,
3148
3358
  txid,
3149
3359
  userId,
3150
- values
3360
+ values,
3151
3361
  }
3152
3362
  : req.criteria && req.changeSpec
3153
3363
  ? {
@@ -3159,7 +3369,7 @@
3159
3369
  criteria: req.criteria,
3160
3370
  changeSpec: req.changeSpec,
3161
3371
  txid,
3162
- userId
3372
+ userId,
3163
3373
  }
3164
3374
  : updates
3165
3375
  ? {
@@ -3170,7 +3380,7 @@
3170
3380
  keys: updates.keys,
3171
3381
  changeSpecs: updates.changeSpecs,
3172
3382
  txid,
3173
- userId
3383
+ userId,
3174
3384
  }
3175
3385
  : {
3176
3386
  type: 'upsert',
@@ -3179,7 +3389,7 @@
3179
3389
  keys,
3180
3390
  values,
3181
3391
  txid,
3182
- userId
3392
+ userId,
3183
3393
  };
3184
3394
  return keys.length > 0 || ('criteria' in req && req.criteria)
3185
3395
  ? mutsTable
@@ -3189,7 +3399,7 @@
3189
3399
  });
3190
3400
  }
3191
3401
  } });
3192
- }
3402
+ },
3193
3403
  };
3194
3404
  }
3195
3405
 
@@ -3548,6 +3758,20 @@
3548
3758
  }
3549
3759
  }
3550
3760
 
3761
+ class InvalidLicenseError extends Error {
3762
+ constructor(license) {
3763
+ super(license === 'expired'
3764
+ ? `License expired`
3765
+ : license === 'deactivated'
3766
+ ? `User deactivated`
3767
+ : 'Invalid license');
3768
+ this.name = 'InvalidLicenseError';
3769
+ if (license) {
3770
+ this.license = license;
3771
+ }
3772
+ }
3773
+ }
3774
+
3551
3775
  function sleep$1(ms) {
3552
3776
  return new Promise((resolve) => setTimeout(resolve, ms));
3553
3777
  }
@@ -3581,7 +3805,12 @@
3581
3805
  function createObservable() {
3582
3806
  return db.cloud.persistedSyncState.pipe(operators.filter((syncState) => syncState === null || syncState === void 0 ? void 0 : syncState.serverRevision), // Don't connect before there's no initial sync performed.
3583
3807
  operators.take(1), // Don't continue waking up whenever syncState change
3584
- operators.switchMap((syncState) => db.cloud.currentUser.pipe(operators.map((userLogin) => [userLogin, syncState]))), operators.switchMap(([userLogin, syncState]) => userIsReallyActive.pipe(operators.map((isActive) => [isActive ? userLogin : null, syncState]))), operators.switchMap(([userLogin, syncState]) => {
3808
+ operators.switchMap((syncState) => db.cloud.currentUser.pipe(operators.map((userLogin) => [userLogin, syncState]))), operators.switchMap(([userLogin, syncState]) => {
3809
+ /*if (userLogin.license?.status && userLogin.license.status !== 'ok') {
3810
+ throw new InvalidLicenseError();
3811
+ }*/
3812
+ return userIsReallyActive.pipe(operators.map((isActive) => [isActive ? userLogin : null, syncState]));
3813
+ }), operators.switchMap(([userLogin, syncState]) => {
3585
3814
  if ((userLogin === null || userLogin === void 0 ? void 0 : userLogin.isLoggedIn) && !(syncState === null || syncState === void 0 ? void 0 : syncState.realms.includes(userLogin.userId))) {
3586
3815
  // We're in an in-between state when user is logged in but the user's realms are not yet synced.
3587
3816
  // Don't make this change reconnect the websocket just yet. Wait till syncState is updated
@@ -3610,14 +3839,20 @@
3610
3839
  yield db.table('$logins').update(user.userId, {
3611
3840
  accessToken: refreshedLogin.accessToken,
3612
3841
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
3842
+ claims: refreshedLogin.claims,
3843
+ license: refreshedLogin.license,
3613
3844
  });
3614
3845
  })), operators.switchMap(() => createObservable()));
3615
3846
  }
3616
3847
  else {
3617
- return rxjs.throwError(error);
3848
+ return rxjs.throwError(() => error);
3618
3849
  }
3619
3850
  }), operators.catchError((error) => {
3620
3851
  db.cloud.webSocketStatus.next("error");
3852
+ if (error instanceof InvalidLicenseError) {
3853
+ // Don't retry. Just throw and don't try connect again.
3854
+ return rxjs.throwError(() => error);
3855
+ }
3621
3856
  return rxjs.from(waitAndReconnectWhenUserDoesSomething(error)).pipe(operators.switchMap(() => createObservable()));
3622
3857
  }));
3623
3858
  }
@@ -3646,97 +3881,12 @@
3646
3881
  });
3647
3882
  }
3648
3883
 
3649
- const SECONDS = 1000;
3650
- const MINUTES = 60 * SECONDS;
3651
-
3652
- const myId = randomString$1(16);
3653
-
3654
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
3655
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
3656
- function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
3657
- return __awaiter(this, void 0, void 0, function* () {
3658
- // Start working.
3659
- //
3660
- // Check if someone else is working on this already.
3661
- //
3662
- const jobsTable = db.table(jobsTableName);
3663
- function aquireLock() {
3664
- return __awaiter(this, void 0, void 0, function* () {
3665
- const gotTheLock = yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
3666
- const currentWork = yield jobsTable.get(jobName);
3667
- if (!currentWork) {
3668
- // No one else is working. Let's record that we are.
3669
- yield jobsTable.add({
3670
- nodeId: myId,
3671
- started: new Date(),
3672
- heartbeat: new Date()
3673
- }, jobName);
3674
- return true;
3675
- }
3676
- else if (currentWork.heartbeat.getTime() <
3677
- Date.now() - GUARDED_JOB_TIMEOUT) {
3678
- 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!`);
3679
- // Now, take over!
3680
- yield jobsTable.put({
3681
- nodeId: myId,
3682
- started: new Date(),
3683
- heartbeat: new Date()
3684
- }, jobName);
3685
- return true;
3686
- }
3687
- return false;
3688
- }));
3689
- if (gotTheLock)
3690
- return true;
3691
- // Someone else took the job.
3692
- if (awaitRemoteJob) {
3693
- try {
3694
- const jobDoneObservable = rxjs.from(Dexie.liveQuery(() => jobsTable.get(jobName))).pipe(operators.timeout(GUARDED_JOB_TIMEOUT), operators.filter((job) => !job)); // Wait til job is not there anymore.
3695
- yield jobDoneObservable.toPromise();
3696
- return false;
3697
- }
3698
- catch (err) {
3699
- if (err.name !== 'TimeoutError') {
3700
- throw err;
3701
- }
3702
- // Timeout stopped us! Try aquire the lock now.
3703
- // It will likely succeed this time unless
3704
- // another client took it.
3705
- return yield aquireLock();
3706
- }
3707
- }
3708
- return false;
3709
- });
3710
- }
3711
- if (yield aquireLock()) {
3712
- // We own the lock entry and can do our job undisturbed.
3713
- // We're not within a transaction, but these type of locks
3714
- // spans over transactions.
3715
- // Start our heart beat during the job.
3716
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
3717
- const heartbeat = setInterval(() => {
3718
- jobsTable.update(jobName, (job) => {
3719
- if (job.nodeId === myId) {
3720
- job.heartbeat = new Date();
3721
- }
3722
- });
3723
- }, GUARDED_JOB_HEARTBEAT);
3724
- try {
3725
- return yield job();
3726
- }
3727
- finally {
3728
- // Stop heartbeat
3729
- clearInterval(heartbeat);
3730
- // Remove the persisted job state:
3731
- yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
3732
- const currentWork = yield jobsTable.get(jobName);
3733
- if (currentWork && currentWork.nodeId === myId) {
3734
- yield jobsTable.delete(jobName);
3735
- }
3736
- }));
3737
- }
3738
- }
3739
- });
3884
+ function performGuardedJob(db, jobName, job) {
3885
+ if (typeof navigator === 'undefined' || !navigator.locks) {
3886
+ // No support for guarding jobs. IE11, node.js, etc.
3887
+ return job();
3888
+ }
3889
+ return navigator.locks.request(db.name + '|' + jobName, () => job());
3740
3890
  }
3741
3891
 
3742
3892
  const ongoingSyncs = new WeakMap();
@@ -3790,6 +3940,9 @@
3790
3940
  function _syncIfPossible() {
3791
3941
  return __awaiter(this, void 0, void 0, function* () {
3792
3942
  try {
3943
+ // Check if should delay sync due to ratelimit:
3944
+ yield checkSyncRateLimitDelay(db);
3945
+ // Check if we need to lock the sync job. Not needed if we are the service worker.
3793
3946
  if (db.cloud.isServiceWorkerDB) {
3794
3947
  // We are the dedicated sync SW:
3795
3948
  yield sync(db, cloudOptions, cloudSchema, options);
@@ -3797,7 +3950,7 @@
3797
3950
  else if (!db.cloud.usingServiceWorker) {
3798
3951
  // We use a flow that is better suited for the case when multiple workers want to
3799
3952
  // do the same thing.
3800
- yield performGuardedJob(db, CURRENT_SYNC_WORKER, '$jobs', () => sync(db, cloudOptions, cloudSchema, options));
3953
+ yield performGuardedJob(db, CURRENT_SYNC_WORKER, () => sync(db, cloudOptions, cloudSchema, options));
3801
3954
  }
3802
3955
  else {
3803
3956
  assert(false);
@@ -3819,19 +3972,29 @@
3819
3972
  }
3820
3973
  }
3821
3974
 
3975
+ const SECONDS = 1000;
3976
+ const MINUTES = 60 * SECONDS;
3977
+
3822
3978
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
3823
3979
  let localSyncEventSubscription = null;
3824
3980
  //let syncHandler: ((event: Event) => void) | null = null;
3825
3981
  //let periodicSyncHandler: ((event: Event) => void) | null = null;
3826
3982
  let cancelToken = { cancelled: false };
3983
+ let retryHandle = null;
3984
+ let retryPurpose = null; // "pull" is superset of "push"
3827
3985
  function syncAndRetry(purpose, retryNum = 1) {
3828
3986
  // Use setTimeout() to get onto a clean stack and
3829
3987
  // break free from possible active transaction:
3830
3988
  setTimeout(() => {
3989
+ if (retryHandle)
3990
+ clearTimeout(retryHandle);
3991
+ const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
3992
+ retryHandle = null;
3993
+ retryPurpose = null;
3831
3994
  syncIfPossible(db, cloudOptions, cloudSchema, {
3832
3995
  cancelToken,
3833
3996
  retryImmediatelyOnFetchError: true,
3834
- purpose,
3997
+ purpose: combPurpose,
3835
3998
  }).catch((e) => {
3836
3999
  console.error('error in syncIfPossible()', e);
3837
4000
  if (cancelToken.cancelled) {
@@ -3841,7 +4004,13 @@
3841
4004
  // Mimic service worker sync event: retry 3 times
3842
4005
  // * first retry after 5 minutes
3843
4006
  // * second retry 15 minutes later
3844
- setTimeout(() => syncAndRetry(purpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
4007
+ const combinedPurpose = retryPurpose && retryPurpose === 'pull' ? 'pull' : purpose;
4008
+ const handle = setTimeout(() => syncAndRetry(combinedPurpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
4009
+ // Cancel the previous retryHandle if it exists to avoid scheduling loads of retries.
4010
+ if (retryHandle)
4011
+ clearTimeout(retryHandle);
4012
+ retryHandle = handle;
4013
+ retryPurpose = combinedPurpose;
3845
4014
  }
3846
4015
  });
3847
4016
  }, 0);
@@ -3908,10 +4077,12 @@
3908
4077
  },
3909
4078
  Alert: {
3910
4079
  error: {
3911
- color: "red"
4080
+ color: "red",
4081
+ fontWeight: "bold"
3912
4082
  },
3913
4083
  warning: {
3914
- color: "yellow"
4084
+ color: "#f80",
4085
+ fontWeight: "bold"
3915
4086
  },
3916
4087
  info: {
3917
4088
  color: "black"
@@ -3952,7 +4123,8 @@
3952
4123
  border: "3px solid #3d3d5d",
3953
4124
  borderRadius: "8px",
3954
4125
  boxShadow: "0 0 80px 10px #666",
3955
- width: "auto"
4126
+ width: "auto",
4127
+ fontFamily: "sans-serif",
3956
4128
  },
3957
4129
  Input: {
3958
4130
  height: "35px",
@@ -3973,11 +4145,26 @@
3973
4145
 
3974
4146
  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}
3975
4147
 
4148
+ /** Resolve a message template with parameters.
4149
+ *
4150
+ * Example:
4151
+ * resolveText({
4152
+ * message: "Hello {name}!",
4153
+ * messageCode: "HELLO",
4154
+ * messageParams: {name: "David"}
4155
+ * }) => "Hello David!"
4156
+ *
4157
+ * @param message Template message with {vars} in it.
4158
+ * @param messageCode Unique code for the message. Can be used for translation.
4159
+ * @param messageParams Parameters to be used in the message.
4160
+ * @returns A final message where parameters have been replaced with values.
4161
+ */
3976
4162
  function resolveText({ message, messageCode, messageParams }) {
3977
- return message.replace(/\{\w+\}/ig, n => messageParams[n.substr(1, n.length - 2)]);
4163
+ return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
3978
4164
  }
3979
4165
 
3980
- function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
4166
+ const OTP_LENGTH = 8;
4167
+ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
3981
4168
  const [params, setParams] = p({});
3982
4169
  const firstFieldRef = _(null);
3983
4170
  s(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
@@ -3985,21 +4172,34 @@
3985
4172
  h(p$1, null,
3986
4173
  h("h3", { style: Styles.WindowHeader }, title),
3987
4174
  alerts.map((alert) => (h("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
3988
- h("form", { onSubmit: ev => {
4175
+ h("form", { onSubmit: (ev) => {
3989
4176
  ev.preventDefault();
3990
4177
  onSubmit(params);
3991
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label },
4178
+ } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label, key: idx },
3992
4179
  label ? `${label}: ` : '',
3993
- 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']) })); } })))))),
4180
+ 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) => {
4181
+ var _a;
4182
+ const value = valueTransformer(type, (_a = ev.target) === null || _a === void 0 ? void 0 : _a['value']);
4183
+ let updatedParams = Object.assign(Object.assign({}, params), { [fieldName]: value });
4184
+ setParams(updatedParams);
4185
+ if (type === 'otp' && (value === null || value === void 0 ? void 0 : value.trim().length) === OTP_LENGTH) {
4186
+ // Auto-submit when OTP is filled in.
4187
+ onSubmit(updatedParams);
4188
+ }
4189
+ } })))))),
3994
4190
  h("div", { style: Styles.ButtonsDiv },
3995
- h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, "Submit"),
3996
- h("button", { style: Styles.Button, onClick: onCancel }, "Cancel"))));
4191
+ h(p$1, null,
4192
+ h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, submitLabel),
4193
+ cancelLabel && (h("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
3997
4194
  }
3998
4195
  function valueTransformer(type, value) {
3999
4196
  switch (type) {
4000
- case "email": return value.toLowerCase();
4001
- case "otp": return value.toUpperCase();
4002
- default: return value;
4197
+ case 'email':
4198
+ return value.toLowerCase();
4199
+ case 'otp':
4200
+ return value.toUpperCase();
4201
+ default:
4202
+ return value;
4003
4203
  }
4004
4204
  }
4005
4205
 
@@ -4053,11 +4253,20 @@
4053
4253
  }
4054
4254
  };
4055
4255
  }
4056
- // TODO:
4057
- /*
4058
- * Gjort klart allt kring user interaction förutom att mounta default-ui på ett element.
4059
- * Också att kolla först om nån annan subscribar och i så fall inte göra nåt.
4060
- */
4256
+
4257
+ function associate(factory) {
4258
+ const wm = new WeakMap();
4259
+ return (x) => {
4260
+ let rv = wm.get(x);
4261
+ if (!rv) {
4262
+ rv = factory(x);
4263
+ wm.set(x, rv);
4264
+ }
4265
+ return rv;
4266
+ };
4267
+ }
4268
+
4269
+ const getCurrentUserEmitter = associate((db) => new rxjs.BehaviorSubject(UNAUTHORIZED_USER));
4061
4270
 
4062
4271
  function computeSyncState(db) {
4063
4272
  let _prevStatus = db.cloud.webSocketStatus.value;
@@ -4085,8 +4294,17 @@
4085
4294
  return rxjs.combineLatest([
4086
4295
  lazyWebSocketStatus,
4087
4296
  db.syncStateChangedEvent.pipe(operators.startWith({ phase: 'initial' })),
4297
+ getCurrentUserEmitter(db.dx._novip),
4088
4298
  userIsReallyActive
4089
- ]).pipe(operators.map(([status, syncState, userIsActive]) => {
4299
+ ]).pipe(operators.map(([status, syncState, user, userIsActive]) => {
4300
+ var _a;
4301
+ if (((_a = user.license) === null || _a === void 0 ? void 0 : _a.status) && user.license.status !== 'ok') {
4302
+ return {
4303
+ phase: 'offline',
4304
+ status: 'offline',
4305
+ license: user.license.status
4306
+ };
4307
+ }
4090
4308
  let { phase, error, progress } = syncState;
4091
4309
  let adjustedStatus = status;
4092
4310
  if (phase === 'error') {
@@ -4119,23 +4337,12 @@
4119
4337
  error,
4120
4338
  progress,
4121
4339
  status: isOnline ? adjustedStatus : 'offline',
4340
+ license: 'ok'
4122
4341
  };
4123
4342
  return retState;
4124
4343
  }));
4125
4344
  }
4126
4345
 
4127
- function associate(factory) {
4128
- const wm = new WeakMap();
4129
- return (x) => {
4130
- let rv = wm.get(x);
4131
- if (!rv) {
4132
- rv = factory(x);
4133
- wm.set(x, rv);
4134
- }
4135
- return rv;
4136
- };
4137
- }
4138
-
4139
4346
  function createSharedValueObservable(o, defaultValue) {
4140
4347
  let currentValue = defaultValue;
4141
4348
  let shared = rxjs.from(o).pipe(rxjs.map((x) => (currentValue = x)), rxjs.share({ resetOnRefCountZero: () => rxjs.timer(1000) }));
@@ -4177,8 +4384,6 @@
4177
4384
  })), {});
4178
4385
  });
4179
4386
 
4180
- const getCurrentUserEmitter = associate((db) => new rxjs.BehaviorSubject(UNAUTHORIZED_USER));
4181
-
4182
4387
  const getInternalAccessControlObservable = associate((db) => {
4183
4388
  return createSharedValueObservable(getCurrentUserEmitter(db._novip).pipe(operators.switchMap((currentUser) => Dexie.liveQuery(() => db.transaction('r', 'realms', 'members', () => Promise.all([
4184
4389
  db.members.where({ userId: currentUser.userId }).toArray(),
@@ -4461,7 +4666,7 @@
4461
4666
  });
4462
4667
  const syncComplete = new rxjs.Subject();
4463
4668
  dexie.cloud = {
4464
- version: '4.0.1-beta.44',
4669
+ version: '4.0.1-beta.47',
4465
4670
  options: Object.assign({}, DEFAULT_OPTIONS),
4466
4671
  schema: null,
4467
4672
  get currentUserId() {
@@ -4497,11 +4702,24 @@
4497
4702
  }
4498
4703
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
4499
4704
  },
4705
+ logout({ force } = {}) {
4706
+ return __awaiter(this, void 0, void 0, function* () {
4707
+ force
4708
+ ? yield _logout(DexieCloudDB(dexie), { deleteUnsyncedData: true })
4709
+ : yield logout(DexieCloudDB(dexie));
4710
+ });
4711
+ },
4500
4712
  sync({ wait, purpose } = { wait: true, purpose: 'push' }) {
4713
+ var _a;
4501
4714
  return __awaiter(this, void 0, void 0, function* () {
4502
4715
  if (wait === undefined)
4503
4716
  wait = true;
4504
4717
  const db = DexieCloudDB(dexie);
4718
+ const licenseStatus = ((_a = db.cloud.currentUser.value.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
4719
+ if (licenseStatus !== 'ok') {
4720
+ // Refresh access token to check for updated license
4721
+ yield loadAccessToken(db);
4722
+ }
4505
4723
  if (purpose === 'pull') {
4506
4724
  const syncState = db.cloud.persistedSyncState.value;
4507
4725
  triggerSync(db, purpose);
@@ -4705,7 +4923,9 @@
4705
4923
  db.syncStateChangedEvent.next({
4706
4924
  phase: 'not-in-sync',
4707
4925
  });
4708
- triggerSync(db, 'push');
4926
+ if (!isEagerSyncDisabled(db)) {
4927
+ triggerSync(db, 'push');
4928
+ }
4709
4929
  }), rxjs.fromEvent(self, 'offline').subscribe(() => {
4710
4930
  console.debug('offline!');
4711
4931
  db.syncStateChangedEvent.next({
@@ -4722,7 +4942,7 @@
4722
4942
  });
4723
4943
  }
4724
4944
  }
4725
- dexieCloud.version = '4.0.1-beta.44';
4945
+ dexieCloud.version = '4.0.1-beta.47';
4726
4946
  Dexie__default["default"].Cloud = dexieCloud;
4727
4947
 
4728
4948
  // In case the SW lives for a while, let it reuse already opened connections: