node-opcua-packet-assembler 2.157.0 → 2.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -0
- package/dist/packet_assembler.d.ts +142 -2
- package/dist/packet_assembler.js +120 -4
- package/dist/packet_assembler.js.map +1 -1
- package/package.json +4 -4
- package/source/packet_assembler.ts +144 -5
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# node-opcua-packet-assembler
|
|
2
|
+
|
|
3
|
+
A high-performance packet assembler for reassembling fragmented data from transport layers into complete message chunks. Features **zero-copy optimization** for maximum performance.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install node-opcua-packet-assembler
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { PacketAssembler } from "node-opcua-packet-assembler";
|
|
15
|
+
|
|
16
|
+
// Create assembler
|
|
17
|
+
const assembler = new PacketAssembler({
|
|
18
|
+
readChunkFunc: (data) => ({
|
|
19
|
+
length: data.readUInt32LE(4),
|
|
20
|
+
messageHeader: {
|
|
21
|
+
msgType: data.toString("ascii", 0, 4),
|
|
22
|
+
isFinal: "F",
|
|
23
|
+
length: data.readUInt32LE(4)
|
|
24
|
+
},
|
|
25
|
+
extra: ""
|
|
26
|
+
}),
|
|
27
|
+
minimumSizeInBytes: 8,
|
|
28
|
+
maxChunkSize: 65536
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Listen for complete chunks
|
|
32
|
+
assembler.on("chunk", (chunk) => {
|
|
33
|
+
console.log("Complete chunk:", chunk.length, "bytes");
|
|
34
|
+
processMessage(chunk);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Feed data from transport
|
|
38
|
+
socket.on("data", (data) => assembler.feed(data));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Key Features
|
|
42
|
+
|
|
43
|
+
### Zero-Copy Performance
|
|
44
|
+
|
|
45
|
+
- **Single-chunk messages**: Returns buffer views without copying (fast!)
|
|
46
|
+
- **Multi-chunk messages**: Concatenates fragments safely with `Buffer.concat()`
|
|
47
|
+
- Optimized for the common case where complete messages arrive in one buffer
|
|
48
|
+
|
|
49
|
+
### Event-Driven API
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Track chunk assembly progress
|
|
53
|
+
assembler.on("startChunk", (packetInfo, partial) => {
|
|
54
|
+
console.log(`Starting chunk: ${packetInfo.length} bytes`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Process complete chunks
|
|
58
|
+
assembler.on("chunk", (chunk) => {
|
|
59
|
+
handleMessage(chunk);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Handle errors
|
|
63
|
+
assembler.on("error", (error, errorCode) => {
|
|
64
|
+
console.error("Assembly error:", error.message);
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Important: Buffer Lifetime
|
|
69
|
+
|
|
70
|
+
⚠️ **When using zero-copy buffers, YOU are responsible for buffer lifetime management.**
|
|
71
|
+
|
|
72
|
+
### ✅ Safe Usage
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
assembler.on("chunk", (chunk) => {
|
|
76
|
+
// Option 1: Process immediately
|
|
77
|
+
const value = chunk.readUInt32LE(0);
|
|
78
|
+
console.log("Value:", value);
|
|
79
|
+
|
|
80
|
+
// Option 2: Make a copy if storing
|
|
81
|
+
const copy = Buffer.from(chunk);
|
|
82
|
+
messageQueue.push(copy);
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### ❌ Unsafe Usage
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const storedChunks = [];
|
|
90
|
+
|
|
91
|
+
assembler.on("chunk", (chunk) => {
|
|
92
|
+
// UNSAFE! Transport may reuse this buffer
|
|
93
|
+
storedChunks.push(chunk);
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Rule of thumb**: If you store buffers beyond immediate processing or pass them to async handlers, create a copy with `Buffer.from(chunk)`.
|
|
98
|
+
|
|
99
|
+
## API Overview
|
|
100
|
+
|
|
101
|
+
For complete API documentation with TypeScript types and detailed examples, see the source code JSDoc comments.
|
|
102
|
+
|
|
103
|
+
### Constructor
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
new PacketAssembler(options: PacketAssemblerOptions)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Option | Type | Description |
|
|
110
|
+
| -------------------- | ------------------------------ | ----------------------- |
|
|
111
|
+
| `readChunkFunc` | `(data: Buffer) => PacketInfo` | Extract packet metadata |
|
|
112
|
+
| `minimumSizeInBytes` | `number` | Minimum header size |
|
|
113
|
+
| `maxChunkSize` | `number` | Maximum chunk size |
|
|
114
|
+
|
|
115
|
+
### Methods
|
|
116
|
+
|
|
117
|
+
- **`feed(data: Buffer)`**: Feed incoming data to the assembler
|
|
118
|
+
|
|
119
|
+
### Events
|
|
120
|
+
|
|
121
|
+
- **`"startChunk"`**: `(packetInfo, partial) => void` - New chunk detected
|
|
122
|
+
- **`"chunk"`**: `(chunk: Buffer) => void` - Complete chunk assembled
|
|
123
|
+
- **`"error"`**: `(error, errorCode) => void` - Assembly error occurred
|
|
124
|
+
|
|
125
|
+
## Common Patterns
|
|
126
|
+
|
|
127
|
+
### TCP Socket Integration
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import net from "net";
|
|
131
|
+
|
|
132
|
+
const server = net.createServer((socket) => {
|
|
133
|
+
const assembler = new PacketAssembler({
|
|
134
|
+
readChunkFunc: readHeader,
|
|
135
|
+
minimumSizeInBytes: 8,
|
|
136
|
+
maxChunkSize: 65536
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
assembler.on("chunk", handleMessage);
|
|
140
|
+
assembler.on("error", (err) => socket.destroy());
|
|
141
|
+
|
|
142
|
+
socket.on("data", (data) => assembler.feed(data));
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### With Progress Tracking
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
assembler.on("startChunk", (packetInfo) => {
|
|
150
|
+
console.log(`📦 Expecting ${packetInfo.length} bytes`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
assembler.on("chunk", (chunk) => {
|
|
154
|
+
console.log(`✅ Received ${chunk.length} bytes`);
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Error Handling
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { PacketAssemblerErrorCode } from "node-opcua-packet-assembler";
|
|
162
|
+
|
|
163
|
+
assembler.on("error", (error, errorCode) => {
|
|
164
|
+
if (errorCode === PacketAssemblerErrorCode.ChunkSizeExceeded) {
|
|
165
|
+
console.error("Chunk too large:", error.message);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Performance Tips
|
|
171
|
+
|
|
172
|
+
1. **Avoid unnecessary copies**: The assembler already optimizes for zero-copy when possible
|
|
173
|
+
2. **Set appropriate limits**: Configure `maxChunkSize` based on your protocol needs
|
|
174
|
+
3. **Process immediately**: When safe, process chunks in the event handler for best performance
|
|
175
|
+
4. **Only copy when needed**: Create copies only when storing or passing to async handlers
|
|
176
|
+
|
|
177
|
+
## TypeScript Support
|
|
178
|
+
|
|
179
|
+
Full TypeScript definitions included. All interfaces, types, and methods are fully documented with JSDoc comments in the source code.
|
|
180
|
+
|
|
181
|
+
## Testing
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npm test
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
190
|
+
|
|
191
|
+
## Related Packages
|
|
192
|
+
|
|
193
|
+
- [node-opcua](https://github.com/node-opcua/node-opcua) - Main package
|
|
194
|
+
- `node-opcua-transport` - Transport layer
|
|
195
|
+
- `node-opcua-chunkmanager` - Message chunk management
|
|
@@ -1,22 +1,51 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
+
/**
|
|
3
|
+
* Message header information extracted from packet data.
|
|
4
|
+
*/
|
|
2
5
|
export interface MessageHeader {
|
|
6
|
+
/** Message type identifier (e.g., "MSG", "OPN", "CLO") */
|
|
3
7
|
msgType: string;
|
|
8
|
+
/** Final chunk indicator ("F" for final, "C" for continuation) */
|
|
4
9
|
isFinal: string;
|
|
10
|
+
/** Total message length in bytes */
|
|
5
11
|
length: number;
|
|
6
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Packet metadata extracted from incoming data.
|
|
15
|
+
*/
|
|
7
16
|
export interface PacketInfo {
|
|
17
|
+
/** Total expected packet length in bytes */
|
|
8
18
|
length: number;
|
|
19
|
+
/** Message header information */
|
|
9
20
|
messageHeader: MessageHeader;
|
|
21
|
+
/** Additional protocol-specific metadata */
|
|
10
22
|
extra: string;
|
|
11
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Function type for extracting packet metadata from buffer data.
|
|
26
|
+
*
|
|
27
|
+
* @param data - Buffer containing at least minimumSizeInBytes of data
|
|
28
|
+
* @returns Packet metadata including expected length and headers
|
|
29
|
+
*/
|
|
12
30
|
export type ReadChunkFuncType = (data: Buffer) => PacketInfo;
|
|
31
|
+
/**
|
|
32
|
+
* Configuration options for PacketAssembler.
|
|
33
|
+
*/
|
|
13
34
|
export interface PacketAssemblerOptions {
|
|
35
|
+
/** Function to extract packet metadata from incoming data */
|
|
14
36
|
readChunkFunc: ReadChunkFuncType;
|
|
37
|
+
/** Minimum bytes required before readChunkFunc can be called (typically header size) */
|
|
15
38
|
minimumSizeInBytes: number;
|
|
39
|
+
/** Maximum allowed chunk size in bytes (chunks exceeding this will trigger an error) */
|
|
16
40
|
maxChunkSize: number;
|
|
17
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Error codes for packet assembly failures.
|
|
44
|
+
*/
|
|
18
45
|
export declare enum PacketAssemblerErrorCode {
|
|
46
|
+
/** Chunk size exceeds configured maxChunkSize */
|
|
19
47
|
ChunkSizeExceeded = 1,
|
|
48
|
+
/** Chunk size is smaller than minimumSizeInBytes */
|
|
20
49
|
ChunkTooSmall = 2
|
|
21
50
|
}
|
|
22
51
|
export interface PacketAssembler {
|
|
@@ -25,8 +54,64 @@ export interface PacketAssembler {
|
|
|
25
54
|
on(eventName: "error", eventHandler: (err: Error, errCode: PacketAssemblerErrorCode) => void): this;
|
|
26
55
|
}
|
|
27
56
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
57
|
+
* Assembles fragmented data from transport layers into complete message chunks.
|
|
58
|
+
*
|
|
59
|
+
* ## Zero-Copy Optimization
|
|
60
|
+
*
|
|
61
|
+
* The PacketAssembler uses zero-copy optimization for performance:
|
|
62
|
+
* - **Single-chunk messages**: Returns a view of the input buffer (no allocation, no copy)
|
|
63
|
+
* - **Multi-chunk messages**: Creates a new buffer via Buffer.concat() (safe copy)
|
|
64
|
+
*
|
|
65
|
+
* ## Buffer Lifetime Management
|
|
66
|
+
*
|
|
67
|
+
* **Important**: When receiving single-chunk messages, the returned buffer is a view of the
|
|
68
|
+
* input buffer. Consumers are responsible for creating copies if they need buffers that
|
|
69
|
+
* survive beyond immediate processing or if the transport layer reuses buffers.
|
|
70
|
+
*
|
|
71
|
+
* ### Safe Usage:
|
|
72
|
+
* ```typescript
|
|
73
|
+
* assembler.on("chunk", (chunk) => {
|
|
74
|
+
* // Option 1: Process immediately (safe)
|
|
75
|
+
* const value = chunk.readUInt32LE(0);
|
|
76
|
+
*
|
|
77
|
+
* // Option 2: Create a copy for later use
|
|
78
|
+
* const copy = Buffer.from(chunk);
|
|
79
|
+
* queue.push(copy);
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* ### Unsafe Usage:
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const chunks = [];
|
|
86
|
+
* assembler.on("chunk", (chunk) => {
|
|
87
|
+
* // UNSAFE: chunk may be reused by transport layer
|
|
88
|
+
* chunks.push(chunk);
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @fires PacketAssembler#startChunk - Emitted when a new chunk begins
|
|
93
|
+
* @fires PacketAssembler#chunk - Emitted when a complete chunk is assembled
|
|
94
|
+
* @fires PacketAssembler#error - Emitted on assembly errors
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* const assembler = new PacketAssembler({
|
|
99
|
+
* readChunkFunc: (data) => ({
|
|
100
|
+
* length: data.readUInt32LE(4),
|
|
101
|
+
* messageHeader: { msgType: "MSG", isFinal: "F", length: data.readUInt32LE(4) },
|
|
102
|
+
* extra: ""
|
|
103
|
+
* }),
|
|
104
|
+
* minimumSizeInBytes: 8,
|
|
105
|
+
* maxChunkSize: 65536
|
|
106
|
+
* });
|
|
107
|
+
*
|
|
108
|
+
* assembler.on("chunk", (chunk) => {
|
|
109
|
+
* console.log("Complete chunk:", chunk.length, "bytes");
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* // Feed data from transport
|
|
113
|
+
* socket.on("data", (data) => assembler.feed(data));
|
|
114
|
+
* ```
|
|
30
115
|
*/
|
|
31
116
|
export declare class PacketAssembler extends EventEmitter {
|
|
32
117
|
static defaultMaxChunkCount: number;
|
|
@@ -38,8 +123,63 @@ export declare class PacketAssembler extends EventEmitter {
|
|
|
38
123
|
private readonly readChunkFunc;
|
|
39
124
|
private readonly minimumSizeInBytes;
|
|
40
125
|
private packetInfo?;
|
|
126
|
+
/**
|
|
127
|
+
* Creates a new PacketAssembler instance.
|
|
128
|
+
*
|
|
129
|
+
* @param options - Configuration options for the assembler
|
|
130
|
+
* @param options.readChunkFunc - Function to extract packet metadata from buffer
|
|
131
|
+
* @param options.minimumSizeInBytes - Minimum bytes needed to read packet header (default: 8)
|
|
132
|
+
* @param options.maxChunkSize - Maximum allowed chunk size (default: 65529)
|
|
133
|
+
*
|
|
134
|
+
* @throws {Error} If readChunkFunc is not a function
|
|
135
|
+
* @throws {Error} If maxChunkSize is less than minimumSizeInBytes
|
|
136
|
+
*/
|
|
41
137
|
constructor(options: PacketAssemblerOptions);
|
|
138
|
+
/**
|
|
139
|
+
* Feeds incoming data to the assembler for processing.
|
|
140
|
+
*
|
|
141
|
+
* This method can be called multiple times with partial data. The assembler will
|
|
142
|
+
* buffer incomplete chunks and emit the "chunk" event when a complete message
|
|
143
|
+
* has been assembled.
|
|
144
|
+
*
|
|
145
|
+
* ## Zero-Copy Behavior
|
|
146
|
+
*
|
|
147
|
+
* - If the data contains a complete single chunk, it returns a **view of the input buffer**
|
|
148
|
+
* (zero-copy optimization)
|
|
149
|
+
* - If multiple fragments are needed, it creates a **new buffer** via Buffer.concat()
|
|
150
|
+
* (safe copy)
|
|
151
|
+
*
|
|
152
|
+
* @param data - Buffer containing incoming data (can be partial or complete chunks)
|
|
153
|
+
*
|
|
154
|
+
* @fires startChunk - When a new chunk header is detected
|
|
155
|
+
* @fires chunk - When a complete chunk has been assembled
|
|
156
|
+
* @fires error - If chunk size validation fails
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* // Feed data as it arrives from transport
|
|
161
|
+
* socket.on("data", (data) => {
|
|
162
|
+
* assembler.feed(data);
|
|
163
|
+
* });
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
42
166
|
feed(data: Buffer): void;
|
|
43
167
|
private _readPacketInfo;
|
|
168
|
+
/**
|
|
169
|
+
* Builds the final chunk buffer from accumulated fragments.
|
|
170
|
+
*
|
|
171
|
+
* ## Zero-Copy Implementation
|
|
172
|
+
*
|
|
173
|
+
* This method implements the zero-copy optimization:
|
|
174
|
+
* - **Single chunk** (data provided, stack empty): Returns the input buffer directly (zero-copy)
|
|
175
|
+
* - **Single buffered chunk** (no data, one item in stack): Returns the stacked buffer
|
|
176
|
+
* - **Multiple fragments**: Concatenates all fragments into a new buffer (safe copy)
|
|
177
|
+
*
|
|
178
|
+
* @param data - Current buffer fragment (may be null)
|
|
179
|
+
* @returns Complete chunk buffer (either view or new buffer)
|
|
180
|
+
*
|
|
181
|
+
* @private
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
44
184
|
private _buildData;
|
|
45
185
|
}
|
package/dist/packet_assembler.js
CHANGED
|
@@ -6,14 +6,75 @@ const node_opcua_assert_1 = require("node-opcua-assert");
|
|
|
6
6
|
const node_opcua_debug_1 = require("node-opcua-debug");
|
|
7
7
|
const doDebug = false;
|
|
8
8
|
const warningLog = (0, node_opcua_debug_1.make_warningLog)("PacketAssembler");
|
|
9
|
+
/**
|
|
10
|
+
* Error codes for packet assembly failures.
|
|
11
|
+
*/
|
|
9
12
|
var PacketAssemblerErrorCode;
|
|
10
13
|
(function (PacketAssemblerErrorCode) {
|
|
14
|
+
/** Chunk size exceeds configured maxChunkSize */
|
|
11
15
|
PacketAssemblerErrorCode[PacketAssemblerErrorCode["ChunkSizeExceeded"] = 1] = "ChunkSizeExceeded";
|
|
16
|
+
/** Chunk size is smaller than minimumSizeInBytes */
|
|
12
17
|
PacketAssemblerErrorCode[PacketAssemblerErrorCode["ChunkTooSmall"] = 2] = "ChunkTooSmall";
|
|
13
18
|
})(PacketAssemblerErrorCode || (exports.PacketAssemblerErrorCode = PacketAssemblerErrorCode = {}));
|
|
14
19
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
20
|
+
* Assembles fragmented data from transport layers into complete message chunks.
|
|
21
|
+
*
|
|
22
|
+
* ## Zero-Copy Optimization
|
|
23
|
+
*
|
|
24
|
+
* The PacketAssembler uses zero-copy optimization for performance:
|
|
25
|
+
* - **Single-chunk messages**: Returns a view of the input buffer (no allocation, no copy)
|
|
26
|
+
* - **Multi-chunk messages**: Creates a new buffer via Buffer.concat() (safe copy)
|
|
27
|
+
*
|
|
28
|
+
* ## Buffer Lifetime Management
|
|
29
|
+
*
|
|
30
|
+
* **Important**: When receiving single-chunk messages, the returned buffer is a view of the
|
|
31
|
+
* input buffer. Consumers are responsible for creating copies if they need buffers that
|
|
32
|
+
* survive beyond immediate processing or if the transport layer reuses buffers.
|
|
33
|
+
*
|
|
34
|
+
* ### Safe Usage:
|
|
35
|
+
* ```typescript
|
|
36
|
+
* assembler.on("chunk", (chunk) => {
|
|
37
|
+
* // Option 1: Process immediately (safe)
|
|
38
|
+
* const value = chunk.readUInt32LE(0);
|
|
39
|
+
*
|
|
40
|
+
* // Option 2: Create a copy for later use
|
|
41
|
+
* const copy = Buffer.from(chunk);
|
|
42
|
+
* queue.push(copy);
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ### Unsafe Usage:
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const chunks = [];
|
|
49
|
+
* assembler.on("chunk", (chunk) => {
|
|
50
|
+
* // UNSAFE: chunk may be reused by transport layer
|
|
51
|
+
* chunks.push(chunk);
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @fires PacketAssembler#startChunk - Emitted when a new chunk begins
|
|
56
|
+
* @fires PacketAssembler#chunk - Emitted when a complete chunk is assembled
|
|
57
|
+
* @fires PacketAssembler#error - Emitted on assembly errors
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const assembler = new PacketAssembler({
|
|
62
|
+
* readChunkFunc: (data) => ({
|
|
63
|
+
* length: data.readUInt32LE(4),
|
|
64
|
+
* messageHeader: { msgType: "MSG", isFinal: "F", length: data.readUInt32LE(4) },
|
|
65
|
+
* extra: ""
|
|
66
|
+
* }),
|
|
67
|
+
* minimumSizeInBytes: 8,
|
|
68
|
+
* maxChunkSize: 65536
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* assembler.on("chunk", (chunk) => {
|
|
72
|
+
* console.log("Complete chunk:", chunk.length, "bytes");
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* // Feed data from transport
|
|
76
|
+
* socket.on("data", (data) => assembler.feed(data));
|
|
77
|
+
* ```
|
|
17
78
|
*/
|
|
18
79
|
class PacketAssembler extends events_1.EventEmitter {
|
|
19
80
|
static defaultMaxChunkCount = 777;
|
|
@@ -25,6 +86,17 @@ class PacketAssembler extends events_1.EventEmitter {
|
|
|
25
86
|
readChunkFunc;
|
|
26
87
|
minimumSizeInBytes;
|
|
27
88
|
packetInfo;
|
|
89
|
+
/**
|
|
90
|
+
* Creates a new PacketAssembler instance.
|
|
91
|
+
*
|
|
92
|
+
* @param options - Configuration options for the assembler
|
|
93
|
+
* @param options.readChunkFunc - Function to extract packet metadata from buffer
|
|
94
|
+
* @param options.minimumSizeInBytes - Minimum bytes needed to read packet header (default: 8)
|
|
95
|
+
* @param options.maxChunkSize - Maximum allowed chunk size (default: 65529)
|
|
96
|
+
*
|
|
97
|
+
* @throws {Error} If readChunkFunc is not a function
|
|
98
|
+
* @throws {Error} If maxChunkSize is less than minimumSizeInBytes
|
|
99
|
+
*/
|
|
28
100
|
constructor(options) {
|
|
29
101
|
super();
|
|
30
102
|
this._stack = [];
|
|
@@ -33,11 +105,39 @@ class PacketAssembler extends events_1.EventEmitter {
|
|
|
33
105
|
this.readChunkFunc = options.readChunkFunc;
|
|
34
106
|
this.minimumSizeInBytes = options.minimumSizeInBytes || 8;
|
|
35
107
|
(0, node_opcua_assert_1.assert)(typeof this.readChunkFunc === "function", "packet assembler requires a readChunkFunc");
|
|
36
|
-
//
|
|
108
|
+
// c8 ignore next
|
|
37
109
|
(0, node_opcua_assert_1.assert)(options.maxChunkSize === undefined || options.maxChunkSize !== 0);
|
|
38
110
|
this.maxChunkSize = options.maxChunkSize || PacketAssembler.defaultMaxMessageSize;
|
|
39
111
|
(0, node_opcua_assert_1.assert)(this.maxChunkSize >= this.minimumSizeInBytes);
|
|
40
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Feeds incoming data to the assembler for processing.
|
|
115
|
+
*
|
|
116
|
+
* This method can be called multiple times with partial data. The assembler will
|
|
117
|
+
* buffer incomplete chunks and emit the "chunk" event when a complete message
|
|
118
|
+
* has been assembled.
|
|
119
|
+
*
|
|
120
|
+
* ## Zero-Copy Behavior
|
|
121
|
+
*
|
|
122
|
+
* - If the data contains a complete single chunk, it returns a **view of the input buffer**
|
|
123
|
+
* (zero-copy optimization)
|
|
124
|
+
* - If multiple fragments are needed, it creates a **new buffer** via Buffer.concat()
|
|
125
|
+
* (safe copy)
|
|
126
|
+
*
|
|
127
|
+
* @param data - Buffer containing incoming data (can be partial or complete chunks)
|
|
128
|
+
*
|
|
129
|
+
* @fires startChunk - When a new chunk header is detected
|
|
130
|
+
* @fires chunk - When a complete chunk has been assembled
|
|
131
|
+
* @fires error - If chunk size validation fails
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* // Feed data as it arrives from transport
|
|
136
|
+
* socket.on("data", (data) => {
|
|
137
|
+
* assembler.feed(data);
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
41
141
|
feed(data) {
|
|
42
142
|
let messageChunk;
|
|
43
143
|
if (this.expectedLength === 0 && this.currentLength + data.length >= this.minimumSizeInBytes) {
|
|
@@ -72,7 +172,7 @@ class PacketAssembler extends events_1.EventEmitter {
|
|
|
72
172
|
else if (this.currentLength + data.length === this.expectedLength) {
|
|
73
173
|
this.currentLength += data.length;
|
|
74
174
|
messageChunk = this._buildData(data);
|
|
75
|
-
//
|
|
175
|
+
// c8 ignore next
|
|
76
176
|
if (doDebug) {
|
|
77
177
|
const packetInfo = this._readPacketInfo(messageChunk);
|
|
78
178
|
(0, node_opcua_assert_1.assert)(this.packetInfo && this.packetInfo.length === packetInfo.length);
|
|
@@ -100,6 +200,22 @@ class PacketAssembler extends events_1.EventEmitter {
|
|
|
100
200
|
_readPacketInfo(data) {
|
|
101
201
|
return this.readChunkFunc(data);
|
|
102
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Builds the final chunk buffer from accumulated fragments.
|
|
205
|
+
*
|
|
206
|
+
* ## Zero-Copy Implementation
|
|
207
|
+
*
|
|
208
|
+
* This method implements the zero-copy optimization:
|
|
209
|
+
* - **Single chunk** (data provided, stack empty): Returns the input buffer directly (zero-copy)
|
|
210
|
+
* - **Single buffered chunk** (no data, one item in stack): Returns the stacked buffer
|
|
211
|
+
* - **Multiple fragments**: Concatenates all fragments into a new buffer (safe copy)
|
|
212
|
+
*
|
|
213
|
+
* @param data - Current buffer fragment (may be null)
|
|
214
|
+
* @returns Complete chunk buffer (either view or new buffer)
|
|
215
|
+
*
|
|
216
|
+
* @private
|
|
217
|
+
* @internal
|
|
218
|
+
*/
|
|
103
219
|
_buildData(data) {
|
|
104
220
|
if (data && this._stack.length === 0) {
|
|
105
221
|
return data;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"packet_assembler.js","sourceRoot":"","sources":["../source/packet_assembler.ts"],"names":[],"mappings":";;;AAAA,mCAAsC;AACtC,yDAA2C;AAC3C,uDAAmD;AAEnD,MAAM,OAAO,GAAG,KAAK,CAAC;AACtB,MAAM,UAAU,GAAG,IAAA,kCAAe,EAAC,iBAAiB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"packet_assembler.js","sourceRoot":"","sources":["../source/packet_assembler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-packet-assembler",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.164.0",
|
|
4
4
|
"description": "pure nodejs OPCUA SDK - module packet-assembler",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"author": "Etienne Rossignon",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"node-opcua-assert": "2.
|
|
16
|
-
"node-opcua-debug": "2.
|
|
15
|
+
"node-opcua-assert": "2.164.0",
|
|
16
|
+
"node-opcua-debug": "2.164.0"
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"internet of things"
|
|
29
29
|
],
|
|
30
30
|
"homepage": "http://node-opcua.github.io/",
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "eb76d34b885c7584785d8eff69ada66f95b55c2e",
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
34
|
"source"
|
|
@@ -5,29 +5,57 @@ import { make_warningLog } from "node-opcua-debug";
|
|
|
5
5
|
const doDebug = false;
|
|
6
6
|
const warningLog = make_warningLog("PacketAssembler");
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Message header information extracted from packet data.
|
|
10
|
+
*/
|
|
8
11
|
export interface MessageHeader {
|
|
12
|
+
/** Message type identifier (e.g., "MSG", "OPN", "CLO") */
|
|
9
13
|
msgType: string;
|
|
14
|
+
/** Final chunk indicator ("F" for final, "C" for continuation) */
|
|
10
15
|
isFinal: string;
|
|
16
|
+
/** Total message length in bytes */
|
|
11
17
|
length: number;
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Packet metadata extracted from incoming data.
|
|
22
|
+
*/
|
|
14
23
|
export interface PacketInfo {
|
|
24
|
+
/** Total expected packet length in bytes */
|
|
15
25
|
length: number;
|
|
26
|
+
/** Message header information */
|
|
16
27
|
messageHeader: MessageHeader;
|
|
28
|
+
/** Additional protocol-specific metadata */
|
|
17
29
|
extra: string;
|
|
18
30
|
}
|
|
19
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Function type for extracting packet metadata from buffer data.
|
|
34
|
+
*
|
|
35
|
+
* @param data - Buffer containing at least minimumSizeInBytes of data
|
|
36
|
+
* @returns Packet metadata including expected length and headers
|
|
37
|
+
*/
|
|
20
38
|
export type ReadChunkFuncType = (data: Buffer) => PacketInfo;
|
|
21
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Configuration options for PacketAssembler.
|
|
42
|
+
*/
|
|
22
43
|
export interface PacketAssemblerOptions {
|
|
44
|
+
/** Function to extract packet metadata from incoming data */
|
|
23
45
|
readChunkFunc: ReadChunkFuncType;
|
|
24
|
-
|
|
46
|
+
/** Minimum bytes required before readChunkFunc can be called (typically header size) */
|
|
25
47
|
minimumSizeInBytes: number;
|
|
48
|
+
/** Maximum allowed chunk size in bytes (chunks exceeding this will trigger an error) */
|
|
26
49
|
maxChunkSize: number;
|
|
27
50
|
}
|
|
28
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Error codes for packet assembly failures.
|
|
54
|
+
*/
|
|
29
55
|
export enum PacketAssemblerErrorCode {
|
|
56
|
+
/** Chunk size exceeds configured maxChunkSize */
|
|
30
57
|
ChunkSizeExceeded = 1,
|
|
58
|
+
/** Chunk size is smaller than minimumSizeInBytes */
|
|
31
59
|
ChunkTooSmall = 2
|
|
32
60
|
}
|
|
33
61
|
export interface PacketAssembler {
|
|
@@ -36,8 +64,64 @@ export interface PacketAssembler {
|
|
|
36
64
|
on(eventName: "error", eventHandler: (err: Error, errCode: PacketAssemblerErrorCode) => void): this;
|
|
37
65
|
}
|
|
38
66
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
67
|
+
* Assembles fragmented data from transport layers into complete message chunks.
|
|
68
|
+
*
|
|
69
|
+
* ## Zero-Copy Optimization
|
|
70
|
+
*
|
|
71
|
+
* The PacketAssembler uses zero-copy optimization for performance:
|
|
72
|
+
* - **Single-chunk messages**: Returns a view of the input buffer (no allocation, no copy)
|
|
73
|
+
* - **Multi-chunk messages**: Creates a new buffer via Buffer.concat() (safe copy)
|
|
74
|
+
*
|
|
75
|
+
* ## Buffer Lifetime Management
|
|
76
|
+
*
|
|
77
|
+
* **Important**: When receiving single-chunk messages, the returned buffer is a view of the
|
|
78
|
+
* input buffer. Consumers are responsible for creating copies if they need buffers that
|
|
79
|
+
* survive beyond immediate processing or if the transport layer reuses buffers.
|
|
80
|
+
*
|
|
81
|
+
* ### Safe Usage:
|
|
82
|
+
* ```typescript
|
|
83
|
+
* assembler.on("chunk", (chunk) => {
|
|
84
|
+
* // Option 1: Process immediately (safe)
|
|
85
|
+
* const value = chunk.readUInt32LE(0);
|
|
86
|
+
*
|
|
87
|
+
* // Option 2: Create a copy for later use
|
|
88
|
+
* const copy = Buffer.from(chunk);
|
|
89
|
+
* queue.push(copy);
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* ### Unsafe Usage:
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const chunks = [];
|
|
96
|
+
* assembler.on("chunk", (chunk) => {
|
|
97
|
+
* // UNSAFE: chunk may be reused by transport layer
|
|
98
|
+
* chunks.push(chunk);
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @fires PacketAssembler#startChunk - Emitted when a new chunk begins
|
|
103
|
+
* @fires PacketAssembler#chunk - Emitted when a complete chunk is assembled
|
|
104
|
+
* @fires PacketAssembler#error - Emitted on assembly errors
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const assembler = new PacketAssembler({
|
|
109
|
+
* readChunkFunc: (data) => ({
|
|
110
|
+
* length: data.readUInt32LE(4),
|
|
111
|
+
* messageHeader: { msgType: "MSG", isFinal: "F", length: data.readUInt32LE(4) },
|
|
112
|
+
* extra: ""
|
|
113
|
+
* }),
|
|
114
|
+
* minimumSizeInBytes: 8,
|
|
115
|
+
* maxChunkSize: 65536
|
|
116
|
+
* });
|
|
117
|
+
*
|
|
118
|
+
* assembler.on("chunk", (chunk) => {
|
|
119
|
+
* console.log("Complete chunk:", chunk.length, "bytes");
|
|
120
|
+
* });
|
|
121
|
+
*
|
|
122
|
+
* // Feed data from transport
|
|
123
|
+
* socket.on("data", (data) => assembler.feed(data));
|
|
124
|
+
* ```
|
|
41
125
|
*/
|
|
42
126
|
export class PacketAssembler extends EventEmitter {
|
|
43
127
|
public static defaultMaxChunkCount = 777;
|
|
@@ -53,6 +137,17 @@ export class PacketAssembler extends EventEmitter {
|
|
|
53
137
|
private readonly minimumSizeInBytes: number;
|
|
54
138
|
private packetInfo?: PacketInfo;
|
|
55
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Creates a new PacketAssembler instance.
|
|
142
|
+
*
|
|
143
|
+
* @param options - Configuration options for the assembler
|
|
144
|
+
* @param options.readChunkFunc - Function to extract packet metadata from buffer
|
|
145
|
+
* @param options.minimumSizeInBytes - Minimum bytes needed to read packet header (default: 8)
|
|
146
|
+
* @param options.maxChunkSize - Maximum allowed chunk size (default: 65529)
|
|
147
|
+
*
|
|
148
|
+
* @throws {Error} If readChunkFunc is not a function
|
|
149
|
+
* @throws {Error} If maxChunkSize is less than minimumSizeInBytes
|
|
150
|
+
*/
|
|
56
151
|
constructor(options: PacketAssemblerOptions) {
|
|
57
152
|
super();
|
|
58
153
|
this._stack = [];
|
|
@@ -62,13 +157,41 @@ export class PacketAssembler extends EventEmitter {
|
|
|
62
157
|
this.minimumSizeInBytes = options.minimumSizeInBytes || 8;
|
|
63
158
|
assert(typeof this.readChunkFunc === "function", "packet assembler requires a readChunkFunc");
|
|
64
159
|
|
|
65
|
-
//
|
|
160
|
+
// c8 ignore next
|
|
66
161
|
assert(options.maxChunkSize === undefined || options.maxChunkSize !== 0);
|
|
67
162
|
|
|
68
163
|
this.maxChunkSize = options.maxChunkSize || PacketAssembler.defaultMaxMessageSize;
|
|
69
164
|
assert(this.maxChunkSize >= this.minimumSizeInBytes);
|
|
70
165
|
}
|
|
71
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Feeds incoming data to the assembler for processing.
|
|
169
|
+
*
|
|
170
|
+
* This method can be called multiple times with partial data. The assembler will
|
|
171
|
+
* buffer incomplete chunks and emit the "chunk" event when a complete message
|
|
172
|
+
* has been assembled.
|
|
173
|
+
*
|
|
174
|
+
* ## Zero-Copy Behavior
|
|
175
|
+
*
|
|
176
|
+
* - If the data contains a complete single chunk, it returns a **view of the input buffer**
|
|
177
|
+
* (zero-copy optimization)
|
|
178
|
+
* - If multiple fragments are needed, it creates a **new buffer** via Buffer.concat()
|
|
179
|
+
* (safe copy)
|
|
180
|
+
*
|
|
181
|
+
* @param data - Buffer containing incoming data (can be partial or complete chunks)
|
|
182
|
+
*
|
|
183
|
+
* @fires startChunk - When a new chunk header is detected
|
|
184
|
+
* @fires chunk - When a complete chunk has been assembled
|
|
185
|
+
* @fires error - If chunk size validation fails
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* // Feed data as it arrives from transport
|
|
190
|
+
* socket.on("data", (data) => {
|
|
191
|
+
* assembler.feed(data);
|
|
192
|
+
* });
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
72
195
|
public feed(data: Buffer) {
|
|
73
196
|
let messageChunk;
|
|
74
197
|
|
|
@@ -109,7 +232,7 @@ export class PacketAssembler extends EventEmitter {
|
|
|
109
232
|
|
|
110
233
|
messageChunk = this._buildData(data);
|
|
111
234
|
|
|
112
|
-
//
|
|
235
|
+
// c8 ignore next
|
|
113
236
|
if (doDebug) {
|
|
114
237
|
const packetInfo = this._readPacketInfo(messageChunk);
|
|
115
238
|
assert(this.packetInfo && this.packetInfo.length === packetInfo.length);
|
|
@@ -139,6 +262,22 @@ export class PacketAssembler extends EventEmitter {
|
|
|
139
262
|
return this.readChunkFunc(data);
|
|
140
263
|
}
|
|
141
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Builds the final chunk buffer from accumulated fragments.
|
|
267
|
+
*
|
|
268
|
+
* ## Zero-Copy Implementation
|
|
269
|
+
*
|
|
270
|
+
* This method implements the zero-copy optimization:
|
|
271
|
+
* - **Single chunk** (data provided, stack empty): Returns the input buffer directly (zero-copy)
|
|
272
|
+
* - **Single buffered chunk** (no data, one item in stack): Returns the stacked buffer
|
|
273
|
+
* - **Multiple fragments**: Concatenates all fragments into a new buffer (safe copy)
|
|
274
|
+
*
|
|
275
|
+
* @param data - Current buffer fragment (may be null)
|
|
276
|
+
* @returns Complete chunk buffer (either view or new buffer)
|
|
277
|
+
*
|
|
278
|
+
* @private
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
142
281
|
private _buildData(data: Buffer): Buffer {
|
|
143
282
|
if (data && this._stack.length === 0) {
|
|
144
283
|
return data;
|