# # Customize StorageDriver

This section will introduce how to customize a StorageDriver, along with related API information and example code.

## # Overview

StorageDriver is an interface used for storing and managing data. It is concerned with how data should be stored, deleted, and partitioned. By customizing a StorageDriver, you can implement operations such as reading, writing, and deleting data, and also listen for data changes.

## # API Reference

### # `StorageContext` Class

    class StorageContext {
        public id: string;
        public feature: string;
    }

The `StorageContext` class represents the storage context used to identify stored data and features.

### # `PDFViewerStorageContext` Class

    class PDFViewerStorageContext extends StorageContext {
        public pdfViewer: PDFViewer;
    }

The `PDFViewerStorageContext` class extends the `StorageContext` class and represents the storage context of a `PDFViewer`. It contains an instance of `PDFViewer`, which allows us to access information such as the current document and current page. By appropriately combining this information, data can be stored in partitioned manner.

### # `PDFViewer` Interface

    interface PDFViewer {
        getInstanceId(): string;
    }

Starting from version 9.1.0, the `PDFViewer` interface has added a method called `getInstanceId()`, which is used to retrieve the instance ID of a `PDFViewer`. When there are multiple instances of `PDFViewer`, it is important to ensure that their IDs are unique in order to partition the data storage for different `PDFViewer` instances.

### # `PDFViewerConstructor` Interface

    interface PDFViewerConstructor {
        new(options: {
            instanceId: string,
            customs: {
                storageDriver: StorageDriver;
            }
        }): PDFViewer;
    }

The `PDFViewerConstructor` interface describes the constructor information for creating a `PDFViewer` instance. Starting from version 9.1.0, this constructor adds an `options` object containing the properties `instanceId` and `customs` as parameters. The `customs` property includes a `StorageDriver` instance used for storing and managing data. By passing a custom `StorageDriver` instance via `custom.storageDriver` to `PDFViewer`, you can achieve custom storage functionality.

### # `StorageDriver` Interface

    interface StorageDriver {
        public getAll(context: StorageContext): Promise<Record<string, any>>;
        public get<T>(context: StorageContext, key: string): Promise<T | null>;
        public set<T>(context: StorageContext, key: string, value: T): Promise<void>;
        public removeAll(context: StorageContext): Promise<void>;
        public remove(context: StorageContext, key: string): Promise<void>;
        public onChange<T>(callback: (event: StorageDriverChangeEvent<T>) => void): Function;
        public onRemove(callback: (event: StorageDriverRemoveEvent) => void): Function;
    }

The `StorageDriver` is an interface that defines a set of methods for storing and managing data. You can customize `StorageDriver` by implementing these methods.

- `getAll(context: StorageContext): Promise<Record<string, any>>`: Retrieves all data in the specified context.
- `get<T>(context: StorageContext, key: string): Promise<T | null>`: Retrieves the data with the specified key in the specified context.
- `set<T>(context: StorageContext, key: string, value: T): Promise<void>`: Sets the data with the specified key in the specified context.
- `removeAll(context: StorageContext): Promise<void>`: Removes all data in the specified context.
- `remove(context: StorageContext, key: string): Promise<void>`: Removes the data with the specified key in the specified context.
- `onChange<T>(callback: (event: StorageDriverChangeEvent<T>) => void): Function`: Registers a callback function to listen for data change events.
- `onRemove(callback: (event: StorageDriverRemoveEvent) => void): Function`: Registers a callback function to listen for data removal events.

### # `StorageDriverChangeEvent` Interface

    interface StorageDriverChangeEvent<T> {
        public context: StorageContext;
        public key: string;
        public oldValue: T;
        public newValue: T;
    }

The `StorageDriverChangeEvent` interface represents an event object for data changes. It contains information such as the context, key, old value, and new value. After a data change occurs, callback function can receive this object.

### # `StorageDriverRemoveEvent` Interface

    interface StorageDriverRemoveEvent {
        public context: StorageContext;
        public key: string;
    }

The `StorageDriverRemoveEvent` class represents an event for data removal. It contains information such as the context and key. After a data removal event occurs, callback function can receive this object.

## # Customize `StorageDriver`

To customize a `StorageDriver`, you need to create a subclass that implements the `StorageDriver` interface and implement its methods.

