Skip to content

Refactor server api#1405

Open
sidorares wants to merge 4 commits intomasterfrom
server-stateful-commands
Open

Refactor server api#1405
sidorares wants to merge 4 commits intomasterfrom
server-stateful-commands

Conversation

@sidorares
Copy link
Copy Markdown
Owner

@sidorares sidorares commented Oct 10, 2021

Refactor: Command-Based Server API with High-Level Handlers

Motivation

The current server API requires manual protocol handling: call serverHandshake(), listen for 'query' events, and manually serialize responses with writeColumns() / writeTextRow() / writeEof(). This is low-level, verbose, and error-prone.

This PR introduces a layered server API where the simplest use case is also the most concise:

const server = mysql.createServer({
  async query(sql) {
    return db.query(sql);  // return data, library handles serialization
  },
});
server.listen(3307);

API Design

Three API styles from simplest to most powerful:

1. Static handlers — pure sql → data functions

createServer({
  query(sql) { return [{ id: 1, name: 'hello' }]; },
  auth({ user }) { if (user !== 'admin') throw new Error('denied'); },
  // ping, quit, init_db have sensible defaults
});

Handlers have zero protocol surface. Return values are automatically serialized:

  • [{ id: 1 }] → ResultSetHeader + ColumnDefinition + TextRow packets (columns inferred)
  • { rows, columns } → ResultSetHeader + explicit ColumnDefinition + TextRow packets
  • { affectedRows: 3, insertId: 10 } → OK packet
  • void → OK packet
  • Thrown error → Error packet

2. Factory function — per-connection state via closure

createServer((connection) => {
  const upstream = mysql.createConnection({ host: 'real-mysql' });
  return {
    async query(sql) { return (await upstream.promise().query(sql))[0]; },
    quit() { upstream.end(); },
  };
});

Connection is available via closure — handler signatures stay clean.

3. Low-level handleCommand — full protocol control

createServer({
  onConnection(conn) { conn.serverHandshake({ ... }); },
  handleCommand(commandCode) { return new MyCustomCommand(); },
});

Each command is a Command subclass (packet-driven state machine) — compose, test in isolation, handle multi-step protocols.

The legacy createServer(handler) + conn.serverHandshake() + conn.on('query') pattern is fully preserved.

Architecture

User API                              Internal
─────────                             ────────
createServer({ query, ping })    →    buildHandleCommand(handlers)
createServer((conn) => ({...}))  →    buildHandleCommand(factory(conn))
createServer({ handleCommand })  →    direct low-level control
createServer((conn) => {...})    →    legacy (imperative)
                                           ↓
                                  handleCommand(commandCode)
                                           ↓
                                  ServerQuery / ServerPing / ServerQuit / ...
                                           ↓
                                  parse packet → call handler → serialize result

What Changed

New: lib/commands/server/ — Server-side command classes

  • ServerQuery, ServerPing, ServerQuit, ServerInitDb — bridge user handlers to protocol
  • sendResult — interprets handler return values, infers column definitions, calls writeXxx
  • buildHandleCommand — maps a handlers object to the handleCommand(code) => Command hook

Changed: index.jscreateServer() detects four API shapes (legacy function, static handlers, factory, low-level options) and wires up handshake + handleCommand automatically

Changed: lib/commands/server_handshake.jsreadClientReply yields to handleCommand after handshake when configured (returns null instead of dispatchCommands)

Changed: lib/base/connection.jshandlePacket() dispatches to handleCommand when server has no active command; _serverEncoding getter for server write helpers

Changed: lib/commands/command.js — Server-side sequenceId bump only when responding to client packet

Changed: lib/packets/Handshake supports configurable getSalt and authPluginName; HandshakeResponse accepts pre-calculated authToken; Query.fromPacket() for server-side deserialization

New: TypeScript typesServerHandlers, ServerFactory, ServerResult, AuthParams; createServer() overloads

New: test/integration/test-server-api.test.mts — 8 integration tests covering static handlers, factory function, auth accept/reject, error handling, backward compatibility

Rewritten: Documentationwebsite/docs/documentation/mysql-server.mdx and website/docs/examples/tests/server.mdx with comprehensive coverage of all API styles

