How to Implement Custom Storage Backends in LINEJS: A Complete Guide
LINEJS abstracts all persistent data operations behind the BaseStorage interface, allowing you to implement custom storage backends by extending the abstract class and passing your implementation to the BaseClient constructor via the storage option.
LINEJS is a powerful open-source library for building LINE messenger clients. When you implement custom storage backends in LINEJS, you gain full control over how authentication tokens, chat state, and request counters persist across sessions. This guide walks you through the BaseStorage contract, built-in reference implementations, and how to integrate your own storage solution.
Understanding the BaseStorage Contract
All storage implementations in LINEJS must satisfy the abstract contract defined in packages/linejs/base/storage/base.ts. The BaseStorage class defines five required methods that handle every persistent operation:
export abstract class BaseStorage {
abstract set(key: Storage["Key"], value: Storage["Value"]): Promise<void>;
abstract get(key: Storage["Key"]): Promise<Storage["Value"] | undefined>;
abstract delete(key: Storage["Key"]): Promise<void>;
abstract clear(): Promise<void>;
abstract migrate(storage: BaseStorage): Promise<void>;
}
The Storage type interface defines the expected key and value shapes:
export interface Storage {
Key: string;
Value: string | number | boolean | null | Record<string | number, LooseType>;
}
Every method returns a Promise, ensuring that asynchronous operations like network requests or disk I/O do not block the client.
Built-in Storage Implementations
LINEJS ships with two reference implementations that demonstrate the contract in action.
MemoryStorage
MemoryStorage provides volatile, in-memory storage using a native Map. This implementation is ideal for testing or ephemeral sessions where persistence is not required.
Source: packages/linejs/base/storage/memory.ts
FileStorage
FileStorage persists data as JSON on the local filesystem. This is the recommended default for Node.js applications that require state to survive process restarts.
Source: packages/linejs/base/storage/file.ts
import { BaseClient } from "@evex/linejs/client";
import { FileStorage } from "@evex/linejs/storage";
const client = new BaseClient({
device: "ANDROID",
storage: new FileStorage("./my-linejs-storage.json"),
});
How to Create a Custom Storage Backend
To implement custom storage backends in LINEJS, you need to create a class that satisfies the BaseStorage interface. The contract is deliberately minimal, allowing integration with IndexedDB, WebSQL, localForage, Redis, or any remote key-value service.
Implementing the Core Methods
Your custom class must implement set, get, delete, and clear. Here is a minimal in-memory-like implementation suitable for testing:
import type { BaseStorage, Storage } from "@evex/linejs/storage";
export class DummyStorage implements BaseStorage {
private map = new Map<string, Storage["Value"]>();
async set(key: string, value: Storage["Value"]): Promise<void> {
this.map.set(key, value);
}
async get(key: string): Promise<Storage["Value"] | undefined> {
return this.map.get(key);
}
async delete(key: string): Promise<void> {
this.map.delete(key);
}
async clear(): Promise<void> {
this.map.clear();
}
async migrate(to: BaseStorage): Promise<void> {
for (const [k, v] of this.map) {
await to.set(k, v);
}
}
}
Handling Data Migration
The migrate method is critical for production usage. When users switch storage types (e.g., from MemoryStorage to FileStorage), LINEJS invokes migrate on the old storage instance, passing the new storage as an argument. Your implementation should iterate over all stored entries and forward them to the target storage.
Real-World Example: IndexedDB Storage
For browser environments, IndexedDB provides durable, structured storage. The LINEJS repository includes a complete reference implementation in example/storage/indexedDB.ts that demonstrates transaction handling and cursor iteration:
export class IndexedDBStorage implements BaseStorage {
// ... constructor & internal helpers ...
public async set(key: string, value: any): Promise<void> {
const db = await this.open();
const tx = db.transaction(this.storeName, "readwrite");
await successToPromise(tx.objectStore(this.storeName).put({ key, value }));
await completeToPromise(tx);
}
public async get(key: string): Promise<any | undefined> {
const db = await this.open();
const tx = db.transaction(this.storeName);
const result = await successToPromise(tx.objectStore(this.storeName).get(key));
await completeToPromise(tx);
return result?.value;
}
public async delete(key: string): Promise<void> {
const db = await this.open();
const tx = db.transaction(this.storeName, "readwrite");
await successToPromise(tx.objectStore(this.storeName).delete(key));
await completeToPromise(tx);
}
public async clear(): Promise<void> {
const db = await this.open();
const version = db.version;
db.close();
const req = indexedDB.open(this.dbName, version + 1);
// onupgradeneeded recreates the object store – effectively clearing it
await successToPromise(req);
}
public async migrate(storage: BaseStorage): Promise<void> {
const db = await this.open();
const tx = db.transaction(this.storeName, "readwrite");
const cursor = tx.objectStore(this.storeName).openCursor();
// iterate cursor, copy each item into `storage`
}
}
This implementation handles IndexedDB’s asynchronous transaction model while satisfying the BaseStorage contract.
Integrating Custom Storage with BaseClient
Once your custom storage class is complete, instantiate it and pass it to the BaseClient constructor via the ClientInit.storage option. The client constructor is defined in packages/linejs/base/core/mod.ts:
import { BaseClient } from "@evex/linejs/client";
import { IndexedDBStorage } from "./my-storage/indexeddb.ts";
const client = new BaseClient({
device: "DESKTOPWIN",
storage: new IndexedDBStorage(), // ← custom backend
});
All internal modules—authentication, request throttling, end-to-end encryption state, and chat history—will now use your custom storage implementation for persistence.
Summary
- BaseStorage is the abstract contract (
packages/linejs/base/storage/base.ts) that definesset,get,delete,clear, andmigratemethods. - MemoryStorage and FileStorage are built-in reference implementations for volatile and Node.js filesystem persistence.
- To implement custom storage backends in LINEJS, create a class that satisfies the
BaseStorageinterface and handles theStoragekey/value types. - The migrate method enables seamless data transfer when switching storage providers.
- Pass your custom instance to
BaseClientvia thestorageoption inClientInit.
Frequently Asked Questions
What methods must a custom storage backend implement?
A custom storage backend must implement five abstract methods defined in BaseStorage: set, get, delete, clear, and migrate. Each method must return a Promise and handle the Storage["Key"] (string) and Storage["Value"] (string, number, boolean, null, or Record) types specified in packages/linejs/base/storage/base.ts.
How do I migrate data from one storage backend to another?
LINEJS handles migration automatically when you switch storage implementations. The client calls migrate on the old storage instance, passing the new storage as an argument. Your migrate implementation should iterate over all existing key/value pairs and call await to.set(key, value) for each entry, effectively copying the dataset to the new backend.
Can I use browser-based storage like IndexedDB or localStorage?
Yes. The BaseStorage contract is environment-agnostic, allowing you to wrap any browser storage API. The repository includes a complete IndexedDB example in example/storage/indexedDB.ts that demonstrates transaction handling, cursor iteration for migrate, and proper async/await patterns compatible with the LINEJS client.
Where do I pass my custom storage instance when initializing the client?
Pass your storage instance via the storage property in the ClientInit options object when constructing BaseClient. The constructor is located in packages/linejs/base/core/mod.ts and accepts an object conforming to the ClientInit interface, where storage must be an instance of a class implementing BaseStorage.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →