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.cacheData before reassembly
  • Service layer: Stores LIFF access tokens in LiffService.liffTokenCache to avoid repeated authentication
  • Request layer: Caches HTTP responses via RequestClient.requestCore for 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 BaseStorage to replace MemoryStorage with IndexedDB or other persistent backends for cross-session caching.
  • Wrap API calls with a TTL-based memoization helper targeting RequestClient.requestCore to reduce redundant network traffic.
  • Extend LiffService token caching by delegating liffTokenCache to persistent storage, eliminating extra authentication round-trips.
  • Leverage Conn.cacheData for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →