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 | /**
* lib/log's update chain — ordering, once-per-hash, and the return types.
*
* rk-log.json pins the projection and says nothing about WHEN anything runs,
* and log's six `_` seams were never driven at all: `logScript` destructures
* `priv` and then does `void priv;`. Everything that makes this file correct is
* a property of the chain, so a re-decomposition could keep every existing
* vector green and still change what the app sees.
*
* helpers/log-effects.mjs states the three properties and why each matters.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { installDom } from "./harness/dom.mjs";
import { mods, readVector } from "./helpers/load.mjs";
import { fakeStack } from "./helpers/fakes.mjs";
installDom();
const M = await mods();
const S = await import("./helpers/log-effects.mjs");
const V = readVector("log-effects");
const env = {
RoomLog: M.RoomLog,
RoomCrypto: M.RoomCrypto,
makeLogEncryption: M.makeLogEncryption,
fakeStack,
};
const isEntry = (e) => e[0] === "onEntry";
const isSweep = (e) => e[0] === "db.iterator";
test("onEntry fires exactly once per hash, oldest first, and sweeps only when needed", async () => {
const got = await S.updateOrdering(env);
assert.deepEqual(got.transcript, V.updateOrdering.transcript);
// sweep-unseen collects the oplog iterator's newest→oldest and REVERSES it,
// so the app sees the log in log order
assert.deepEqual(got.emittedHashes, ["e1", "e2", "e3", "e4", "e5", "e6", "e7"]);
// exactly once each: no hash appears twice, and the count equals the set size
assert.equal(got.emittedHashes.length, new Set(got.emittedHashes).size);
assert.equal(got.transcript.filter(isEntry).length, 7);
// exactly ONE sweep in the whole run — the first update, whose `next` named
// an unseen hash. Every later update found its links already seen.
assert.equal(got.transcript.filter(isSweep).length, 1);
assert.equal(got.transcript.findIndex(isSweep), 1, "and it happened before the first emit");
// `_seen` is read at RUN time, not at SCHEDULE time: e6 and e7 were queued in
// the same turn, e7 links to e6, and at queue time e6 was unseen. Hoisting
// that check out of the `.then` — precisely what a `p/let` rewrite does —
// would have swept for e7.
assert.equal(got.sweepsForThePair, 0);
assert.equal(got.onUpdateReturn, "undefined");
});
test("on-update returns undefined; emit-unseen returns the chain mailbox awaits", async () => {
const got = await S.returnTypes(env);
assert.deepEqual(got.transcript, V.returnTypes.transcript);
// lib/mailbox's join-phase does `js-await [_ (.emitUnseen log)]` and only then
// reports how many entries joined. If emit-unseen returned undefined the
// await would resolve immediately, mailbox would report and flush before the
// entries had surfaced, and nothing in either file's tests would notice.
assert.equal(got.emitUnseen.isThenable, true);
assert.equal(got.emitUnseen.isTheChain, true, "the returned value IS _updateChain, not a copy");
assert.equal(got.emitUnseen.emittedBeforeAwait, 0);
assert.equal(got.emitUnseen.emittedAfterAwait, 2, "awaiting it means the sweep has finished");
// and the other half of the pair: on-update is fire-and-forget
assert.equal(got.onUpdateReturn, "undefined");
});
test("the log-effects vectors are not vacuous", () => {
const clone = (x) => JSON.parse(JSON.stringify(x));
const t = V.updateOrdering.transcript;
// emission order reversed — sweep-unseen without its .reverse
const reversed = clone(t);
const idx = reversed.map((e, i) => [e, i]).filter(([e]) => isEntry(e)).map(([, i]) => i);
const rows = idx.map((i) => reversed[i]).reverse();
idx.forEach((i, k) => { reversed[i] = rows[k]; });
assert.notDeepEqual(reversed, t);
// a hash emitted twice — `_seen` not marked, or marked too late
const twice = clone(t);
twice.splice(twice.findIndex(isEntry) + 1, 0, ["onEntry", "e1", 1]);
assert.notDeepEqual(twice, t);
// a second sweep — the schedule-time check
const swept = clone(t);
swept.push(["db.iterator", 500]);
assert.notDeepEqual(swept, t);
// a dropped emit
const dropped = clone(t).filter((e) => !(isEntry(e) && e[1] === "e5"));
assert.notEqual(dropped.length, t.length);
assert.notDeepEqual(dropped, t);
// the return-type record, perturbed both ways
const rt = clone(V.returnTypes);
rt.emitUnseen.isThenable = false;
assert.notDeepEqual(rt.emitUnseen, V.returnTypes.emitUnseen);
const rt2 = clone(V.returnTypes);
rt2.emitUnseen.emittedAfterAwait = 0;
assert.notDeepEqual(rt2.emitUnseen, V.returnTypes.emitUnseen);
// ...and the recorded transcripts are real
assert.equal(t.filter(isEntry).length, 7);
assert.equal(t.filter(isSweep).length, 1);
assert.equal(V.returnTypes.transcript.filter(isEntry).length, 3);
});
|