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
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ==========================================================================
10
10
  *
11
- * Version 4.0.1-beta.44, Wed Jul 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;
@@ -3413,9 +3507,19 @@
3413
3507
  const mutClone = changeClone.muts[mutIndex];
3414
3508
  const rewrittenKey = JSON.stringify(key);
3415
3509
  mutClone.keys[keyIndex] = rewrittenKey;
3416
- if (rewriteValues) {
3417
- Dexie__default["default"].setByKeyPath(mutClone.values[keyIndex], primaryKey.keyPath, rewrittenKey);
3418
- }
3510
+ /* Bug (#1777)
3511
+ We should not rewrite values. It will fail because the key is array and the value is string.
3512
+ Only the keys should be rewritten and it's already done on the server.
3513
+ We should take another round of revieweing how key transformations are being done between
3514
+ client and server and let the server do the key transformations entirely instead now that
3515
+ we have the primary key schema on the server making it possible to do so.
3516
+ if (rewriteValues) {
3517
+ Dexie.setByKeyPath(
3518
+ (mutClone as DBInsertOperation).values[keyIndex],
3519
+ primaryKey.keyPath!,
3520
+ rewrittenKey
3521
+ );
3522
+ }*/
3419
3523
  }
3420
3524
  else if (key[0] === '#') {
3421
3525
  // Private ID - translate!
@@ -3446,6 +3550,40 @@
3446
3550
  : change.muts.map((m) => (Object.assign(Object.assign({}, m), { keys: m.keys.slice() }))) });
3447
3551
  }
3448
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
+
3449
3587
  //import {BisonWebStreamReader} from "dreambase-library/dist/typeson-simplified/BisonWebStreamReader";
3450
3588
  function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser) {
3451
3589
  return __awaiter(this, void 0, void 0, function* () {
@@ -3454,9 +3592,20 @@
3454
3592
  //
3455
3593
  const headers = {
3456
3594
  Accept: 'application/json, application/x-bison, application/x-bison-stream',
3457
- 'Content-Type': 'application/tson'
3595
+ 'Content-Type': 'application/tson',
3458
3596
  };
3459
- 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;
3460
3609
  if (accessToken) {
3461
3610
  headers.Authorization = `Bearer ${accessToken}`;
3462
3611
  }
@@ -3465,27 +3614,31 @@
3465
3614
  dbID: syncState === null || syncState === void 0 ? void 0 : syncState.remoteDbId,
3466
3615
  clientIdentity,
3467
3616
  schema: schema || {},
3468
- lastPull: syncState ? {
3469
- serverRevision: syncState.serverRevision,
3470
- realms: syncState.realms,
3471
- inviteRealms: syncState.inviteRealms
3472
- } : undefined,
3617
+ lastPull: syncState
3618
+ ? {
3619
+ serverRevision: syncState.serverRevision,
3620
+ realms: syncState.realms,
3621
+ inviteRealms: syncState.inviteRealms,
3622
+ }
3623
+ : undefined,
3473
3624
  baseRevs,
3474
- changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes)
3625
+ changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
3475
3626
  };
3476
- console.debug("Sync request", syncRequest);
3627
+ console.debug('Sync request', syncRequest);
3477
3628
  db.syncStateChangedEvent.next({
3478
3629
  phase: 'pushing',
3479
3630
  });
3480
3631
  const res = yield fetch(`${databaseUrl}/sync`, {
3481
3632
  method: 'post',
3482
3633
  headers,
3483
- body: TSON.stringify(syncRequest)
3634
+ credentials: 'include',
3635
+ body: TSON.stringify(syncRequest),
3484
3636
  });
3485
3637
  //const contentLength = Number(res.headers.get('content-length'));
3486
3638
  db.syncStateChangedEvent.next({
3487
- phase: 'pulling'
3639
+ phase: 'pulling',
3488
3640
  });
3641
+ updateSyncRateLimitDelays(db, res);
3489
3642
  if (!res.ok) {
3490
3643
  throw new HttpError(res);
3491
3644
  }
@@ -3687,12 +3840,13 @@
3687
3840
  function sync(db, options, schema, syncOptions) {
3688
3841
  return _sync
3689
3842
  .apply(this, arguments)
3690
- .then(() => {
3843
+ .then((result) => {
3691
3844
  if (!(syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)) { // && syncOptions?.purpose !== 'push') {
3692
3845
  db.syncStateChangedEvent.next({
3693
3846
  phase: 'in-sync',
3694
3847
  });
3695
3848
  }
3849
+ return result;
3696
3850
  })
3697
3851
  .catch((error) => __awaiter(this, void 0, void 0, function* () {
3698
3852
  if (syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)
@@ -3722,7 +3876,7 @@
3722
3876
  });
3723
3877
  db.syncStateChangedEvent.next({
3724
3878
  phase: isOnline ? 'error' : 'offline',
3725
- error,
3879
+ error: new Error('' + (error === null || error === void 0 ? void 0 : error.message) || error),
3726
3880
  });
3727
3881
  return Promise.reject(error);
3728
3882
  }));
@@ -3907,6 +4061,7 @@
3907
4061
  }));
3908
4062
  if (!done) {
3909
4063
  console.debug('MORE SYNC NEEDED. Go for it again!');
4064
+ yield checkSyncRateLimitDelay(db);
3910
4065
  return yield _sync(db, options, schema, { isInitialSync, cancelToken });
3911
4066
  }
3912
4067
  console.debug('SYNC DONE', { isInitialSync });
@@ -4043,6 +4198,8 @@
4043
4198
  yield db.table('$logins').update(user.userId, {
4044
4199
  accessToken: refreshedLogin.accessToken,
4045
4200
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
4201
+ claims: refreshedLogin.claims,
4202
+ license: refreshedLogin.license,
4046
4203
  });
4047
4204
  // Updating $logins will trigger emission of db.cloud.currentUser observable, which
4048
4205
  // in turn will lead to that connectWebSocket.ts will reconnect the socket with the
@@ -4611,6 +4768,13 @@
4611
4768
 
4612
4769
  const outstandingTransactions = new rxjs.BehaviorSubject(new Set());
4613
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
+
4614
4778
  /** Tracks all mutations in the same transaction as the mutations -
4615
4779
  * so it is guaranteed that no mutation goes untracked - and if transaction
4616
4780
  * aborts, the mutations won't be tracked.
@@ -4619,7 +4783,7 @@
4619
4783
  * changes to server and cleanup the tracked mutations once the server has
4620
4784
  * ackowledged that it got them.
4621
4785
  */
