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
  *
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import Dexie, { cmp, liveQuery } from 'dexie';
20
- import { Observable as Observable$1, BehaviorSubject, Subject, fromEvent, of, merge, Subscription as Subscription$1, from as from$1, throwError, combineLatest, map as map$1, share, timer as timer$1 } from 'rxjs';
20
+ import { firstValueFrom, from as from$1, filter as filter$1, Observable as Observable$1, BehaviorSubject, Subject, fromEvent, of, merge, Subscription as Subscription$1, throwError, combineLatest, map as map$1, share, timer as timer$1 } from 'rxjs';
21
21
 
22
22
  /******************************************************************************
23
23
  Copyright (c) Microsoft Corporation.
@@ -1917,58 +1917,6 @@ function tap(observerOrNext, error, complete) {
1917
1917
  identity;
1918
1918
  }
1919
1919
 
1920
- var TimeoutError = createErrorClass(function (_super) {
1921
- return function TimeoutErrorImpl(info) {
1922
- if (info === void 0) { info = null; }
1923
- _super(this);
1924
- this.message = 'Timeout has occurred';
1925
- this.name = 'TimeoutError';
1926
- this.info = info;
1927
- };
1928
- });
1929
- function timeout(config, schedulerArg) {
1930
- 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;
1931
- if (first == null && each == null) {
1932
- throw new TypeError('No timeout provided.');
1933
- }
1934
- return operate(function (source, subscriber) {
1935
- var originalSourceSubscription;
1936
- var timerSubscription;
1937
- var lastValue = null;
1938
- var seen = 0;
1939
- var startTimer = function (delay) {
1940
- timerSubscription = executeSchedule(subscriber, scheduler, function () {
1941
- try {
1942
- originalSourceSubscription.unsubscribe();
1943
- innerFrom(_with({
1944
- meta: meta,
1945
- lastValue: lastValue,
1946
- seen: seen,
1947
- })).subscribe(subscriber);
1948
- }
1949
- catch (err) {
1950
- subscriber.error(err);
1951
- }
1952
- }, delay);
1953
- };
1954
- originalSourceSubscription = source.subscribe(createOperatorSubscriber(subscriber, function (value) {
1955
- timerSubscription === null || timerSubscription === void 0 ? void 0 : timerSubscription.unsubscribe();
1956
- seen++;
1957
- subscriber.next((lastValue = value));
1958
- each > 0 && startTimer(each);
1959
- }, undefined, undefined, function () {
1960
- if (!(timerSubscription === null || timerSubscription === void 0 ? void 0 : timerSubscription.closed)) {
1961
- timerSubscription === null || timerSubscription === void 0 ? void 0 : timerSubscription.unsubscribe();
1962
- }
1963
- lastValue = null;
1964
- }));
1965
- !seen && startTimer(first != null ? (typeof first === 'number' ? first : +first - scheduler.now()) : each);
1966
- });
1967
- }
1968
- function timeoutErrorFactory(info) {
1969
- throw new TimeoutError(info);
1970
- }
1971
-
1972
1920
  //const hasSW = 'serviceWorker' in navigator;
1973
1921
  let hasComplainedAboutSyncEvent = false;
1974
1922
  function registerSyncEvent(db, purpose) {
@@ -2029,6 +1977,7 @@ function registerPeriodicSyncEvent(db) {
2029
1977
 
2030
1978
  function triggerSync(db, purpose) {
2031
1979
  if (db.cloud.usingServiceWorker) {
1980
+ console.debug('registering sync event');
2032
1981
  registerSyncEvent(db, purpose);
2033
1982
  }
2034
1983
  else {
@@ -2060,14 +2009,24 @@ const b64encode = typeof Buffer !== "undefined"
2060
2009
  return btoa(String.fromCharCode.apply(null, ArrayBuffer.isView(b) ? b : new Uint8Array(b)));
2061
2010
  };
2062
2011
 
2012
+ class TokenErrorResponseError extends Error {
2013
+ constructor({ title, message, messageCode, messageParams, }) {
2014
+ super(message);
2015
+ this.name = 'TokenErrorResponseError';
2016
+ this.title = title;
2017
+ this.messageCode = messageCode;
2018
+ this.messageParams = messageParams;
2019
+ }
2020
+ }
2021
+
2063
2022
  function interactWithUser(userInteraction, req) {
2064
2023
  return new Promise((resolve, reject) => {
2065
- const interactionProps = Object.assign(Object.assign({}, req), { onSubmit: (res) => {
2024
+ const interactionProps = Object.assign(Object.assign({ submitLabel: 'Submit', cancelLabel: 'Cancel' }, req), { onSubmit: (res) => {
2066
2025
  userInteraction.next(undefined);
2067
2026
  resolve(res);
2068
2027
  }, onCancel: () => {
2069
2028
  userInteraction.next(undefined);
2070
- reject(new Dexie.AbortError("User cancelled"));
2029
+ reject(new Dexie.AbortError('User cancelled'));
2071
2030
  } });
2072
2031
  userInteraction.next(interactionProps);
2073
2032
  // Start subscribing for external updates to db.cloud.userInteraction, and if so, cancel this request.
@@ -2086,7 +2045,9 @@ function alertUser(userInteraction, title, ...alerts) {
2086
2045
  type: 'message-alert',
2087
2046
  title,
2088
2047
  alerts,
2089
- fields: {}
2048
+ fields: {},
2049
+ submitLabel: 'OK',
2050
+ cancelLabel: null,
2090
2051
  });
2091
2052
  }
2092
2053
  function promptForEmail(userInteraction, title, emailHint) {
@@ -2145,22 +2106,48 @@ function promptForOTP(userInteraction, email, alert) {
2145
2106
  return otp;
2146
2107
  });
2147
2108
  }
2109
+ function confirmLogout(userInteraction, currentUserId, numUnsyncedChanges) {
2110
+ return __awaiter(this, void 0, void 0, function* () {
2111
+ const alerts = [
2112
+ {
2113
+ type: 'warning',
2114
+ messageCode: 'LOGOUT_CONFIRMATION',
2115
+ message: `{numUnsyncedChanges} unsynced changes will get lost!
2116
+ Logout anyway?`,
2117
+ messageParams: {
2118
+ currentUserId,
2119
+ numUnsyncedChanges: numUnsyncedChanges.toString(),
2120
+ }
2121
+ },
2122
+ ];
2123
+ return yield interactWithUser(userInteraction, {
2124
+ type: 'logout-confirmation',
2125
+ title: 'Confirm Logout',
2126
+ alerts,
2127
+ fields: {},
2128
+ submitLabel: 'Confirm logout',
2129
+ cancelLabel: 'Cancel'
2130
+ })
2131
+ .then(() => true)
2132
+ .catch(() => false);
2133
+ });
2134
+ }
2148
2135
 
2149
2136
  function loadAccessToken(db) {
2150
- var _a, _b;
2137
+ var _a, _b, _c;
2151
2138
  return __awaiter(this, void 0, void 0, function* () {
2152
2139
  const currentUser = yield db.getCurrentUser();
2153
2140
  const { accessToken, accessTokenExpiration, refreshToken, refreshTokenExpiration, claims, } = currentUser;
2154
2141
  if (!accessToken)
2155
- return;
2142
+ return null;
2156
2143
  const expTime = (_a = accessTokenExpiration === null || accessTokenExpiration === void 0 ? void 0 : accessTokenExpiration.getTime()) !== null && _a !== void 0 ? _a : Infinity;
2157
- if (expTime > Date.now()) {
2158
- return accessToken;
2144
+ if (expTime > Date.now() && (((_b = currentUser.license) === null || _b === void 0 ? void 0 : _b.status) || 'ok') === 'ok') {
2145
+ return currentUser;
2159
2146
  }
2160
2147
  if (!refreshToken) {
2161
2148
  throw new Error(`Refresh token missing`);
2162
2149
  }
2163
- const refreshExpTime = (_b = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _b !== void 0 ? _b : Infinity;
2150
+ const refreshExpTime = (_c = refreshTokenExpiration === null || refreshTokenExpiration === void 0 ? void 0 : refreshTokenExpiration.getTime()) !== null && _c !== void 0 ? _c : Infinity;
2164
2151
  if (refreshExpTime <= Date.now()) {
2165
2152
  throw new Error(`Refresh token has expired`);
2166
2153
  }
@@ -2168,8 +2155,10 @@ function loadAccessToken(db) {
2168
2155
  yield db.table('$logins').update(claims.sub, {
2169
2156
  accessToken: refreshedLogin.accessToken,
2170
2157
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
2158
+ claims: refreshedLogin.claims,
2159
+ license: refreshedLogin.license,
2171
2160
  });
2172
- return refreshedLogin.accessToken;
2161
+ return refreshedLogin;
2173
2162
  });
2174
2163
  }
2175
2164
  function authenticate(url, context, fetchToken, userInteraction, hints) {
@@ -2217,10 +2206,24 @@ function refreshAccessToken(url, login) {
2217
2206
  if (res.status !== 200)
2218
2207
  throw new Error(`RefreshToken: Status ${res.status} from ${url}/token`);
2219
2208
  const response = yield res.json();
2209
+ if (response.type === 'error') {
2210
+ throw new TokenErrorResponseError(response);
2211
+ }
2220
2212
  login.accessToken = response.accessToken;
2221
2213
  login.accessTokenExpiration = response.accessTokenExpiration
2222
2214
  ? new Date(response.accessTokenExpiration)
2223
2215
  : undefined;
2216
+ login.claims = response.claims;
2217
+ login.license = {
2218
+ type: response.userType,
2219
+ status: response.claims.license || 'ok',
2220
+ };
2221
+ if (response.evalDaysLeft != null) {
2222
+ login.license.evalDaysLeft = response.evalDaysLeft;
2223
+ }
2224
+ if (response.userValidUntil != null) {
2225
+ login.license.validUntil = new Date(response.userValidUntil);
2226
+ }
2224
2227
  return login;
2225
2228
  });
2226
2229
  }
@@ -2252,8 +2255,15 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
2252
2255
  public_key: publicKeyPEM,
2253
2256
  hints,
2254
2257
  });
2258
+ if (response2.type === 'error') {
2259
+ throw new TokenErrorResponseError(response2);
2260
+ }
2255
2261
  if (response2.type !== 'tokens')
2256
2262
  throw new Error(`Unexpected response type from token endpoint: ${response2.type}`);
2263
+ /*const licenseStatus = response2.claims.license || 'ok';
2264
+ if (licenseStatus !== 'ok') {
2265
+ throw new InvalidLicenseError(licenseStatus);
2266
+ }*/
2257
2267
  context.accessToken = response2.accessToken;