Remaining Work

  • Server-side COM_STMT_PREPARE / COM_STMT_EXECUTE support
  • Binary protocol result writing
  • Streaming large result sets from async iterators
  • Connection attributes parsing on server side

- Add handleCommand hook for server-side command dispatch in handlePacket
- Thread serverOptions (handleCommand, encoding) through createServer → Server → ConnectionConfig → Connection
- Add Query.fromPacket() for server-side COM_QUERY deserialization
- Make Handshake packet support configurable getSalt and authPluginName
- Simplify HandshakeResponse to accept pre-calculated authToken
- Use _serverEncoding getter for server write helpers (writeColumns, writeOk, etc.)
- Bump sequenceId by 1 on server-side command start
- Emit auth errors to connection instead of throwing
@sidorares sidorares force-pushed the server-stateful-commands branch from 9173fea to fb4d774 Compare April 12, 2026 02:15
…ump, update tests

- Restore this.database = connection.config.database in sendCredentials (dropped during rebase)
- Only bump server sequenceId when responding to a client packet (not for initial handshake)
- Default HandshakeResponse.authToken to empty buffer when not provided
- Update HandshakeResponse tests to match simplified constructor API
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 12, 2026

Codecov Report

❌ Patch coverage is 75.75758% with 112 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.55%. Comparing base (cb5adcc) to head (ad08c77).

Files with missing lines Patch % Lines
lib/commands/server/init_db.js 28.00% 36 Missing ⚠️
lib/commands/server/ping.js 56.52% 20 Missing ⚠️
lib/commands/server/query.js 64.81% 19 Missing ⚠️
lib/commands/server/index.js 73.21% 15 Missing ⚠️
lib/base/connection.js 75.00% 7 Missing ⚠️
lib/commands/server/send_result.js 92.95% 5 Missing ⚠️
lib/commands/auth_switch.js 0.00% 4 Missing ⚠️
lib/packets/handshake.js 85.00% 3 Missing ⚠️
lib/packets/query.js 77.77% 2 Missing ⚠️
index.js 98.70% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1405      +/-   ##
==========================================
- Coverage   90.86%   90.55%   -0.32%     
==========================================
  Files          89       95       +6     
  Lines       14490    14848     +358     
  Branches     1864     1941      +77     
==========================================
+ Hits        13167    13445     +278     
- Misses       1323     1403      +80     
Flag Coverage Δ
compression-0 89.83% <75.75%> (-0.31%) ⬇️
compression-1 90.53% <75.75%> (-0.32%) ⬇️
static-parser-0 88.32% <75.75%> (-0.27%) ⬇️
static-parser-1 89.05% <75.75%> (-0.29%) ⬇️
tls-0 90.00% <75.75%> (-0.31%) ⬇️
tls-1 90.33% <75.75%> (-0.32%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- Add ServerOptions interface with onConnection, handleCommand, encoding
- Update createServer() type signature to accept ServerOptions | handler function
- Add serverOptions to ConnectionOptions type
- Export ServerOptions from package typings
- Improve error message when server receives packets without handleCommand configured
Two new API forms for creating MySQL servers:

Static handlers (pure data-in/data-out, no protocol concerns):

  createServer({
    async query(sql) { return db.query(sql); },
    auth({ user }) { if (user !== 'admin') throw new Error('denied'); },
  });

Factory function (per-connection state via closure):

  createServer((connection) => ({
    async query(sql) { return upstream.query(sql); },
  }));

Handler return values are automatically serialized:
- Array of objects -> writeTextResult with inferred columns
- { rows, columns } -> writeTextResult with explicit columns
- { affectedRows, insertId } -> writeOk
- void -> writeOk
- Thrown error -> writeError

Implementation:
- lib/commands/server/ - ServerQuery, ServerPing, ServerQuit, ServerInitDb command classes
- buildHandleCommand() bridges high-level handlers to low-level handleCommand hook
- ServerHandshake yields to handleCommand after handshake when configured
- Comprehensive TypeScript types (ServerHandlers, ServerFactory, ServerResult)
- Integration tests for both API forms, auth, and backward compatibility
- Rewritten documentation with examples for all API styles
@wellwelwel wellwelwel removed the needs rebase For internal organization purpose label Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants