Skip to content

Commit 0a2df4c

Browse files
committed
feat(registry): add registry index, cache and workers
1 parent 78f88da commit 0a2df4c

File tree

8 files changed

+513
-3
lines changed

8 files changed

+513
-3
lines changed

vix-site/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"docs:preview": "vitepress preview docs",
1414
"release": "npm run build && npm run docs:build && node scripts/deploy.js",
1515
"deploy": "npm run release",
16-
"preview": "vite preview"
16+
"preview": "vite preview",
17+
"registry:build": "node tools/build_registry_index.js"
1718
},
1819
"dependencies": {
1920
"vue": "^3.5.24",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"meta":{"registryId":"vixcpp-registry","specVersion":"1.0.0","generatedAt":"2026-02-10T15:36:29.772Z","sourceRepo":"https://github.com/vixcpp/registry","indexFormat":"json-per-package","entryCount":2},"entries":[{"description":"","displayName":"binary_search","homepage":"https://github.com/Gaspardkirira/binary_search","keywords":[],"latest":"0.1.1","license":"MIT","maintainers":[{"github":"","name":""}],"manifestPath":"vix.json","name":"binary_search","namespace":"gaspardkirira","repo":{"defaultBranch":"main","url":"https://github.com/Gaspardkirira/binary_search"},"type":"header-only","versions":{"0.1.1":{"commit":"b2cc3302637f336f2a798f8bf4e855c4b20f7522","notes":"Fix registry id + publishable commit","tag":"v0.1.1"}}},{"description":"Tiny header-only example library for demonstrating the Vix Registry workflow.","displayName":"tree","homepage":"https://github.com/GaspardKirira/tree","keywords":["c++","header-only","demo","registry","vix"],"latest":"0.7.0","license":"MIT","maintainers":[{"github":"GaspardKirira","name":"Gaspard Kirira"}],"manifestPath":"vix.json","name":"tree","namespace":"gaspardkirira","repo":{"defaultBranch":"main","url":"https://github.com/GaspardKirira/tree"},"type":"header-only","versions":{"0.1.0":{"commit":"ffcf9d703e6113f5ac5887c99b45baaccf7e4937","notes":"Initial demo release","tag":"v0.1.0"},"0.2.0":{"commit":"b79ba53ca60a78a3c4b7baaa1da728907ee0a5fa","notes":"Add count_leaves helper","tag":"v0.2.0"},"0.5.0":{"commit":"b184d0cf2220f825ca41139bede5543c5e717df4","notes":"Add index_by_id helper","tag":"v0.5.0"},"0.6.0":{"commit":"6d6a9abdf1b80a85b2f97832153b754f9f746629","notes":"Add count_internal_nodes helper","tag":"v0.6.0"},"0.7.0":{"commit":"18363babf297b4050d2585596238012faee65d52","notes":"Add tree helper","tag":"v0.7.0"}}}]}

vix-site/src/data/github_stats.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"repo": "vixcpp/vix",
3-
"fetched_at": "2026-02-10T15:24:19.487Z",
3+
"fetched_at": "2026-02-10T15:36:35.833Z",
44
"stars": 267,
55
"forks": 22,
66
"open_issues": 182,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { getCachedRegistry, setCachedRegistry } from "@/lib/registryCache";
2+
3+
const REGISTRY_URL = "/registry/index/all.min.json";
4+
5+
async function safeFetchJson(url, timeoutMs = 2500) {
6+
const ctrl = new AbortController();
7+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
8+
9+
try {
10+
const res = await fetch(url, {
11+
cache: "no-cache",
12+
signal: ctrl.signal,
13+
});
14+
if (!res.ok) throw new Error(`http_${res.status}`);
15+
return await res.json();
16+
} finally {
17+
clearTimeout(t);
18+
}
19+
}
20+
21+
export async function loadRegistryIndex() {
22+
// 1) instant: cache d'abord
23+
const cached = await getCachedRegistry();
24+
if (cached?.data) {
25+
// on tente un refresh en parallèle (mais sans bloquer l'UI)
26+
refreshInBackground(cached.meta?.generatedAt || "");
27+
return { source: "cache", data: cached.data };
28+
}
29+
30+
// 2) sinon: réseau
31+
const data = await safeFetchJson(REGISTRY_URL, 6000);
32+
await setCachedRegistry(data.meta || null, data);
33+
return { source: "network", data };
34+
}
35+
36+
async function refreshInBackground(currentVersion) {
37+
try {
38+
const data = await safeFetchJson(REGISTRY_URL, 2500);
39+
const v = data?.meta?.generatedAt || "";
40+
if (v && v !== currentVersion) {
41+
await setCachedRegistry(data.meta || null, data);
42+
}
43+
} catch {
44+
// ignore: réseau instable
45+
}
46+
}

