How to Implement Caching Strategies for API Responses in LINEJS
Implement caching in LINEJS by implementing the BaseStorage interface for persistent storage, wrapping API calls with TTL-based memoization helpers, and extending the built-in LiffService token cache to survive page reloads.
LINEJS provides a modular architecture for interacting with the LINE platform, but frequent API calls can trigger rate limits and degrade performance. By leveraging the library’s pluggable storage system and built-in buffering mechanisms, you can implement robust caching strategies for API responses in LINEJS that reduce latency and preserve bandwidth across sessions.
Understanding the Caching Layers
LINEJS operates through three distinct layers where caching improves efficiency:
- Push connection layer: Buffers fragmented TCP packets in
Conn.cacheDatabefore reassembly - Service layer: Stores LIFF access tokens in
LiffService.liffTokenCacheto avoid repeated authentication - Request layer: Caches HTTP responses via
RequestClient.requestCorefor idempotent operations like profile fetching
Each layer serves a different purpose, from low-level packet integrity to high-level API optimization.
Choosing a Persistent Storage Backend
By default, LINEJS uses MemoryStorage, which loses data on page reload. For cross-session caching, implement the BaseStorage interface backed by IndexedDB or another persistent store.
In packages/linejs/base/storage/base.ts, the abstract contract requires four methods: get, set, delete, and clear. Here is a complete IndexedDB implementation:
// src/custom-storage.ts
import { BaseStorage, type Storage } from "@evex/linejs/storage";
export class IndexedDBStorage extends BaseStorage {
private db: IDBDatabase | null = null;
constructor() {
super();
const request = indexedDB.open("linejs-cache", 1);
request.onupgradeneeded = () => {
request.result.createObjectStore("kv");
};
request.onsuccess = () => (this.db = request.result);
}
async set(key: Storage["Key"], value: Storage["Value"]): Promise<void> {
await this.ensureDB();
const tx = this.db!.transaction("kv", "readwrite");
tx.objectStore("kv").put(value, key);
await tx.complete;
}
async get(key: Storage["Key"]): Promise<Storage["Value"] | undefined> {
await this.ensureDB();
const tx = this.db!.transaction("kv", "readonly");
const result = await tx.objectStore("kv").get(key);
await tx.complete;
return result ?? undefined;
}
async delete(key: Storage["Key"]): Promise<void> {
await this.ensureDB();
const tx = this.db!.transaction("kv", "readwrite");
tx.objectStore("kv").delete(key);
await tx.complete;
}
async clear(): Promise<void> {
await this.ensureDB();
const tx = this.db!.transaction("kv", "readwrite");
tx.objectStore("kv").clear();
await tx.complete;
}
private async ensureDB(): Promise<void> {
if (!this.db) {
await new Promise((resolve) => setTimeout(resolve, 100));
return this.ensureDB();
}
}
}
Inject this storage into BaseClient at initialization. In packages/linejs/base/core/mod.ts, the constructor accepts a storage property:
import { Client } from "@evex/linejs/client";
import { IndexedDBStorage } from "./custom-storage";
const client = new Client({
device: "iOS",
storage: new IndexedDBStorage(), // Persists across reloads
});
Implementing TTL-Based API Response Caching
For general API responses, create a memoization wrapper that stores JSON payloads with expiration timestamps. This strategy targets the RequestClient.requestCore method found in packages/linejs/base/request/mod.ts.
The helper checks storage before executing the network request:
// src/cache-helper.ts
import type { BaseClient } from "@evex/linejs/client";
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
export async function cachedRequest<T = unknown>(
client: BaseClient,
cacheKey: string,
request: () => Promise<T>,
ttlMs: number = DEFAULT_TTL_MS,
): Promise<T> {
const now = Date.now();
// Check for valid cached entry
const raw = await client.storage.get(cacheKey) as
| { expires: number; value: T }
| undefined;
if (raw && raw.expires > now) {
client.log("cache-hit", { key: cacheKey });
return raw.value;
}
// Execute request and store with new expiry
const value = await request();
await client.storage.set(cacheKey, {
expires: now + ttlMs,
value,
});
client.log("cache-miss", { key: cacheKey });
return value;
}
Use this helper to cache expensive calls like profile retrieval:
import { cachedRequest } from "./cache-helper";
async function getMyProfile(client: BaseClient) {
return cachedRequest(
client,
"profile:self",
() => client.request.request(
[[12, 1, []]], // Empty args for getProfile()
"getProfile",
4, // ProtocolKey for Profile API
true,
"/S3"
),
60000 // 1 minute TTL for fresh profile data
);
}
Caching LIFF Access Tokens
The LiffService class maintains an in-memory map called liffTokenCache defined in packages/linejs/base/service/liff/mod.ts. To make tokens survive page reloads, delegate the cache to your persistent storage implementation.
Create a dedicated cache manager:
// src/liff-cache.ts
import type { BaseClient } from "@evex/linejs/client";
export class LiffCache {
constructor(private client: BaseClient) {}
async get(to: string, liffId: string): Promise<string | undefined> {
const key = `liff-token:${to}:${liffId}`;
return (await this.client.storage.get(key)) as string | undefined;
}
async set(to: string, liffId: string, token: string): Promise<void> {
const key = `liff-token:${to}:${liffId}`;
await this.client.storage.set(key, token);
}
}
Integrate this into your LIFF workflow to bypass redundant issueLiffView calls:
// Inside LiffService.sendLiff modification
if (!this.liffTokenCache[to] || forceIssue) {
token = await this.getLiffToken({ chatMid: to, liffId: this.liffId });
await this.liffCache.set(to, this.liffId, token); // Persist token
} else {
token = this.liffTokenCache[to];
}
Handling Push Connection Buffers
At the transport layer, Conn in packages/linejs/base/push/conn.ts uses cacheData to reassemble fragmented TCP packets. When partial frames arrive, the buffer concatenates fragments until a complete packet is ready:
// Conceptual excerpt from Conn.ts
if (this.isNotFinished) {
const concat = new Uint8Array(this.cacheData.length + data.length);
concat.set(this.cacheData);
concat.set(data, this.cacheData.length);
data = concat; // Full packet assembled
}
While you rarely modify this layer directly, understanding Conn.cacheData helps when implementing prefetch logic for media referenced in push payloads.
Complete Integration Example
Combine these strategies for a production-ready client:
import { Client } from "@evex/linejs/client";
import { IndexedDBStorage } from "./custom-storage";
import { cachedRequest } from "./cache-helper";
const client = new Client({
device: "iOS",
storage: new IndexedDBStorage(),
});
// Cache friend list for 10 minutes
async function getFriends() {
return cachedRequest(
client,
"friends:list",
() => client.request.request([], "getFriends", 3, true, "/S4"),
10 * 60 * 1000
);
}
// Send LIFF message using persistent token cache
async function notifyChat(chatMid: string, text: string) {
await client.liff.sendLiff({
to: chatMid,
messages: [{ type: "text", text }],
});
}
// Usage
await getFriends(); // Hits cache on subsequent calls
await notifyChat("u1234...", "Hello!"); // Token cached across reloads
Summary
- Implement
BaseStorageto replaceMemoryStoragewith IndexedDB or other persistent backends for cross-session caching. - Wrap API calls with a TTL-based memoization helper targeting
RequestClient.requestCoreto reduce redundant network traffic. - Extend
LiffServicetoken caching by delegatingliffTokenCacheto persistent storage, eliminating extra authentication round-trips. - Leverage
Conn.cacheDatafor understanding packet-level buffering, though modification is rarely required for API caching. - Scope cache keys per user (e.g.,
user:<mid>:profile) and invalidate entries after mutating operations to prevent stale data.
Frequently Asked Questions
How does LINEJS handle storage by default?
LINEJS uses MemoryStorage from packages/linejs/base/storage/memory.ts by default, which stores all cached data in RAM. This implementation clears automatically when the application closes or the page reloads, making it suitable only for temporary session data.
Can I use Redis or another external cache with LINEJS?
Yes. Implement the BaseStorage interface from packages/linejs/base/storage/base.ts with async methods that call your Redis client. Since BaseClient accepts any object matching the BaseStorage contract, you can inject Redis, DynamoDB, or any other external store without modifying the core library.
What is the difference between Conn.cacheData and API response caching?
Conn.cacheData in packages/linejs/base/push/conn.ts is a low-level byte buffer that reassembles fragmented TCP packets for the push connection protocol. It handles transport-layer integrity. API response caching operates at the application layer, storing parsed JSON results from RequestClient to avoid redundant HTTP requests.
How should I invalidate cached API responses after sending messages?
Delete the relevant cache keys immediately after successful mutating operations. For example, after calling sendMessage, remove entries matching chat:<id>:messages or chat:<id>:history from your storage implementation to force fresh data on the next read.
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 →