Aplikasi Webxdc berjalan sebagai web app mini yang berjalan di dalam platform messenger terdesentralisasi (seperti Delta Chat) tanpa backend tersentralisasi. Komunikasi antar-peer bergantung secara eksklusif pada append-only message log melalui API webxdc.sendUpdate() dan webxdc.setUpdateListener(). Ketika payload pembaruan dengan skema tidak valid atau bug mutasi (poison payload) terkirim ke dalam chat, seluruh klien yang menerima payload tersebut akan mengalami kegagalan parsing atau crash pada state machine lokal. Karena riwayat pesan Webxdc bersifat immutable, memuat ulang aplikasi hanya akan mengulang eksekusi payload rusak tersebut (CrashLoop permanen).

Anatomi Insiden Poison Payload

Webxdc menggunakan model konsistensi berbasis event sourcing. Klien merekonstruksi state aplikasi dengan memproses deretan pembaruan secara serial:

// Pola dasar inisialisasi Webxdc yang rentan crash loop
window.webxdc.setUpdateListener((update) => {
  // Jika update.payload merusak struktur state, error berikut
  // menghentikan eksekusi JavaScript dan merusak aplikasi secara permanen.
  state = applyUpdate(state, update.payload);
  renderUI(state);
}, lastKnownSerial);

Terdapat tiga titik kegagalan utama saat payload berbahaya terdistribusi:

  • State Engine Crash: Reducer melempar uncaught exception (misalnya TypeError: Cannot read properties of undefined) saat memproses payload, mencegah update berikutnya diproses.
  • State Divergence (Split-Brain): Sebagian klien memproses payload dengan interpretasi berbeda akibat perbedaan implementasi versi aplikasi lama dan baru, menghasilkan state lokal yang tidak identik antar-peer.
  • Deadlock Pemulihan: Karena peer tidak dapat menghapus pesan yang telah disimpan di transport layer messenger, deploy bundle aplikasi baru tetap harus membaca riwayat yang mengandung payload rusak tersebut saat bootstrap.

Observability Tanpa Backend Terpusat

Dalam arsitektur peer-to-peer murni tanpa server APM sentral (seperti Datadog atau Sentry), observability harus dibangun langsung di dalam protokol aplikasi menggunakan dua instrumen: verifikasi hash state terdistribusi dan telemetri in-band.

1. State Hash Verification

Untuk mendeteksi desinkronisasi sebelum crash terjadi, setiap pengirim payload wajib menyertakan checksum dari state lokal yang dihasilkan setelah mutasi selesai. Peer penerima memvalidasi hash tersebut dengan state miliknya.

function computeStateHash(state) {
  // Normalisasi urutan key objek sebelum hashing untuk determinisme
  const serialized = JSON.stringify(state, Object.keys(state).sort());
  let hash = 0;
  for (let i = 0; i < serialized.length; i++) {
    hash = (Math.imul(31, hash) + serialized.charCodeAt(i)) | 0;
  }
  return hash.toString(16);
}

function dispatchAction(action) {
  const nextState = reducer(currentState, action);
  const expectedHash = computeStateHash(nextState);

  window.webxdc.sendUpdate({
    payload: {
      action,
      _v: PROTOCOL_VERSION,
      _hash: expectedHash
    },
    info: `Action: ${action.type}`
  }, `Update state to ${expectedHash}`);
}

2. Telemetri Error In-Band

Ketika klien mendeteksi error pada saat mengeksekusi payload, tangkap kegagalan melalui error boundary lokal dan sebarkan diagnostik ke chat room sebagai update berbobot rendah (low-priority telemetry). Pola ini memungkinkan pengguna atau automated diagnostic peer mendeteksi kegagalan massal.

window.addEventListener("error", (event) => {
  window.webxdc.sendUpdate({
    payload: {
      type: "SYSTEM_TELEMETRY_ERROR",
      serial: currentProcessingSerial,
      error: event.message,
      userAgent: navigator.userAgent
    },
    info: "Diagnostic error report"
  }, "Telemetry event");
});

Strategi Rollback Menggunakan Compensating Events

Log Webxdc tidak mengizinkan operasi DROP atau DELETE. Pemulihan state dilakukan dengan menyuntikkan compensating event untuk menetralkan efek poison payload atau memaksa reset state ke snapshot stabil terakhir.

Pola Tombstone dan State Reset

