# 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.