dexie-cloud-addon 4.0.0-beta.14 → 4.0.0-beta.17

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.
@@ -4667,110 +4667,13 @@
4667
4667
  };
4668
4668
  }
4669
4669
 
4670
- const SECONDS = 1000;
4671
- const MINUTES = 60 * SECONDS;
4672
-
4673
- const myId = randomString(16);
4674
-
4675
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
4676
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
4677
- async function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
4678
- // Start working.
4679
- //
4680
- // Check if someone else is working on this already.
4681
- //
4682
- const jobsTable = db.table(jobsTableName);
4683
- async function aquireLock() {
4684
- const gotTheLock = await db.transaction('rw!', jobsTableName, async () => {
4685
- const currentWork = await jobsTable.get(jobName);
4686
- if (!currentWork) {
4687
- // No one else is working. Let's record that we are.
4688
- await jobsTable.add({
4689
- nodeId: myId,
4690
- started: new Date(),
4691
- heartbeat: new Date()
4692
- }, jobName);
4693
- return true;
4694
- }
4695
- else if (currentWork.heartbeat.getTime() <
4696
- Date.now() - GUARDED_JOB_TIMEOUT) {
4697
- 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!`);
4698
- // Now, take over!
4699
- await jobsTable.put({
4700
- nodeId: myId,
4701
- started: new Date(),
4702
- heartbeat: new Date()
4703
- }, jobName);
4704
- return true;
4705
- }
4706
- return false;
4707
- });
4708
- if (gotTheLock)
4709
- return true;
4710
- // Someone else took the job.
4711
- if (awaitRemoteJob) {
4712
- try {
4713
- const jobDoneObservable = rxjs.from(Dexie.liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
4714
- await jobDoneObservable.toPromise();
4715
- return false;
4716
- }
4717
- catch (err) {
4718
- if (err.name !== 'TimeoutError') {
4719
- throw err;
4720
- }
4721
- // Timeout stopped us! Try aquire the lock now.
4722
- // It will likely succeed this time unless
4723
- // another client took it.
4724
- return await aquireLock();
4725
- }
4726
- }
4727
- return false;
4728
- }
4729
- if (await aquireLock()) {
4730
- // We own the lock entry and can do our job undisturbed.
4731
- // We're not within a transaction, but these type of locks
4732
- // spans over transactions.
4733
- // Start our heart beat during the job.
4734
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
4735
- const heartbeat = setInterval(() => {
4736
- jobsTable.update(jobName, (job) => {
4737
- if (job.nodeId === myId) {
4738
- job.heartbeat = new Date();
4739
- }
4740
- });
4741
- }, GUARDED_JOB_HEARTBEAT);
4742
- try {
4743
- return await job();
4744
- }
4745
- finally {
4746
- // Stop heartbeat
4747
- clearInterval(heartbeat);
4748
- // Remove the persisted job state:
4749
- await db.transaction('rw!', jobsTableName, async () => {
4750
- const currentWork = await jobsTable.get(jobName);
4751
- if (currentWork && currentWork.nodeId === myId) {
4752
- jobsTable.delete(jobName);
4753
- }
4754
- });
4755
- }
4756
- }
4757
- }
4758
-
4759
4670
  async function performInitialSync(db, cloudOptions, cloudSchema) {
4760
- console.debug("Performing initial sync");
4761
- await performGuardedJob(db, 'initialSync', '$jobs', async () => {
4762
- // Even though caller has already checked it,
4763
- // Do check again (now within a transaction) that we really do not have a sync state:
4764
- const syncState = await db.getPersistedSyncState();
4765
- if (!syncState?.initiallySynced) {
4766
- await sync(db, cloudOptions, cloudSchema, { isInitialSync: true });
4767
- }
4768
- }, { awaitRemoteJob: true } // Don't return until the job is done!
4769
- );
4770
- console.debug("Done initial sync");
4671
+ console.debug('Performing initial sync');
4672
+ await sync(db, cloudOptions, cloudSchema, { isInitialSync: true });
4673
+ console.debug('Done initial sync');
4771
4674
  }
4772
4675
 
4773
- const USER_INACTIVITY_TIMEOUT = 300000; // 300_000;
4676
+ const USER_INACTIVITY_TIMEOUT = 180000; // 3 minutes
4774
4677
  const INACTIVE_WAIT_TIME = 20000;
4775
4678
  // This observable will be emitted to later down....
4776
4679
  const userIsActive = new rxjs.BehaviorSubject(true);
@@ -4784,9 +4687,13 @@
4784
4687
  // for just a short time.
4785
4688
  const userIsReallyActive = new rxjs.BehaviorSubject(true);
4786
4689
  userIsActive
4787
- .pipe(switchMap((isActive) => isActive
4788
- ? rxjs.of(true)
4789
- : rxjs.of(false).pipe(delay(INACTIVE_WAIT_TIME))), distinctUntilChanged())
4690
+ .pipe(switchMap((isActive) => {
4691
+ //console.debug('SyncStatus: DUBB: isActive changed to', isActive);
4692
+ return isActive
4693
+ ? rxjs.of(true)
4694
+ : rxjs.of(false).pipe(delay(INACTIVE_WAIT_TIME))
4695
+ ;
4696
+ }), distinctUntilChanged())
4790
4697
  .subscribe(userIsReallyActive);
4791
4698
  //
4792
4699
  // First create some corner-stone observables to build the flow on
@@ -4801,7 +4708,7 @@
4801
4708
  const documentBecomesVisible = visibilityStateIsChanged.pipe(filter(() => document.visibilityState === 'visible'));
4802
4709
  // Any of various user-activity-related events happen:
4803
4710
  const userDoesSomething = typeof window !== 'undefined'
4804
- ? rxjs.merge(documentBecomesVisible, rxjs.fromEvent(window, 'mousemove'), rxjs.fromEvent(window, 'keydown'), rxjs.fromEvent(window, 'wheel'), rxjs.fromEvent(window, 'touchmove'))
4711
+ ? rxjs.merge(documentBecomesVisible, rxjs.fromEvent(window, 'mousedown'), rxjs.fromEvent(window, 'mousemove'), rxjs.fromEvent(window, 'keydown'), rxjs.fromEvent(window, 'wheel'), rxjs.fromEvent(window, 'touchmove'))
4805
4712
  : rxjs.of({});
4806
4713
  if (typeof document !== 'undefined') {
4807
4714
  //
@@ -4852,6 +4759,7 @@
4852
4759
  constructor(databaseUrl, rev, realmSetHash, clientIdentity, token, tokenExpiration, subscriber, messageProducer, webSocketStatus) {
4853
4760
  super(() => this.teardown());
4854
4761
  this.id = ++counter;
4762
+ this.reconnecting = false;
4855
4763
  console.debug('New WebSocket Connection', this.id, token ? 'authorized' : 'unauthorized');
4856
4764
  this.databaseUrl = databaseUrl;
4857
4765
  this.rev = rev;
@@ -4871,7 +4779,7 @@
4871
4779
  this.disconnect();
4872
4780
  }
4873
4781
  disconnect() {
4874
- this.webSocketStatus.next("disconnected");
4782
+ this.webSocketStatus.next('disconnected');
4875
4783
  if (this.pinger) {
4876
4784
  clearInterval(this.pinger);
4877
4785
  this.pinger = null;
@@ -4889,11 +4797,18 @@
4889
4797
  }
4890
4798
  }
4891
4799
  reconnect() {
4892
- this.disconnect();
4893
- this.connect();
4800
+ if (this.reconnecting)
4801
+ return;
4802
+ this.reconnecting = true;
4803
+ try {
4804
+ this.disconnect();
4805
+ }
4806
+ catch { }
4807
+ this.connect()
4808
+ .catch(() => { })
4809
+ .then(() => (this.reconnecting = false)); // finally()
4894
4810
  }
4895
4811
  async connect() {
4896
- this.webSocketStatus.next("connecting");
4897
4812
  this.lastServerActivity = new Date();
4898
4813
  if (this.pauseUntil && this.pauseUntil > new Date()) {
4899
4814
  console.debug('WS not reconnecting just yet', {
@@ -4908,12 +4823,14 @@
4908
4823
  if (!this.databaseUrl)
4909
4824
  throw new Error(`Cannot connect without a database URL`);
4910
4825
  if (this.closed) {
4826
+ //console.debug('SyncStatus: DUBB: Ooops it was closed!');
4911
4827
  return;
4912
4828
  }
4913
4829
  if (this.tokenExpiration && this.tokenExpiration < new Date()) {
4914
4830
  this.subscriber.error(new TokenExpiredError()); // Will be handled in connectWebSocket.ts.
4915
4831
  return;
4916
4832
  }
4833
+ this.webSocketStatus.next('connecting');
4917
4834
  this.pinger = setInterval(async () => {
4918
4835
  if (this.closed) {
4919
4836
  console.debug('pinger check', this.id, 'CLOSED.');
@@ -4960,7 +4877,7 @@
4960
4877
  const searchParams = new URLSearchParams();
4961
4878
  if (this.subscriber.closed)
4962
4879
  return;
4963
- searchParams.set('v', "2");
4880
+ searchParams.set('v', '2');
4964
4881
  searchParams.set('rev', this.rev);
4965
4882
  searchParams.set('realmsHash', this.realmSetHash);
4966
4883
  searchParams.set('clientId', this.clientIdentity);
@@ -4999,23 +4916,30 @@
4999
4916
  }
5000
4917
  };
5001
4918
  try {
4919
+ let everConnected = false;
5002
4920
  await new Promise((resolve, reject) => {
5003
4921
  ws.onopen = (event) => {
5004
4922
  console.debug('dexie-cloud WebSocket onopen');
4923
+ everConnected = true;
5005
4924
  resolve(null);
5006
4925
  };
5007
4926
  ws.onerror = (event) => {
5008
- const error = event.error || new Error('WebSocket Error');
5009
- this.disconnect();
5010
- this.subscriber.error(error);
5011
- this.webSocketStatus.next("error");
5012
- reject(error);
4927
+ if (!everConnected) {
4928
+ const error = event.error || new Error('WebSocket Error');
4929
+ this.subscriber.error(error);
4930
+ this.webSocketStatus.next('error');
4931
+ reject(error);
4932
+ }
4933
+ else {
4934
+ this.reconnect();
4935
+ }
5013
4936
  };
5014
4937
  });
5015
- this.messageProducerSubscription = this.messageProducer.subscribe(msg => {
4938
+ this.messageProducerSubscription = this.messageProducer.subscribe((msg) => {
5016
4939
  if (!this.closed) {
5017
- if (msg.type === 'ready' && this.webSocketStatus.value !== 'connected') {
5018
- this.webSocketStatus.next("connected");
4940
+ if (msg.type === 'ready' &&
4941
+ this.webSocketStatus.value !== 'connected') {
4942
+ this.webSocketStatus.next('connected');
5019
4943
  }
5020
4944
  this.ws?.send(TSON.stringify(msg));
5021
4945
  }
@@ -5052,9 +4976,9 @@
5052
4976
  rev: syncState.serverRevision,
5053
4977
  })));
5054
4978
  function createObservable() {
5055
- return db.cloud.persistedSyncState.pipe(filter(syncState => syncState?.serverRevision), // Don't connect before there's no initial sync performed.
4979
+ return db.cloud.persistedSyncState.pipe(filter((syncState) => syncState?.serverRevision), // Don't connect before there's no initial sync performed.
5056
4980
  take(1), // Don't continue waking up whenever syncState change
5057
- switchMap((syncState) => db.cloud.currentUser.pipe(map(userLogin => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]))), switchMap(async ([userLogin, syncState]) => [userLogin, await computeRealmSetHash(syncState)]), switchMap(([userLogin, realmSetHash]) =>
4981
+ switchMap((syncState) => db.cloud.currentUser.pipe(map((userLogin) => [userLogin, syncState]))), switchMap(([userLogin, syncState]) => userIsReallyActive.pipe(map((isActive) => [isActive ? userLogin : null, syncState]))), switchMap(async ([userLogin, syncState]) => [userLogin, await computeRealmSetHash(syncState)]), switchMap(([userLogin, realmSetHash]) =>
5058
4982
  // Let server end query changes from last entry of same client-ID and forward.
5059
4983
  // If no new entries, server won't bother the client. If new entries, server sends only those
5060
4984
  // and the baseRev of the last from same client-ID.
@@ -5077,7 +5001,10 @@
5077
5001
  else {
5078
5002
  return rxjs.throwError(error);
5079
5003
  }
5080
- }), catchError((error) => rxjs.from(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()))));
5004
+ }), catchError((error) => {
5005
+ db.cloud.webSocketStatus.next("error");
5006
+ return rxjs.from(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
5007
+ }));
5081
5008
  }
5082
5009
  return createObservable().subscribe((msg) => {
5083
5010
  if (msg) {
@@ -5097,6 +5024,95 @@
5097
5024
  : false;
5098
5025
  }
5099
5026
 
5027
+ const SECONDS = 1000;
5028
+ const MINUTES = 60 * SECONDS;
5029
+
5030
+ const myId = randomString(16);
5031
+
5032
+ const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
5033
+ const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
5034
+ async function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
5035
+ // Start working.
5036
+ //
5037
+ // Check if someone else is working on this already.
5038
+ //
5039
+ const jobsTable = db.table(jobsTableName);
5040
+ async function aquireLock() {
5041
+ const gotTheLock = await db.transaction('rw!', jobsTableName, async () => {
5042
+ const currentWork = await jobsTable.get(jobName);
5043
+ if (!currentWork) {
5044
+ // No one else is working. Let's record that we are.
5045
+ await jobsTable.add({
5046
+ nodeId: myId,
5047
+ started: new Date(),
5048
+ heartbeat: new Date()
5049
+ }, jobName);
5050
+ return true;
5051
+ }
5052
+ else if (currentWork.heartbeat.getTime() <
5053
+ Date.now() - GUARDED_JOB_TIMEOUT) {
5054
+ 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!`);
5055
+ // Now, take over!
5056
+ await jobsTable.put({
5057
+ nodeId: myId,
5058
+ started: new Date(),
5059
+ heartbeat: new Date()
5060
+ }, jobName);
5061
+ return true;
5062
+ }
5063
+ return false;
5064
+ });
5065
+ if (gotTheLock)
5066
+ return true;
5067
+ // Someone else took the job.
5068
+ if (awaitRemoteJob) {
5069
+ try {
5070
+ const jobDoneObservable = rxjs.from(Dexie.liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
5071
+ await jobDoneObservable.toPromise();
5072
+ return false;
5073
+ }
5074
+ catch (err) {
5075
+ if (err.name !== 'TimeoutError') {
5076
+ throw err;
5077
+ }
5078
+ // Timeout stopped us! Try aquire the lock now.
5079
+ // It will likely succeed this time unless
5080
+ // another client took it.
5081
+ return await aquireLock();
5082
+ }
5083
+ }
5084
+ return false;
5085
+ }
5086
+ if (await aquireLock()) {
5087
+ // We own the lock entry and can do our job undisturbed.
5088
+ // We're not within a transaction, but these type of locks
5089
+ // spans over transactions.
5090
+ // Start our heart beat during the job.
5091
+ // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
5092
+ const heartbeat = setInterval(() => {
5093
+ jobsTable.update(jobName, (job) => {
5094
+ if (job.nodeId === myId) {
5095
+ job.heartbeat = new Date();
5096
+ }
5097
+ });
5098
+ }, GUARDED_JOB_HEARTBEAT);
5099
+ try {
5100
+ return await job();
5101
+ }
5102
+ finally {
5103
+ // Stop heartbeat
5104
+ clearInterval(heartbeat);
5105
+ // Remove the persisted job state:
5106
+ await db.transaction('rw!', jobsTableName, async () => {
5107
+ const currentWork = await jobsTable.get(jobName);
5108
+ if (currentWork && currentWork.nodeId === myId) {
5109
+ await jobsTable.delete(jobName);
5110
+ }
5111
+ });
5112
+ }
5113
+ }
5114
+ }
5115
+
5100
5116
  const ongoingSyncs = new WeakMap();