4622
- function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4786
+ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
4623
4787
  return {
4624
4788
  stack: 'dbcore',
4625
4789
  name: 'MutationTrackingMiddleware',
@@ -4630,7 +4794,7 @@
4630
4794
  try {
4631
4795
  mutTableMap = new Map(ordinaryTables.map((tbl) => [
4632
4796
  tbl.name,
4633
- core.table(`$${tbl.name}_mutations`)
4797
+ core.table(`$${tbl.name}_mutations`),
4634
4798
  ]));
4635
4799
  }
4636
4800
  catch (_a) {
@@ -4664,15 +4828,9 @@
4664
4828
  outstandingTransactions.next(outstandingTransactions.value);
4665
4829
  };
4666
4830
  const txComplete = () => {
4667
- var _a;
4668
- if (tx.mutationsAdded && ((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl)) {
4669
- if (db.cloud.usingServiceWorker) {
4670
- console.debug('registering sync event');
4671
- registerSyncEvent(db, "push");
4672
- }
4673
- else {
4674
- db.localSyncEvent.next({ purpose: "push" });
4675
- }
4831
+ if (tx.mutationsAdded &&
4832
+ !isEagerSyncDisabled(db)) {
4833
+ triggerSync(db, 'push');
4676
4834
  }
4677
4835
  removeTransaction();
4678
4836
  };
@@ -4739,7 +4897,7 @@
4739
4897
  .query({
4740
4898
  query: { range: req.range, index: schema.primaryKey },
4741
4899
  trans: req.trans,
4742
- values: false
4900
+ values: false,
4743
4901
  })
4744
4902
  // Do a delete request instead, but keep the criteria info for the server to execute
4745
4903
  .then((res) => {
@@ -4747,7 +4905,7 @@
4747
4905
  type: 'delete',
4748
4906
  keys: res.result,
4749
4907
  trans: req.trans,
4750
- criteria: { index: null, range: req.range }
4908
+ criteria: { index: null, range: req.range },
4751
4909
  });
4752
4910
  })
4753
4911
  : mutateAndLog(req);
@@ -4755,7 +4913,7 @@
4755
4913
  function mutateAndLog(req) {
4756
4914
  const trans = req.trans;
4757
4915
  trans.mutationsAdded = true;
4758
- const { txid, currentUser: { userId } } = trans;
4916
+ const { txid, currentUser: { userId }, } = trans;
4759
4917
  const { type } = req;
4760
4918
  const opNo = ++trans.opCount;
4761
4919
  return table.mutate(req).then((res) => {
@@ -4776,7 +4934,7 @@
4776
4934
  keys,
4777
4935
  criteria: req.criteria,
4778
4936
  txid,
4779
- userId
4937
+ userId,
4780
4938
  }
4781
4939
  : req.type === 'add'
4782
4940
  ? {
@@ -4786,7 +4944,7 @@
4786
4944
  keys,
4787
4945
  txid,
4788
4946
  userId,
4789
- values
4947
+ values,
4790
4948
  }
4791
4949
  : req.criteria && req.changeSpec
4792
4950
  ? {
@@ -4798,7 +4956,7 @@
4798
4956
  criteria: req.criteria,
4799
4957
  changeSpec: req.changeSpec,
4800
4958
  txid,
4801
- userId
4959
+ userId,
4802
4960
  }
4803
4961
  : updates
4804
4962
  ? {
@@ -4809,7 +4967,7 @@
4809
4967
  keys: updates.keys,
4810
4968
  changeSpecs: updates.changeSpecs,
4811
4969
  txid,
4812
- userId
4970
+ userId,
4813
4971
  }
4814
4972
  : {
4815
4973
  type: 'upsert',
@@ -4818,7 +4976,7 @@
4818
4976
  keys,
4819
4977
  values,
4820
4978
  txid,
4821
- userId
4979
+ userId,
4822
4980
  };
4823
4981
  return keys.length > 0 || ('criteria' in req && req.criteria)
4824
4982
  ? mutsTable
@@ -4828,7 +4986,7 @@
4828
4986
  });
4829
4987
  }
4830
4988
  } });
4831
- }
4989
+ },
4832
4990
  };
4833
4991
  }
4834
4992
 
@@ -5187,6 +5345,20 @@
5187
5345
  }
5188
5346
  }
5189
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
+
5190
5362
  function sleep(ms) {
5191
5363
  return new Promise((resolve) => setTimeout(resolve, ms));
5192
5364
  }
@@ -5220,7 +5392,12 @@
5220
5392
  function createObservable() {
5221
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.
5222
5394
  take(1), // Don't continue waking up whenever syncState change
5223
- 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]) => {
5224
5401
  if ((userLogin === null || userLogin === void 0 ? void 0 : userLogin.isLoggedIn) && !(syncState === null || syncState === void 0 ? void 0 : syncState.realms.includes(userLogin.userId))) {
5225
5402
  // We're in an in-between state when user is logged in but the user's realms are not yet synced.
5226
5403
  // Don't make this change reconnect the websocket just yet. Wait till syncState is updated
@@ -5249,14 +5426,20 @@
5249
5426
  yield db.table('$logins').update(user.userId, {
5250
5427
  accessToken: refreshedLogin.accessToken,
5251
5428
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
5429
+ claims: refreshedLogin.claims,
5430
+ license: refreshedLogin.license,
5252
5431
  });
5253
5432
  })), switchMap(() => createObservable()));
5254
5433
  }
5255
5434
  else {
5256
- return rxjs.throwError(error);
5435
+ return rxjs.throwError(() => error);
5257
5436
  }
5258
5437
  }), catchError((error) => {
5259
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
+ }
5260
5443
  return rxjs.from(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
5261
5444
  }));
5262
5445
  }
@@ -5285,97 +5468,12 @@
5285
5468
  });
5286
5469
  }
5287
5470
 
5288
- const SECONDS = 1000;
5289
- const MINUTES = 60 * SECONDS;
5290
-
5291
- const myId = randomString(16);
5292
-
5293
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
5294
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
5295
- function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
5296
- return __awaiter(this, void 0, void 0, function* () {
5297
- // Start working.
5298
- //
5299
- // Check if someone else is working on this already.
5300
- //
5301
- const jobsTable = db.table(jobsTableName);
5302
- function aquireLock() {
5303
- return __awaiter(this, void 0, void 0, function* () {
5304
- const gotTheLock = yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5305
- const currentWork = yield jobsTable.get(jobName);
5306
- if (!currentWork) {
5307
- // No one else is working. Let's record that we are.
5308
- yield jobsTable.add({
5309
- nodeId: myId,
5310
- started: new Date(),
5311
- heartbeat: new Date()
5312
- }, jobName);
5313
- return true;
5314
- }
5315
- else if (currentWork.heartbeat.getTime() <
5316
- Date.now() - GUARDED_JOB_TIMEOUT) {
5317
- 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!`);
5318
- // Now, take over!
5319
- yield jobsTable.put({
5320
- nodeId: myId,
5321
- started: new Date(),
5322
- heartbeat: new Date()
5323
- }, jobName);
5324
- return true;
5325
- }
5326
- return false;
5327
- }));
5328
- if (gotTheLock)
5329
- return true;
5330
- // Someone else took the job.
5331
- if (awaitRemoteJob) {
5332
- try {
5333
- const jobDoneObservable = rxjs.from(Dexie.liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
5334
- yield jobDoneObservable.toPromise();
5335
- return false;
5336
- }
5337
- catch (err) {
5338
- if (err.name !== 'TimeoutError') {
5339
- throw err;
5340
- }
5341
- // Timeout stopped us! Try aquire the lock now.
5342
- // It will likely succeed this time unless
5343
- // another client took it.
5344
- return yield aquireLock();
5345
- }
5346
- }
5347
- return false;
5348
- });
5349
- }
5350
- if (yield aquireLock()) {
5351
- // We own the lock entry and can do our job undisturbed.
5352
- // We're not within a transaction, but these type of locks
5353
- // spans over transactions.
5354
- // Start our heart beat during the job.
5355
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
5356
- const heartbeat = setInterval(() => {
5357
- jobsTable.update(jobName, (job) => {
5358
- if (job.nodeId === myId) {
5359
- job.heartbeat = new Date();
5360
- }
5361
- });
5362
- }, GUARDED_JOB_HEARTBEAT);
5363
- try {
5364
- return yield job();
5365
- }
5366
- finally {
5367
- // Stop heartbeat
5368
- clearInterval(heartbeat);
5369
- // Remove the persisted job state:
5370
- yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5371
- const currentWork = yield jobsTable.get(jobName);
5372
- if (currentWork && currentWork.nodeId === myId) {
5373
- yield jobsTable.delete(jobName);
5374
- }
5375
- }));
5376
- }
5377
- }
5378
- });
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());
5379
5477
  }
5380
5478
 
5381
5479
  const ongoingSyncs = new WeakMap();
@@ -5429,6 +5527,9 @@
5429
5527
  function _syncIfPossible() {
5430
5528
  return __awaiter(this, void 0, void 0, function* () {
5431
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.
5432
5533
  if (db.cloud.isServiceWorkerDB) {
5433
5534
  // We are the dedicated sync SW:
5434
5535
  yield sync(db, cloudOptions, cloudSchema, options);
@@ -5436,7 +5537,7 @@
5436
5537
  else if (!db.cloud.usingServiceWorker) {
5437
5538
  // We use a flow that is better suited for the case when multiple workers want to
5438
5539
  // do the same thing.
5439
- yield performGuardedJob(db, CURRENT_SYNC_WORKER, '$jobs', () => sync(db, cloudOptions, cloudSchema, options));
5540
+ yield performGuardedJob(db, CURRENT_SYNC_WORKER, () => sync(db, cloudOptions, cloudSchema, options));
5440
5541
  }
5441
5542
  else {
5442
5543
  assert(false);
@@ -5458,19 +5559,29 @@
5458
5559
  }
5459
5560
  }
5460
5561
 
5562
+ const SECONDS = 1000;
5563
+ const MINUTES = 60 * SECONDS;
5564
+
5461
5565
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
5462
5566
  let localSyncEventSubscription = null;
5463
5567
  //let syncHandler: ((event: Event) => void) | null = null;
5464
5568
  //let periodicSyncHandler: ((event: Event) => void) | null = null;
5465
5569
  let cancelToken = { cancelled: false };
5570
+ let retryHandle = null;
5571
+ let retryPurpose = null; // "pull" is superset of "push"
5466
5572
  function syncAndRetry(purpose, retryNum = 1) {
5467
5573
  // Use setTimeout() to get onto a clean stack and
5468
5574
  // break free from possible active transaction:
5469
5575
  setTimeout(() => {
5576
+ if (retryHandle)
5577
+ clearTimeout(retryHandle);
5578
+ const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
5579
+ retryHandle = null;
5580
+ retryPurpose = null;
5470
5581
  syncIfPossible(db, cloudOptions, cloudSchema, {
5471
5582
  cancelToken,
5472
5583
  retryImmediatelyOnFetchError: true,
5473
- purpose,
5584
+ purpose: combPurpose,
5474
5585
  }).catch((e) => {
5475
5586
  console.error('error in syncIfPossible()', e);
5476
5587
  if (cancelToken.cancelled) {
@@ -5480,7 +5591,13 @@
5480
5591
  // Mimic service worker sync event: retry 3 times
5481
5592
  // * first retry after 5 minutes
5482
5593
  // * second retry 15 minutes later
5483
- 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;
5484
5601
  }
5485
5602
  });
5486
5603
  }, 0);
@@ -5547,10 +5664,12 @@
5547
5664
  },
5548
5665
  Alert: {
5549
5666
  error: {
5550
- color: "red"
5667
+ color: "red",
5668
+ fontWeight: "bold"
5551
5669
  },
5552
5670
  warning: {
5553
- color: "yellow"
5671
+ color: "#f80",
5672
+ fontWeight: "bold"
5554
5673
  },
5555
5674
  info: {
5556
5675
  color: "black"
@@ -5591,7 +5710,8 @@
5591
5710
  border: "3px solid #3d3d5d",
5592
5711
  borderRadius: "8px",
5593
5712
  boxShadow: "0 0 80px 10px #666",
5594
- width: "auto"
5713
+ width: "auto",
5714
+ fontFamily: "sans-serif",
5595
5715
  },
5596
5716
  Input: {
5597
5717
  height: "35px",
@@ -5612,11 +5732,26 @@
5612
5732
 
5613
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}
5614
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
+ */
5615
5749
  function resolveText({ message, messageCode, messageParams }) {
5616
- 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)]);
5617
5751
  }
5618
5752
 
5619
- function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
5753
+ const OTP_LENGTH = 8;
5754
+ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5620
5755
  const [params, setParams] = p({});
5621
5756
  const firstFieldRef = _(null);
5622
5757
  s(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
@@ -5624,21 +5759,34 @@
5624
5759
  h(p$1, null,
5625
5760
  h("h3", { style: Styles.WindowHeader }, title),
5626
5761
  alerts.map((alert) => (h("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
5627
- h("form", { onSubmit: ev => {
5762
+ h("form", { onSubmit: (ev) => {
5628
5763
  ev.preventDefault();
5629
5764
  onSubmit(params);
5630
- } }, 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 },
5631
5766
  label ? `${label}: ` : '',
5632
- 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
+ } })))))),
5633
5777
  h("div", { style: Styles.ButtonsDiv },
5634
- h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, "Submit"),
5635
- 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))))));
5636
5781
  }
5637
5782
  function valueTransformer(type, value) {
5638
5783
  switch (type) {
5639
- case "email": return value.toLowerCase();
5640
- case "otp": return value.toUpperCase();
5641
- default: return value;
5784
+ case 'email':
5785
+ return value.toLowerCase();
5786
+ case 'otp':
5787
+ return value.toUpperCase();
5788
+ default:
5789
+ return value;
5642
5790
  }
5643
5791
  }
5644
5792
 
@@ -5692,11 +5840,20 @@
5692
5840
  }
5693
5841
  };
5694
5842
  }
5695
- // TODO:
5696
- /*
5697
- * Gjort klart allt kring user interaction förutom att mounta default-ui på ett element.
5698
- * Också att kolla först om nån annan subscribar och i så fall inte göra nåt.
5699
- */
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));
5700
5857
 
5701
5858
  function computeSyncState(db) {
5702
5859
  let _prevStatus = db.cloud.webSocketStatus.value;
@@ -5724,8 +5881,17 @@
5724
5881
  return rxjs.combineLatest([
5725
5882
  lazyWebSocketStatus,
5726
5883
  db.syncStateChangedEvent.pipe(startWith({ phase: 'initial' })),
5884
+ getCurrentUserEmitter(db.dx._novip),
5727
5885
  userIsReallyActive
5728
- ]).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
+ }
5729
5895
  let { phase, error, progress } = syncState;
5730
5896
  let adjustedStatus = status;
5731
5897
  if (phase === 'error') {
@@ -5758,23 +5924,12 @@
5758
5924
  error,
5759
5925
  progress,
5760
5926
  status: isOnline ? adjustedStatus : 'offline',
5927
+ license: 'ok'
5761
5928
  };
5762
5929
  return retState;
5763
5930
  }));
5764
5931
  }
5765
5932
 
5766
- function associate(factory) {
5767
- const wm = new WeakMap();
5768
- return (x) => {
5769
- let rv = wm.get(x);
5770
- if (!rv) {
5771
- rv = factory(x);
5772
- wm.set(x, rv);
5773
- }
5774
- return rv;
5775
- };
5776
- }
5777
-
5778
5933
  function createSharedValueObservable(o, defaultValue) {
5779
5934
  let currentValue = defaultValue;
5780
5935
  let shared = rxjs.from(o).pipe(rxjs.map((x) => (currentValue = x)), rxjs.share({ resetOnRefCountZero: () => rxjs.timer(1000) }));
@@ -5816,8 +5971,6 @@
5816
5971
  })), {});
5817
5972
  });
5818
5973
 
5819
- const getCurrentUserEmitter = associate((db) => new rxjs.BehaviorSubject(UNAUTHORIZED_USER));
5820
-
5821
5974
  const getInternalAccessControlObservable = associate((db) => {
5822
5975
  return createSharedValueObservable(getCurrentUserEmitter(db._novip).pipe(switchMap((currentUser) => Dexie.liveQuery(() => db.transaction('r', 'realms', 'members', () => Promise.all([
5823
5976
  db.members.where({ userId: currentUser.userId }).toArray(),
@@ -6107,7 +6260,7 @@
6107
6260
  });
6108
6261
  const syncComplete = new rxjs.Subject();
6109
6262
  dexie.cloud = {
6110
- version: '4.0.1-beta.44',
6263
+ version: '4.0.1-beta.47',
6111
6264
  options: Object.assign({}, DEFAULT_OPTIONS),
6112
6265
  schema: null,
6113
6266
  get currentUserId() {
@@ -6143,11 +6296,24 @@
6143
6296
  }
6144
6297
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
6145
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
+ },
6146
6306
  sync({ wait, purpose } = { wait: true, purpose: 'push' }) {
6307
+ var _a;
6147
6308
  return __awaiter(this, void 0, void 0, function* () {
6148
6309
  if (wait === undefined)
6149
6310
  wait = true;
6150
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
+ }
6151
6317
  if (purpose === 'pull') {
6152
6318
  const syncState = db.cloud.persistedSyncState.value;
6153
6319
  triggerSync(db, purpose);
@@ -6351,7 +6517,9 @@
6351
6517
  db.syncStateChangedEvent.next({
6352
6518
  phase: 'not-in-sync',
6353
6519
  });
6354
- triggerSync(db, 'push');
6520
+ if (!isEagerSyncDisabled(db)) {
6521
+ triggerSync(db, 'push');
6522
+ }
6355
6523
  }), rxjs.fromEvent(self, 'offline').subscribe(() => {
6356
6524
  console.debug('offline!');
6357
6525
  db.syncStateChangedEvent.next({
@@ -6368,7 +6536,7 @@
6368
6536
  });
6369
6537
  }
6370
6538
  }
6371
- dexieCloud.version = '4.0.1-beta.44';
6539
+ dexieCloud.version = '4.0.1-beta.47';
6372
6540
  Dexie__default["default"].Cloud = dexieCloud;
6373
6541
 
6374
6542
  exports["default"] = dexieCloud;