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.
@@ -1,5 +1,5 @@
1
1
  import Dexie, { cmp, liveQuery } from 'dexie';
2
- import { Observable as Observable$1, BehaviorSubject, Subject, from as from$1, fromEvent, of, merge, Subscription as Subscription$1, throwError, combineLatest, map as map$1, share, timer as timer$1, switchMap as switchMap$1 } from 'rxjs';
2
+ 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, switchMap as switchMap$1 } from 'rxjs';
3
3
 
4
4
  const UNAUTHORIZED_USER = {
5
5
  userId: "unauthorized",
@@ -4642,110 +4642,13 @@ function overrideParseStoresSpec(origFunc, dexie) {
4642
4642
  };
4643
4643
  }
4644
4644
 
4645
- const SECONDS = 1000;
4646
- const MINUTES = 60 * SECONDS;
4647
-
4648
- const myId = randomString$1(16);
4649
-
4650
- const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
4651
- const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
4652
- async function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
4653
- // Start working.
4654
- //
4655
- // Check if someone else is working on this already.
4656
- //
4657
- const jobsTable = db.table(jobsTableName);
4658
- async function aquireLock() {
4659
- const gotTheLock = await db.transaction('rw!', jobsTableName, async () => {
4660
- const currentWork = await jobsTable.get(jobName);
4661
- if (!currentWork) {
4662
- // No one else is working. Let's record that we are.
4663
- await jobsTable.add({
4664
- nodeId: myId,
4665
- started: new Date(),
4666
- heartbeat: new Date()
4667
- }, jobName);
4668
- return true;
4669
- }
4670
- else if (currentWork.heartbeat.getTime() <
4671
- Date.now() - GUARDED_JOB_TIMEOUT) {
4672
- 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!`);
4673
- // Now, take over!
4674
- await jobsTable.put({
4675
- nodeId: myId,
4676
- started: new Date(),
4677
- heartbeat: new Date()
4678
- }, jobName);
4679
- return true;
4680
- }
4681
- return false;
4682
- });
4683
- if (gotTheLock)
4684
- return true;
4685
- // Someone else took the job.
4686
- if (awaitRemoteJob) {
4687
- try {
4688
- const jobDoneObservable = from$1(liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
4689
- await jobDoneObservable.toPromise();
4690
- return false;
4691
- }
4692
- catch (err) {
4693
- if (err.name !== 'TimeoutError') {
4694
- throw err;
4695
- }
4696
- // Timeout stopped us! Try aquire the lock now.
4697
- // It will likely succeed this time unless
4698
- // another client took it.
4699
- return await aquireLock();
4700
- }
4701
- }
4702
- return false;
4703
- }
4704
- if (await aquireLock()) {
4705
- // We own the lock entry and can do our job undisturbed.
4706
- // We're not within a transaction, but these type of locks
4707
- // spans over transactions.
4708
- // Start our heart beat during the job.
4709
- // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
4710
- const heartbeat = setInterval(() => {
4711
- jobsTable.update(jobName, (job) => {
4712
- if (job.nodeId === myId) {
4713
- job.heartbeat = new Date();
4714
- }
4715
- });
4716
- }, GUARDED_JOB_HEARTBEAT);
4717
- try {
4718
- return await job();
4719
- }
4720
- finally {
4721
- // Stop heartbeat
4722
- clearInterval(heartbeat);
4723
- // Remove the persisted job state:
4724
- await db.transaction('rw!', jobsTableName, async () => {
4725
- const currentWork = await jobsTable.get(jobName);
4726
- if (currentWork && currentWork.nodeId === myId) {
4727
- jobsTable.delete(jobName);
4728
- }
4729
- });
4730
- }
4731
- }
4732
- }
4733
-
4734
4645
  async function performInitialSync(db, cloudOptions, cloudSchema) {
4735
- console.debug("Performing initial sync");
4736
- await performGuardedJob(db, 'initialSync', '$jobs', async () => {
4737
- // Even though caller has already checked it,
4738
- // Do check again (now within a transaction) that we really do not have a sync state:
4739
- const syncState = await db.getPersistedSyncState();
4740
- if (!syncState?.initiallySynced) {
4741
- await sync(db, cloudOptions, cloudSchema, { isInitialSync: true });
4742
- }
4743
- }, { awaitRemoteJob: true } // Don't return until the job is done!
4744
- );
4745
- console.debug("Done initial sync");
4646
+ console.debug('Performing initial sync');
4647
+ await sync(db, cloudOptions, cloudSchema, { isInitialSync: true });
4648
+ console.debug('Done initial sync');
4746
4649
  }
4747
4650
 
4748
- const USER_INACTIVITY_TIMEOUT = 300000; // 300_000;
4651
+ const USER_INACTIVITY_TIMEOUT = 180000; // 3 minutes
4749
4652
  const INACTIVE_WAIT_TIME = 20000;
4750
4653
  // This observable will be emitted to later down....
4751
4654
  const userIsActive = new BehaviorSubject(true);
@@ -4759,9 +4662,13 @@ const userIsActive = new BehaviorSubject(true);
4759
4662
  // for just a short time.
4760
4663
  const userIsReallyActive = new BehaviorSubject(true);
4761
4664
  userIsActive
4762
- .pipe(switchMap((isActive) => isActive
4763
- ? of(true)
4764
- : of(false).pipe(delay(INACTIVE_WAIT_TIME))), distinctUntilChanged())
4665
+ .pipe(switchMap((isActive) => {
4666
+ //console.debug('SyncStatus: DUBB: isActive changed to', isActive);
4667
+ return isActive
4668
+ ? of(true)
4669
+ : of(false).pipe(delay(INACTIVE_WAIT_TIME))
4670
+ ;
4671
+ }), distinctUntilChanged())
4765
4672
  .subscribe(userIsReallyActive);
4766
4673
  //
4767
4674
  // First create some corner-stone observables to build the flow on
@@ -4776,7 +4683,7 @@ const documentBecomesHidden = visibilityStateIsChanged.pipe(filter(() => documen
4776
4683
  const documentBecomesVisible = visibilityStateIsChanged.pipe(filter(() => document.visibilityState === 'visible'));
4777
4684
  // Any of various user-activity-related events happen:
4778
4685
  const userDoesSomething = typeof window !== 'undefined'
4779
- ? merge(documentBecomesVisible, fromEvent(window, 'mousemove'), fromEvent(window, 'keydown'), fromEvent(window, 'wheel'), fromEvent(window, 'touchmove'))
4686
+ ? merge(documentBecomesVisible, fromEvent(window, 'mousedown'), fromEvent(window, 'mousemove'), fromEvent(window, 'keydown'), fromEvent(window, 'wheel'), fromEvent(window, 'touchmove'))
4780
4687
  : of({});
4781
4688
  if (typeof document !== 'undefined') {
4782
4689
  //
@@ -4827,6 +4734,7 @@ class WSConnection extends Subscription$1 {
4827
4734
  constructor(databaseUrl, rev, realmSetHash, clientIdentity, token, tokenExpiration, subscriber, messageProducer, webSocketStatus) {
4828
4735
  super(() => this.teardown());
4829
4736
  this.id = ++counter;
4737
+ this.reconnecting = false;
4830
4738
  console.debug('New WebSocket Connection', this.id, token ? 'authorized' : 'unauthorized');
4831
4739
  this.databaseUrl = databaseUrl;
4832
4740
  this.rev = rev;
@@ -4846,7 +4754,7 @@ class WSConnection extends Subscription$1 {
4846
4754
  this.disconnect();
4847
4755
  }
4848
4756
  disconnect() {
4849
- this.webSocketStatus.next("disconnected");
4757
+ this.webSocketStatus.next('disconnected');
4850
4758
  if (this.pinger) {
4851
4759
  clearInterval(this.pinger);
4852
4760
  this.pinger = null;
@@ -4864,11 +4772,18 @@ class WSConnection extends Subscription$1 {
4864
4772
  }
4865
4773
  }
4866
4774
  reconnect() {
4867
- this.disconnect();
4868
- this.connect();
4775
+ if (this.reconnecting)
4776
+ return;
4777
+ this.reconnecting = true;
4778
+ try {
4779
+ this.disconnect();
4780
+ }
4781
+ catch { }
4782
+ this.connect()
4783
+ .catch(() => { })
4784
+ .then(() => (this.reconnecting = false)); // finally()
4869
4785
  }
4870
4786
  async connect() {
4871
- this.webSocketStatus.next("connecting");
4872
4787
  this.lastServerActivity = new Date();
4873
4788
  if (this.pauseUntil && this.pauseUntil > new Date()) {
4874
4789
  console.debug('WS not reconnecting just yet', {
@@ -4883,12 +4798,14 @@ class WSConnection extends Subscription$1 {
4883
4798
  if (!this.databaseUrl)
4884
4799
  throw new Error(`Cannot connect without a database URL`);
4885
4800
  if (this.closed) {
4801
+ //console.debug('SyncStatus: DUBB: Ooops it was closed!');
4886
4802
  return;
4887
4803
  }
4888
4804
  if (this.tokenExpiration && this.tokenExpiration < new Date()) {
4889
4805
  this.subscriber.error(new TokenExpiredError()); // Will be handled in connectWebSocket.ts.
4890
4806
  return;
4891
4807
  }
4808
+ this.webSocketStatus.next('connecting');
4892
4809
  this.pinger = setInterval(async () => {
4893
4810
  if (this.closed) {
4894
4811
  console.debug('pinger check', this.id, 'CLOSED.');
@@ -4935,7 +4852,7 @@ class WSConnection extends Subscription$1 {
4935
4852
  const searchParams = new URLSearchParams();
4936
4853
  if (this.subscriber.closed)
4937
4854
  return;
4938
- searchParams.set('v', "2");
4855
+ searchParams.set('v', '2');
4939
4856
  searchParams.set('rev', this.rev);
4940
4857
  searchParams.set('realmsHash', this.realmSetHash);
4941
4858
  searchParams.set('clientId', this.clientIdentity);
@@ -4974,23 +4891,30 @@ class WSConnection extends Subscription$1 {
4974
4891
  }
4975
4892
  };
4976
4893
  try {
4894
+ let everConnected = false;
4977
4895
  await new Promise((resolve, reject) => {
4978
4896
  ws.onopen = (event) => {
4979
4897
  console.debug('dexie-cloud WebSocket onopen');
4898
+ everConnected = true;
4980
4899
  resolve(null);
4981
4900
  };
4982
4901
  ws.onerror = (event) => {
4983
- const error = event.error || new Error('WebSocket Error');
4984
- this.disconnect();
4985
- this.subscriber.error(error);
4986
- this.webSocketStatus.next("error");
4987
- reject(error);
4902
+ if (!everConnected) {
4903
+ const error = event.error || new Error('WebSocket Error');
4904
+ this.subscriber.error(error);
4905
+ this.webSocketStatus.next('error');
4906
+ reject(error);
4907
+ }
4908
+ else {
4909
+ this.reconnect();
4910
+ }
4988
4911
  };
4989
4912
  });
4990
- this.messageProducerSubscription = this.messageProducer.subscribe(msg => {
4913
+ this.messageProducerSubscription = this.messageProducer.subscribe((msg) => {
4991
4914
  if (!this.closed) {
4992
- if (msg.type === 'ready' && this.webSocketStatus.value !== 'connected') {
4993
- this.webSocketStatus.next("connected");
4915
+ if (msg.type === 'ready' &&
4916
+ this.webSocketStatus.value !== 'connected') {
4917
+ this.webSocketStatus.next('connected');
4994
4918
  }
4995
4919
  this.ws?.send(TSON.stringify(msg));
4996
4920
  }
@@ -5027,9 +4951,9 @@ function connectWebSocket(db) {
5027
4951
  rev: syncState.serverRevision,
5028
4952
  })));
5029
4953
  function createObservable() {
5030
- return db.cloud.persistedSyncState.pipe(filter(syncState => syncState?.serverRevision), // Don't connect before there's no initial sync performed.
4954
+ return db.cloud.persistedSyncState.pipe(filter((syncState) => syncState?.serverRevision), // Don't connect before there's no initial sync performed.
5031
4955
  take(1), // Don't continue waking up whenever syncState change
5032
- 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]) =>
4956
+ 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]) =>
5033
4957
  // Let server end query changes from last entry of same client-ID and forward.
5034
4958
  // If no new entries, server won't bother the client. If new entries, server sends only those
5035
4959
  // and the baseRev of the last from same client-ID.
@@ -5052,7 +4976,10 @@ function connectWebSocket(db) {
5052
4976
  else {
5053
4977
  return throwError(error);
5054
4978
  }
5055
- }), catchError((error) => from$1(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()))));
4979
+ }), catchError((error) => {
4980
+ db.cloud.webSocketStatus.next("error");
4981
+ return from$1(waitAndReconnectWhenUserDoesSomething(error)).pipe(switchMap(() => createObservable()));
4982
+ }));
5056
4983
  }
5057
4984
  return createObservable().subscribe((msg) => {
5058
4985
  if (msg) {
@@ -5072,6 +4999,95 @@ async function isSyncNeeded(db) {
5072
4999
  : false;
5073
5000
  }
5074
5001
 
5002
+ const SECONDS = 1000;
5003
+ const MINUTES = 60 * SECONDS;
5004
+
5005
+ const myId = randomString$1(16);
5006
+
5007
+ const GUARDED_JOB_HEARTBEAT = 1 * SECONDS;
5008
+ const GUARDED_JOB_TIMEOUT = 1 * MINUTES;
5009
+ async function performGuardedJob(db, jobName, jobsTableName, job, { awaitRemoteJob } = {}) {
5010
+ // Start working.
5011
+ //
5012
+ // Check if someone else is working on this already.
5013
+ //
5014
+ const jobsTable = db.table(jobsTableName);
5015
+ async function aquireLock() {
5016
+ const gotTheLock = await db.transaction('rw!', jobsTableName, async () => {
5017
+ const currentWork = await jobsTable.get(jobName);
5018
+ if (!currentWork) {
5019
+ // No one else is working. Let's record that we are.
5020
+ await jobsTable.add({
5021
+ nodeId: myId,
5022
+ started: new Date(),
5023
+ heartbeat: new Date()
5024
+ }, jobName);
5025
+ return true;
5026
+ }
5027
+ else if (currentWork.heartbeat.getTime() <
5028
+ Date.now() - GUARDED_JOB_TIMEOUT) {
5029
+ 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!`);
5030
+ // Now, take over!
5031
+ await jobsTable.put({
5032
+ nodeId: myId,
5033
+ started: new Date(),
5034
+ heartbeat: new Date()
5035
+ }, jobName);
5036
+ return true;
5037
+ }
5038
+ return false;
5039
+ });
5040
+ if (gotTheLock)
5041
+ return true;
5042
+ // Someone else took the job.
5043
+ if (awaitRemoteJob) {
5044
+ try {
5045
+ const jobDoneObservable = from$1(liveQuery(() => jobsTable.get(jobName))).pipe(timeout(GUARDED_JOB_TIMEOUT), filter((job) => !job)); // Wait til job is not there anymore.
5046
+ await jobDoneObservable.toPromise();
5047
+ return false;
5048
+ }
5049
+ catch (err) {
5050
+ if (err.name !== 'TimeoutError') {
5051
+ throw err;
5052
+ }
5053
+ // Timeout stopped us! Try aquire the lock now.
5054
+ // It will likely succeed this time unless
5055
+ // another client took it.
5056
+ return await aquireLock();
5057
+ }
5058
+ }
5059
+ return false;
5060
+ }
5061
+ if (await aquireLock()) {
5062
+ // We own the lock entry and can do our job undisturbed.
5063
+ // We're not within a transaction, but these type of locks
5064
+ // spans over transactions.
5065
+ // Start our heart beat during the job.
5066
+ // Use setInterval to make sure we are updating heartbeat even during long-lived fetch calls.
5067
+ const heartbeat = setInterval(() => {
5068
+ jobsTable.update(jobName, (job) => {
5069
+ if (job.nodeId === myId) {
5070
+ job.heartbeat = new Date();
5071
+ }
5072
+ });
5073
+ }, GUARDED_JOB_HEARTBEAT);
5074
+ try {
5075
+ return await job();
5076
+ }
5077
+ finally {
5078
+ // Stop heartbeat
5079
+ clearInterval(heartbeat);
5080
+ // Remove the persisted job state:
5081
+ await db.transaction('rw!', jobsTableName, async () => {
5082
+ const currentWork = await jobsTable.get(jobName);
5083
+ if (currentWork && currentWork.nodeId === myId) {
5084
+ await jobsTable.delete(jobName);
5085
+ }
5086
+ });
5087
+ }
5088
+ }
5089
+ }
5090
+
5075
5091
  const ongoingSyncs = new WeakMap();
