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
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.0.1-beta.46, Sat Aug 05 2023
11
+ * Version 4.0.1-beta.47, Fri Oct 13 2023
12
12
  *
13
13
  * https://dexie.org
14
14
  *
@@ -1924,58 +1924,6 @@
1924
1924
  identity;
1925
1925
  }
1926
1926
 
1927
- var TimeoutError = createErrorClass(function (_super) {
1928
- return function TimeoutErrorImpl(info) {
1929
- if (info === void 0) { info = null; }
1930
- _super(this);
1931
- this.message = 'Timeout has occurred';
1932
- this.name = 'TimeoutError';
1933
- this.info = info;
1934
- };
1935
- });
1936
- function timeout(config, schedulerArg) {
1937
- var _a = (isValidDate(config) ? { first: config } : typeof config === 'number' ? { each: config } : config), first = _a.first, each = _a.each, _b = _a.with, _with = _b === void 0 ? timeoutErrorFactory : _b, _c = _a.scheduler, scheduler = _c === void 0 ? schedulerArg !== null && schedulerArg !== void 0 ? schedulerArg : asyncScheduler : _c, _d = _a.meta, meta = _d === void 0 ? null : _d;
1938
- if (first == null && each == null) {
1939
- throw new TypeError('No timeout provided.');
1940
- }
1941
- return operate(function (source, subscriber) {
1942
- var originalSourceSubscription;
1943
- var timerSubscription;
1944
- var lastValue = null;
1945
- var seen = 0;
1946
- var startTimer = function (delay) {
1947
- timerSubscription = executeSchedule(subscriber, scheduler, function () {
1948
- try {
1949
- originalSourceSubscription.unsubscribe();
1950
- innerFrom(_with({
1951
- meta: meta,
1952
- lastValue: lastValue,
1953
- seen: seen,
1954
- })).subscribe(subscriber);
1955
- }
1956
- catch (err) {
1957
- subscriber.error(err);
1958
- }
1959
- }, delay);
1960
- };
1961
- originalSourceSubscription = source.subscribe(createOperatorSubscriber(subscriber, function (value) {
1962
- timerSubscription === null || timerSubscription === void 0 ? void 0 : timerSubscription.unsubscribe();
1963
- seen++;
1964
- subscriber.next((lastValue = value));
1965
- each > 0 && startTimer(each);
1966
- }, undefined, undefined, function () {
1967
- if (!(timerSubscription === null || timerSubscription === void 0 ? void 0 : timerSubscription.closed)) {
1968
- timerSubscription === null || timerSubscription === void 0 ? void 0 : timerSubscription.unsubscribe();
1969
- }
1970
- lastValue = null;
1971
- }));
1972
- !seen && startTimer(first != null ? (typeof first === 'number' ? first : +first - scheduler.now()) : each);
1973
- });
1974
- }
1975
- function timeoutErrorFactory(info) {
1976
- throw new TimeoutError(info);
1977
- }
1978
-
1979
1927
  //const hasSW = 'serviceWorker' in navigator;
1980
1928
  let hasComplainedAboutSyncEvent = false;
1981
1929
  function registerSyncEvent(db, purpose) {
@@ -2036,6 +1984,7 @@
2036
1984
 
2037
1985
  function triggerSync(db, purpose) {
2038
1986
  if (db.cloud.usingServiceWorker) {
1987
+ console.debug('registering sync event');
2039
1988
  registerSyncEvent(db, purpose);
2040
1989
  }
2041
1990
  else {
@@ -2067,14 +2016,24 @@
2067
2016
  return btoa(String.fromCharCode.apply(null, ArrayBuffer.isView(b) ? b : new Uint8Array(b)));
2068
2017
  };
2069
2018
 
2019
+ class TokenErrorResponseError extends Error {
2020
+ constructor({ title, message, messageCode, messageParams, }) {
2021
+ super(message);
2022
+ this.name = 'TokenErrorResponseError';
2023
+ this.title = title;
2024
+ this.messageCode = messageCode;
2025
+ this.messageParams = messageParams;
2026
+ }
2027
+ }
2028
+
2070
2029
  function interactWithUser(userInteraction, req) {
2071
2030
  return new Promise((resolve, reject) => {
2072
- const interactionProps = Object.assign(Object.assign({}, req), { onSubmit: (res) => {
2031
+ const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
2073
2032
  userInteraction.next(undefined);
2074
2033
  resolve(res);
2075
2034
  }, onCancel: () => {
2076
2035
  userInteraction.next(undefined);
2077
- reject(new Dexie__default["default"].AbortError("User cancelled"));
2036
+ reject(new Dexie__default["default"].AbortError('User cancelled'));
2078
2037
  } });
2079
2038
  userInteraction.next(interactionProps);
2080
2039
  // Start subscribing for external updates to db.cloud.userInteraction, and if so, cancel this request.
@@ -2093,7 +2052,9 @@
2093
2052
  type: 'message-alert',
2094
2053
  title,
2095
2054
  alerts,
2096
- fields: {}
2055
+ fields: {},
2056
+ submitLabel: 'OK',
2057
+ cancelLabel: null,
2097
2058
  });
2098
2059
  }
2099
2060
  function promptForEmail(userInteraction, title, emailHint) {
@@ -2152,22 +2113,48 @@
2152
2113
  return otp;
2153
2114
  });
2154
2115
  }
2116
+ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
2117
+ return __awaiter(this, void 0, void 0, function* () {
2118
+ const alerts = [
2119
+ {
2120
+ type: 'warning',
2121
+ messageCode: 'LOGOUT_CONFIRMATION',
2122
+ message: `{numUnsyncedChanges} unsynced changes will get lost!
2123
+ Logout anyway?`,
2124
+ messageParams: {
2125
+ currentUserId,
2126
+ numUnsyncedChanges: numUnsyncedChanges.toString(),
2127
+ }
2128
+ },
2129
+ ];
2130
+ return yield interactWithUser(userInteraction, {
2131
+ type: 'logout-confirmation',
2132
+ title: 'Confirm Logout',
2133
+ alerts,
2134
+ fields: {},
2135
+ submitLabel: 'Confirm logout',
2136
+ cancelLabel: 'Cancel'
2137
+ })
2138
+ .then(() => true)
2139
+ .catch(() => false);
2140
+ });
2141
+ }
2155
2142
 
2156
2143
  function loadAccessToken(db) {
2157
- var _a, _b;
2144
+ var _a, _b, _c;
2158
2145
  return __awaiter(this, void 0, void 0, function* () {
2159
2146
  const currentUser = yield db.getCurrentUser();
2160
2147
  const { accessToken, accessTokenExpiration, refreshToken, refreshTokenExpiration, claims, } = currentUser;
2161
2148
  if (!accessToken)
2162
- return;
2149
+ return null;
2163
2150
  const expTime = (_a = accessTokenExpiration === null || accessTokenExpiration === void 0 ? void 0 : accessTokenExpiration.getTime()) !== null && _a !== void 0 ? _a : Infinity;
2164
- if (expTime > Date.now()) {
2165
- return accessToken;
2151
+ if (expTime > Date.now() && (((_b = currentUser.license) === null || _b === void 0 ? void 0 : _b.status) || 'ok') === 'ok') {
2152
+ return currentUser;
2166
2153
  }
2167
2154
  if (!refreshToken) {
2168
2155
  throw new Error(`Refresh token missing`);
2169
2156
  }
2170
- const refreshExpTime = (_b = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _b !== void 0 ? _b : Infinity;
2157
+ const refreshExpTime = (_c = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _c !== void 0 ? _c : Infinity;
2171
2158
  if (refreshExpTime <= Date.now()) {
2172
2159
  throw new Error(`Refresh token has expired`);
2173
2160
  }
@@ -2175,8 +2162,10 @@
2175
2162
  yield db.table('$logins').update(claims.sub, {
2176
2163
  accessToken: refreshedLogin.accessToken,
2177
2164
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
2165
+ claims: refreshedLogin.claims,
2166
+ license: refreshedLogin.license,
2178
2167
  });
2179
- return refreshedLogin.accessToken;
2168
+ return refreshedLogin;
2180
2169
  });
2181
2170
  }
2182
2171
  function authenticate(url, context, fetchToken, userInteraction, hints) {
@@ -2224,10 +2213,24 @@
2224
2213
  if (res.status !== 200)
2225
2214
  throw new Error(`RefreshToken: Status ${res.status} from ${url}/token`);
2226
2215
  const response = yield res.json();
2216
+ if (response.type === 'error') {
2217
+ throw new TokenErrorResponseError(response);
2218
+ }
2227
2219
  login.accessToken = response.accessToken;
2228
2220
  login.accessTokenExpiration = response.accessTokenExpiration
2229
2221
  ? new Date(response.accessTokenExpiration)
2230
2222
  : undefined;
2223
+ login.claims = response.claims;
2224
+ login.license = {
2225
+ type: response.userType,
2226
+ status: response.claims.license || 'ok',
2227
+ };
2228
+ if (response.evalDaysLeft != null) {
2229
+ login.license.evalDaysLeft = response.evalDaysLeft;
2230
+ }
2231
+ if (response.userValidUntil != null) {
2232
+ login.license.validUntil = new Date(response.userValidUntil);
2233
+ }
2231
2234
  return login;
2232
2235
  });
2233
2236
  }
@@ -2259,8 +2262,15 @@
2259
2262
  public_key: publicKeyPEM,
2260
2263
  hints,
2261
2264
  });
2265
+ if (response2.type === 'error') {
2266
+ throw new TokenErrorResponseError(response2);
2267
+ }
2262
2268
  if (response2.type !== 'tokens')
2263
2269
  throw new Error(`Unexpected response type from token endpoint: ${response2.type}`);
2270
+ /*const licenseStatus = response2.claims.license || 'ok';
2271
+ if (licenseStatus !== 'ok') {
2272
+ throw new InvalidLicenseError(licenseStatus);
2273
+ }*/
2264
2274
  context.accessToken = response2.accessToken;
2265
2275
  context.accessTokenExpiration = new Date(response2.accessTokenExpiration);
2266
2276
  context.refreshToken = response2.refreshToken;
@@ -2271,6 +2281,16 @@
2271
2281
  context.email = response2.claims.email;
2272
2282
  context.name = response2.claims.name;
2273
2283
  context.claims = response2.claims;
2284
+ context.license = {
2285
+ type: response2.userType,
2286
+ status: response2.claims.license || 'ok',
2287
+ };
2288
+ if (response2.evalDaysLeft != null) {
2289
+ context.license.evalDaysLeft = response2.evalDaysLeft;
2290
+ }
2291
+ if (response2.userValidUntil != null) {
2292
+ context.license.validUntil = new Date(response2.userValidUntil);
2293
+ }
2274
2294
  if (response2.alerts && response2.alerts.length > 0) {
2275
2295
  yield interactWithUser(userInteraction, {
2276
2296
  type: 'message-alert',
@@ -2282,12 +2302,36 @@
2282
2302
  return context;
2283
2303
  }
2284
2304
  catch (error) {
2285
- yield alertUser(userInteraction, 'Authentication Failed', {
2286
- type: 'error',
2287
- messageCode: 'GENERIC_ERROR',
2288
- message: `We're having a problem authenticating right now.`,
2289
- messageParams: {},
2290
- }).catch(() => { });
2305
+ if (error instanceof TokenErrorResponseError) {
2306
+ yield alertUser(userInteraction, error.title, {
2307
+ type: 'error',
2308
+ messageCode: error.messageCode,
2309
+ message: error.message,
2310
+ messageParams: {},
2311
+ });
2312
+ throw error;
2313
+ }
2314
+ let message = `We're having a problem authenticating right now.`;
2315
+ console.error(`Error authenticating`, error);
2316
+ if (error instanceof TypeError) {
2317
+ const isOffline = typeof navigator !== undefined && !navigator.onLine;
2318
+ if (isOffline) {
2319
+ message = `You seem to be offline. Please connect to the internet and try again.`;
2320
+ }
2321
+ else if (Dexie__default["default"].debug || (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1'))) {
2322
+ // The audience is most likely the developer. Suggest to whitelist the localhost origin:
2323
+ message = `Could not connect to server. Please verify that your origin '${location.origin}' is whitelisted using \`npx dexie-cloud whitelist\``;
2324
+ }
2325
+ else {
2326
+ message = `Could not connect to server. Please verify the connection.`;
2327
+ }
2328
+ yield alertUser(userInteraction, 'Authentication Failed', {
2329
+ type: 'error',
2330
+ messageCode: 'GENERIC_ERROR',
2331
+ message,
2332
+ messageParams: {},
2333
+ }).catch(() => { });
2334
+ }
2291
2335
  throw error;
2292
2336
  }
2293
2337
  });
@@ -2334,6 +2378,75 @@
2334
2378
  }
2335
2379
  }
2336
2380
 
