MessagePack (@msgpack/msgpack)

シリアライゼーションTypeScriptMessagePackバイナリフォーマット高効率

シリアライゼーションライブラリ

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エコシステムとの互換性がない
  • 学習コストがやや高い

参考ページ

書き方の例

基本的な使用方法

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);