एक सेकंड एक छोटी इकाई है — जब तक आप कामकाजी दिन के हर सेकंड में एक न लिख दें। 1 Hz पर आठ घंटे की सैंपलिंग 28,800 पंक्तियाँ हैं। डेस्क पर बैठे एक व्यक्ति का पूरा साल कहीं सत्तर लाख पंक्तियों के पार है, और हर एक छोटी, उबाऊ और अपनी पड़ोसन जैसी ही है। एक आर्किटेक्ट के लिए दिलचस्प सवाल यह नहीं है कि सत्तर लाख पंक्तियाँ कैसे रखी जाएँ — यह तो कोई भी डेटाबेस नींद में कर लेता है — बल्कि यह है कि उन्हें एक मशीन पर कैसे रखा जाए, जिसका मालिक एक व्यक्ति हो, बीच में कोई सर्वर न हो, ऐसे कि फ़ाइल 2050 में भी खुलने योग्य, क्वेरी करने योग्य और भरोसेमंद रहे।
यही डिज़ाइन समस्या Timex के पीछे है — हमारा Mac के लिए स्वचालित टाइम ट्रैकर। Timex एक क्लोज़्ड-सोर्स प्रोडक्ट है — वह ऐप जो आप gettimex.app पर खरीदते हैं — पर इसके नीचे का आर्किटेक्चर ठीक वैसी चीज़ है जिसके बारे में डेस्कटॉप टूल बनाने वाले हर इंजीनियर को आख़िरकार सोचना पड़ता है, और इसमें कुछ भी गुप्त नहीं है। तो यह पोस्ट डिज़ाइन गाइड है, सोर्स डंप नहीं। मैं पैटर्न की बात करूँगा: local-first, एक एम्बेडेड SQLite फ़ाइल, 1 Hz सैंपलिंग, और कच्चे सेकंडों को उन सेशनों में समेटना जिन्हें एक इंसान सचमुच पहचानता है। नीचे की स्कीमा और क्वेरीज़ मैंने इस पोस्ट के लिए आकार दिखाने को लिखी हैं, यह प्रोडक्ट की नकल नहीं हैं।
Muvon का डेवलपर स्टैक — Octomind, Octocode, और बाकी सब — Apache-2.0 के तहत ओपन सोर्स है। Timex वही है जिसे हम बंद रखते हैं। आर्किटेक्चर फिर भी ईमानदारी से बताने लायक है।
Local-first एक निर्णय है, कोई मिज़ाज नहीं
"Local-first" को मार्केटिंग शब्द की तरह बरता जाता है, जो अफ़सोसनाक है, क्योंकि असल में यह एक भार-वहन करने वाला आर्किटेक्चरल निर्णय है जिसके परिणामों को आप पहले ही दिन से अपना लेते हैं।
निर्णय यह है: सत्य का स्रोत यूज़र के डिवाइस पर रहता है, और नेटवर्क हटाने पर भी ऐप पूरी तरह काम करता है। "ऑफ़लाइन चलता है और बाद में सिंक करता है" नहीं — वह offline-first है, जो एक सिंक आर्किटेक्चर है जिसके ऊपर एक लोकल कैश कस दिया गया है। Local-first का मतलब है कोई बाद में है ही नहीं। कोई सर्वर नहीं जो प्रामाणिक प्रति रखे। डिस्क पर पड़ी फ़ाइल ही प्रोडक्ट की स्मृति है, और बाकी सब उसका एक दृश्य है।
एकल-यूज़र डेस्कटॉप टाइम ट्रैकर के लिए यह कठिन निर्णय नहीं है। विकल्पों पर चलते हैं:
- एक क्लाउड डेटाबेस। अब आप एक सर्वर चलाते हैं। हर यूज़र द्वारा प्रति सेकंड, हमेशा के लिए बनने वाले डेटा के लिए राइट पथ पर आपके पास नेटवर्क राउंड-ट्रिप है। आपके पास एक आउटेज बजट है, एक बैकअप रणनीति, एक GDPR सतह, एक ऐसी चीज़ जिसमें सेंध लग सकती है, और एक आवर्ती बिल जो आपको यूज़र पर सब्सक्रिप्शन के रूप में डालना पड़ता है। एक ऐसे टूल के लिए जिसका पूरा काम यह रिकॉर्ड करना है कि एक व्यक्ति ने एक Mac पर क्या किया, सर्वर शुद्ध देनदारी है।
- एक अपना बाइनरी लॉग। लुभावना — सिर्फ़-अपेंड, तेज़, सघन। पर अब फ़ॉर्मेट हमेशा के लिए आपका है। आप रीडर लिखते हैं, माइग्रेशन टूल, कॉम्पैक्शन लॉजिक, क्रैश-रिकवरी कोड। जिस दिन यूज़र "बस मेरे डेटा को क्वेरी करना चाहता हूँ" कहे, आपको एक क्वेरी इंजन देना पड़ेगा। आपने एक डेटाबेस फिर से ईज़ाद किया, बुरी तरह, और यूज़र को ऐसे फ़ॉर्मेट में बंद कर दिया जिसे सिर्फ़ आपका कोड समझता है।
- एक फ़ाइल में एक एम्बेडेड SQL डेटाबेस। एक फ़ाइल। कोई सर्वर नहीं। ACID ट्रांज़ैक्शन। एक क्वेरी भाषा जिसे सब पहले से जानते हैं। एक फ़ाइल फ़ॉर्मेट जो दो दशकों से स्थिर है और डेटासेट के लिए अमेरिकी लाइब्रेरी ऑफ़ कांग्रेस का आधिकारिक अनुशंसित भंडारण फ़ॉर्मेट है। उन सैकड़ों टूल्स से पढ़ने योग्य जो आपने नहीं लिखे।
तीसरा विकल्प SQLite है, और एकल-लेखक डेस्कटॉप ऐप के लिए यह कोई समझौता नहीं — यह सही उत्तर है। जो प्राइवेसी कहानी लोगों को पसंद आती है ("कोई क्लाउड नहीं, कोई टेलीमेट्री नहीं, आपका डेटा आपके Mac पर रहता है") वह इसी का परिणाम है। यह कोई नीति नहीं जो हमने लिखी। यह आर्किटेक्चर है: आपके गतिविधि डेटा के लीक होने का कोई एंडपॉइंट नहीं, क्योंकि वह फ़ाइल से बाहर जाता ही नहीं। (Timex जो इकलौती नेटवर्क कॉल करता है वह लाइसेंस सक्रियण है — आपके ट्रैकिंग इतिहास का उसमें कोई हिस्सा नहीं।)
ठीक SQLite ही क्यों
SQLite दुनिया का सबसे ज़्यादा तैनात डेटाबेस है — यह आपके फ़ोन, आपके ब्राउज़र, आपकी कार में है — और जिन कारणों से यह डेस्कटॉप ऐप पर फ़िट बैठता है, उन्हें खुलकर कहना ज़रूरी है, क्योंकि ये वही कारण हैं जिनके चलते सिर्फ़ क्लाइंट-सर्वर में सोचने वाले लोग इसे ख़ारिज कर देते हैं।
यह एम्बेडेड है, सर्वर नहीं। न कोई प्रक्रिया संभालनी, न कोई पोर्ट बाँधना, न कनेक्शन पूल, न pg_hba.conf। डेटाबेस आपके ऐप में लिंक की गई एक लाइब्रेरी है। "कनेक्शन" एक फ़ंक्शन कॉल है। एकल-यूज़र ऐप के लिए यह परिचालनगत चिंताओं की एक पूरी श्रेणी हटा देता है।
यह एक फ़ाइल है। आपकी बैकअप कहानी cp है। आपका "एक्सपोर्ट" "यह रही फ़ाइल" है। आपका सिंक, अगर कभी चाहिए, "फ़ाइल हिलाओ" है — कुछ चेतावनियों के साथ, जिन तक मैं पहुँचूँगा। यूज़र इसे ढूँढ सकता है, USB पर कॉपी कर सकता है, ऐसे फ़ोल्डर में डाल सकता है जिसे iCloud या Dropbox देखता हो, और आपके ऐप में कुछ नहीं टूटेगा। इसकी तुलना किसी ग़ैर-तकनीकी यूज़र को यह समझाने से करें कि क्लाउड अकाउंट से अपना डेटा कैसे एक्सपोर्ट करें।
इसे हर कोई क्वेरी कर सकता है। "अपने डेटा का मालिक बनो" के नारे की जगह सच होने के लिए यही हिस्सा सबसे अहम है। यूज़र का डेटा ऐसे फ़ॉर्मेट में है जिसे sqlite3 CLI, DB Browser for SQLite, Python का sqlite3 मॉड्यूल, DuckDB, Datasette, और धरती का लगभग हर एनालिटिक्स टूल सीधे पढ़ सकता है। आप अपने ही यूज़रों के डेटा के द्वारपाल नहीं हैं। वे उन सवालों के जवाब दे सकते हैं जिनके लिए आपने कभी कोई स्क्रीन बनाई ही नहीं।
यह टिकाऊ और ट्रांज़ैक्शनल है। SQLite ACID है। एक COMMIT या तो पूरा होता है या नहीं, चाहे राइट के बीच में बिजली चली जाए। एक सतत धारा रिकॉर्ड करने वाले टूल के लिए, "अगर ग़लत माइक्रोसेकंड पर ढक्कन धड़ाक से बंद हो जाए तो डेटाबेस का क्या होगा" का जवाब भ्रष्ट फ़ाइल की जगह एक उबाऊ, सही जवाब है।
जो आपत्ति आप सुनेंगे वह है "SQLite स्केल नहीं करता / समवर्ती लेखक नहीं संभालता"। दोनों सच और दोनों यहाँ अप्रासंगिक। एकल-यूज़र डेस्कटॉप ऐप में ठीक एक लेखक है: वह ख़ुद। जिसमें SQLite ख़राब है — कई मशीनें नेटवर्क पर राइट ठोक रही हों — वह यह आर्किटेक्चर कभी करता ही नहीं। एक्सेस पैटर्न से मेल खाता डेटाबेस चुनना ही पूरा खेल है, और यहाँ एक्सेस पैटर्न है एक लेखक, ज़्यादातर अपेंड, कभी-कभार विश्लेषणात्मक पठन। यह SQLite का अपना मैदान है।
1 Hz सैंपलिंग लूप
ट्रैकर का काम है सतत यह जवाब देना कि "यूज़र क्या कर रहा था"। तरीका एक सैंपलर है: प्रति सेकंड एक बार, OS से तीन सवाल पूछो।
- कौन-सा एप्लिकेशन सबसे आगे है?
- उसकी फ़ोकस्ड विंडो का शीर्षक क्या है?
- (अनुमति से, ब्राउज़रों के लिए) सक्रिय टैब का URL क्या है?
macOS पर ये जवाब फ्रंटमोस्ट-एप्लिकेशन API और Accessibility (AXUIElement) ट्री से आते हैं — वही अनुमति जो विंडो शीर्षक उजागर करती है, ब्राउज़र की एड्रेस बार भी उजागर करती है, इसीलिए Timex अधिकांश ब्राउज़रों के लिए अलग Automation प्रॉम्प्ट के बिना टैब URL पढ़ता है। इसमें कुछ भी प्रोप्राइटरी नहीं; यह macOS की प्रलेखित सतह है जिसे कोई भी ट्रैकर इस्तेमाल करता है। सैंपलों के साथ आप क्या करते हैं — डिज़ाइन वहीं रहता है।
भोली कार्यान्वयन प्रति सैंपल एक पंक्ति लिखता है। एक सेकंड, एक पंक्ति। यह सही है और फ़िज़ूलख़र्च: 8-घंटे के दिन के लिए 28,800 लगभग एक-जैसी पंक्तियाँ जहाँ आपने दो घंटे एक ही एडिटर विंडो में बिताए। "एडिटर, एडिटर, एडिटर, …" को 7,200 बार रखना डेटा नहीं, एक जला हुआ पिक्सेल है।
बेहतर मॉडल सेकंड-दर-सेकंड धारा को इनपुट और एक सेगमेंट को आउटपुट मानता है। एक सेगमेंट वह सतत दौर है जहाँ तीनों सवालों का जवाब एक-जैसा रहा। आप मौजूदा खुले सेगमेंट को मेमोरी में रखते हैं — (bundleId, title, url, startAt) — और हर टिक पर नए सैंपल की उससे तुलना करते हैं:
- वही संदर्भ? खुले सेगमेंट को बढ़ाओ। डिस्क पर कुछ नहीं जाता।
- संदर्भ बदला? खुले सेगमेंट को बंद करो (
endAtमुहर लगाओ,durationगणना करो), उसे लिखो, और एक नया खोलो।
तो दो-घंटे का एडिटर सेशन एक पंक्ति है दो घंटे की अवधि के साथ, 7,200 पंक्तियाँ नहीं। डिस्क राइट तभी देखती है जब सचमुच कुछ बदले — जो असली मानवीय काम के लिए प्रति सेकंड से कहीं कम बार होता है। 1 Hz लूप आपको रिज़ॉल्यूशन देता है; सेगमेंट मॉडल समझदार भंडारण देता है। आपको दोनों मिलते हैं।
टिक (1 Hz) ─▶ सैंपल(app, title, url)
│
├─ खुले सेगमेंट के बराबर? ──▶ मेमोरी में बढ़ाओ, कोई राइट नहीं
│
└─ अलग? ──▶ खुला सेगमेंट बंद + फ़्लश ──▶ नया खोलो
निष्क्रियता कोई संदर्भ नहीं है
अंधाधुंध चलने वाला सैंपलर ख़ुशी-ख़ुशी आठ घंटे "एडिटर" रिकॉर्ड कर देगा क्योंकि आपके लंच पर रहते एडिटर सबसे आगे था। वह डेटा झूठ है, और झूठ बोलने वाला टाइम ट्रैकर बिना ट्रैकर से भी बुरा है।
इसलिए एक आइडल गेट चाहिए: इनपुट गतिविधि ट्रैक करो, और बिना कीबोर्ड या माउस के किसी सीमा के बाद (Timex दो मिनट उपयोग करता है) अग्रभूमि ऐप को समय देना बंद कर दो। इसे मॉडल करने का ईमानदार तरीका निष्क्रियता को एक अवस्था-संक्रमण मानना है जो सक्रिय सेगमेंट को बंद कर देता है, ठीक एक संदर्भ-स्विच की तरह। जब इनपुट दोबारा शुरू होता है, एक ताज़ा सेगमेंट खुलता है। उनके बीच का अंतराल असली है — यह वह समय है जब मशीन चालू थी और इंसान नहीं — और आप उस अंतराल को स्कीमा में प्रथम-श्रेणी तथ्य की तरह रख सकते हैं या सेगमेंटों के बीच की खाली जगह से अनुमान लगा सकते हैं — Timex दूसरा तरीका अपनाता है।
यहाँ एक निर्णय दबा है: निष्क्रिय होने से ठीक पहले के सेकंड फिर भी उसी को दिए जाते हैं जो सबसे आगे था। कोई आदर्श जवाब नहीं (आप नहीं जान सकते कि यूज़र उठ गया जब तक वह लौटे न), तो डिज़ाइन एक सीमा चुनता है और उसे प्रलेखित करता है। धुँधले किनारे के बारे में ईमानदारी उस झूठी सटीकता से बेहतर है जो दिखावा करे कि सैंपलिंग मन पढ़ सकती है।
टाइम इंटरवल के लिए एक स्कीमा
यह रही एक उदाहरण-स्कीमा — मेरी, इस पोस्ट के लिए लिखी — जो सेगमेंट मॉडल को पकड़ती है। यह आकार है, प्रोडक्ट का असली DDL नहीं, पर यह बनाने लायक एक बिलकुल वाजिब चीज़ है।
-- बंद गतिविधि सेगमेंट: समेटी हुई इकाई, कच्चे सैंपल नहीं।
CREATE TABLE activity (
id INTEGER PRIMARY KEY,
bundle_id TEXT NOT NULL, -- उदा. com.apple.dt.Xcode
app_name TEXT NOT NULL,
title TEXT, -- फ़ोकस्ड विंडो शीर्षक (nullable)
url TEXT, -- सक्रिय टैब, केवल ब्राउज़र
domain TEXT, -- निकाला गया host, तेज़ समूहन हेतु
started_at INTEGER NOT NULL, -- unix epoch सेकंड
ended_at INTEGER NOT NULL,
duration INTEGER NOT NULL, -- ended_at - started_at, डीनॉर्मलाइज़्ड
category_id INTEGER REFERENCES category(id)
);
CREATE INDEX idx_activity_started ON activity(started_at);
CREATE INDEX idx_activity_bundle ON activity(bundle_id);
CREATE INDEX idx_activity_domain ON activity(domain) WHERE domain IS NOT NULL;
CREATE TABLE category (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
color TEXT NOT NULL, -- hex
score INTEGER NOT NULL -- उत्पादकता भार, -100..100
);
-- किसी ऐप को डिफ़ॉल्ट श्रेणी से जोड़ता है। यूज़र द्वारा बदला जा सकता है।
CREATE TABLE app_category (
bundle_id TEXT PRIMARY KEY,
category_id INTEGER NOT NULL REFERENCES category(id)
);
इसमें तीन डिज़ाइन विकल्प बचाव लायक हैं:
बंद इंटरवल रखो, कच्चे टिक नहीं। activity टेबल सेगमेंट हैं। प्रति-सेकंड सैंपल कभी सहेजे नहीं जाते — वे एक टिक मेमोरी में रहते हैं और खुले सेगमेंट में समा जाते हैं। यही फ़र्क है उस डेटाबेस में जो साल में ~70 लाख पंक्तियाँ बढ़ता है और उसमें जो उतनी बार बढ़ता है जितनी बार आपने सचमुच विंडो बदली, जो ज़्यादातर लोगों के लिए एक-दो परिमाण-कोटि कम है।
duration को डीनॉर्मलाइज़ करो। यह ended_at - started_at से निकाली जा सकती है, और फिर भी उसे रखना "जो गणना सकते हो उसे मत रखो" का जानबूझकर उल्लंघन है। औचित्य: UI की हर क्वेरी अवधियाँ जोड़ती है, और एक इंडेक्स वाला सहेजा हुआ पूर्णांक कॉलम हर "मेरा हफ़्ता कहाँ गया" सवाल पर लाखों पंक्तियों में घटाव दोबारा गिनने से बेहतर है। डीनॉर्मलाइज़ेशन एक औज़ार है; नियम यह है कि जिसे लगातार पढ़ते हो उसे डीनॉर्मलाइज़ करो और जिसे कभी-कभार पढ़ते हो उसे निकालो।
आधे-खुले इंटरवल [started_at, ended_at)। शुरुआत समावेशी, अंत अनन्य। समय-श्रेणियों के लिए यही उबाऊ-पर-सही परिपाटी है: दो सटे सेगमेंट एक सीमा-टाइमस्टैम्प साझा करते हैं बिना ओवरलैप और बिना अंतराल के, और "क्या यह सेगमेंट इस दिन में पड़ता है" एक साफ़ started_at < day_end AND ended_at > day_start है बिना आधी रात पर ऑफ़-बाय-वन के। इसे ग़लत करो तो हर समुच्चयन सूक्ष्म रूप से शापित हो जाता है।
जो मैं नहीं दिखा रहा वह है प्रोडक्ट का असली माइग्रेशन इतिहास, उसके सटीक इंडेक्स, या उसका श्रेणी-सीडिंग लॉजिक — वह प्रोप्राइटरी है, और सच कहें तो वह उबाऊ हिस्सा भी है। ऊपर का पैटर्न ही हस्तांतरणीय ज्ञान है।
क्वेरीज़ जो आप अपनी फ़ाइल पर चला सकते हैं
यहीं "अपने डेटा का मालिक बनो" नारा होना छोड़ देता है। चूँकि भंडार एक सादी SQLite फ़ाइल है, यूज़र — सिर्फ़ ऐप नहीं — उससे सवाल पूछ सकता है। ये ऊपर की उदाहरण-स्कीमा के विरुद्ध क्वेरीज़ हैं जिन्हें कोई भी यूज़र अपने डेटाबेस पर sqlite3 CLI से ढालकर चला सकता है।
आज कहाँ गया, ऐप के हिसाब से:
SELECT app_name, SUM(duration) / 3600.0 AS hours
FROM activity
WHERE started_at >= strftime('%s', 'now', 'start of day')
GROUP BY bundle_id
ORDER BY hours DESC;
इस हफ़्ते की शीर्ष वेबसाइटें:
SELECT domain, SUM(duration) / 60 AS minutes
FROM activity
WHERE domain IS NOT NULL
AND started_at >= strftime('%s', 'now', '-7 days')
GROUP BY domain
ORDER BY minutes DESC
LIMIT 20;
केंद्रित बनाम भटका समय, श्रेणी भार का उपयोग करके:
SELECT c.name,
SUM(a.duration) / 3600.0 AS hours
FROM activity a
JOIN category c ON c.id = a.category_id
WHERE a.started_at >= strftime('%s', 'now', '-30 days')
GROUP BY c.id
ORDER BY hours DESC;
इनमें से किसी को कोई फ़ीचर रिक्वेस्ट, CSV एक्सपोर्ट या API नहीं चाहिए था। यूज़र फ़ाइल को DuckDB में डालता है अगर विंडो फ़ंक्शन चाहिए, Datasette में अगर वेब UI चाहिए, pandas नोटबुक में अगर चार्ट चाहिए। प्रोडक्ट ने एक Today व्यू दिया; डेटा ने बाकी सब का जवाब देने की क्षमता दी। यही उस फ़ॉर्मेट को चुनने का लाभांश है जिसे पूरी दुनिया पढ़ सकती है।
टिकाऊपन: WAL, क्रैश और फ़ाइल वृद्धि
एक लैपटॉप पर सतत-लिखने वाले ऐप को, जिसका ढक्कन धड़ाक से बंद हो जाए, "राइट के बीच में प्रक्रिया मरने पर फ़ाइल का क्या होगा" का जवाब चाहिए। SQLite का जवाब अच्छा है, पर अच्छे संस्करण में आपको जान-बूझकर घुसना पड़ता है।
WAL मोड इस्तेमाल करो। डिफ़ॉल्ट रोलबैक जर्नल पाठकों को लेखक के विरुद्ध सीरियलाइज़ करता है और ज़्यादा fsync काम करता है। Write-Ahead Logging (PRAGMA journal_mode=WAL) बदलावों को एक -wal साइडकार फ़ाइल में लिखता है और पठनों को एकमात्र लेखक के साथ समानांतर चलने देता है — जो ठीक एक ट्रैकर का आकार है जो सेगमेंट अपेंड करता है जबकि UI उन्हें टाइमलाइन खींचने के लिए पढ़ता है। इसीलिए आप डिस्क पर तीन फ़ाइलें देखेंगे: timex.sqlite, timex.sqlite-wal, और timex.sqlite-shm। यह भ्रष्टता नहीं; यह WAL का अपना काम है। -wal समय-समय पर मुख्य फ़ाइल में वापस चेकपॉइंट करता है।
क्रैश सेफ़्टी असली है, पर sync ट्यून करो। WAL के तहत synchronous=NORMAL के साथ, बिजली जाने पर आपकी आख़िरी बिना-चेकपॉइंट ट्रांज़ैक्शन जा सकती है पर डेटाबेस भ्रष्ट नहीं हो सकता — एक सौदा जो अधिकांश डेस्कटॉप ऐप को ले लेना चाहिए, क्योंकि "आप अपने एडिटर में थे" के आख़िरी कुछ सेकंड खोना राउंडिंग त्रुटि है और टिकाऊपन/थ्रूपुट सौदा फ़ायदेमंद है। synchronous=FULL ज़्यादा सुरक्षित और धीमा है; सेकंड-रिज़ॉल्यूशन गतिविधि डेटा के लिए NORMAL समझदार डिफ़ॉल्ट है। जो आपको नहीं करना है वह है बेंचमार्क नंबरों के पीछे सिंक्रनाइज़ेशन बंद करके दौड़ना और फिर हैरान होना जब कर्नेल पैनिक फ़ाइल चट कर जाए।
राइट बैच करो। यद्यपि सेगमेंट सैंपलों की तुलना में विरल हैं, फ़्लश को ट्रांज़ैक्शन में लपेटना और हर नन्ही राइट पर fsync न करना डिस्क को ख़ुश रखता है। सैंपलर राइट थोड़ी देर रोक सकता है और एक छोटा बैच फ़्लश कर सकता है; UI को सेगमेंट के बंद होते ही उस माइक्रोसेकंड में डिस्क पर होने की ज़रूरत नहीं।
वृद्धि और प्रतिधारण की योजना बनाओ। सेगमेंट धीरे बढ़ते हैं, पर हमेशा बढ़ते हैं, और "हमेशा" 256 GB लैपटॉप पर लंबा समय है। ईमानदार डिज़ाइन यूज़र को एक प्रतिधारण घुंडी देता है — कच्चे सेगमेंट N महीने रखो, वैकल्पिक रूप से पुराने डेटा को मोटे दैनिक समुच्चयों में समेटो, और हटाने से जगह वापस पाने को कभी-कभी VACUUM करो। यह सब पहले दिन बनाने की ज़रूरत नहीं, पर पता होना चाहिए कि यह कहाँ जाता है, क्योंकि "वह डेटाबेस जो सिर्फ़ बढ़ता है" एक धीमी-गति का बग है।
Local-first के ईमानदार समझौते
अगर मैं अच्छाई पर रुक जाऊँ तो वही परीकथा बेचूँगा जिसकी मैं आलोचना कर रहा हूँ। Local-first आपको स्वामित्व और प्राइवेसी दो चीज़ें ख़र्च करके देता है, और आपको पता होना चाहिए कि उनकी कीमत क्या है।
कोई अंतर्निहित सिंक नहीं। अगर कोई व्यक्ति एक डेस्कटॉप और एक लैपटॉप इस्तेमाल करे, हर मशीन की अपनी फ़ाइल और अपना सत्य है। कोई सर्वर उन्हें मिलाता नहीं, क्योंकि उस सर्वर की अनुपस्थिति ही पूरा मुद्दा है। अपना-लाओ जवाब है फ़ाइल को ऐसे फ़ोल्डर में रखो जिसे iCloud या Dropbox सिंक करता हो — पर समझो कि यह फ़ाइल सिंक है, डेटाबेस सिंक नहीं, और SQLite स्पष्ट चेताता है कि एक WAL डेटाबेस उन भोले फ़ाइल-सिंक टूल्स के साथ अच्छा नहीं चलता जो .sqlite को तब कॉपी करते हैं जब -wal फ़्लश के बीच में हो। यह चल सकता है अगर फ़ाइल हिलते वक़्त ऐप बंद हो; यह भ्रष्ट हो सकता है अगर दो मशीनें एक ही फ़ोल्डर में एक साथ लिखें। तो ईमानदार रुख यह है: local-first आपको प्रति मशीन एक उत्तम प्रति देता है, और मल्टी-डिवाइस एक अलग, ऑप्ट-इन फ़ीचर है जिसे आप जान-बूझकर बनाते हैं या बिलकुल नहीं।
बैकअप यूज़र का काम है — तो उसे मामूली बनाओ। बिना क्लाउड, यूज़र के अलावा कोई यूज़र का इतिहास बैकअप नहीं करता। शमन यह है कि बैकअप एक फ़ाइल है जिसे आप कॉपी कर सकते हैं, जो सॉफ़्टवेयर की सबसे आसान बैकअप कहानी है। पर प्रोडक्ट को इसे साफ़ कहना होगा बजाय यह दिखावा करने के कि क्लाउड उसकी पीठ संभाल रहा है, क्योंकि वह नहीं, डिज़ाइन से।
और यहाँ वह हिस्सा है जिसे बेबाक कहना ठीक है: जिस पल आप असली मल्टी-डिवाइस सिंक चाहेंगे, आपने कंप्यूटिंग की सचमुच कठिन समस्याओं में से एक पर हस्ताक्षर कर दिए। स्वतंत्र रूप से संपादित करने वाले और ऑफ़लाइन जाने वाले डिवाइसों के बीच डेटाबेस सिंक करना संघर्ष-समाधान, CRDT, वेक्टर घड़ियों और last-writer-wins पछतावे का क्षेत्र है। यह "बस फ़ाइल rsync करो" नहीं है। बहुत से प्रोडक्ट जो local-first शुरू होते हैं और फिर सिंक कस देते हैं, आख़िरकार वही क्लाउड डेटाबेस फिर से बना बैठते हैं जिससे बचने पर वे गर्व करते थे — अब ऊपर से एक वितरित-सिस्टम बग सतह के साथ। बचाव लायक चाल है local-first को ईमानदार डिफ़ॉल्ट के रूप में देना, ऑप्ट-इन सिंक को एक जान-बूझकर, अलग से इंजीनियर किए फ़ीचर के रूप में दरवाज़ा खुला रखना, और सिंक की सुविधा को कभी चुपके से सत्य के स्रोत को सर्वर पर वापस घसीटने न देना। जिस दिन प्रामाणिक प्रति यूज़र की डिस्क के अलावा कहीं रहती है, आप अब local-first नहीं — आप एक अच्छे ऑफ़लाइन कैश वाला क्लाउड ऐप हैं, जो होना बिलकुल ठीक है, बस वह नहीं जो आपने कहा था।
यह यूज़र को क्या देता है
सब हटा दें तो आर्किटेक्चर ठीक एक वादा करता है: फ़ाइल आपकी है। यह उस कंपनी के अधिग्रहण से बच जाती है जिसने ऐप बनाई। यह उड़ान में आपका wifi बंद होने से बच जाती है। यह अगली सेंध-सुर्खी से बच जाती है, क्योंकि आपका ट्रैकिंग इतिहास किसी के सर्वर पर है ही नहीं कि उसमें सेंध लगे। यह उन टूल्स से पढ़ने योग्य है जो अगले दशक तक मौजूद नहीं होंगे, क्योंकि फ़ॉर्मेट अभी मौजूद ज़्यादातर टूल्स से पुराना है। आपको बाहर करने वाला कोई अकाउंट नहीं, कोई सब्सक्रिप्शन नहीं जिसकी चूक आपका इतिहास मिटा दे, कोई एक्सपोर्ट विज़ार्ड नहीं जिससे अपना ही डेटा माँगना पड़े।
यही local-first का पूरा मूल्य-प्रस्ताव है, और इसीलिए हमने Timex को एक SQLite फ़ाइल पर बनाया न कि ऐसे बैकएंड पर जिसे होस्ट, सुरक्षित और आख़िरकार बंद करना पड़ता। यही प्रवृत्ति Muvon के ओपन-सोर्स स्टैक में बहती है — आपका कोड, आपके टूल, आपका रेपो — और हमारे एकमात्र बंद प्रोडक्ट में: आपका समय, आपकी फ़ाइल, आपकी मशीन।
एक सेकंड एक छोटी इकाई है। उनमें से सत्तर लाख, एक ऐसी फ़ाइल में जिसके आप मालिक हैं और जिसे धरती के किसी भी टूल से क्वेरी कर सकते हैं, यह फ़र्क निकलता है कि "मेरा साल कहाँ गया" एक अलंकारिक सवाल हो या एक उत्तर-योग्य सवाल।
— Vladimir
अक्सर पूछे जाने वाले सवाल
डेस्कटॉप ऐप के लिए क्लाउड डेटाबेस की जगह SQLite क्यों?
क्योंकि एक्सेस पैटर्न एक मशीन पर एक लेखक है, ज़्यादातर अपेंड, कभी-कभार विश्लेषणात्मक पठन के साथ — जो ठीक वहीं है जहाँ एम्बेडेड एकल-फ़ाइल डेटाबेस चमकता है और क्लाइंट-सर्वर शुद्ध परिचालनगत ओवरहेड है। आपके डेटा के लिए कोई सर्वर नहीं मतलब कोई आउटेज बजट नहीं, कोई आवर्ती बिल नहीं, आपके इतिहास वाली कोई सेंध सतह नहीं, और बैकअप कहानी एक फ़ाइल कॉपी है।
हर सेकंड रखने के बजाय 1 Hz सैंपलों को सेगमेंट में क्यों समेटें?
प्रति-सेकंड रिज़ॉल्यूशन वही है जो आप सटीकता के लिए चाहते हैं; प्रति-सेकंड भंडारण बरबादी है, क्योंकि असली काम मिनटों या घंटों एक ही संदर्भ में बिताता है। समान लगातार सैंपलों को एक बंद इंटरवल में समेटना पंक्ति-संख्या को एक-दो परिमाण-कोटि घटा देता है बिना सटीकता खोए।
क्या मैं सचमुच अपना डेटा क्वेरी कर सकता हूँ?
हाँ — SQLite चुनने का यही मक़सद है। फ़ाइल sqlite3 CLI, DB Browser, DuckDB, Datasette, एक Python नोटबुक, या SQLite पढ़ने वाली किसी भी चीज़ में खुलती है। इस पोस्ट की उदाहरण-क्वेरीज़ दिखाई गई स्कीमा पर चलती हैं; उन्हें अपनी फ़ाइल के लिए ढाल लें।
क्या local-first और offline-first एक ही हैं?
नहीं। Offline-first एक क्लाउड ऐप है जिसमें एक लोकल कैश है जो जब हो सके तब सिंक करता है — सर्वर अब भी प्रामाणिक प्रति रखता है। Local-first का मतलब डिवाइस सत्य का स्रोत रखता है और कोई प्रामाणिक सर्वर प्रति है ही नहीं। अलग आर्किटेक्चर, अलग गारंटी।
Local-first में पेच क्या है?
कोई अंतर्निहित मल्टी-डिवाइस सिंक नहीं, और बैकअप आप पर। दोनों असली लागत हैं। शमन यह है कि "आपका डेटा" एक फ़ाइल है जिसे कहीं भी कॉपी कर सकते हैं — पर अगर आप चाहते हैं कि डिवाइस अपने आप संपादन मिलाएँ, वह एक अलग कठिन समस्या (संघर्ष-समाधान, CRDT) है जिसे कितना भी फ़ाइल-कॉपी हल नहीं करता।
क्या Timex ओपन सोर्स है?
नहीं। Timex एक क्लोज़्ड-सोर्स प्रोडक्ट है। Muvon के डेवलपर टूल — Octomind, Octocode और बाकी — Apache-2.0 के तहत ओपन सोर्स हैं। यह पोस्ट आर्किटेक्चर और पैटर्न के बारे में है, जो सामान्य इंजीनियरिंग ज्ञान हैं, प्रोडक्ट के सोर्स के बारे में नहीं।
Timex एक क्लोज़्ड-सोर्स, local-first स्वचालित Mac टाइम ट्रैकर है: 1 Hz सैंपलिंग एक लोकल SQLite फ़ाइल में जिसके आप मालिक हैं — आपकी गतिविधि का कोई क्लाउड भंडारण नहीं, आप क्या करते हैं इसकी कोई टेलीमेट्री नहीं; इकलौती नेटवर्क कॉल लाइसेंस सक्रियण है। यहाँ की स्कीमा और क्वेरीज़ उदाहरणात्मक हैं और इस पोस्ट के लिए लिखी गईं, प्रोडक्ट के सोर्स से नहीं। Muvon का डेवलपर स्टैक — Octomind और साथी — Apache-2.0 के तहत ओपन सोर्स है।


