npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@capitec/omni-state

v0.1.3

Published

Simple web app state and storage management

Downloads

116

Readme


Introduction

Omni State is a collection of utilities that makes it simple to manage the local state and data storage in web applications. The library is lightweight and comes with zero runtime dependencies, minimizing bloat to your project.

Core features of the library include:

  • ObservableProperty - Provides an observable property that enables immutable editing of state using a draft before persisting model.
  • StatefulProperty - An extension of the ObservableProperty that persists the state data to a provided store when set, e.g. local storage, session storage, or a custom store.
  • Storage decorator - A decorator that allows you to annotate any class property to persist its value to storage when set.
  • Local & Session Data Stores - Default implementation are provided to persist data to the browse local storage and session storage.
  • Custom Data Stores - Custom data stores can be created by implementing either the SyncStorage or AsyncStorage interfaces, enabling you to e.g. persist data online when a property is set.

Usage

1️⃣   Install the library in your project.

npm install @capitec/omni-state

2️⃣   Use any of the example property patterns below where you implement state management in your app. Here we provide an example using global singleton to manage the state of an app.

// e.g. my-app/AppState.ts

import { LocalStorage, ObservableProperty, SessionStorage, StatefulProperty, stateExperimental } from '@capitec/omni-state';
import { MyCustomStore } from './stores/MyCustomStore';

export type Person = {
    firstName: string;
    lastName: string;
}

export class AppState {

    // OBSERVABLE PROPERTY

    // A simple in-memory property that can be observed for changes, e.g.:
    simpleObservable = new ObservableProperty<Person>();

    // STATEFUL PROPERTY

    // An observable property that is persisted to SessionStorage when set, e.g.:
    statefulSessionObservable = new StatefulProperty({ storage: SessionStorage, key: 'statefulSessionObservable' });

    // An observable property that is persisted to LocalStorage when set, e.g.:
    statefulLocalObservable = new StatefulProperty({ storage: LocalStorage, key: 'statefulLocalObservable' });

    // An observable property that is persisted to a custom async store when set, e.g.:
    statefulCustomObservable = new StatefulProperty({ storage: MyCustomStore, key: 'statefulCustomObservable' });

    // STATE DECORATOR - using Typescript "experimentalDecorators": true

    // A simple property that is persisted to SessionStorage when set, e.g.:
    @stateExperimental({ storage: SessionStorage, key: 'decoratorSession' })
    decoratorSession?: string;

    // A simple property that is persisted to LocalStorage when set, e.g.:
    @stateExperimental({ storage: LocalStorage, key: 'decoratorSession' })
    decoratorLocal?: string;

    // A simple property that is persisted to a custom async store when set, e.g.:
    @stateExperimental({ storage: MyCustomStore, key: 'decoratorCustom' })
    decoratorCustom?: string;

    // OBSERVABLE PROPERTY + STATE DECORATOR = STATEFUL PROPERTY

    // A simple property that is persisted to SessionStorage when set, e.g.:
    @stateExperimental({ storage: SessionStorage, key: 'observableSession' })
    observableSession = new ObservableProperty<string>();

    // A simple property that is persisted to LocalStorage when set, e.g.:
    @stateExperimental({ storage: LocalStorage, key: 'observableLocal' })
    observableLocal = new ObservableProperty<string>();

    // A simple property that is persisted to a custom async store when set, e.g.:
    @stateExperimental({ storage: MyCustomStore, key: 'observableCustom' })
    observableCustom = new ObservableProperty<string>();

    /**
     * Get an instance of the shared state.
     * 
     * @returns The shared state instance.
     */
    static getInstance() {

        if (!AppState.instance) {
            AppState.instance = new SharedState();
        }

        return AppState.instance;
    }

    /**
     * Initialize App global state, read persisted settings.
     * 
     * Note: Only to be called once by the app entrypoint.
     * 
     * @returns Nothing.
     */
    async init() {

        // Ensure all state properties have been initialized from storage.
        await StateManager.allSettled;
    }
}

3️⃣   Make use of any of the below patterns to access and mutate the app state properties. Note both ObservableProperty and StatefulProperty implement the observable pattern, allowing you to .get(), .set(), and .subscribe() to the property. Decorated properties are implemented as standard properties, thus they can be get and set like any other primitive or object.

// my-app/App.ts

import { AppState } from './AppState';

class App {

    private appState: AppState;

    constructor() {

        this.appState = AppState.getInstance();
    }

