Stable Asura series IDs: strip rotating build hash (#6)
Asura series slugs carry a site-wide build hash (-059befe1) that rotates on every redeploy, silently orphaning all asura bookmarks (old-hash URLs 302 to new-hash ones, so detect() yields keys that never match stored rows).
Backend:
- migrateAsuraKeys in OpenStore: one-off idempotent migration rewriting hashed asura keys to the stable hashless ID, merging collisions to newest updated_at; losers deleted before winner rewrite (PK-collision safe) — regression tests included
- poller latestChapterFrom: build hash made optional in chapter-scoping regex so latest_chapter survives redeploys
Userscript:
- stripBuildHash(/-[0-9a-f]{8}$/) applied to seriesId in both asura detect branches; URLs keep full slug (stale hashes 302)
- load-time migration rewrites cached + retry-queued asura keys to the stripped form so a queued PUT cannot resurrect an orphaned row
Docs: AGENTS.md + CLAUDE.md URL-shape notes; design spec at docs/superpowers/specs/2026-07-28-asura-stable-series-id-design.md
Verified: go test -count=1 ./... green (new rotation + collision tests), node --check green, regex verified against live asurascans.com slugs.
Deploy order: backend first (migration runs at OpenStore). Orphan rows created by not-yet-updated userscripts self-heal on next backend restart.
Reviewed-on: #6
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #6.
This commit is contained in:
@@ -66,6 +66,18 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
// Asura appends a site-wide build hash to every slug
|
||||
// ("...-059befe1"); it rotates on each redeploy, so it must not be part
|
||||
// of the series identity — old keys can never match pages after a
|
||||
// redeploy. Only the ID strips it; seriesUrl keeps the full slug because
|
||||
// navigation needs the current hash (stale ones 302). Must stay in sync
|
||||
// with asuraBuildHash in backend/store.go.
|
||||
// ponytail: a real title ending in exactly 8 hex chars would lose its
|
||||
// last segment; none known.
|
||||
function stripBuildHash(slug) {
|
||||
return slug.replace(/-[0-9a-f]{8}$/, "");
|
||||
}
|
||||
|
||||
const asura = {
|
||||
site: "asura",
|
||||
// asuracomic.net deep links 301 to the asurascans.com *root*, dropping the
|
||||
@@ -83,7 +95,7 @@
|
||||
return {
|
||||
type: "chapter",
|
||||
site: this.site,
|
||||
seriesId: m[1],
|
||||
seriesId: stripBuildHash(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/comics/" + m[1],
|
||||
@@ -98,7 +110,7 @@
|
||||
return {
|
||||
type: "series",
|
||||
site: this.site,
|
||||
seriesId: m[1],
|
||||
seriesId: stripBuildHash(m[1]),
|
||||
title: cleanTitle(meta("og:title")),
|
||||
cover: meta("og:image") || "",
|
||||
seriesUrl: loc.origin + "/comics/" + m[1],
|
||||
@@ -384,6 +396,38 @@
|
||||
|
||||
const queue = loadQueue();
|
||||
|
||||
// Keys cached (or parked in the retry queue) by versions before the
|
||||
// Asura build-hash strip still carry the hash. Align them with the
|
||||
// server-side migration so a queued PUT cannot resurrect an orphaned
|
||||
// row and the cache matches what the server now returns. Duplicate
|
||||
// cache entries collapsing onto one key are left as-is — the next
|
||||
// refresh() replaces the whole list from the already-merged server.
|
||||
(function migrateLocalKeys() {
|
||||
let dirty = false;
|
||||
for (let i = 0; i < state.list.length; i++) {
|
||||
const b = state.list[i];
|
||||
if (b.site !== "asura" || !b.series_id) continue;
|
||||
const id = stripBuildHash(b.series_id);
|
||||
if (id === b.series_id) continue;
|
||||
state.list[i] = Object.assign({}, b, { series_id: id, key: "asura:" + id });
|
||||
dirty = true;
|
||||
}
|
||||
if (dirty) {
|
||||
reindex();
|
||||
saveCache(state.list);
|
||||
}
|
||||
dirty = false;
|
||||
for (const e of queue) {
|
||||
if (!e.key.startsWith("asura:")) continue;
|
||||
const k = "asura:" + stripBuildHash(e.key.slice("asura:".length));
|
||||
if (k !== e.key) {
|
||||
e.key = k;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if (dirty) saveQueue(queue);
|
||||
})();
|
||||
|
||||
function queueCount() {
|
||||
return queue.length;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user