Artikel mendalam tentang teknologi yang membentuk masa depan.

IRCv3: Pelajaran Protokol yang Kompatibel ke Belakang

IRCv3 memodernisasi protokol 1988 tanpa merusaknya. Pelajaran negosiasi kapabilitas, message tags, dan evolusi protokol yang kompatibel ke belakang.

Mesin telegraf kuno yang terhubung lewat kabel bercahaya ke smartphone modern di atas meja
Trik IRCv3: format kawat dari 1988 tetap berfungsi, tapi klien modern mendapat pengalaman modern.

Pada 1988, seorang mahasiswa universitas di Finlandia menulis protokol chat yang muat dalam beberapa halaman saja. Hampir empat dekade kemudian, protokol yang sama, yaitu IRC, masih mengirim jutaan pesan setiap hari, dan working group IRCv3 diam-diam mengajarkan kita cara berevolusi tanpa menghanguskan basis pengguna yang sudah terpasang. Kalau kamu pernah mencoba memversikan API yang dipakai orang asing, IRCv3 adalah studi kasus yang layak kamu contek.

Saya sudah membuat bot dan klien iseng untuk IRC selama bertahun-tahun, dan yang terus membuat saya terkejut adalah betapa modern rasanya ketika kedua sisi memakai ekstensi baru. Riwayat pesan dikirim ulang oleh server. Autentikasi yang sungguhan. Timestamp pesan yang tetap bertahan lewat bouncer. Namun klien yang ditulis tahun 1995 tetap bisa terhubung ke server yang sama dan mengobrol. Kombinasi itu tidak terjadi begitu saja. Itu hasil serangkaian keputusan desain yang disengaja, dan beberapa kesalahan menyakitkan yang harus diakali komunitas.

Dosa asal: kapabilitas yang tidak bisa ditanyakan

Protokol IRC inti, sebagaimana dikodifikasi dalam RFC 1459 dan kemudian RFC 2812, tidak punya mekanisme bagi klien untuk menanyakan apa yang didukung server. Tidak ada sama sekali. Kamu terhubung, mengirim NICK dan USER, lalu menebak-nebak. Ingin tahu apakah server mendukung mode atau ekstensi tertentu? Kirim saja dan lihat apakah muncul error. Ini terdengar kuno sampai kamu harus memelihara library klien yang menangani kasus khusus untuk lima implementasi server berbeda, yang masing-masing menafsirkan bagian spesifikasi yang ambigu dengan sedikit berbeda.

Mode kegagalan ini bukan teori. Developer menulis klien yang memeriksa fitur dengan mengirim perintah lalu mengurai numeric error, kekacauan yang rapuh dan patah setiap kali server mengubah string error. Bot bergabung ke channel dengan asumsi yang salah dan membanjiri log dengan kegagalan. Inilah celah spesifikasi-implementasi di habitat aslinya: ketika spesifikasi tidak memberi mekanisme penemuan, setiap implementer menciptakan sendiri, dan biasanya dengan buruk.

Perbaikan IRCv3 adalah negosiasi kapabilitas, dan saking sederhananya hampir memalukan. Saat registrasi koneksi, klien mengirim CAP LS dan server membalas dengan daftar kapabilitas bernama yang didukungnya, misalnya sasl, server-time, atau message-tags. Klien kemudian mengirim CAP REQ dengan subset yang persis diinginkannya, server mengonfirmasi, dan kedua pihak melanjutkan dengan pemahaman bersama yang eksplisit tentang apa yang didukung sesi itu. Klien lama yang tidak pernah mengirim CAP hanya mendapat perilaku lama. Server lama yang tidak memahami CAP mengembalikan error yang aman diabaikan klien sehingga bisa kembali ke perilaku dasar.

-> CAP LS 302
<- CAP * LS :sasl=PLAIN,EXTERNAL server-time message-tags echo-message
-> CAP REQ :sasl server-time
<- CAP * ACK :sasl server-time
-> AUTHENTICATE PLAIN
<- AUTHENTICATE +
-> AUTHENTICATE dXNlcgB1c2VyAHBhc3N3b3Jk
<- 900 jane jane!jane@host jane :You are now logged in

Pelajaran ini berlaku jauh di luar IRC. Kalau kamu sedang mendesain protokol atau API apa pun hari ini, bangun mekanisme negosiasi di versi pertama, meski belum ada yang perlu dinegosiasikan. Biayanya hanya satu pertukaran pesan; imbalannya, setiap fitur di masa depan menjadi opt-in alih-alih perubahan yang merusak.

Pelajaran: ekstensibilitas mengalahkan perintah baru

