Temporal.Instant を使用した高精度タイムスタンプの並べ替え

ウェブ機能: Temporal

ログビューア、監査タイムライン、テレメトリー UI では、ISO 8601 タイムスタンプがサーバー、データベース、ネイティブ クロックによってマイクロ秒またはナノ秒の精度で記録されたイベント(2026-04-01T12:00:00.000123456Z など)が取り込まれることがよくあります。以前の Date オブジェクトはミリ秒のみを保存するため、new Date() または Date.parse() を使用してこれらの文字列を解析すると、ミリ秒未満の桁が暗黙的に切り捨てられ、同じミリ秒内の異なるイベントが競合し、不安定な順序で並べ替えられます。

Temporal.Instant は、UTC タイムライン上の正確な時点をナノ秒単位の精度で表します。タイムゾーンは付加されません。Temporal.Instant.from() を使用して、事前に記録された高精度のタイムスタンプを解析する場合、ナノ秒単位の値を完全に保持し、Temporal.Instant.compare() を使用して決定論的に並べ替え、イベント間の正確な Temporal.Duration 間隔を計算します。ブラウザでは、Spectre タイミング攻撃を軽減するために、Temporal.Now.instant() は意図的にミリ秒またはマイクロ秒の解像度に制限されています。外部クロックによって生成されたナノ秒のタイムスタンプを保持するには Temporal.Instant.from() を使用し、クライアントでローカルにミリ秒未満の間隔を測定する場合は performance.now() を使用します。

実装方法

高精度タイムスタンプの間隔を解析、並べ替え、測定するには:

  1. 記録されたタイムスタンプを Temporal.Instant として解析する: サーバー、データベース、またはログ行から提供されたタイムスタンプ文字列に Temporal.Instant.from(isoString) を使用します。ソースが記録したタイムスタンプを保持します。ブラウザで Temporal.Now.instant() に置き換えないでください。
  2. イベントを時系列で並べ替える: Temporal.Instant.compare(a, b) を Array.prototype.sort() または Array.prototype.toSorted() に渡すコンパレータとして使用します。ナノ秒単位の完全な値を比較するため、ミリ秒未満の順序が保持されます。
  3. 間隔を計算する: instant.since(previousInstant) を使用して、連続するイベント間の Temporal.Duration を取得します。a.equals(b) を使用して、タイムスタンプが同じイベントを検出します。
  4. toLocaleString() を使用して間隔を表示する: duration.toLocaleString() を使用してユーザー向けに Temporal.Duration をフォーマットします。これは Intl.DurationFormat に委任されます。表示用にナノ秒の単一の Number に変換しないでください。
  5. 送信用のシリアル化: Temporal.Instant.prototype.toString() を使用して、標準の ISO 8601 文字列(常に UTC で、Z サフィックス付き)を出力します。

コード例: 高精度タイムスタンプの並べ替え

// Events received with nanosecond-precision ISO 8601 timestamps.
const rawEvents = [
  { nodeId: 'api-2', eventType: 'db.query', ts: '2026-04-01T12:00:00.000123456Z' },
  { nodeId: 'api-1', eventType: 'request.start', ts: '2026-04-01T12:00:00.000120000Z' },
  { nodeId: 'api-2', eventType: 'request.end', ts: '2026-04-01T12:00:00.000987654Z' },
];

// 1. Parse recorded timestamps as Temporal.Instant (never `new Date(ts)`,
// which drops everything below the millisecond).
const events = rawEvents.map(evt => ({
  ...evt,
  timestamp: Temporal.Instant.from(evt.ts),
}));

// 2. Sort chronologically with the static compare() method
function sequenceEvents(items) {
  // Instant.compare returns -1, 0, or 1, which is what sort() expects.
  return [...items].sort((a, b) => Temporal.Instant.compare(a.timestamp, b.timestamp));
}

// 3. Calculate and display the interval between consecutive events
function analyzeTrace(sortedEvents) {
  for (let i = 1; i < sortedEvents.length; i++) {
    const prev = sortedEvents[i - 1];
    const curr = sortedEvents[i];

    // since() returns a Temporal.Duration that retains nanosecond precision.
    const gap = curr.timestamp.since(prev.timestamp);

    // 4. toLocaleString() formats the duration for people (e.g., "3μs 456ns").
    // Avoid gap.total('nanoseconds'): spans over ~104 days exceed Number.MAX_SAFE_INTEGER.
    // 'en-US' is used here for deterministic output; pass undefined to use the user's locale.
    console.log(`${prev.eventType} → ${curr.eventType}: ${gap.toLocaleString('en-US', { style: 'narrow' })}`);
  }
}

analyzeTrace(sequenceEvents(events));

戦略的な実装とベスト プラクティス

  • 推奨: 順序付けや差分が必要な単一のクロック(サーバー、データベース、ログ パイプラインなど)に対して記録された高精度のタイムスタンプを解析する場合は、Temporal.Instant.from() を使用します。Temporal.PlainDateTime とは異なり、Temporal.Instant はユニバーサル タイムライン上の明確なポイントを表します。
  • 推奨: ソースによって記録されたタイムスタンプを保持します。ブラウザでは、Spectre のタイミング攻撃とフィンガープリントの緩和のために、Temporal.Now.instant() はミリ秒 / マイクロ秒の精度に制限されます。ローカルのミリ秒未満のクライアント間隔には performance.now() / performance.mark() を使用し、サーバーまたはネイティブ クロックからナノ秒のタイムスタンプを解析するには Temporal.Instant.from() を使用します。
  • 推奨: 並べ替えには Temporal.Instant.compare() を、等価性のチェックには a.equals(b) を使用します。Temporal.Instant.compare(a, b) === 0 のチェックや .toString() 出力の比較は行わないでください。
  • new Date() または Date.parse() を使用して高精度タイムスタンプ文字列を解析して順序付けしないでください。ミリ秒単位に切り捨てられ、競合が発生します。
  • 表示のために間隔を 1 つの数値にまとめないでください。duration.total('nanoseconds') は Number を返します。これは、約 104 日を超えるスパンの Number.MAX_SAFE_INTEGER を超え、精度が失われます。epochNanoseconds(BigInt)を減算すると正確になりますが、単位構造が破棄されます。間隔を Temporal.Duration として保持し、.toLocaleString() でフォーマットします。
  • 推奨: イベントのローカライズされた実時間をユーザーに表示する必要がある場合は、instant.toZonedDateTimeISO(timeZone) を使用して Instant を Temporal.ZonedDateTime に変換します。

代替戦略

Temporal のフォールバックとブラウザ サポート

ネイティブの Temporal API をまだサポートしていないブラウザの場合は、機能検出を使用して temporal-polyfill(約 15 KB min+gz)を動的に読み込みます。

temporal-polyfill をインポートしても、Temporal オブジェクトがグローバル スコープに自動的に割り当てられるわけではありません。コードが globalThis.Temporal に依存している場合は、明示的に割り当ててください。

// Check if Temporal is supported natively
(async () => {
  if (typeof Temporal === 'undefined') {
    // Load the polyfill conditionally
    const module = await import('https://esm.sh/temporal-polyfill');
    globalThis.Temporal = module.Temporal;
    // Extend Date.prototype if needed
    Date.prototype.toTemporalInstant = module.toTemporalInstant;
  }
  initializeApp();
})();