2258
2268
  context.accessTokenExpiration = new Date(response2.accessTokenExpiration);
2259
2269
  context.refreshToken = response2.refreshToken;
@@ -2264,6 +2274,16 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
2264
2274
  context.email = response2.claims.email;
2265
2275
  context.name = response2.claims.name;
2266
2276
  context.claims = response2.claims;
2277
+ context.license = {
2278
+ type: response2.userType,
2279
+ status: response2.claims.license || 'ok',
2280
+ };
2281
+ if (response2.evalDaysLeft != null) {
2282
+ context.license.evalDaysLeft = response2.evalDaysLeft;
2283
+ }
2284
+ if (response2.userValidUntil != null) {
2285
+ context.license.validUntil = new Date(response2.userValidUntil);
2286
+ }
2267
2287
  if (response2.alerts && response2.alerts.length > 0) {
2268
2288
  yield interactWithUser(userInteraction, {
2269
2289
  type: 'message-alert',
@@ -2275,12 +2295,36 @@ function userAuthenticate(context, fetchToken, userInteraction, hints) {
2275
2295
  return context;
2276
2296
  }
2277
2297
  catch (error) {
2278
- yield alertUser(userInteraction, 'Authentication Failed', {
2279
- type: 'error',
2280
- messageCode: 'GENERIC_ERROR',
2281
- message: `We're having a problem authenticating right now.`,
2282
- messageParams: {},
2283
- }).catch(() => { });
2298
+ if (error instanceof TokenErrorResponseError) {
2299
+ yield alertUser(userInteraction, error.title, {
2300
+ type: 'error',
2301
+ messageCode: error.messageCode,
2302
+ message: error.message,
2303
+ messageParams: {},
2304
+ });
2305
+ throw error;
2306
+ }
2307
+ let message = `We're having a problem authenticating right now.`;
2308
+ console.error(`Error authenticating`, error);
2309
+ if (error instanceof TypeError) {
2310
+ const isOffline = typeof navigator !== undefined && !navigator.onLine;
2311
+ if (isOffline) {
2312
+ message = `You seem to be offline. Please connect to the internet and try again.`;
2313
+ }
2314
+ else if (Dexie.debug || (typeof location !== 'undefined' && (location.hostname === 'localhost' || location.hostname === '127.0.0.1'))) {
2315
+ // The audience is most likely the developer. Suggest to whitelist the localhost origin:
2316
+ message = `Could not connect to server. Please verify that your origin '${location.origin}' is whitelisted using \`npx dexie-cloud whitelist\``;
2317
+ }
2318
+ else {
2319
+ message = `Could not connect to server. Please verify the connection.`;
2320
+ }
2321
+ yield alertUser(userInteraction, 'Authentication Failed', {
2322
+ type: 'error',
2323
+ messageCode: 'GENERIC_ERROR',
2324
+ message,
2325
+ messageParams: {},
2326
+ }).catch(() => { });
2327
+ }
2284
2328
  throw error;
2285
2329
  }
2286
2330
  });
@@ -2327,6 +2371,75 @@ class AuthPersistedContext {
2327
2371
  }
2328
2372
  }
2329
2373
 
2374
+ const UNAUTHORIZED_USER = {
2375
+ userId: "unauthorized",
2376
+ name: "Unauthorized",
2377
+ claims: {
2378
+ sub: "unauthorized",
2379
+ },
2380
+ lastLogin: new Date(0)
2381
+ };
2382
+ try {
2383
+ Object.freeze(UNAUTHORIZED_USER);
2384
+ Object.freeze(UNAUTHORIZED_USER.claims);
2385
+ }
2386
+ catch (_a) { }
2387
+
2388
+ function waitUntil(o, // Works with Dexie's liveQuery observables if we'd need that
2389
+ predicate) {
2390
+ return firstValueFrom(from$1(o).pipe(filter$1(predicate)));
2391
+ }
2392
+
2393
+ function logout(db) {
2394
+ return __awaiter(this, void 0, void 0, function* () {
2395
+ const numUnsyncedChanges = yield _logout(db);
2396
+ if (numUnsyncedChanges) {
2397
+ if (yield confirmLogout(db.cloud.userInteraction, db.cloud.currentUserId, numUnsyncedChanges)) {
2398
+ yield _logout(db, { deleteUnsyncedData: true });
2399
+ }
2400
+ else {
2401
+ throw new Error(`User cancelled logout due to unsynced changes`);
2402
+ }
2403
+ }
2404
+ });
2405
+ }
2406
+ function _logout(db, { deleteUnsyncedData = false } = {}) {
2407
+ return __awaiter(this, void 0, void 0, function* () {
2408
+ // Clear the database without emptying configuration options.
2409
+ const [numUnsynced, loggedOut] = yield db.dx.transaction('rw', db.dx.tables, (tx) => __awaiter(this, void 0, void 0, function* () {
2410
+ // @ts-ignore
2411
+ const idbtrans = tx.idbtrans;
2412
+ idbtrans.disableChangeTracking = true;
2413
+ idbtrans.disableAccessControl = true;
2414
+ const mutationTables = tx.storeNames.filter((tableName) => tableName.endsWith('_mutations'));
2415
+ // Count unsynced changes
2416
+ const unsyncCounts = yield Promise.all(mutationTables.map((mutationTable) => tx.table(mutationTable).count()));
2417
+ const sumUnSynced = unsyncCounts.reduce((a, b) => a + b, 0);
2418
+ if (sumUnSynced > 0 && !deleteUnsyncedData) {
2419
+ // Let caller ask user if they want to delete unsynced data.
2420
+ return [sumUnSynced, false];
2421
+ }
2422
+ // Either there are no unsynched changes, or caller provided flag deleteUnsynchedData = true.
2423
+ // Clear all tables except $jobs and $syncState (except the persisted sync state which is
2424
+ // also cleared because we're going to rebuild it using a fresh sync).
2425
+ db.$syncState.delete('syncState');
2426
+ for (const table of db.dx.tables) {
2427
+ if (table.name !== '$jobs' && table.name !== '$syncState') {
2428
+ table.clear();
2429
+ }
2430
+ }
2431
+ return [sumUnSynced, true];
2432
+ }));
2433
+ if (loggedOut) {
2434
+ // Wait for currentUser observable to emit UNAUTHORIZED_USER
2435
+ yield waitUntil(db.cloud.currentUser, (user) => user.userId === UNAUTHORIZED_USER.userId);
2436
+ // Then perform an initial sync
2437
+ yield db.cloud.sync({ purpose: 'pull', wait: true });
2438
+ }
2439
+ return numUnsynced;
2440
+ });
2441
+ }
2442
+
2330
2443
  class HttpError extends Error {
2331
2444
  constructor(res, message) {
2332
2445
  super(message || `${res.status} ${res.statusText}`);
@@ -2380,8 +2493,9 @@ function otpFetchTokenCallback(db) {
2380
2493
  throw new HttpError(res1, errMsg);
2381
2494
  }
2382
2495
  const response = yield res1.json();
2383
- if (response.type === 'tokens') {
2496
+ if (response.type === 'tokens' || response.type === 'error') {
2384
2497
  // Demo user request can get a "tokens" response right away
2498
+ // Error can also be returned right away.
2385
2499
  return response;
2386
2500
  }
2387
2501
  else if (tokenRequest.grant_type === 'otp') {
@@ -2413,12 +2527,6 @@ function otpFetchTokenCallback(db) {
2413
2527
  }
2414
2528
  if (res2.status !== 200) {
2415
2529
  const errMsg = yield res2.text();
2416
- yield alertUser(userInteraction, "OTP Authentication Failed", {
2417
- type: 'error',
2418
- messageCode: 'GENERIC_ERROR',
2419
- message: errMsg,
2420
- messageParams: {}
2421
- }).catch(() => { });
2422
2530
  throw new HttpError(res2, errMsg);
2423
2531
  }
2424
2532
  const response2 = yield res2.json();
@@ -2431,6 +2539,18 @@ function otpFetchTokenCallback(db) {
2431
2539
  };
2432
2540
  }
2433
2541
 
2542
+ /** A way to log to console in production without terser stripping out
2543
+ * it from the release bundle.
2544
+ * This should be used very rarely and only in places where it's
2545
+ * absolutely necessary to log something in production.
2546
+ *
2547
+ * @param level
2548
+ * @param args
2549
+ */
2550
+ function prodLog(level, ...args) {
2551
+ globalThis["con" + "sole"][level](...args);
2552
+ }
2553
+
2434
2554
  /** This function changes or sets the current user as requested.
2435
2555
  *
2436
2556
  * Use cases:
@@ -2457,102 +2577,76 @@ function setCurrentUser(db, user) {
2457
2577
  }));
2458
2578
  user.isLoggedIn = true;
2459
2579
  user.lastLogin = new Date();
2460
- yield user.save();
2461
- console.debug('Saved new user', user.email);
2462
- }));
2463
- yield new Promise((resolve) => {
2464
- if (db.cloud.currentUserId === user.userId) {
2465
- resolve(null);
2580
+ try {
2581
+ yield user.save();
2466
2582
  }
2467
- else {
2468
- const subscription = db.cloud.currentUser.subscribe((currentUser) => {
2469
- if (currentUser.userId === user.userId) {
2470
- subscription.unsubscribe();
2471
- resolve(null);
2583
+ catch (e) {
2584
+ try {
2585
+ if (e.name === 'DataCloneError') {
2586
+ // We've seen this buggy behavior in some browsers and in case it happens
2587
+ // again we really need to collect the details to understand what's going on.
2588
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2589
+ prodLog('debug', `Login context property names:`, Object.keys(user));
2590
+ prodLog('debug', `Login context:`, user);
2591
+ prodLog('debug', `Login context JSON:`, JSON.stringify(user));
2472
2592
  }
2473
- });
2593
+ }
2594
+ catch (_a) { }
2595
+ throw e;
2474
2596
  }
2475
- });
2476
- // TANKAR!!!!
2477
- // V: Service workern kommer inte ha tillgång till currentUserObservable om den inte istället härrör från ett liveQuery.
2478
- // V: Samma med andra windows.
2479
- // V: Så kanske göra om den till att häröra från liveQuery som läser $logins.orderBy('lastLogin').last().
2480
- // V: Då bara vara medveten om:
2481
- // V: En sån observable börjar hämta data vid första subscribe
2482
- // V: Vi har inget "inital value" men kan emulera det till att vara ANONYMOUS_USER
2483
- // V: Om requireAuth är true, så borde db.on(ready) hålla databasen stängd för alla utom denna observable.
2484
- // V: Om inte så behöver den inte blocka.
2485
- // Andra tankar:
2486
- // * Man kan inte byta användare när man är offline. Skulle gå att flytta realms till undanstuff-tabell vid user-change.
2487
- // men troligen inte värt det.
2488
- // * Istället: sälj inte inte switch-user funktionalitet utan tala enbart om inloggat vs icke inloggat läge.
2489
- // * populate $logins med ANONYMOUS så att en påbörjad inloggning inte räknas, alternativt ha en boolean prop!
2490
- // Kanske bäst ha en boolean prop!
2491
- // * Alternativ switch-user funktionalitet:
2492
- // * DBCore gömmer data från realms man inte har tillgång till.
2493
- // * Cursor impl behövs också då.
2494
- // * Då blir det snabba user switch.
2495
- // * claims-settet som skickas till servern blir summan av alla claims. Då måste servern stödja multipla tokens eller
2496
- // att ens token är ett samlad.
2597
+ console.debug('Saved new user', user.email);
2598
+ }));
2599
+ yield waitUntil(db.cloud.currentUser, (currentUser) => currentUser.userId === user.userId);
2497
2600
  });
2498
2601
  }
2499
2602
 
2500
2603
  function login(db, hints) {
2604
+ var _a;
2501
2605
  return __awaiter(this, void 0, void 0, function* () {
2502
2606
  const currentUser = yield db.getCurrentUser();
2503
- if (currentUser.isLoggedIn) {
2504
- if (hints) {
2505
- if (hints.email && db.cloud.currentUser.value.email !== hints.email) {
2506
- throw new Error(`Must logout before changing user`);
2507
- }
2508
- if (hints.userId && db.cloud.currentUserId !== hints.userId) {
2509
- throw new Error(`Must logout before changing user`);
2510
- }
2607
+ const origUserId = currentUser.userId;
2608
+ if (currentUser.isLoggedIn && (!hints || (!hints.email && !hints.userId))) {
2609
+ const licenseStatus = ((_a = currentUser.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
2610
+ if (licenseStatus === 'ok' && currentUser.accessToken && (!currentUser.accessTokenExpiration || currentUser.accessTokenExpiration.getTime() > Date.now())) {
2611
+ // Already authenticated according to given hints. And license is valid.
2612
+ return false;
2511
2613
  }
2512
- // Already authenticated according to given hints.
2513
- return false;
2614
+ if (currentUser.refreshToken && (!currentUser.refreshTokenExpiration || currentUser.refreshTokenExpiration.getTime() > Date.now())) {
2615
+ // Refresh the token
2616
+ yield loadAccessToken(db);
2617
+ return false;
2618
+ }
2619
+ // No refresh token - must re-authenticate:
2514
2620
  }
2515
2621
  const context = new AuthPersistedContext(db, {
2516
2622
  claims: {},
2517
2623
  lastLogin: new Date(0),
2518
2624
  });
2519
2625
  yield authenticate(db.cloud.options.databaseUrl, context, db.cloud.options.fetchTokens || otpFetchTokenCallback(db), db.cloud.userInteraction, hints);
2520
- try {
2521
- yield context.save();
2522
- }
2523
- catch (e) {
2524
- try {
2525
- if (e.name === 'DataCloneError') {
2526
- console.debug(`Login context property names:`, Object.keys(context));
2527
- console.debug(`Login context:`, context);
2528
- console.debug(`Login context JSON:`, JSON.stringify(context));
2529
- }
2530
- }
2531
- catch (_a) { }
2532
- throw e;
2533
- }
2626
+ if (origUserId !== UNAUTHORIZED_USER.userId && context.userId !== origUserId) {
2627
+ // User was logged in before, but now logged in as another user.
2628
+ yield logout(db);
2629
+ }
2630
+ /*try {
2631
+ await context.save();
2632
+ } catch (e) {
2633
+ try {
2634
+ if (e.name === 'DataCloneError') {
2635
+ console.debug(`Login context property names:`, Object.keys(context));
2636
+ console.debug(`Login context:`, context);
2637
+ console.debug(`Login context JSON:`, JSON.stringify(context));
2638
+ }
2639
+ } catch {}
2640
+ throw e;
2641
+ }*/
2534
2642
  yield setCurrentUser(db, context);
2535
2643
  // Make sure to resync as the new login will be authorized
2536
2644
  // for new realms.
2537
2645
  triggerSync(db, "pull");
2538
- return true;
2646
+ return context.userId !== origUserId;
2539
2647
  });
2540
2648
  }
2541
2649
 
2542
- const UNAUTHORIZED_USER = {
2543
- userId: "unauthorized",
2544
- name: "Unauthorized",
2545
- claims: {
2546
- sub: "unauthorized",
2547
- },
2548
- lastLogin: new Date(0)
2549
- };
2550
- try {
2551
- Object.freeze(UNAUTHORIZED_USER);
2552
- Object.freeze(UNAUTHORIZED_USER.claims);
2553
- }
2554
- catch (_a) { }
2555
-
2556
2650
  const swHolder = {};
2557
2651
  const swContainer = typeof self !== 'undefined' && self.document && // self.document is to verify we're not the SW ourself
2558
2652
  typeof navigator !== 'undefined' && navigator.serviceWorker;
@@ -3449,6 +3543,40 @@ function cloneChange(change, rewriteValues) {
3449
3543
  : change.muts.map((m) => (Object.assign(Object.assign({}, m), { keys: m.keys.slice() }))) });
3450
3544
  }
3451
3545
 
3546
+ // If we get Ratelimit-Limit and Ratelimit-Remaining where Ratelimit-Remaining is below
3547
+ // (Ratelimit-Limit / 2), we should delay the next sync by (Ratelimit-Reset / Ratelimit-Remaining)
3548
+ // seconds (given that there is a Ratelimit-Reset header).
3549
+ let syncRatelimitDelays = new WeakMap();
3550
+ function checkSyncRateLimitDelay(db) {
3551
+ var _a, _b;
3552
+ return __awaiter(this, void 0, void 0, function* () {
3553
+ const delatMilliseconds = ((_b = (_a = syncRatelimitDelays.get(db)) === null || _a === void 0 ? void 0 : _a.getTime()) !== null && _b !== void 0 ? _b : 0) - Date.now();
3554
+ if (delatMilliseconds > 0) {
3555
+ console.debug(`Stalling sync request ${delatMilliseconds} ms to spare ratelimits`);
3556
+ yield new Promise(resolve => setTimeout(resolve, delatMilliseconds));
3557
+ }
3558
+ });
3559
+ }
3560
+ function updateSyncRateLimitDelays(db, res) {
3561
+ const limit = res.headers.get('Ratelimit-Limit');
3562
+ const remaining = res.headers.get('Ratelimit-Remaining');
3563
+ const reset = res.headers.get('Ratelimit-Reset');
3564
+ if (limit && remaining && reset) {
3565
+ const limitNum = Number(limit);
3566
+ const remainingNum = Math.max(0, Number(remaining));
3567
+ const willResetInSeconds = Number(reset);
3568
+ if (remainingNum < limitNum / 2) {
3569
+ const delay = Math.ceil(willResetInSeconds / (remainingNum + 1));
3570
+ syncRatelimitDelays.set(db, new Date(Date.now() + delay * 1000));
3571
+ console.debug(`Sync ratelimit delay set to ${delay} seconds`);
3572
+ }
3573
+ else {
3574
+ syncRatelimitDelays.delete(db);
3575
+ console.debug(`Sync ratelimit delay cleared`);
3576
+ }
3577
+ }
3578
+ }
3579
+
3452
3580
  //import {BisonWebStreamReader} from "dreambase-library/dist/typeson-simplified/BisonWebStreamReader";
