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
138
139
140
141
142
143
144
145
146 | /**
* lib/log's update chain: ordering, once-per-hash, and the two return types.
*
* `_onUpdate`, `_sweepUnseen` and `emitUnseen` all write to the same
* `_updateChain`, and everything about them that matters is a property of WHEN
* things run, not of what they compute. rk-log.json pins the projection —
* author binding, ingest result table, entryMeta — and says nothing about the
* chain. The plan's own ground truth calls log's six `_` seams "never
* exercised": `logScript` destructures `priv` and then literally does
* `void priv;`.
*
* Three properties, and one of them is invisible from inside this file:
*
* 1. `onEntry` fires EXACTLY ONCE per hash, in sweep-unseen's reverse-iterator
* order (the oplog iterator yields newest→oldest; sweep reverses it, so the
* app sees oldest→newest).
* 2. `_seen` is consulted at RUN time, not at SCHEDULE time. Two updates queued
* back to back, the second linking to the first, must produce ONE sweep: by
* the time the second callback runs, the first has already marked its hash
* seen. Hoisting that check out of the `.then` — which is exactly what a
* `p/let` rewrite does — turns one sweep into two.
* 3. The RETURN TYPES. `on-update` returns `js/undefined`; `emit-unseen`
* returns the chain, and it is the very object `_updateChain` now holds.
* lib/mailbox's join-phase does `js-await [_ (.emitUnseen log)]` before it
* reports how many entries joined — if a rewrite swapped those two returns,
* the await would resolve immediately, mailbox's replay would report and
* flush before the entries had surfaced, and NOTHING in either file's tests
* would notice.
*/
import { makeSink } from "./det-clock.mjs";
export const mkEntry = (o) => ({ next: [], refs: [], clock: { id: "c", time: 0 }, v: 2, ...o });
const IDENTITIES = { idOK: { id: "b64urlPub", publicKey: "deviceKeyA", type: "sueta" } };
const chat = (hash, time, text, next = []) =>
mkEntry({
hash, next, identity: "idOK", key: "deviceKeyA", clock: { id: "c", time },
payload: { op: "ADD", key: null, value: { t: "chat", ts: time * 10, name: "a", text } },
});
/** fakeStack, with the oplog iterator recorded — "did a sweep happen?" is the
* question every assertion here turns on. */
function recordingStack(fakeStack, entries, sink) {
const stack = fakeStack({ entries, identities: IDENTITIES });
const real = stack.db.log.iterator;
stack.db.log.iterator = (opts) => {
sink.push(["db.iterator", opts?.amount ?? null]);
return real(opts);
};
return stack;
}
async function build({ RoomLog, RoomCrypto, makeLogEncryption, fakeStack }, entries, sink) {
const rc = await RoomCrypto.create("l".repeat(43), "chat.example/v2", "12D3KooME");
const encryption = await makeLogEncryption(rc);
const imgCipher = await rc.imgCipher();
const stack = recordingStack(fakeStack, entries, sink);
const log = new RoomLog(stack.db, stack.orbitdb, stack.helia, imgCipher, encryption);
log.onEntry = (e) => sink.push(["onEntry", e.hash, e.clock]);
log.onError = (err) => sink.push(["onError", String(err?.message ?? err)]);
return { log, stack };
}
/** Real microtask turns only — nothing in this file schedules a macrotask. */
const settle = async (n = 40) => {
for (let i = 0; i < n; i++) await Promise.resolve();
};
/**
* The chain, end to end.
*/
export async function updateOrdering(M) {
const sink = makeSink();
const say = (...w) => sink.push(["script", ...w]);
// e1..e3 are already in the log; e4 is the new head and links to e3
const entries = [chat("e1", 1, "one"), chat("e2", 2, "two"), chat("e3", 3, "three"), chat("e4", 4, "four", ["e3"])];
const { log } = await build(M, entries.slice(), sink);
say("_onUpdate(e4) — its `next` names e3, which is unseen, so sweep first");
const ret = log._onUpdate(entries[3]);
await settle();
const onUpdateReturn = ret === undefined ? "undefined" : typeof ret;
say("_onUpdate(e4) again — every hash is seen now");
log._onUpdate(entries[3]);
await settle();
// e5 is a fresh head linking to e4, which IS seen: no sweep, a direct emit.
const e5 = chat("e5", 5, "five", ["e4"]);
say("_onUpdate(e5) — `next` names e4, already seen, so no sweep");
log._onUpdate(e5);
await settle();
// THE run-time-vs-schedule-time case. e6 is unknown to the log; e7 links to
// e6. Queued back to back: at SCHEDULE time e6 is unseen (a naive rewrite
// would sweep for e7); at RUN time e6's own update has already marked it.
const e6 = chat("e6", 6, "six");
const e7 = chat("e7", 7, "seven", ["e6"]);
say("_onUpdate(e6) and _onUpdate(e7) queued back to back, neither awaited");
const mark = sink.length;
log._onUpdate(e6);
log._onUpdate(e7);
await settle();
const sweepsForThePair = sink.slice(mark).filter((e) => e[0] === "db.iterator").length;
return {
transcript: sink,
onUpdateReturn,
sweepsForThePair,
emittedHashes: sink.filter((e) => e[0] === "onEntry").map((e) => e[1]),
};
}
/**
* The two return types, and the consequence of each.
*/
export async function returnTypes(M) {
const sink = makeSink();
const say = (...w) => sink.push(["script", ...w]);
const entries = [chat("a1", 1, "one"), chat("a2", 2, "two")];
const { log } = await build(M, entries.slice(), sink);
say("emitUnseen()");
const p = log.emitUnseen();
const emittedBeforeAwait = sink.filter((e) => e[0] === "onEntry").length;
const isThenable = typeof p?.then === "function";
// the returned value IS the chain the log now holds — that identity is what
// makes a caller's await serialize against every later update
const isTheChain = p === log._updateChain;
await p;
const emittedAfterAwait = sink.filter((e) => e[0] === "onEntry").length;
say("_onUpdate(a3)");
const a3 = chat("a3", 3, "three");
const r = log._onUpdate(a3);
const onUpdateReturn = r === undefined ? "undefined" : typeof r;
await settle();
return {
transcript: sink,
emitUnseen: { isThenable, isTheChain, emittedBeforeAwait, emittedAfterAwait },
onUpdateReturn,
};
}
|