2381
+ const UNAUTHORIZED_USER = {
2382
+ userId: "unauthorized",
2383
+ name: "Unauthorized",
2384
+ claims: {
2385
+ sub: "unauthorized",
2386
+ },
2387
+ lastLogin: new Date(0)
2388
+ };
2389
+ try {
2390
+ Object.freeze(UNAUTHORIZED_USER);
2391
+ Object.freeze(UNAUTHORIZED_USER.claims);
2392
+ }
2393
+ catch (_a) { }
2394
+
2395
+ function waitUntil(o, // Works with Dexie's liveQuery observables if we'd need that
2396
+ predicate) {
2397
+ return rxjs.firstValueFrom(rxjs.from(o).pipe(rxjs.filter(predicate)));
2398
+ }
2399
+
2400
+ function logout(db) {
2401
+ return __awaiter(this, void 0, void 0, function* () {
2402
+ const numUnsyncedChanges = yield _logout(db);
2403
+ if (numUnsyncedChanges) {
2404
+ if (yield confirmLogout(db.cloud.userInteraction, db.cloud.currentUserId, numUnsyncedChanges)) {
2405
+ yield _logout(db, { deleteUnsyncedData: true });
2406
+ }
2407
+ else {
2408
+ throw new Error(`User cancelled logout due to unsynced changes`);
2409
+ }
2410
+ }
2411
+ });
2412
+ }
2413
+ function _logout(db, { deleteUnsyncedData = false } = {}) {
2414
+ return __awaiter(this, void 0, void 0, function* () {
2415
+ // Clear the database without emptying configuration options.
2416
+ const [numUnsynced, loggedOut] = yield db.dx.transaction('rw', db.dx.tables, (tx) => __awaiter(this, void 0, void 0, function* () {
2417
+ // @ts-ignore
2418
+ const idbtrans = tx.idbtrans;
2419
+ idbtrans.disableChangeTracking = true;
2420
+ idbtrans.disableAccessControl = true;
2421
+ const mutationTables = tx.storeNames.filter((tableName) => tableName.endsWith('_mutations'));
2422
+ // Count unsynced changes
2423
+ const unsyncCounts = yield Promise.all(mutationTables.map((mutationTable) => tx.table(mutationTable).count()));
2424
+ const sumUnSynced = unsyncCounts.reduce((a, b) => a + b, 0);
2425
+ if (sumUnSynced > 0 && !deleteUnsyncedData) {
2426
+ // Let caller ask user if they want to delete unsynced data.
2427
+ return [sumUnSynced, false];
2428
+ }
2429
+ // Either there are no unsynched changes, or caller provided flag deleteUnsynchedData = true.
2430
+ // Clear all tables except $jobs and $syncState (except the persisted sync state which is
2431
+ // also cleared because we're going to rebuild it using a fresh sync).
2432
+ db.$syncState.delete('syncState');
2433
+ for (const table of db.dx.tables) {
2434
+ if (table.name !== '$jobs' && table.name !== '$syncState') {
2435
+ table.clear();
2436
+ }
2437
+ }
2438
+ return [sumUnSynced, true];
2439
+ }));
2440
+ if (loggedOut) {
2441
+ // Wait for currentUser observable to emit UNAUTHORIZED_USER
2442
+ yield waitUntil(db.cloud.currentUser, (user) => user.userId === UNAUTHORIZED_USER.userId);
2443
+ // Then perform an initial sync
2444
+ yield db.cloud.sync({ purpose: 'pull', wait: true });
2445
+ }
2446
+ return numUnsynced;
2447
+ });
2448
+ }
2449
+
2337
2450
  class HttpError extends Error {
2338
2451
  constructor(res, message) {
2339
2452
  super(message || `${res.status} ${res.statusText}`);
@@ -2387,8 +2500,9 @@
2387
2500
  throw new HttpError(res1, errMsg);
2388
2501
  }
2389
2502
  const response = yield res1.json();
2390
- if (response.type === 'tokens') {
2503
+ if (response.type === 'tokens' || response.type === 'error') {
2391
2504
  // Demo user request can get a "tokens" response right away
2505
+ // Error can also be returned right away.
2392
2506
  return response;
2393
2507
  }
2394
2508
  else if (tokenRequest.grant_type === 'otp') {
@@ -2420,12 +2534,6 @@
2420
2534
  }
2421
2535
  if (res2.status !== 200) {
2422
2536
  const errMsg = yield res2.text();
2423
- yield alertUser(userInteraction, "OTP Authentication Failed", {
2424
- type: 'error',
2425
- messageCode: 'GENERIC_ERROR',
2426
- message: errMsg,
2427
- messageParams: {}
2428
- }).catch(() => { });
2429
2537
  throw new HttpError(res2, errMsg);
2430
2538
  }
2431
2539
  const response2 = yield res2.json();
@@ -2438,6 +2546,18 @@
2438
2546
  };
2439
2547
  }
2440
2548
 
2549
+ /** A way to log to console in production without terser stripping out
2550
+ * it from the release bundle.
2551
+ * This should be used very rarely and only in places where it's
2552
+ * absolutely necessary to log something in production.
2553
+ *
2554
+ * @param level
2555
+ * @param args
2556
+ */
2557
+ function prodLog(level, ...args) {
2558
+ globalThis["con" + "sole"][level](...args);
2559
+ }
2560
+
2441
2561
  /** This function changes or sets the current user as requested.
2442
2562
  *
2443
2563
  * Use cases:
@@ -2464,102 +2584,76 @@
2464
2584
  }));
2465
2585
  user.isLoggedIn = true;
2466
2586
  user.lastLogin = new Date();
2467
- yield user.save();
2468
- console.debug('Saved new user', user.email);
2469
- }));
2470
- yield new Promise((resolve) => {
2471
- if (db.cloud.currentUserId === user.userId) {
2472
- resolve(null);
2587
+ try {
2588
+ yield user.save();
2473
2589
  }
2474
- else {
2475
- const subscription = db.cloud.currentUser.subscribe((currentUser) => {
2476
- if (currentUser.userId === user.userId) {
2477
- subscription.unsubscribe();
2478
- resolve(null);
2590
+ catch (e) {
2591
+ try {
2592
+ if (e.name === 'DataCloneError') {
2593
+ // We've seen this buggy behavior in some browsers and in case it happens
2594
+ // again we really need to collect the details to understand what's going on.
2595
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2596
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2597
+ prodLog('debug', `Login context:`, user);
2598
+ prodLog('debug', `Login context JSON:`, JSON.stringify(user));
2479
2599
  }
2480
- });
2600
+ }
2601
+ catch (_a) { }
2602
+ throw e;
2481
2603
  }
2482
- });
2483
- // TANKAR!!!!
2484
- // V: Service workern kommer inte ha tillgång till currentUserObservable om den inte istället härrör från ett liveQuery.
2485
- // V: Samma med andra windows.
2486
- // V: Så kanske göra om den till att häröra från liveQuery som läser $logins.orderBy('lastLogin').last().
2487
- // V: Då bara vara medveten om:
2488
- // V: En sån observable börjar hämta data vid första subscribe
2489
- // V: Vi har inget "inital value" men kan emulera det till att vara ANONYMOUS_USER
2490
- // V: Om requireAuth är true, så borde db.on(ready) hålla databasen stängd för alla utom denna observable.
2491
- // V: Om inte så behöver den inte blocka.
2492
- // Andra tankar:
2493
- // * Man kan inte byta användare när man är offline. Skulle gå att flytta realms till undanstuff-tabell vid user-change.
2494
- // men troligen inte värt det.
2495
- // * Istället: sälj inte inte switch-user funktionalitet utan tala enbart om inloggat vs icke inloggat läge.
2496
- // * populate $logins med ANONYMOUS så att en påbörjad inloggning inte räknas, alternativt ha en boolean prop!
2497
- // Kanske bäst ha en boolean prop!
2498
- // * Alternativ switch-user funktionalitet:
2499
- // * DBCore gömmer data från realms man inte har tillgång till.
2500
- // * Cursor impl behövs också då.
2501
- // * Då blir det snabba user switch.
2502
- // * claims-settet som skickas till servern blir summan av alla claims. Då måste servern stödja multipla tokens eller
2503
- // att ens token är ett samlad.
2604
+ console.debug('Saved new user', user.email);
2605
+ }));
2606
+ yield waitUntil(db.cloud.currentUser, (currentUser) => currentUser.userId === user.userId);
2504
2607
  });
2505
2608
  }
2506
2609
 
2507
2610
  function login(db, hints) {
2611
+ var _a;
2508
2612
  return __awaiter(this, void 0, void 0, function* () {
2509
2613
  const currentUser = yield db.getCurrentUser();
2510
- if (currentUser.isLoggedIn) {
2511
- if (hints) {
2512
- if (hints.email && db.cloud.currentUser.value.email !== hints.email) {
2513
- throw new Error(`Must logout before changing user`);
2514
- }
2515
- if (hints.userId && db.cloud.currentUserId !== hints.userId) {
2516
- throw new Error(`Must logout before changing user`);
2517
- }
2614
+ const origUserId = currentUser.userId;
2615
+ if (currentUser.isLoggedIn && (!hints || (!hints.email && !hints.userId))) {
2616
+ const licenseStatus = ((_a = currentUser.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
2617
+ if (licenseStatus === 'ok' && currentUser.accessToken && (!currentUser.accessTokenExpiration || currentUser.accessTokenExpiration.getTime() > Date.now())) {
2618
+ // Already authenticated according to given hints. And license is valid.
2619
+ return false;
2518
2620
  }
2519
- // Already authenticated according to given hints.
2520
- return false;
2621
+ if (currentUser.refreshToken && (!currentUser.refreshTokenExpiration || currentUser.refreshTokenExpiration.getTime() > Date.now())) {
2622
+ // Refresh the token
2623
+ yield loadAccessToken(db);
2624
+ return false;
2625
+ }
2626
+ // No refresh token - must re-authenticate:
2521
2627
  }
2522
2628
  const context = new AuthPersistedContext(db, {
2523
2629
  claims: {},
2524
2630
  lastLogin: new Date(0),
2525
2631
  });
2526
2632
  yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
2527
- try {
2528
- yield context.save();
2529
- }
2530
- catch (e) {
2531
- try {
2532
- if (e.name === 'DataCloneError') {
2533
- console.debug(`Login context property names:`, Object.keys(context));
2534
- console.debug(`Login context:`, context);
2535
- console.debug(`Login context JSON:`, JSON.stringify(context));
2536
- }
2537
- }
2538
- catch (_a) { }
2539
- throw e;
2540
- }
2633
+ if (origUserId !== UNAUTHORIZED_USER.userId && context.userId !== origUserId) {
2634
+ // User was logged in before, but now logged in as another user.
2635
+ yield logout(db);
2636
+ }
2637
+ /*try {
2638
+ await context.save();
2639
+ } catch (e) {
2640
+ try {
2641
+ if (e.name === 'DataCloneError') {
2642
+ console.debug(`Login context property names:`, Object.keys(context));
2643
+ console.debug(`Login context:`, context);
2644
+ console.debug(`Login context JSON:`, JSON.stringify(context));
2645
+ }
2646
+ } catch {}
2647
+ throw e;
2648
+ }*/
2541
2649
  yield setCurrentUser(db, context);
2542
2650
  // Make sure to resync as the new login will be authorized
2543
2651
  // for new realms.
2544
2652
  triggerSync(db, "pull");
2545
- return true;
2653
+ return context.userId !== origUserId;
2546
2654
  });
2547
2655
  }
2548
2656
 
2549
- const UNAUTHORIZED_USER = {
2550
- userId: "unauthorized",
2551
- name: "Unauthorized",
2552
- claims: {
2553
- sub: "unauthorized",
2554
- },
2555
- lastLogin: new Date(0)
2556
- };
2557
- try {
2558
- Object.freeze(UNAUTHORIZED_USER);
2559
- Object.freeze(UNAUTHORIZED_USER.claims);
2560
- }
2561
- catch (_a) { }
2562
-
2563
2657
  const swHolder = {};
2564
2658
  const swContainer = typeof self !== 'undefined' && self.document && // self.document is to verify we're not the SW ourself
2565
2659
  typeof navigator !== 'undefined' && navigator.serviceWorker;
@@ -3456,6 +3550,40 @@
3456
3550
  : change.muts.map((m) => (Object.assign(Object.assign({}, m), { keys: m.keys.slice() }))) });
3457
3551
  }
3458
3552
 
3553
+ // If we get Ratelimit-Limit and Ratelimit-Remaining where Ratelimit-Remaining is below
3554
+ // (Ratelimit-Limit / 2), we should delay the next sync by (Ratelimit-Reset / Ratelimit-Remaining)
3555
+ // seconds (given that there is a Ratelimit-Reset header).
3556
+ let syncRatelimitDelays = new WeakMap();
3557
+ function checkSyncRateLimitDelay(db) {
3558
+ var _a, _b;
3559
+ return __awaiter(this, void 0, void 0, function* () {
3560
+ const delatMilliseconds = ((_b = (_a = syncRatelimitDelays.get(db)) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : 0) - Date.now();
3561
+ if (delatMilliseconds > 0) {
3562
+ console.debug(`Stalling sync request ${delatMilliseconds} ms to spare ratelimits`);
3563
+ yield new Promise(resolve => setTimeout(resolve, delatMilliseconds));
3564
+ }
3565
+ });
3566
+ }
3567
+ function updateSyncRateLimitDelays(db, res) {
3568
+ const limit = res.headers.get('Ratelimit-Limit');
3569
+ const remaining = res.headers.get('Ratelimit-Remaining');
3570
+ const reset = res.headers.get('Ratelimit-Reset');
3571
+ if (limit && remaining && reset) {
3572
+ const limitNum = Number(limit);
3573
+ const remainingNum = Math.max(0, Number(remaining));
3574
+ const willResetInSeconds = Number(reset);
3575
+ if (remainingNum < limitNum / 2) {
3576
+ const delay = Math.ceil(willResetInSeconds / (remainingNum + 1));
3577
+ syncRatelimitDelays.set(db, new Date(Date.now() + delay * 1000));
3578
+ console.debug(`Sync ratelimit delay set to ${delay} seconds`);
3579
+ }
3580
+ else {
3581
+ syncRatelimitDelays.delete(db);
3582
+ console.debug(`Sync ratelimit delay cleared`);
3583
+ }
3584
+ }
3585
+ }
3586
+
3459
3587
  //import {BisonWebStreamReader} from "dreambase-library/dist/typeson-simplified/BisonWebStreamReader";
3460
3588
  function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser) {
3461
3589
  return __awaiter(this, void 0, void 0, function* () {
@@ -3464,9 +3592,20 @@
3464
3592
  //
3465
3593
  const headers = {
3466
3594
  Accept: 'application/json, application/x-bison, application/x-bison-stream',
3467
- 'Content-Type': 'application/tson'
3595
+ 'Content-Type': 'application/tson',
3468
3596
  };
3469
- const accessToken = yield loadAccessToken(db);
3597
+ const updatedUser = yield loadAccessToken(db);
3598
+ /*
3599
+ if (updatedUser?.license && changes.length > 0) {
3600
+ if (updatedUser.license.status === 'expired') {
3601
+ throw new Error(`License has expired`);
3602
+ }
3603
+ if (updatedUser.license.status === 'deactivated') {
3604
+ throw new Error(`License deactivated`);
3605
+ }
3606
+ }
3607
+ */
3608
+ const accessToken = updatedUser === null || updatedUser === void 0 ? void 0 : updatedUser.accessToken;
3470
3609
  if (accessToken) {
3471
3610
  headers.Authorization = `Bearer ${accessToken}`;
3472
3611
  }
@@ -3475,27 +3614,31 @@
3475
3614
  dbID: syncState === null || syncState === void 0 ? void 0 : syncState.remoteDbId,
3476
3615
  clientIdentity,
3477
3616
  schema: schema || {},
3478
- lastPull: syncState ? {
3479
- serverRevision: syncState.serverRevision,
3480
- realms: syncState.realms,
3481
- inviteRealms: syncState.inviteRealms
3482
- } : undefined,
3617
+ lastPull: syncState
3618
+ ? {
3619
+ serverRevision: syncState.serverRevision,
3620
+ realms: syncState.realms,
3621
+ inviteRealms: syncState.inviteRealms,
3622
+ }
3623
+ : undefined,
3483
3624
  baseRevs,
3484
- changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes)
3625
+ changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
3485
3626
  };
3486
- console.debug("Sync request", syncRequest);
3627
+ console.debug('Sync request', syncRequest);
3487
3628
  db.syncStateChangedEvent.next({
3488
3629
  phase: 'pushing',
3489
3630
  });
3490
3631
  const res = yield fetch(`${databaseUrl}/sync`, {
3491
3632
  method: 'post',
3492
3633
  headers,
3493
- body: TSON.stringify(syncRequest)
3634
+ credentials: 'include',
3635
+ body: TSON.stringify(syncRequest),
3494
3636
  });
3495
3637
  //const contentLength = Number(res.headers.get('content-length'));
3496
3638
  db.syncStateChangedEvent.next({
3497
- phase: 'pulling'
3639
+ phase: 'pulling',
3498
3640
  });
3641
+ updateSyncRateLimitDelays(db, res);
3499
3642
  if (!res.ok) {
3500
3643
  throw new HttpError(res);
3501
3644
  }
@@ -3697,12 +3840,13 @@
3697
3840
  function sync(db, options, schema, syncOptions) {
3698
3841
  return _sync
3699
3842
  .apply(this, arguments)
3700
- .then(() => {
3843
+ .then((result) => {
3701
3844
  if (!(syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)) { // && syncOptions?.purpose !== 'push') {
3702
3845
  db.syncStateChangedEvent.next({
3703
3846
  phase: 'in-sync',
3704
3847
  });
3705
3848
  }
3849
+ return result;
3706
3850
  })
3707
3851
  .catch((error) => __awaiter(this, void 0, void 0, function* () {
3708
3852
  if (syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)
@@ -3917,6 +4061,7 @@
3917
4061
  }));
3918
4062
  if (!done) {
3919
4063
  console.debug('MORE SYNC NEEDED. Go for it again!');
4064
+ yield checkSyncRateLimitDelay(db);
3920
4065
  return yield _sync(db, options, schema, { isInitialSync, cancelToken });
3921
4066
  }
3922
4067
  console.debug('SYNC DONE', { isInitialSync });
@@ -4053,6 +4198,8 @@
4053
4198
  yield db.table('$logins').update(user.userId, {
4054
4199
  accessToken: refreshedLogin.accessToken,
4055
4200
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
4201
+ claims: refreshedLogin.claims,
4202
+ license: refreshedLogin.license,
4056
4203
  });
4057
4204
  // Updating $logins will trigger emission of db.cloud.currentUser observable, which
4058
4205
  // in turn will lead to that connectWebSocket.ts will reconnect the socket with the
@@ -4621,6 +4768,13 @@
4621
4768
 
4622
4769
  const outstandingTransactions = new rxjs.BehaviorSubject(new Set());
4623
4770
 
4771
+ function isEagerSyncDisabled(db) {
4772
+ var _a, _b, _c, _d;
4773
+ return (((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.disableEagerSync) ||
4774
+ ((_c = (_b = db.cloud.currentUser.value) === null || _b === void 0 ? void 0 : _b.license) === null || _c === void 0 ? void 0 : _c.status) !== 'ok' ||
4775
+ !((_d = db.cloud.options) === null || _d === void 0 ? void 0 : _d.databaseUrl));
4776
+ }
4777
+
4624
4778
  /** Tracks all mutations in the same transaction as the mutations -
4625
4779
  * so it is guaranteed that no mutation goes untracked - and if transaction
4626
4780
  * aborts, the mutations won't be tracked.
@@ -4629,7 +4783,7 @@
4629
4783
  * changes to server and cleanup the tracked mutations once the server has
4630
4784
  * ackowledged that it got them.
4631
4785
  */
4632
- function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4786
+ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
4633
4787
  return {
4634
4788
  stack: 'dbcore',
4635
4789
  name: 'MutationTrackingMiddleware',
@@ -4640,7 +4794,7 @@
4640
4794
  try {
4641
4795
  mutTableMap = new Map(ordinaryTables.map((tbl) => [
4642
4796
  tbl.name,
4643
- core.table(`$${tbl.name}_mutations`)
4797
+ core.table(`$${tbl.name}_mutations`),
4644
4798
  ]));
4645
4799
  }
4646
4800
  catch (_a) {
@@ -4674,15 +4828,9 @@
4674
4828
  outstandingTransactions.next(outstandingTransactions.value);
4675
4829
  };
4676
4830
  const txComplete = () => {
4677
- var _a;
4678
- if (tx.mutationsAdded && ((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl)) {
4679
- if (db.cloud.usingServiceWorker) {
4680
- console.debug('registering sync event');
4681
- registerSyncEvent(db, "push");
4682
- }
4683
- else {
4684
- db.localSyncEvent.next({ purpose: "push" });
4685
- }
4831
+ if (tx.mutationsAdded &&
4832
+ !isEagerSyncDisabled(db)) {
4833
+ triggerSync(db, 'push');
4686
4834
  }
4687
4835
  removeTransaction();
4688
4836
  };
@@ -4749,7 +4897,7 @@
4749
4897
  .query({
4750
4898
  query: { range: req.range, index: schema.primaryKey },
4751
4899
  trans: req.trans,
4752
- values: false
4900
+ values: false,
4753
4901
  })
4754
4902
  // Do a delete request instead, but keep the criteria info for the server to execute
4755
4903
  .then((res) => {
@@ -4757,7 +4905,7 @@
4757
4905
  type: 'delete',
4758
4906
  keys: res.result,
4759
4907
  trans: req.trans,
4760
- criteria: { index: null, range: req.range }
4908
+ criteria: { index: null, range: req.range },
4761
4909
  });
4762
4910
  })
4763
4911
  : mutateAndLog(req);
@@ -4765,7 +4913,7 @@
4765
4913
  function mutateAndLog(req) {
4766
4914
  const trans = req.trans;
4767
4915
  trans.mutationsAdded = true;
4768
- const { txid, currentUser: { userId } } = trans;
4916
+ const { txid, currentUser: { userId }, } = trans;
4769
4917
  const { type } = req;
4770
4918
  const opNo = ++trans.opCount;
4771
4919
  return table.mutate(req).then((res) => {
@@ -4786,7 +4934,7 @@
4786
4934
  keys,
4787
4935
  criteria: req.criteria,
4788
4936
  txid,
4789
- userId
4937
+ userId,
4790
4938
  }
4791
4939
  : req.type === 'add'
4792
4940
  ? {
@@ -4796,7 +4944,7 @@
4796
4944
  keys,
4797
4945
  txid,
4798
4946
  userId,
4799
- values
4947
+ values,
4800
4948
  }
4801
4949
  : req.criteria && req.changeSpec
4802
4950
  ? {
@@ -4808,7 +4956,7 @@
4808
4956
  criteria: req.criteria,
4809
4957
  changeSpec: req.changeSpec,
4810
4958
  txid,
4811
- userId
4959
+ userId,
4812
4960
  }
4813
4961
  : updates
4814
4962
  ? {
@@ -4819,7 +4967,7 @@
4819
4967
  keys: updates.keys,
4820
4968
  changeSpecs: updates.changeSpecs,
4821
4969
  txid,
4822
- userId
4970
+ userId,
4823
4971
  }
4824
4972
  : {
4825
4973
  type: 'upsert',
@@ -4828,7 +4976,7 @@
4828
4976
  keys,
4829
4977
  values,
4830
4978
  txid,
4831
- userId
4979
+ userId,
4832
4980
  };
4833
4981
  return keys.length > 0 || ('criteria' in req && req.criteria)
4834
4982
  ? mutsTable
@@ -4838,7 +4986,7 @@
4838
4986
  });
4839
4987
  }
4840
4988
  } });
4841
- }
4989
+ },
4842
4990
  };