vix-site/src/lib/registryCache.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const DB_NAME = "vix_registry_cache";
2+
const DB_VERSION = 1;
3+
4+
const STORE_META = "meta";
5+
const STORE_BLOBS = "blobs";
6+
7+
function openDb() {
8+
return new Promise((resolve, reject) => {
9+
const req = indexedDB.open(DB_NAME, DB_VERSION);
10+
11+
req.onupgradeneeded = () => {
12+
const db = req.result;
13+
14+
if (!db.objectStoreNames.contains(STORE_META)) {
15+
db.createObjectStore(STORE_META, { keyPath: "key" });
16+
}
17+
if (!db.objectStoreNames.contains(STORE_BLOBS)) {
18+
db.createObjectStore(STORE_BLOBS, { keyPath: "key" });
19+
}
20+
};
21+
22+
req.onsuccess = () => resolve(req.result);
23+
req.onerror = () => reject(req.error);
24+
});
25+
}
26+
27+
function txGet(store, key) {
28+
return new Promise((resolve, reject) => {
29+
const req = store.get(key);
30+
req.onsuccess = () => resolve(req.result || null);
31+
req.onerror = () => reject(req.error);
32+
});
33+
}
34+
35+
function txPut(store, value) {
36+
return new Promise((resolve, reject) => {
37+
const req = store.put(value);
38+
req.onsuccess = () => resolve(true);
39+
req.onerror = () => reject(req.error);
40+
});
41+
}
42+
43+
export async function getCachedRegistry() {
44+
const db = await openDb();
45+
const tx = db.transaction([STORE_META, STORE_BLOBS], "readonly");
46+
const metaStore = tx.objectStore(STORE_META);
47+
const blobStore = tx.objectStore(STORE_BLOBS);
48+
49+
const meta = await txGet(metaStore, "registry_meta");
50+
const blob = await txGet(blobStore, "registry_all_json");
51+
52+
db.close();
53+
54+
if (!meta || !blob || !blob.json) return null;
55+
56+
return {
57+
meta: meta.value || null,
58+
data: blob.json,
59+
};
60+
}
61+
62+
export async function setCachedRegistry(meta, json) {
63+
const db = await openDb();
64+
const tx = db.transaction([STORE_META, STORE_BLOBS], "readwrite");
65+
const metaStore = tx.objectStore(STORE_META);
66+
const blobStore = tx.objectStore(STORE_BLOBS);
67+
68+
await txPut(metaStore, { key: "registry_meta", value: meta });
69+
await txPut(blobStore, { key: "registry_all_json", json });
70+
71+
db.close();
72+
return true;
73+
}
Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,184 @@
1+
<script setup>
2+
import { ref, onMounted, watch, onBeforeUnmount } from "vue";
3+
import { useRoute } from "vue-router";
4+
import { loadRegistryIndex } from "@/lib/loadRegistryIndex";
5+
6+
import RegistrySearchWorker from "@/workers/registrySearch.worker.js?worker";
7+
8+
const route = useRoute();
9+
10+
const q = ref((route.query.q || "").toString());
11+
const hits = ref([]);
12+
const total = ref(0);
13+
const loading = ref(true);
14+
const version = ref("");
15+
const error = ref("");
16+
17+
const worker = new RegistrySearchWorker();
18+
19+
function doSearch() {
20+
worker.postMessage({
21+
type: "search",
22+
query: q.value || "",
23+
limit: 20
24+
});
25+
}
26+
27+
onMounted(async () => {
28+
worker.onmessage = (ev) => {
29+
const msg = ev.data || {};
30+
if (msg.type === "loaded") {
31+
version.value = msg.version || "";
32+
loading.value = false;
33+
doSearch();
34+
}
35+
if (msg.type === "searchResult") {
36+
hits.value = msg.hits || [];
37+
total.value = msg.total || 0;
38+
version.value = msg.version || version.value;
39+
}
40+
if (msg.type === "error") {
41+
error.value = msg.error || "worker_error";
42+
}
43+
};
44+
45+
try {
46+
const { data } = await loadRegistryIndex();
47+
worker.postMessage({ type: "load", data });
48+
} catch {
49+
loading.value = false;
50+
error.value = "cannot_load_registry";
51+
}
52+
});
53+
54+
onBeforeUnmount(() => {
55+
worker.terminate();
56+
});
57+
58+
watch(
59+
() => route.query.q,
60+
(v) => {
61+
q.value = (v || "").toString();
62+
doSearch();
63+
}
64+
);
65+
</script>
66+
167
<template>
2-
<h1>Registry Browse</h1>
68+
<section class="reg-browse">
69+
<header class="top">
70+
<div>
71+
<h1 class="title">Browse packages</h1>
72+
<p v-if="version" class="muted">Index: {{ version }}</p>
73+
</div>
74+
75+
<div v-if="q" class="muted">Query: "{{ q }}"</div>
76+
</header>
77+
78+
<div v-if="loading" class="muted">Loading registry…</div>
79+
<div v-else-if="error" class="muted">Error: {{ error }}</div>
80+
81+
<div v-else>
82+
<div v-if="!q" class="muted">Type a query to search.</div>
83+
84+
<div v-else-if="total === 0" class="muted">
85+
No results for "{{ q }}".
86+
</div>
87+
88+
<div v-else class="muted">Found {{ total }} result(s).</div>
89+
90+
<ul class="list">
91+
<li v-for="h in hits" :key="h.id" class="item">
92+
<div class="row">
93+
<div class="id">{{ h.id }}</div>
94+
<div class="ver">{{ h.latest }}</div>
95+
</div>
96+
97+
<div v-if="h.description" class="desc">{{ h.description }}</div>
98+
99+
<a
100+
v-if="h.repo"
101+
class="repo"
102+
:href="h.repo"
103+
target="_blank"
104+
rel="noreferrer"
105+
>
106+
{{ h.repo }}
107+
</a>
108+
</li>
109+
</ul>
110+
</div>
111+
</section>
3112
</template>
4113