Following is an example of a custom `StorageDriver` based on `sessionStorage`
(opens new window) implementation:

    class MyStorageDriver extends StorageDriver {
        // Use a third-party library, https://www.npmjs.com/package/eventemitter3, to implement event distribution
        private readonly eventEmitter = new EventEmitter();
        // Generate a storage space name based on the context information, primarily for partitioning data
        private getSpace(context: StorageContext): string {
            const instanceId = context.id;
            return [instanceId, context.feature].join('.');
        }
        async getAll(context: StorageContext): Promise<Record<string, any>> {
            const space = this.getSpace(context);
            const keys = this.getSpaceKeys(context);
            return keys.reduce((result, completeKey) => {
                const key = completeKey.slice(space.length + 1);
                const rawData = sessionStorage.getItem(completeKey);
                result[key] = rawData ? JSON.parse(rawData) : null;
                return result;
            }, {});
        }
        async get<T>(context: StorageContext, key: string): Promise<T | null> {
            const storageKey = this.generateUniqueKey(context, key);
            const valueStr = sessionStorage.getItem(storageKey);
            if (valueStr) {
                return JSON.parse(valueStr) as T;
            } else {
                return null as T;
            }
        }
        async set<T>(context: StorageContext, key: string, value: T): Promise<void> {
            const storageKey = this.generateUniqueKey(context, key);
            const oldValueJSON = sessionStorage.getItem(storageKey);
            const oldValue = oldValueJSON ? JSON.parse(oldValueJSON) : undefined;
            const newValue = JSON.stringify(value);
            if (oldValueJSON === newValue) {
                return;
            }
            sessionStorage.setItem(storageKey, newValue);
            this.emitChangeEvent({
                context,
                key,
                oldValue,
                newValue: value
            });
        }
        async removeAll(context: StorageContext): Promise<void> {
            const keys = this.getSpaceKeys(context);
            keys.forEach(key => {
                const oldValue = sessionStorage.getItem(key);
                sessionStorage.removeItem(key);
                const newValue = sessionStorage.getItem(key);
                if (newValue !== oldValue) {
                    this.emitRemoveEvent({
                        context,
                        key
                    } as StorageDriverRemoveEvent);
                }
            });
        }
        async remove(context: StorageContext, key: string): Promise<void> {
            const storageKey = this.generateUniqueKey(context, key);
            const oldValue = sessionStorage.getItem(storageKey);
            sessionStorage.removeItem(storageKey);
            const newValue =

     sessionStorage.getItem(storageKey);
            if (newValue !== oldValue) {
                this.emitRemoveEvent({
                    context,
                    key
                });
            }
        }
        onChange<T>(callback: (event: StorageDriverChangeEvent<T>) => void): () => void {
            return this.addEventListener<T>('change', callback);
        }
        onRemove(callback: (event: StorageDriverRemoveEvent) => void): () => void {
            return this.addEventListener('remove', callback);
        }

        private addEventListener<T>(event: 'change' | 'remove', callback: (event: any) => void) {
            const listener = e => {
                callback(e);
            };
            this.eventEmitter.addListener(event, listener);
            return () => {
                this.eventEmitter.removeListener(event, listener);
            };
        }
        // Generate a unique index based on the namespace and key
        private generateUniqueKey(context: StorageContext, key: string) {
            const space = this.getSpace(context);
            return [space, key].join('.');
        }
        // Get all key-value pairs stored under the namespace
        private getSpaceKeys(context: StorageContext): string[] {
            const space = this.getSpace(context);
            const prefix = space + '.';
            return Array(sessionStorage.length)
            .fill(0)
            .map((_, index) => sessionStorage.key(index))
            .filter(it => !!it)
            .filter(it => it!.indexOf(prefix) === 0) as string[];
        }
        // Trigger a data change event when data changes
        private emitChangeEvent<T>(event: StorageDriverChangeEvent<T>) {
            this.eventEmitter.emit('change', event);
        }
        // Trigger a data removal event when data is deleted
        private emitRemoveEvent<T>(event: StorageDriverRemoveEvent) {
            this.eventEmitter.emit('remove', event);
        }
    }

You can customize `StorageDriver` according to your specific needs. During the implementation process, you can use techniques such as asynchronous operations and database queries to achieve data storage and management.

## # Use Custom `StorageDriver`

To use a custom `StorageDriver`, you need to create a `PDFViewer` instance and pass the custom `StorageDriver` as a parameter to the constructor.

Following is an example of using a custom `StorageDriver`:

    const storageDriver = new MyStorageDriver();

    const pdfViewer = new PDFViewer({
        instanceId: 'pdf-viewer-1',
        customs: {
            storageDriver: storageDriver
        }
    });

When creating a `PDFViewer` instance, pass the custom `StorageDriver` to the `customs` property. PDFViewer will use this `StorageDriver` instance to store and manage data.