- TypeScript 65.7%
- JavaScript 33.4%
- Shell 0.9%
| bench | ||
| demo | ||
| src | ||
| tests | ||
| .gitignore | ||
| build.mjs | ||
| package.json | ||
| push.sh | ||
| README.md | ||
| tsconfig.json | ||
discordsim-browser
A simulated Discord that runs entirely in the browser. No server, no login, no developer area. Bake your bot in, drop it on a page, and a visitor can talk to it as an anonymous guest.
The intended use is a live demo of a bot you are developing: a site where someone can try it without joining a server or installing anything.
<script src="discordsim.min.js"></script>
<script>
DiscordSim.createDemo({
mount: '#demo',
guild: { name: 'My Server', channels: ['general'] },
bot: {
name: 'MyBot',
setup: (client) => {
client.on('messageCreate', async (message) => {
if (message.author.bot) return
if (message.content === '!ping') await message.reply('pong')
})
}
}
})
</script>
That is the whole integration. ~12 KB gzipped, no runtime dependencies.
What it is, and what it is not
This is the third implementation in a line:
| V1 | V2 | V3 (this) | |
|---|---|---|---|
| language | TypeScript | Rust | TypeScript |
| runs as | server | server | in the page |
| interface | HTTP + WebSocket | HTTP + WebSocket | direct calls |
| audience | bot development | bot development, stress testing | demoing a bot |
V3 deliberately drops what a browser demo cannot use: no HTTP layer, no WebSocket, no gateway handshake, no tokens, no developer portal, no multi-session, no load generation. A page has no sockets and a visitor has no business minting bot tokens.
What it keeps is the part that took the longest to get right: backend behaviour.
Fidelity
The payload shapes are not re-derived by hand. They are ported from V2 and then compared against responses captured from a running V2 server:
node bench/engine-diff.mjs # V3 payloads vs captured V2 responses
node --test tests/*.test.js # behaviour a shape diff cannot see
# The strongest check: V2's own probe files, unmodified, against this engine.
node bench/http-shim.mjs 8145 & # maps V2's REST routes onto Sim
python3 ../SimulatedDiscord2/bench/pagination-check.py 8145 # exit 0
python3 ../SimulatedDiscord2/bench/semantics-check.py 8145 # exit 0
# Strongest of all: the same bot workflow driven against BOTH, outcomes diffed.
node bench/workflow-diff.mjs <v2-port> <v3-port> # 22 steps, 0 divergences
workflow-diff.mjs answers the question the other checks cannot. engine-diff
compares payload shape, and V2's probes test V2's semantics; neither shows
whether a bot doing real work gets the same answers from both. It drives one
scenario against each and compares only the values a bot would branch on, so
ids and timestamps are excluded by construction rather than by a skip list.
It found three divergences on its first run that everything else had passed over: pinning 404'd because only one of Discord's two pin routes was wired, and thread creation returned 200 where V2 returns 201.
The shim exists for that one purpose. The same behavioural probes that pass against a running Rust V2 pass against the engine here, which is evidence of equivalence rather than an assertion of it.
The differ found 6 missing fields on its first run, which is exactly why it exists rather than being assumed unnecessary.
The differ compares shape, not content. Emptying an array still reports a
match. That limit is inherited knowingly from V2, where it was discovered by
fault injection: the tool built in response to a bug about an empty roles
array could not detect that bug. So the five gap classes found in V2 are
asserted directly in tests/semantics.test.js instead:
- Content, not just shape —
MESSAGE_CREATEcarries the author's real roles, so a bot gating onmember.rolesdoes not fail closed. - Mentions are parsed server-side —
mentions[],mention_rolesandmention_everyoneare derived from the content, not left empty. V1 and V2 both shipped with this missing. allowed_mentionsgates each kind independently — and the test compares against a non-empty baseline, because a blunt suppress-everything implementation passes a suppression-only check.- Paging cursors move the window —
before,afterandaroundwork, and a paging loop terminates having seen each message exactly once. In V2 these were parsed by nobody, so such a loop ran forever. - Input is validated — an empty message, content over 2000 characters, and a send without permission are all rejected rather than silently accepted.
Every one of these was proven to fail before it was fixed. Injecting three faults (roles emptied, cursors ignored, mentions dropped) fails 5 of the 13 tests.
API
createDemo(options) is the one-call path. The pieces are exported too:
Sim— the simulator. State plus operations, no transport.SimClient— a discord.js-shaped client:on(Events.MessageCreate, ...),message.reply(),message.member.roles.has(),channel.send().mountUI(sim, element)— the chat interface on its own.
The client covers the surface a message-handling bot actually touches. It is not discord.js and does not try to be: discord.js opens a real WebSocket and needs a REST base URL, neither of which exists in a page with no server. Your bot's logic is unchanged; its import line is not.
Supported in the UI
Messages with author grouping and timestamps, embeds, buttons and slash
commands raising real interactions (type: 3 components, type: 2
APPLICATION_COMMAND via /name args), reactions (clickable, toggleable),
typing indicators that expire, multiple channels, pinning, mention
highlighting, and the subset of Discord markdown bots actually use:
**bold**, *italic*, __underline__, ~~strike~~, `code`,
code blocks and ||spoilers||.
Engine-side, ported from V2: message history with working paging cursors,
bulk delete dispatching one MESSAGE_DELETE_BULK, reaction add/remove/clear,
pin/unpin with CHANNEL_PINS_UPDATE, threads (THREAD_CREATE, including
message-anchored ones), invites, role grants, permission enforcement, and
input validation matching Discord.
Development
npm run typecheck # strict TypeScript, zero errors
npm run build # dist/discordsim.min.js + .esm.js
npm test # 13 behaviour tests
node bench/engine-diff.mjs # payload parity against V2
python3 -m http.server 8099 # then open demo/index.html
Verified in a real browser, not only in node: the demo page was loaded, messages sent, buttons clicked and interactions handled. A node test cannot prove the DOM works.
Publishing
The remote is git@git.unityailab.com:Sponge/SimulatedDiscord3.git. Forgejo
disables push-to-create, so the repository has to be created once by hand at
https://git.unityailab.com/repo/create (owner Sponge, name
SimulatedDiscord3), empty — no README or licence, since an initialised
repo gets its own root commit and pushing this history would then be rejected
as unrelated.
After that, ./push.sh pushes develop and main and verifies the result.
It exits with instructions if the remote is still missing.