    async init(): void {

        // Ensure all app state values are loaded from storage.
        await appState.init();

        // USING OBSERVABLE OR STATEFUL PROPERTIES

        // Initializing an observable (or stateful) property.
        appState.simpleObservable.set({
            firstName: 'Hello',
            lastName: 'World';
        });

        // Subscribing to value changes on an observable (or stateful) property.
        appState.simpleObservable.subscribe(value => {
            console.log(value);
        });

        // Editing specific values on an observable (or stateful) property.
        appState.simpleObservable.set(draft => {
            draft.firstName = 'Test';
        });

        // USING STATE DECORATOR PROPERTIES

        // Setting a decorated property.
        this.decoratorLocal = 'Test';

        // Getting a decorated property
        console.log(this.decoratorLocal);
    }
}

await new App().init();

4️⃣   Omni State exposes implementations for LocalStorage and SessionStorage stores. However, you can implement a custom store by creating an implementation of either the SyncStorage or AsyncStorage interfaces.

The SyncStorage interface is used to implement the LocalStorage and SessionStorage stores, while the AsyncStorage interface allows you to build a custom storage implementation that can persist data to environments that have to be contacted asynchronously, e.g. saving values to an online service.

// my-app/stores/MyCustomStore.ts

import { SyncStorage } from '@capitec/omni-state';

/**
 * Simple wrapper around the browser `localStorage` that simplifies storing values across browser sessions.
 * 
 * Values are persisted to storage as JSON strings, and can be read back as typed objects.
 */
class MyCustomStoreImpl implements SyncStorage {

    /**
     * Gets a value from storage for the given key.
     * 
     * @param key - The key under which the value is stored.
     * 
     * @returns The stored value parsed from JSON, or null if not set.
     */
    get<T>(key: string): T | undefined {

        try {

            const result = window.localStorage.getItem(key);

            console.log(`Reading Key: ${key}`, result);

            if (!result) {
                return undefined;
            }

            return JSON.parse(result) as T;

        } catch (err) {

            return undefined;
        }
    }

    /** 
     * Sets a value in storage for the given key.
     * 
     * @param key - The key under which to store the value.
     * @param value - The value to store.
     * 
     * @returns Nothing.
     */
    set(key: string, value: unknown): void {

        console.log(`Writing Key: ${key}`, value);

        window.localStorage.setItem(key, JSON.stringify(value));
    }

    /**
     * Removes a value from storage for the given key.
     * 
     * @param key - The key of the value to remove.
     * 
     * @returns Nothing.
     */
    remove(key: string): void {

        console.log(`Removing Key: ${key}`);

        window.localStorage.removeItem(key);
    }

    /**
     * Removes all values from storage.
     * 
     * @returns Nothing.
     */
    clear(): void {

        console.log(`Clearing all keys`);

        window.localStorage.clear();
    }

    /**
     * Get the name of the key at a given index.
     * 
     * @param index - The index number to get the key name for.
     * 
     * @returns The name of the key at the index.
     */
    key(index: number): string | undefined {

        return window.localStorage.key(index) || undefined;
    }

    /**
     * Finds a list of all keys in storage.
     * 
     * @returns The list of keys in storage.
     */
    keys(): string[] {

        return Object.keys(window.localStorage);
    }

    /**
     * Get the number of items in storage.
     * 
     * @returns The storage item count.
     */
    size(): number {

        return window.localStorage.length;
    }
}

export const MyCustomStore = new MyCustomStoreImpl();

5️⃣   To make use of the experimental decorators, ensure your environment configurations includes the following:

TypeScript

If using TypeScript, include this in your tsconfig.json:

{
    "experimentalDecorators": true
}

Babel

If using Babel, include this in your .babelrc:

{
    "plugins": [
        [
            "@babel/plugin-proposal-decorators",
            {
                "version": "2018-09",
                "decoratorsBeforeExport": true
            }
        ]
    ]
}

Webpack

If using Webpack, include this in your webpack.build.js:

export default {
    module: {
        rules: [
            {
                test: /\.(js|mjs|jsx|ts|tsx)$/,
                exclude: /node_modules[\\/](?!(omni-components|omni-router|omni-state)[\\/]).*/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env'],
                            plugins: [
                                ['@babel/transform-runtime'],
                                [
                                    '@babel/plugin-proposal-decorators', {
                                        version: '2018-09',
                                        decoratorsBeforeExport: true
                                    }
                                ]
                            ]
                        }
                    }
                ]
            }
        ]
    }
};

Contributors

Made possible by these fantastic people. 💖

See the CONTRIBUTING.md guide to get involved.

License

Licensed under MIT