4843
4991
  }
4844
4992
 
@@ -5197,6 +5345,20 @@
5197
5345
  }
5198
5346
  }
5199
5347
 
5348
+ class InvalidLicenseError extends Error {
5349
+ constructor(license) {
5350
+ super(license === 'expired'
5351
+ ? `License expired`
5352
+ : license === 'deactivated'
5353
+ ? `User deactivated`
5354
+ : 'Invalid license');
5355
+ this.name = 'InvalidLicenseError';
5356
+ if (license) {
5357
+ this.license = license;
5358
+ }
5359
+ }
5360
+ }
5361
+
5200
5362
  function sleep(ms) {
5201
5363
  return new Promise((resolve) => setTimeout(resolve, ms));
5202
5364
  }
@@ -5230,7 +5392,12 @@
5230
5392
  function createObservable() {
5231
5393
  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.
5232
5394
  take(1), // Don't continue waking up whenever syncState change
5233
- switchMap((syncState) => db.cloud.currentUser.pipe(map((userLogin) => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]))), switchMap(([userLogin, syncState]) => {
5395
+ switchMap((syncState) => db.cloud.currentUser.pipe(map((userLogin) => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => {
5396
+ /*if (userLogin.license?.status && userLogin.license.status !== 'ok') {
5397
+ throw new InvalidLicenseError();
5398
+ }*/
5399
+ return userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]));
5400
+ }), switchMap(([userLogin, syncState]) => {
5234
5401
  if ((userLogin === null || userLogin === void 0 ? void 0 : userLogin.isLoggedIn) && !(syncState === null || syncState === void 0 ? void 0 : syncState.realms.includes(userLogin.userId))) {
5235
5402
  // We're in an in-between state when user is logged in but the user's realms are not yet synced.
5236
5403
  // Don't make this change reconnect the websocket just yet. Wait till syncState is updated
@@ -5259,14 +5426,20 @@
5259
5426
  yield db.table('$logins').update(user.userId, {
5260
5427
  accessToken: refreshedLogin.accessToken,
5261
5428
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
5429
+ claims: refreshedLogin.claims,
5430
+ license: refreshedLogin.license,
5262
5431
  });
5263
5432
  })), switchMap(() => createObservable()));