3453
3581
  function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser) {
3454
3582
  return __awaiter(this, void 0, void 0, function* () {
@@ -3457,9 +3585,20 @@ function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, c
3457
3585
  //
3458
3586
  const headers = {
3459
3587
  Accept: 'application/json, application/x-bison, application/x-bison-stream',
3460
- 'Content-Type': 'application/tson'
3588
+ 'Content-Type': 'application/tson',
3461
3589
  };
3462
- const accessToken = yield loadAccessToken(db);
3590
+ const updatedUser = yield loadAccessToken(db);
3591
+ /*
3592
+ if (updatedUser?.license && changes.length > 0) {
3593
+ if (updatedUser.license.status === 'expired') {
3594
+ throw new Error(`License has expired`);
3595
+ }
3596
+ if (updatedUser.license.status === 'deactivated') {
3597
+ throw new Error(`License deactivated`);
3598
+ }
3599
+ }
3600
+ */
3601
+ const accessToken = updatedUser === null || updatedUser === void 0 ? void 0 : updatedUser.accessToken;
3463
3602
  if (accessToken) {
3464
3603
  headers.Authorization = `Bearer ${accessToken}`;
3465
3604
  }
@@ -3468,27 +3607,31 @@ function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, c
3468
3607
  dbID: syncState === null || syncState === void 0 ? void 0 : syncState.remoteDbId,
3469
3608
  clientIdentity,
3470
3609
  schema: schema || {},
3471
- lastPull: syncState ? {
3472
- serverRevision: syncState.serverRevision,
3473
- realms: syncState.realms,
3474
- inviteRealms: syncState.inviteRealms
3475
- } : undefined,
3610
+ lastPull: syncState
3611
+ ? {
3612
+ serverRevision: syncState.serverRevision,
3613
+ realms: syncState.realms,
3614
+ inviteRealms: syncState.inviteRealms,
3615
+ }
3616
+ : undefined,
3476
3617
  baseRevs,
3477
- changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes)
3618
+ changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
3478
3619
  };
3479
- console.debug("Sync request", syncRequest);
3620
+ console.debug('Sync request', syncRequest);
3480
3621
  db.syncStateChangedEvent.next({
3481
3622
  phase: 'pushing',
3482
3623
  });
3483
3624
  const res = yield fetch(`${databaseUrl}/sync`, {
3484
3625
  method: 'post',
3485
3626
  headers,
3486
- body: TSON.stringify(syncRequest)
3627
+ credentials: 'include',
3628
+ body: TSON.stringify(syncRequest),
3487
3629
  });
3488
3630
  //const contentLength = Number(res.headers.get('content-length'));
3489
3631
  db.syncStateChangedEvent.next({
3490
- phase: 'pulling'
3632
+ phase: 'pulling',
3491
3633
  });
3634
+ updateSyncRateLimitDelays(db, res);
3492
3635
  if (!res.ok) {
3493
3636
  throw new HttpError(res);
3494
3637
  }
@@ -3690,12 +3833,13 @@ const CURRENT_SYNC_WORKER = 'currentSyncWorker';
3690
3833
  function sync(db, options, schema, syncOptions) {
3691
3834
  return _sync
3692
3835
  .apply(this, arguments)
3693
- .then(() => {
3836
+ .then((result) => {
3694
3837
  if (!(syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)) { // && syncOptions?.purpose !== 'push') {
3695
3838
  db.syncStateChangedEvent.next({
3696
3839
  phase: 'in-sync',
3697
3840
  });
3698
3841
  }
3842
+ return result;
3699
3843
  })
3700
3844
  .catch((error) => __awaiter(this, void 0, void 0, function* () {
3701
3845
  if (syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)
@@ -3910,6 +4054,7 @@ function _sync(db, options, schema, { isInitialSync, cancelToken, justCheckIfNee
3910
4054
  }));
3911
4055
  if (!done) {
3912
4056
  console.debug('MORE SYNC NEEDED. Go for it again!');
4057
+ yield checkSyncRateLimitDelay(db);
3913
4058
  return yield _sync(db, options, schema, { isInitialSync, cancelToken });
3914
4059
  }
3915
4060
  console.debug('SYNC DONE', { isInitialSync });
@@ -4046,6 +4191,8 @@ function MessagesFromServerConsumer(db) {
4046
4191
  yield db.table('$logins').update(user.userId, {
4047
4192
  accessToken: refreshedLogin.accessToken,
4048
4193
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
4194
+ claims: refreshedLogin.claims,
4195
+ license: refreshedLogin.license,
4049
4196
  });
4050
4197
  // Updating $logins will trigger emission of db.cloud.currentUser observable, which
4051
4198
  // in turn will lead to that connectWebSocket.ts will reconnect the socket with the
@@ -4614,6 +4761,13 @@ function writeLock(fn, prop) {
4614
4761
 
4615
4762
  const outstandingTransactions = new BehaviorSubject(new Set());
4616
4763
 
4764
+ function isEagerSyncDisabled(db) {
4765
+ var _a, _b, _c, _d;
4766
+ return (((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.disableEagerSync) ||
4767
+ ((_c = (_b = db.cloud.currentUser.value) === null || _b === void 0 ? void 0 : _b.license) === null || _c === void 0 ? void 0 : _c.status) !== 'ok' ||
4768
+ !((_d = db.cloud.options) === null || _d === void 0 ? void 0 : _d.databaseUrl));
4769
+ }
4770
+
4617
4771
  /** Tracks all mutations in the same transaction as the mutations -
4618
4772
  * so it is guaranteed that no mutation goes untracked - and if transaction
4619
4773
  * aborts, the mutations won't be tracked.
@@ -4622,7 +4776,7 @@ const outstandingTransactions = new BehaviorSubject(new Set());
4622
4776
  * changes to server and cleanup the tracked mutations once the server has
4623
4777
  * ackowledged that it got them.
4624
4778
  */
4625
- function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4779
+ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
4626
4780
  return {
4627
4781
  stack: 'dbcore',
4628
4782
  name: 'MutationTrackingMiddleware',
@@ -4633,7 +4787,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4633
4787
  try {
4634
4788
  mutTableMap = new Map(ordinaryTables.map((tbl) => [
4635
4789
  tbl.name,
4636
- core.table(`$${tbl.name}_mutations`)
4790
+ core.table(`$${tbl.name}_mutations`),
4637
4791
  ]));
4638
4792
  }
4639
4793
  catch (_a) {
@@ -4667,15 +4821,9 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4667
4821
  outstandingTransactions.next(outstandingTransactions.value);
4668
4822
  };
4669
4823
  const txComplete = () => {
4670
- var _a;
4671
- if (tx.mutationsAdded && ((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl)) {
4672
- if (db.cloud.usingServiceWorker) {
4673
- console.debug('registering sync event');
4674
- registerSyncEvent(db, "push");
4675
- }
4676
- else {
4677
- db.localSyncEvent.next({ purpose: "push" });
4678
- }
4824
+ if (tx.mutationsAdded &&
4825
+ !isEagerSyncDisabled(db)) {
4826
+ triggerSync(db, 'push');
4679
4827
  }
4680
4828
  removeTransaction();
4681
4829
  };
@@ -4742,7 +4890,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4742
4890
  .query({
4743
4891
  query: { range: req.range, index: schema.primaryKey },
4744
4892
  trans: req.trans,
4745
- values: false
4893
+ values: false,
4746
4894
  })
4747
4895
  // Do a delete request instead, but keep the criteria info for the server to execute
4748
4896
  .then((res) => {
@@ -4750,7 +4898,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4750
4898
  type: 'delete',
4751
4899
  keys: res.result,
4752
4900
  trans: req.trans,
4753
- criteria: { index: null, range: req.range }
4901
+ criteria: { index: null, range: req.range },
4754
4902
  });
4755
4903
  })
4756
4904
  : mutateAndLog(req);
@@ -4758,7 +4906,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4758
4906
  function mutateAndLog(req) {
4759
4907
  const trans = req.trans;
4760
4908
  trans.mutationsAdded = true;
4761
- const { txid, currentUser: { userId } } = trans;
4909
+ const { txid, currentUser: { userId }, } = trans;
4762
4910
  const { type } = req;
4763
4911
  const opNo = ++trans.opCount;
4764
4912
  return table.mutate(req).then((res) => {
@@ -4779,7 +4927,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4779
4927
  keys,
4780
4928
  criteria: req.criteria,
4781
4929
  txid,
4782
- userId
4930
+ userId,
4783
4931
  }
4784
4932
  : req.type === 'add'
4785
4933
  ? {
@@ -4789,7 +4937,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4789
4937
  keys,
4790
4938
  txid,
4791
4939
  userId,
4792
- values
4940
+ values,
4793
4941
  }
4794
4942
  : req.criteria && req.changeSpec
4795
4943
  ? {
@@ -4801,7 +4949,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4801
4949
  criteria: req.criteria,
4802
4950
  changeSpec: req.changeSpec,
4803
4951
  txid,
4804
- userId
4952
+ userId,
4805
4953
  }
4806
4954
  : updates
4807
4955
  ? {
@@ -4812,7 +4960,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4812
4960
  keys: updates.keys,
4813
4961
  changeSpecs: updates.changeSpecs,
4814
4962
  txid,
4815
- userId
4963
+ userId,
4816
4964
  }
4817
4965
  : {
4818
4966
  type: 'upsert',
@@ -4821,7 +4969,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4821
4969
  keys,
4822
4970
  values,
4823
4971
  txid,
4824
- userId
4972
+ userId,
4825
4973
  };
4826
4974
  return keys.length > 0 || ('criteria' in req && req.criteria)
4827
4975
  ? mutsTable
@@ -4831,7 +4979,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4831
4979
  });
4832
4980
  }
4833
4981
  } });
4834
- }
4982
+ },
4835
4983
  };
4836
4984
  }
4837
4985
 
@@ -5190,6 +5338,20 @@ class WSConnection extends Subscription$1 {
5190
5338
  }
5191
5339
  }
5192
5340
 
5341
+ class InvalidLicenseError extends Error {
5342
+ constructor(license) {
5343
+ super(license === 'expired'
5344
+ ? `License expired`
5345
+ : license === 'deactivated'
5346
+ ? `User deactivated`
5347
+ : 'Invalid license');
5348
+ this.name = 'InvalidLicenseError';
5349
+ if (license) {
5350
+ this.license = license;
5351
+ }
5352
+ }
5353
+ }
5354
+
5193
5355
  function sleep(ms) {
5194
5356
  return new Promise((resolve) => setTimeout(resolve, ms));
5195
5357
  }
@@ -5223,7 +5385,12 @@ function connectWebSocket(db) {
5223
5385
  function createObservable() {
5224
5386
  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.
5225
5387
  take(1), // Don't continue waking up whenever syncState change
5226
- switchMap((syncState) => db.cloud.currentUser.pipe(map((userLogin) => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]))), switchMap(([userLogin, syncState]) => {
5388
+ switchMap((syncState) => db.cloud.currentUser.pipe(map((userLogin) => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => {
5389
+ /*if (userLogin.license?.status && userLogin.license.status !== 'ok') {
5390
+ throw new InvalidLicenseError();
5391
+ }*/
5392
+ return userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]));
5393
+ }), switchMap(([userLogin, syncState]) => {
5227
5394
  if ((userLogin === null || userLogin === void 0 ? void 0 : userLogin.isLoggedIn) && !(syncState === null || syncState === void 0 ? void 0 : syncState.realms.includes(userLogin.userId))) {
5228
5395
  // We're in an in-between state when user is logged in but the user's realms are not yet synced.
5229
5396
  // Don't make this change reconnect the websocket just yet. Wait till syncState is updated
@@ -5252,14 +5419,20 @@ function connectWebSocket(db) {
5252
5419
  yield db.table('$logins').update(user.userId, {
5253
5420
  accessToken: refreshedLogin.accessToken,
5254
5421
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
5422
+ claims: refreshedLogin.claims,
5423
+ license: refreshedLogin.license,
5255
5424
  });
5256
5425
  })), switchMap(() => createObservable()));
5257
5426
  }
5258
5427
  else {
5259
- return throwError(error);
5428
+ return throwError(() => error);
5260
5429
  }
5261
5430
  }), catchError((error) => {
5262
5431
  db.cloud.webSocketStatus.next("error");
5432
+ if (error instanceof InvalidLicenseError) {
5433
+ // Don't retry. Just throw and don't try connect again.
5434
+ return throwError(() => error);
5435
+ }
5263
5436
  return from$1(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
5264
5437
  }));
5265
5438
  }
@@ -5288,97 +5461,12 @@ function isSyncNeeded(db) {
5288
5461
  });
5289
5462
  }
5290
5463
 
5291
- const SECONDS = 1000;
5292
- const MINUTES = 60 * SECONDS;
5293
-
5294
- const myId = randomString(16);
5295
-
5296
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
5297
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
5298
- function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
5299
- return __awaiter(this, void 0, void 0, function* () {
5300
- // Start working.
5301
- //
5302
- // Check if someone else is working on this already.
5303
- //
5304
- const jobsTable = db.table(jobsTableName);
5305
- function aquireLock() {
5306
- return __awaiter(this, void 0, void 0, function* () {
5307
- const gotTheLock = yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5308
- const currentWork = yield jobsTable.get(jobName);
5309
- if (!currentWork) {
5310
- // No one else is working. Let's record that we are.
5311
- yield jobsTable.add({
5312
- nodeId: myId,
5313
- started: new Date(),
5314
- heartbeat: new Date()
5315
- }, jobName);
5316
- return true;
5317
- }
5318
- else if (currentWork.heartbeat.getTime() <
5319
- Date.now() - GUARDED_JOB_TIMEOUT) {
5320
- 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!`);
5321
- // Now, take over!
5322
- yield jobsTable.put({
5323
- nodeId: myId,
5324
- started: new Date(),
5325
- heartbeat: new Date()
5326
- }, jobName);
5327
- return true;
5328
- }
5329
- return false;
5330
- }));
5331
- if (gotTheLock)
5332
- return true;
5333
- // Someone else took the job.
5334
- if (awaitRemoteJob) {
5335
- try {
5336
- const jobDoneObservable = from$1(liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
5337
- yield jobDoneObservable.toPromise();
5338
- return false;
5339
- }
5340
- catch (err) {
5341
- if (err.name !== 'TimeoutError') {
5342
- throw err;
5343
- }
5344
- // Timeout stopped us! Try aquire the lock now.
5345
- // It will likely succeed this time unless
5346
- // another client took it.
5347
- return yield aquireLock();
5348
- }
5349
- }
5350
- return false;
5351
- });
5352
- }
5353
- if (yield aquireLock()) {
5354
- // We own the lock entry and can do our job undisturbed.
5355
- // We're not within a transaction, but these type of locks
5356
- // spans over transactions.
5357
- // Start our heart beat during the job.
5358
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
5359
- const heartbeat = setInterval(() => {
5360
- jobsTable.update(jobName, (job) => {
5361
- if (job.nodeId === myId) {
5362
- job.heartbeat = new Date();
5363
- }
5364
- });
5365
- }, GUARDED_JOB_HEARTBEAT);
5366
- try {
5367
- return yield job();
5368
- }
5369
- finally {
5370
- // Stop heartbeat
5371
- clearInterval(heartbeat);
5372
- // Remove the persisted job state:
5373
- yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5374
- const currentWork = yield jobsTable.get(jobName);
5375
- if (currentWork && currentWork.nodeId === myId) {
5376
- yield jobsTable.delete(jobName);
5377
- }
5378
- }));
5379
- }
5380
- }
5381
- });
5464
+ function performGuardedJob(db, jobName, job) {
5465
+ if (typeof navigator === 'undefined' || !navigator.locks) {
5466
+ // No support for guarding jobs. IE11, node.js, etc.
5467
+ return job();
5468
+ }
5469
+ return navigator.locks.request(db.name + '|' + jobName, () => job());
5382
5470
  }
5383
5471
 
5384
5472
  const ongoingSyncs = new WeakMap();
@@ -5432,6 +5520,9 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5432
5520
  function _syncIfPossible() {
5433
5521
  return __awaiter(this, void 0, void 0, function* () {
5434
5522
  try {
5523
+ // Check if should delay sync due to ratelimit:
5524
+ yield checkSyncRateLimitDelay(db);
5525
+ // Check if we need to lock the sync job. Not needed if we are the service worker.
5435
5526
  if (db.cloud.isServiceWorkerDB) {
5436
5527
  // We are the dedicated sync SW:
5437
5528
  yield sync(db, cloudOptions, cloudSchema, options);
@@ -5439,7 +5530,7 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5439
5530
  else if (!db.cloud.usingServiceWorker) {
5440
5531
  // We use a flow that is better suited for the case when multiple workers want to
5441
5532
  // do the same thing.
5442
- yield performGuardedJob(db, CURRENT_SYNC_WORKER, '$jobs', () => sync(db, cloudOptions, cloudSchema, options));
5533
+ yield performGuardedJob(db, CURRENT_SYNC_WORKER, () => sync(db, cloudOptions, cloudSchema, options));
5443
5534
  }
5444
5535
  else {
5445
5536
  assert(false);
@@ -5461,19 +5552,29 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5461
5552
  }
5462
5553
  }
5463
5554
 
5555
+ const SECONDS = 1000;
5556
+ const MINUTES = 60 * SECONDS;
5557
+
5464
5558
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
5465
5559
  let localSyncEventSubscription = null;
5466
5560
  //let syncHandler: ((event: Event) => void) | null = null;
5467
5561
  //let periodicSyncHandler: ((event: Event) => void) | null = null;
5468
5562
  let cancelToken = { cancelled: false };
5563
+ let retryHandle = null;
5564
+ let retryPurpose = null; // "pull" is superset of "push"
5469
5565
  function syncAndRetry(purpose, retryNum = 1) {
5470
5566
  // Use setTimeout() to get onto a clean stack and
5471
5567
  // break free from possible active transaction:
5472
5568
  setTimeout(() => {
5569
+ if (retryHandle)
5570
+ clearTimeout(retryHandle);
5571
+ const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
5572
+ retryHandle = null;
5573
+ retryPurpose = null;
5473
5574
  syncIfPossible(db, cloudOptions, cloudSchema, {
5474
5575
  cancelToken,
5475
5576
  retryImmediatelyOnFetchError: true,
5476
- purpose,
5577
+ purpose: combPurpose,
5477
5578
  }).catch((e) => {
5478
5579
  console.error('error in syncIfPossible()', e);
5479
5580
  if (cancelToken.cancelled) {
@@ -5483,7 +5584,13 @@ function LocalSyncWorker(db, cloudOptions, cloudSchema) {
5483
5584
  // Mimic service worker sync event: retry 3 times
5484
5585
  // * first retry after 5 minutes
5485
5586
  // * second retry 15 minutes later
5486
- setTimeout(() => syncAndRetry(purpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
5587
+ const combinedPurpose = retryPurpose && retryPurpose === 'pull' ? 'pull' : purpose;
5588
+ const handle = setTimeout(() => syncAndRetry(combinedPurpose, retryNum + 1), [0, 5, 15][retryNum] * MINUTES);
5589
+ // Cancel the previous retryHandle if it exists to avoid scheduling loads of retries.
5590
+ if (retryHandle)
5591
+ clearTimeout(retryHandle);
5592
+ retryHandle = handle;
5593
+ retryPurpose = combinedPurpose;
5487
5594
  }
5488
5595
  });
5489
5596
  }, 0);
@@ -5550,10 +5657,12 @@ const Styles = {
5550
5657
  },
5551
5658
  Alert: {
5552
5659
  error: {
5553
- color: "red"
5660
+ color: "red",
5661
+ fontWeight: "bold"
5554
5662
  },
5555
5663
  warning: {
5556
- color: "yellow"
5664
+ color: "#f80",
5665
+ fontWeight: "bold"
5557
5666
  },
5558
5667
  info: {
5559
5668
  color: "black"
@@ -5594,7 +5703,8 @@ const Styles = {
5594
5703
  border: "3px solid #3d3d5d",
5595
5704
  borderRadius: "8px",
5596
5705
  boxShadow: "0 0 80px 10px #666",
5597
- width: "auto"
5706
+ width: "auto",
5707
+ fontFamily: "sans-serif",
5598
5708
  },
5599
5709
  Input: {
5600
5710
  height: "35px",
@@ -5615,11 +5725,26 @@ function Dialog({ children, className }) {
5615
5725
 
5616
5726
  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}
5617
5727
 
5728
+ /** Resolve a message template with parameters.
5729
+ *
5730
+ * Example:
5731
+ * resolveText({
5732
+ * message: "Hello {name}!",
5733
+ * messageCode: "HELLO",
5734
+ * messageParams: {name: "David"}
5735
+ * }) => "Hello David!"
5736
+ *
5737
+ * @param message Template message with {vars} in it.
5738
+ * @param messageCode Unique code for the message. Can be used for translation.
5739
+ * @param messageParams Parameters to be used in the message.
5740
+ * @returns A final message where parameters have been replaced with values.
5741
+ */
5618
5742
  function resolveText({ message, messageCode, messageParams }) {
5619
- return message.replace(/\{\w+\}/ig, n => messageParams[n.substr(1, n.length - 2)]);
5743
+ return message.replace(/\{\w+\}/ig, n => messageParams[n.substring(1, n.length - 1)]);
5620
5744
  }
5621
5745
 
5622
- function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
5746
+ const OTP_LENGTH = 8;
5747
+ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5623
5748
  const [params, setParams] = p({});
5624
5749
  const firstFieldRef = _(null);
5625
5750
  s(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
@@ -5627,21 +5752,34 @@ function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
5627
5752
  h(p$1, null,
5628
5753
  h("h3", { style: Styles.WindowHeader }, title),
5629
5754
  alerts.map((alert) => (h("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
5630
- h("form", { onSubmit: ev => {
5755
+ h("form", { onSubmit: (ev) => {
5631
5756
  ev.preventDefault();
5632
5757
  onSubmit(params);
5633
- } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label },
5758
+ } }, Object.entries(fields).map(([fieldName, { type, label, placeholder }], idx) => (h("label", { style: Styles.Label, key: idx },
5634
5759
  label ? `${label}: ` : '',
5635
- 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']) })); } })))))),
5760
+ 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) => {
5761
+ var _a;
5762
+ const value = valueTransformer(type, (_a = ev.target) === null || _a === void 0 ? void 0 : _a['value']);
5763
+ let updatedParams = Object.assign(Object.assign({}, params), { [fieldName]: value });
5764
+ setParams(updatedParams);
5765
+ if (type === 'otp' && (value === null || value === void 0 ? void 0 : value.trim().length) === OTP_LENGTH) {
5766
+ // Auto-submit when OTP is filled in.
5767
+ onSubmit(updatedParams);
5768
+ }
5769
+ } })))))),
5636
5770
  h("div", { style: Styles.ButtonsDiv },
5637
- h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, "Submit"),
5638
- h("button", { style: Styles.Button, onClick: onCancel }, "Cancel"))));
5771
+ h(p$1, null,
5772
+ h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, submitLabel),
5773
+ cancelLabel && (h("button", { style: Styles.Button, onClick: onCancel }, cancelLabel))))));
5639
5774
  }
5640
5775
  function valueTransformer(type, value) {
5641
5776
  switch (type) {
5642
- case "email": return value.toLowerCase();
5643
- case "otp": return value.toUpperCase();
5644
- default: return value;
5777
+ case 'email':
5778
+ return value.toLowerCase();
5779
+ case 'otp':
5780
+ return value.toUpperCase();
5781
+ default:
5782
+ return value;
5645
5783
  }
5646
5784
  }
5647
5785
 
@@ -5695,11 +5833,20 @@ function setupDefaultGUI(db) {
5695
5833
  }
5696
5834
  };
5697
5835
  }
5698
- // TODO:
5699
- /*
5700
- * Gjort klart allt kring user interaction förutom att mounta default-ui på ett element.
5701
- * Också att kolla först om nån annan subscribar och i så fall inte göra nåt.
5702
- */
5836
+
5837
+ function associate(factory) {
5838
+ const wm = new WeakMap();
5839
+ return (x) => {
5840
+ let rv = wm.get(x);
5841
+ if (!rv) {
5842
+ rv = factory(x);
5843
+ wm.set(x, rv);
5844
+ }
5845
+ return rv;
5846
+ };
5847
+ }
5848
+
5849
+ const getCurrentUserEmitter = associate((db) => new BehaviorSubject(UNAUTHORIZED_USER));
5703
5850
 
5704
5851
  function computeSyncState(db) {
5705
5852
  let _prevStatus = db.cloud.webSocketStatus.value;
@@ -5727,8 +5874,17 @@ function computeSyncState(db) {
5727
5874
  return combineLatest([
5728
5875
  lazyWebSocketStatus,
5729
5876
  db.syncStateChangedEvent.pipe(startWith({ phase: 'initial' })),
5877
+ getCurrentUserEmitter(db.dx._novip),
5730
5878
  userIsReallyActive
5731
- ]).pipe(map(([status, syncState, userIsActive]) => {
5879
+ ]).pipe(map(([status, syncState, user, userIsActive]) => {
5880
+ var _a;
5881
+ if (((_a = user.license) === null || _a === void 0 ? void 0 : _a.status) && user.license.status !== 'ok') {
5882
+ return {
5883
+ phase: 'offline',
5884
+ status: 'offline',
5885
+ license: user.license.status
5886
+ };
5887
+ }
5732
5888
  let { phase, error, progress } = syncState;
5733
5889
  let adjustedStatus = status;
5734
5890
  if (phase === 'error') {
@@ -5761,23 +5917,12 @@ function computeSyncState(db) {
5761
5917
  error,
5762
5918
  progress,
5763
5919
  status: isOnline ? adjustedStatus : 'offline',
5920
+ license: 'ok'
5764
5921
  };
5765
5922
  return retState;
5766
5923
  }));
5767
5924
  }
5768
5925
 
5769
- function associate(factory) {
5770
- const wm = new WeakMap();
5771
- return (x) => {
5772
- let rv = wm.get(x);
5773
- if (!rv) {
5774
- rv = factory(x);
5775
- wm.set(x, rv);
5776
- }
5777
- return rv;
5778
- };
5779
- }
5780
-
5781
5926
  function createSharedValueObservable(o, defaultValue) {
5782
5927
  let currentValue = defaultValue;
5783
5928
  let shared = from$1(o).pipe(map$1((x) => (currentValue = x)), share({ resetOnRefCountZero: () => timer$1(1000) }));
@@ -5819,8 +5964,6 @@ const getGlobalRolesObservable = associate((db) => {
5819
5964
  })), {});
5820
5965
  });
5821
5966
 
5822
- const getCurrentUserEmitter = associate((db) => new BehaviorSubject(UNAUTHORIZED_USER));
5823
-
5824
5967
  const getInternalAccessControlObservable = associate((db) => {
5825
5968
  return createSharedValueObservable(getCurrentUserEmitter(db._novip).pipe(switchMap((currentUser) => liveQuery(() => db.transaction('r', 'realms', 'members', () => Promise.all([
5826
5969
  db.members.where({ userId: currentUser.userId }).toArray(),
@@ -6110,7 +6253,7 @@ function dexieCloud(dexie) {
6110
6253
  });
6111
6254
  const syncComplete = new Subject();
6112
6255
  dexie.cloud = {
6113
- version: '4.0.1-beta.46',
6256
+ version: '4.0.1-beta.47',
6114
6257
  options: Object.assign({}, DEFAULT_OPTIONS),
6115
6258
  schema: null,
6116
6259
  get currentUserId() {
@@ -6146,11 +6289,24 @@ function dexieCloud(dexie) {
6146
6289
  }
6147
6290
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
6148
6291
  },
6292
+ logout({ force } = {}) {
6293
+ return __awaiter(this, void 0, void 0, function* () {
6294
+ force
6295
+ ? yield _logout(DexieCloudDB(dexie), { deleteUnsyncedData: true })
6296
+ : yield logout(DexieCloudDB(dexie));
6297
+ });
6298
+ },
6149
6299
  sync({ wait, purpose } = { wait: true, purpose: 'push' }) {
6300
+ var _a;
6150
6301
  return __awaiter(this, void 0, void 0, function* () {
6151
6302
  if (wait === undefined)
6152
6303
  wait = true;
6153
6304
  const db = DexieCloudDB(dexie);
6305
+ const licenseStatus = ((_a = db.cloud.currentUser.value.license) === null || _a === void 0 ? void 0 : _a.status) || 'ok';
6306
+ if (licenseStatus !== 'ok') {
6307
+ // Refresh access token to check for updated license
6308
+ yield loadAccessToken(db);
6309
+ }
6154
6310
  if (purpose === 'pull') {
6155
6311
  const syncState = db.cloud.persistedSyncState.value;
6156
6312
  triggerSync(db, purpose);
@@ -6354,7 +6510,9 @@ function dexieCloud(dexie) {
6354
6510
  db.syncStateChangedEvent.next({
6355
6511
  phase: 'not-in-sync',
6356
6512
  });
6357
- triggerSync(db, 'push');
6513
+ if (!isEagerSyncDisabled(db)) {
6514
+ triggerSync(db, 'push');
6515
+ }
6358
6516
  }), fromEvent(self, 'offline').subscribe(() => {
6359
6517
  console.debug('offline!');
6360
6518
  db.syncStateChangedEvent.next({
@@ -6371,7 +6529,7 @@ function dexieCloud(dexie) {
6371
6529
  });
6372
6530
  }
6373
6531
  }
6374
- dexieCloud.version = '4.0.1-beta.46';
6532
+ dexieCloud.version = '4.0.1-beta.47';
6375
6533
  Dexie.Cloud = dexieCloud;
6376
6534
 
6377
6535
  export { dexieCloud as default, dexieCloud, getTiedObjectId, getTiedRealmId };