rooms-kit / test / types / consumer.ts
  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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/**
 * TypeScript smoke-compile of the exported API — the tripwire for `.d.ts` drift.
 *
 * It is never RUN; `npm run check` type-checks it with `tsc --noEmit`. The
 * constructs here are exactly the ones real consumers use, so a missing static
 * factory, a getter that became a data property, or a renamed subpath fails the
 * gate rather than a deployed process:
 *
 *  - the bot room worker's literal import list (`./chat`, `./board`, root
 *    `Identity`) and the exact calls it makes on the objects it gets back;
 *  - `import type`, inline `import("pkg").Type`, `new`, static async factories,
 *    getters, and the EventEmitter-typed `on(...)` overloads;
 *  - the vendor-retirement invariant: an `Identity` obtained from
 *    ardegazu-id-kit must be assignable to `ChatOptions.identity`, i.e. the two
 *    packages must agree on ONE Identity type (v1.0.0's vendored copy did not).
 */
import { ChatClient, CHAT_SALT, newRoomSecret } from "ardegazu-rooms-kit/chat";
import type { ChatOptions, ChatMessage, ChatPeer, ChatEvents } from "ardegazu-rooms-kit/chat";
import { BoardClient, BOARD_SALT } from "ardegazu-rooms-kit/board";
import type { BoardElement, BoardOptions, BoardEvents, StrokeSpec } from "ardegazu-rooms-kit/board";
import { Identity, installOrigin, installBrowserGlobals } from "ardegazu-rooms-kit";
import type { LogOp, LogEntryMeta, IngestResult, PeerConnState, RelayConfig, Net, RoomLog } from "ardegazu-rooms-kit";
import { Identity as IdKitIdentity } from "ardegazu-id-kit";

// ---- the bot room worker's flow, verbatim ----------------------------------

async function botWorker(job: { secret: string; nick: string; dataDir: string; origin: string; seed: string }) {
  const identity = await Identity.fromSeed(job.seed);
  const pub: string = identity.publicKeyB64;
  const fpEmoji: string = identity.fingerprint.emoji;

  const chat = await ChatClient.join({
    roomSecret: job.secret,
    name: job.nick,
    dataDir: job.dataDir,
    identity,
    origin: job.origin,
  });
  chat.on("message", (m) => {
    const text: string | undefined = m.text;
    const thread: string | undefined = m.thread;
    void chat.send(`echo: ${text ?? ""}`, thread);
  });
  chat.on("ready", () => void 0);
  chat.on("peer", (id, p) => {
    const st: PeerConnState = p.state;
    void id;
    void st;
    void p.ident?.fp.hex;
  });
  chat.on("reaction", (target, emoji, by, op) => void `${target}${emoji}${by}${op}`);
  chat.on("peerGone", (id) => void id);
  chat.on("status", (up: boolean) => void up);

  const board = await BoardClient.join({
    roomSecret: job.secret,
    name: job.nick,
    dataDir: job.dataDir,
    identity,
    origin: job.origin,
  });
  board.on("ready", () => {
    const signed = board.elements().some((el: BoardElement) => el.k === "text" && el.text === `${job.nick} was here`);
    if (!signed) void board.addText(40, 40, `${job.nick} was here`, { size: 14, color: 5 });
  });
  board.on("element", (el, from) => void `${el.k}${from}`);
  board.on("edited", (id, patch) => void `${id}${Object.keys(patch).length}`);
  board.on("deleted", (ids: string[]) => void ids.length);

  return { pub, fpEmoji, chat, board };
}

// ---- statics, getters, and the rest of the surface -------------------------

async function surface() {
  const seed: string = Identity.newSeed();
  const identity = await Identity.fromSeed(seed);
  const sig: string = await identity.assert(CHAT_SALT, "roomid", "12D3KooWPeer");
  const raw: Uint8Array<ArrayBuffer> = await identity.signRaw(new Uint8Array([1]) as Uint8Array<ArrayBuffer>);

  installOrigin("https://chat.ardegazu.ro");
  installOrigin("https://chat.ardegazu.ro", [/\.ardegazu\.ro$/]);
  installBrowserGlobals();
  installBrowserGlobals("./state/kv.json");

  const secret: string = newRoomSecret();
  const relay: Partial<RelayConfig> = { relayMultiaddr: "/dns4/h/tcp/443/tls/ws/p2p/12D3KooRELAY" };

  const opts: ChatOptions = { roomSecret: secret, name: "bot", dataDir: "./s", identity, relay, appSalt: CHAT_SALT };
  const chat = await ChatClient.join(opts);

  // getters
  const author: string = chat.myAuthorId;
  const room: string = chat.roomId;
  const net: Net = chat.net;
  const log: RoomLog = chat.log;
  const up: boolean = net.relayUp;
  const myId: string = net.myId;
  const addr: string = log.address;

  // methods
  const hash: string = await chat.send("hi");
  const rHash: string = await chat.react(hash, "🔥", "remove");
  await chat.setName("renamed");
  const hist: ChatMessage[] = chat.history();
  const emojis: string[] = [...(hist[0]?.reactions.keys() ?? [])];
  const peers: ReadonlyMap<string, ChatPeer> = chat.peers();

  // the log surface
  const op: LogOp = { t: "chat", ts: Date.now(), name: "bot", text: "hi" };
  await log.append(op);
  const replayed: number = await log.loadTail(500);
  log.onEntry = (e: LogEntryMeta) => void (e.op.t === "chat" ? e.op.text : e.hash);
  log.onError = (err: Error) => void err.message;
  log.imageChunkBytes = 60_000;
  const img = await log.putImage(new Uint8Array([1, 2]) as Uint8Array<ArrayBuffer>);
  const back = await log.getImage(img.cid, AbortSignal.timeout(1000));
  const sealed: Uint8Array | null = await log.sealedEntryBytes(hash);
  const decoded = sealed ? await log.decodeSealedEntry(sealed) : null;
  const ingest: IngestResult | null = decoded ? await log.ingestEntry(decoded) : null;
  await log.pruneImages(new Set([img.cid]), []);

  // net surface
  await net.broadcast({ kind: "presence", op: "beacon", name: "bot", ts: Date.now() });
  const ok: boolean = await net.sendTo("12D3KooWPeer", { kind: "hello", name: "bot" });
  await net.sendBinaryTo("12D3KooWPeer", new Uint8Array([1]) as Uint8Array<ArrayBuffer>);
  await net.handleFrames("/sueta/2/msg/1.0", (from, frame) => void `${from}${frame.byteLength}`);
  const open: string[] = net.openPeers();

  await chat.close();

  const bOpts: BoardOptions = { roomSecret: secret, name: "bot", dataDir: "./s" };
  const board = await BoardClient.join(bOpts);
  const strokeId: string = await board.drawStroke([{ x: 0, y: 0 }, { x: 1, y: 1 }], { width: 3, color: 2 });
  await board.addText(0, 0, "hello");
  await board.erase([strokeId]);
  await board.close();

  return { sig, raw, author, room, up, myId, addr, rHash, emojis, peers, replayed, back, ingest, ok, open };
}

// ---- inline import() types and the ONE-Identity invariant ------------------

type InlineChat = import("ardegazu-rooms-kit/chat").ChatClient;
type InlineStroke = import("ardegazu-rooms-kit/board").StrokeSpec;
type InlineEvents = import("ardegazu-rooms-kit/chat").ChatEvents;
type InlineBoardEvents = import("ardegazu-rooms-kit/board").BoardEvents;

declare const someStroke: StrokeSpec;
declare const inlineStroke: InlineStroke;
const strokesAgree: StrokeSpec = inlineStroke;
const strokesAgreeBack: InlineStroke = someStroke;

declare const chatEvents: ChatEvents;
const eventsAgree: InlineEvents = chatEvents;
declare const boardEvents: BoardEvents;
const boardEventsAgree: InlineBoardEvents = boardEvents;

/**
 * The vendor-retirement invariant, at the type level: an Identity built by
 * ardegazu-id-kit must be accepted where rooms-kit asks for one. With the old
 * src/vendor/id copy these were two structurally-similar but distinct classes.
 * One DECLARATION is still the invariant after id-kit moved onto this build's
 * classpath — the runtime class object is now this build's own compile of
 * id-kit's sources, so what has to hold is that the declared types are the same
 * declaration and the two compiles interoperate on the wire (asserted in
 * test/api.test.mjs, which signs with ours and verifies with id-kit's dist).
 */
async function oneIdentityClass() {
  const fromIdKit = await IdKitIdentity.fromSeed(IdKitIdentity.newSeed());
  const accepted: ChatOptions = { roomSecret: "x", name: "n", dataDir: "./s", identity: fromIdKit };
  const alsoAccepted: BoardOptions = { roomSecret: "x", name: "n", dataDir: "./s", identity: fromIdKit };
  const sameClass: typeof Identity = IdKitIdentity;
  return { accepted, alsoAccepted, sameClass };
}

// keep every binding used so noUnusedLocals stays available if it is turned on
export { botWorker, surface, oneIdentityClass, strokesAgree, strokesAgreeBack, eventsAgree, boardEventsAgree };
export type { InlineChat, InlineEvents, BOARD_SALT };

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/rooms-kit.git