Переглянути джерело

Use safeSave to update the session upon reload.

Ran prettier by mistake.
JC Brand 4 місяців тому
батько
коміт
12c96e6bdc

+ 2 - 1
src/headless/plugins/muc/muc.js

@@ -27,7 +27,8 @@ import { TimeoutError, ItemNotFoundError, StanzaError } from '../../shared/error
 import { computeAffiliationsDelta, setAffiliations, getAffiliationList } from './affiliations/utils.js';
 import { initStorage, createStore } from '../../utils/storage.js';
 import { isArchived, parseErrorStanza } from '../../shared/parsers.js';
-import { getUniqueId, safeSave } from '../../utils/index.js';
+import { getUniqueId } from '../../utils/index.js';
+import { safeSave } from '../../utils/init.js';
 import { isUniView } from '../../utils/session.js';
 import { parseMUCMessage, parseMUCPresence } from './parsers.js';
 import { sendMarker } from '../../shared/actions.js';

+ 1 - 1
src/headless/plugins/muc/utils.js

@@ -3,7 +3,7 @@ import api from '../../shared/api/index.js';
 import converse from '../../shared/api/public.js';
 import log from '../../log.js';
 import { MUC_ROLE_WEIGHTS } from './constants.js';
-import { safeSave } from '../../utils/index.js';
+import { safeSave } from '../../utils/init.js';
 import { CHATROOMS_TYPE } from '../../shared/constants.js';
 import { getUnloadEvent } from '../../utils/session.js';
 

+ 1 - 1
src/headless/shared/connection/index.js

@@ -278,7 +278,7 @@ export class Connection extends Strophe.Connection {
      */
     setConnectionStatus (status, message) {
         this.status = status;
-        _converse.state.connfeedback.set({'connection_status': status, message });
+        _converse.state.connfeedback.set({ connection_status: status, message });
     }
 
     async finishDisconnection () {

+ 3 - 3
src/headless/types/utils/index.d.ts

@@ -8,7 +8,6 @@ export function isEmptyMessage(attrs: any): boolean;
  * inserted before the mentioned nicknames.
  */
 export function prefixMentions(message: any): any;
-export function safeSave(model: any, attributes: any, options: any): void;
 export function getRandomInt(max: any): number;
 /**
  * @param {string} [suffix]
@@ -21,7 +20,6 @@ declare const _default: {
     isEmptyMessage: typeof isEmptyMessage;
     onMultipleEvents: typeof onMultipleEvents;
     prefixMentions: typeof prefixMentions;
-    safeSave: typeof safeSave;
     shouldCreateMessage: typeof shouldCreateMessage;
     triggerEvent: typeof triggerEvent;
     isValidURL(text: string): boolean;
@@ -63,6 +61,7 @@ declare const _default: {
     isFunction(val: unknown): boolean;
     isUndefined(x: unknown): boolean;
     isErrorObject(o: unknown): boolean;
+    isPersistableModel(model: import("@converse/skeletor").Model): boolean;
     isValidJID(jid?: string | null): boolean;
     isValidMUCJID(jid: string): boolean;
     isSameBareJID(jid1: string, jid2: string): boolean;
@@ -77,6 +76,7 @@ declare const _default: {
     cleanup(_converse: ConversePrivateGlobal): Promise<void>;
     attemptNonPreboundSession(credentials?: import("./types.js").Credentials, automatic?: boolean): Promise<void>;
     savedLoginInfo(jid: string): Promise<Model>;
+    safeSave(model: Model, attributes: any, options: any): void;
     isElement(el: unknown): boolean;
     isTagEqual(stanza: Element | typeof import("strophe.js").Builder, name: string): boolean;
     stringToElement(s: string): Element;
@@ -124,6 +124,6 @@ declare function shouldCreateMessage(attrs: any): any;
 declare function triggerEvent(el: Element, name: string, type?: string, bubbles?: boolean, cancelable?: boolean): void;
 import * as url from './url.js';
 import * as session from './session.js';
-import * as init from './init.js';
 import { Model } from '@converse/skeletor';
+import * as init from './init.js';
 //# sourceMappingURL=index.d.ts.map

+ 7 - 1
src/headless/types/utils/init.d.ts

@@ -57,6 +57,12 @@ export function attemptNonPreboundSession(credentials?: import("./types").Creden
  *  used login keys.
  */
 export function savedLoginInfo(jid: string): Promise<Model>;
+/**
+ * @param {Model} model
+ * @param {Object} attributes
+ * @param {Object} options
+ */
+export function safeSave(model: Model, attributes: any, options: any): void;
 export type ConversePrivateGlobal = any;
-import { Model } from '@converse/skeletor';
+import { Model } from "@converse/skeletor";
 //# sourceMappingURL=init.d.ts.map

+ 5 - 0
src/headless/types/utils/object.d.ts

@@ -24,4 +24,9 @@ export function isUndefined(x: unknown): boolean;
  * @returns {boolean} True if the value is an Error
  */
 export function isErrorObject(o: unknown): boolean;
+/**
+ * @param {import('@converse/skeletor').Model} model
+ * @returns {boolean}
+ */
+export function isPersistableModel(model: import("@converse/skeletor").Model): boolean;
 //# sourceMappingURL=object.d.ts.map

+ 0 - 13
src/headless/utils/index.js

@@ -99,18 +99,6 @@ function onMultipleEvents (events=[], callback) {
     events.forEach(e => e.object.on(e.event, handler));
 }
 
-function isPersistableModel (model) {
-    return model.collection && model.collection.browserStorage;
-}
-
-export function safeSave (model, attributes, options) {
-    if (isPersistableModel(model)) {
-        model.save(attributes, options);
-    } else {
-        model.set(attributes, options);
-    }
-}
-
 /**
  * @param {Element} el
  * @param {string} name
@@ -166,7 +154,6 @@ export default Object.assign({
     isEmptyMessage,
     onMultipleEvents,
     prefixMentions,
-    safeSave,
     shouldCreateMessage,
     triggerEvent,
 }, u);

+ 126 - 135
src/headless/utils/init.js

@@ -1,20 +1,20 @@
 /**
  * @typedef {module:shared.converse.ConversePrivateGlobal} ConversePrivateGlobal
  */
-import Storage from '@converse/skeletor/src/storage.js';
-import _converse from '../shared/_converse';
-import debounce from 'lodash-es/debounce';
-import localDriver from 'localforage-webextensionstorage-driver/local';
-import log from '../log.js';
-import syncDriver from 'localforage-webextensionstorage-driver/sync';
-import { ANONYMOUS, CORE_PLUGINS, EXTERNAL, LOGIN } from '../shared/constants.js';
-import { Model } from '@converse/skeletor';
-import { Strophe } from 'strophe.js';
-import { createStore, initStorage } from './storage.js';
-import { generateResource, getConnectionServiceURL } from '../shared/connection/utils';
-import { isValidJID } from './jid.js';
-import { getUnloadEvent, isTestEnv } from './session.js';
-
+import Storage from "@converse/skeletor/src/storage.js";
+import _converse from "../shared/_converse";
+import debounce from "lodash-es/debounce";
+import localDriver from "localforage-webextensionstorage-driver/local";
+import log from "../log.js";
+import syncDriver from "localforage-webextensionstorage-driver/sync";
+import { ANONYMOUS, CORE_PLUGINS, EXTERNAL, LOGIN } from "../shared/constants.js";
+import { Model } from "@converse/skeletor";
+import { Strophe } from "strophe.js";
+import { createStore, initStorage } from "./storage.js";
+import { generateResource, getConnectionServiceURL } from "../shared/connection/utils";
+import { isValidJID } from "./jid.js";
+import { getUnloadEvent, isTestEnv } from "./session.js";
+import { isPersistableModel } from "./object";
 
 /**
  * Initializes the plugins for the Converse instance.
@@ -22,7 +22,7 @@ import { getUnloadEvent, isTestEnv } from './session.js';
  * @fires _converse#pluginsInitialized - Triggered once all plugins have been initialized.
  * @memberOf _converse
  */
-export function initPlugins (_converse) {
+export function initPlugins(_converse) {
     // If initialize gets called a second time (e.g. during tests), then we
     // need to re-apply all plugins (for a new converse instance), and we
     // therefore need to clear this array that prevents plugins from being
@@ -33,19 +33,12 @@ export function initPlugins (_converse) {
     const whitelist = CORE_PLUGINS.concat(_converse.api.settings.get("whitelisted_plugins"));
 
     if (_converse.api.settings.get("singleton")) {
-        [
-            'converse-bookmarks',
-            'converse-controlbox',
-            'converse-headline',
-            'converse-register'
-        ].forEach(name => _converse.api.settings.get("blacklisted_plugins").push(name));
+        ["converse-bookmarks", "converse-controlbox", "converse-headline", "converse-register"].forEach((name) =>
+            _converse.api.settings.get("blacklisted_plugins").push(name)
+        );
     }
 
-    _converse.pluggable.initializePlugins(
-        { _converse },
-        whitelist,
-        _converse.api.settings.get("blacklisted_plugins")
-    );
+    _converse.pluggable.initializePlugins({ _converse }, whitelist, _converse.api.settings.get("blacklisted_plugins"));
 
     /**
      * Triggered once all plugins have been initialized. This is a useful event if you want to
@@ -61,27 +54,26 @@ export function initPlugins (_converse) {
      * @memberOf _converse
      * @example _converse.api.listen.on('pluginsInitialized', () => { ... });
      */
-    _converse.api.trigger('pluginsInitialized');
+    _converse.api.trigger("pluginsInitialized");
 }
 
-
 /**
  * @param {ConversePrivateGlobal} _converse
  */
-export async function initClientConfig (_converse) {
+export async function initClientConfig(_converse) {
     /* The client config refers to configuration of the client which is
      * independent of any particular user.
      * What this means is that config values need to persist across
      * user sessions.
      */
-    const id = 'converse.client-config';
-    const config = new Model({ id, 'trusted': true });
+    const id = "converse.client-config";
+    const config = new Model({ id, "trusted": true });
     config.browserStorage = createStore(id, "session");
 
     Object.assign(_converse, { config }); // XXX DEPRECATED
     Object.assign(_converse.state, { config });
 
-    await new Promise(r => config.fetch({'success': r, 'error': r}));
+    await new Promise((r) => config.fetch({ "success": r, "error": r }));
     /**
      * Triggered once the XMPP-client configuration has been initialized.
      * The client configuration is independent of any particular and its values
@@ -91,66 +83,62 @@ export async function initClientConfig (_converse) {
      * @example
      * _converse.api.listen.on('clientConfigInitialized', () => { ... });
      */
-    _converse.api.trigger('clientConfigInitialized');
+    _converse.api.trigger("clientConfigInitialized");
 }
 
-
 /**
  * @param {ConversePrivateGlobal} _converse
  */
-export async function initSessionStorage (_converse) {
+export async function initSessionStorage(_converse) {
     await Storage.sessionStorageInitialized;
-    _converse.storage['session'] = Storage.localForage.createInstance({
-        'name': isTestEnv() ? 'converse-test-session' : 'converse-session',
-        'description': 'sessionStorage instance',
-        'driver': ['sessionStorageWrapper']
+    _converse.storage["session"] = Storage.localForage.createInstance({
+        "name": isTestEnv() ? "converse-test-session" : "converse-session",
+        "description": "sessionStorage instance",
+        "driver": ["sessionStorageWrapper"],
     });
 }
 
-
 /**
  * Initializes persistent storage
  * @param {ConversePrivateGlobal} _converse
  * @param {string} store_name - The name of the store.
  */
-function initPersistentStorage (_converse, store_name) {
-    if (_converse.api.settings.get('persistent_store') === 'sessionStorage') {
+function initPersistentStorage(_converse, store_name) {
+    if (_converse.api.settings.get("persistent_store") === "sessionStorage") {
         return;
-    } else if (_converse.api.settings.get("persistent_store") === 'BrowserExtLocal') {
-        Storage.localForage.defineDriver(localDriver).then(
-            () => Storage.localForage.setDriver('webExtensionLocalStorage')
-        );
-        _converse.storage['persistent'] = Storage.localForage;
+    } else if (_converse.api.settings.get("persistent_store") === "BrowserExtLocal") {
+        Storage.localForage
+            .defineDriver(localDriver)
+            .then(() => Storage.localForage.setDriver("webExtensionLocalStorage"));
+        _converse.storage["persistent"] = Storage.localForage;
         return;
-
-    } else if (_converse.api.settings.get("persistent_store") === 'BrowserExtSync') {
-        Storage.localForage.defineDriver(syncDriver).then(
-            () => Storage.localForage.setDriver('webExtensionSyncStorage')
-        );
-        _converse.storage['persistent'] = Storage.localForage;
+    } else if (_converse.api.settings.get("persistent_store") === "BrowserExtSync") {
+        Storage.localForage
+            .defineDriver(syncDriver)
+            .then(() => Storage.localForage.setDriver("webExtensionSyncStorage"));
+        _converse.storage["persistent"] = Storage.localForage;
         return;
     }
 
     const config = {
-        'name': isTestEnv() ? 'converse-test-persistent' : 'converse-persistent',
-        'storeName': store_name
-    }
-    if (_converse.api.settings.get("persistent_store") === 'localStorage') {
-        config['description'] = 'localStorage instance';
-        config['driver'] = [Storage.localForage.LOCALSTORAGE];
-    } else if (_converse.api.settings.get("persistent_store") === 'IndexedDB') {
-        config['description'] = 'indexedDB instance';
-        config['driver'] = [Storage.localForage.INDEXEDDB];
+        "name": isTestEnv() ? "converse-test-persistent" : "converse-persistent",
+        "storeName": store_name,
+    };
+    if (_converse.api.settings.get("persistent_store") === "localStorage") {
+        config["description"] = "localStorage instance";
+        config["driver"] = [Storage.localForage.LOCALSTORAGE];
+    } else if (_converse.api.settings.get("persistent_store") === "IndexedDB") {
+        config["description"] = "indexedDB instance";
+        config["driver"] = [Storage.localForage.INDEXEDDB];
     }
-    _converse.storage['persistent'] = Storage.localForage.createInstance(config);
+    _converse.storage["persistent"] = Storage.localForage.createInstance(config);
 }
 
-
 /**
  * @param {ConversePrivateGlobal} _converse
  * @param {string} jid
  */
-function saveJIDtoSession (_converse, jid) {
+function saveJIDtoSession(_converse, jid) {
     const { api } = _converse;
 
     if (api.settings.get("authentication") !== ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
@@ -164,12 +152,16 @@ function saveJIDtoSession (_converse, jid) {
     // TODO: Storing directly on _converse is deprecated
     Object.assign(_converse, { jid, bare_jid, resource, domain });
 
-    _converse.session.save({ jid, bare_jid, resource, domain,
+    _converse.session.save({
+        jid,
+        bare_jid,
+        resource,
+        domain,
         // We use the `active` flag to determine whether we should use the values from sessionStorage.
         // When "cloning" a tab (e.g. via middle-click), the `active` flag will be set and we'll create
         // a new empty user session, otherwise it'll be false and we can re-use the user session.
         // When the tab is reloaded, the `active` flag is set to `false`.
-       'active': true
+        "active": true,
     });
     // Set JID on the connection object so that when we call `connection.bind`
     // the new resource is found by Strophe.js and sent to the XMPP server.
@@ -188,44 +180,43 @@ function saveJIDtoSession (_converse, jid) {
  * @emits _converse#setUserJID
  * @param {string} jid
  */
-export async function setUserJID (jid) {
+export async function setUserJID(jid) {
     await initSession(_converse, jid);
 
     /**
      * Triggered whenever the user's JID has been updated
      * @event _converse#setUserJID
      */
-    _converse.api.trigger('setUserJID');
+    _converse.api.trigger("setUserJID");
     return jid;
 }
 
-
 /**
  * @param {ConversePrivateGlobal} _converse
  * @param {string} jid
  */
-export async function initSession (_converse, jid) {
-    const is_shared_session = _converse.api.settings.get('connection_options').worker;
+export async function initSession(_converse, jid) {
+    const is_shared_session = _converse.api.settings.get("connection_options").worker;
 
     const bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase();
     const id = `converse.session-${bare_jid}`;
-    if (_converse.session?.get('id') !== id) {
+    if (_converse.session?.get("id") !== id) {
         initPersistentStorage(_converse, bare_jid);
 
         _converse.session.set({ id });
         initStorage(_converse.session, id, is_shared_session ? "persistent" : "session");
-        await new Promise(r => _converse.session.fetch({'success': r, 'error': r}));
+        await new Promise((r) => _converse.session.fetch({ "success": r, "error": r }));
 
-        if (!is_shared_session && _converse.session.get('active')) {
+        if (!is_shared_session && _converse.session.get("active")) {
             // If the `active` flag is set, it means this tab was cloned from
             // another (e.g. via middle-click), and its session data was copied over.
             _converse.session.clear();
-            _converse.session.save({id});
+            _converse.session.save({ id });
         }
         saveJIDtoSession(_converse, jid);
 
         // Set `active` flag to false when the tab gets reloaded
-        window.addEventListener(getUnloadEvent(), () => _converse.session?.save('active', false));
+        window.addEventListener(getUnloadEvent(), () => safeSave(_converse.session, { active: false }));
 
         /**
          * Triggered once the user's session has been initialized. The session is a
@@ -233,53 +224,49 @@ export async function initSession (_converse, jid) {
          * @event _converse#userSessionInitialized
          * @memberOf _converse
          */
-        _converse.api.trigger('userSessionInitialized');
+        _converse.api.trigger("userSessionInitialized");
     } else {
         saveJIDtoSession(_converse, jid);
     }
 }
 
-
 /**
  * @param {ConversePrivateGlobal} _converse
  */
-export function registerGlobalEventHandlers (_converse) {
+export function registerGlobalEventHandlers(_converse) {
     /**
      * Plugins can listen to this event as cue to register their
      * global event handlers.
      * @event _converse#registeredGlobalEventHandlers
      * @example _converse.api.listen.on('registeredGlobalEventHandlers', () => { ... });
      */
-    _converse.api.trigger('registeredGlobalEventHandlers');
+    _converse.api.trigger("registeredGlobalEventHandlers");
 }
 
-
 /**
  * @param {ConversePrivateGlobal} _converse
  */
-function unregisterGlobalEventHandlers (_converse) {
-    _converse.api.trigger('unregisteredGlobalEventHandlers');
+function unregisterGlobalEventHandlers(_converse) {
+    _converse.api.trigger("unregisteredGlobalEventHandlers");
 }
 
-
 /**
  * Make sure everything is reset in case this is a subsequent call to
  * converse.initialize (happens during tests).
  * @param {ConversePrivateGlobal} _converse
  */
-export async function cleanup (_converse) {
+export async function cleanup(_converse) {
     const { api } = _converse;
-    await api.trigger('cleanup', {'synchronous': true});
+    await api.trigger("cleanup", { "synchronous": true });
     unregisterGlobalEventHandlers(_converse);
     api.connection.get()?.reset();
     _converse.stopListening();
     _converse.off();
-    if (_converse.promises['initialized'].isResolved) {
-        api.promises.add('initialized')
+    if (_converse.promises["initialized"].isResolved) {
+        api.promises.add("initialized");
     }
 }
 
-
 /**
  * Fetches login credentials from the server.
  * @param {number} [wait=0]
@@ -288,19 +275,19 @@ export async function cleanup (_converse) {
  *  A promise that resolves with the provided login credentials (JID and password).
  * @throws {Error} If the request fails or returns an error status.
  */
-function fetchLoginCredentials (wait=0) {
+function fetchLoginCredentials(wait = 0) {
     return new Promise(
         debounce(async (resolve, reject) => {
             let xhr = new XMLHttpRequest();
-            xhr.open('GET', _converse.api.settings.get("credentials_url"), true);
-            xhr.setRequestHeader('Accept', 'application/json, text/javascript');
+            xhr.open("GET", _converse.api.settings.get("credentials_url"), true);
+            xhr.setRequestHeader("Accept", "application/json, text/javascript");
             xhr.onload = () => {
                 if (xhr.status >= 200 && xhr.status < 400) {
                     const data = JSON.parse(xhr.responseText);
                     setUserJID(data.jid).then(() => {
                         resolve({
                             jid: data.jid,
-                            password: data.password
+                            password: data.password,
                         });
                     });
                 } else {
@@ -312,24 +299,23 @@ function fetchLoginCredentials (wait=0) {
              * *Hook* which allows modifying the server request
              * @event _converse#beforeFetchLoginCredentials
              */
-            xhr = await _converse.api.hook('beforeFetchLoginCredentials', this, xhr);
+            xhr = await _converse.api.hook("beforeFetchLoginCredentials", this, xhr);
             xhr.send();
         }, wait)
     );
 }
 
-
 /**
  * @returns {Promise<import('./types').Credentials>}
  */
-async function getLoginCredentialsFromURL () {
+async function getLoginCredentialsFromURL() {
     let credentials;
     let wait = 0;
     while (!credentials) {
         try {
             credentials = await fetchLoginCredentials(wait); // eslint-disable-line no-await-in-loop
         } catch (e) {
-            log.error('Could not fetch login credentials');
+            log.error("Could not fetch login credentials");
             log.error(e);
         }
         // If unsuccessful, we wait 2 seconds between subsequent attempts to
@@ -339,19 +325,18 @@ async function getLoginCredentialsFromURL () {
     return credentials;
 }
 
-
-async function getLoginCredentialsFromBrowser () {
-    const jid = localStorage.getItem('conversejs-session-jid');
+async function getLoginCredentialsFromBrowser() {
+    const jid = localStorage.getItem("conversejs-session-jid");
     if (!jid) return null;
 
     try {
-        const creds = await navigator.credentials.get({ password: true});
-        if (creds && creds.type == 'password' && isValidJID(creds.id)) {
+        const creds = await navigator.credentials.get({ password: true });
+        if (creds && creds.type == "password" && isValidJID(creds.id)) {
             // XXX: We don't actually compare `creds.id` with `jid` because
             // the user might have been presented a list of credentials with
             // which to log in, and we want to respect their wish.
             await setUserJID(creds.id);
-            return {'jid': creds.id, 'password': creds.password};
+            return { "jid": creds.id, "password": creds.password };
         }
     } catch (e) {
         log.error(e);
@@ -359,24 +344,22 @@ async function getLoginCredentialsFromBrowser () {
     }
 }
 
-
-async function getLoginCredentialsFromSCRAMKeys () {
-    const jid = localStorage.getItem('conversejs-session-jid');
+async function getLoginCredentialsFromSCRAMKeys() {
+    const jid = localStorage.getItem("conversejs-session-jid");
     if (!jid) return null;
 
     await setUserJID(jid);
 
     const login_info = await savedLoginInfo(jid);
-    const scram_keys = login_info.get('scram_keys');
-    return scram_keys ? { jid , password: scram_keys } : null;
+    const scram_keys = login_info.get("scram_keys");
+    return scram_keys ? { jid, password: scram_keys } : null;
 }
 
-
 /**
  * @param {import('./types').Credentials} [credentials]
  * @param {boolean} [automatic]
  */
-export async function attemptNonPreboundSession (credentials, automatic) {
+export async function attemptNonPreboundSession(credentials, automatic) {
     const { api } = _converse;
     /**
      * *Hook* to allow 3rd party plugins to provide their own login credentials.
@@ -389,7 +372,7 @@ export async function attemptNonPreboundSession (credentials, automatic) {
     if (new_creds) return connect(new_creds);
 
     if (api.settings.get("authentication") === LOGIN) {
-        const jid = _converse.session.get('jid');
+        const jid = _converse.session.get("jid");
         // XXX: If EITHER ``keepalive`` or ``auto_login`` is ``true`` and
         // ``authentication`` is set to ``login``, then Converse will try to log the user in,
         // since we don't have a way to distinguish between wether we're
@@ -406,18 +389,17 @@ export async function attemptNonPreboundSession (credentials, automatic) {
             return connect();
         }
 
-        if (api.settings.get('reuse_scram_keys')) {
+        if (api.settings.get("reuse_scram_keys")) {
             const credentials = await getLoginCredentialsFromSCRAMKeys();
             if (credentials) return connect(credentials);
         }
 
-        if (!isTestEnv() && 'credentials' in navigator) {
+        if (!isTestEnv() && "credentials" in navigator) {
             const credentials = await getLoginCredentialsFromBrowser();
             if (credentials) return connect(credentials);
         }
 
         if (!isTestEnv()) log.debug("attemptNonPreboundSession: Couldn't find credentials to log in with");
-
     } else if (
         [ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication")) &&
         (!automatic || api.settings.get("auto_login"))
@@ -426,7 +408,6 @@ export async function attemptNonPreboundSession (credentials, automatic) {
     }
 }
 
-
 /**
  * Fetch the stored SCRAM keys for the given JID, if available.
  *
@@ -437,15 +418,14 @@ export async function attemptNonPreboundSession (credentials, automatic) {
  * @returns {Promise<Model>} A promise which resolves once we've fetched the previously
  *  used login keys.
  */
-export async function savedLoginInfo (jid) {
+export async function savedLoginInfo(jid) {
     const id = `converse.scram-keys-${Strophe.getBareJidFromJid(jid)}`;
     const login_info = new Model({ id });
-    initStorage(login_info, id, 'persistent');
-    await new Promise(f => login_info.fetch({'success': f, 'error': f}));
+    initStorage(login_info, id, "persistent");
+    await new Promise((f) => login_info.fetch({ "success": f, "error": f }));
     return login_info;
 }
 
-
 /**
  * @param {Object} [credentials]
  * @param {string} credentials.password
@@ -453,28 +433,31 @@ export async function savedLoginInfo (jid) {
  * @param {string} credentials.password.ck
  * @returns {Promise<void>}
  */
-async function connect (credentials) {
+async function connect(credentials) {
     const { api } = _converse;
-    const jid = _converse.session.get('jid');
+    const jid = _converse.session.get("jid");
     const connection = api.connection.get();
     if ([ANONYMOUS, EXTERNAL].includes(api.settings.get("authentication"))) {
         if (!jid) {
-            throw new Error("Config Error: when using anonymous login " +
-                "you need to provide the server's domain via the 'jid' option. " +
-                "Either when calling converse.initialize, or when calling " +
-                "_converse.api.user.login.");
+            throw new Error(
+                "Config Error: when using anonymous login " +
+                    "you need to provide the server's domain via the 'jid' option. " +
+                    "Either when calling converse.initialize, or when calling " +
+                    "_converse.api.user.login."
+            );
         }
         if (!connection.reconnecting) {
             connection.reset();
         }
         connection.connect(jid.toLowerCase());
-
     } else if (api.settings.get("authentication") === LOGIN) {
         const password = credentials?.password ?? (connection?.pass || api.settings.get("password"));
         if (!password) {
             if (api.settings.get("auto_login")) {
-                throw new Error("autoLogin: If you use auto_login and "+
-                    "authentication='login' then you also need to provide a password.");
+                throw new Error(
+                    "autoLogin: If you use auto_login and " +
+                        "authentication='login' then you also need to provide a password."
+                );
             }
             connection.setDisconnectionCause(Strophe.Status.AUTHFAIL, undefined, true);
             api.connection.disconnect();
@@ -487,12 +470,7 @@ async function connect (credentials) {
 
         let callback;
         // Save the SCRAM data if we're not already logged in with SCRAM
-        if (
-            _converse.state.config.get('trusted') &&
-            jid &&
-            api.settings.get("reuse_scram_keys") &&
-            !password?.ck
-        ) {
+        if (_converse.state.config.get("trusted") && jid && api.settings.get("reuse_scram_keys") && !password?.ck) {
             // Store scram keys in scram storage
             const login_info = await savedLoginInfo(jid);
             callback =
@@ -509,3 +487,16 @@ async function connect (credentials) {
         connection.connect(jid, password, callback);
     }
 }
+
+/**
+ * @param {Model} model
+ * @param {Object} attributes
+ * @param {Object} options
+ */
+export function safeSave(model, attributes, options) {
+    if (isPersistableModel(model)) {
+        model.save(attributes, options);
+    } else {
+        model.set(attributes, options);
+    }
+}

+ 9 - 1
src/headless/utils/object.js

@@ -44,6 +44,14 @@ export function isUndefined(x) {
  * @param {unknown} o - The value to check.
  * @returns {boolean} True if the value is an Error
  */
-export function isErrorObject (o) {
+export function isErrorObject(o) {
     return o instanceof Error;
 }
+
+/**
+ * @param {import('@converse/skeletor').Model} model
+ * @returns {boolean}
+ */
+export function isPersistableModel(model) {
+    return model.collection && model.collection.browserStorage;
+}