wai-rs gains the feature floor (the mandatory floor of SPEC 5: PNG, FLAC and zstd, decoded with no system codec library) and quickstart (the floor, and the receipt checks hello and verify run). The wai command line gains four verbs, one module each, named in one table (dx::VERBS): - hello: name, dispatch, rebuild and receipt on a picture made here, the receipt laid out as http-delivery 8 lays receipts beside an object; - create: a new project from one of the examples, whose files the binary holds; tests/cli_dx.rs holds them to wai/examples/ byte for byte; - dev: the relay, the room service and the room page on one loopback origin, as a production front serves them; --check checks each piece through it; - verify: the receipt indexes under a directory, checked offline. The project file a template writes, wai.json, is read in one place (dx/project.rs). wai/examples/ holds video-room, watch-together, messaging, encoder-ingest, package-and-deliver, verify-receipts, apple-app, android-app and remote-desktop, each with a README and a test that runs it through wai create and wai dev. wai/quickstart/ builds the room client and the archive that holds wai, the relay, the room service, the remote host and the packager. Co-Authored-By: Claude Opus 5.5 <[email protected]>
62 lines
4.1 KiB
JavaScript
62 lines
4.1 KiB
JavaScript
// The messaging example, as a person runs it: `wai create` makes the project, `wai dev` runs a
|
|
// conversation beside a call room, and two browsers open its page. Both join the conversation
|
|
// and each opens what the other sealed; then one calls the other, the callee's inbox rings,
|
|
// it answers, and each receives and decodes the other's camera and microphone.
|
|
//
|
|
// node wai/examples/messaging/test/messaging.test.mjs
|
|
import { check, create, dev, dump, finish, frame, launch, open, STEP, tmpdir, until } from "../../../quickstart/test/lib.mjs";
|
|
|
|
const out = tmpdir("messaging");
|
|
const dir = await create("messaging", out);
|
|
const d = await dev(dir);
|
|
const browser = await launch();
|
|
const pages = [];
|
|
try {
|
|
const people = [];
|
|
for (const name of ["alice", "bob"]) {
|
|
const p = await open(browser, `${d.url}/`, name);
|
|
pages.push(p);
|
|
const m = await frame(p, "/room/messages.html");
|
|
await until(`${name}: the messages page ready`, STEP, () => m.evaluate(() => !!(window.msgTest && window.msgTest.join)), 200);
|
|
const phase = await m.evaluate(() => window.msgTest.join());
|
|
if (phase !== "member") throw new Error(`${name} did not join: ${phase}`);
|
|
const c = await frame(p, "/room/call.html");
|
|
await until(`${name}: the call page ready`, STEP, () => c.evaluate(() => window.callTest && window.callTest.state.phase === "ready"), 200);
|
|
people.push({ p, m, c, key: await c.evaluate(() => window.callTest.state.me) });
|
|
}
|
|
const [A, B] = people;
|
|
await until("both keyed into one group of two", STEP, async () => {
|
|
const s = await Promise.all([A, B].map((x) => x.m.evaluate(() => window.msgTest.state)));
|
|
return s.every((x) => x.members.length === 2 && x.epoch && x.epoch.epoch === s[0].epoch.epoch);
|
|
}, 300);
|
|
await A.m.evaluate(() => window.msgTest.send("hello from alice"));
|
|
await B.m.evaluate(() => window.msgTest.send("hello from bob"));
|
|
const texts = (v) => v.messages.filter((m) => m.outcome === "accepted").map((m) => m.text).sort().join("|");
|
|
for (const x of [A, B]) {
|
|
const got = await until(`${x.p.name} opens both messages`, STEP, async () => { const t = texts(await x.m.evaluate(() => window.msgTest.view())); return t === "hello from alice|hello from bob" && t; }, 300);
|
|
check(`${x.p.name}'s page opens and verifies what each member sealed`, true, got);
|
|
}
|
|
// Alice calls Bob; his inbox rings; he answers.
|
|
await A.c.evaluate((k) => { window.callTest.place([k]).catch((e) => { window.callTest.state.error = String(e); }); }, B.key);
|
|
const ringing = await until("bob's inbox rings", STEP, async () => { const s = await B.c.evaluate(() => window.callTest.state); return s.inbox.find((x) => x.state === "ringing") && s; }, 300);
|
|
check("the callee's inbox holds the invitation, checked with its call before it rings", ringing.inbox[0].check.ok && ringing.inbox[0].check.caller === A.key);
|
|
await B.c.evaluate(() => { window.callTest.accept().catch((e) => { window.callTest.state.error = String(e); }); });
|
|
const room = (x) => frame(x.p, "/room/index.html");
|
|
const principal = async (x) => (await room(x)).evaluate(() => window.roomTest.snapshot().me.principal);
|
|
for (const x of [A, B]) {
|
|
await until(`${x.p.name}'s call publishes`, STEP * 2, async () => (await (await room(x)).evaluate(() => window.roomTest && window.roomTest.state.phase)) === "publishing", 500);
|
|
}
|
|
const [pa, pb] = [await principal(A), await principal(B)];
|
|
for (const [x, other] of [[A, pb], [B, pa]]) {
|
|
const t = await until(`${x.p.name} decodes the other's call media`, STEP * 2, async () => { const s = await (await room(x)).evaluate(() => window.roomTest.snapshot()); const t = s.subs[other]; return t && t.video.decoded >= 20 && t.audio.decoded >= 30 && t; }, 500);
|
|
check(`${x.p.name}'s call carries the other's camera and microphone, decoded`, true, `${t.video.decoded} video frames, ${t.audio.decoded} audio frames`);
|
|
}
|
|
check("caller and callee publish under the grants their call came with", pa === A.key && pb === B.key);
|
|
} catch (e) {
|
|
check("the example ran to its end", false, (e && e.stack || String(e)).split("\n").slice(0, 3).join(" | "));
|
|
} finally {
|
|
dump(pages, out);
|
|
await browser.close().catch(() => {});
|
|
await d.stop();
|
|
}
|
|
finish("messaging");
|