1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137 | /**
* #/a/<shard>/<org>/<aid> โ public activity permalink (P1). Resolves the
* activity (LWW winner per organizer+aid) and its joins from the shard's feed
* rooms by plain mailbox replay โ organizer offline is fine, like every
* public view. Honest terminal states: invalid link, mailbox unreachable, or
* not found within the TTL window (the link may simply have expired, ยง7).
*/
import { RuntimeLimits } from "../../config";
import { openFeed, type FeedHandle } from "../../public/feed-service";
import { isAid, isShard } from "../../public/public-doc";
import { t } from "../i18n";
import { h } from "../ui-kit";
import { activityCard } from "./activities";
const PUB_RE = /^[A-Za-z0-9_-]{43}$/;
/**
* Stale-doc replay resilience bound: after the first (org, aid) match, at most
* this many EXTRA loadOlder passes hunt for a higher-seq revision hidden below
* a re-deposited stale head (each pass that finds one keeps the hunt alive).
*/
const STALE_EXTRA_PASSES_MAX = 3;
function frame(...children: (HTMLElement | null)[]): HTMLElement {
return h(
"div",
{ class: "profile-view", "data-testid": "av-view" },
h(
"div",
{ class: "profile-view-inner" },
h("a", { class: "btn ghost small", href: "#/" }, t("backHome")),
...children.filter((c): c is HTMLElement => c !== null),
),
);
}
function terminal(root: HTMLElement, glyph: string, msg: string, testid: string): void {
root.replaceChildren(
frame(
h(
"section",
{ class: "card placeholder" },
h("div", { class: "glyph" }, glyph),
h("p", { class: "add-msg", "data-testid": testid }, msg),
),
),
);
}
let openHandle: FeedHandle | null = null;
/** Run `cleanup` the moment `el` leaves the document (route change). */
function onDisconnect(el: HTMLElement, cleanup: () => void): void {
const obs = new MutationObserver(() => {
if (!el.isConnected) {
obs.disconnect();
cleanup();
}
});
obs.observe(document.body, { childList: true, subtree: true });
}
export function renderActivityView(root: HTMLElement, shard: string, org: string, aid: string): void {
openHandle?.close();
openHandle = null;
const host = h("div", { class: "pv-host" });
root.replaceChildren(host);
if (!isShard(shard) || !PUB_RE.test(org) || !isAid(aid)) {
terminal(host, "๐ซ", t("upInvalid"), "av-invalid");
return;
}
host.replaceChildren(
frame(
h(
"section",
{ class: "card placeholder" },
h("div", { class: "glyph" }, "๐"),
h("p", { class: "muted", "data-testid": "av-loading" }, t("avLoading")),
),
),
);
void (async () => {
const handle = await openFeed(shard);
if (!host.isConnected) {
handle.close();
return;
}
openHandle = handle;
onDisconnect(host, () => {
handle.close();
if (openHandle === handle) openHandle = null;
});
const draw = (): void => {
const act = handle.activity(org, aid);
if (!act) {
terminal(host, "โณ", t("avNotFound", { hours: RuntimeLimits.ttlHours }), "av-notfound");
return;
}
host.replaceChildren(
frame(
h(
"div",
{ class: "feed-list pv-single" },
activityCard(handle, act, {
refresh: draw,
remoteRefresh: () => void handle.refresh().then(draw).catch(() => {}),
}),
),
),
);
};
if (!handle.activity(org, aid)) {
// the initial page budget may not have reached it โ page further back
let more = true;
while (more && !handle.activity(org, aid)) more = await handle.loadOlder();
}
// Stale-doc replay resilience: an attacker can re-deposit an OLD signed
// revision of this activity at the room head (and pad the window with
// filler) so a first-match resolver would show a superseded state (e.g.
// hide a cancellation). After the first match, keep paging while each
// further pass still surfaces a HIGHER-seq revision for this aid; stop at
// the first pass that adds none (bounded extra passes).
for (let extra = 0; extra < STALE_EXTRA_PASSES_MAX; extra++) {
const cur = handle.activity(org, aid);
if (!cur) break;
const more = await handle.loadOlder();
const next = handle.activity(org, aid);
const improved = next !== null && next.seq > cur.seq;
if (!improved || !more) break;
}
if (!host.isConnected) return;
draw();
})().catch(() => {
if (host.isConnected) terminal(host, "๐ก", t("upNet"), "av-net");
});
}
|