114+
<style scoped>
115+
.reg-browse{
116+
padding: 26px 18px;
117+
max-width: 980px;
118+
margin: 0 auto;
119+
}
120+
121+
.top{
122+
display: flex;
123+
justify-content: space-between;
124+
align-items: baseline;
125+
gap: 16px;
126+
flex-wrap: wrap;
127+
}
128+
129+
.title{
130+
margin: 0;
131+
font-size: 1.55rem;
132+
font-weight: 900;
133+
color: #e5f9f6;
134+
}
135+
136+
.muted{
137+
color: rgba(148,163,184,.95);
138+
}
139+
140+
.list{
141+
list-style: none;
142+
padding: 0;
143+
margin: 18px 0 0;
144+
display: grid;
145+
gap: 14px;
146+
}
147+
148+
.item{
149+
border: 1px solid rgba(148,163,184,.18);
150+
border-radius: 12px;
151+
padding: 14px;
152+
background: rgba(2,6,23,.35);
153+
}
154+
155+
.row{
156+
display: flex;
157+
justify-content: space-between;
158+
gap: 12px;
159+
align-items: baseline;
160+
}
161+
162+
.id{
163+
font-weight: 900;
164+
color: #e5f9f6;
165+
}
166+
167+
.ver{
168+
font-weight: 800;
169+
color: rgba(94,234,212,.95);
170+
}
171+
172+
.desc{
173+
margin-top: 6px;
174+
color: rgba(226,232,240,.92);
175+
}
5176
177+
.repo{
178+
display: inline-block;
179+
margin-top: 8px;
180+
color: rgba(147,197,253,.95);
181+
text-decoration: underline;
182+
text-underline-offset: 3px;
183+
}
184+
</style>

0 commit comments

Comments
 (0)