5264
5433
  }
5265
5434
  else {
5266
- return rxjs.throwError(error);
5435
+ return rxjs.throwError(() => error);
5267
5436
  }
5268
5437
  }), catchError((error) => {
5269
5438
  db.cloud.webSocketStatus.next("error");
5439
+ if (error instanceof InvalidLicenseError) {
5440
+ // Don't retry. Just throw and don't try connect again.
5441
+ return rxjs.throwError(() => error);
5442
+ }
5270
5443
  return rxjs.from(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
5271
5444
  }));
5272
5445
  }
@@ -5295,97 +5468,12 @@
5295
5468
  });
5296
5469
  }
5297
5470
 
5298
- const SECONDS = 1000;
5299
- const MINUTES = 60 * SECONDS;
5300
-
5301
- const myId = randomString(16);
5302
-
5303
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
5304
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
5305
- function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
5306
- return __awaiter(this, void 0, void 0, function* () {
5307
- // Start working.
5308
- //
5309
- // Check if someone else is working on this already.
5310
- //
5311
- const jobsTable = db.table(jobsTableName);
5312
- function aquireLock() {
5313
- return __awaiter(this, void 0, void 0, function* () {
5314
- const gotTheLock = yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5315
- const currentWork = yield jobsTable.get(jobName);
5316
- if (!currentWork) {
5317
- // No one else is working. Let's record that we are.
5318
- yield jobsTable.add({
5319
- nodeId: myId,
5320
- started: new Date(),
5321
- heartbeat: new Date()
5322
- }, jobName);
5323
- return true;
5324
- }
5325
- else if (currentWork.heartbeat.getTime() <
5326
- Date.now() - GUARDED_JOB_TIMEOUT) {
5327
- 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!`);
5328
- // Now, take over!
5329
- yield jobsTable.put({
5330
- nodeId: myId,
5331
- started: new Date(),
5332
- heartbeat: new Date()
5333
- }, jobName);
5334
- return true;
5335
- }
5336
- return false;
5337
- }));
5338
- if (gotTheLock)
5339
- return true;
5340
- // Someone else took the job.
5341
- if (awaitRemoteJob) {
5342
- try {
5343
- const jobDoneObservable = rxjs.from(Dexie.liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
5344
- yield jobDoneObservable.toPromise();
5345
- return false;
5346
- }
5347
- catch (err) {
5348
- if (err.name !== 'TimeoutError') {
5349
- throw err;
5350
- }
5351
- // Timeout stopped us! Try aquire the lock now.
5352
- // It will likely succeed this time unless
5353
- // another client took it.
5354
- return yield aquireLock();
5355
- }
5356
- }
5357
- return false;
5358
- });
5359
- }
5360
- if (yield aquireLock()) {
5361
- // We own the lock entry and can do our job undisturbed.
5362
- // We're not within a transaction, but these type of locks
5363
- // spans over transactions.
5364
- // Start our heart beat during the job.
5365
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
5366
- const heartbeat = setInterval(() => {
5367
- jobsTable.update(jobName, (job) => {
5368
- if (job.nodeId === myId) {
5369
- job.heartbeat = new Date();
5370
- }
5371
- });
5372
- }, GUARDED_JOB_HEARTBEAT);
5373
- try {
5374
- return yield job();
5375
- }
5376
- finally {
5377
- // Stop heartbeat
5378
- clearInterval(heartbeat);
5379
- // Remove the persisted job state:
5380
- yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5381
- const currentWork = yield jobsTable.get(jobName);
5382
- if (currentWork && currentWork.nodeId === myId) {
5383
- yield jobsTable.delete(jobName);
5384
- }
5385
- }));
5386
- }
5387
- }
5388
- });
5471
+ function performGuardedJob(db, jobName, job) {
5472
+ if (typeof navigator === 'undefined' || !navigator.locks) {
5473
+ // No support for guarding jobs. IE11, node.js, etc.
5474
+ return job();
5475
+ }
5476
+ return navigator.locks.request(db.name + '|' + jobName, () => job());
5389
5477
  }
5390
5478
 
5391
5479
  const ongoingSyncs = new WeakMap();
@@ -5439,6 +5527,9 @@
5439
5527
  function _syncIfPossible() {
5440
5528
  return __awaiter(this, void 0, void 0, function* () {
5441
5529
  try {
5530
+ // Check if should delay sync due to ratelimit:
5531
+ yield checkSyncRateLimitDelay(db);
5532
+ // Check if we need to lock the sync job. Not needed if we are the service worker.
5442
5533
  if (db.cloud.isServiceWorkerDB) {
5443
5534
  // We are the dedicated sync SW:
5444
5535
  yield sync(db, cloudOptions, cloudSchema, options);
@@ -5446,7 +5537,7 @@
5446
5537
  else if (!db.cloud.usingServiceWorker) {
5447
5538
  // We use a flow that is better suited for the case when multiple workers want to
5448
5539
  // do the same thing.
5449
- yield performGuardedJob(db, CURRENT_SYNC_WORKER, '$jobs', () => sync(db, cloudOptions, cloudSchema, options));
5540
+ yield performGuardedJob(db, CURRENT_SYNC_WORKER, () => sync(db, cloudOptions, cloudSchema, options));
5450
5541
  }
5451
5542
  else {
5452
5543
  assert(false);
@@ -5468,19 +5559,29 @@
5468
5559
  }
5469
5560
  }
5470
5561
 
5562
+ const SECONDS = 1000;
5563
+ const MINUTES = 60 * SECONDS;
5564
+
5471
5565
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
5472
5566
  let localSyncEventSubscription = null;
5473
5567
  //let syncHandler: ((event: Event) => void) | null = null;
5474
5568
  //let periodicSyncHandler: ((event: Event) => void) | null = null;
5475
5569
  let cancelToken = { cancelled: false };
5570
+ let retryHandle = null;
5571
+ let retryPurpose = null; // "pull" is superset of "push"
5476
5572
  function syncAndRetry(purpose, retryNum = 1) {
5477
5573
  // Use setTimeout() to get onto a clean stack and
5478
5574
  // break free from possible active transaction:
5479
5575
  setTimeout(() => {
5576
+ if (retryHandle)
5577
+ clearTimeout(retryHandle);
5578
+ const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
5579
+ retryHandle = null;
5580
+ retryPurpose = null;
5480
5581
  syncIfPossible(db, cloudOptions, cloudSchema, {
5481
5582
  cancelToken,
5482
5583
  retryImmediatelyOnFetchError: true,
5483
- purpose,
5584
+ purpose: combPurpose,
5484
5585
  }).catch((e) => {
5485
5586
  console.error('error in syncIfPossible()', e);
5486
5587
  if (cancelToken.cancelled) {
@@ -5490,7 +5591,13 @@
5490
5591
  // Mimic service worker sync event: retry 3 times
5491
5592
  // * first retry after 5 minutes
5492
5593
  // * second retry 15 minutes later
5493
- setTimeout(() => syncAndRetry(purpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
5594
+ const combinedPurpose = retryPurpose && retryPurpose === 'pull' ? 'pull' : purpose;
5595
+ const handle = setTimeout(() => syncAndRetry(combinedPurpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
5596
+ // Cancel the previous retryHandle if it exists to avoid scheduling loads of retries.
5597
+ if (retryHandle)
5598
+ clearTimeout(retryHandle);
5599
+ retryHandle = handle;
5600
+ retryPurpose = combinedPurpose;
5494
5601
  }
5495
5602
  });
5496
5603
  }, 0);
@@ -5557,10 +5664,12 @@
5557
5664
  },
5558
5665
  Alert: {
5559
5666
  error: {
5560
- color: "red"
5667
+ color: "red",
5668
+ fontWeight: "bold"
5561
5669
  },
5562
5670
  warning: {
5563
- color: "yellow"
5671
+ color: "#f80",
5672
+ fontWeight: "bold"
5564
5673
  },
5565
5674
  info: {
5566
5675
  color: "black"
@@ -5601,7 +5710,8 @@
5601
5710
  border: "3px solid #3d3d5d",
5602
5711
  borderRadius: "8px",
5603
5712
  boxShadow: "0 0 80px 10px #666",
5604
- width: "auto"
5713
+ width: "auto",
5714
+ fontFamily: "sans-serif",
5605
5715
  },
5606
5716
  Input: {
5607
5717
  height: "35px",
@@ -5622,11 +5732,26 @@
5622
5732
 
5623
5733
  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}
5624
5734
 
5735
+ /** Resolve a message template with parameters.
5736
+ *
5737
+ * Example:
5738
+ * resolveText({
5739
+ * message: "Hello {name}!",
5740
+ * messageCode: "HELLO",
5741
+ * messageParams: {name: "David"}
5742
+ * }) => "Hello David!"
5743
+ *
5744
+ * @param message Template message with {vars} in it.
5745
+ * @param messageCode Unique code for the message. Can be used for translation.
5746
+ * @param messageParams Parameters to be used in the message.
5747
+ * @returns A final message where parameters have been replaced with values.
5748
+ */
5625
5749
  function resolveText({ message, messageCode, messageParams }) {
5626
- return message.replace(/\{\w+\}/ig, n => messageParams[n.substr(1, n.length - 2)]);
5750
+ return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
5627
5751
  }
5628
5752
 
5629
- function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
5753
+ const OTP_LENGTH = 8;
5754
+ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5630
5755
  const [params, setParams] = p({});
5631
5756
  const firstFieldRef = _(null);
5632
5757
  s(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
@@ -5634,21 +5759,34 @@
5634
5759
  h(p$1, null,
5635
5760
  h("h3", { style: Styles.WindowHeader }, title),
5636
5761
  alerts.map((alert) => (h("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
5637
- h("form", { onSubmit: ev => {
5762
+ h("form", { onSubmit: (ev) => {
5638
5763
  ev.preventDefault();
5639
5764
  onSubmit(params);
5640
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label },
5765
+ } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label, key: idx },
5641
5766
  label ? `${label}: ` : '',
5642
- 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']) })); } })))))),
5767
+ 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) => {
5768
+ var _a;
5769
+ const value = valueTransformer(type, (_a = ev.target) === null || _a === void 0 ? void 0 : _a['value']);
5770
+ let updatedParams = Object.assign(Object.assign({}, params), { [fieldName]: value });
5771
+ setParams(updatedParams);
5772
+ if (type === 'otp' && (value === null || value === void 0 ? void 0 : value.trim().length) === OTP_LENGTH) {
5773
+ // Auto-submit when OTP is filled in.
5774
+ onSubmit(updatedParams);
5775
+ }
5776
+ } })))))),
5643
5777
  h("div", { style: Styles.ButtonsDiv },
5644
- h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, "Submit"),
5645
- h("button", { style: Styles.Button, onClick: onCancel }, "Cancel"))));
5778
+ h(p$1, null,
5779
+ h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, submitLabel),
5780
+ cancelLabel && (h("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
5646
5781
  }
5647
5782
  function valueTransformer(type, value) {
5648
5783
  switch (type) {
5649
- case "email": return value.toLowerCase();
5650
- case "otp": return value.toUpperCase();
5651
- default: return value;
5784
+ case 'email':
5785
+ return value.toLowerCase();
5786
+ case 'otp':
5787
+ return value.toUpperCase();
5788
+ default:
5789
+ return value;
5652
5790
  }
5653
5791
  }
5654
5792
 
@@ -5702,11 +5840,20 @@
5702
5840
  }
5703
5841
  };
5704
5842
  }
5705
- // TODO:
5706
- /*
5707
- * Gjort klart allt kring user interaction förutom att mounta default-ui på ett element.
5708
- * Också att kolla först om nån annan subscribar och i så fall inte göra nåt.
5709
- */
5843
+
5844
+ function associate(factory) {
5845
+ const wm = new WeakMap();
5846
+ return (x) => {
5847
+ let rv = wm.get(x);
5848
+ if (!rv) {
5849
+ rv = factory(x);
5850
+ wm.set(x, rv);
5851
+ }
5852
+ return rv;
5853
+ };
5854
+ }
5855
+
5856
+ const getCurrentUserEmitter = associate((db) => new rxjs.BehaviorSubject(UNAUTHORIZED_USER));
5710
5857
 
5711
5858
  function computeSyncState(db) {
5712
5859
  let _prevStatus = db.cloud.webSocketStatus.value;
@@ -5734,8 +5881,17 @@
5734
5881
  return rxjs.combineLatest([
5735
5882
  lazyWebSocketStatus,
5736
5883
  db.syncStateChangedEvent.pipe(startWith({ phase: 'initial' })),
5884
+ getCurrentUserEmitter(db.dx._novip),
5737
5885
  userIsReallyActive
5738
- ]).pipe(map(([status, syncState, userIsActive]) => {
5886
+ ]).pipe(map(([status, syncState, user, userIsActive]) => {
5887
+ var _a;
5888
+ if (((_a = user.license) === null || _a === void 0 ? void 0 : _a.status) && user.license.status !== 'ok') {
5889
+ return {
5890
+ phase: 'offline',
5891
+ status: 'offline',
5892
+ license: user.license.status
5893
+ };
5894
+ }
5739
5895
  let { phase, error, progress } = syncState;
5740
5896
  let adjustedStatus = status;
5741
5897
  if (phase === 'error') {
@@ -5768,23 +5924,12 @@
5768
5924
  error,
5769
5925
  progress,
5770
5926
  status: isOnline ? adjustedStatus : 'offline',
5927
+ license: 'ok'
5771
5928
  };
5772
5929
  return retState;
5773
5930
  }));
5774
5931
  }
5775
5932
 
5776
- function associate(factory) {
5777
- const wm = new WeakMap();
5778
- return (x) => {
5779
- let rv = wm.get(x);
5780
- if (!rv) {
5781
- rv = factory(x);
5782
- wm.set(x, rv);
5783
- }
5784
- return rv;
5785
- };
5786
- }
5787
-
5788
5933
  function createSharedValueObservable(o, defaultValue) {
5789
5934
  let currentValue = defaultValue;
5790
5935
  let shared = rxjs.from(o).pipe(rxjs.map((x) => (currentValue = x)), rxjs.share({ resetOnRefCountZero: () => rxjs.timer(1000) }));
@@ -5826,8 +5971,6 @@
5826
5971
  })), {});
5827
5972
  });
5828
5973
 
5829
- const getCurrentUserEmitter = associate((db) => new rxjs.BehaviorSubject(UNAUTHORIZED_USER));
5830
-
5831
5974
  const getInternalAccessControlObservable = associate((db) => {
5832
5975
  return createSharedValueObservable(getCurrentUserEmitter(db._novip).pipe(switchMap((currentUser) => Dexie.liveQuery(() => db.transaction('r', 'realms', 'members', () => Promise.all([
5833
5976
  db.members.where({ userId: currentUser.userId }).toArray(),
@@ -6117,7 +6260,7 @@
6117
6260
  });
6118
6261
  const syncComplete = new rxjs.Subject();
6119
6262
  dexie.cloud = {
6120
- version: '4.0.1-beta.46',
6263
+ version: '4.0.1-beta.47',
6121
6264
  options: Object.assign({}, DEFAULT_OPTIONS),
6122
6265
  schema: null,
6123
6266
  get currentUserId() {
@@ -6153,11 +6296,24 @@
6153
6296
  }
6154
6297
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
6155
6298
  },
6299
+ logout({ force } = {}) {
6300
+ return __awaiter(this, void 0, void 0, function* () {
6301
+ force
6302
+ ? yield _logout(DexieCloudDB(dexie), { deleteUnsyncedData: true })
6303
+ : yield logout(DexieCloudDB(dexie));
6304
+ });
6305
+ },
6156
6306
  sync({ wait, purpose } = { wait: true, purpose: 'push' }) {
6307
+ var _a;
6157
6308
  return __awaiter(this, void 0, void 0, function* () {
6158
6309
  if (wait === undefined)
6159
6310
  wait = true;
6160
6311
  const db = DexieCloudDB(dexie);
6312
+ const licenseStatus = ((_a = db.cloud.currentUser.value.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
6313
+ if (licenseStatus !== 'ok') {
6314
+ // Refresh access token to check for updated license
6315
+ yield loadAccessToken(db);
6316
+ }
6161
6317
  if (purpose === 'pull') {
6162
6318
  const syncState = db.cloud.persistedSyncState.value;
6163
6319
  triggerSync(db, purpose);
@@ -6361,7 +6517,9 @@
6361
6517
  db.syncStateChangedEvent.next({
6362
6518
  phase: 'not-in-sync',
6363
6519
  });
6364
- triggerSync(db, 'push');
6520
+ if (!isEagerSyncDisabled(db)) {
6521
+ triggerSync(db, 'push');
6522
+ }
6365
6523
  }), rxjs.fromEvent(self, 'offline').subscribe(() => {
6366
6524
  console.debug('offline!');
6367
6525
  db.syncStateChangedEvent.next({
@@ -6378,7 +6536,7 @@
6378
6536
  });
6379
6537
  }
6380
6538
  }
6381
- dexieCloud.version = '4.0.1-beta.46';
6539
+ dexieCloud.version = '4.0.1-beta.47';
6382
6540
  Dexie__default["default"].Cloud = dexieCloud;
6383
6541
 
6384
6542
  exports["default"] = dexieCloud;