Ketika poison payload teridentifikasi pada index serial tertentu, administrator atau peer dengan hak resolusi konflik menyiarkan event kompensasi tipe STATE_OVERRIDE atau TOMBSTONE yang menginstruksikan state machine lokal untuk membatalkan mutasi pada serial tersebut atau me-replace state sepenuhnya.

interface UpdatePayload {
  type: string;
  epoch: number;
  targetSerial?: number;
  snapshot?: AppState;
  data?: unknown;
}

function rootReducer(state: AppState, update: UpdatePayload, serial: number): AppState {
  // 1. Tangani hard reset / state recovery broadcast
  if (update.type === "COMPENSATING_RESET") {
    if (update.epoch > state.currentEpoch) {
      return {
        ...update.snapshot!,
        currentEpoch: update.epoch,
        quarantinedSerials: [...state.quarantinedSerials, update.targetSerial!]
      };
    }
    return state;
  }

  // 2. Abaikan serial yang telah masuk daftar karantina tombstone
  if (state.quarantinedSerials.includes(serial)) {
    return state;
  }

  // 3. Eksekusi reducer domain dengan fallback isolasi
  try {
    return domainReducer(state, update);
  } catch (err) {
    console.error(`Reducer failure at serial ${serial}:`, err);
    return {
      ...state,
      quarantinedSerials: [...state.quarantinedSerials, serial]
    };
  }
}

Tindakan Pencegahan dan Hardening Sistem

Mencegah payload berbahaya jauh lebih murah daripada melakukan recovery manual pada ratusan instalasi klien messenger yang terisolasi.

1. Validasi Skema Runtime pada Listener

Jangan pernah memproses update.payload langsung ke domain logic tanpa parsing ketat. Gunakan runtime validator seperti Zod atau pengecekan berbasis guard function sebelum event menyentuh reducer.

function isValidPayload(payload: any): payload is ValidAction {
  return (
    typeof payload === "object" &&
    payload !== null &&
    typeof payload.type === "string" &&
    typeof payload._v === "number" &&
    Number.isInteger(payload._v)
  );
}

window.webxdc.setUpdateListener((update) => {
  if (!isValidPayload(update.payload)) {
    console.warn(`Poison payload terdeteksi pada serial ${update.serial}. Payload diabaikan.`);
    return;
  }
  processUpdate(update);
}, lastSerial);

2. Protocol Version Gating

Setiap payload wajib membawa field versi protokol (_v). Klien yang menerima payload dengan versi lebih tinggi daripada yang didukung oleh binary aplikasi lokal harus menghentikan mutasi domain dan menampilkan prompt kepada user untuk memperbarui paket .xdc.

if (update.payload._v > CLIENT_SUPPORTED_PROTOCOL_VERSION) {
  renderUpdateRequiredNotification();
  return; // Hentikan eksekusi, cegah korupsi state lokal
}

3. Pipeline CI untuk Validasi Bundle .xdc

Integritas aplikasi Webxdc diverifikasi otomatis pada pipeline deployment sebelum bundle .xdc didistribusikan. Pengujian harus mencakup eksekusi simulasi mutasi balik (replay verification):

# Langkah validasi bundle pada CI workflow
name: Webxdc Package Validation
on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: Run Schema Regression Tests
        run: npm run test:schema-compatibility
      - name: Run Replay Simulation Test
        run: npm run test:simulate-replay
      - name: Build and Validate XDC archive
        run: |
          npm run build
          zip -r -FS app.xdc . -x '*.git*' 'node_modules/*'
          npx @webxdc/xdc-validator app.xdc

Ringkasan Postmortem Insiden

Insiden: Webxdc State Lockout via Corrupted Payload
Penyebab: Payload TASK_DELETE dikirim tanpa validasi keberadaan field targetId pada versi bundle v1.2.0, memicu uncaught exception pada loop pemrosesan event klien versi lama.
Dampak: 100% klien v1.0.0-v1.1.0 gagal render saat startup setelah membuka chat.
Remediasi: Menyiarkan payload COMPENSATING_RESET dengan versi epoch baru dari klien v1.2.1 untuk me-reset snapshot state dan mengabaikan serial bermasalah.
Tindakan Korektif: Implementasi Zod schema parsing pada entry-point setUpdateListener dan penambahan automated backward-compatibility test pada log event di CI.