5076
5092
  function syncIfPossible(db, cloudOptions, cloudSchema, options) {
5077
5093
  const ongoing = ongoingSyncs.get(db);
@@ -5476,6 +5492,21 @@ function createSharedValueObservable(o, defaultValue) {
5476
5492
  return rv;
5477
5493
  }
5478
5494
 
5495
+ const getGlobalRolesObservable = associate((db) => {
5496
+ return createSharedValueObservable(liveQuery(() => db.roles
5497
+ .where({ realmId: 'rlm-public' })
5498
+ .toArray()
5499
+ .then((roles) => {
5500
+ const rv = {};
5501
+ for (const role of roles
5502
+ .slice()
5503
+ .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))) {
5504
+ rv[role.name] = role;
5505
+ }
5506
+ return rv;
5507
+ })), {});
5508
+ });
5509
+
5479
5510
  const getCurrentUserEmitter = associate((db) => new BehaviorSubject(UNAUTHORIZED_USER));
5480
5511
 
5481
5512
  const getInternalAccessControlObservable = associate((db) => {
@@ -5577,18 +5608,38 @@ function mergePermissions(...permissions) {
5577
5608
  }
5578
5609
 
5579
5610
  const getPermissionsLookupObservable = associate((db) => {
5580
- const o = getInternalAccessControlObservable(db._novip);
5581
- return mapValueObservable(o, ({ selfMembers, realms, userId }) => {
5611
+ const o = createSharedValueObservable(combineLatest([
5612
+ getInternalAccessControlObservable(db._novip),
5613
+ getGlobalRolesObservable(db._novip),
5614
+ ]).pipe(map(([{ selfMembers, realms, userId }, globalRoles]) => ({
5615
+ selfMembers,
5616
+ realms,
5617
+ userId,
5618
+ globalRoles,
5619
+ }))), {
5620
+ selfMembers: [],
5621
+ realms: [],
5622
+ userId: UNAUTHORIZED_USER.userId,
5623
+ globalRoles: {},
5624
+ });
5625
+ return mapValueObservable(o, ({ selfMembers, realms, userId, globalRoles }) => {
5582
5626
  const rv = realms
5583
- .map((realm) => ({
5584
- ...realm,
5585
- permissions: realm.owner === userId
5586
- ? { manage: '*' }
5587
- : mergePermissions(...selfMembers
5588
- .filter((m) => m.realmId === realm.realmId)
5589
- .map((m) => m.permissions)
5590
- .filter((p) => p)),
5591
- }))
5627
+ .map((realm) => {
5628
+ const selfRealmMembers = selfMembers.filter((m) => m.realmId === realm.realmId);
5629
+ const directPermissionSets = selfRealmMembers
5630
+ .map((m) => m.permissions)
5631
+ .filter((p) => p);
5632
+ const rolePermissionSets = flatten(selfRealmMembers.map((m) => m.roles).filter((roleName) => roleName))
5633
+ .map((role) => globalRoles[role])
5634
+ .filter((role) => role)
5635
+ .map((role) => role.permissions);
5636
+ return {
5637
+ ...realm,
5638
+ permissions: realm.owner === userId
5639
+ ? { manage: '*' }
5640
+ : mergePermissions(...directPermissionSets, ...rolePermissionSets),
5641
+ };
5642
+ })
5592
5643
  .reduce((p, c) => ({ ...p, [c.realmId]: c }), {
5593
5644
  [userId]: {
5594
5645
  realmId: userId,
@@ -5672,7 +5723,7 @@ function permissions(dexie, obj, tableName) {
5672
5723
  const realm = permissionsLookup[realmId || dexie.cloud.currentUserId];
5673
5724
  if (!realm)
5674
5725
  return new PermissionChecker({}, tableName, !owner || owner === dexie.cloud.currentUserId);
5675
- return new PermissionChecker(realm.permissions, tableName, !owner || owner === dexie.cloud.currentUserId);
5726
+ return new PermissionChecker(realm.permissions, tableName, realmId === dexie.cloud.currentUserId || owner === dexie.cloud.currentUserId);
5676
5727
  };
5677
5728
  const o = source.pipe(map(mapper));
5678
5729
  o.getValue = () => mapper(source.getValue());
@@ -5701,6 +5752,7 @@ function dexieCloud(dexie) {
5701
5752
  //
5702
5753
  const currentUserEmitter = getCurrentUserEmitter(dexie);
5703
5754
  const subscriptions = [];
5755
+ let configuredProgramatically = false;
5704
5756
  // local sync worker - used when there's no service worker.
5705
5757
  let localSyncWorker = null;
5706
5758
  dexie.on('ready', async (dexie) => {
@@ -5727,7 +5779,7 @@ function dexieCloud(dexie) {
5727
5779
  currentUserEmitter.next(UNAUTHORIZED_USER);
5728
5780
  });
5729
5781
  dexie.cloud = {
5730
- version: '4.0.0-beta.14',
5782
+ version: '4.0.0-beta.17',
5731
5783
  options: { ...DEFAULT_OPTIONS },
5732
5784
  schema: null,
5733
5785
  serverState: null,
@@ -5748,8 +5800,10 @@ function dexieCloud(dexie) {
5748
5800
  await login(db, hint);
5749
5801
  },
5750
5802
  invites: getInvitesObservable(dexie),
5803
+ roles: getGlobalRolesObservable(dexie),
5751
5804
  configure(options) {
5752
5805
  options = dexie.cloud.options = { ...dexie.cloud.options, ...options };
5806
+ configuredProgramatically = true;
5753
5807
  if (options.databaseUrl && options.nameSuffix) {
5754
5808
  // @ts-ignore
5755
5809
  dexie.name = `${origIdbName}-${getDbNameFromDbUrl(options.databaseUrl)}`;
@@ -5836,7 +5890,7 @@ function dexieCloud(dexie) {
5836
5890
  db.getSchema(),
5837
5891
  db.getPersistedSyncState(),
5838
5892
  ]);
5839
- if (!options) {
5893
+ if (!configuredProgramatically) {
5840
5894
  // Options not specified programatically (use case for SW!)
5841
5895
  // Take persisted options:
5842
5896
  db.cloud.options = persistedOptions || null;
@@ -5844,6 +5898,8 @@ function dexieCloud(dexie) {
5844
5898
  else if (!persistedOptions ||
5845
5899
  JSON.stringify(persistedOptions) !== JSON.stringify(options)) {
5846
5900
  // Update persisted options:
5901
+ if (!options)
5902
+ throw new Error(`Internal error`); // options cannot be null if configuredProgramatically is set.
5847
5903
  await db.$syncState.put(options, 'options');
5848
5904
  }
5849
5905
  if (db.cloud.options?.tryUseServiceWorker &&
@@ -5965,7 +6021,7 @@ function dexieCloud(dexie) {
5965
6021
  }
5966
6022
  }
5967
6023
  }
5968
- dexieCloud.version = '4.0.0-beta.14';
6024
+ dexieCloud.version = '4.0.0-beta.17';
5969
6025
  Dexie.Cloud = dexieCloud;
5970
6026
 
5971
6027
  // In case the SW lives for a while, let it reuse already opened connections: