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:
-
Talk Events Loop: When
talk: trueis set, the client iterates overpolling.listenTalkEvents(). This method (located inpackages/linejs/base/polling/mod.tslines 80-84) renews theopStream, ensures the legacy pusher is active, and returnsopStream.streamas aReadableStream<Operation>. -
Square Events Loop: When
square: trueis set, the client usespolling.listenSquareEvents()to consumesqStream, yieldingSquareEventobjects 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 rawLINETypes.Operationobjects for all Talk operations"message": Emits fully-decryptedTalkMessageobjects for chat messages"square:event": EmitsLINETypes.SquareEventobjects for OpenChat activities"square:message": EmitsSquareMessageobjects 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()andsqStream.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
*_listenTalkEventsand*_listenSquareEventsgenerators 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: AReadableStreamyieldingOperationobjects for Talk eventssqStream: AReadableStreamyieldingSquareEventobjects 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 inpackages/linejs/client/client.ts, which orchestrates the entire pipeline. - Polling management is handled by the
Pollingclass inpackages/linejs/base/polling/mod.ts, which renews push streams and maintains the legacy pusher connection. - Dual stream architecture processes Talk events via
opStreamand Square (OpenChat) events viasqStream, both provided byBaseClient.push. - E2EE decryption occurs automatically in
packages/linejs/base/e2ee/mod.tsbefore message events are emitted, ensuring plaintext content inTalkMessageobjects. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →