Request Sequence Guard Menolak Response UI yang Terlambat

Interface interaktif sering mengirim request baru sebelum request sebelumnya selesai. Search box, filter, perubahan route, autocomplete, dan detail panel dapat menghasilkan pola ini. Urutan selesainya operasi jaringan tidak dijamin sama dengan urutan perubahan state oleh pengguna.

Perbedaan urutan tersebut dapat menghasilkan race yang halus. Request A dimulai lebih dahulu, request B dimulai setelahnya, B selesai lebih cepat, lalu interface merender B. Jika A kemudian selesai dan callback miliknya menulis tanpa memeriksa konteks, layar kembali ke data yang terkait dengan intent lama.

Request sequence guard memberikan nomor lokal yang meningkat secara monoton kepada setiap request yang relevan. Sebuah response hanya boleh mengubah interface jika nomornya masih cocok dengan sequence aktif untuk batas state tersebut.

Urutan selesai dapat berlawanan dengan urutan intent

Perhatikan sebuah search field:

t=0  query = "ca"    -> request 17
t=1  query = "cache" -> request 18
t=2  response 18 tiba
t=4  response 17 tiba

Request kedua mewakili input yang lebih baru, tetapi request pertama memerlukan waktu lebih lama. Callback yang menerapkan setiap response sukses menghasilkan urutan berikut:

render hasil untuk "cache"
render hasil untuk "ca"

Kedua response dapat sepenuhnya valid dari sisi server. Masalahnya bukan transport yang rusak atau API yang salah. Client menerapkan hasil valid ke state yang intent-nya sudah bergerak maju.

Variasi latency membuat race ini tidak selalu muncul. Jaringan development yang cepat dapat mempertahankan urutan request pada banyak percobaan, sedangkan traffic production, cache miss, retry, atau backend path yang berbeda lebih sering membalik urutan selesai.

Sequence number merepresentasikan urutan intent lokal

Component dapat menyimpan counter yang terkait dengan state yang sedang dimuat:

current_sequence = 0

load(query):
    current_sequence += 1
    my_sequence = current_sequence

    result = await fetch_results(query)

    if my_sequence != current_sequence:
        return

    render(result)

Request 17 menyimpan sequence 17. Ketika request 18 dimulai, sequence aktif berubah menjadi 18. Jika response 17 tiba setelahnya, guard gagal dan callback membuang hasil tersebut.

Counter tidak mengurutkan network packet dan tidak mengubah eksekusi server. Nilai itu hanya mencatat asynchronous continuation mana yang masih memiliki authority untuk mengubah bagian tertentu dari client state.

Pola ini merupakan bentuk kecil optimistic concurrency control. Pekerjaan dapat berjalan secara concurrent, tetapi commit hasilnya memerlukan version check terhadap state terkini.

Guard ditempatkan dekat dengan state mutation

Memeriksa sequence hanya sebelum request dikirim tidak melindungi write yang terjadi kemudian. Pemeriksaan penting dilakukan setelah pekerjaan asynchronous selesai dan tepat sebelum hasil mengubah state.

Flow dengan beberapa tahap asynchronous dapat memerlukan guard pada setiap batas side effect:

result = await fetch_results(query)

if stale(sequence):
    return

details = await enrich(result)

if stale(sequence):
    return

render(details)

Pemeriksaan kedua diperlukan karena intent baru dapat muncul saat enrich masih berjalan. Prinsip yang sama berlaku untuk callback, promise, worker message, dan model deferred execution lain.

Guard perlu melindungi setiap mutation yang validitasnya bergantung pada request generation yang sama. Mengubah data secara kondisional tetapi mengubah loading state tanpa guard masih dapat membuat request lama mematikan spinner atau mengganti error milik request yang lebih baru.

Cancellation mengurangi pekerjaan tetapi tidak menggantikan guard

Request API modern sering mendukung cancellation. Ketika query baru dimulai, client dapat melakukan abort terhadap request sebelumnya. Langkah ini berguna karena dapat melepaskan socket, parsing work, server effort, atau resource aplikasi.

Cancellation saja bukan aturan lengkap untuk stale response. Operasi lama mungkin selesai sesaat sebelum cancellation, transport mungkin tidak menyediakan cancellation yang andal, atau pekerjaan asynchronous setelah network request dapat tetap berjalan.

Pola yang kuat dapat memakai keduanya:

intent baru
  -> naikkan sequence
  -> cancel pekerjaan lama jika memungkinkan
  -> mulai pekerjaan baru
  -> terima hasil hanya jika sequence masih aktif

Cancellation terutama merupakan mekanisme pengelolaan resource. Sequence check merupakan admission rule untuk state mutation.

Equality biasanya lebih jelas daripada aturan greater-than

Untuk satu component generation, aturan yang umum adalah exact equality:

response_sequence == current_sequence

Callback hanya memiliki state jika tidak ada request baru yang menggantikannya. Menerima sequence yang lebih besar dari nilai tersimpan lebih cocok saat memproses stream revision yang diberikan sistem eksternal, sedangkan counter UI lokal memiliki invariant yang lebih sederhana: hanya generation yang sedang aktif boleh melakukan commit.

Counter juga memerlukan lifetime yang sesuai. Jika component dihancurkan lalu dibuat kembali, mempertahankan counter lama tidak bermasalah ketika instance baru memiliki state terisolasi. Namun, berbagi counter antar-instance yang tidak berkaitan dapat menghubungkan request lifecycle keduanya tanpa sengaja.

Pada bahasa dengan bounded integer, wraparound secara teori tetap relevan. Dalam lifetime UI biasa, integer yang cukup lebar membuat collision tidak praktis, sedangkan generation object unik atau opaque token dapat menghindari persoalan wrap numerik sepenuhnya.

Scope sequence harus mengikuti scope state

Satu counter global untuk seluruh page dapat terlalu luas. Jika pengguna memuat profile panel dan notification panel yang tidak berkaitan secara concurrent, pekerjaan baru pada satu panel seharusnya tidak membatalkan hasil panel lain.

State domain yang terpisah umumnya memakai generation terpisah:

profile_sequence
search_sequence
notification_sequence

Kesalahan sebaliknya adalah memakai counter terpisah untuk operasi yang bersaing menulis state yang sama. Jika dua request path sama-sama dapat mengganti searchResults, keduanya memerlukan definisi bersama mengenai intent yang aktif atau conflict rule lain yang eksplisit.

Scope yang tepat mengikuti mutation boundary, bukan selalu HTTP endpoint. Beberapa endpoint dapat berkontribusi pada satu state generation, sedangkan pemanggilan berulang ke satu endpoint dapat mengisi widget yang independen.

Loading dan error state memerlukan aturan ownership yang sama

Data sukses yang tiba di luar urutan adalah bentuk race yang paling terlihat, tetapi status state dapat gagal dengan pola serupa.

Misalkan request 31 lambat dan request 32 cepat gagal. Interface menampilkan error untuk 32. Jika request 31 kemudian sukses dan menghapus error tanpa guard, pengguna kehilangan status yang terkait dengan query aktif.

Hal yang sama berlaku pada loading flag:

request 31 mulai   -> loading = true
request 32 mulai   -> loading = true
request 31 selesai -> loading = false
request 32 masih aktif

State model yang sadar generation mengaitkan perubahan status dengan request yang memilikinya. Pilihan lain adalah menurunkan loading state dari active request record, bukan membiarkan callback mana pun mengubah shared boolean.

Server mutation memerlukan semantik yang lebih kuat

Client-side sequence guard cocok untuk menentukan response mana yang boleh mengubah presentation state lokal. Mekanisme ini tidak membuat server mutation yang tumpang tindih menjadi aman.

Jika request 17 dan request 18 sama-sama mengubah persistent data, membuang response 17 di browser tidak membatalkan request 17 di server. Server mungkin memerlukan version column, conditional request, idempotency key, transaction, serialization, atau domain-specific conflict handling.

Perbedaan ini penting pada autosave. UI dapat mengabaikan response lama sementara write lama tetap melakukan commit setelah write yang lebih baru pada persistent storage. Tampilan client dapat terlihat benar sampai reload berikutnya memperlihatkan urutan akhir di server.

Untuk mutation, concurrency rule harus menjangkau authority yang menyimpan data. Sequence number lokal tetap dapat membantu presentation, tetapi tidak dapat menggantikan server-side write ordering.

Test perlu memaksa pembalikan urutan

Test yang memberi setiap mock request delay sama sering melewatkan race ini. Test yang efektif mengendalikan completion order secara eksplisit:

mulai request A
mulai request B
selesaikan B
assert state == B
selesaikan A
assert state == B

Assertion terakhir menangkap invariant secara langsung: selesainya pekerjaan yang sudah digantikan tidak boleh mengubah state aktif.

Kasus tambahan dapat mencakup stale failure, stale loading cleanup, component disposal, cancellation, dan dua state scope yang independen. Kontrol completion yang deterministik lebih bernilai daripada berharap timing-based test kebetulan menghasilkan pembalikan urutan.

Request sequence guard adalah concurrency primitive yang ringkas untuk interface dengan asynchronous read yang tumpang tindih. Mekanisme ini tidak memaksa request selesai sesuai urutan mulai. Completion order dibuat tidak relevan terhadap state ownership karena setiap hasil harus membuktikan bahwa intent yang memulainya masih aktif pada saat hasil tersebut melakukan commit ke interface.