diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f587070..0000000 --- a/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Files and directories created by pub -.dart_tool -.packages -.pub/ -build/ -packages -# Remove the following pattern if you wish to check in your lock file -pubspec.lock - -# Files created by dart2js -*.dart.js -*.part.js -*.js.deps -*.js.map -*.info.json - -# Directory created by dartdoc -doc/api/ - -# JetBrains IDEs -.idea/ -*.iml -*.ipr -*.iws diff --git a/CHANGELOG.md b/CHANGELOG.md index ef03978..cac80dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,68 @@ -## 1.0.1 - -**Bug Fix:** - -* [#45](https://github.com/rikulo/socket.io-dart/issues/45) [BUG] Calling close on Server raises ConcurrentModificationError - -**New Feature:** - -* [#47](https://github.com/rikulo/socket.io-dart/pull/47) Added Future to start and stop server - -## 1.0.0 - -**New Feature:** - -* [#33](https://github.com/rikulo/socket.io-dart/issues/33) Null safety - - -## 0.9.4 - -**Bug Fix:** - -* [#23](https://github.com/rikulo/socket.io-dart/issues/23) [BUG] Calling close on Server raises ConcurrentModificationError - -## 0.9.3 - -**Bug Fix:** - -* [#18](https://github.com/rikulo/socket.io-dart/pull/18) make sure it is accessing the rooms map - - -## 0.9.2 - -**Bug Fix:** - -* [#17](https://github.com/rikulo/socket.io-dart/pull/17) inference error - - -## 0.9.1+1 - -**New Feature:** - -* [#16](https://github.com/rikulo/socket.io-dart/pull/16) Apply Pedantic recommendations +# Changelog + +All notable changes to this fork are documented here. +This fork tracks upstream [`rikulo/socket.io-dart`](https://github.com/rikulo/socket.io-dart) +and adds the fixes listed below. Once these are accepted upstream the +`dependency_override` in consuming projects can be removed. + +--- + +## 2.0.1 – 2026-05-03 + +### Fixed + +- **Polling transport – packet splitting** (`lib/src/engine/transport/polling_transport.dart`) + `onData()` previously called `PacketParser.decodePayload()` which cannot + parse Engine.IO v4 / Socket.IO v3+ payloads. Replaced with a custom + `_splitPackets()` splitter that locates packet boundaries on `]`, `}` or + `"` followed by a digit, then decodes each packet individually via + `PacketParser.decodePacket()`. This makes polling fully equivalent to the + WebSocket transport for all current Socket.IO clients (JS, browser, Dart). +- **Polling transport – connection close race** (`doWrite / respond`) + `respond()` was synchronous and used `unawaited(connect!.close())`, causing + premature or failed connection closes that produced *xhr poll error* on + clients. `respond` is now `async` and `await`s `connect!.close()`. +- **Polling transport – CORS headers** + Added `Access-Control-Allow-Origin`, `Access-Control-Allow-Credentials`, + `Access-Control-Allow-Methods`, and cache-control headers to every polling + response so browser and cross-origin Dart clients do not get blocked. +- **Namespace – typed connection handler** + `onConnection()` now casts the raw `dynamic` listener argument to `Socket` + before invoking the typed `EventHandler` callback. + +### Added + +- `example/polling_smoke.dart` – raw Engine.IO v4 polling smoke test that + can be run against a live server to verify the polling fix end-to-end. +- `--with-polling-smoke` flag in `tool/check.sh`. + +--- + +## 2.0.0 – 2025-10-11 + +### Changed (breaking – Dart 3 migration) + +- Minimum SDK raised to `>=3.0.0 <4.0.0`. +- Dart 3 modernisation: sealed classes, records, exhaustive switches, and + improved null-safety throughout. +- Package renamed from `tp_socket_io` to `socket_io`; all import paths + updated (`package:socket_io/socket_io.dart`). + +### Added + +- **Typed API** – value objects (`ConnectionId`, `RoomName`, `EventName`, + `NamespaceName`, `PortNumber`, …) and sealed domain models + (`SocketIOPacket`, `SocketIOError`, `TransportData`, …) alongside the + legacy untyped API for gradual migration. +- **Expanded test suite** – 773 tests across 34 files covering every value + object, model, extension, and error type. +- `doc/ARCHITECTURE.md`, `doc/TYPE_SAFE_EXAMPLES.md`, + `doc/TYPE_SAFETY_MIGRATION_GUIDE.md`, `doc/QUICK_REFERENCE.md`. + +--- + +## 1.0.1 – upstream baseline + +Upstream release from [`rikulo/socket.io-dart`](https://github.com/rikulo/socket.io-dart). +See [upstream CHANGELOG](https://github.com/rikulo/socket.io-dart/blob/master/CHANGELOG.md) +for history prior to this fork. diff --git a/README.md b/README.md index b578007..764ee01 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,537 @@ -# socket.io-dart +# socket_io -Port of awesome JavaScript Node.js library - [Socket.io v2.0.1](https://github.com/socketio/socket.io) - in Dart +[![Dart](https://img.shields.io/badge/Dart-%3E%3D3.0.0-blue.svg)](https://dart.dev) +[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE) +[![Fork of rikulo/socket.io-dart](https://img.shields.io/badge/fork-rikulo%2Fsocket.io--dart-orange.svg)](https://github.com/rikulo/socket.io-dart) -## Usage +> **This is a fork** of [`rikulo/socket.io-dart`](https://github.com/rikulo/socket.io-dart) with critical polling-transport fixes for Engine.IO v4 / +> Socket.IO v3+ clients that are not yet merged upstream. Once upstream accepts these changes this +> package can be replaced with the official `socket_io` from pub.dev and the +> `dependency_override` can be removed. +> +> **Upstream PR tracking:** see [`doc/UPSTREAM_PR.md`](doc/UPSTREAM_PR.md). + +A modern, type-safe Dart implementation of [Socket.IO](https://socket.io/) for real-time +bidirectional event-based communication. + +Port of the JavaScript Node.js library [Socket.IO](https://github.com/socketio/socket.io) with +modern Dart 3.0+ features including sealed classes, value objects, and comprehensive type safety. + +## Features + +✨ **Modern & Type-Safe** +- Full Dart 3.0+ support with sealed classes and pattern matching +- Comprehensive type safety with value objects and domain models +- Dual API: backward-compatible legacy API + modern typed API + +🔌 **Complete Socket.IO Implementation** +- Real-time bidirectional communication +- Multiple transport support (WebSocket, HTTP long-polling, JSONP) +- Namespace and room support for message isolation +- Acknowledgment callbacks +- Binary data support + +🏗️ **Production Ready** +- 748+ passing tests with comprehensive coverage +- Zero analysis issues +- Extensive error handling with typed error models +- Full backward compatibility + +## Installation + +### Using this fork (recommended until upstream fix is merged) + +Add a `dependency_override` in your project's `pubspec.yaml` to use this fork +from your local checkout or from GitHub: + +```yaml +# pubspec.yaml of your consuming project +dependencies: + socket_io: ^2.0.1 + +dependency_overrides: + socket_io: + git: + url: https://github.com/reinbeumer/socket.io-dart.git + ref: master +``` + +Or point directly to a local path during development: + +```yaml +dependency_overrides: + socket_io: + path: /path/to/socket.io-dart +``` + +Then run: + +```zsh +dart pub get +``` + +### Removing the override (once fix is upstream) + +When the polling fix is accepted into [`rikulo/socket.io-dart`](https://github.com/rikulo/socket.io-dart) +and a new version is published to pub.dev, simply remove the `dependency_overrides` block and bump +the version constraint to require the fixed release. + +## Quick Start + +### Server Example ```dart import 'package:socket_io/socket_io.dart'; -main() { - var io = new Server(); - var nsp = io.of('/some'); - nsp.on('connection', (client) { - print('connection /some'); - client.on('msg', (data) { - print('data from /some => $data'); - client.emit('fromServer', "ok 2"); - }); +void main() { + // Create a Socket.IO server + final io = Server(); + + // Listen for client connections + io.onConnection((socket) { + print('Client connected: ${socket.id}'); + + // Listen for messages from client + socket.on('message', (data) { + print('Received: $data'); + + // Send acknowledgment back to client + socket.emit('messageReceived', ['Message processed']); + }); + + // Handle disconnection + socket.on('disconnect', (_) { + print('Client disconnected: ${socket.id}'); }); - io.on('connection', (client) { - print('connection default namespace'); - client.on('msg', (data) { - print('data from default => $data'); - client.emit('fromServer', "ok"); - }); - }); - io.listen(3000); + }); + + // Start listening on port 3000 + io.listen(3000); + print('Socket.IO server running on port 3000'); } ``` -```js -// JS client -var socket = io('http://localhost:3000'); -socket.on('connect', function(){console.log('connect')}); -socket.on('event', function(data){console.log(data)}); -socket.on('disconnect', function(){console.log('disconnect')}); -socket.on('fromServer', function(e){console.log(e)}); +### Client Example (JavaScript) + +```javascript +const socket = io('http://localhost:3000'); + +socket.on('connect', () => { + console.log('Connected to server'); + socket.emit('message', 'Hello from client!'); +}); + +socket.on('messageReceived', (data) => { + console.log('Server response:', data); +}); + +socket.on('disconnect', () => { + console.log('Disconnected from server'); +}); ``` +### Client Example (Dart) + ```dart -// Dart client import 'package:socket_io_client/socket_io_client.dart' as IO; -IO.Socket socket = IO.io('http://localhost:3000'); -socket.on('connect', (_) { - print('connect'); - socket.emit('msg', 'test'); +void main() { + final socket = IO.io('http://localhost:3000'); + + socket.on('connect', (_) { + print('Connected to server'); + socket.emit('message', 'Hello from Dart client!'); + }); + + socket.on('messageReceived', (data) { + print('Server response: $data'); + }); + + socket.on('disconnect', (_) { + print('Disconnected from server'); + }); +} +``` + +## Core Concepts + +### Namespaces + +Namespaces allow you to create separate communication channels over a single connection: + +```dart +final io = Server(); + +// Default namespace +io.onConnection((socket) { + print('Client connected to default namespace'); +}); + +// Custom namespace +final chatNamespace = io.of('/chat'); +chatNamespace.on('connection', (socket) { + print('Client connected to /chat namespace'); + + socket.on('chatMessage', (data) { + // Broadcast to all clients in this namespace + chatNamespace.emit('newMessage', data); + }); +}); + +io.listen(3000); +``` + +### Rooms + +Rooms are arbitrary channels that sockets can join and leave: + +```dart +io.onConnection((socket) { + // Join a room + socket.join('room1'); + + // Emit to all clients in a room + io.to('room1').emit('announcement', ['Welcome to room1']); + + // Leave a room + socket.leave('room1'); + + // Broadcast to all except sender + socket.broadcast.to('room1').emit('userJoined', [socket.id]); +}); +``` + +### Acknowledgments + +Request callbacks from the receiving side: + +```dart +// Server side +socket.on('question', (data) { + socket.emit('answer', ['The answer is 42'], ack: (response) { + print('Client acknowledged: $response'); + }); +}); + +// Client side +socket.emit('question', ['What is the meaning of life?'], ack: (answer) { + print('Server answered: $answer'); }); -socket.on('event', (data) => print(data)); -socket.on('disconnect', (_) => print('disconnect')); -socket.on('fromServer', (_) => print(_)); ``` -## Multiplexing support +## Modern Typed API + +This library provides a modern, type-safe API alongside the legacy API for backward compatibility: + +### Type-Safe Event Handling + +```dart +import 'package:socket_io/socket_io.dart'; + +// Using typed models +socket.on('userLogin', (data) { + final eventData = EventData.fromDynamic(data); + if (eventData is MapEventData) { + final username = eventData.value['username']; + print('User logged in: $username'); + } +}); + +// Using typed query parameters +socket.queryParameters?.get('token'); // Type-safe access + +// Using typed handshake data +final handshake = socket.handshakeData; +print('Connected from: ${handshake?.address}'); +print('Secure connection: ${handshake?.secure}'); +``` + +### Value Objects + +The library uses value objects for type safety: + +```dart +// Connection ID (validated non-empty string) +final connectionId = ConnectionId('socket-123'); + +// Room names (validated) +final room = RoomName('chatRoom'); + +// Event names (validated, no reserved names) +final event = EventName('customEvent'); + +// Namespace names (must start with /) +final namespace = NamespaceName('/admin'); +``` + +### Sealed Classes for Pattern Matching + +```dart +// Type-safe error handling +socket.on('error', (error) { + if (error is SocketIOError) { + switch (error) { + case TransportErrorModel(): + print('Transport error: ${error.message}'); + case ConnectionErrorModel(): + print('Connection error: ${error.message}'); + case ValidationErrorModel(): + print('Validation error: ${error.message}'); + } + } +}); +``` + +## Advanced Features + +### Middleware + +Add middleware to process connections: + +```dart +final io = Server(); + +// Namespace-level middleware +final adminNamespace = io.of('/admin'); +adminNamespace.use((socket, next) { + // Verify authentication token + final token = socket.handshake?['auth']?['token']; + if (token == 'secret') { + next(null); // Allow connection + } else { + next('Authentication failed'); // Reject connection + } +}); + +adminNamespace.onConnection((socket) { + print('Authenticated admin connected'); +}); +``` + +### Custom Server Options + +```dart +final io = Server( + options: { + 'pingTimeout': 60000, + 'pingInterval': 25000, + 'transports': ['websocket', 'polling'], + 'path': '/socket.io', + }, +); +``` + +### Broadcasting + +```dart +io.onConnection((socket) { + // To all clients + io.emit('broadcast', ['Message to everyone']); + + // To all clients except sender + socket.broadcast.emit('broadcast', ['Message to others']); + + // To specific room + io.to('room1').emit('roomMessage', ['Message to room1']); + + // To multiple rooms + io.to('room1').to('room2').emit('multiRoom', ['Message']); + + // To room except sender + socket.broadcast.to('room1').emit('announcement', ['User joined']); +}); +``` + +### Binary Data + +```dart +socket.on('image', (data) { + if (data is List) { + // Handle binary data + print('Received binary data of length: ${data.length}'); + } +}); + +// Send binary data +socket.emit('imageResponse', [imageBytes]); +``` + +## Transport Layers + +The library supports multiple transport mechanisms: + +1. **WebSocket** - Full-duplex communication channel +2. **HTTP Long-polling** - Fallback for environments without WebSocket support +3. **JSONP Polling** - Legacy support for older browsers + +Transport selection is automatic based on client capabilities and can be configured: + +```dart +final io = Server( + options: { + 'transports': ['websocket', 'polling'], // Preferred order + }, +); +``` + +## Error Handling + +Comprehensive typed error handling: + +```dart +socket.on('error', (error) { + if (error is SocketIOError) { + print('Error type: ${error.type}'); + print('Message: ${error.message}'); + print('Code: ${error.code}'); + print('Context: ${error.context}'); + } +}); + +// Graceful error recovery +socket.on('connect_error', (error) { + print('Connection failed, retrying...'); +}); +``` + +## Testing + +Run tests: + +```bash +dart test +``` + +Run tests with coverage: + +```bash +dart test --coverage=coverage +dart pub global activate coverage +dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib +``` + +## API Documentation + +For detailed API documentation, see: + +- [TYPE_SAFE_EXAMPLES.md](doc/TYPE_SAFE_EXAMPLES.md) - Comprehensive examples of the typed API +- [TYPE_SAFETY_MIGRATION_GUIDE.md](doc/TYPE_SAFETY_MIGRATION_GUIDE.md) - Migration guide from legacy to typed API +- [QUICK_REFERENCE.md](doc/QUICK_REFERENCE.md) - Quick reference guide + +## Architecture + +This library follows modern Dart architectural patterns: + +- **Value Objects**: Type-safe primitives with validation (ConnectionId, RoomName, etc.) +- **Domain Models**: Rich models for business logic (SocketIOPacket, HandshakeData, etc.) +- **Sealed Classes**: Exhaustive pattern matching for errors and data types +- **Extension Methods**: Utility methods without cluttering core classes +- **Dual API**: Backward compatibility with modern type-safe alternatives + +For detailed architecture documentation, see [doc/ARCHITECTURE.md](doc/ARCHITECTURE.md). + +## Compatibility + +- **Dart SDK**: >= 3.0.0 < 4.0.0 +- **Socket.IO Protocol**: Compatible with Socket.IO v2.x clients +- **Platforms**: VM, Web (with appropriate transport configuration) + +## Migration from Legacy API + +If you're using the legacy untyped API, you can gradually migrate to the typed API: + +```dart +// Legacy (still supported) +socket.on('message', (data) { + final Map map = data as Map; + print(map['text']); +}); + +// Modern typed API +socket.on('message', (data) { + final eventData = EventData.fromDynamic(data); + if (eventData is MapEventData) { + print(eventData.value['text']); + } +}); +``` + +See [TYPE_SAFETY_MIGRATION_GUIDE.md](doc/TYPE_SAFETY_MIGRATION_GUIDE.md) for details. + +## Contributing + +Contributions are welcome! Please read our contributing guidelines: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes with tests +4. Ensure all tests pass (`dart test`) +5. Ensure code is formatted (`dart format .`) +6. Ensure no analysis issues (`dart analyze`) +7. Commit your changes (`git commit -m 'Add amazing feature'`) +8. Push to the branch (`git push origin feature/amazing-feature`) +9. Open a Pull Request + +For detailed contribution guidelines, see [doc/CONTRIBUTING.md](doc/CONTRIBUTING.md). + +## Examples + +The [example](example/) directory is now intentionally small and focused: + +- `example/example_server.dart` - server example (listens on port `3005`) +- `example/example_client.js` - Node.js client example +- `example/example_client.dart` - basic Dart client example +- `example/example_client.html` - browser client example with live logs +- `example/polling_smoke.dart` - raw Engine.IO polling smoke test + +Quick run: + +```zsh +dart run example/example_server.dart +dart run example/polling_smoke.dart +``` + +Optional full quality check including polling smoke: + +```zsh +bash tool/check.sh --with-polling-smoke +``` -Same as Socket.IO, this project allows you to create several Namespaces, which will act as separate communication channels but will share the same underlying connection. +Note: this package is server-only; client application examples are provided via +Node.js (`example/example_client.js`) and browser (`example/example_client.html`). -## Room support +## Projects Using socket_io -Within each Namespace, you can define arbitrary channels, called Rooms, that sockets can join and leave. You can then broadcast to any given room, reaching every socket that has joined it. +- [Quire](https://quire.io) - A simple, collaborative, multi-level task management tool +- [KEIKAI](https://keikai.io/) - A web spreadsheet for Big Data -## Transports support - Refers to [engine.io](https://github.com/socketio/engine.io) +## Related Projects -- `polling`: XHR / JSONP polling transport. -- `websocket`: WebSocket transport. +- [socket.io-client-dart](https://github.com/rikulo/socket.io-client-dart) - Dart client for Socket.IO +- [socket_io_common](https://pub.dev/packages/socket_io_common) - Common components for Socket.IO -## Adapters support +## License -* Default socket.io in-memory adapter class. Refers to [socket.io-adapter](https://github.com/socketio/socket.io-adapter) +Apache License 2.0 - see [LICENSE](LICENSE) file for details. -## Notes to Contributors +## Acknowledgments -### Fork socket.io-dart +This is a port of the JavaScript [Socket.IO](https://socket.io/) library. Special thanks to: -If you'd like to contribute back to the core, you can [fork this repository](https://help.github.com/articles/fork-a-repo) and send us a pull request, when it is ready. +- The Socket.IO team for the original implementation +- All contributors who have helped improve this library +- The Dart team for the excellent language and tooling -If you are new to Git or GitHub, please read [this guide](https://help.github.com/) first. +## Support -## Who Uses +- **Issues**: [GitHub Issues](https://github.com/reinbeumer/socket.io-dart/issues) +- **Discussions**: Use GitHub Discussions for questions and ideas +- **Documentation**: Check the [doc](doc/) directory -* [Quire](https://quire.io) - a simple, collaborative, multi-level task management tool. -* [KEIKAI](https://keikai.io/) - a web spreadsheet for Big Data. +## Changelog -## Socket.io Dart Client +See [CHANGELOG.md](CHANGELOG.md) for version history and migration notes. -* [socket.io-client-dart](https://github.com/rikulo/socket.io-client-dart) +--- -## Contributors -* Thanks [@felangel](https://github.com/felangel) for https://github.com/rikulo/socket.io-dart/issues/7 -* Thanks [@ThinkDigitalSoftware](https://github.com/ThinkDigitalSoftware) for https://github.com/rikulo/socket.io-dart/pull/15 -* Thanks [@guilhermecaldas](https://github.com/guilhermecaldas) for https://github.com/rikulo/socket.io-dart/pull/16 -* Thanks [@jodinathan](https://github.com/jodinathan) for https://github.com/rikulo/socket.io-dart/pull/17 -* Thanks [@jodinathan](https://github.com/jodinathan) for https://github.com/rikulo/socket.io-dart/pull/18 -* Thanks [@nicobritos](https://github.com/nicobritos) for https://github.com/rikulo/socket.io-dart/pull/46 -* Thanks [@nicobritos](https://github.com/nicobritos) for https://github.com/rikulo/socket.io-dart/pull/47 \ No newline at end of file +Made with ❤️ by the Dart community diff --git a/analysis_options.yaml b/analysis_options.yaml index 80bb834..f5e631d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,12 +1,95 @@ -# This file allows you to configure the Dart analyzer. -# -# The commented part below is just for inspiration. Read the guide here: -# https://www.dartlang.org/guides/language/analysis-options -include: package:pedantic/analysis_options.yaml +#include: package:lint/strict.yaml +include: package:lints/recommended.yaml + +linter: + rules: + always_declare_return_types: true + always_specify_types: true + + # https://www.reddit.com/r/FlutterDev/comments/1pggt66/a_lint_package_that_prevents_subtle_singleton/ + always_use_package_imports: false + prefer_relative_imports: true + + avoid_bool_literals_in_conditional_expressions: false + avoid_classes_with_only_static_members: false + avoid_final_parameters: false + avoid_function_literals_in_foreach_calls: false + avoid_implementing_value_types: false + avoid_print: true + camel_case_types: false + cascade_invocations: true + combinators_ordering: true + constant_identifier_names: false + curly_braces_in_flow_control_structures: true + directives_ordering: true + discarded_futures: true + join_return_with_assignment: false + library_private_types_in_public_api: false + missing_whitespace_between_adjacent_strings: false + no_adjacent_strings_in_list: false + non_constant_identifier_names: false + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_final_locals: true + # prefer_final_parameters: true + parameter_assignments: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_single_quotes: true + prefer_void_to_null: false + switch_on_type: true + type_literal_in_constant_pattern: true + unawaited_futures: true + unintended_html_in_doc_comment: false + unnecessary_await_in_return: true + unnecessary_breaks: false + unnecessary_lambdas: true + unnecessary_raw_strings: true + unnecessary_unawaited: true + use_null_aware_elements: true + use_setters_to_change_properties: false + use_string_in_part_of_directives: false + + # consistency (from `dart_flutter_team_lints`) + lines_longer_than_80_chars: false + omit_local_variable_types: false + prefer_asserts_in_initializer_lists: false + prefer_const_constructors: true + sort_pub_dependencies: false + unnecessary_library_directive: false + unnecessary_library_name: false + unnecessary_parenthesis: false + unnecessary_statements: true + use_is_even_rather_than_modulo: true + + # correctness (from `dart_flutter_team_lints`) + avoid_catching_errors: false + avoid_dynamic_calls: false + comment_references: false + conditional_uri_does_not_exist: true + only_throw_errors: true + test_types_in_equals: true + throw_in_finally: true + type_annotate_public_apis: true + unreachable_from_main: false + analyzer: -# exclude: -# - path/to/excluded/files/** -# linter: -# rules: -# # see catalog here: http://dart-lang.github.io/linter/lints/ -# - hash_and_equals + plugins: + - dart_code_linter + language: + strict-casts: false + strict-inference: true + strict-raw-types: true + exclude: + - "**/*.g.dart" + - build/** + - "test/**" + - "example/**" + - "test*.dart" + - "**/test_*.dart" + +formatter: + page_width: 120 + trailing_commas: preserve + + diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md new file mode 100644 index 0000000..15ebd34 --- /dev/null +++ b/doc/ARCHITECTURE.md @@ -0,0 +1,803 @@ +# Architecture Documentation + +**socket_io-dart** - Modern Socket.IO Server Implementation + +This document describes the architectural patterns, design decisions, and code organization of the socket_io library. + +--- + +## Table of Contents + +1. [High-Level Architecture](#high-level-architecture) +2. [Directory Structure](#directory-structure) +3. [Core Concepts](#core-concepts) +4. [Type System & Value Objects](#type-system--value-objects) +5. [Domain Models](#domain-models) +6. [Extension Methods Pattern](#extension-methods-pattern) +7. [Sealed Classes & Pattern Matching](#sealed-classes--pattern-matching) +8. [Backward Compatibility Strategy](#backward-compatibility-strategy) +9. [Transport Layer](#transport-layer) +10. [Event System](#event-system) +11. [Testing Strategy](#testing-strategy) + +--- + +## High-Level Architecture + +socket_io follows a **layered architecture** with clear separation of concerns: + +``` +┌─────────────────────────────────────────────────────┐ +│ Public API │ +│ (Server, Socket, Namespace) │ +├─────────────────────────────────────────────────────┤ +│ Domain Layer │ +│ (Models, Value Objects, Business Logic) │ +├─────────────────────────────────────────────────────┤ +│ Transport Layer │ +│ (WebSocket, Polling, JSONP Transports) │ +├─────────────────────────────────────────────────────┤ +│ Engine.IO Layer │ +│ (Low-level connection management) │ +└─────────────────────────────────────────────────────┘ +``` + +### Key Architectural Principles + +1. **Type Safety First**: Extensive use of value objects, sealed classes, and typed models +2. **Dual API Pattern**: Backward compatibility with legacy API while providing modern typed alternatives +3. **Domain-Driven Design**: Rich domain models with business logic encapsulation +4. **Extension-based Utilities**: Non-intrusive functionality additions via extensions +5. **Event-Driven Architecture**: Observer pattern for real-time communication +6. **Adapter Pattern**: Pluggable room/namespace management + +--- + +## Directory Structure + +``` +lib/ +├── src/ +│ ├── adapter/ # Room and namespace adapters +│ │ └── adapter.dart # Default memory adapter +│ ├── constants/ # Shared constants +│ │ └── socket_events.dart +│ ├── engine/ # Engine.IO transport layer +│ │ ├── engine.dart # Engine.IO server +│ │ ├── socket.dart # Engine.IO socket +│ │ └── transport/ # Transport implementations +│ │ ├── polling_transport.dart +│ │ ├── websocket_transport.dart +│ │ ├── jsonp_transport.dart +│ │ └── transports.dart +│ ├── extensions/ # Extension methods +│ │ ├── duration_extensions.dart +│ │ ├── list_extensions.dart +│ │ ├── map_extensions.dart +│ │ ├── packet_extensions.dart +│ │ ├── socket_extensions.dart +│ │ └── string_extensions.dart +│ ├── models/ # Domain models (26+ files) +│ │ ├── packet_models.dart +│ │ ├── server_options_models.dart +│ │ ├── error_models.dart +│ │ ├── callbacks_models.dart +│ │ └── ... (20+ more) +│ ├── value_objects/ # Value objects (14 types) +│ │ ├── connection_id_vo.dart +│ │ ├── room_name_vo.dart +│ │ ├── event_name_vo.dart +│ │ └── ... (11 more) +│ ├── util/ # Utilities +│ │ └── event_emitter.dart +│ ├── client.dart # Client connection handler +│ ├── namespace.dart # Namespace management +│ ├── server.dart # Socket.IO server +│ └── socket.dart # Socket connection +└── socket_io.dart # Public API exports + +test/ # Test suite (748 tests) +├── models/ # Model tests +├── value_objects/ # Value object tests +└── ... +``` + +### Organization Principles + +- **models/**: Rich domain models with business logic +- **value_objects/**: Immutable validated primitives +- **extensions/**: Utility methods organized by type +- **engine/**: Low-level transport and connection handling +- **adapter/**: Room and namespace management strategies + +--- + +## Core Concepts + +### 1. Server + +The `Server` class is the main entry point: + +```dart +final server = Server(); +server.on('connection', (socket) { + // Handle socket connection +}); +server.listen(3000); +``` + +**Responsibilities:** +- Manages namespaces +- Handles client connections +- Configures transport options +- Provides adapter configuration + +### 2. Namespace + +Namespaces create separate communication channels: + +```dart +final chatNamespace = server.of('/chat'); +chatNamespace.on('connection', (socket) { + // Chat-specific logic +}); +``` + +**Responsibilities:** +- Isolates message routing +- Manages socket connections within namespace +- Applies middleware +- Manages rooms + +### 3. Socket + +Individual client connections: + +```dart +socket.on('message', (data) { + socket.emit('response', ['received']); +}); +``` + +**Responsibilities:** +- Event emission and reception +- Room membership management +- Binary data handling +- Acknowledgment callbacks + +### 4. Adapter + +Manages rooms and broadcasts: + +```dart +class Adapter { + void add(String id, String room); + void broadcast(Map packet, Map? opts); + void clients(List? rooms, ClientsCallback fn); +} +``` + +**Default:** In-memory adapter +**Extensible:** Can implement Redis, MongoDB adapters + +--- + +## Type System & Value Objects + +### Philosophy + +Value Objects provide **type safety** and **validation** at the boundaries: + +```dart +// Instead of: String roomName +final room = RoomName('chatRoom'); // Validated, non-empty + +// Instead of: int port +final port = PortNumber(3000); // Validated range 1-65535 + +// Instead of: String eventName +final event = EventName('message'); // No reserved names +``` + +### Value Object Catalog + +| Value Object | Validation | Purpose | +|--------------|------------|---------| +| `ConnectionId` | Non-empty string | Socket connection identifier | +| `RoomName` | Non-empty string | Room identifier | +| `EventName` | No reserved names | Custom event names | +| `NamespaceName` | Starts with `/` | Namespace identifier | +| `PortNumber` | 1-65535 | Port validation | +| `TimeoutDuration` | Non-negative | Timeout configuration | +| `TransportName` | Valid transport type | Transport selection | +| `UrlPath` | Starts with `/` | URL path validation | +| `QueryParameters` | Key-value map | Type-safe query access | +| `ErrorCode` | Numeric or string | Error identification | +| `DisconnectReason` | Enum-based | Disconnect classification | +| `SocketState` | Enum-based | Connection state | +| `PacketId` | Non-empty | Packet identification | +| `EventArguments` | Type-safe list | Event data container | + +### Value Object Pattern + +```dart +class ConnectionId { + final String value; + + // Private constructor + const ConnectionId._(this.value); + + // Validated factory + factory ConnectionId(String id) { + if (id.isEmpty) { + throw ArgumentError('Connection ID cannot be empty'); + } + return ConnectionId._(id); + } + + // Unchecked for trusted sources + const ConnectionId.unchecked(this.value); + + @override + bool operator ==(Object other) => + other is ConnectionId && value == other.value; + + @override + int get hashCode => value.hashCode; + + @override + String toString() => value; +} +``` + +--- + +## Domain Models + +### Model Categories + +**1. Packet Models** (`packet_models.dart`) +- `SocketIOPacket` (sealed class) +- `ConnectPacket`, `DisconnectPacket`, `EventPacket`, `AckPacket` +- Type-safe packet construction + +**2. Configuration Models** +- `ServerOptionsModel`: Server configuration +- `HandshakeDataModel`: Connection handshake data +- `SocketDataModel`: Per-socket data storage + +**3. Error Models** (`error_models.dart`) +- `SocketIOError` (sealed class) +- `TransportErrorModel`, `ConnectionErrorModel`, `ValidationErrorModel` +- Typed error handling + +**4. Transport Models** +- `TransportData` (sealed class) +- `StringTransportData`, `BinaryTransportData`, `JsonTransportData` + +**5. Room & Broadcast Models** +- `RoomMembership`: Type-safe room tracking +- `BroadcastOptions`: Broadcast configuration +- `RoomFilter`: Room selection logic + +### Sealed Class Pattern + +Sealed classes enable **exhaustive pattern matching**: + +```dart +sealed class SocketIOError implements Exception { + String get type; + String get message; +} + +class TransportErrorModel extends SocketIOError { ... } +class ConnectionErrorModel extends SocketIOError { ... } +class ValidationErrorModel extends SocketIOError { ... } + +// Exhaustive switch - compiler ensures all cases covered +void handleError(SocketIOError error) { + switch (error) { + case TransportErrorModel(): + // Handle transport error + case ConnectionErrorModel(): + // Handle connection error + case ValidationErrorModel(): + // Handle validation error + } + // Compiler error if any case is missing! +} +``` + +--- + +## Extension Methods Pattern + +Extensions add utility methods **without modifying core classes**: + +### Examples + +**Packet Extensions** (`packet_extensions.dart`): +```dart +extension PacketExtensions on SocketIOPacket { + bool get isConnect => type == CONNECT; + bool get isDisconnect => type == DISCONNECT; + bool get isEvent => type == EVENT || type == BINARY_EVENT; + String get typeName { ... } + String get description { ... } +} +``` + +**Map Extensions** (`map_extensions.dart`): +```dart +extension TypeSafeMapAccess on Map { + T? getTyped(String key) => this[key] as T?; + String? getString(String key) => getTyped(key); + int? getInt(String key) => getTyped(key); + Map? getMap(String key) => + getTyped>(key); +} +``` + +**Duration Extensions** (`duration_extensions.dart`): +```dart +extension DurationFormatting on Duration { + String toReadableString() { ... } + bool get isLongerThan(Duration other) { ... } +} +``` + +### Extension Benefits + +✅ **Non-intrusive**: Don't modify original classes +✅ **Organized**: Group related utilities +✅ **Discoverable**: IDE auto-completion +✅ **Type-safe**: Compile-time checking +✅ **Testable**: Easy to unit test + +--- + +## Sealed Classes & Pattern Matching + +### Why Sealed Classes? + +Dart 3.0 introduced sealed classes for **closed type hierarchies**: + +```dart +sealed class TransportData { + const TransportData(); +} + +final class StringTransportData extends TransportData { + final String value; + const StringTransportData(this.value); +} + +final class BinaryTransportData extends TransportData { + final List bytes; + const BinaryTransportData(this.bytes); +} + +final class JsonTransportData extends TransportData { + final Map data; + const JsonTransportData(this.data); +} +``` + +### Benefits + +1. **Exhaustive Checking**: Compiler ensures all cases handled +2. **No Default Case Needed**: All subtypes known at compile time +3. **Refactoring Safety**: Adding new subtype causes compile errors +4. **IDE Support**: Better auto-completion and hints + +### Usage in socket_io + +- `SocketIOPacket`: All packet types +- `SocketIOError`: All error types +- `TransportData`: All data formats +- `EventData`: All event data types +- `CookieConfig`: Enabled/disabled states +- `ValidationError`: All validation error types + +--- + +## Backward Compatibility Strategy + +### Dual API Pattern + +The library maintains **two parallel APIs**: + +**Legacy API** (for backward compatibility): +```dart +socket.on('message', (data) { + final map = data as Map; + print(map['text']); +}); + +socket.handshake['query']; // Map +socket.data['userId'] = 123; // Map +socket.acks[id] = callback; // Map +``` + +**Modern Typed API**: +```dart +socket.on('message', (data) { + final eventData = EventData.fromDynamic(data); + if (eventData is MapEventData) { + print(eventData.value['text']); + } +}); + +socket.handshakeData?.query; // QueryParameters +socket.socketData.set('userId', 123); // SocketDataModel +socket.acksTyped[id] = callback; // Map +``` + +### Implementation Strategy + +**Dual Fields**: +```dart +class Socket { + // Legacy (kept for BC) + Map? handshake; + Map data = {}; + Map acks = {}; + + // Modern typed (new) + HandshakeDataModel? handshakeData; + SocketDataModel socketData = SocketDataModel(); + Map acksTyped = {}; +} +``` + +**Synchronization**: +```dart +// Both APIs work with same underlying data +data = socketData.toMap(); // Sync old → new +socketData.fromMap(data); // Sync new → old +``` + +### Migration Path + +1. **Phase 1** (Current): Both APIs available +2. **Phase 2** (Future): Deprecate old API +3. **Phase 3** (Major version): Remove old API + +--- + +## Transport Layer + +### Transport Hierarchy + +``` +Transport (abstract) +├── WebSocketTransport - Full-duplex, lowest latency +├── PollingTransport - HTTP long-polling fallback +└── JSONPTransport - Legacy browser support +``` + +### Transport Selection + +**Client-driven** with server preferences: + +```dart +final server = Server(options: { + 'transports': ['websocket', 'polling'], // Preference order +}); +``` + +**Automatic fallback**: +1. Try WebSocket +2. Fall back to Polling if WebSocket unavailable +3. Use JSONP for legacy browsers if needed + +### Transport Interface + +```dart +abstract class Transport { + void send(List> packets); + void close(); + void onPacket(Map packet); + void onClose(); +} +``` + +### Engine.IO Integration + +socket_io builds on **Engine.IO** for transport management: + +``` +Socket.IO (Application Protocol) + ↓ + SocketIOPacket encoding/decoding + ↓ +Engine.IO (Transport Protocol) + ↓ +WebSocket | HTTP Polling | JSONP +``` + +--- + +## Event System + +### EventEmitter Pattern + +Core event system based on **Observer pattern**: + +```dart +class EventEmitter { + // Event → List of handlers + HashMap> _events; + + void on(String event, EventHandler handler) { ... } + void once(String event, EventHandler handler) { ... } + void emit(String event, [dynamic data]) { ... } + void off(String event, [EventHandler? handler]) { ... } +} +``` + +### Event Flow + +``` +Client Event + ↓ +Transport.onPacket() + ↓ +Engine.Socket.onPacket() + ↓ +Client.onpacket() + ↓ +Socket.emit(eventName, data) + ↓ +User Handler +``` + +### Reserved Events + +Defined in `SocketEvents.blacklisted`: +- `connect` / `connection` +- `disconnect` +- `error` +- `newListener` +- `removeListener` + +Custom events **cannot** use these names (validated by `EventName` value object). + +--- + +## Testing Strategy + +### Test Organization + +``` +test/ +├── models/ # Domain model tests +│ ├── packet_models_test.dart +│ ├── error_models_test.dart +│ └── ... (15 more) +├── value_objects/ # Value object tests +│ ├── connection_id_vo_test.dart +│ ├── room_name_vo_test.dart +│ └── ... (12 more) +├── adapter_broadcast_models_test.dart +├── namespace_config_models_test.dart +└── typed_event_emitter_test.dart +``` + +### Testing Principles + +1. **Unit Tests**: Every value object and model tested independently +2. **Property-Based**: Validation rules thoroughly tested +3. **Edge Cases**: Null, empty, invalid inputs +4. **Type Safety**: Ensure compile-time type checking works +5. **Backward Compatibility**: Both APIs tested + +### Test Coverage + +- **748 tests** across 34 test files +- **Value Objects**: 100% coverage of validation rules +- **Models**: Factory methods, equality, serialization +- **Extensions**: All utility methods tested +- **Error Handling**: All error types and factories + +### Example Test Pattern + +```dart +group('ConnectionId', () { + test('creates valid ConnectionId from non-empty string', () { + final id = ConnectionId('test-id'); + expect(id.value, equals('test-id')); + }); + + test('throws ArgumentError for empty string', () { + expect(() => ConnectionId(''), throwsArgumentError); + }); + + test('equality works correctly', () { + final id1 = ConnectionId('test'); + final id2 = ConnectionId('test'); + expect(id1, equals(id2)); + }); + + test('hashCode works correctly', () { + final id1 = ConnectionId('test'); + final id2 = ConnectionId('test'); + expect(id1.hashCode, equals(id2.hashCode)); + }); +}); +``` + +--- + +## Key Design Patterns + +### 1. Factory Pattern + +Used extensively for packet creation: + +```dart +// Factory methods for different packet types +EventPacket.typed(eventName: 'message', data: ...); +AckPacket.typed(id: '123', data: ...); +ConnectPacket.typed(namespace: '/chat'); +``` + +### 2. Builder Pattern + +Complex object construction: + +```dart +HandshakeDataBuilder() + .headers(request.headers) + .time(DateTime.now()) + .address(remoteAddress) + .secure(true) + .build(); +``` + +### 3. Adapter Pattern + +Pluggable room/namespace management: + +```dart +abstract class Adapter { + void add(String id, String room); + void broadcast(Map packet, Map? opts); +} + +// Default implementation +class _MemoryStoreAdapter extends Adapter { ... } + +// Future: RedisAdapter, MongoAdapter, etc. +``` + +### 4. Observer Pattern + +Event system foundation: + +```dart +server.on('connection', (socket) { ... }); +socket.on('message', (data) { ... }); +``` + +### 5. Strategy Pattern + +Transport selection and fallback: + +```dart +final transports = [ + WebSocketTransport(), + PollingTransport(), + JSONPTransport(), +]; +``` + +--- + +## Extension Points + +### Custom Adapters + +Implement `Adapter` interface for distributed setups: + +```dart +class RedisAdapter extends Adapter { + @override + void broadcast(Map packet, Map? opts) { + redis.publish(channel, packet); + } +} + +server.adapter = 'redis'; +Adapter.register('redis', (nsp) => RedisAdapter(nsp)); +``` + +### Custom Transports + +Extend `Transport` for new protocols: + +```dart +class CustomTransport extends Transport { + @override + void send(List packets) { ... } +} +``` + +### Middleware + +Add custom authentication, logging, rate limiting: + +```dart +namespace.use((socket, next) { + if (isAuthenticated(socket)) { + next(null); + } else { + next('Authentication required'); + } +}); +``` + +--- + +## Performance Considerations + +### Memory Efficiency + +- **Value Objects**: Immutable, can be cached/reused +- **Sealed Classes**: Optimized pattern matching +- **Event Handlers**: Lazy initialization of maps + +### Type Safety Benefits + +- **Compile-time checks**: Catch errors early +- **No runtime casts**: Faster execution +- **Better tree-shaking**: Unused code eliminated + +### Scalability + +- **Adapter pattern**: Enables horizontal scaling with Redis +- **Namespace isolation**: Prevents cross-talk +- **Room-based broadcasting**: Efficient message routing + +--- + +## Future Architecture Plans + +### Planned Improvements + +1. **Distributed Adapters**: Redis, MongoDB support +2. **Binary Protocol**: More efficient encoding +3. **Streaming**: Large file transfer support +4. **State Management**: Persistent session state +5. **Metrics**: Built-in performance monitoring + +### API Evolution + +Following **deprecation policy**: + +``` +v2.x: Dual API (current) +v3.x: Deprecate legacy API +v4.x: Remove legacy API (breaking) +``` + +--- + +## Conclusion + +socket_io's architecture balances: + +✅ **Modern Dart**: Leverages Dart 3.0+ features +✅ **Type Safety**: Value objects and sealed classes +✅ **Backward Compatibility**: Smooth migration path +✅ **Extensibility**: Adapters, middleware, custom transports +✅ **Performance**: Efficient event routing and transport selection +✅ **Maintainability**: Clear separation of concerns, comprehensive tests + +The architecture supports both immediate production use and long-term evolution. + +--- + +**Document Version:** 1.0 +**Last Updated:** 2025-10-11 +**Maintainers:** socket_io team diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md new file mode 100644 index 0000000..7def395 --- /dev/null +++ b/doc/CONTRIBUTING.md @@ -0,0 +1,686 @@ +# Contributing to socket_io + +Thank you for your interest in contributing to socket_io! This document provides guidelines and instructions for contributing to the project. + +--- + +## Table of Contents + +1. [Code of Conduct](#code-of-conduct) +2. [Getting Started](#getting-started) +3. [Development Setup](#development-setup) +4. [Coding Standards](#coding-standards) +5. [Testing Requirements](#testing-requirements) +6. [Pull Request Process](#pull-request-process) +7. [Architecture Guidelines](#architecture-guidelines) +8. [Common Tasks](#common-tasks) +9. [Release Process](#release-process) + +--- + +## Code of Conduct + +### Our Standards + +- **Be respectful** and constructive in all interactions +- **Welcome newcomers** and help them get started +- **Focus on what is best** for the community and project +- **Show empathy** towards other community members +- **Accept constructive criticism** gracefully + +### Unacceptable Behavior + +- Harassment, discrimination, or offensive comments +- Trolling, insulting, or derogatory remarks +- Public or private harassment +- Publishing others' private information without permission + +--- + +## Getting Started + +### Prerequisites + +- **Dart SDK**: >= 3.0.0 < 4.0.0 +- **Git**: For version control +- **IDE**: VS Code, IntelliJ IDEA, or Android Studio (recommended) + +### Fork and Clone + +1. **Fork** the repository on GitHub +2. **Clone** your fork locally: + ```bash + git clone https://github.com/YOUR_USERNAME/socket.io-dart.git + cd socket.io-dart + ``` + +3. **Add upstream** remote: + ```bash + git remote add upstream https://github.com/rikulo/socket.io-dart.git + ``` + +4. **Install dependencies**: + ```bash + dart pub get + ``` + +--- + +## Development Setup + +### Install Development Tools + +```bash +# Format checker +dart format --version + +# Analyzer +dart analyze --version + +# Test runner +dart test --version +``` + +### Verify Setup + +Run all quality checks: + +```bash +# Format check +dart format . --output none --set-exit-if-changed + +# Analysis +dart analyze + +# Tests +dart test +``` + +All commands should complete successfully. + +--- + +## Coding Standards + +### File Naming + +- **Snake case** for files: `socket_options_models.dart` +- **PascalCase** for classes: `SocketOptionsModel` +- **camelCase** for variables and functions: `connectionId`, `buildHandshake()` + +### Code Style + +**Follow official Dart style guide**: https://dart.dev/guides/language/effective-dart/style + +#### Formatting + +```bash +# Format all Dart files +dart format . +``` + +Our configuration (in `analysis_options.yaml`): +- **Page width**: 120 characters +- **Trailing commas**: Preserved for better diffs + +#### Naming Conventions + +```dart +// Classes: PascalCase +class ConnectionManager { } + +// Variables: camelCase +final connectionId = ConnectionId('123'); + +// Constants: lowerCamelCase +const defaultPort = 3000; + +// Private: prefix with _ +String _privateField; +void _privateMethod() { } +``` + +#### Import Organization + +```dart +// 1. Dart imports +import 'dart:async'; +import 'dart:io'; + +// 2. Package imports +import 'package:logging/logging.dart'; +import 'package:socket_io_common/socket_io_common.dart'; + +// 3. Relative imports +import 'models/packet_models.dart'; +import 'value_objects/connection_id_vo.dart'; +``` + +#### Comments + +```dart +/// Public API documentation (triple-slash) +/// +/// Detailed description of the class/method. +/// +/// Example: +/// ```dart +/// final socket = Socket(...); +/// socket.emit('event', ['data']); +/// ``` +class Socket { + // Implementation comments (double-slash) + void _privateMethod() { + // Explain complex logic here + } +} +``` + +### Type Safety + +#### Always Specify Types + +```dart +// ✅ Good +final String name = 'socket'; +final List rooms = []; +final Map data = {}; + +// ❌ Avoid +var name = 'socket'; // Don't use var +final rooms = []; // Missing type +``` + +#### Avoid Dynamic + +```dart +// ✅ Good - Use specific types +void handleEvent(EventData data) { } +final Map query = {}; + +// ❌ Avoid - Dynamic is not type-safe +void handleEvent(dynamic data) { } +final Map query = {}; // Only if truly needed +``` + +#### Null Safety + +```dart +// ✅ Good - Explicit nullable types +String? optionalValue; +final String requiredValue = optionalValue ?? 'default'; + +// ✅ Good - Late initialization only when necessary +late final String lateValue; + +// ❌ Avoid - Use nullable instead of late when possible +late String? confusing; // Rarely needed +``` + +--- + +## Testing Requirements + +### Test Coverage + +**All new code must have tests** covering: + +1. **Happy paths**: Normal usage scenarios +2. **Edge cases**: Null, empty, boundary values +3. **Error cases**: Invalid inputs, exceptions +4. **Type safety**: Ensure compile-time checking + +### Test Organization + +```dart +group('FeatureName', () { + group('Method/Aspect', () { + test('specific behavior description', () { + // Arrange + final instance = MyClass(); + + // Act + final result = instance.method(); + + // Assert + expect(result, equals(expected)); + }); + }); +}); +``` + +### Running Tests + +```bash +# Run all tests +dart test + +# Run specific test file +dart test test/models/packet_models_test.dart + +# Run with coverage +dart test --coverage=coverage +``` + +### Test Naming + +```dart +// ✅ Good - Descriptive test names +test('creates valid ConnectionId from non-empty string', () { }); +test('throws ArgumentError when ID is empty', () { }); +test('equality works correctly for same values', () { }); + +// ❌ Avoid - Vague test names +test('test1', () { }); +test('works', () { }); +``` + +### Example Test + +```dart +import 'package:test/test.dart'; +import 'package:socket_io/socket_io.dart'; + +void main() { + group('ConnectionId', () { + test('creates valid ConnectionId from non-empty string', () { + final id = ConnectionId('test-123'); + expect(id.value, equals('test-123')); + }); + + test('throws ArgumentError for empty string', () { + expect(() => ConnectionId(''), throwsArgumentError); + }); + + test('equality works correctly', () { + final id1 = ConnectionId('test'); + final id2 = ConnectionId('test'); + final id3 = ConnectionId('other'); + + expect(id1, equals(id2)); + expect(id1, isNot(equals(id3))); + }); + + test('hashCode is consistent', () { + final id1 = ConnectionId('test'); + final id2 = ConnectionId('test'); + + expect(id1.hashCode, equals(id2.hashCode)); + }); + }); +} +``` + +--- + +## Pull Request Process + +### Before Creating a PR + +1. **Sync with upstream**: + ```bash + git fetch upstream + git rebase upstream/main + ``` + +2. **Run all checks**: + ```bash + dart format . + dart analyze + dart test + ``` + +3. **Commit with clear messages**: + ```bash + git commit -m "feat: add binary data detection + + - Implement _containsBinaryData() method + - Add recursive checking for nested structures + - Update tests to cover new functionality" + ``` + +### Commit Message Format + +``` +: + + + +