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
  *
@@ -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;
@@ -3406,9 +3500,19 @@ function encodeIdsForServer(schema, currentUser, changes) {
3406
3500
  const mutClone = changeClone.muts[mutIndex];
3407
3501
  const rewrittenKey = JSON.stringify(key);
3408
3502
  mutClone.keys[keyIndex] = rewrittenKey;
3409
- if (rewriteValues) {
3410
- Dexie.setByKeyPath(mutClone.values[keyIndex], primaryKey.keyPath, rewrittenKey);
3411
- }
3503
+ /* Bug (#1777)
3504
+ We should not rewrite values. It will fail because the key is array and the value is string.
3505
+ Only the keys should be rewritten and it's already done on the server.
3506
+ We should take another round of revieweing how key transformations are being done between
3507
+ client and server and let the server do the key transformations entirely instead now that
3508
+ we have the primary key schema on the server making it possible to do so.
3509
+ if (rewriteValues) {
3510
+ Dexie.setByKeyPath(
3511
+ (mutClone as DBInsertOperation).values[keyIndex],
3512
+ primaryKey.keyPath!,
3513
+ rewrittenKey
3514
+ );
3515
+ }*/
3412
3516
  }
3413
3517
  else if (key[0] === '#') {
3414
3518
  // Private ID - translate!
@@ -3439,6 +3543,40 @@ function cloneChange(change, rewriteValues) {
3439
3543
  : change.muts.map((m) => (Object.assign(Object.assign({}, m), { keys: m.keys.slice() }))) });
3440
3544
  }
3441
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
+
3442
3580
  //import {BisonWebStreamReader} from "dreambase-library/dist/typeson-simplified/BisonWebStreamReader";
3443
3581
  function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, clientIdentity, currentUser) {
3444
3582
  return __awaiter(this, void 0, void 0, function* () {
@@ -3447,9 +3585,20 @@ function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, c
3447
3585
  //
3448
3586
  const headers = {
3449
3587
  Accept: 'application/json, application/x-bison, application/x-bison-stream',
3450
- 'Content-Type': 'application/tson'
3588
+ 'Content-Type': 'application/tson',
3451
3589
  };
3452
- 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;
3453
3602
  if (accessToken) {
3454
3603
  headers.Authorization = `Bearer ${accessToken}`;
3455
3604
  }
@@ -3458,27 +3607,31 @@ function syncWithServer(changes, syncState, baseRevs, db, databaseUrl, schema, c
3458
3607
  dbID: syncState === null || syncState === void 0 ? void 0 : syncState.remoteDbId,
3459
3608
  clientIdentity,
3460
3609
  schema: schema || {},
3461
- lastPull: syncState ? {
3462
- serverRevision: syncState.serverRevision,
3463
- realms: syncState.realms,
3464
- inviteRealms: syncState.inviteRealms
3465
- } : undefined,
3610
+ lastPull: syncState
3611
+ ? {
3612
+ serverRevision: syncState.serverRevision,
3613
+ realms: syncState.realms,
3614
+ inviteRealms: syncState.inviteRealms,
3615
+ }
3616
+ : undefined,
3466
3617
  baseRevs,
3467
- changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes)
3618
+ changes: encodeIdsForServer(db.dx.core.schema, currentUser, changes),
3468
3619
  };
3469
- console.debug("Sync request", syncRequest);
3620
+ console.debug('Sync request', syncRequest);
3470
3621
  db.syncStateChangedEvent.next({
3471
3622
  phase: 'pushing',
3472
3623
  });
3473
3624
  const res = yield fetch(`${databaseUrl}/sync`, {
3474
3625
  method: 'post',
3475
3626
  headers,
3476
- body: TSON.stringify(syncRequest)
3627
+ credentials: 'include',
3628
+ body: TSON.stringify(syncRequest),
3477
3629
  });
3478
3630
  //const contentLength = Number(res.headers.get('content-length'));
3479
3631
  db.syncStateChangedEvent.next({
3480
- phase: 'pulling'
3632
+ phase: 'pulling',
3481
3633
  });
3634
+ updateSyncRateLimitDelays(db, res);
3482
3635
  if (!res.ok) {
3483
3636
  throw new HttpError(res);
3484
3637
  }
@@ -3680,12 +3833,13 @@ const CURRENT_SYNC_WORKER = 'currentSyncWorker';
3680
3833
  function sync(db, options, schema, syncOptions) {
3681
3834
  return _sync
3682
3835
  .apply(this, arguments)
3683
- .then(() => {
3836
+ .then((result) => {
3684
3837
  if (!(syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)) { // && syncOptions?.purpose !== 'push') {
3685
3838
  db.syncStateChangedEvent.next({
3686
3839
  phase: 'in-sync',
3687
3840
  });
3688
3841
  }
3842
+ return result;
3689
3843
  })
3690
3844
  .catch((error) => __awaiter(this, void 0, void 0, function* () {
3691
3845
  if (syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.justCheckIfNeeded)
@@ -3715,7 +3869,7 @@ function sync(db, options, schema, syncOptions) {
3715
3869
  });
3716
3870
  db.syncStateChangedEvent.next({
3717
3871
  phase: isOnline ? 'error' : 'offline',
3718
- error,
3872
+ error: new Error('' + (error === null || error === void 0 ? void 0 : error.message) || error),
3719
3873
  });
3720
3874
  return Promise.reject(error);
3721
3875
  }));
@@ -3900,6 +4054,7 @@ function _sync(db, options, schema, { isInitialSync, cancelToken, justCheckIfNee
3900
4054
  }));
3901
4055
  if (!done) {
3902
4056
  console.debug('MORE SYNC NEEDED. Go for it again!');
4057
+ yield checkSyncRateLimitDelay(db);
3903
4058
  return yield _sync(db, options, schema, { isInitialSync, cancelToken });
3904
4059
  }
3905
4060
  console.debug('SYNC DONE', { isInitialSync });
@@ -4036,6 +4191,8 @@ function MessagesFromServerConsumer(db) {
4036
4191
  yield db.table('$logins').update(user.userId, {
4037
4192
  accessToken: refreshedLogin.accessToken,
4038
4193
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
4194
+ claims: refreshedLogin.claims,
4195
+ license: refreshedLogin.license,
4039
4196
  });
4040
4197
  // Updating $logins will trigger emission of db.cloud.currentUser observable, which
4041
4198
  // in turn will lead to that connectWebSocket.ts will reconnect the socket with the
@@ -4604,6 +4761,13 @@ function writeLock(fn, prop) {
4604
4761
 
4605
4762
  const outstandingTransactions = new BehaviorSubject(new Set());
4606
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
+
4607
4771
  /** Tracks all mutations in the same transaction as the mutations -
4608
4772
  * so it is guaranteed that no mutation goes untracked - and if transaction
4609
4773
  * aborts, the mutations won't be tracked.
@@ -4612,7 +4776,7 @@ const outstandingTransactions = new BehaviorSubject(new Set());
4612
4776
  * changes to server and cleanup the tracked mutations once the server has
4613
4777
  * ackowledged that it got them.
4614
4778
  */
4615
- function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4779
+ function createMutationTrackingMiddleware({ currentUserObservable, db, }) {
4616
4780
  return {
4617
4781
  stack: 'dbcore',
4618
4782
  name: 'MutationTrackingMiddleware',
@@ -4623,7 +4787,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4623
4787
  try {
4624
4788
  mutTableMap = new Map(ordinaryTables.map((tbl) => [
4625
4789
  tbl.name,
4626
- core.table(`$${tbl.name}_mutations`)
4790
+ core.table(`$${tbl.name}_mutations`),
4627
4791
  ]));
4628
4792
  }
4629
4793
  catch (_a) {
@@ -4657,15 +4821,9 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4657
4821
  outstandingTransactions.next(outstandingTransactions.value);
4658
4822
  };
4659
4823
  const txComplete = () => {
4660
- var _a;
4661
- if (tx.mutationsAdded && ((_a = db.cloud.options) === null || _a === void 0 ? void 0 : _a.databaseUrl)) {
4662
- if (db.cloud.usingServiceWorker) {
4663
- console.debug('registering sync event');
4664
- registerSyncEvent(db, "push");
4665
- }
4666
- else {
4667
- db.localSyncEvent.next({ purpose: "push" });
4668
- }
4824
+ if (tx.mutationsAdded &&
4825
+ !isEagerSyncDisabled(db)) {
4826
+ triggerSync(db, 'push');
4669
4827
  }
4670
4828
  removeTransaction();
4671
4829
  };
@@ -4732,7 +4890,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4732
4890
  .query({
4733
4891
  query: { range: req.range, index: schema.primaryKey },
4734
4892
  trans: req.trans,
4735
- values: false
4893
+ values: false,
4736
4894
  })
4737
4895
  // Do a delete request instead, but keep the criteria info for the server to execute
4738
4896
  .then((res) => {
@@ -4740,7 +4898,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4740
4898
  type: 'delete',
4741
4899
  keys: res.result,
4742
4900
  trans: req.trans,
4743
- criteria: { index: null, range: req.range }
4901
+ criteria: { index: null, range: req.range },
4744
4902
  });
4745
4903
  })
4746
4904
  : mutateAndLog(req);
@@ -4748,7 +4906,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4748
4906
  function mutateAndLog(req) {
4749
4907
  const trans = req.trans;
4750
4908
  trans.mutationsAdded = true;
4751
- const { txid, currentUser: { userId } } = trans;
4909
+ const { txid, currentUser: { userId }, } = trans;
4752
4910
  const { type } = req;
4753
4911
  const opNo = ++trans.opCount;
4754
4912
  return table.mutate(req).then((res) => {
@@ -4769,7 +4927,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4769
4927
  keys,
4770
4928
  criteria: req.criteria,
4771
4929
  txid,
4772
- userId
4930
+ userId,
4773
4931
  }
4774
4932
  : req.type === 'add'
4775
4933
  ? {
@@ -4779,7 +4937,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4779
4937
  keys,
4780
4938
  txid,
4781
4939
  userId,
4782
- values
4940
+ values,
4783
4941
  }
4784
4942
  : req.criteria && req.changeSpec
4785
4943
  ? {
@@ -4791,7 +4949,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4791
4949
  criteria: req.criteria,
4792
4950
  changeSpec: req.changeSpec,
4793
4951
  txid,
4794
- userId
4952
+ userId,
4795
4953
  }
4796
4954
  : updates
4797
4955
  ? {
@@ -4802,7 +4960,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4802
4960
  keys: updates.keys,
4803
4961
  changeSpecs: updates.changeSpecs,
4804
4962
  txid,
4805
- userId
4963
+ userId,
4806
4964
  }
4807
4965
  : {
4808
4966
  type: 'upsert',
@@ -4811,7 +4969,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4811
4969
  keys,
4812
4970
  values,
4813
4971
  txid,
4814
- userId
4972
+ userId,
4815
4973
  };
4816
4974
  return keys.length > 0 || ('criteria' in req && req.criteria)
4817
4975
  ? mutsTable
@@ -4821,7 +4979,7 @@ function createMutationTrackingMiddleware({ currentUserObservable, db }) {
4821
4979
  });
4822
4980
  }
4823
4981
  } });
4824
- }
4982
+ },
4825
4983
  };
4826
4984
  }
4827
4985
 
@@ -5180,6 +5338,20 @@ class WSConnection extends Subscription$1 {
5180
5338
  }
5181
5339
  }
5182
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
+
5183
5355
  function sleep(ms) {
5184
5356
  return new Promise((resolve) => setTimeout(resolve, ms));
5185
5357
  }
@@ -5213,7 +5385,12 @@ function connectWebSocket(db) {
5213
5385
  function createObservable() {
5214
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.
5215
5387
  take(1), // Don't continue waking up whenever syncState change
5216
- 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]) => {
5217
5394
  if ((userLogin === null || userLogin === void 0 ? void 0 : userLogin.isLoggedIn) && !(syncState === null || syncState === void 0 ? void 0 : syncState.realms.includes(userLogin.userId))) {
5218
5395
  // We're in an in-between state when user is logged in but the user's realms are not yet synced.
5219
5396
  // Don't make this change reconnect the websocket just yet. Wait till syncState is updated
@@ -5242,14 +5419,20 @@ function connectWebSocket(db) {
5242
5419
  yield db.table('$logins').update(user.userId, {
5243
5420
  accessToken: refreshedLogin.accessToken,
5244
5421
  accessTokenExpiration: refreshedLogin.accessTokenExpiration,
5422
+ claims: refreshedLogin.claims,
5423
+ license: refreshedLogin.license,
5245
5424
  });
5246
5425
  })), switchMap(() => createObservable()));
5247
5426
  }
5248
5427
  else {
5249
- return throwError(error);
5428
+ return throwError(() => error);
5250
5429
  }
5251
5430
  }), catchError((error) => {
5252
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
+ }
5253
5436
  return from$1(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
5254
5437
  }));
5255
5438
  }
@@ -5278,97 +5461,12 @@ function isSyncNeeded(db) {
5278
5461
  });
5279
5462
  }
5280
5463
 
5281
- const SECONDS = 1000;
5282
- const MINUTES = 60 * SECONDS;
5283
-
5284
- const myId = randomString(16);
5285
-
5286
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
5287
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
5288
- function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
5289
- return __awaiter(this, void 0, void 0, function* () {
5290
- // Start working.
5291
- //
5292
- // Check if someone else is working on this already.
5293
- //
5294
- const jobsTable = db.table(jobsTableName);
5295
- function aquireLock() {
5296
- return __awaiter(this, void 0, void 0, function* () {
5297
- const gotTheLock = yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5298
- const currentWork = yield jobsTable.get(jobName);
5299
- if (!currentWork) {
5300
- // No one else is working. Let's record that we are.
5301
- yield jobsTable.add({
5302
- nodeId: myId,
5303
- started: new Date(),
5304
- heartbeat: new Date()
5305
- }, jobName);
5306
- return true;
5307
- }
5308
- else if (currentWork.heartbeat.getTime() <
5309
- Date.now() - GUARDED_JOB_TIMEOUT) {
5310
- 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!`);
5311
- // Now, take over!
5312
- yield jobsTable.put({
5313
- nodeId: myId,
5314
- started: new Date(),
5315
- heartbeat: new Date()
5316
- }, jobName);
5317
- return true;
5318
- }
5319
- return false;
5320
- }));
5321
- if (gotTheLock)
5322
- return true;
5323
- // Someone else took the job.
5324
- if (awaitRemoteJob) {
5325
- try {
5326
- const jobDoneObservable = from$1(liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
5327
- yield jobDoneObservable.toPromise();
5328
- return false;
5329
- }
5330
- catch (err) {
5331
- if (err.name !== 'TimeoutError') {
5332
- throw err;
5333
- }
5334
- // Timeout stopped us! Try aquire the lock now.
5335
- // It will likely succeed this time unless
5336
- // another client took it.
5337
- return yield aquireLock();
5338
- }
5339
- }
5340
- return false;
5341
- });
5342
- }
5343
- if (yield aquireLock()) {
5344
- // We own the lock entry and can do our job undisturbed.
5345
- // We're not within a transaction, but these type of locks
5346
- // spans over transactions.
5347
- // Start our heart beat during the job.
5348
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
5349
- const heartbeat = setInterval(() => {
5350
- jobsTable.update(jobName, (job) => {
5351
- if (job.nodeId === myId) {
5352
- job.heartbeat = new Date();
5353
- }
5354
- });
5355
- }, GUARDED_JOB_HEARTBEAT);
5356
- try {
5357
- return yield job();
5358
- }
5359
- finally {
5360
- // Stop heartbeat
5361
- clearInterval(heartbeat);
5362
- // Remove the persisted job state:
5363
- yield db.transaction('rw!', jobsTableName, () => __awaiter(this, void 0, void 0, function* () {
5364
- const currentWork = yield jobsTable.get(jobName);
5365
- if (currentWork && currentWork.nodeId === myId) {
5366
- yield jobsTable.delete(jobName);
5367
- }
5368
- }));
5369
- }
5370
- }
5371
- });
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());
5372
5470
  }
5373
5471
 
5374
5472
  const ongoingSyncs = new WeakMap();
@@ -5422,6 +5520,9 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5422
5520
  function _syncIfPossible() {
5423
5521
  return __awaiter(this, void 0, void 0, function* () {
5424
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.
5425
5526
  if (db.cloud.isServiceWorkerDB) {
5426
5527
  // We are the dedicated sync SW:
5427
5528
  yield sync(db, cloudOptions, cloudSchema, options);
@@ -5429,7 +5530,7 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5429
5530
  else if (!db.cloud.usingServiceWorker) {
5430
5531
  // We use a flow that is better suited for the case when multiple workers want to
5431
5532
  // do the same thing.
5432
- yield performGuardedJob(db, CURRENT_SYNC_WORKER, '$jobs', () => sync(db, cloudOptions, cloudSchema, options));
5533
+ yield performGuardedJob(db, CURRENT_SYNC_WORKER, () => sync(db, cloudOptions, cloudSchema, options));
5433
5534
  }
5434
5535
  else {
5435
5536
  assert(false);
@@ -5451,19 +5552,29 @@ function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5451
5552
  }
5452
5553
  }
5453
5554
 
5555
+ const SECONDS = 1000;
5556
+ const MINUTES = 60 * SECONDS;
5557
+
5454
5558
  function LocalSyncWorker(db, cloudOptions, cloudSchema) {
5455
5559
  let localSyncEventSubscription = null;
5456
5560
  //let syncHandler: ((event: Event) => void) | null = null;
5457
5561
  //let periodicSyncHandler: ((event: Event) => void) | null = null;
5458
5562
  let cancelToken = { cancelled: false };
5563
+ let retryHandle = null;
5564
+ let retryPurpose = null; // "pull" is superset of "push"
5459
5565
  function syncAndRetry(purpose, retryNum = 1) {
5460
5566
  // Use setTimeout() to get onto a clean stack and
5461
5567
  // break free from possible active transaction:
5462
5568
  setTimeout(() => {
5569
+ if (retryHandle)
5570
+ clearTimeout(retryHandle);
5571
+ const combPurpose = retryPurpose === 'pull' ? 'pull' : purpose;
5572
+ retryHandle = null;
5573
+ retryPurpose = null;
5463
5574
  syncIfPossible(db, cloudOptions, cloudSchema, {
5464
5575
  cancelToken,
5465
5576
  retryImmediatelyOnFetchError: true,
5466
- purpose,
5577
+ purpose: combPurpose,
5467
5578
  }).catch((e) => {
5468
5579
  console.error('error in syncIfPossible()', e);
5469
5580
  if (cancelToken.cancelled) {
@@ -5473,7 +5584,13 @@ function LocalSyncWorker(db, cloudOptions, cloudSchema) {
5473
5584
  // Mimic service worker sync event: retry 3 times
5474
5585
  // * first retry after 5 minutes
5475
5586
  // * second retry 15 minutes later
5476
- 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;
5477
5594
  }
5478
5595
  });
5479
5596
  }, 0);
@@ -5540,10 +5657,12 @@ const Styles = {
5540
5657
  },
5541
5658
  Alert: {
5542
5659
  error: {
5543
- color: "red"
5660
+ color: "red",
5661
+ fontWeight: "bold"
5544
5662
  },
5545
5663
  warning: {
5546
- color: "yellow"
5664
+ color: "#f80",
5665
+ fontWeight: "bold"
5547
5666
  },
5548
5667
  info: {
5549
5668
  color: "black"
@@ -5584,7 +5703,8 @@ const Styles = {
5584
5703
  border: "3px solid #3d3d5d",
5585
5704
  borderRadius: "8px",
5586
5705
  boxShadow: "0 0 80px 10px #666",
5587
- width: "auto"
5706
+ width: "auto",
5707
+ fontFamily: "sans-serif",
5588
5708
  },
5589
5709
  Input: {
5590
5710
  height: "35px",
@@ -5605,11 +5725,26 @@ function Dialog({ children, className }) {
5605
5725
 
5606
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}
5607
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
+ */
5608
5742
  function resolveText({ message, messageCode, messageParams }) {
5609
- 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)]);
5610
5744
  }
5611
5745
 
5612
- function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
5746
+ const OTP_LENGTH = 8;
5747
+ function LoginDialog({ title, type, alerts, fields, submitLabel, cancelLabel, onCancel, onSubmit, }) {
5613
5748
  const [params, setParams] = p({});
5614
5749
  const firstFieldRef = _(null);
5615
5750
  s(() => { var _a; return (_a = firstFieldRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []);
@@ -5617,21 +5752,34 @@ function LoginDialog({ title, alerts, fields, onCancel, onSubmit, }) {
5617
5752
  h(p$1, null,
5618
5753
  h("h3", { style: Styles.WindowHeader }, title),
5619
5754
  alerts.map((alert) => (h("p", { style: Styles.Alert[alert.type] }, resolveText(alert)))),
5620
- h("form", { onSubmit: ev => {
5755
+ h("form", { onSubmit: (ev) => {
5621
5756
  ev.preventDefault();
5622
5757
  onSubmit(params);
5623
- } }, 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 },
5624
5759
  label ? `${label}: ` : '',
5625
- 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
+ } })))))),
5626
5770
  h("div", { style: Styles.ButtonsDiv },
5627
- h("button", { type: "submit", style: Styles.Button, onClick: () => onSubmit(params) }, "Submit"),
5628
- 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))))));
5629
5774
  }
5630
5775
  function valueTransformer(type, value) {
5631
5776
  switch (type) {
5632
- case "email": return value.toLowerCase();
5633
- case "otp": return value.toUpperCase();
5634
- default: return value;
5777
+ case 'email':
5778
+ return value.toLowerCase();
5779
+ case 'otp':
5780
+ return value.toUpperCase();
5781
+ default:
5782
+ return value;
5635
5783
  }
5636
5784
  }
5637
5785
 
@@ -5685,11 +5833,20 @@ function setupDefaultGUI(db) {
5685
5833
  }
5686
5834
  };
5687
5835
  }
5688
- // TODO:
5689
- /*
5690
- * Gjort klart allt kring user interaction förutom att mounta default-ui på ett element.
5691
- * Också att kolla först om nån annan subscribar och i så fall inte göra nåt.
5692
- */
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));
5693
5850
 
5694
5851
  function computeSyncState(db) {
5695
5852
  let _prevStatus = db.cloud.webSocketStatus.value;
@@ -5717,8 +5874,17 @@ function computeSyncState(db) {
5717
5874
  return combineLatest([
5718
5875
  lazyWebSocketStatus,
5719
5876
  db.syncStateChangedEvent.pipe(startWith({ phase: 'initial' })),
5877
+ getCurrentUserEmitter(db.dx._novip),
5720
5878
  userIsReallyActive
5721
- ]).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
+ }
5722
5888
  let { phase, error, progress } = syncState;
5723
5889
  let adjustedStatus = status;
5724
5890
  if (phase === 'error') {
@@ -5751,23 +5917,12 @@ function computeSyncState(db) {
5751
5917
  error,
5752
5918
  progress,
5753
5919
  status: isOnline ? adjustedStatus : 'offline',
5920
+ license: 'ok'
5754
5921
  };
5755
5922
  return retState;
5756
5923
  }));
5757
5924
  }
5758
5925
 
5759
- function associate(factory) {
5760
- const wm = new WeakMap();
5761
- return (x) => {
5762
- let rv = wm.get(x);
5763
- if (!rv) {
5764
- rv = factory(x);
5765
- wm.set(x, rv);
5766
- }
5767
- return rv;
5768
- };
5769
- }
5770
-
5771
5926
  function createSharedValueObservable(o, defaultValue) {
5772
5927
  let currentValue = defaultValue;
5773
5928
  let shared = from$1(o).pipe(map$1((x) => (currentValue = x)), share({ resetOnRefCountZero: () => timer$1(1000) }));
@@ -5809,8 +5964,6 @@ const getGlobalRolesObservable = associate((db) => {
5809
5964
  })), {});
5810
5965
  });
5811
5966
 
5812
- const getCurrentUserEmitter = associate((db) => new BehaviorSubject(UNAUTHORIZED_USER));
5813
-
5814
5967
  const getInternalAccessControlObservable = associate((db) => {
5815
5968
  return createSharedValueObservable(getCurrentUserEmitter(db._novip).pipe(switchMap((currentUser) => liveQuery(() => db.transaction('r', 'realms', 'members', () => Promise.all([
5816
5969
  db.members.where({ userId: currentUser.userId }).toArray(),
@@ -6100,7 +6253,7 @@ function dexieCloud(dexie) {
6100
6253
  });
6101
6254
  const syncComplete = new Subject();
6102
6255
  dexie.cloud = {
6103
- version: '4.0.1-beta.44',
6256
+ version: '4.0.1-beta.47',
6104
6257
  options: Object.assign({}, DEFAULT_OPTIONS),
6105
6258
  schema: null,
6106
6259
  get currentUserId() {
@@ -6136,11 +6289,24 @@ function dexieCloud(dexie) {
6136
6289
  }
6137
6290
  updateSchemaFromOptions(dexie.cloud.schema, dexie.cloud.options);
6138
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
+ },
6139
6299
  sync({ wait, purpose } = { wait: true, purpose: 'push' }) {
6300
+ var _a;
6140
6301
  return __awaiter(this, void 0, void 0, function* () {
6141
6302
  if (wait === undefined)
6142
6303
  wait = true;
6143
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
+ }
6144
6310
  if (purpose === 'pull') {
6145
6311
  const syncState = db.cloud.persistedSyncState.value;
6146
6312
  triggerSync(db, purpose);
@@ -6344,7 +6510,9 @@ function dexieCloud(dexie) {
6344
6510
  db.syncStateChangedEvent.next({
6345
6511
  phase: 'not-in-sync',
6346
6512
  });
6347
- triggerSync(db, 'push');
6513
+ if (!isEagerSyncDisabled(db)) {
6514
+ triggerSync(db, 'push');
6515
+ }
6348
6516
  }), fromEvent(self, 'offline').subscribe(() => {
6349
6517
  console.debug('offline!');
6350
6518
  db.syncStateChangedEvent.next({
@@ -6361,7 +6529,7 @@ function dexieCloud(dexie) {
6361
6529
  });
6362
6530
  }
6363
6531
  }
6364
- dexieCloud.version = '4.0.1-beta.44';
6532
+ dexieCloud.version = '4.0.1-beta.47';
6365
6533
  Dexie.Cloud = dexieCloud;
6366
6534
 
6367
6535
  export { dexieCloud as default, dexieCloud, getTiedObjectId, getTiedRealmId };