Ide besar kedua di IRCv3 adalah message tags, dan ini lahir dari kesalahan nyata: orang terus-menerus mencoba menambah fitur dengan mendefinisikan perintah baru atau menjejalkan semantik ke parameter pesan yang sudah ada. Format kawat IRC terkenal ringkas: sebuah prefix, sebuah perintah, dan daftar parameter trailing. Tidak ada tempat untuk menggantungkan metadata. Jadi ketika seseorang ingin timestamp pada riwayat yang dikirim ulang, atau menandai akun mana yang mengirim pesan, jawaban klasiknya adalah menciptakan numeric baru atau merusak teks trailing. Kedua pendekatan itu merusak klien lama atau memaksa setiap parser tumbuh kasus khusus.

Perbaikannya adalah menambahkan bagian baru yang opsional di depan pesan: @key=value;key2=value2 sebelum prefix. Yang krusial, server hanya mengirim tag ke klien yang sudah menegosiasikan kapabilitas message-tags, jadi klien lama tidak pernah melihat byte yang tidak bisa mereka urai. Begini wujud pesan ber-timestamp di kawat:

@time=2024-03-14T09:26:00.000Z;account=jane :jane!jane@host PRIVMSG #rust :borrow checker finally clicked for me

Ini langkah arsitektur yang sama seperti header HTTP, metadata gRPC, atau toleransi field tak dikenal di protobuf: sediakan titik ekstensi generik dan fitur berhenti membutuhkan operasi besar pada protokol. Ekstensi server-time, yang memperbaiki masalah lama bouncer di mana semua pesan yang diputar ulang terlihat baru saja datang, secara harfiah hanya satu tag. Draft +reply, yang memungkinkan klien merangkai balasan ke pesan tertentu, adalah tag lain. Tidak ada perintah baru. Tidak ada numeric baru. Tidak ada hari perubahan versi besar-besaran.

Saya sendiri pernah membuat kesalahan kebalikannya. Di sistem pub/sub internal beberapa tahun lalu, kami menambah fitur dengan menumpuk field JSON dengan prefix ajaib, karena mengubah format envelope "terlalu merepotkan". Delapan bulan kemudian kami harus memelihara regex di tiga bahasa untuk membongkar sihir itu, dan parser satu tim memperlakukan string ajaib tersebut sebagai data literal, yang memicu insiden yang tak terlupakan. Sebuah field metadata yang sederhana dan eksplisit akan memakan waktu sehari. String ajaib itu memakan waktu berkuartal-kuartal. Message tags IRCv3 adalah hal yang seharusnya dimiliki sistem itu sejak awal.

Amplop di jalur konveyor, sebagian membawa stiker metadata kecil berwarna-warni
Message tags: metadata opsional yang melekat per pesan, tak terlihat bagi klien yang tidak memintanya.

Pelajaran: hentikan workaround kata sandi telanjang

Tanya siapa pun yang menjalankan klien IRC di tahun 2000-an bagaimana proses login, dan jawabannya akan sama: kamu mengetik /msg NickServ IDENTIFY password. Kredensialmu terkirim sebagai pesan chat biasa, terlihat di log, kadang tanpa sengaja ter-paste ke channel publik ketika klienmu salah menjalankan perintah. Ritus peralihan ini begitu umum sampai jadi meme. Autentikasi ditempelkan di lapisan aplikasi karena protokolnya tidak punya konsep untuk itu.

IRCv3 membenahi ini dengan benar dengan menstandardisasi autentikasi SASL saat registrasi. Alih-alih mengobrol dengan bot setelah terhubung, klien menegosiasikan kapabilitas sasl dan melakukan autentikasi sebagai bagian dari handshake, sebelum punya nickname yang bisa dilihat siapa pun. Server tahu siapa kamu sejak pesan pertama, dan ini berimbas ke banyak hal: ekstensi account-tag bisa melekatkan nama akunmu ke setiap pesan, bot bisa mengambil keputusan izin tanpa heuristik parsing NickServ sendiri, dan klien bisa membangun alur registrasi tanpa harus membaca tembok teks NOTICE.

Pelajaran yang lebih luas: ketika sebuah workaround menjadi universal, ia berhenti menjadi workaround dan menjelma jadi protokol de facto milikmu, tanpa perhatian desain yang layak. NickServ tidak ada dalam RFC mana pun, tapi ia membentuk dua dekade perilaku klien, termasuk mode kegagalannya. Menstandardisasi hal yang sudah dilakukan orang (autentikasi saat terhubung) dengan mekanisme yang nyata (SASL, yang mencakup PLAIN, EXTERNAL untuk sertifikat klien, dan ruang untuk lebih banyak) mengubah folklor menjadi infrastruktur. Kalau penggunamu semua melakukan tarian canggung yang sama untuk menyelesaikan sesuatu, itu adalah item spesifikasi berikutnya, bukan FAQ dukungan.

Pelajaran: riwayat adalah masalah protokol, bukan masalah klien

Selama sebagian besar umur IRC, protokolnya murni live: kalau klienmu tidak terhubung saat sebuah pesan dikirim, pesan itu tidak pernah ada bagimu. Jawaban komunitas adalah bouncer, yaitu proxy yang selalu terhubung (ZNC adalah contoh klasiknya) yang berjalan di VPS, menjaga sesimu, dan memutar ulang apa yang terlewat. Bouncer memang berfungsi, tetapi itu tambahan infrastruktur yang harus dijalankan, kredensial lain yang harus dikelola, dan pengalaman replay-nya buruk: semuanya tiba dengan cap waktu replay, berantakan urutannya dibanding apa pun yang sudah kamu lihat secara langsung.

IRCv3 menyerang ini secara berlapis. Pertama, server-time membuat riwayat yang diputar ulang bisa dipakai dengan membawa timestamp penerimaan yang sebenarnya. Lalu draft chathistory memberi klien cara standar untuk meminta rentang riwayat, dan server seperti Ergo mulai menyimpan riwayat channel secara langsung. Hasilnya, seperti banyak orang temukan saat menjalankan server Ergo kecil untuk teman-teman, cukup mengejutkan: hubungkan dari ponselmu setelah seminggu offline dan server memutar ulang semuanya dengan timestamp yang benar, tanpa bouncer, tanpa repot mengurus VPS. Ini lompatan kualitas hidup terbesar yang pernah dimiliki IRC, dan datangnya dari protokol, bukan dari trik klien yang cerdas.

Ada argumen balasan yang layak diperhatikan, dan pengguna IRC harian sering mengajukannya: sebagian besar 'kekacauan' IRC sebenarnya ada di sisi klien, dan kamu bisa mendapatkan pengalaman yang baik hari ini dengan klien yang bagus dan bouncer. Benar, tapi argumen itu justru membuktikan hal sebaliknya. Setiap pembuat klien secara mandiri mengimplementasi ulang penyambungan scrollback, pelacakan highlight, dan deduplikasi replay, dengan bug yang sedikit berbeda-beda. Memindahkan bagian sulit ke protokol dan server berarti ia diimplementasi sekali, dengan benar, dan semua klien diuntungkan. Kita membuat keputusan serupa terus-menerus dalam pengembangan web: dorong invarian ke platform (lihat berapa banyak kode aplikasi yang hilang ketika browser mendapat fetch native, atau ketika TLS menjadi standar minimum) alih-alih membiarkan seribu codebase menyelesaikannya setengah-setengah.

Pelajaran: fitur opsional menciptakan realitas yang berbeda-beda

Sekarang soal kesalahan yang belum sepenuhnya diselesaikan IRCv3, dan ini yang paling relevan bagi siapa pun yang mendesain sistem yang bisa diperluas. Karena setiap ekstensi bersifat opsional dan dinegosiasikan secara independen, dua pengguna di channel yang sama bisa benar-benar melihat percakapan yang berbeda. Satu klien menerima pesan dengan tag +reply dan menampilkannya sebagai balasan berulir; klien lain menampilkan pesan yang sama sebagai baris datar tanpa konteks. Satu pengguna melihat reaksi emoji sebagai glyph rapi yang menempel pada pesan; pengguna lain tidak melihat apa-apa, atau fallback yang membingungkan. Matriks dukungan klien IRCv3 adalah kisi-kisi ya dan tidak, dan pengalamanmu bergantung pada kolom mana tempat kamu berada.

Ini sisi gelap dari degradasi yang anggun. Kompatibilitas ke belakang menjamin tidak ada yang rusak, tetapi tidak ada yang menjamin koherensi dalam modelnya. Slack dan Discord, apa pun pendapatmu tentang keduanya, tidak pernah punya masalah ini: platformnya mengendalikan kedua ujung cukup ketat sehingga reaksi tetap menjadi reaksi bagi semua orang. IRCv3 memilih evolusi di atas kontrol, dan harganya adalah lanskap fitur yang permanen tidak rata, di mana spesifikasi draft kadang bertahan sebagai draft selama bertahun-tahun.

