यह पृष्ठ अंग्रेज़ी से अनुवादित है। अंग्रेज़ी पृष्ठ पढ़ें
Zeyn के अनुरोध स्टेटलेस हैं: सर्वर कोई सॉकेट और प्रति उपयोगकर्ता Screen स्थिति नहीं रखता। कुछ Screen को अधिक चाहिए: एक डायग्राम को संपादित करते दो लोग, या ऐसी सूची जो तब अपडेट हो जब कोई और सहेजे। realtime Cargo feature इसके लिए वैकल्पिक WebSocket चैनल जोड़ता है। जो ऐप इसे चालू नहीं करते वे इसका कुछ कंपाइल नहीं करते, और जो पृष्ठ इसे इस्तेमाल नहीं करते वे कुछ लोड नहीं करते।
WebSocket अक्सर कॉर्पोरेट नेटवर्क पर अवरुद्ध होता है, इसलिए चैनल को सुधार मानें: हर Screen को इसके बिना भी काम करना चाहिए (डायग्राम एडिटर अब भी अपनी action से सहेजता है; सूची अब भी तब रिफ़्रेश होती है जब उपयोगकर्ता उसे फिर खोलता है)।
1. इसे चालू करना
zeyn = { version = "...", features = ["realtime"] }let realtime = Realtime::new()
.documents("diagram", "diagram.read", "diagram.write", MyDocStore::new(pool))
.topic("orders", "sales.order.read")
.limits(Limits::default());
let hub = realtime.hub(); // clone it into stores that notify
App::new(..).realtime(realtime) // also registers `Hub` as a service2. एंडपॉइंट
| एंडपॉइंट | उद्देश्य |
|---|---|
GET /_zeyn/live/doc/{kind}/{doc}?csrf=TOKEN | WebSocket: सहयोगी दस्तावेज़ |
GET /_zeyn/live/topics?t=a,b&csrf=TOKEN | WebSocket: topic के लिए बदलाव सूचनाएँ |
GET /_zeyn/live.js | topic स्क्रिप्ट, केवल उन पृष्ठों द्वारा जोड़ी जाती है जो सब्सक्राइब करते हैं |
हर हैंडशेक अपग्रेड से पहले इस क्रम में जाँचा जाता है:
- चैनल चालू है (वरना
404)। - Origin:
Originहेडर को वही होस्ट नाम देना होगा जिस पर अनुरोध आया, याRealtime::allow_originसे जोड़ा गया origin। ब्राउज़र इसे WebSocket हैंडशेक पर हमेशा भेजते हैं; छूटा हुआ अस्वीकार होता है। यही दूसरी साइट को उपयोगकर्ता की कुकी के साथ सॉकेट खोलने से रोकता है। - CSRF टोकन: ब्राउज़र WebSocket पर हेडर सेट नहीं कर सकते, इसलिए
meta[name=zeyn-csrf]का टोकनcsrfक्वेरी पैरामीटर में जाता है औरzeyn_csrfकुकी से constant time में तुलना किया जाता है, ठीक वैसे जैसेPOSTके लिएx-zeyn-csrf। विफलताएँ403से उत्तर पाती हैं। - आइडेंटिटी: हर अनुरोध जैसा ही ऑथेंटिकेटर; उपयोगकर्ता साइन इन होना चाहिए (
403)। - प्रति उपयोगकर्ता सीमा (
429)। - दस्तावेज़: घोषित kind और सादा आईडी (1 से 64 अक्षर, अंक,
-,_,.), वरना404; kind की पठन अनुमति (403)। Topic: हर topic घोषित और उसकी अनुमति धारित (403)। - इंटरसेप्टर: ऐप के इंटरसेप्टर
Livekind की कॉल के साथ चलते हैं (screenदस्तावेज़ का kind है, topic के लिए खाली), इसलिएRequireMfaया निलंबित खाता जाँच जैसा ऐप-व्यापी नियम लाइव चैनल को भी कवर करता है। अस्वीकृति अपने स्टेटस से उत्तर देती है। - रूम सीमाएँ: मेमोरी में रूम और प्रति रूम क्लाइंट (
Retry-Afterके साथ503)।
लेखन अनुमति प्रति कनेक्शन एक बार जाँची जाती है: उसके बिना क्लाइंट अपडेट पाता है लेकिन जैसे ही वह कोई भेजता है 1008 से बंद कर दिया जाता है।
3. दस्तावेज़ और Y-CRDT
दस्तावेज़ Y-CRDT दस्तावेज़ हैं जिन्हें yrs से रखा जाता है। रूम टेनेंट, kind और दस्तावेज़ आईडी से कुंजीबद्ध हैं, इसलिए एक जैसी आईडी के साथ भी दो टेनेंट कभी रूम साझा नहीं करते।
फ़्रेम बाइनरी हैं। पहला बाइट टैग है; 1 (FRAME_UPDATE) v1 एन्कोडिंग में Y अपडेट रखता है।
- कनेक्ट होने पर सर्वर पूरा दस्तावेज़ एक अपडेट के रूप में भेजता है।
- क्लाइंट अपने बदलावों के अपडेट भेजता है; सर्वर हर एक लागू करता है (जो फ़्रेम वैध अपडेट नहीं है वह सॉकेट को
1008से बंद करता है) और वही बाइट रूम के हर दूसरे क्लाइंट को देता है। - हर (पुनः) कनेक्ट के बाद island अपनी पूरी स्थिति वापस भेजता है, इसलिए ऑफ़लाइन किए संपादन मर्ज हो जाते हैं। Y अपडेट idempotent और commutative हैं, इसलिए क्रम और दोहराव मायने नहीं रखते।
स्थायित्व trait से जाता है:
pub trait DocStore: Send + Sync + 'static {
fn load(&self, key: RoomKey) -> BoxFuture<Result<Option<Vec<u8>>>>;
fn save(&self, key: RoomKey, state: Vec<u8>) -> BoxFuture<Result<()>>;
}load तब चलता है जब रूम खुलता है (त्रुटि कनेक्शन को 503 से अस्वीकार करती है, यही वह तरीका भी है जिससे स्टोर सीमित करता है कि कौन से दस्तावेज़ मौजूद हैं)। save पूरा दस्तावेज़ एक अपडेट के रूप में पाता है और दस्तावेज़ में बदलाव होने पर हर persist_every पर चलता है, और निष्क्रिय रूम छोड़े जाने से पहले। विफल सेव अगले राउंड में फिर आज़माया जाता है और रूम रखा जाता है। MemoryDocStore टेस्ट और डेमो के लिए है।
island की ओर, realtime feature के साथ zeyn-island live::Channel देता है:
let channel = Channel::document("diagram", "main", on_open, on_update, on_state)?;
channel.send_update(&update); // after a local change
channel.close(); // in unmountयह 1 सेकंड बाद फिर कनेक्ट होता है, 30 सेकंड तक दोगुना करते हुए, और close या 1008 के बाद रुक जाता है।
4. Topic: Screen जो उसी जगह रिफ़्रेश होती हैं
Live::new("orders").add(DataGrid::of(search_orders)...)
#[action(permission = "sales.order.write")]
pub async fn save_order(o: Valid<Order>, store: Orders, hub: Hub, ctx: Ctx) -> Result<Order> {
let saved = store.save(o.into_inner())?;
hub.notify(&ctx, "orders");
Ok(saved)
}Live div[data-zeyn-live="orders"] रेंडर करता है। जिस पृष्ठ की Screen में एक हो वह /_zeyn/live.js जोड़ता है (जो Screen nav.js से आती है उसे x-zeyn-live: 1 मिलता है और स्क्रिप्ट तब इम्पोर्ट होती है)। स्क्रिप्ट पृष्ठ के सभी topic के लिए एक सॉकेट खोलती है। सूचना पर हर सब्सक्राइब किया एलिमेंट अपने ग्रिड उसी जगह रीलोड करता है (zeyn:reload, ग्रिड का मौजूदा पृष्ठ, क्रम और फ़िल्टर), या जब कोई ग्रिड न हो तो पूरी Screen उसी जगह (zeyn:refresh)। 300 ms के भीतर की सूचनाएँ एक बार संभाली जाती हैं।
सूचनाएँ केवल topic का नाम रखती हैं, कभी डेटा नहीं: रिफ़्रेश हर अनुमति जाँच के साथ सामान्य ग्रिड एंडपॉइंट से जाता है। notify केवल कॉलर के टेनेंट तक पहुँचता है; notify_tenant(Some(id), topic) एक को नाम देता है, None बिना टेनेंसी वाला ऐप है।
5. मेमोरी और बैकप्रेशर
स्थिति का हर टुकड़ा सीमित है (Limits, डिफ़ॉल्ट कोष्ठक में):
| सीमा | डिफ़ॉल्ट | पहुँचने पर |
|---|---|---|
per_user सॉकेट | 8 | हैंडशेक 429 |
per_room क्लाइंट | 32 | हैंडशेक 503 |
मेमोरी में rooms | 256 | हैंडशेक 503 |
क्लाइंट से message_bytes | 64 KB | सॉकेट बंद |
प्रति दस्तावेज़ doc_bytes | 1 MB | अपडेट अस्वीकार और सॉकेट बंद (1009) |
प्रति क्लाइंट queue | 32 संदेश | क्लाइंट रूम से हटाया जाता है और 1013 से बंद |
बिना क्लाइंट का idle रूम | 30 s | सहेजा जाता है और मेमोरी से हटाया जाता है |
heartbeat | 20 s | एक ping; 3 अंतराल चुप रहने वाला क्लाइंट बंद |
persist_every | 2 s | बदले दस्तावेज़ सहेजे जाते हैं |
ब्रॉडकास्ट कभी इंतज़ार नहीं करते: अपडेट try_send से हर क्लाइंट की सीमित कतार को दिया जाता है। जिस क्लाइंट की कतार भरी हो उसे तुरंत रूम से हटा दिया जाता है, इसलिए एक धीमा पाठक मेमोरी नहीं रोक सकता या दूसरों को धीमा नहीं कर सकता; वह फिर कनेक्ट होता है और पूरा दस्तावेज़ पाता है। भरी कतार वाला topic सब्सक्राइबर केवल वह सूचना चूकता है (अगली फिर भी उसे रिफ़्रेश करती है)।
इसलिए सबसे बुरी स्थिति की मेमोरी दस्तावेज़ों के लिए लगभग rooms * doc_bytes और कतारों के लिए clients * queue * message_bytes है (अपडेट कतारों के बीच साझा होता है, कॉपी नहीं)। डायग्राम पर दो लाइव टैब वाला शोकेस सर्वर कुल मिलाकर लगभग 19 MB इस्तेमाल करता है (डिबग बिल्ड)। Hub::stats() निगरानी के लिए रूम, क्लाइंट, सब्सक्राइबर और दस्तावेज़ बाइट बताता है।
6. सुरक्षा टिप्पणियाँ
- चैनल कभी action नहीं चलाता: दस्तावेज़ वह डेटा हैं जिसे ऐप अपने
DocStoreसे पढ़ता है, जहाँ वह उन्हें सत्यापित करता है (शोकेस हर सहेजे डायग्राम कोDiagram::clampedसे सीमित करता है)। - दस्तावेज़ को टेनेंट में kind की पठन अनुमति वाला कोई भी पढ़ सकता है; अलग दर्शकों वाले दस्तावेज़ों के लिए अलग kind (या ऐसी आईडी जिन्हें स्टोर अस्वीकार करे) इस्तेमाल करें।
- हर पृष्ठ की नीति
default-src 'self'से ऐप के अपने origin पर WebSocket की अनुमति देती है; चैनल के लिए content security policy में कुछ ढीला नहीं किया जाता।
7. उदाहरण
examples/showcase: Islands, Diagram editor को दो टैब में खोलें। एक टैब में आकृति हिलाना या जोड़ना दूसरे में दिखता है, और नीचे का ग्रिड "Shapes stored on the server" लगभग दो सेकंड में उसी जगह रिफ़्रेश होता है, जब दस्तावेज़ DocStore से सहेजा जाता है और diagram topic को सूचित किया जाता है।
वर्तमान संस्करण की सीमाएँ: रूम एक प्रक्रिया में रहते हैं (कई सर्वर इंस्टेंस को sticky सत्र या साझा ब्रोकर चाहिए), उपस्थिति (और कौन यहाँ है, कर्सर) बिल्ट इन नहीं है, और दस्तावेज़ हर कनेक्ट पर पूरा भेजा जाता है।