MessagePack (@msgpack/msgpack)
シリアライゼーションライブラリ
MessagePack (@msgpack/msgpack)
概要
@msgpack/msgpackは、TypeScriptおよびJavaScriptのための公式MessagePack実装です。JSONより高速でコンパクトなバイナリシリアライゼーションフォーマットを提供し、Node.jsとブラウザ環境の両方で動作します。タイムスタンプ拡張、カスタム拡張、型安全性を備えた包括的なMessagePackソリューションです。
詳細
MessagePackは、JSONより高速でコンパクトなバイナリシリアライゼーションフォーマットです。@msgpack/msgpackは、このフォーマットの完全なTypeScript実装を提供し、正確性、互換性、相互運用性、パフォーマンスに焦点を当てています。
主な特徴:
- TypeScriptネイティブ: 完全な型定義とTypeScript支援
- ユニバーサル: Node.js、ブラウザ、その他のES2015+環境で動作
- 高効率: JSONより高速でコンパクトなバイナリフォーマット
- 拡張可能: カスタム拡張コーデックとタイムスタンプ拡張
- 仕様準拠: MessagePack仕様の完全実装
技術的詳細:
- encode(): オブジェクトをMessagePackバイナリに変換
- decode(): MessagePackバイナリをオブジェクトに変換
- ExtensionCodec: カスタム拡張の実装
- データ型マッピング: JavaScriptとMessagePackの型対応
- useBigInt64: 64bit整数の設定オプション
データ型マッピング:
- プリミティブ型(null、boolean、number、string)
- バイナリデータ(Uint8Array、ArrayBuffer)
- 配列(Array)
- オブジェクト(Record<string, unknown>)
- 特殊型(Date、BigInt、Map、Set)
メリット・デメリット
メリット
- JSONより高速でコンパクトなシリアライゼーション
- 完全なTypeScriptサポートと型安全性
- Node.jsとブラウザ環境の両方で動作
- 豊富な拡張機能とカスタマイズ
- 小さな整数の効率的なエンコーディング
- MessagePack仕様の完全実装
デメリット
- JSONと比べて人間が読みにくい
- デバッグ時のデータ確認が困難
- バイナリフォーマットのため、テキストツールで編集不可
- 既存のJSONエコシステムとの互換性がない
- 学習コストがやや高い
参考ページ
- GitHubリポジトリ: https://github.com/msgpack/msgpack-javascript
- ドキュメント: https://github.com/msgpack/msgpack-javascript/blob/main/README.md
- MessagePack公式サイト: https://msgpack.org/
- NPMパッケージ: https://www.npmjs.com/package/@msgpack/msgpack
書き方の例
基本的な使用方法
import { encode, decode } from '@msgpack/msgpack';
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
const user: User = {
id: 1,
name: 'Alice',
email: '[email protected]',
active: true
};
// MessagePackにエンコード
const encoded = encode(user);
console.log('Encoded:', encoded.length, 'bytes');
// MessagePackからデコード
const decoded = decode(encoded) as User;
console.log('Decoded:', decoded);
複雑なデータ構造の処理
import { encode, decode } from '@msgpack/msgpack';
interface ApiResponse {
success: boolean;
data: {
users: User[];
metadata: {
total: number;
page: number;
limit: number;
};
};
timestamp: Date;
}
const response: ApiResponse = {
success: true,
data: {
users: [
{ id: 1, name: 'Alice', email: '[email protected]', active: true },
{ id: 2, name: 'Bob', email: '[email protected]', active: false }
],
metadata: {
total: 100,
page: 1,
limit: 10
}
},
timestamp: new Date()
};
// MessagePackにエンコード
const encoded = encode(response);
console.log('Encoded size:', encoded.length);
// MessagePackからデコード
const decoded = decode(encoded) as ApiResponse;
console.log('Users count:', decoded.data.users.length);
console.log('Timestamp:', decoded.timestamp);
バイナリデータの処理
import { encode, decode } from '@msgpack/msgpack';
interface FileData {
filename: string;
contentType: string;
data: Uint8Array;
metadata: {
size: number;
lastModified: Date;
};
}
// バイナリデータを作成
const imageData = new Uint8Array([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG header
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52
]);
const fileData: FileData = {
filename: 'sample.png',
contentType: 'image/png',
data: imageData,
metadata: {
size: imageData.length,
lastModified: new Date()
}
};
// MessagePackにエンコード
const encoded = encode(fileData);
console.log('Encoded file data:', encoded.length, 'bytes');
// MessagePackからデコード
const decoded = decode(encoded) as FileData;
console.log('Filename:', decoded.filename);
console.log('Data size:', decoded.data.length);
console.log('Last modified:', decoded.metadata.lastModified);
カスタム拡張の実装
import { encode, decode, ExtensionCodec } from '@msgpack/msgpack';
// カスタムクラス
class Point {
constructor(public x: number, public y: number) {}
toString(): string {
return `Point(${this.x}, ${this.y})`;
}
}
// カスタム拡張コーデック
const extensionCodec = new ExtensionCodec();
// Pointクラスの拡張を登録
extensionCodec.register({
type: 1, // 拡張タイプ
encode: (obj: unknown): Uint8Array | null => {
if (obj instanceof Point) {
return encode([obj.x, obj.y]);
}
return null;
},
decode: (data: Uint8Array): Point => {
const [x, y] = decode(data) as [number, number];
return new Point(x, y);
}
});
const point = new Point(10, 20);
console.log('Original:', point.toString());
// カスタム拡張でエンコード
const encoded = encode(point, { extensionCodec });
console.log('Encoded with extension:', encoded.length, 'bytes');
// カスタム拡張でデコード
const decoded = decode(encoded, { extensionCodec }) as Point;
console.log('Decoded:', decoded.toString());
console.log('Is Point instance?', decoded instanceof Point);
タイムスタンプ拡張
import { encode, decode } from '@msgpack/msgpack';
interface EventLog {
id: number;
event: string;
timestamp: Date;
data: any;
}
const eventLog: EventLog = {
id: 1,
event: 'user_login',
timestamp: new Date(),
data: { userId: 123, ip: '192.168.1.100' }
};
// Dateオブジェクトは自動的にタイムスタンプ拡張でエンコード
const encoded = encode(eventLog);
console.log('Encoded event log:', encoded.length, 'bytes');
// デコード時にDateオブジェクトが復元される
const decoded = decode(encoded) as EventLog;
console.log('Event:', decoded.event);
console.log('Timestamp:', decoded.timestamp);
console.log('Is Date?', decoded.timestamp instanceof Date);
大きなデータセットの処理
import { encode, decode } from '@msgpack/msgpack';
interface SensorReading {
sensorId: string;
timestamp: Date;
temperature: number;
humidity: number;
pressure: number;
}
function generateSensorData(count: number): SensorReading[] {
return Array.from({ length: count }, (_, i) => ({
sensorId: `sensor-${i % 10}`,
timestamp: new Date(Date.now() - i * 1000),
temperature: 20 + Math.random() * 10,
humidity: 40 + Math.random() * 20,
pressure: 1000 + Math.random() * 50
}));
}
function benchmarkSerialization(data: SensorReading[]): void {
// MessagePackでの処理
const msgpackStart = Date.now();
const msgpackEncoded = encode(data);
const msgpackEncodeTime = Date.now() - msgpackStart;
const msgpackDecodeStart = Date.now();
const msgpackDecoded = decode(msgpackEncoded) as SensorReading[];
const msgpackDecodeTime = Date.now() - msgpackDecodeStart;
// JSONでの処理(比較用)
const jsonStart = Date.now();
const jsonEncoded = JSON.stringify(data);
const jsonEncodeTime = Date.now() - jsonStart;
const jsonDecodeStart = Date.now();
const jsonDecoded = JSON.parse(jsonEncoded) as SensorReading[];
const jsonDecodeTime = Date.now() - jsonDecodeStart;
console.log('Performance Comparison:');
console.log(`MessagePack: ${msgpackEncoded.length} bytes, encode: ${msgpackEncodeTime}ms, decode: ${msgpackDecodeTime}ms`);
console.log(`JSON: ${jsonEncoded.length} bytes, encode: ${jsonEncodeTime}ms, decode: ${jsonDecodeTime}ms`);
console.log(`Size reduction: ${((jsonEncoded.length - msgpackEncoded.length) / jsonEncoded.length * 100).toFixed(1)}%`);
}
const sensorData = generateSensorData(1000);
benchmarkSerialization(sensorData);
エラーハンドリング
import { encode, decode, DecodeError } from '@msgpack/msgpack';
interface SafeData {
id: number;
name: string;
value: number;
}
function safeEncode<T>(obj: T): Uint8Array | null {
try {
return encode(obj);
} catch (error) {
console.error('Encode error:', error);
return null;
}
}
function safeDecode<T>(data: Uint8Array): T | null {
try {
return decode(data) as T;
} catch (error) {
if (error instanceof DecodeError) {
console.error('Decode error:', error.message);
} else {
console.error('Unknown error:', error);
}
return null;
}
}
// 正常なケース
const validData: SafeData = { id: 1, name: 'test', value: 42 };
const encoded = safeEncode(validData);
if (encoded) {
const decoded = safeDecode<SafeData>(encoded);
console.log('Decoded:', decoded);
}
// 無効なデータでのテスト
const invalidData = new Uint8Array([0xFF, 0xFF, 0xFF]);
const result = safeDecode<SafeData>(invalidData);
console.log('Invalid decode result:', result);
ストリーム処理
import { encode, decode } from '@msgpack/msgpack';
interface LogEntry {
level: 'INFO' | 'WARN' | 'ERROR';
message: string;
timestamp: Date;
metadata?: Record<string, any>;
}
class MessagePackStream {
private buffer: Uint8Array[] = [];
write(entry: LogEntry): void {
const encoded = encode(entry);
this.buffer.push(encoded);
}
read(): LogEntry[] {
const entries: LogEntry[] = [];
for (const data of this.buffer) {
try {
const entry = decode(data) as LogEntry;
entries.push(entry);
} catch (error) {
console.error('Failed to decode entry:', error);
}
}
return entries;
}
clear(): void {
this.buffer = [];
}
size(): number {
return this.buffer.reduce((sum, data) => sum + data.length, 0);
}
}
// 使用例
const stream = new MessagePackStream();
// ログエントリを追加
stream.write({
level: 'INFO',
message: 'Application started',
timestamp: new Date(),
metadata: { pid: 12345 }
});
stream.write({
level: 'ERROR',
message: 'Database connection failed',
timestamp: new Date(),
metadata: { error: 'ECONNREFUSED' }
});
// ストリームから読み取り
const entries = stream.read();
console.log(`Read ${entries.length} entries, total size: ${stream.size()} bytes`);
entries.forEach(entry => {
console.log(`[${entry.level}] ${entry.message}`);
});
BigInt と数値の処理
import { encode, decode } from '@msgpack/msgpack';
interface NumericData {
id: number;
largeNumber: bigint;
coordinates: number[];
metadata: {
precision: number;
scale: number;
};
}
const numericData: NumericData = {
id: 1,
largeNumber: BigInt('12345678901234567890'),
coordinates: [123.456789, 987.654321],
metadata: {
precision: 10,
scale: 6
}
};
// useBigInt64オプションでBigIntを処理
const encoded = encode(numericData, { useBigInt64: true });
console.log('Encoded numeric data:', encoded.length, 'bytes');
const decoded = decode(encoded, { useBigInt64: true }) as NumericData;
console.log('Large number:', decoded.largeNumber.toString());
console.log('Type check:', typeof decoded.largeNumber === 'bigint');
console.log('Coordinates:', decoded.coordinates);
設定オプション
import { encode, decode } from '@msgpack/msgpack';
interface ConfigurableData {
text: string;
rawData: Uint8Array;
numbers: number[];
}
const data: ConfigurableData = {
text: 'Hello MessagePack',
rawData: new Uint8Array([1, 2, 3, 4, 5]),
numbers: [1, 2, 3, 4, 5]
};
// エンコード時の設定
const encoded = encode(data, {
maxDepth: 100, // 最大ネスト深度
initialBufferSize: 2048, // 初期バッファサイズ
sortKeys: true, // キーのソート
forceFloat32: false // Float32の強制使用
});
// デコード時の設定
const decoded = decode(encoded, {
maxStrLength: 1000, // 最大文字列長
maxBinLength: 1000, // 最大バイナリ長
maxArrayLength: 1000, // 最大配列長
maxMapLength: 1000, // 最大マップ長
maxExtLength: 1000 // 最大拡張長
}) as ConfigurableData;
console.log('Decoded with options:', decoded);