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.
@@ -4649,110 +4649,13 @@
4649
4649
  };
4650
4650
  }
4651
4651
 
4652
- const SECONDS = 1000;
4653
- const MINUTES = 60 * SECONDS;
4654
-
4655
- const myId = randomString$1(16);
4656
-
4657
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
4658
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
4659
- async function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
4660
- // Start working.
4661
- //
4662
- // Check if someone else is working on this already.
4663
- //
4664
- const jobsTable = db.table(jobsTableName);
4665
- async function aquireLock() {
4666
- const gotTheLock = await db.transaction('rw!', jobsTableName, async () => {
4667
- const currentWork = await jobsTable.get(jobName);
4668
- if (!currentWork) {
4669
- // No one else is working. Let's record that we are.
4670
- await jobsTable.add({
4671
- nodeId: myId,
4672
- started: new Date(),
4673
- heartbeat: new Date()
4674
- }, jobName);
4675
- return true;
4676
- }
4677
- else if (currentWork.heartbeat.getTime() <
4678
- Date.now() - GUARDED_JOB_TIMEOUT) {
4679
- 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!`);
4680
- // Now, take over!
4681
- await jobsTable.put({
4682
- nodeId: myId,
4683
- started: new Date(),
4684
- heartbeat: new Date()
4685
- }, jobName);
4686
- return true;
4687
- }
4688
- return false;
4689
- });
4690
- if (gotTheLock)
4691
- return true;
4692
- // Someone else took the job.
4693
- if (awaitRemoteJob) {
4694
- try {
4695
- const jobDoneObservable = rxjs.from(Dexie.liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
4696
- await jobDoneObservable.toPromise();
4697
- return false;
4698
- }
4699
- catch (err) {
4700
- if (err.name !== 'TimeoutError') {
4701
- throw err;
4702
- }
4703
- // Timeout stopped us! Try aquire the lock now.
4704
- // It will likely succeed this time unless
4705
- // another client took it.
4706
- return await aquireLock();
4707
- }
4708
- }
4709
- return false;
4710
- }
4711
- if (await aquireLock()) {
4712
- // We own the lock entry and can do our job undisturbed.
4713
- // We're not within a transaction, but these type of locks
4714
- // spans over transactions.
4715
- // Start our heart beat during the job.
4716
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
4717
- const heartbeat = setInterval(() => {
4718
- jobsTable.update(jobName, (job) => {
4719
- if (job.nodeId === myId) {
4720
- job.heartbeat = new Date();
4721
- }
4722
- });
4723
- }, GUARDED_JOB_HEARTBEAT);
4724
- try {
4725
- return await job();
4726
- }
4727
- finally {
4728
- // Stop heartbeat
4729
- clearInterval(heartbeat);
4730
- // Remove the persisted job state:
4731
- await db.transaction('rw!', jobsTableName, async () => {
4732
- const currentWork = await jobsTable.get(jobName);
4733
- if (currentWork && currentWork.nodeId === myId) {
4734
- jobsTable.delete(jobName);
4735
- }
4736
- });
4737
- }
4738
- }
4739
- }
4740
-
4741
4652
  async function performInitialSync(db, cloudOptions, cloudSchema) {
4742
- console.debug("Performing initial sync");
4743
- await performGuardedJob(db, 'initialSync', '$jobs', async () => {
4744
- // Even though caller has already checked it,
4745
- // Do check again (now within a transaction) that we really do not have a sync state:
4746
- const syncState = await db.getPersistedSyncState();
4747
- if (!syncState?.initiallySynced) {
4748
- await sync(db, cloudOptions, cloudSchema, { isInitialSync: true });
4749
- }
4750
- }, { awaitRemoteJob: true } // Don't return until the job is done!
4751
- );
4752
- console.debug("Done initial sync");
4653
+ console.debug('Performing initial sync');
4654
+ await sync(db, cloudOptions, cloudSchema, { isInitialSync: true });
4655
+ console.debug('Done initial sync');
4753
4656
  }
4754
4657
 
4755
- const USER_INACTIVITY_TIMEOUT = 300000; // 300_000;
4658
+ const USER_INACTIVITY_TIMEOUT = 180000; // 3 minutes
4756
4659
  const INACTIVE_WAIT_TIME = 20000;
4757
4660
  // This observable will be emitted to later down....
4758
4661
  const userIsActive = new rxjs.BehaviorSubject(true);
@@ -4766,9 +4669,13 @@
4766
4669
  // for just a short time.
4767
4670
  const userIsReallyActive = new rxjs.BehaviorSubject(true);
4768
4671
  userIsActive
4769
- .pipe(switchMap((isActive) => isActive
4770
- ? rxjs.of(true)
4771
- : rxjs.of(false).pipe(delay(INACTIVE_WAIT_TIME))), distinctUntilChanged())
4672
+ .pipe(switchMap((isActive) => {
4673
+ //console.debug('SyncStatus: DUBB: isActive changed to', isActive);
4674
+ return isActive
4675
+ ? rxjs.of(true)
4676
+ : rxjs.of(false).pipe(delay(INACTIVE_WAIT_TIME))
4677
+ ;
4678
+ }), distinctUntilChanged())
4772
4679
  .subscribe(userIsReallyActive);
4773
4680
  //
4774
4681
  // First create some corner-stone observables to build the flow on
@@ -4783,7 +4690,7 @@
4783
4690
  const documentBecomesVisible = visibilityStateIsChanged.pipe(filter(() => document.visibilityState === 'visible'));
4784
4691
  // Any of various user-activity-related events happen:
4785
4692
  const userDoesSomething = typeof window !== 'undefined'
4786
- ? rxjs.merge(documentBecomesVisible, rxjs.fromEvent(window, 'mousemove'), rxjs.fromEvent(window, 'keydown'), rxjs.fromEvent(window, 'wheel'), rxjs.fromEvent(window, 'touchmove'))
4693
+ ? rxjs.merge(documentBecomesVisible, rxjs.fromEvent(window, 'mousedown'), rxjs.fromEvent(window, 'mousemove'), rxjs.fromEvent(window, 'keydown'), rxjs.fromEvent(window, 'wheel'), rxjs.fromEvent(window, 'touchmove'))
4787
4694
  : rxjs.of({});
4788
4695
  if (typeof document !== 'undefined') {
4789
4696
  //
@@ -4834,6 +4741,7 @@
4834
4741
  constructor(databaseUrl, rev, realmSetHash, clientIdentity, token, tokenExpiration, subscriber, messageProducer, webSocketStatus) {
4835
4742
  super(() => this.teardown());
4836
4743
  this.id = ++counter;
4744
+ this.reconnecting = false;
4837
4745
  console.debug('New WebSocket Connection', this.id, token ? 'authorized' : 'unauthorized');
4838
4746
  this.databaseUrl = databaseUrl;
4839
4747
  this.rev = rev;
@@ -4853,7 +4761,7 @@
4853
4761
  this.disconnect();
4854
4762
  }
4855
4763
  disconnect() {
4856
- this.webSocketStatus.next("disconnected");
4764
+ this.webSocketStatus.next('disconnected');
4857
4765
  if (this.pinger) {
4858
4766
  clearInterval(this.pinger);
4859
4767
  this.pinger = null;
@@ -4871,11 +4779,18 @@
4871
4779
  }
4872
4780
  }
4873
4781
  reconnect() {
4874
- this.disconnect();
4875
- this.connect();
4782
+ if (this.reconnecting)
4783
+ return;
4784
+ this.reconnecting = true;
4785
+ try {
4786
+ this.disconnect();
4787
+ }
4788
+ catch { }
4789
+ this.connect()
4790
+ .catch(() => { })
4791
+ .then(() => (this.reconnecting = false)); // finally()
4876
4792
  }
4877
4793
  async connect() {
4878
- this.webSocketStatus.next("connecting");
4879
4794
  this.lastServerActivity = new Date();
4880
4795
  if (this.pauseUntil && this.pauseUntil > new Date()) {
4881
4796
  console.debug('WS not reconnecting just yet', {
@@ -4890,12 +4805,14 @@
4890
4805
  if (!this.databaseUrl)
4891
4806
  throw new Error(`Cannot connect without a database URL`);
4892
4807
  if (this.closed) {
4808
+ //console.debug('SyncStatus: DUBB: Ooops it was closed!');
4893
4809
  return;
4894
4810
  }
4895
4811
  if (this.tokenExpiration && this.tokenExpiration < new Date()) {
4896
4812
  this.subscriber.error(new TokenExpiredError()); // Will be handled in connectWebSocket.ts.
4897
4813
  return;
4898
4814
  }
4815
+ this.webSocketStatus.next('connecting');
4899
4816
  this.pinger = setInterval(async () => {
4900
4817
  if (this.closed) {
4901
4818
  console.debug('pinger check', this.id, 'CLOSED.');
@@ -4942,7 +4859,7 @@
4942
4859
  const searchParams = new URLSearchParams();
4943
4860
  if (this.subscriber.closed)
4944
4861
  return;
4945
- searchParams.set('v', "2");
4862
+ searchParams.set('v', '2');
4946
4863
  searchParams.set('rev', this.rev);
4947
4864
  searchParams.set('realmsHash', this.realmSetHash);
4948
4865
  searchParams.set('clientId', this.clientIdentity);
@@ -4981,23 +4898,30 @@
4981
4898
  }
4982
4899
  };
4983
4900
  try {
4901
+ let everConnected = false;
4984
4902
  await new Promise((resolve, reject) => {
4985
4903
  ws.onopen = (event) => {
4986
4904
  console.debug('dexie-cloud WebSocket onopen');
4905
+ everConnected = true;
4987
4906
  resolve(null);
4988
4907
  };
4989
4908
  ws.onerror = (event) => {
4990
- const error = event.error || new Error('WebSocket Error');
4991
- this.disconnect();
4992
- this.subscriber.error(error);
4993
- this.webSocketStatus.next("error");
4994
- reject(error);
4909
+ if (!everConnected) {
4910
+ const error = event.error || new Error('WebSocket Error');
4911
+ this.subscriber.error(error);
4912
+ this.webSocketStatus.next('error');
4913
+ reject(error);
4914
+ }
4915
+ else {
4916
+ this.reconnect();
4917
+ }
4995
4918
  };
4996
4919
  });
4997
- this.messageProducerSubscription = this.messageProducer.subscribe(msg => {
4920
+ this.messageProducerSubscription = this.messageProducer.subscribe((msg) => {
4998
4921
  if (!this.closed) {
4999
- if (msg.type === 'ready' && this.webSocketStatus.value !== 'connected') {
5000
- this.webSocketStatus.next("connected");
4922
+ if (msg.type === 'ready' &&
4923
+ this.webSocketStatus.value !== 'connected') {
4924
+ this.webSocketStatus.next('connected');
5001
4925
  }
5002
4926
  this.ws?.send(TSON.stringify(msg));
5003
4927
  }
@@ -5034,9 +4958,9 @@
5034
4958
  rev: syncState.serverRevision,
5035
4959
  })));
5036
4960
  function createObservable() {
5037
- return db.cloud.persistedSyncState.pipe(filter(syncState => syncState?.serverRevision), // Don't connect before there's no initial sync performed.
4961
+ return db.cloud.persistedSyncState.pipe(filter((syncState) => syncState?.serverRevision), // Don't connect before there's no initial sync performed.
5038
4962
  take(1), // Don't continue waking up whenever syncState change
5039
- 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]) =>
4963
+ 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]) =>
5040
4964
  // Let server end query changes from last entry of same client-ID and forward.
5041
4965
  // If no new entries, server won't bother the client. If new entries, server sends only those
5042
4966
  // and the baseRev of the last from same client-ID.
@@ -5059,7 +4983,10 @@
5059
4983
  else {
5060
4984
  return rxjs.throwError(error);
5061
4985
  }
5062
- }), catchError((error) => rxjs.from(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()))));
4986
+ }), catchError((error) => {
4987
+ db.cloud.webSocketStatus.next("error");
4988
+ return rxjs.from(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
4989
+ }));
5063
4990
  }
5064
4991
  return createObservable().subscribe((msg) => {
5065
4992
  if (msg) {
@@ -5079,6 +5006,95 @@
5079
5006
  : false;
5080
5007
  }
5081
5008
 
5009
+ const SECONDS = 1000;
5010
+ const MINUTES = 60 * SECONDS;
5011
+
5012
+ const myId = randomString$1(16);
5013
+
5014
+ const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
5015
+ const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
5016
+ async function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
5017
+ // Start working.
5018
+ //
5019
+ // Check if someone else is working on this already.
5020
+ //
5021
+ const jobsTable = db.table(jobsTableName);
5022
+ async function aquireLock() {
5023
+ const gotTheLock = await db.transaction('rw!', jobsTableName, async () => {
5024
+ const currentWork = await jobsTable.get(jobName);
5025
+ if (!currentWork) {
5026
+ // No one else is working. Let's record that we are.
5027
+ await jobsTable.add({
5028
+ nodeId: myId,
5029
+ started: new Date(),
5030
+ heartbeat: new Date()
5031
+ }, jobName);
5032
+ return true;
5033
+ }
5034
+ else if (currentWork.heartbeat.getTime() <
5035
+ Date.now() - GUARDED_JOB_TIMEOUT) {
5036
+ 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!`);
5037
+ // Now, take over!
5038
+ await jobsTable.put({
5039
+ nodeId: myId,
5040
+ started: new Date(),
5041
+ heartbeat: new Date()
5042
+ }, jobName);
5043
+ return true;
5044
+ }
5045
+ return false;
5046
+ });
5047
+ if (gotTheLock)
5048
+ return true;
5049
+ // Someone else took the job.
5050
+ if (awaitRemoteJob) {
5051
+ try {
5052
+ const jobDoneObservable = rxjs.from(Dexie.liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
5053
+ await jobDoneObservable.toPromise();
5054
+ return false;
5055
+ }
5056
+ catch (err) {
5057
+ if (err.name !== 'TimeoutError') {
5058
+ throw err;
5059
+ }
5060
+ // Timeout stopped us! Try aquire the lock now.
5061
+ // It will likely succeed this time unless
5062
+ // another client took it.
5063
+ return await aquireLock();
5064
+ }
5065
+ }
5066
+ return false;
5067
+ }
5068
+ if (await aquireLock()) {
5069
+ // We own the lock entry and can do our job undisturbed.
5070
+ // We're not within a transaction, but these type of locks
5071
+ // spans over transactions.
5072
+ // Start our heart beat during the job.
5073
+ // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
5074
+ const heartbeat = setInterval(() => {
5075
+ jobsTable.update(jobName, (job) => {
5076
+ if (job.nodeId === myId) {
5077
+ job.heartbeat = new Date();
5078
+ }
5079
+ });
5080
+ }, GUARDED_JOB_HEARTBEAT);
5081
+ try {
5082
+ return await job();
5083
+ }
5084
+ finally {
5085
+ // Stop heartbeat
5086
+ clearInterval(heartbeat);
5087
+ // Remove the persisted job state:
5088
+ await db.transaction('rw!', jobsTableName, async () => {
5089
+ const currentWork = await jobsTable.get(jobName);
5090
+ if (currentWork && currentWork.nodeId === myId) {
5091
+ await jobsTable.delete(jobName);
5092
+ }
5093
+ });
5094
+ }
5095
+ }
5096
+ }
5097
+
5082
5098
  const ongoingSyncs = new WeakMap();
5083
5099
  function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5084
5100
  const ongoing = ongoingSyncs.get(db);
@@ -5483,6 +5499,21 @@
5483
5499
  return rv;
5484
5500
  }
5485
5501
 
5502
+ const getGlobalRolesObservable = associate((db) => {
5503
+ return createSharedValueObservable(Dexie.liveQuery(() => db.roles
5504
+ .where({ realmId: 'rlm-public' })
5505
+ .toArray()
5506
+ .then((roles) => {
5507
+ const rv = {};
5508
+ for (const role of roles
5509
+ .slice()
5510
+ .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))) {
5511
+ rv[role.name] = role;
5512
+ }
5513
+ return rv;
5514
+ })), {});
5515
+ });
5516
+
5486
5517
  const getCurrentUserEmitter = associate((db) => new rxjs.BehaviorSubject(UNAUTHORIZED_USER));
5487
5518
 
5488
5519
  const getInternalAccessControlObservable = associate((db) => {
@@ -5584,18 +5615,38 @@
5584
5615
  }
5585
5616
 
5586
5617
  const getPermissionsLookupObservable = associate((db) => {
5587
- const o = getInternalAccessControlObservable(db._novip);
5588
- return mapValueObservable(o, ({ selfMembers, realms, userId }) => {
5618
+ const o = createSharedValueObservable(rxjs.combineLatest([
5619
+ getInternalAccessControlObservable(db._novip),
5620
+ getGlobalRolesObservable(db._novip),
5621
+ ]).pipe(map(([{ selfMembers, realms, userId }, globalRoles]) => ({
5622
+ selfMembers,
5623
+ realms,
5624
+ userId,
5625
+ globalRoles,
5626
+ }))), {
5627
+ selfMembers: [],
5628
+ realms: [],
5629
+ userId: UNAUTHORIZED_USER.userId,
5630
+ globalRoles: {},
5631
+ });
5632
+ return mapValueObservable(o, ({ selfMembers, realms, userId, globalRoles }) => {
5589
5633
  const rv = realms
5590
- .map((realm) => ({
5591
- ...realm,
5592
- permissions: realm.owner === userId
5593
- ? { manage: '*' }
5594
- : mergePermissions(...selfMembers
5595
- .filter((m) => m.realmId === realm.realmId)
5596
- .map((m) => m.permissions)
5597
- .filter((p) => p)),
5598
- }))
5634
+ .map((realm) => {
5635
+ const selfRealmMembers = selfMembers.filter((m) => m.realmId === realm.realmId);
5636
+ const directPermissionSets = selfRealmMembers
5637
+ .map((m) => m.permissions)
5638
+ .filter((p) => p);
5639
+ const rolePermissionSets = flatten(selfRealmMembers.map((m) => m.roles).filter((roleName) => roleName))
5640
+ .map((role) => globalRoles[role])
5641
+ .filter((role) => role)
5642
+ .map((role) => role.permissions);
5643
+ return {
5644
+ ...realm,
5645
+ permissions: realm.owner === userId
5646
+ ? { manage: '*' }
5647
+ : mergePermissions(...directPermissionSets, ...rolePermissionSets),
5648
+ };
5649
+ })
5599
5650
  .reduce((p, c) => ({ ...p, [c.realmId]: c }), {
5600
5651
  [userId]: {
5601
5652
  realmId: userId,
@@ -5679,7 +5730,7 @@
5679
5730
  const realm = permissionsLookup[realmId || dexie.cloud.currentUserId];
5680
5731
  if (!realm)
5681
5732
  return new PermissionChecker({}, tableName, !owner || owner === dexie.cloud.currentUserId);
5682
- return new PermissionChecker(realm.permissions, tableName, !owner || owner === dexie.cloud.currentUserId);
5733
+ return new PermissionChecker(realm.permissions, tableName, realmId === dexie.cloud.currentUserId || owner === dexie.cloud.currentUserId);
5683
5734
  };
5684
5735
  const o = source.pipe(map(mapper));
5685
5736
  o.getValue = () => mapper(source.getValue());
@@ -5708,6 +5759,7 @@
5708
5759
  //
5709
5760
  const currentUserEmitter = getCurrentUserEmitter(dexie);
5710
5761
  const subscriptions = [];
5762
+ let configuredProgramatically = false;
5711
5763
  // local sync worker - used when there's no service worker.
5712
5764
  let localSyncWorker = null;
5713
5765
  dexie.on('ready', async (dexie) => {
@@ -5734,7 +5786,7 @@
5734
5786
  currentUserEmitter.next(UNAUTHORIZED_USER);
5735
5787
  });
5736
5788
  dexie.cloud = {
5737
- version: '4.0.0-beta.14',
5789
+ version: '4.0.0-beta.17',
5738
5790
  options: { ...DEFAULT_OPTIONS },
5739
5791
  schema: null,
5740
5792
  serverState: null,
@@ -5755,8 +5807,10 @@
5755
5807
  await login(db, hint);
5756
5808
  },
5757
5809
  invites: getInvitesObservable(dexie),
5810
+ roles: getGlobalRolesObservable(dexie),
5758
5811
  configure(options) {
5759
5812
  options = dexie.cloud.options = { ...dexie.cloud.options, ...options };
5813
+ configuredProgramatically = true;
5760
5814
  if (options.databaseUrl && options.nameSuffix) {
5761
5815
  // @ts-ignore
5762
5816
  dexie.name = `${origIdbName}-${getDbNameFromDbUrl(options.databaseUrl)}`;
@@ -5843,7 +5897,7 @@
5843
5897
  db.getSchema(),
5844
5898
  db.getPersistedSyncState(),
5845
5899
  ]);
5846
- if (!options) {
5900
+ if (!configuredProgramatically) {
5847
5901
  // Options not specified programatically (use case for SW!)
5848
5902
  // Take persisted options:
5849
5903
  db.cloud.options = persistedOptions || null;
@@ -5851,6 +5905,8 @@
5851
5905
  else if (!persistedOptions ||
5852
5906
  JSON.stringify(persistedOptions) !== JSON.stringify(options)) {
5853
5907
  // Update persisted options:
5908
+ if (!options)
5909
+ throw new Error(`Internal error`); // options cannot be null if configuredProgramatically is set.
5854
5910
  await db.$syncState.put(options, 'options');
5855
5911
  }
5856
5912
  if (db.cloud.options?.tryUseServiceWorker &&
@@ -5972,7 +6028,7 @@
5972
6028
  }
5973
6029
  }
5974
6030
  }
5975
- dexieCloud.version = '4.0.0-beta.14';
6031
+ dexieCloud.version = '4.0.0-beta.17';
5976
6032
  Dexie__default["default"].Cloud = dexieCloud;
5977
6033
 
5978
6034
  // In case the SW lives for a while, let it reuse already opened connections: