How the LINEJS Client Listens to Messages and Events: A Deep Dive into the Polling Architecture

The LINEJS Client listens to messages and events by creating a Polling instance that manages push streams for Talk (chat) and Square (OpenChat) operations, decrypts E2EE payloads, and emits strongly-typed events through a TypedEventEmitter.

The evex-dev/linejs library provides a high-level Client class that abstracts the complexity of LINE's real-time messaging protocol. When you invoke the listen() method, the client initializes a sophisticated pipeline that handles push connections, end-to-end encryption (E2EE) decryption, and event emission. This article examines the internal mechanics of how the LINEJS Client listens to messages and events, tracing the flow from the initial method call through to the final typed event emission.

Architecture Overview

The listening architecture in LINEJS is built on a layered design that separates transport concerns from high-level event handling. At the core lies the BaseClient, which manages the underlying push connection to LINE's servers. The Client class in packages/linejs/client/client.ts acts as a consumer-facing wrapper that orchestrates the Polling mechanism, E2EE decryption, and event emission.

The system utilizes two primary push streams provided by BaseClient.push:

  • opStream – Handles Talk operations (personal and group chat messages)
  • sqStream – Handles Square events (OpenChat notifications and activities)

These streams are managed by the Polling class in packages/linejs/base/polling/mod.ts, which ensures stream renewal and connection persistence through the legacy LINE push service.

The Listening Flow

When you call client.listen(), the Client executes a precise sequence of operations to establish the message listening pipeline.

Initializing the Polling Instance

The process begins with the creation of a Polling instance via this.base.createPolling(). This method, defined in packages/linejs/base/core/mod.ts, instantiates a new Polling object that holds synchronization state and manages the push connection lifecycle.

The Polling class is responsible for:

  • Maintaining the push connection to LINE's servers
  • Renewing streams when connections drop
  • Managing the legacy pusher initialization via initLegyPusher()

Starting Talk and Square Event Loops

Once the Polling instance is ready, client.listen() optionally starts two asynchronous loops based on the provided options:

  1. Talk Events Loop: When talk: true is set, the client iterates over polling.listenTalkEvents(). This method (located in packages/linejs/base/polling/mod.ts lines 80-84) renews the opStream, ensures the legacy pusher is active, and returns opStream.stream as a ReadableStream<Operation>.

  2. Square Events Loop: When square: true is set, the client uses polling.listenSquareEvents() to consume sqStream, yielding SquareEvent objects for OpenChat notifications.

Both loops respect the AbortSignal provided in the listen options, allowing for graceful shutdown when the signal is aborted.

Decrypting E2EE Messages

For Talk events, the client filters for message operations specifically. When the event type is SEND_MESSAGE or RECEIVE_MESSAGE, the raw operation requires decryption before emission.

The client calls this.base.e2ee.decryptE2EEMessage(event.message) from packages/linejs/base/e2ee/mod.ts. This method handles the end-to-end encryption protocol, ensuring that the resulting TalkMessage object contains plaintext fields such as text, stickerId, and other content metadata.

Emitting Typed Events

After processing, the client emits events through the TypedEventEmitter pattern defined in packages/linejs/base/core/typed-event-emitter/index.ts. The Client class declares specific event types:

  • "event": Emits raw LINETypes.Operation objects for all Talk operations
  • "message": Emits fully-decrypted TalkMessage objects for chat messages
  • "square:event": Emits LINETypes.SquareEvent objects for OpenChat activities
  • "square:message": Emits SquareMessage objects for OpenChat notifications

This type-safe emission allows consumers to listen to specific event types with full TypeScript support and automatic decryption handling.

Key Components Deep Dive

Understanding the internal components reveals how LINEJS maintains stable real-time connections.

Polling Class (packages/linejs/base/polling/mod.ts)

The Polling class manages the lifecycle of push connections. Its primary responsibilities include:

  • Stream Renewal: Methods like opStream.renew() and sqStream.renew() recreate HTTP/2 or WebSocket connections when the underlying transport fails.
  • Legacy Pusher Initialization: The initLegyPusher() method opens the legacy LINE push service, registering target channels (listenTarget = [3, 8]) to maintain connection persistence.
  • Deprecated Generators: The class includes *_listenTalkEvents and *_listenSquareEvents generators that represent the original HTTP-polling fallback mechanism, though the current implementation prefers direct push streams.

Push Connection Management

The BaseClient.push property provides the low-level transport layer:

  • opStream: A ReadableStream yielding Operation objects for Talk events
  • sqStream: A ReadableStream yielding SquareEvent objects for OpenChat

These streams abstract the underlying protocol complexity, whether using HTTP/2 streams or WebSocket connections, providing a consistent async iterator interface for the Polling class.

TypedEventEmitter (packages/linejs/base/core/typed-event-emitter/index.ts)

This generic class provides type-safe event handling. The Client extends this emitter with a specific ClientEvents type map:

export type ClientEvents = {
  message: (msg: TalkMessage) => void;
  event: (op: LINETypes.Operation) => void;
  "square:message": (msg: SquareMessage) => void;
  "square:event": (ev: LINETypes.SquareEvent) => void;
};

This architecture ensures that TypeScript users receive full IntelliSense and type checking when registering event listeners, eliminating runtime errors from incorrect event names or handler signatures.

Practical Implementation Examples

Implementing message listening requires understanding the event lifecycle and proper resource management.

Basic Message Listener Setup

To start listening for personal and group chat messages, instantiate the Client and register the "message" event listener:

import { Client } from "@evex/linejs/client";

const client = new Client(base); // `base` is an authenticated BaseClient

client.on("message", (msg) => {
  console.log("🗨️ Talk message:", msg.text);
});

// Start listening with default options (talk: true, square: true)
client.listen({
  talk: true,
  square: true,
});

The msg object is a fully-decrypted TalkMessage instance defined in packages/linejs/client/features/message/mod.ts, containing plaintext fields like text, stickerId, and sender metadata.

Handling Square (OpenChat) Events

For OpenChat notifications, listen to the "square:message" and "square:event" events:

client.on("square:message", (msg) => {
  console.log("🔲 Square notification:", msg.text);
});

client.on("square:event", (ev) => {
  console.log("Raw square event type:", ev.type);
});

These events originate from the sqStream processed in packages/linejs/base/polling/mod.ts and are filtered for NOTIFICATION_MESSAGE types before being wrapped in SquareMessage objects.

AbortController for Graceful Shutdown

To properly stop listening and close connections, provide an AbortSignal:

const controller = new AbortController();

client.listen({
  talk: true,
  square: false,
  signal: controller.signal,
});

// Gracefully stop after 5 minutes
setTimeout(() => controller.abort(), 5 * 60_000);

When aborted, the signal triggers cleanup in client.listen() that closes both opStream and sqStream connections, ensuring no resource leaks occur.

Summary

  • The LINEJS Client listens to messages through the listen() method in packages/linejs/client/client.ts, which orchestrates the entire pipeline.
  • Polling management is handled by the Polling class in packages/linejs/base/polling/mod.ts, which renews push streams and maintains the legacy pusher connection.
  • Dual stream architecture processes Talk events via opStream and Square (OpenChat) events via sqStream, both provided by BaseClient.push.
  • E2EE decryption occurs automatically in packages/linejs/base/e2ee/mod.ts before message events are emitted, ensuring plaintext content in TalkMessage objects.
  • Type-safe events are emitted through TypedEventEmitter (packages/linejs/base/core/typed-event-emitter/index.ts) with strongly-typed handlers for "message", "square:message", and raw operation events.

Frequently Asked Questions

How does the LINEJS Client handle connection drops during listening?

The Polling class in packages/linejs/base/polling/mod.ts automatically handles connection drops by calling opStream.renew() and sqStream.renew() methods. These methods recreate the underlying HTTP/2 or WebSocket streams when the connection fails. Additionally, initLegyPusher() maintains the legacy LINE push service registration with listenTarget = [3, 8] to ensure the connection remains alive and automatically reconnects when network issues occur.

What is the difference between the "event" and "message" events in LINEJS?

The "event" emitter in packages/linejs/client/client.ts emits raw LINETypes.Operation objects for every Talk operation received through the opStream, providing unfiltered access to all protocol-level events. In contrast, the "message" event emits fully processed TalkMessage instances that have been filtered specifically for SEND_MESSAGE and RECEIVE_MESSAGE operation types, and have passed through E2EE decryption in packages/linejs/base/e2ee/mod.ts to ensure plaintext content is available.

Can I listen to only Talk messages without Square (OpenChat) events?

Yes, the listen() method accepts an options object that allows selective enabling of event streams. By passing { talk: true, square: false } to client.listen(), the Client will only initialize the Talk event loop via polling.listenTalkEvents() while skipping the Square event loop. This reduces resource usage and network traffic if you do not need OpenChat notifications, and the opStream will be the only active push connection maintained by the Polling instance.

How does LINEJS decrypt end-to-end encrypted messages automatically?

Before emitting a "message" event, the Client checks if the operation type is SEND_MESSAGE or RECEIVE_MESSAGE and then calls this.base.e2ee.decryptE2EEMessage(event.message) from packages/linejs/base/e2ee/mod.ts. This method handles the cryptographic decryption of the E2EE payload using the client's encryption keys, transforming the raw encrypted message into a plaintext format. The decrypted content is then wrapped in a TalkMessage object (defined in packages/linejs/client/features/message/mod.ts) and emitted via the "message" event with accessible fields like text and stickerId.

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 →