Kalau kamu membangun sesuatu yang bisa diperluas, anggarkan hal ini sejak hari pertama. Beberapa mitigasi yang diambil IRCv3 dan ekosistem serupa:

  • Rancang degradasi secara sengaja. Tag balasan sebaiknya jatuh ke baris kutipan yang bisa dibaca, bukan menghilang. Kalau fallback-nya tidak bisa dipakai, fitur itu belum siap dirilis sebagai opsional.
  • Publikasikan matriks dukungan dan jaga kejujurannya. Tabel klien dan server IRCv3 membantu pengguna membuat pilihan yang tepat; ketidakjelasan hanya memicu menyalahkan perangkat lunak yang salah.
  • Definisikan baseline dan dorong adopsinya. Rilis bernomor (3.1, 3.2) mengemas kapabilitas sehingga klien bisa menargetkan "dukungan 3.2" alih-alih sebelas flag individual. Beri ekosistemmu lantai untuk dituju bersama.
  • Batasi umur draft-mu. Draft yang dikirim di server tetapi tidak pernah difinalisasi, dan riwayat channel pernah mendekati nasib ini, lebih buruk daripada tidak ada draft sama sekali karena ia memecah perilaku tanpa berkomitmen.

Kita pernah melihat versi dari hal ini dalam artikel tentang mengapa kolaborasi editing lebih sulit dari yang kamu kira: begitu peserta bisa tidak sepakat soal bentuk state bersama, kamu butuh aturan konvergensi yang eksplisit, bukan sekadar niat baik. Opsional-secara-default adalah strategi yang bagus untuk kapabilitas, tetapi semantik yang terlihat pengguna perlu rencana bagi orang-orang yang terjebak di sisi lain matriks.

Dua jendela bergaya yang menampilkan aliran yang sama, satu penuh utas dan simbol, satu polos
Ekstensi opsional berarti dua pengguna bisa memantau channel yang sama dan melihat percakapan yang berbeda.

Apa artinya untuk API atau protokol berikutnya

Lepaskan nostalgia chat, dan IRCv3 adalah playbook ringkas untuk mengevolusikan apa pun yang punya format kawat. Inilah checklist yang akan saya berikan ke tim mana pun yang memulai protokol, skema event, atau bahkan REST API publik:

  1. Kirim negosiasi lebih dulu. Pertukaran kapabilitas di versi pertama, meski daftar kapabilitasnya masih kosong. Menambahkan penemuan belakangan jauh sepuluh kali lebih sulit.
  2. Sediakan titik ekstensi. Tag, header, peta metadata: satu slot generik tempat fitur masa depan bisa digantungkan tanpa menyentuh tata bahasa inti.
  3. Buat perilaku baru bersifat opt-in per koneksi. Klien lama harus tetap menerima perilaku lama yang identik byte per byte. Server IRCv3 tidak pernah mengirim tag ke klien yang tidak memintanya.
  4. Standardisasi folklor. Apa pun workaround yang sudah disepakati penggunamu (auth gaya NickServ, replay gaya bouncer) adalah spesifikasi yang menunggu untuk ditulis dengan benar.
  5. Rencanakan matriks dukungan. Fitur opsional akan terpecah; putuskan seperti apa pengalaman yang terdegradasi dan terbitkan siapa mendukung apa.
  6. Kemas kemajuan dalam milestone bernama. Flag individual untuk implementer; rilis bernomor untuk manusia.

Yang menurut saya benar-benar mengesankan adalah pengekangannya. IRCv3 tidak mengejar enkripsi end-to-end, redesign federasi, atau penulisan ulang besar-besaran. Ia memilih masalah kecil yang tajam (penemuan, metadata, autentikasi, waktu) dan menyelesaikannya dengan cara yang bisa diabaikan oleh klien tahun 1995. Itulah sebabnya IRC hampir menjadi satu-satunya penyintas dari generasi protokol internet itu yang masih berjalan sebagian besar sesuai desain awalnya, dan mengapa proyek akhir pekan hari ini bisa memberi sekelompok teman sinkronisasi riwayat, TLS, dan akun sungguhan di VPS seharga 5 dolar.

Kompatibilitas ke belakang bukan ketiadaan perubahan. Ia adalah disiplin untuk membuat setiap perubahan menjadi sesuatu yang masa lalu bisa abaikan dengan aman.

Lain kali kamu tergoda memversikan API dengan mengganti path dari /v1 ke /v2 dan memaksa setiap konsumen bermigrasi, ingatlah protokol yang memungkinkan klien era dial-up dan aplikasi seluler modern berbagi channel yang sama. Negosiasikan kapabilitas. Lampirkan metadata alih-alih menciptakan perintah baru. Jaga lantainya tetap kokoh dan biarkan langit-langitnya naik sesuai jadwalnya sendiri. IRCv3 membuktikan kamu bisa punya protokol yang menghormati para pendahulunya dan tetap belajar trik baru, dan jika sistem chat dari 1988 bisa melakukannya, microservice tiga tahunmu tidak punya alasan.