Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Custom PersistenceProvider #3902

Merged
merged 8 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## v72.0.0-SNAPSHOT - unreleased

### ⚙️ Technical

* Added support for providing custom `PersistenceProvider` implementations to `PersistOptions`.


### ⚙️ Typescript API Adjustments

* Improved signature of `HoistBase.markPersist`.
Expand Down
24 changes: 19 additions & 5 deletions core/persist/PersistOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@
* Copyright © 2025 Extremely Heavy Industries Inc.
*/

import {DebounceSpec} from '../';
import {Class} from 'type-fest';
import {DebounceSpec, PersistenceProvider, PersistenceProviderConfig} from '../';
import type {DashViewModel} from '@xh/hoist/desktop/cmp/dash'; // Import type only
import type {ViewManagerModel} from '@xh/hoist/cmp/viewmanager'; // Import type only

/**
* Built-in Hoist PersistenceProviders.
*/
export type PersistenceProviderType =
| 'pref'
| 'localStorage'
| 'sessionStorage'
| 'dashView'
| 'viewManager'
| 'custom';

export interface PersistOptions {
/** Dot delimited path to store state. */
path?: string;
Expand All @@ -17,11 +29,13 @@ export interface PersistOptions {
debounce?: DebounceSpec;

/**
* Type of PersistenceProvider to create. If not provided, defaulted based
* on the presence of `prefKey`, `localStorageKey`, `dashViewModel`, 'viewManagerModel',
* `getData` and `setData`.
* Type of PersistenceProvider to create. Specify as one of the built-in string types,
* or a subclass of PersistenceProvider.
*
* If not provided, defaulted to one of the built-in string types based on the presence of
* `prefKey`, `localStorageKey`, `dashViewModel`, 'viewManagerModel', or `getData/setData`.
*/
type?: 'pref' | 'localStorage' | 'sessionStorage' | 'dashView' | 'viewManager' | 'custom';
type?: PersistenceProviderType | Class<PersistenceProvider, [PersistenceProviderConfig]>;

/** Predefined Hoist application Preference key used to store state. */
prefKey?: string;
Expand Down
103 changes: 48 additions & 55 deletions core/persist/PersistenceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,29 @@ import {
get,
isEmpty,
isNumber,
isString,
isUndefined,
set,
toPath
} from 'lodash';
import {IReactionDisposer, reaction} from 'mobx';
import {DebounceSpec, HoistBase, Persistable, PersistableState, XH} from '../';
import {Class} from 'type-fest';
import {DebounceSpec, HoistBase, Persistable, PersistableState} from '../';
import {
CustomProvider,
DashViewProvider,
LocalStorageProvider,
SessionStorageProvider,
PersistOptions,
PrefProvider,
SessionStorageProvider,
ViewManagerProvider
} from './';

export type PersistenceProviderConfig<S> =
| {
persistOptions: PersistOptions;
target: Persistable<S>;
owner: HoistBase;
}
| {
persistOptions: PersistOptions;
target: Persistable<S> & HoistBase;
owner?: HoistBase;
};
export type PersistenceProviderConfig<S = any> = {
persistOptions: PersistOptions;
target: Persistable<S>;
owner?: HoistBase;
};

/**
* Abstract superclass for adaptor objects used by models and components to (re)store state to and
Expand All @@ -57,7 +53,7 @@ export type PersistenceProviderConfig<S> =
* - {@link ViewManagerProvider} - persists to saved views managed by {@link ViewManagerModel}.
* - {@link CustomProvider} - API for app and components to provide their own storage mechanism.
*/
export abstract class PersistenceProvider<S> {
export abstract class PersistenceProvider<S = any> {
readonly path: string;
readonly debounce: DebounceSpec;
readonly owner: HoistBase;
Expand All @@ -79,49 +75,14 @@ export abstract class PersistenceProvider<S> {
* target without thrashing.
*/
static create<S>(cfg: PersistenceProviderConfig<S>): PersistenceProvider<S> {
cfg = {
owner: cfg.target instanceof HoistBase ? cfg.target : cfg.owner,
...cfg
};
const {target, persistOptions} = cfg;

let {type, ...rest} = persistOptions,
ret: PersistenceProvider<S>;

let ret: PersistenceProvider<S>;
try {
if (!type) {
if (rest.prefKey) type = 'pref';
if (rest.localStorageKey) type = 'localStorage';
if (rest.sessionStorageKey) type = 'sessionStorage';
if (rest.dashViewModel) type = 'dashView';
if (rest.viewManagerModel) type = 'viewManager';
if (rest.getData || rest.setData) type = 'custom';
}

switch (type) {
case 'pref':
ret = new PrefProvider(cfg);
break;
case 'localStorage':
ret = new LocalStorageProvider(cfg);
break;
case 'sessionStorage':
ret = new SessionStorageProvider(cfg);
break;
case `dashView`:
ret = new DashViewProvider(cfg);
break;
case `viewManager`:
ret = new ViewManagerProvider(cfg);
break;
case 'custom':
ret = new CustomProvider(cfg);
break;
default:
throw XH.exception(`Unknown Persistence Provider for type: ${type}`);
}
// default owner to target
cfg = {owner: cfg.target instanceof HoistBase ? cfg.target : cfg.owner, ...cfg};

ret.bindToTarget(target);
const providerClass = this.parseProviderClass<S>(cfg.persistOptions);
ret = new providerClass(cfg);
ret.bindToTarget(cfg.target);
return ret;
} catch (e) {
logError(e, cfg.owner);
Expand Down Expand Up @@ -211,7 +172,39 @@ export abstract class PersistenceProvider<S> {
}

protected writeRaw(obj: Record<typeof this.path, S>) {}

protected readRaw(): Record<typeof this.path, S> {
return null;
}

private static parseProviderClass<S>(
opts: PersistOptions
): Class<PersistenceProvider<S>, [PersistenceProviderConfig<S>]> {
// 1) Recognize shortcut form
const {type, ...rest} = opts;
if (!type) {
if (rest.prefKey) return PrefProvider;
if (rest.localStorageKey) return LocalStorageProvider;
if (rest.sessionStorageKey) return SessionStorageProvider;
if (rest.dashViewModel) return DashViewProvider;
if (rest.viewManagerModel) return ViewManagerProvider;
if (rest.getData || rest.setData) return CustomProvider;
}

// 2) Map any string to known Provider Class, or return raw class
const ret = isString(type)
? {
pref: PrefProvider,
localStorage: LocalStorageProvider,
sessionStorage: SessionStorageProvider,
dashView: DashViewProvider,
viewManager: ViewManagerProvider,
custom: CustomProvider
}[type]
: type;

throwIf(!ret, `Unknown Persistence Provider: ${type}`);

return ret;
}
}
Loading