5101
5117
  function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5102
5118
  const ongoing = ongoingSyncs.get(db);
@@ -5501,6 +5517,21 @@
5501
5517
  return rv;
5502
5518
  }
5503
5519
 
5520
+ const getGlobalRolesObservable = associate((db) => {
5521
+ return createSharedValueObservable(Dexie.liveQuery(() => db.roles
5522
+ .where({ realmId: 'rlm-public' })
5523
+ .toArray()
5524
+ .then((roles) => {
5525
+ const rv = {};
5526
+ for (const role of roles
5527
+ .slice()
5528
+ .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))) {
5529
+ rv[role.name] = role;
5530
+ }
5531
+ return rv;
5532
+ })), {});
5533
+ });
5534
+
5504
5535
  const getCurrentUserEmitter = associate((db) => new rxjs.BehaviorSubject(UNAUTHORIZED_USER));
5505
5536
 
5506
5537
  const getInternalAccessControlObservable = associate((db) => {
@@ -5602,18 +5633,38 @@
5602
5633
  }
5603
5634
 
5604
5635
  const getPermissionsLookupObservable = associate((db) => {
5605
- const o = getInternalAccessControlObservable(db._novip);
5606
- return mapValueObservable(o, ({ selfMembers, realms, userId }) => {
5636
+ const o = createSharedValueObservable(rxjs.combineLatest([
5637
+ getInternalAccessControlObservable(db._novip),
5638
+ getGlobalRolesObservable(db._novip),
5639
+ ]).pipe(map(([{ selfMembers, realms, userId }, globalRoles]) => ({
5640
+ selfMembers,
5641
+ realms,
5642
+ userId,
5643
+ globalRoles,
5644
+ }))), {
5645
+ selfMembers: [],
5646
+ realms: [],
5647
+ userId: UNAUTHORIZED_USER.userId,
5648
+ globalRoles: {},
5649
+ });
5650
+ return mapValueObservable(o, ({ selfMembers, realms, userId, globalRoles }) => {
5607
5651
  const rv = realms
5608
- .map((realm) => ({
5609
- ...realm,
5610
- permissions: realm.owner === userId
5611
- ? { manage: '*' }
5612
- : mergePermissions(...selfMembers
5613
- .filter((m) => m.realmId === realm.realmId)
5614
- .map((m) => m.permissions)
5615
- .filter((p) => p)),
5616
- }))
5652
+ .map((realm) => {
5653
+ const selfRealmMembers = selfMembers.filter((m) => m.realmId === realm.realmId);
5654
+ const directPermissionSets = selfRealmMembers
5655
+ .map((m) => m.permissions)
5656
+ .filter((p) => p);
5657
+ const rolePermissionSets = flatten(selfRealmMembers.map((m) => m.roles).filter((roleName) => roleName))
5658
+ .map((role) => globalRoles[role])
5659
+ .filter((role) => role)
5660
+ .map((role) => role.permissions);
5661
+ return {
5662
+ ...realm,
5663
+ permissions: realm.owner === userId
5664
+ ? { manage: '*' }
5665
+ : mergePermissions(...directPermissionSets, ...rolePermissionSets),
5666
+ };
5667
+ })
5617
5668
  .reduce((p, c) => ({ ...p, [c.realmId]: c }), {
5618
5669
  [userId]: {
5619
5670
  realmId: userId,
@@ -5697,7 +5748,7 @@
5697
5748
  const realm = permissionsLookup[realmId || dexie.cloud.currentUserId];
5698
5749
  if (!realm)
5699
5750
  return new PermissionChecker({}, tableName, !owner || owner === dexie.cloud.currentUserId);
5700
- return new PermissionChecker(realm.permissions, tableName, !owner || owner === dexie.cloud.currentUserId);
5751
+ return new PermissionChecker(realm.permissions, tableName, realmId === dexie.cloud.currentUserId || owner === dexie.cloud.currentUserId);
5701
5752
  };
5702
5753
  const o = source.pipe(map(mapper));
5703
5754
  o.getValue = () => mapper(source.getValue());
@@ -5733,6 +5784,7 @@
5733
5784
  //
5734
5785
  const currentUserEmitter = getCurrentUserEmitter(dexie);
5735
5786
  const subscriptions = [];
5787
+ let configuredProgramatically = false;
5736
5788
  // local sync worker - used when there's no service worker.
5737
5789
  let localSyncWorker = null;
5738
5790
  dexie.on('ready', async (dexie) => {
@@ -5780,8 +5832,10 @@
5780
5832
  await login(db, hint);
5781
5833
  },
5782
5834
  invites: getInvitesObservable(dexie),
5835
+ roles: getGlobalRolesObservable(dexie),
5783
5836
  configure(options) {
5784
5837
  options = dexie.cloud.options = { ...dexie.cloud.options, ...options };
5838
+ configuredProgramatically = true;
5785
5839
  if (options.databaseUrl && options.nameSuffix) {
5786
5840
  // @ts-ignore
5787
5841
  dexie.name = `${origIdbName}-${getDbNameFromDbUrl(options.databaseUrl)}`;
@@ -5868,7 +5922,7 @@
5868
5922
  db.getSchema(),
5869
5923
  db.getPersistedSyncState(),
5870
5924
  ]);
5871
- if (!options) {
5925
+ if (!configuredProgramatically) {
5872
5926
  // Options not specified programatically (use case for SW!)
5873
5927
  // Take persisted options:
5874
5928
  db.cloud.options = persistedOptions || null;
@@ -5876,6 +5930,8 @@
5876
5930
  else if (!persistedOptions ||
5877
5931
  JSON.stringify(persistedOptions) !== JSON.stringify(options)) {
5878
5932
  // Update persisted options:
5933
+ if (!options)
5934
+ throw new Error(`Internal error`); // options cannot be null if configuredProgramatically is set.
5879
5935
  await db.$syncState.put(options, 'options');
5880
5936
  }
5881
5937
  if (db.cloud.options?.tryUseServiceWorker &&