├── .pubignore ├── example ├── clock │ ├── backend │ │ ├── .dockerignore │ │ ├── bin │ │ │ └── main.dart │ │ ├── Dockerfile │ │ ├── pubspec.yaml │ │ └── lib │ │ │ └── server.dart │ ├── frontend │ │ ├── .dockerignore │ │ ├── web │ │ │ ├── main.dart │ │ │ ├── styles.css │ │ │ └── index.html │ │ ├── Dockerfile │ │ ├── pubspec.yaml │ │ └── lib │ │ │ └── site.dart │ ├── README.md │ └── docker-compose.yml ├── benchmark │ ├── linux │ │ ├── .gitignore │ │ ├── main.cc │ │ ├── runner │ │ │ ├── main.cc │ │ │ ├── my_application.h │ │ │ └── CMakeLists.txt │ │ ├── flutter │ │ │ ├── generated_plugin_registrant.cc │ │ │ ├── generated_plugin_registrant.h │ │ │ ├── generated_plugins.cmake │ │ │ └── CMakeLists.txt │ │ └── my_application.h │ ├── test │ │ └── widget_test.dart │ ├── ios │ │ ├── Flutter │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── AppFrameworkInfo.plist │ │ ├── Runner │ │ │ ├── Runner-Bridging-Header.h │ │ │ ├── Assets.xcassets │ │ │ │ ├── LaunchImage.imageset │ │ │ │ │ ├── LaunchImage.png │ │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ │ ├── README.md │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ │ └── Contents.json │ │ │ ├── AppDelegate.swift │ │ │ ├── Base.lproj │ │ │ │ ├── Main.storyboard │ │ │ │ └── LaunchScreen.storyboard │ │ │ └── Info.plist │ │ ├── Runner.xcodeproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── RunnerTests │ │ │ └── RunnerTests.swift │ │ └── .gitignore │ ├── .firebaserc │ ├── macos │ │ ├── Flutter │ │ │ ├── Flutter-Debug.xcconfig │ │ │ ├── Flutter-Release.xcconfig │ │ │ └── GeneratedPluginRegistrant.swift │ │ ├── Runner │ │ │ ├── Configs │ │ │ │ ├── Debug.xcconfig │ │ │ │ ├── Release.xcconfig │ │ │ │ ├── Warnings.xcconfig │ │ │ │ └── AppInfo.xcconfig │ │ │ ├── Assets.xcassets │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ ├── app_icon_16.png │ │ │ │ │ ├── app_icon_32.png │ │ │ │ │ ├── app_icon_64.png │ │ │ │ │ ├── app_icon_1024.png │ │ │ │ │ ├── app_icon_128.png │ │ │ │ │ ├── app_icon_256.png │ │ │ │ │ ├── app_icon_512.png │ │ │ │ │ └── Contents.json │ │ │ ├── Release.entitlements │ │ │ ├── AppDelegate.swift │ │ │ ├── DebugProfile.entitlements │ │ │ ├── MainFlutterWindow.swift │ │ │ └── Info.plist │ │ ├── .gitignore │ │ ├── Runner.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── Runner.xcodeproj │ │ │ ├── project.xcworkspace │ │ │ │ └── xcshareddata │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── RunnerTests │ │ │ └── RunnerTests.swift │ ├── web │ │ ├── favicon.png │ │ ├── icons │ │ │ ├── Icon-192.png │ │ │ ├── Icon-512.png │ │ │ ├── Icon-maskable-192.png │ │ │ └── Icon-maskable-512.png │ │ ├── manifest.json │ │ └── index.html │ ├── windows │ │ ├── runner │ │ │ ├── resources │ │ │ │ └── app_icon.ico │ │ │ ├── resource.h │ │ │ ├── runner.exe.manifest │ │ │ ├── utils.h │ │ │ ├── flutter_window.h │ │ │ ├── main.cpp │ │ │ ├── CMakeLists.txt │ │ │ ├── utils.cpp │ │ │ ├── flutter_window.cpp │ │ │ ├── Runner.rc │ │ │ └── win32_window.h │ │ ├── flutter │ │ │ ├── generated_plugin_registrant.cc │ │ │ ├── generated_plugin_registrant.h │ │ │ ├── generated_plugins.cmake │ │ │ └── CMakeLists.txt │ │ ├── .gitignore │ │ └── CMakeLists.txt │ ├── firebase.json │ ├── android │ │ ├── app │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ ├── res │ │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── drawable │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── values │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ └── values-night │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── kotlin │ │ │ │ │ │ └── dev │ │ │ │ │ │ │ └── plugfox │ │ │ │ │ │ │ └── spinifybenchmark │ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── debug │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── profile │ │ │ │ │ └── AndroidManifest.xml │ │ │ └── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ ├── .gitignore │ │ ├── build.gradle │ │ └── settings.gradle │ ├── lib │ │ ├── src │ │ │ └── constant.dart │ │ └── main.dart │ ├── pubspec.yaml │ ├── .gitignore │ ├── docker-compose.yml │ ├── .metadata │ └── README.md └── echo │ └── main.dart ├── test ├── smoke │ ├── platform │ │ └── vm_smoke_test.dart │ ├── platform_smoke_test.dart │ └── create_client.dart ├── smoke_test.dart ├── unit_test.dart └── unit │ ├── codecs.dart │ ├── async_test.dart │ ├── spinify_test.mocks.dart │ ├── config_test.dart │ ├── jwt_test.dart │ ├── logs_test.dart │ └── web_socket_fake.dart ├── lib ├── src │ ├── model │ │ ├── platform │ │ │ ├── js.dart │ │ │ └── vm.dart │ │ ├── stream_position.dart │ │ ├── constant.dart │ │ ├── codec.dart │ │ ├── presence_stats.dart │ │ ├── history.dart │ │ ├── subscription_states.dart │ │ ├── states_stream.dart │ │ ├── transport_interface.dart │ │ ├── annotations.dart │ │ ├── client_info.dart │ │ ├── channel_events.dart │ │ └── subscription_config.dart │ ├── protobuf │ │ ├── client.pbenum.dart │ │ └── client.pbserver.dart │ ├── util │ │ ├── list_equals.dart │ │ ├── map_equals.dart │ │ ├── backoff.dart │ │ ├── guarded.dart │ │ ├── event_queue.dart │ │ └── mutex.dart │ ├── web_socket_stub.dart │ └── transport_adapter.dart └── spinify.dart ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yaml ├── workflows │ ├── deploy.yml │ ├── tests-report.yml │ └── checkout.yml └── FUNDING.yml ├── .vscode ├── extensions.json └── settings.json ├── dart_test.yaml ├── tool ├── echo_down.dart └── echo │ ├── go.mod │ ├── index.html │ └── go.sum ├── CHANGELOG.md ├── LICENSE ├── CONTRIBUTING.md ├── pubspec.yaml ├── .gitignore ├── benchmark └── encoding_benchmark.dart └── Makefile /.pubignore: -------------------------------------------------------------------------------- 1 | example/benchmark/ -------------------------------------------------------------------------------- /example/clock/backend/.dockerignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ -------------------------------------------------------------------------------- /example/clock/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ -------------------------------------------------------------------------------- /example/benchmark/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /example/benchmark/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | -------------------------------------------------------------------------------- /test/smoke/platform/vm_smoke_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | -------------------------------------------------------------------------------- /example/benchmark/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/benchmark/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/benchmark/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "spinify-benchmark" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example/benchmark/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/benchmark/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /lib/src/model/platform/js.dart: -------------------------------------------------------------------------------- 1 | /// Whether the current platform is web. 2 | const bool $kIsWeb = true; 3 | -------------------------------------------------------------------------------- /lib/src/model/platform/vm.dart: -------------------------------------------------------------------------------- 1 | /// Whether the current platform is web. 2 | const bool $kIsWeb = false; 3 | -------------------------------------------------------------------------------- /example/benchmark/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/web/favicon.png -------------------------------------------------------------------------------- /example/clock/backend/bin/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:spinify_clock_backend/server.dart'; 2 | 3 | void main() => runServer(); 4 | -------------------------------------------------------------------------------- /example/benchmark/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/benchmark/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/benchmark/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /example/benchmark/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/benchmark/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/benchmark/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /example/benchmark/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build/web", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/smoke/platform_smoke_test.dart: -------------------------------------------------------------------------------- 1 | export 'platform/vm_smoke_test.dart' 2 | // ignore: uri_does_not_exist 3 | if (dart.library.js_interop) 'platform/js_smoke_test.dart'; 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions & Help 4 | url: https://t.me/ru_dart 5 | about: Ask a question about Spinify -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dart-code.dart-code", 4 | "github.vscode-github-actions", 5 | "golang.go", 6 | "kangping.protobuf" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/benchmark/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/clock/frontend/web/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:web/web.dart' as web; 2 | import 'package:spinify_clock_frontend/site.dart'; 3 | 4 | // & webdev serve 5 | void main() => runSite(); 6 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | timeout: 1x 2 | 3 | platforms: 4 | - vm 5 | 6 | file_reporters: 7 | json: reports/tests.json 8 | 9 | tags: 10 | unit: 11 | timeout: 1x 12 | smoke: 13 | timeout: 2x 14 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/clock/README.md: -------------------------------------------------------------------------------- 1 | # Clock example 2 | 3 | ## How to run 4 | 5 | ```bash 6 | docker compose up --build 7 | ``` 8 | 9 | and open [http://localhost:3081](http://localhost:3081) in your browser. 10 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /lib/src/model/stream_position.dart: -------------------------------------------------------------------------------- 1 | import 'package:fixnum/fixnum.dart' as fixnum; 2 | 3 | /// Stream position. 4 | /// {@category Entity} 5 | typedef SpinifyStreamPosition = ({fixnum.Int64 offset, String epoch}); 6 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/spinify/HEAD/example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/benchmark/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/kotlin/dev/plugfox/spinifybenchmark/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.plugfox.spinifybenchmark 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /example/benchmark/linux/runner/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/benchmark/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | 9 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 10 | } 11 | -------------------------------------------------------------------------------- /example/benchmark/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void fl_register_plugins(FlPluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/benchmark/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 6 | -------------------------------------------------------------------------------- /example/benchmark/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - directory: "/" 5 | open-pull-requests-limit: 5 6 | package-ecosystem: "pub" 7 | rebase-strategy: auto 8 | schedule: 9 | interval: "monthly" 10 | timezone: "Etc/GMT" -------------------------------------------------------------------------------- /example/benchmark/lib/src/constant.dart: -------------------------------------------------------------------------------- 1 | /// HMAC key for the JWT token. 2 | const String tokenHmacSecretKey = '80e88856-fe08-4a01-b9fc-73d1d03c2eee'; 3 | 4 | /// Default endpoint for the WebSocket connection. 5 | const String defaultEndpoint = 'ws://localhost:8000/connection/websocket'; 6 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/smoke_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import 'smoke/platform_smoke_test.dart' as platform_smoke_test; 4 | import 'smoke/smoke_test.dart' as smoke_test; 5 | 6 | void main() { 7 | group('Smoke', () { 8 | smoke_test.main(); 9 | platform_smoke_test.main(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/clock/frontend/web/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); 2 | 3 | html, body { 4 | width: 100%; 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | font-family: 'Roboto', sans-serif; 9 | } 10 | 11 | #output { 12 | padding: 20px; 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /example/benchmark/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "[0-9]+.[0-9]+.[0-9]+*" 8 | 9 | jobs: 10 | deploy: 11 | name: "Deploy to Pub.dev" 12 | permissions: 13 | id-token: write # Required for authentication using OIDC 14 | uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 15 | -------------------------------------------------------------------------------- /example/benchmark/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/benchmark/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/benchmark/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/benchmark/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/benchmark/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /lib/src/model/constant.dart: -------------------------------------------------------------------------------- 1 | import 'platform/vm.dart' 2 | // ignore: uri_does_not_exist 3 | if (dart.library.js_interop) 'platform/js.dart'; 4 | 5 | /// Whether the current platform is web. 6 | const bool kIsWeb = $kIsWeb; // identical(0, 0.0); 7 | 8 | /// Maximum integer value. 9 | const int kMaxInt = int.fromEnvironment( 10 | 'SPINIFY_MAX_INT', 11 | defaultValue: 0x20000000000000, // 0x7FFFFFFFFFFFFFFF 12 | ); 13 | -------------------------------------------------------------------------------- /example/benchmark/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/protobuf/client.pbenum.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: client.proto 4 | // 5 | // @dart = 3.3 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names, library_prefixes 9 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 10 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 11 | -------------------------------------------------------------------------------- /lib/src/util/list_equals.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Check if two lists are equal. 4 | @internal 5 | bool listEquals(List? a, List? b) { 6 | if (a == null) return b == null; 7 | if (b == null || a.length != b.length) return false; 8 | if (identical(a, b)) return true; 9 | for (var index = 0; index < a.length; index += 1) { 10 | if (a[index] != b[index]) return false; 11 | } 12 | return true; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/util/map_equals.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Check if two maps are equal. 4 | @internal 5 | bool mapEquals(Map? a, Map? b) { 6 | if (a == null) return b == null; 7 | if (b == null || a.length != b.length) return false; 8 | if (identical(a, b)) return true; 9 | for (final key in a.keys) { 10 | if (!b.containsKey(key) || b[key] != a[key]) return false; 11 | } 12 | return true; 13 | } 14 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/benchmark/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /example/benchmark/linux/runner/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /tool/echo_down.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io' as io; 3 | 4 | /// Exit the server. 5 | void main() => io.HttpClient() 6 | .getUrl(Uri( 7 | scheme: 'http', 8 | host: '127.0.0.1', 9 | port: 8000, 10 | path: 'exit', 11 | )) 12 | .then((request) => request.close()) 13 | .timeout(const Duration(seconds: 15)) 14 | .then((response) => response.statusCode == 200) 15 | .onError((error, stackTrace) => false) 16 | .whenComplete(() => io.exit(0)); 17 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /lib/src/protobuf/client.pbserver.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Generated code. Do not modify. 3 | // source: client.proto 4 | // 5 | // @dart = 3.3 6 | 7 | // ignore_for_file: annotate_overrides, camel_case_types, comment_references 8 | // ignore_for_file: constant_identifier_names 9 | // ignore_for_file: deprecated_member_use_from_same_package, library_prefixes 10 | // ignore_for_file: non_constant_identifier_names, prefer_final_fields 11 | // ignore_for_file: unnecessary_import, unnecessary_this, unused_import 12 | 13 | export 'client.pb.dart'; 14 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/benchmark/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: spinifybenchmark 2 | description: "Spinify Benchmark" 3 | 4 | publish_to: 'none' 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ^3.4.0 10 | flutter: ^3.24.0 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | 16 | # UI 17 | cupertino_icons: ^1.0.8 18 | 19 | # Centrifuge clients 20 | centrifuge: ^0.15.0 21 | spinify: 22 | path: ../../ 23 | 24 | # Utilities 25 | l: ^5.0.0-pre.2 26 | 27 | dev_dependencies: 28 | flutter_test: 29 | sdk: flutter 30 | flutter_lints: ^4.0.0 31 | 32 | flutter: 33 | uses-material-design: true 34 | -------------------------------------------------------------------------------- /lib/src/model/codec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'command.dart'; 4 | import 'reply.dart'; 5 | 6 | /// A codec for encoding and decoding Spinify commands and replies. 7 | abstract interface class SpinifyCodec { 8 | /// The protocol used by the codec. 9 | /// e.g. 'centrifuge-protobuf' 10 | abstract final String protocol; 11 | 12 | /// Decodes a Spinify replies from a list of bytes. 13 | abstract final Converter, Iterable> decoder; 14 | 15 | /// Encodes a Spinify command to a list of bytes. 16 | abstract final Converter> encoder; 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/tests-report.yml: -------------------------------------------------------------------------------- 1 | name: "Tests Report" 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Tests"] # runs after "Tests" workflow 6 | types: 7 | - completed 8 | 9 | permissions: 10 | contents: read 11 | actions: read 12 | checks: write 13 | 14 | jobs: 15 | report: 16 | name: "🚛 Tests report" 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | steps: 20 | - name: Test report 21 | uses: dorny/test-reporter@v1 22 | with: 23 | artifact: test-results 24 | name: Test Report 25 | path: "**/tests.json" 26 | reporter: flutter-json 27 | fail-on-error: false 28 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 2 | 3 | - **CHANGED**: Update protobuf to 4.0.0 4 | 5 | ## 0.2.0 6 | 7 | - Headers emulation 8 | 9 | ## 0.1.0 10 | 11 | - Add benchmark information to README 12 | 13 | ## 0.1.0-pre.2 14 | 15 | - Support of custom HTTP client for WebSocket VM connection 16 | 17 | ## 0.1.0-pre.1 18 | 19 | - Large scale refactoring 20 | - Test coverage increased to 88% 21 | 22 | ## 0.0.4 23 | 24 | - Update `SpinifyState$Disconnected` state and extend with `bool get temporary` 25 | 26 | ## 0.0.3 27 | 28 | - Move benchmark to example folder 29 | 30 | ## 0.0.2 31 | 32 | - Basic functionality implemented 33 | 34 | ## 0.0.1-pre.9 35 | 36 | - **ADDED**: Initial release 37 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = spinifybenchmark 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = dev.plugfox.spinifybenchmark 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2025 dev.plugfox. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/benchmark/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /test/unit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import 'unit/codec_test.dart' as codec_test; 4 | import 'unit/config_test.dart' as config_test; 5 | import 'unit/jwt_test.dart' as jwt_test; 6 | import 'unit/logs_test.dart' as logs_test; 7 | import 'unit/model_test.dart' as model_test; 8 | import 'unit/spinify_test.dart' as spinify_test; 9 | import 'unit/subscription_test.dart' as subscription_test; 10 | import 'unit/util_test.dart' as util_test; 11 | 12 | void main() { 13 | group('Unit', () { 14 | util_test.main(); 15 | model_test.main(); 16 | config_test.main(); 17 | logs_test.main(); 18 | codec_test.main(); 19 | jwt_test.main(); 20 | spinify_test.main(); 21 | subscription_test.main(); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /lib/src/web_socket_stub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'model/annotations.dart'; 6 | import 'model/exception.dart'; 7 | import 'model/transport_interface.dart'; 8 | 9 | /// Stub for WebSocket client. 10 | @unsafe 11 | @internal 12 | @Throws([SpinifyTransportException]) 13 | Future $webSocketConnect({ 14 | required String url, // e.g. 'ws://localhost:8000/connection/websocket' 15 | Map? headers, // e.g. {'Authorization': 'Bearer '} 16 | Iterable? protocols, // e.g. {'centrifuge-protobuf'} 17 | Map? options, // Other options 18 | }) => 19 | throw const SpinifyTransportException( 20 | message: 'WebSocket is not supported at current platform', 21 | ); 22 | -------------------------------------------------------------------------------- /example/clock/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile для веб-приложения Dart (JS) 2 | FROM dart:stable AS frontend_build 3 | 4 | # Установим рабочую директорию 5 | WORKDIR /app 6 | 7 | # Копируем pubspec и pubspec.lock для установки зависимостей 8 | COPY pubspec.* ./ 9 | 10 | # Устанавливаем зависимости 11 | RUN dart pub get 12 | 13 | # Копируем остальной код приложения 14 | COPY . . 15 | 16 | # Сборка JS 17 | #RUN dart compile js -O3 -o build/main.dart.js web/main.dart 18 | RUN dart pub global activate webdev && webdev build --output=build --release 19 | 20 | # Финальный образ для сервиса на NGINX 21 | FROM nginx:alpine 22 | 23 | # Копируем скомпилированные файлы 24 | COPY --from=frontend_build /app/build/web /usr/share/nginx/html 25 | 26 | # Порт для NGINX 27 | EXPOSE 80 28 | 29 | CMD ["nginx", "-g", "daemon off;"] 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: plugfox 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.buymeacoffee.com/plugfox', 'https://boosty.to/plugfox'] 14 | -------------------------------------------------------------------------------- /example/benchmark/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.1.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /example/benchmark/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/benchmark/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/clock/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile для сервера на Dart 2 | FROM dart:stable AS backend_build 3 | 4 | # Установим рабочую директорию 5 | WORKDIR /app 6 | 7 | # Копируем pubspec и pubspec.lock для установки зависимостей 8 | COPY pubspec.* ./ 9 | 10 | # Устанавливаем зависимости 11 | RUN dart pub get 12 | 13 | # Копируем остальной код приложения 14 | COPY . . 15 | 16 | # Компилируем серверное приложение 17 | RUN dart compile exe bin/main.dart -o /server 18 | 19 | # Финальный образ 20 | FROM debian:bullseye-slim 21 | 22 | # Устанавливаем необходимые зависимости 23 | RUN apt-get update && apt-get install -y \ 24 | ca-certificates && \ 25 | rm -rf /var/lib/apt/lists/* 26 | 27 | # Копируем скомпилированный сервер 28 | COPY --from=backend_build /server /usr/local/bin/server 29 | 30 | # Порт для сервера 31 | EXPOSE 8080 32 | 33 | # Команда запуска сервера 34 | CMD ["/usr/local/bin/server"] -------------------------------------------------------------------------------- /test/unit/codecs.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_classes_with_only_static_members 2 | 3 | import 'package:protobuf/protobuf.dart' as pb; 4 | 5 | abstract final class ProtobufCodec { 6 | /// Encode a protobuf message to a list of bytes. 7 | static List encode(pb.GeneratedMessage msg) { 8 | final bytes = msg.writeToBuffer(); 9 | return (pb.CodedBufferWriter() 10 | ..writeInt32NoTag(bytes.lengthInBytes) 11 | ..writeRawBytes(bytes)) 12 | .toBuffer(); 13 | } 14 | 15 | /// Decode a protobuf message from a list of bytes. 16 | static T decode(T msg, List bytes) { 17 | final reader = pb.CodedBufferReader(bytes); 18 | assert(!reader.isAtEnd(), 'No data to read'); 19 | reader.readMessage(msg, pb.ExtensionRegistry.EMPTY); 20 | assert(reader.isAtEnd(), 'Not all data was read'); 21 | return msg; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/benchmark/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /example/clock/frontend/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Spinify: Clock 11 | 12 | 13 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/benchmark/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:l/l.dart'; 5 | import 'package:spinifybenchmark/src/benchmark_app.dart'; 6 | 7 | void main() => _appZone(() async { 8 | runApp(const BenchmarkApp()); 9 | }); 10 | 11 | /// Catch all application errors and logs. 12 | void _appZone(FutureOr Function() fn) => l.capture( 13 | () => runZonedGuarded( 14 | () => fn(), 15 | l.e, 16 | ), 17 | const LogOptions( 18 | handlePrint: true, 19 | messageFormatting: _messageFormatting, 20 | outputInRelease: true, 21 | printColors: true, 22 | ), 23 | ); 24 | 25 | /// Formats the log message. 26 | Object _messageFormatting(LogMessage log) => 27 | '${_timeFormat(log.timestamp)} | ${log.message}'; 28 | 29 | /// Formats the time. 30 | String _timeFormat(DateTime time) => 31 | '${time.hour}:${time.minute.toString().padLeft(2, '0')}'; 32 | -------------------------------------------------------------------------------- /lib/src/util/backoff.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_classes_with_only_static_members 2 | 3 | import 'dart:math' as math; 4 | 5 | import 'package:meta/meta.dart'; 6 | 7 | /// Backoff strategy for reconnection. 8 | @internal 9 | abstract final class Backoff { 10 | /// Randomizer for full jitter technique. 11 | static final math.Random _rnd = math.Random(); 12 | 13 | /// Full jitter technique. 14 | /// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ 15 | /// step - current step in backoff strategy. 16 | /// minDelay - minimum delay in milliseconds. 17 | /// maxDelay - maximum delay in milliseconds. 18 | static Duration nextDelay(int step, int minDelay, int maxDelay) { 19 | if (minDelay >= maxDelay) return Duration(milliseconds: maxDelay); 20 | final val = math.min(maxDelay, minDelay * math.pow(2, step.clamp(0, 31))); 21 | final interval = _rnd.nextInt(val.toInt()); 22 | return Duration(milliseconds: (minDelay + interval).clamp(0, maxDelay)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /lib/src/model/presence_stats.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// {@template presence_stats} 4 | /// Presence stats 5 | /// {@endtemplate} 6 | /// {@category Entity} 7 | @immutable 8 | final class SpinifyPresenceStats { 9 | /// {@macro presence_stats} 10 | const SpinifyPresenceStats({ 11 | required this.channel, 12 | required this.clients, 13 | required this.users, 14 | }); 15 | 16 | /// Channel 17 | final String channel; 18 | 19 | /// Clients count 20 | final int clients; 21 | 22 | /// Users count 23 | final int users; 24 | 25 | @override 26 | int get hashCode => Object.hash( 27 | channel, 28 | clients, 29 | users, 30 | ); 31 | 32 | @override 33 | bool operator ==(Object other) => 34 | identical(this, other) || 35 | other is SpinifyPresenceStats && 36 | channel == other.channel && 37 | clients == other.clients && 38 | users == other.users; 39 | 40 | @override 41 | String toString() => 'SpinifyPresenceStats{channel: $channel}'; 42 | } 43 | -------------------------------------------------------------------------------- /lib/spinify.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'package:fixnum/fixnum.dart' show Int64; 4 | 5 | export 'src/model/channel_event.dart'; 6 | export 'src/model/client_info.dart'; 7 | export 'src/model/codec.dart'; 8 | export 'src/model/codes.dart'; 9 | export 'src/model/command.dart'; 10 | export 'src/model/config.dart'; 11 | export 'src/model/exception.dart'; 12 | export 'src/model/history.dart'; 13 | export 'src/model/jwt.dart'; 14 | export 'src/model/metric.dart'; 15 | export 'src/model/presence_stats.dart'; 16 | export 'src/model/reply.dart'; 17 | export 'src/model/state.dart'; 18 | export 'src/model/states_stream.dart'; 19 | export 'src/model/stream_position.dart'; 20 | export 'src/model/subscription_config.dart'; 21 | export 'src/model/subscription_state.dart'; 22 | export 'src/model/subscription_states.dart'; 23 | export 'src/model/transport_interface.dart'; 24 | export 'src/protobuf/protobuf_codec.dart'; 25 | export 'src/spinify.dart' show Spinify; 26 | export 'src/spinify_interface.dart'; 27 | export 'src/subscription_interface.dart'; 28 | export 'src/transport_adapter.dart'; 29 | -------------------------------------------------------------------------------- /tool/echo/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PlugFox/spinify/echo 2 | 3 | go 1.22 4 | 5 | require github.com/centrifugal/centrifuge v0.32.2 6 | 7 | require ( 8 | github.com/FZambia/eagle v0.1.0 // indirect 9 | github.com/beorn7/perks v1.0.1 // indirect 10 | github.com/centrifugal/protocol v0.12.1 // indirect 11 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 12 | github.com/google/uuid v1.6.0 // indirect 13 | github.com/josharian/intern v1.0.0 // indirect 14 | github.com/mailru/easyjson v0.7.7 // indirect 15 | github.com/prometheus/client_golang v1.19.0 // indirect 16 | github.com/prometheus/client_model v0.5.0 // indirect 17 | github.com/prometheus/common v0.48.0 // indirect 18 | github.com/prometheus/procfs v0.12.0 // indirect 19 | github.com/redis/rueidis v1.0.35 // indirect 20 | github.com/segmentio/asm v1.2.0 // indirect 21 | github.com/segmentio/encoding v0.4.0 // indirect 22 | github.com/valyala/bytebufferpool v1.0.0 // indirect 23 | golang.org/x/sync v0.7.0 // indirect 24 | golang.org/x/sys v0.19.0 // indirect 25 | google.golang.org/protobuf v1.33.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /example/benchmark/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spinifybenchmark", 3 | "short_name": "spinifybenchmark", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Spinify Benchmark", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/benchmark/linux/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} 10 | "main.cc" 11 | "my_application.cc" 12 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 13 | ) 14 | 15 | # Apply the standard set of build settings. This can be removed for applications 16 | # that need different build settings. 17 | apply_standard_settings(${BINARY_NAME}) 18 | 19 | # Add preprocessor definitions for the application ID. 20 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 21 | 22 | # Add dependency libraries. Add any application-specific dependencies here. 23 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 24 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 25 | 26 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 27 | -------------------------------------------------------------------------------- /lib/src/model/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../util/list_equals.dart'; 4 | import 'channel_event.dart'; 5 | import 'stream_position.dart'; 6 | 7 | /// {@template history} 8 | /// History result. 9 | /// {@endtemplate} 10 | /// {@category Reply} 11 | @immutable 12 | final class SpinifyHistory { 13 | /// {@macro history} 14 | const SpinifyHistory({ 15 | required this.publications, 16 | required this.since, 17 | }); 18 | 19 | /// Publications 20 | final List publications; 21 | 22 | /// Offset and epoch of last publication in publications list 23 | final SpinifyStreamPosition since; 24 | 25 | @override 26 | int get hashCode => Object.hashAll([ 27 | since.epoch, 28 | since.offset, 29 | publications, 30 | ]); 31 | 32 | @override 33 | bool operator ==(Object other) => 34 | identical(this, other) || 35 | other is SpinifyHistory && 36 | since == other.since && 37 | listEquals(publications, other.publications); 38 | 39 | @override 40 | String toString() => 'SpinifyHistory{}'; 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matiunin Mikhail 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/src/model/subscription_states.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'subscription_state.dart'; 4 | 5 | /// Stream of Spinify's [SpinifySubscriptionState] changes. 6 | /// {@category State} 7 | /// {@category Client} 8 | /// {@category Subscription} 9 | extension type SpinifySubscriptionStates( 10 | Stream _) implements Stream { 11 | /// Unsubscribed 12 | SpinifySubscriptionStates 13 | unsubscribed() => filter(); 14 | 15 | /// Subscribing 16 | SpinifySubscriptionStates 17 | subscribing() => filter(); 18 | 19 | /// Subscribed 20 | SpinifySubscriptionStates subscribed() => 21 | filter(); 22 | 23 | /// Filtered stream of [SpinifySubscriptionState]. 24 | SpinifySubscriptionStates filter() => 25 | SpinifySubscriptionStates( 26 | transform(StreamTransformer.fromHandlers( 27 | handleData: (data, sink) => switch (data) { 28 | S valid => sink.add(valid), 29 | _ => null, 30 | }, 31 | ))); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/model/states_stream.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'state.dart'; 4 | 5 | /// Stream of Spinify's [SpinifyState] changes. 6 | /// {@category Client} 7 | /// {@category Entity} 8 | final class SpinifyStatesStream extends StreamView { 9 | /// Stream of Spinify's [SpinifyState] changes. 10 | SpinifyStatesStream(super.stream); 11 | 12 | /// Connection has not yet been established, but the WebSocket is trying. 13 | late final Stream disconnected = 14 | whereType(); 15 | 16 | /// Disconnected state 17 | late final Stream connecting = 18 | whereType(); 19 | 20 | /// Connected 21 | late final Stream connected = 22 | whereType(); 23 | 24 | /// Permanently closed 25 | late final Stream closed = 26 | whereType(); 27 | 28 | /// Filtered stream of data of [SpinifyState]. 29 | Stream whereType() => 30 | transform(StreamTransformer.fromHandlers( 31 | handleData: (data, sink) => switch (data) { 32 | T valid => sink.add(valid), 33 | _ => null, 34 | }, 35 | )).asBroadcastStream(); 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/model/transport_interface.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'annotations.dart'; 4 | 5 | /// WebSocket interface. 6 | abstract interface class WebSocket implements Sink> { 7 | /// Stream of incoming messages. 8 | abstract final Stream> stream; 9 | 10 | /// Close code. 11 | /// May be `null` if connection still open. 12 | int? get closeCode; 13 | 14 | /// Close reason. 15 | /// May be `null` if connection still open. 16 | String? get closeReason; 17 | 18 | /// Is connection closed. 19 | /// Returns `true` if connection closed. 20 | /// After connection closed no more messages can be sent or received. 21 | bool get isClosed; 22 | 23 | /// Adds [data] to the sink. 24 | /// Must not be called after a call to [close]. 25 | @unsafe 26 | @override 27 | void add(List data); 28 | 29 | @safe 30 | @override 31 | void close([int? code, String? reason]); 32 | } 33 | 34 | /// Create a Spinify transport 35 | /// (e.g. WebSocket or gRPC with JSON or Protocol Buffers). 36 | typedef SpinifyTransportBuilder = Future Function({ 37 | required String url, // e.g. 'ws://localhost:8000/connection/websocket' 38 | Map? headers, // e.g. {'Authorization': 'Bearer '} 39 | Iterable? protocols, // e.g. {'centrifuge-protobuf'} 40 | }); 41 | -------------------------------------------------------------------------------- /lib/src/model/annotations.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// Method used manually by the user. 4 | @internal 5 | const SpinifyAnnotation interactive = SpinifyAnnotation('interactive'); 6 | 7 | /// Method used by the side effect. 8 | @internal 9 | const SpinifyAnnotation sideEffect = SpinifyAnnotation('sideEffect'); 10 | 11 | /// Method that shouldn't throw an any exception. 12 | @internal 13 | const SpinifyAnnotation safe = SpinifyAnnotation('safe'); 14 | 15 | /// Method that can throw an exception. 16 | @internal 17 | const SpinifyAnnotation unsafe = SpinifyAnnotation('unsafe'); 18 | 19 | /// Annotation for Spinify library. 20 | @internal 21 | @immutable 22 | class SpinifyAnnotation { 23 | @literal 24 | const SpinifyAnnotation( 25 | this.name, { 26 | this.meta = const {}, 27 | }); 28 | 29 | /// Annotation name. 30 | final String name; 31 | 32 | /// Annotation metadata. 33 | final Map meta; 34 | } 35 | 36 | /// Annotation for Spinify library that mark methods as possible to throw 37 | /// exceptions of specified types. 38 | @internal 39 | @immutable 40 | class Throws extends SpinifyAnnotation { 41 | @literal 42 | const Throws(this.exceptions) : super('throws'); 43 | 44 | /// List of exceptions that can be thrown. 45 | final List exceptions; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/model/client_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../util/list_equals.dart'; 4 | 5 | /// {@template client_info} 6 | /// Client information. 7 | /// {@endtemplate} 8 | /// {@category Reply} 9 | /// {@subCategory Channel} 10 | @immutable 11 | final class SpinifyClientInfo { 12 | /// {@macro client_info} 13 | SpinifyClientInfo({ 14 | required this.user, 15 | required this.client, 16 | required this.connectionInfo, 17 | required this.channelInfo, 18 | }); 19 | 20 | /// User 21 | final String user; 22 | 23 | /// Client 24 | final String client; 25 | 26 | /// Connection information 27 | final List? connectionInfo; 28 | 29 | /// Channel information 30 | final List? channelInfo; 31 | 32 | @override 33 | late final int hashCode = Object.hashAll([ 34 | user, 35 | client, 36 | connectionInfo, 37 | channelInfo, 38 | ]); 39 | 40 | @override 41 | bool operator ==(Object other) => 42 | identical(this, other) || 43 | other is SpinifyClientInfo && 44 | user == other.user && 45 | client == other.client && 46 | listEquals(connectionInfo, other.connectionInfo) && 47 | listEquals(channelInfo, other.channelInfo); 48 | 49 | @override 50 | String toString() => 'SpinifyClientInfo{' 51 | 'user: $user, ' 52 | 'client: $client' 53 | '}'; 54 | } 55 | -------------------------------------------------------------------------------- /example/benchmark/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | spinifybenchmark 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Spinify: contribution guide 2 | 3 | ## How to regenerate protobuf files 4 | 5 | Windows: 6 | 7 | ```ps1 8 | $ choco install protoc 9 | $ dart pub global activate protoc_plugin 10 | $ dart pub get 11 | $ protoc --proto_path=lib/src/protobuf --dart_out=lib/src/protobuf lib/src/protobuf/client.proto 12 | $ dart pub global activate pubspec_generator 13 | $ dart pub global run pubspec_generator:generate -o lib/src/model/pubspec.yaml.g.dart 14 | $ dart format -l 80 lib/ test/ 15 | ``` 16 | 17 | Linux: 18 | 19 | ```bash 20 | $ sudo apt update 21 | $ sudo apt install -y protobuf-compiler dart 22 | $ export PATH="$PATH":"$HOME/.pub-cache/bin" 23 | $ dart pub global activate protoc_plugin 24 | $ dart pub get 25 | $ protoc --proto_path=lib/src/protobuf --dart_out=lib/src/protobuf lib/src/protobuf/client.proto 26 | $ dart pub global activate pubspec_generator 27 | $ dart pub global run pubspec_generator:generate -o lib/src/model/pubspec.yaml.g.dart 28 | $ dart format -l 80 lib/ test/ 29 | ``` 30 | 31 | macOS: 32 | 33 | ```zsh 34 | $ brew update 35 | $ brew install protobuf dart 36 | $ export PATH="$PATH":"$HOME/.pub-cache/bin" 37 | $ dart pub global activate protoc_plugin 38 | $ dart pub get 39 | $ protoc --proto_path=lib/src/protobuf --dart_out=lib/src/protobuf lib/src/protobuf/client.proto 40 | $ dart pub global activate pubspec_generator 41 | $ dart pub global run pubspec_generator:generate -o lib/src/model/pubspec.yaml.g.dart 42 | $ dart format -l 80 lib/ test/ 43 | ``` 44 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"spinifybenchmark", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /example/clock/backend/pubspec.yaml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # PROJECT METADATA 3 | # ============================================================================== 4 | 5 | name: spinify_clock_backend 6 | 7 | description: "Spinify Clock: Backend" 8 | 9 | publish_to: 'none' 10 | 11 | version: 0.0.1+1 12 | 13 | homepage: https://github.com/plugfox/spinify 14 | 15 | repository: https://github.com/plugfox/spinify 16 | 17 | issue_tracker: https://github.com/plugfox/spinify/issues 18 | 19 | #funding: 20 | # - 21 | 22 | #topics: 23 | # - 24 | 25 | platforms: 26 | linux: 27 | macos: 28 | windows: 29 | 30 | #screenshots: 31 | # - description: 'Screenshot 1' 32 | # path: screenshot_1.png 33 | 34 | 35 | # ============================================================================== 36 | # PROJECT STRUCTURE 37 | # ============================================================================== 38 | 39 | # https://dart.dev/tools/pub/workspaces 40 | #workspace: 41 | # - ../shared 42 | 43 | #executables: 44 | # - 45 | 46 | 47 | # ============================================================================== 48 | # DEPENDENCIES 49 | # ============================================================================== 50 | 51 | environment: 52 | sdk: '>=3.6.1 <4.0.0' 53 | 54 | dependencies: 55 | # Annotation 56 | l: ^5.0.0 57 | meta: any 58 | 59 | # Centrifugo 60 | spinify: any 61 | 62 | dev_dependencies: 63 | # Linting 64 | lints: ^5.1.1 65 | -------------------------------------------------------------------------------- /example/benchmark/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "dev.plugfox.spinifybenchmark" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "dev.plugfox.spinifybenchmark" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /test/unit/async_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:fake_async/fake_async.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | Timer? _$cacheTimer; 8 | Map _$cache = () { 9 | final cache = {}; 10 | return cache; 11 | }(); 12 | 13 | Uint8List _cached(int key, Uint8List Function() create) => 14 | _$cache.putIfAbsent(key, () { 15 | _$cacheTimer ??= Timer(const Duration(minutes: 5), () { 16 | _$cacheTimer = null; 17 | _$cache.clear(); 18 | }); 19 | return create(); 20 | }); 21 | 22 | void main() => group('Async test', () { 23 | test( 24 | 'Async cache', 25 | () => fakeAsync( 26 | (async) { 27 | expect(_$cache, isEmpty); 28 | expect(_$cacheTimer, isNull); 29 | final value = _cached(1, () => Uint8List.fromList([1, 2, 3])); 30 | expect(value, equals([1, 2, 3])); 31 | expect(_$cache, isNotEmpty); 32 | expect(_$cacheTimer, isNotNull); 33 | async.elapse(const Duration(minutes: 2)); 34 | expect(_$cache, isNotEmpty); 35 | expect(_$cacheTimer, isNotNull); 36 | expect(_cached(1, () => Uint8List.fromList([])), same(value)); 37 | async.elapse(const Duration(minutes: 3)); 38 | expect(_$cache, isEmpty); 39 | expect(_$cacheTimer, isNull); 40 | expect( 41 | _cached(1, () => Uint8List.fromList([])), isNot(same(value))); 42 | }, 43 | ), 44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: spinify 2 | 3 | description: > 4 | Dart client to communicate with Centrifuge and Centrifugo from Dart and Flutter 5 | over WebSockets with Protobuf support. 6 | 7 | version: 0.3.0 8 | 9 | homepage: https://centrifugal.dev 10 | 11 | repository: https://github.com/PlugFox/spinify 12 | 13 | issue_tracker: https://github.com/PlugFox/spinify/issues 14 | 15 | funding: 16 | - https://www.buymeacoffee.com/plugfox 17 | - https://www.patreon.com/plugfox 18 | - https://boosty.to/plugfox 19 | 20 | topics: 21 | - spinify 22 | - centrifugo 23 | - centrifuge 24 | - websocket 25 | - cross-platform 26 | 27 | platforms: 28 | android: 29 | ios: 30 | linux: 31 | macos: 32 | web: 33 | windows: 34 | 35 | #screenshots: 36 | # - description: 'Example of using the library to connect to a Centrifugo server.' 37 | # path: example.png 38 | 39 | environment: 40 | sdk: ">=3.4.0 <4.0.0" 41 | 42 | dependencies: 43 | # Annotations 44 | meta: ^1.9.1 45 | 46 | # Protocol Buffers 47 | protobuf: ^4.0.0 48 | 49 | # Web related libraries 50 | web: ^1.0.0 51 | 52 | # Utilities 53 | crypto: ^3.0.3 54 | fixnum: ^1.1.0 55 | stack_trace: ^1.11.0 56 | 57 | dev_dependencies: 58 | benchmark_harness: ^2.2.2 59 | lints: ^5.0.0 60 | test: ^1.25.8 61 | fake_async: ^1.3.2 62 | mockito: ^5.0.0 63 | # https://github.com/dart-lang/mockito/issues/732 64 | # https://github.com/dart-lang/mockito/pull/738 65 | # https://github.com/dart-lang/mockito/issues/755 66 | #mockito: 67 | # git: 68 | # url: https://github.com/dart-lang/mockito.git 69 | # ref: master 70 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/benchmark/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker Compose configuration file for running Centrifugo benchmark. 2 | # docker compose up -d 3 | # docker compose down 4 | # docker compose logs -f 5 | 6 | services: 7 | centrifugo-benchmark: 8 | container_name: centrifugo-benchmark 9 | image: centrifugo/centrifugo:latest 10 | restart: unless-stopped 11 | command: centrifugo --client.insecure --admin 12 | tty: true 13 | ports: 14 | - 8000:8000 15 | environment: 16 | - "CENTRIFUGO_ADMIN=true" 17 | - "CENTRIFUGO_TOKEN_HMAC_SECRET_KEY=80e88856-fe08-4a01-b9fc-73d1d03c2eee" 18 | - "CENTRIFUGO_ADMIN_PASSWORD=6cec4cc2-960d-4e4a-b650-0cbd4bbf0530" 19 | - "CENTRIFUGO_ADMIN_SECRET=70957aac-555b-4bce-b9b8-53ada3a8029e" 20 | - "CENTRIFUGO_API_KEY=8aba9113-d67a-41c6-818a-27aaaaeb64e7" 21 | - "CENTRIFUGO_ALLOWED_ORIGINS=*" 22 | - "CENTRIFUGO_HEALTH=true" 23 | - "CENTRIFUGO_HISTORY_SIZE=10" 24 | - "CENTRIFUGO_HISTORY_TTL=300s" 25 | - "CENTRIFUGO_FORCE_RECOVERY=true" 26 | - "CENTRIFUGO_ALLOW_PUBLISH_FOR_CLIENT=true" 27 | - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_CLIENT=true" 28 | - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_ANONYMOUS=true" 29 | - "CENTRIFUGO_ALLOW_PUBLISH_FOR_SUBSCRIBER=true" 30 | - "CENTRIFUGO_ALLOW_PUBLISH_FOR_ANONYMOUS=true" 31 | - "CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS=true" 32 | - "CENTRIFUGO_LOG_LEVEL=debug" 33 | healthcheck: 34 | test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8000/health"] 35 | interval: 3s 36 | timeout: 3s 37 | retries: 3 38 | ulimits: 39 | nofile: 40 | soft: 65535 41 | hard: 65535 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don’t commit the following directories created by pub. 2 | .buildlog 3 | .dart_tool/ 4 | .pub/ 5 | build/ 6 | packages 7 | *.packages 8 | .idea/ 9 | doc 10 | 11 | # Or the files created by dart2js. 12 | *.dart.js 13 | *.js_ 14 | *.js.deps 15 | *.js.map 16 | 17 | # Include when developing application packages. 18 | pubspec.lock 19 | coverage* 20 | 21 | # Codegen 22 | *.g.dart 23 | !pubspec.yaml.g.dart 24 | 25 | # Logs 26 | l/ 27 | 28 | # Miscellaneous 29 | *.class 30 | *.log 31 | *.pyc 32 | *.swp 33 | .DS_Store 34 | .atom/ 35 | .buildlog/ 36 | .history 37 | .svn/ 38 | 39 | # IntelliJ related 40 | *.iml 41 | *.ipr 42 | *.iws 43 | 44 | # The .vscode folder contains launch configuration and tasks you configure in 45 | # VS Code which you may wish to be included in version control, so this line 46 | # is commented out by default. 47 | #.vscode/ 48 | 49 | # Flutter/Dart/Pub related 50 | **/doc/api/ 51 | **/ios/Flutter/.last_build_id 52 | .flutter-plugins 53 | .flutter-plugins-dependencies 54 | .packages 55 | .pub-cache/ 56 | /build/ 57 | 58 | # Web related 59 | lib/generated_plugin_registrant.dart 60 | 61 | # Symbolication related 62 | app.*.symbols 63 | 64 | # Obfuscation related 65 | app.*.map.json 66 | 67 | # Android Studio will place build artifacts here 68 | /android/app/debug 69 | /android/app/profile 70 | /android/app/release 71 | 72 | # Pana 73 | log.pana.json 74 | 75 | # Test 76 | coverage/ 77 | .coverage/ 78 | /test/**/*.json 79 | /test/.test_coverage.dart 80 | reports/ 81 | .reports/ 82 | 83 | # Centifuge 84 | centrifugo-config.json 85 | config.json 86 | 87 | # Golang 88 | 89 | # Binaries for programs and plugins 90 | *.exe 91 | *.exe~ 92 | 93 | # Firebase 94 | .firebase/ -------------------------------------------------------------------------------- /example/clock/frontend/pubspec.yaml: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # PROJECT METADATA 3 | # ============================================================================== 4 | 5 | name: spinify_clock_frontend 6 | 7 | description: "Spinify Clock: Frontend" 8 | 9 | publish_to: "none" 10 | 11 | version: 0.0.1+1 12 | 13 | homepage: https://github.com/plugfox/spinify 14 | 15 | repository: https://github.com/plugfox/spinify 16 | 17 | issue_tracker: https://github.com/plugfox/spinify/issues 18 | 19 | #funding: 20 | # - 21 | 22 | #topics: 23 | # - 24 | 25 | platforms: 26 | web: 27 | 28 | #screenshots: 29 | # - description: 'Screenshot 1' 30 | # path: screenshot_1.png 31 | 32 | # ============================================================================== 33 | # PROJECT STRUCTURE 34 | # ============================================================================== 35 | 36 | # https://dart.dev/tools/pub/workspaces 37 | #workspace: 38 | # - ../shared 39 | 40 | #executables: 41 | # - 42 | 43 | # ============================================================================== 44 | # DEPENDENCIES 45 | # ============================================================================== 46 | 47 | environment: 48 | sdk: ">=3.6.1 <4.0.0" 49 | 50 | dependencies: 51 | # Localization 52 | intl: ^0.20.2 53 | 54 | # Annotation 55 | l: ^5.0.0 56 | meta: any 57 | 58 | # Web 59 | web: ^1.1.0 60 | 61 | # Centrifugo 62 | spinify: any 63 | 64 | dev_dependencies: 65 | # Linting 66 | lints: ^5.1.1 67 | 68 | # Testing 69 | test: any 70 | 71 | # Code generation 72 | build_runner: ^2.4.13 73 | build_web_compilers: ^4.1.1 74 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Spinifybenchmark 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | spinifybenchmark 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /benchmark/encoding_benchmark.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:benchmark_harness/benchmark_harness.dart'; 4 | import 'package:protobuf/protobuf.dart' as pb; 5 | import 'package:spinify/src/protobuf/client.pb.dart' as pb; 6 | 7 | void main() { 8 | final command = pb.Command( 9 | send: pb.SendRequest( 10 | data: Uint16List.fromList([for (var i = 0; i < 256; i++) i]), 11 | ), 12 | ); 13 | 14 | final a = _EncdingBenchmark$Concatination(command)..report(); 15 | final b = _EncdingBenchmark$Builder(command)..report(); 16 | 17 | if (a.bytes.length != b.bytes.length) { 18 | throw StateError('Bytes length mismatch'); 19 | } 20 | for (var i = 0; i < a.bytes.length; i++) { 21 | if (a.bytes[i] != b.bytes[i]) { 22 | throw StateError('Bytes mismatch at index $i'); 23 | } 24 | } 25 | } 26 | 27 | class _EncdingBenchmark$Concatination extends BenchmarkBase { 28 | _EncdingBenchmark$Concatination(this.command) 29 | : super('Encoding concatination'); 30 | 31 | final pb.Command command; 32 | 33 | List bytes = Uint8List(0); 34 | 35 | @override 36 | void run() { 37 | final commandData = command.writeToBuffer(); 38 | final length = commandData.lengthInBytes; 39 | final writer = pb.CodedBufferWriter()..writeInt32NoTag(length); 40 | bytes = writer.toBuffer() + commandData; 41 | } 42 | } 43 | 44 | class _EncdingBenchmark$Builder extends BenchmarkBase { 45 | _EncdingBenchmark$Builder(this.command) : super('Encoding builder'); 46 | 47 | final pb.Command command; 48 | 49 | List bytes = Uint8List(0); 50 | 51 | @override 52 | void run() { 53 | final commandData = command.writeToBuffer(); 54 | final length = commandData.lengthInBytes; 55 | final writer = pb.CodedBufferWriter() 56 | ..writeInt32NoTag(length) 57 | ..writeRawBytes(commandData); 58 | bytes = writer.toBuffer(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/benchmark/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "c519ee916eaeb88923e67befb89c0f1dabfa83e6" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 17 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 18 | - platform: android 19 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 20 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 21 | - platform: ios 22 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 23 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 24 | - platform: linux 25 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 26 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 27 | - platform: macos 28 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 29 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 30 | - platform: web 31 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 32 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 33 | - platform: windows 34 | create_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 35 | base_revision: c519ee916eaeb88923e67befb89c0f1dabfa83e6 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /example/echo/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print, implicit_call_tearoffs 2 | 3 | import 'dart:async'; 4 | import 'dart:io' as io; 5 | 6 | import 'package:spinify/spinify.dart'; 7 | 8 | void main(List args) async { 9 | var url = args.firstWhere((a) => a.startsWith('--url='), orElse: () => ''); 10 | if (url.isNotEmpty) url = url.substring(6).trim(); 11 | if (url.isEmpty) url = io.Platform.environment['URL'] ?? ''; 12 | if (url.isEmpty) url = const String.fromEnvironment('URL', defaultValue: ''); 13 | if (url.isEmpty) url = 'ws://localhost:8000/connection/websocket'; 14 | 15 | final httpClient = io.HttpClient( 16 | context: io.SecurityContext( 17 | withTrustedRoots: true, 18 | ), //..setTrustedCertificatesBytes([/* bytes array */]) 19 | ); 20 | 21 | final client = Spinify( 22 | config: SpinifyConfig( 23 | client: (name: 'app', version: '1.0.0'), 24 | timeout: const Duration(seconds: 15), 25 | serverPingDelay: const Duration(seconds: 8), 26 | connectionRetryInterval: ( 27 | min: const Duration(milliseconds: 250), 28 | max: const Duration(seconds: 15), 29 | ), 30 | /* getToken: () async => '', */ 31 | /* getPayload: () async => utf8.encode('Hello, World!'), */ 32 | codec: SpinifyProtobufCodec(), 33 | transportBuilder: SpinifyTransportAdapter.vm( 34 | compression: io.CompressionOptions.compressionDefault, 35 | customClient: httpClient, 36 | userAgent: 'Dart', 37 | ), 38 | logger: (level, event, message, context) => print('[$event] $message'), 39 | ), 40 | ); 41 | 42 | Timer(const Duration(minutes: 1), () async { 43 | await client.close(); 44 | io.exit(0); 45 | }); 46 | 47 | var prev = client.state; 48 | client.states.listen((next) { 49 | print('$prev -> $next'); 50 | prev = next; 51 | }); 52 | 53 | await client.connect(url); 54 | } 55 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /lib/src/util/guarded.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | /// Runs the given [callback] in a zone that catches uncaught errors and 6 | /// forwards them to the returned future. 7 | /// 8 | /// [ignore] is used to ignore the errors and not throw them. 9 | @internal 10 | Future asyncGuarded( 11 | Future Function() callback, { 12 | bool ignore = false, 13 | }) async { 14 | Object? $error; 15 | StackTrace? $stackTrace; 16 | 17 | await runZonedGuarded>( 18 | () async { 19 | try { 20 | await callback(); 21 | } on Object catch (error, stackTrace) { 22 | $error = error; 23 | $stackTrace = stackTrace; 24 | } 25 | }, 26 | (error, stackTrace) { 27 | // This should never be called. 28 | //debugger(); 29 | $error = error; 30 | $stackTrace = stackTrace; 31 | }, 32 | ); 33 | 34 | final error = $error; 35 | if (error == null) return; 36 | if (ignore) return; 37 | Error.throwWithStackTrace(error, $stackTrace ?? StackTrace.empty); 38 | } 39 | 40 | /// Runs the given [callback] in a zone that catches uncaught errors and 41 | /// rethrows them. 42 | /// 43 | /// [ignore] is used to ignore the errors and not throw them. 44 | @internal 45 | void guarded( 46 | void Function() callback, { 47 | bool ignore = false, 48 | }) { 49 | Object? $error; 50 | StackTrace? $stackTrace; 51 | 52 | runZonedGuarded( 53 | () { 54 | try { 55 | callback(); 56 | } on Object catch (error, stackTrace) { 57 | $error = error; 58 | $stackTrace = stackTrace; 59 | } 60 | }, 61 | (error, stackTrace) { 62 | // This should never be called. 63 | //debugger(); 64 | $error = error; 65 | $stackTrace = stackTrace; 66 | }, 67 | ); 68 | 69 | final error = $error; 70 | if (error == null) return; 71 | if (ignore) return; 72 | Error.throwWithStackTrace(error, $stackTrace ?? StackTrace.empty); 73 | } 74 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /test/unit/spinify_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.5 from annotations 2 | // in spinify/test/unit/spinify_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i3; 7 | 8 | import 'package:mockito/mockito.dart' as _i1; 9 | import 'package:spinify/src/model/transport_interface.dart' as _i2; 10 | 11 | // ignore_for_file: type=lint 12 | // ignore_for_file: avoid_redundant_argument_values 13 | // ignore_for_file: avoid_setters_without_getters 14 | // ignore_for_file: comment_references 15 | // ignore_for_file: deprecated_member_use 16 | // ignore_for_file: deprecated_member_use_from_same_package 17 | // ignore_for_file: implementation_imports 18 | // ignore_for_file: invalid_use_of_visible_for_testing_member 19 | // ignore_for_file: must_be_immutable 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | // ignore_for_file: subtype_of_sealed_class 24 | 25 | /// A class which mocks [WebSocket]. 26 | /// 27 | /// See the documentation for Mockito's code generation for more information. 28 | class MockWebSocket extends _i1.Mock implements _i2.WebSocket { 29 | @override 30 | _i3.Stream> get stream => (super.noSuchMethod( 31 | Invocation.getter(#stream), 32 | returnValue: _i3.Stream>.empty(), 33 | returnValueForMissingStub: _i3.Stream>.empty(), 34 | ) as _i3.Stream>); 35 | 36 | @override 37 | bool get isClosed => (super.noSuchMethod( 38 | Invocation.getter(#isClosed), 39 | returnValue: false, 40 | returnValueForMissingStub: false, 41 | ) as bool); 42 | 43 | @override 44 | void add(List? data) => super.noSuchMethod( 45 | Invocation.method(#add, [data]), 46 | returnValueForMissingStub: null, 47 | ); 48 | 49 | @override 50 | void close([int? code, String? reason]) => super.noSuchMethod( 51 | Invocation.method(#close, [code, reason]), 52 | returnValueForMissingStub: null, 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /example/clock/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker Compose configuration file for running Centrifugo powered clock application. 2 | # docker compose build 3 | # docker compose up --build 4 | # docker compose up -d 5 | # docker compose down 6 | # docker compose logs -f 7 | 8 | services: 9 | # Backend service 10 | backend: 11 | container_name: backend 12 | image: clock_backend:latest 13 | restart: unless-stopped 14 | command: /usr/local/bin/server 15 | depends_on: 16 | - centrifugo 17 | ports: 18 | - 3080:8080 19 | networks: 20 | - clock_network 21 | healthcheck: 22 | test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8080/health"] 23 | interval: 3s 24 | timeout: 3s 25 | retries: 3 26 | environment: 27 | - "TZ=UTC" # set timezone to UTC for backend 28 | build: 29 | context: backend 30 | dockerfile: Dockerfile 31 | 32 | # Frontend service 33 | # http://localhost:3081 34 | frontend: 35 | container_name: frontend 36 | image: clock_frontend:latest 37 | restart: unless-stopped 38 | depends_on: 39 | - centrifugo 40 | ports: 41 | - 3081:80 42 | healthcheck: 43 | test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:80"] 44 | interval: 3s 45 | timeout: 3s 46 | retries: 3 47 | build: 48 | context: frontend 49 | dockerfile: Dockerfile 50 | 51 | # Centrifugo service 52 | # docker compose up centrifugo 53 | centrifugo: 54 | container_name: centrifugo 55 | image: centrifugo/centrifugo:latest 56 | restart: unless-stopped 57 | command: centrifugo --client.insecure --health.enabled 58 | tty: true 59 | ports: 60 | - 3082:8000 61 | networks: 62 | - clock_network 63 | healthcheck: 64 | test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8000/health"] 65 | interval: 3s 66 | timeout: 3s 67 | retries: 3 68 | ulimits: 69 | nofile: 70 | soft: 65535 71 | hard: 65535 72 | environment: 73 | - "CENTRIFUGO_CLIENT_ALLOWED_ORIGINS=*" 74 | 75 | networks: 76 | clock_network: 77 | driver: bridge -------------------------------------------------------------------------------- /test/unit/config_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:spinify/spinify.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Config', () { 6 | test('Create', () { 7 | expect(SpinifyConfig.new, returnsNormally); 8 | expect(SpinifyConfig.byDefault, returnsNormally); 9 | expect(SpinifyConfig(), isA()); 10 | expect(SpinifyConfig().toString(), equals('SpinifyConfig{}')); 11 | }); 12 | 13 | test('Fields', () { 14 | final logBuffer = SpinifyLogBuffer(size: 10); 15 | Future transportBuilder({ 16 | required String url, 17 | Map? headers, 18 | Iterable? protocols, 19 | }) => 20 | throw UnimplementedError(); 21 | 22 | final config = SpinifyConfig( 23 | getToken: () => Future.value(''), 24 | getPayload: () => Future>.value([1, 2, 3]), 25 | connectionRetryInterval: ( 26 | min: const Duration(seconds: 1), 27 | max: const Duration(seconds: 2), 28 | ), 29 | client: ( 30 | name: 'name', 31 | version: 'version', 32 | ), 33 | timeout: const Duration(seconds: 15), 34 | serverPingDelay: const Duration(seconds: 8), 35 | headers: const {'key': 'value'}, 36 | logger: logBuffer.add, 37 | transportBuilder: transportBuilder, 38 | ); 39 | expectLater(config.getToken?.call(), completion('')); 40 | expectLater(config.getPayload?.call(), completion([1, 2, 3])); 41 | expect(config.connectionRetryInterval.min, 42 | equals(const Duration(seconds: 1))); 43 | expect(config.connectionRetryInterval.max, 44 | equals(const Duration(seconds: 2))); 45 | expect(config.client.name, equals('name')); 46 | expect(config.client.version, equals('version')); 47 | expect(config.timeout, equals(const Duration(seconds: 15))); 48 | expect(config.serverPingDelay, equals(const Duration(seconds: 8))); 49 | expect(config.headers, equals(const {'key': 'value'})); 50 | expect(config.logger, equals(logBuffer.add)); 51 | expect(config.transportBuilder, equals(transportBuilder)); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/model/channel_events.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'channel_event.dart'; 4 | 5 | /// Stream of received pushes from Centrifugo server for a channels. 6 | /// {@category Event} 7 | /// {@category Client} 8 | /// {@category Subscription} 9 | /// {@subCategory Push} 10 | /// {@subCategory Channel} 11 | extension type SpinifyChannelEvents(Stream _) 12 | implements Stream { 13 | /// Stream of publication events. 14 | SpinifyChannelEvents publication({String? channel}) => 15 | filter(channel: channel); 16 | 17 | /// Stream of presence events. 18 | SpinifyChannelEvents presence({String? channel}) => 19 | filter(channel: channel); 20 | 21 | /// Stream of unsubscribe events. 22 | SpinifyChannelEvents unsubscribe({String? channel}) => 23 | filter(channel: channel); 24 | 25 | /// Stream of message events. 26 | SpinifyChannelEvents message({String? channel}) => 27 | filter(channel: channel); 28 | 29 | /// Stream of subscribe events. 30 | SpinifyChannelEvents subscribe({String? channel}) => 31 | filter(channel: channel); 32 | 33 | /// Stream of connect events. 34 | SpinifyChannelEvents connect({String? channel}) => 35 | filter(channel: channel); 36 | 37 | /// Stream of disconnect events. 38 | SpinifyChannelEvents disconnect({String? channel}) => 39 | filter(channel: channel); 40 | 41 | /// Stream of refresh events. 42 | SpinifyChannelEvents refresh({String? channel}) => 43 | filter(channel: channel); 44 | 45 | /// Filtered stream of [SpinifyChannelEvent]. 46 | SpinifyChannelEvents filter( 47 | {String? channel}) => 48 | SpinifyChannelEvents(transform(StreamTransformer.fromHandlers( 49 | handleData: (data, sink) => switch (data) { 50 | S valid when channel == null || valid.channel == channel => 51 | sink.add(valid), 52 | _ => null, 53 | }, 54 | ))); 55 | } 56 | -------------------------------------------------------------------------------- /example/benchmark/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /example/benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Spinify: benchmark 2 | 3 | ## Build 4 | 5 | JS 6 | 7 | ```bash 8 | flutter build web --release --no-source-maps --pwa-strategy offline-first --web-resources-cdn --base-href / --web-renderer canvaskit 9 | ``` 10 | 11 | WASM 12 | 13 | ```bash 14 | flutter build web --release --no-source-maps --pwa-strategy offline-first --web-resources-cdn --base-href / --wasm 15 | ``` 16 | 17 | ## Serve 18 | 19 | Windows 20 | 21 | ```bash 22 | flutter run -d windows --release 23 | ``` 24 | 25 | JS 26 | 27 | ```bash 28 | flutter run -d chrome --release --web-resources-cdn --web-renderer canvaskit 29 | ``` 30 | 31 | WASM 32 | 33 | ```bash 34 | flutter run -d chrome --release --web-resources-cdn --wasm 35 | ``` 36 | 37 | ## Deploy 38 | 39 | ```bash 40 | firebase deploy --only hosting 41 | ``` 42 | 43 | ## Docker Compose 44 | 45 | Start: `docker-compose up -d` 46 | 47 | Stop: `docker-compose down` 48 | 49 | Logs: `docker-compose logs -f` 50 | 51 | ```yaml 52 | services: 53 | centrifugo-benchmark: 54 | container_name: centrifugo-benchmark 55 | image: centrifugo/centrifugo:latest 56 | restart: unless-stopped 57 | command: centrifugo --client.insecure --admin 58 | tty: true 59 | ports: 60 | - 8000:8000 61 | environment: 62 | - "CENTRIFUGO_ADMIN=true" 63 | - "CENTRIFUGO_TOKEN_HMAC_SECRET_KEY=80e88856-fe08-4a01-b9fc-73d1d03c2eee" 64 | - "CENTRIFUGO_ADMIN_PASSWORD=6cec4cc2-960d-4e4a-b650-0cbd4bbf0530" 65 | - "CENTRIFUGO_ADMIN_SECRET=70957aac-555b-4bce-b9b8-53ada3a8029e" 66 | - "CENTRIFUGO_API_KEY=8aba9113-d67a-41c6-818a-27aaaaeb64e7" 67 | - "CENTRIFUGO_ALLOWED_ORIGINS=*" 68 | - "CENTRIFUGO_HEALTH=true" 69 | - "CENTRIFUGO_HISTORY_SIZE=10" 70 | - "CENTRIFUGO_HISTORY_TTL=300s" 71 | - "CENTRIFUGO_FORCE_RECOVERY=true" 72 | - "CENTRIFUGO_ALLOW_PUBLISH_FOR_CLIENT=true" 73 | - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_CLIENT=true" 74 | - "CENTRIFUGO_ALLOW_SUBSCRIBE_FOR_ANONYMOUS=true" 75 | - "CENTRIFUGO_ALLOW_PUBLISH_FOR_SUBSCRIBER=true" 76 | - "CENTRIFUGO_ALLOW_PUBLISH_FOR_ANONYMOUS=true" 77 | - "CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS=true" 78 | - "CENTRIFUGO_LOG_LEVEL=debug" 79 | healthcheck: 80 | test: ["CMD", "sh", "-c", "wget -nv -O - http://localhost:8000/health"] 81 | interval: 3s 82 | timeout: 3s 83 | retries: 3 84 | ulimits: 85 | nofile: 86 | soft: 65535 87 | hard: 65535 88 | ``` 89 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /test/unit/jwt_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | 3 | import 'package:spinify/spinify.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('JWT', () { 8 | test('Create', () { 9 | expect(() => SpinifyJWT(sub: 'sub'), returnsNormally); 10 | }); 11 | 12 | test('Encode_and_decode', () { 13 | final jwt = SpinifyJWT( 14 | sub: 'sub', 15 | channel: 'channel', 16 | iat: 1234567890, 17 | exp: 1234567890, 18 | iss: 'iss', 19 | aud: 'aud', 20 | jti: 'jti', 21 | expireAt: 1234567890, 22 | b64info: 'b64info', 23 | channels: const [ 24 | 'channel1', 25 | 'channel2', 26 | ], 27 | subs: const {}, 28 | info: const {}, 29 | meta: const {}, 30 | claims: const { 31 | 'key': 'value', 32 | 'a': 1, 33 | }, 34 | ); 35 | expect(jwt, isA()); 36 | expect(jwt.toString(), equals('SpinifyJWT{sub: ${jwt.sub}}')); 37 | final encoded = jwt.encode('secret'); 38 | expect(encoded, isA()); 39 | final decoded = SpinifyJWT.decode(encoded, 'secret'); 40 | expect( 41 | decoded, 42 | isA() 43 | .having((e) => e.sub, 'sub', jwt.sub) 44 | .having((e) => e.channel, 'channel', jwt.channel) 45 | .having((e) => e.iat, 'iat', jwt.iat) 46 | .having((e) => e.exp, 'exp', jwt.exp) 47 | .having((e) => e.iss, 'iss', jwt.iss) 48 | .having((e) => e.aud, 'aud', jwt.aud) 49 | .having((e) => e.jti, 'jti', jwt.jti) 50 | .having((e) => e.expireAt, 'expireAt', jwt.expireAt) 51 | .having((e) => e.b64info, 'b64info', jwt.b64info) 52 | .having((e) => e.channels, 'channels', jwt.channels) 53 | .having((e) => e.subs, 'subs', jwt.subs) 54 | .having( 55 | (e) => e.claims, 56 | 'claims', 57 | allOf( 58 | containsPair('key', 'value'), 59 | containsPair('a', 1), 60 | containsPair('sub', jwt.sub), 61 | containsPair('channel', jwt.channel), 62 | ), 63 | ), 64 | ); 65 | expect(decoded.toJson(), equals(jwt.toJson())); 66 | expect(decoded.toString(), equals(jwt.toString())); 67 | expect(SpinifyJWT(sub: 'sub').toString(), equals('SpinifyJWT{sub: sub}')); 68 | expect(SpinifyJWT().toString(), equals('SpinifyJWT{}')); 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /example/clock/backend/lib/server.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | 6 | import 'package:l/l.dart'; 7 | import 'package:spinify/spinify.dart'; 8 | 9 | void runServer() => l.capture( 10 | () => runZonedGuarded( 11 | () async { 12 | const url = 'ws://centrifugo:8000/connection/websocket'; 13 | final spinify = Spinify.connect( 14 | url, 15 | config: SpinifyConfig( 16 | client: (name: 'Server', version: '1.0.0'), 17 | logger: (level, event, message, context) => l.log( 18 | LogMessage.verbose( 19 | timestamp: DateTime.now(), 20 | level: switch (level) { 21 | 0 => const LogLevel.debug(), 22 | 1 => const LogLevel.vvvv(), 23 | 2 => const LogLevel.vvv(), 24 | 3 => const LogLevel.info(), 25 | 4 => const LogLevel.warning(), 26 | 5 => const LogLevel.error(), 27 | 6 => const LogLevel.shout(), 28 | _ => const LogLevel.info(), 29 | }, 30 | message: message, 31 | context: context, 32 | ), 33 | ), 34 | ), 35 | ); 36 | final subscription = 37 | spinify.newSubscription('clock', subscribe: true); 38 | final encoder = const JsonEncoder().fuse(const Utf8Encoder()); 39 | Timer.periodic(const Duration(seconds: 1), (timer) { 40 | // If not connected - try to reconnect 41 | if (spinify.state.isDisconnected) { 42 | spinify.connect(url); 43 | return; 44 | } 45 | 46 | // If the subscription is not active, do nothing 47 | if (!subscription.state.isSubscribed) return; 48 | 49 | // Publish the current time 50 | final now = DateTime.now(); 51 | subscription.publish( 52 | encoder.convert( 53 | { 54 | 'hour': now.hour, 55 | 'minute': now.minute, 56 | 'second': now.second, 57 | }, 58 | ), 59 | ); 60 | }); 61 | }, 62 | l.e, 63 | ), 64 | LogOptions( 65 | outputInRelease: true, 66 | handlePrint: true, 67 | printColors: false, 68 | output: LogOutput.platform, 69 | overrideOutput: (message) => '[${message.level}] ${message.message}', 70 | messageFormatting: (message) => message, 71 | ), 72 | ); 73 | -------------------------------------------------------------------------------- /lib/src/util/event_queue.dart: -------------------------------------------------------------------------------- 1 | @internal 2 | import 'dart:async'; 3 | import 'dart:collection'; 4 | 5 | import 'package:meta/meta.dart'; 6 | 7 | /// Async callback 8 | typedef EventCallback = FutureOr Function(); 9 | 10 | /// An event queue is a queue of [EventCallback]s that are executed in order. 11 | class EventQueue implements Sink { 12 | /// Creates a new event queue. 13 | EventQueue(); 14 | final Queue<_EventQueueTask> _queue = Queue<_EventQueueTask>(); 15 | Future? _processing; 16 | 17 | /// Returns `true` if the queue is closed. 18 | bool get isClosed => _closed; 19 | bool _closed = false; 20 | 21 | @override 22 | Future add(EventCallback event) { 23 | if (_closed) { 24 | throw StateError('EventQueue is closed'); 25 | } 26 | final task = _EventQueueTask(event); 27 | _queue.add(task); 28 | _start(); 29 | return task.future; 30 | } 31 | 32 | @override 33 | Future close({bool force = false}) async { 34 | if (_closed) return; 35 | _closed = true; 36 | if (force) { 37 | for (final task in _queue) { 38 | task.reject( 39 | StateError('OctopusStateQueue is closed'), 40 | StackTrace.current, 41 | ); 42 | } 43 | _queue.clear(); 44 | } else { 45 | await _processing; 46 | } 47 | } 48 | 49 | Future _start() { 50 | final processing = _processing; 51 | if (processing != null) { 52 | return processing; 53 | } 54 | return _processing = Future.doWhile(() async { 55 | if (_queue.isEmpty) { 56 | _processing = null; 57 | return false; 58 | } 59 | try { 60 | await _queue.removeFirst()(); 61 | } on Object catch (_, __) {/* ignore */} // coverage:ignore-line 62 | return true; 63 | }); 64 | } 65 | } 66 | 67 | class _EventQueueTask { 68 | _EventQueueTask(EventCallback event) 69 | : _fn = event, 70 | _completer = Completer(); 71 | 72 | final EventCallback _fn; 73 | final Completer _completer; 74 | 75 | Future get future => _completer.future; 76 | 77 | /// Execute the task. 78 | Future call() async { 79 | try { 80 | if (_completer.isCompleted) return; 81 | await _fn(); 82 | if (_completer.isCompleted) return; 83 | _completer.complete(); 84 | } on Object catch (error, stackTrace) { 85 | _completer.completeError(error, stackTrace); // coverage:ignore-line 86 | } 87 | } 88 | 89 | /// Reject the task with an error. 90 | void reject(Object error, [StackTrace? stackTrace]) { 91 | if (_completer.isCompleted) return; // coverage:ignore-line 92 | _completer.completeError(error, stackTrace); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tool/echo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Centrifuge Client Example 8 | 9 | 15 | 16 | 17 | 18 |

Example

19 | 20 | 21 |

Status: Disconnected

22 | 23 | 26 | 29 | 30 | 31 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /example/clock/frontend/lib/site.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | 6 | import 'package:l/l.dart'; 7 | import 'package:spinify/spinify.dart'; 8 | import 'package:spinify_clock_frontend/src/clock_layer.dart'; 9 | import 'package:spinify_clock_frontend/src/engine.dart'; 10 | 11 | void runSite() => l.capture( 12 | () => runZonedGuarded( 13 | () async { 14 | final layer = ClockLayer(); 15 | final _ = RenderingEngine.instance 16 | ..addLayer(layer) 17 | ..start(); 18 | 19 | final spinify = Spinify.connect( 20 | 'ws://127.0.0.1:3082/connection/websocket', 21 | config: SpinifyConfig( 22 | client: (name: 'Website', version: '1.0.0'), 23 | logger: (level, event, message, context) => l.log( 24 | LogMessage.verbose( 25 | timestamp: DateTime.now(), 26 | level: switch (level) { 27 | 0 => const LogLevel.debug(), 28 | 1 => const LogLevel.vvvv(), 29 | 2 => const LogLevel.vvv(), 30 | 3 => const LogLevel.info(), 31 | 4 => const LogLevel.warning(), 32 | 5 => const LogLevel.error(), 33 | 6 => const LogLevel.shout(), 34 | _ => const LogLevel.info(), 35 | }, 36 | message: message, 37 | context: context, 38 | ), 39 | ), 40 | ), 41 | ); 42 | final subscription = 43 | spinify.newSubscription('clock', subscribe: true); 44 | final decoder = const Utf8Decoder() 45 | .fuse(const JsonDecoder()) 46 | .cast, Map>(); 47 | subscription.stream.publication().listen( 48 | (event) { 49 | final update = decoder.convert(event.data); 50 | if (update 51 | case { 52 | 'hour': int hour, 53 | 'minute': int minute, 54 | 'second': int second 55 | }) { 56 | layer.setTime( 57 | hour: hour, 58 | minute: minute, 59 | second: second, 60 | ); 61 | } 62 | }, 63 | cancelOnError: false, 64 | ); 65 | l.i('Engine started'); 66 | }, 67 | l.e, 68 | ), 69 | LogOptions( 70 | outputInRelease: true, 71 | handlePrint: true, 72 | printColors: false, 73 | output: LogOutput.platform, 74 | overrideOutput: (message) => '[${message.level}] ${message.message}', 75 | messageFormatting: (message) => message, 76 | ), 77 | ); 78 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[dart]": { 3 | "editor.insertSpaces": true, 4 | "editor.tabSize": 2, 5 | "editor.suggest.snippetsPreventQuickSuggestions": false, 6 | "editor.suggestSelection": "first", 7 | "editor.tabCompletion": "onlySnippets", 8 | "editor.wordBasedSuggestions": "off", 9 | "editor.selectionHighlight": false, 10 | "editor.defaultFormatter": "Dart-Code.dart-code", 11 | "editor.formatOnSave": true, 12 | "editor.formatOnType": true, 13 | "editor.formatOnPaste": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll": "explicit", 16 | "source.organizeImports": "explicit" 17 | }, 18 | "editor.quickSuggestions": { 19 | "comments": "on", 20 | "strings": "on", 21 | "other": "on" 22 | }, 23 | "editor.links": true, 24 | "editor.rulers": [ 25 | 80 26 | ] 27 | }, 28 | "dart.lineLength": 80, 29 | "dart.doNotFormat": [ 30 | "**.g.dart", 31 | "**.gql.dart", 32 | "**.freezed.dart", 33 | "**.config.dart", 34 | "**.mocks.dart", 35 | "**.gen.dart", 36 | "**.pb.dart", 37 | "**.pbenum.dart", 38 | "**.pbjson.dart", 39 | "**/generated/**" 40 | ], 41 | // Flutter Version Manager 42 | //"dart.flutterSdkPath": ".fvm/flutter_sdk", 43 | // Remove .fvm files from search 44 | "search.exclude": { 45 | //"**/.fvm": true, 46 | ".dart_tool": true, 47 | "coverage": true, 48 | "build": true 49 | }, 50 | // Remove from file watching 51 | "files.watcherExclude": { 52 | //"**/.fvm": true, 53 | ".dart_tool": true, 54 | "coverage": true, 55 | "build": true 56 | }, 57 | // Causes the debug view to automatically appear when a breakpoint is hit. This 58 | // setting is global and not configurable per-language. 59 | "debug.openDebug": "openOnDebugBreak", 60 | "explorer.fileNesting.enabled": true, 61 | "explorer.fileNesting.expand": false, 62 | "explorer.fileNesting.patterns": { 63 | "pubspec.yaml": ".flutter-plugins, .packages, .dart_tool, .flutter-plugins-dependencies, .metadata, .packages, pubspec.lock, build.yaml, analysis_options.yaml, all_lint_rules.yaml, dart*.yaml, flutter*.yaml, icons_launcher.yaml", 64 | ".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*", 65 | "readme.*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md", 66 | "*.dart": "$(capture).g.dart, $(capture).freezed.dart, $(capture).config.dart" 67 | } 68 | /* "files.associations": { 69 | "*.drift": "sql" 70 | }, */ 71 | /* "highlight.regexes": { 72 | "(\"@\\s*.+\":\\s{0,1}{},)": { 73 | "filterFileRegex": ".*\\.arb", 74 | "decorations": [ 75 | { 76 | "overviewRulerColor": "#d19a66", 77 | "backgroundColor": "#d19a66", 78 | "color": "#282c34", 79 | "fontWeight": "bold" 80 | } 81 | ] 82 | } 83 | } */ 84 | } -------------------------------------------------------------------------------- /test/unit/logs_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:spinify/spinify.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('Logs', () { 6 | void Function({ 7 | SpinifyLogLevel level, 8 | String event, 9 | String message, 10 | Map context, 11 | }) addFakeTo(SpinifyLogBuffer buffer) => ( 12 | {level = const SpinifyLogLevel.debug(), 13 | event = 'fake', 14 | message = 'Fake', 15 | context = const {}}) => 16 | buffer.add(level, event, message, context); 17 | 18 | test('LogLevel', () { 19 | for (final v in SpinifyLogLevel.values) { 20 | expect(v, isNotNull); 21 | expect(v.isError, v.level >= const SpinifyLogLevel.warning().level); 22 | { 23 | final level = v.map( 24 | debug: () => 0, 25 | transport: () => 1, 26 | config: () => 2, 27 | info: () => 3, 28 | warning: () => 4, 29 | error: () => 5, 30 | critical: () => 6, 31 | ); 32 | expect(level, equals(v.level)); 33 | } 34 | { 35 | final isError = v.maybeMap( 36 | orElse: () => false, 37 | warning: () => true, 38 | error: () => true, 39 | critical: () => true, 40 | ); 41 | expect(isError, equals(v.isError)); 42 | } 43 | { 44 | final isError = v.mapOrNull( 45 | warning: () => true, 46 | error: () => true, 47 | critical: () => true, 48 | ) ?? 49 | false; 50 | expect(isError, equals(v.isError)); 51 | } 52 | } 53 | }); 54 | 55 | test('LogBuffer', () { 56 | final buffer = SpinifyLogBuffer(size: 10); 57 | expect(buffer.logs, isEmpty); 58 | expect(buffer.length, 0); 59 | expect(buffer.size, 10); 60 | expect(buffer.isFull, isFalse); 61 | expect(buffer.isEmpty, isTrue); 62 | final addFake = addFakeTo(buffer); 63 | addFake(); 64 | expect(buffer.logs, hasLength(1)); 65 | expect(buffer.length, 1); 66 | expect(buffer.isFull, isFalse); 67 | expect(buffer.isEmpty, isFalse); 68 | buffer.clear(); 69 | expect(buffer.logs, isEmpty); 70 | expect(buffer.isFull, isFalse); 71 | expect(buffer.isEmpty, isTrue); 72 | for (var i = 0; i < buffer.size; i++) { 73 | addFake(); 74 | } 75 | expect(buffer.logs, hasLength(buffer.size)); 76 | expect(buffer.length, buffer.size); 77 | expect(buffer.isFull, isTrue); 78 | expect(buffer.isEmpty, isFalse); 79 | addFake(); 80 | expect(buffer.logs, hasLength(buffer.size)); 81 | expect(buffer.isFull, isTrue); 82 | expect(buffer.isEmpty, isFalse); 83 | for (var i = 0; i < buffer.size * 2; i++) { 84 | addFake(); 85 | } 86 | expect(buffer.logs, hasLength(buffer.size)); 87 | expect(buffer.length, buffer.size); 88 | expect(buffer.isFull, isTrue); 89 | expect(buffer.isEmpty, isFalse); 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /example/benchmark/linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tool/echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/FZambia/eagle v0.1.0 h1:9gyX6x+xjoIfglgyPTcYm7dvY7FJ93us1QY5De4CyXA= 2 | github.com/FZambia/eagle v0.1.0/go.mod h1:YjGSPVkQTNcVLfzEUQJNgW9ScPR0K4u/Ky0yeFa4oDA= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/centrifugal/centrifuge v0.32.2 h1:iBq2Xx4PMxQtyADhcz2oF6kcXBHRNQatxX8r2mLa7IM= 6 | github.com/centrifugal/centrifuge v0.32.2/go.mod h1:EqdCalAQ1YXtIO92ifTjNwFGQOtWoTpXQlh7MvZAK/E= 7 | github.com/centrifugal/protocol v0.12.1 h1:hGbIl9Y0UbVsESgLcsqgZ7duwEnrZebFUYdu5Opwzgo= 8 | github.com/centrifugal/protocol v0.12.1/go.mod h1:5Z0SuNdXEt83Fkoi34BCyY23p1P8+zQakQS6/BfJHak= 9 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 10 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 14 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 15 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 16 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 17 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= 18 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= 19 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 20 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 21 | github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= 22 | github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= 23 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 24 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 25 | github.com/redis/rueidis v1.0.35 h1:S1q50VYRl8Hg/ekcF5UPZsRXD4GYDLLU2b+oEogycnI= 26 | github.com/redis/rueidis v1.0.35/go.mod h1:bnbkk4+CkXZgDPEbUtSos/o55i4RhFYYesJ4DS2zmq0= 27 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 28 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 29 | github.com/segmentio/encoding v0.4.0 h1:MEBYvRqiUB2nfR2criEXWqwdY6HJOUrCn5hboVOVmy8= 30 | github.com/segmentio/encoding v0.4.0/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= 31 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 32 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 33 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 34 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 35 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 36 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 37 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 38 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 39 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "dev.plugfox" "\0" 93 | VALUE "FileDescription", "spinifybenchmark" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "spinifybenchmark" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2025 dev.plugfox. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "spinifybenchmark.exe" "\0" 98 | VALUE "ProductName", "spinifybenchmark" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /test/smoke/create_client.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:spinify/spinify.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | extension type _SpinifyChannelEventView(SpinifyChannelEvent event) {} 7 | 8 | const String $url = String.fromEnvironment('TEST_URL', 9 | defaultValue: 'ws://localhost:8000/connection/websocket'); 10 | 11 | const bool _enablePrint = 12 | bool.fromEnvironment('TEST_ENABLE_PRINT', defaultValue: false); 13 | 14 | final SpinifyLogBuffer $logBuffer = SpinifyLogBuffer(size: 100); 15 | 16 | final SpinifyConfig $config = SpinifyConfig( 17 | connectionRetryInterval: ( 18 | min: const Duration(milliseconds: 50), 19 | max: const Duration(milliseconds: 150), 20 | ), 21 | serverPingDelay: const Duration(milliseconds: 500), 22 | logger: _logger, 23 | ); 24 | 25 | void _loggerPrint(SpinifyLogLevel level, String event, String message, 26 | Map context) => 27 | print('[$event] $message'); 28 | 29 | SpinifyReply? _$prevPeply; // ignore: unused_local_variable 30 | void _loggerCheckReply(SpinifyLogLevel level, String event, String message, 31 | Map context) { 32 | if (context['reply'] case SpinifyReply reply) { 33 | expect( 34 | reply, 35 | isA() 36 | .having((r) => r.id, 'id', isNonNegative) 37 | .having((r) => r.timestamp, 'timestamp', isA()) 38 | .having((r) => r.type, 'type', isNotEmpty) 39 | .having((r) => r.isResult, 'isResult', 40 | equals(reply is SpinifyReplyResult)) 41 | .having((r) => r.toString(), 'toString()', isNotEmpty), 42 | ); 43 | expect(reply.hashCode, equals(reply.hashCode)); 44 | if (reply is SpinifyPush) { 45 | expect(reply.channel, equals(reply.event.channel)); 46 | } 47 | if (_$prevPeply != null) { 48 | expect(() => reply == _$prevPeply, returnsNormally); 49 | expect(() => reply.compareTo(_$prevPeply!), returnsNormally); 50 | } 51 | _$prevPeply = reply; 52 | } 53 | } 54 | 55 | SpinifyChannelEvent? _$prevEvent; 56 | void _loggerCheckEvents(SpinifyLogLevel level, String event, String message, 57 | Map context) { 58 | if (context['event'] case SpinifyChannelEvent event) { 59 | expect( 60 | event, 61 | isA() 62 | .having((s) => s.channel, 'channel', isNotNull) 63 | .having((s) => s.type, 'type', isNotEmpty) 64 | .having((s) => s.toString(), 'toString()', isNotEmpty) 65 | .having( 66 | (s) => s, 67 | 'equals', 68 | equals(_SpinifyChannelEventView(event)), 69 | ), 70 | ); 71 | expect( 72 | event.mapOrNull( 73 | publication: (e) => e.isPublication, 74 | presence: (e) => e.isPresence, 75 | unsubscribe: (e) => e.isUnsubscribe, 76 | message: (e) => e.isMessage, 77 | subscribe: (e) => e.isSubscribe, 78 | connect: (e) => e.isConnect, 79 | disconnect: (e) => e.isDisconnect, 80 | refresh: (e) => e.isRefresh, 81 | ) ?? 82 | false, 83 | isTrue, 84 | ); 85 | if (_$prevEvent != null) { 86 | expect(event.compareTo(_$prevEvent!), isNonNegative); 87 | } 88 | _$prevEvent = event; 89 | } 90 | } 91 | 92 | void _logger(SpinifyLogLevel level, String event, String message, 93 | Map context) { 94 | final args = [level, event, message, context]; 95 | if (_enablePrint) Function.apply(_loggerPrint, args); 96 | Function.apply($logBuffer.add, args); 97 | Function.apply(_loggerCheckReply, args); 98 | Function.apply(_loggerCheckEvents, args); 99 | } 100 | 101 | ISpinify $createClient() => Spinify( 102 | config: $config, 103 | ); 104 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL :=/bin/bash -e -o pipefail 2 | PWD :=$(shell pwd) 3 | 4 | .DEFAULT_GOAL := all 5 | .PHONY: all 6 | all: ## build pipeline 7 | all: generate format check test 8 | 9 | .PHONY: ci 10 | ci: ## CI build pipeline 11 | ci: all 12 | 13 | .PHONY: precommit 14 | precommit: ## validate the branch before commit 15 | precommit: all 16 | 17 | .PHONY: help 18 | help: 19 | @echo 'Usage: make ... ' 20 | @echo '' 21 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 22 | 23 | .PHONY: format 24 | format: ## Format the code 25 | @dart format -l 80 --fix lib/ test/ 26 | @dart fix --apply . 27 | 28 | .PHONY: get 29 | get: ## Get the dependencies 30 | @dart pub get 31 | 32 | .PHONY: outdated 33 | outdated: get ## Check for outdated dependencies 34 | @dart pub outdated --show-all --dev-dependencies --dependency-overrides --transitive --no-prereleases 35 | 36 | .PHONY: test 37 | test: get ## Run the tests 38 | @dart test --debug --coverage=coverage --platform vm,chrome test/unit_test.dart 39 | 40 | .PHONY: publish-check 41 | publish-check: ## Check the package before publishing 42 | @dart pub publish --dry-run 43 | 44 | .PHONY: deploy-check 45 | deploy-check: publish-check 46 | 47 | .PHONY: publish 48 | publish: generate ## Publish the package 49 | @yes | dart pub publish 50 | 51 | .PHONY: deploy 52 | deploy: publish 53 | 54 | .PHONY: echo-go 55 | echo-go: ## Start the echo server 56 | @cd tool/echo && go run echo.go 57 | 58 | .PHONY: echo-dart 59 | echo-dart: ## Start the echo client 60 | @cd example/echo && dart run main.dart 61 | 62 | .PHONY: echo-up 63 | echo-up: ## Start the echo server 64 | @dart run tool/echo_up.dart 65 | 66 | .PHONY: echo-down 67 | echo-down: ## Stop the echo server 68 | @dart run tool/echo_down.dart 69 | 70 | .PHONY: coverage 71 | coverage: get ## Generate the coverage report 72 | @dart pub global activate coverage 73 | @dart pub global run coverage:test_with_coverage -fb -o coverage -- \ 74 | --platform vm --compiler=kernel --coverage=coverage \ 75 | --reporter=expanded --file-reporter=json:coverage/tests.json \ 76 | --timeout=10m --concurrency=12 --color \ 77 | test/unit_test.dart 78 | # @dart test --concurrency=6 --platform vm --coverage=coverage test/ 79 | # @dart run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib 80 | @mv coverage/lcov.info coverage/lcov.base.info 81 | @lcov -r coverage/lcov.base.info -o coverage/lcov.base.info "lib/src/protobuf/client.*.dart" "lib/**/*.g.dart" 82 | @mv coverage/lcov.base.info coverage/lcov.info 83 | @lcov --list coverage/lcov.info 84 | @genhtml -o coverage coverage/lcov.info 85 | 86 | .PHONY: analyze 87 | analyze: get ## Analyze the code 88 | @dart format --set-exit-if-changed -l 80 -o none lib/ test/ 89 | @dart analyze --fatal-infos --fatal-warnings lib/ test/ 90 | 91 | .PHONY: check 92 | check: analyze publish-check ## Check the code 93 | @dart pub global activate pana 94 | @pana --json --no-warning --line-length 80 > log.pana.json 95 | 96 | .PHONY: pana 97 | pana: check 98 | 99 | .PHONY: generate 100 | generate: get ## Generate the code 101 | @dart pub global activate protoc_plugin 102 | @protoc --proto_path=lib/src/protobuf --dart_out=lib/src/protobuf lib/src/protobuf/client.proto 103 | @dart pub global run pubspec_generator:generate -o lib/src/model/pubspec.yaml.g.dart 104 | @dart format -l 80 lib/src/model/pubspec.yaml.g.dart lib/src/protobuf/ test/ 105 | 106 | .PHONY: gen 107 | gen: generate 108 | 109 | .PHONY: codegen 110 | codegen: generate 111 | 112 | .PHONY: dart-version 113 | dart-version: ## Show the Dart version 114 | @dart --version 115 | @which dart 116 | 117 | .PHONY: diff 118 | diff: ## git diff 119 | $(call print-target) 120 | @git diff --exit-code 121 | @RES=$$(git status --porcelain) ; if [ -n "$$RES" ]; then echo $$RES && exit 1 ; fi 122 | -------------------------------------------------------------------------------- /example/benchmark/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | -------------------------------------------------------------------------------- /lib/src/model/subscription_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | import 'stream_position.dart'; 6 | 7 | /// Token used for subscription. 8 | /// {@category Subscription} 9 | /// {@category Entity} 10 | typedef SpinifySubscriptionToken = String; 11 | 12 | /// Callback to get token for subscription. 13 | /// If method returns null then subscription will be established without token. 14 | /// {@category Subscription} 15 | /// {@category Entity} 16 | typedef SpinifySubscriptionTokenCallback = Future 17 | Function(); 18 | 19 | /// Callback to set subscription payload data. 20 | /// 21 | /// If method returns null then no payload will be sent at subscribe time. 22 | 23 | /// {@category Subscription} 24 | /// {@category Entity} 25 | typedef SpinifySubscribePayloadCallback = Future?> Function(); 26 | 27 | /// {@template subscription_config} 28 | /// Subscription common options 29 | /// 30 | /// There are several common options available when 31 | /// creating Subscription instance: 32 | /// 33 | /// - option to set subscription token and callback to get subscription token 34 | /// upon expiration (see below more details) 35 | /// - option to set subscription data 36 | /// (attached to every subscribe/resubscribe request) 37 | /// - options to tweak resubscribe backoff algorithm 38 | /// - option to start Subscription since known 39 | /// Stream Position (i.e. attempt recovery on first subscribe) 40 | /// - option to ask server to make subscription positioned 41 | /// (if not forced by a server) 42 | /// - option to ask server to make subscription recoverable 43 | /// (if not forced by a server) 44 | /// - option to ask server to push Join/Leave messages 45 | /// (if not forced by a server) 46 | /// {@endtemplate} 47 | /// {@category Subscription} 48 | /// {@category Entity} 49 | @immutable 50 | class SpinifySubscriptionConfig { 51 | /// {@macro subscription_config} 52 | const SpinifySubscriptionConfig({ 53 | this.getToken, 54 | this.getPayload, 55 | this.resubscribeInterval = ( 56 | min: const Duration(milliseconds: 500), 57 | max: const Duration(seconds: 20), 58 | ), 59 | this.since, 60 | this.positioned = false, 61 | this.recoverable = false, 62 | this.joinLeave = false, 63 | this.timeout = const Duration(seconds: 15), 64 | }) : assert( 65 | (recoverable == false && since == null) || 66 | (recoverable == true && since != null), 67 | 'recoverable and since must be set together'); 68 | 69 | /// Create a default config 70 | /// 71 | /// {@macro subscription_config} 72 | @literal 73 | const factory SpinifySubscriptionConfig.byDefault() = 74 | SpinifySubscriptionConfig; 75 | 76 | /// Callback to get token for subscription 77 | /// and get updated token upon expiration. 78 | final SpinifySubscriptionTokenCallback? getToken; 79 | 80 | /// Data to send with subscription request. 81 | /// Subscription `data` is attached to every subscribe/resubscribe request. 82 | final SpinifySubscribePayloadCallback? getPayload; 83 | 84 | /// Resubscribe backoff algorithm 85 | final ({Duration min, Duration max}) resubscribeInterval; 86 | 87 | /// Start Subscription [since] known Stream Position 88 | /// (i.e. attempt recovery on first subscribe) 89 | final SpinifyStreamPosition? since; 90 | 91 | /// Ask server to make subscription [positioned] (if not forced by a server) 92 | final bool positioned; 93 | 94 | /// Ask server to make subscription [recoverable] (if not forced by a server) 95 | final bool recoverable; 96 | 97 | /// Ask server to push Join/Leave messages (if not forced by a server) 98 | final bool joinLeave; 99 | 100 | /// Maximum time to wait for the subscription to be established. 101 | /// If not specified, the timeout will be 15 seconds. 102 | final Duration timeout; 103 | 104 | @override 105 | String toString() => 'SpinifySubscriptionConfig{}'; 106 | } 107 | -------------------------------------------------------------------------------- /example/benchmark/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/benchmark/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/benchmark/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /test/unit/web_socket_fake.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: use_setters_to_change_properties 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:meta/meta.dart'; 6 | import 'package:spinify/spinify.dart'; 7 | import 'package:spinify/src/protobuf/client.pb.dart' as pb; 8 | 9 | import 'codecs.dart'; 10 | 11 | /// Fake WebSocket implementation. 12 | @visibleForTesting 13 | class WebSocket$Fake implements WebSocket { 14 | /// Create a fake WebSocket. 15 | WebSocket$Fake() { 16 | _init(); 17 | } 18 | 19 | void _init() { 20 | _socket?.close(); 21 | // ignore: close_sinks 22 | final controller = _socket = StreamController>(sync: true); 23 | _stream = controller.stream.transform>( 24 | StreamTransformer, List>.fromHandlers( 25 | handleData: _dataHandler, 26 | handleError: _errorHandler, 27 | /* handleDone: _doneHandler, */ 28 | ), 29 | ); 30 | onAdd = _defaultOnAddCallback; 31 | /* onDone = _defaultOnDoneCallback; */ 32 | } 33 | 34 | // Default callbacks to handle connects and disconnects. 35 | void _defaultOnAddCallback(List bytes, Sink> sink) { 36 | final command = ProtobufCodec.decode(pb.Command(), bytes); 37 | Future.delayed(const Duration(milliseconds: 5), () { 38 | if (isClosed) return; // Connection is closed, ignore command processing. 39 | if (command.hasConnect()) { 40 | sink.add( 41 | ProtobufCodec.encode( 42 | pb.Reply( 43 | id: command.id, 44 | connect: pb.ConnectResult( 45 | client: 'fake', 46 | version: '0.0.1', 47 | expires: false, 48 | ttl: null, 49 | data: null, 50 | ping: 600, 51 | pong: false, 52 | session: 'fake', 53 | node: 'fake', 54 | ), 55 | ), 56 | ), 57 | ); 58 | } 59 | }); 60 | } 61 | 62 | /* void _defaultOnDoneCallback() {} */ 63 | 64 | StreamController>? _socket; 65 | 66 | Stream>? _stream; 67 | 68 | @override 69 | Stream> get stream => _stream ?? const Stream>.empty(); 70 | 71 | /// Handle incoming data. 72 | void _dataHandler(List data, EventSink> sink) => 73 | sink.add(data); 74 | 75 | /// Handle incoming error. 76 | void _errorHandler( 77 | Object error, 78 | StackTrace stackTrace, 79 | EventSink> sink, 80 | ) => 81 | sink.addError( 82 | SpinifyTransportException( 83 | message: 'Fake WebSocket error', 84 | error: error, 85 | ), 86 | stackTrace, 87 | ); 88 | 89 | /* /// Handle socket close. 90 | void _doneHandler(EventSink> sink) { 91 | sink.close(); 92 | _isClosed = true; 93 | onDone.call(); 94 | } */ 95 | 96 | @override 97 | int? get closeCode => _closeCode; 98 | int? _closeCode; 99 | 100 | @override 101 | String? get closeReason => _closeReason; 102 | String? _closeReason; 103 | 104 | @override 105 | bool get isClosed => _isClosed; 106 | bool _isClosed = false; 107 | 108 | @override 109 | void add(List bytes) { 110 | onAdd(bytes, _socket!.sink); 111 | } 112 | 113 | /// Add callback to handle sending data and allow to respond with reply. 114 | late void Function(List bytes, Sink> sink) onAdd = 115 | _defaultOnAddCallback; 116 | 117 | /* /// Add callback to handle socket close event. 118 | late void Function() onDone = _defaultOnDoneCallback; */ 119 | 120 | /// Send asynchroniously a reply to the client. 121 | void reply(List bytes) { 122 | _socket?.sink.add(bytes); 123 | } 124 | 125 | @override 126 | void close([int? code, String? reason]) { 127 | _closeCode = code; 128 | _closeReason = reason; 129 | _isClosed = true; 130 | final socket = _socket; 131 | if (socket != null && !socket.isClosed) { 132 | _socket?.close().ignore(); 133 | _socket = null; 134 | } 135 | } 136 | 137 | /// Reset the WebSocket client. 138 | void reset() { 139 | _closeCode = null; 140 | _closeReason = null; 141 | _isClosed = false; 142 | _init(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /.github/workflows/checkout.yml: -------------------------------------------------------------------------------- 1 | name: Checkout 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "main" 8 | - "master" 9 | #- "dev" 10 | #- "develop" 11 | #- "feature/**" 12 | #- "bugfix/**" 13 | #- "hotfix/**" 14 | #- "support/**" 15 | paths: 16 | - "lib/**.dart" 17 | - "test/**.dart" 18 | - "example/**.dart" 19 | - ".github/workflows/*.yml" 20 | - "pubspec.yaml" 21 | - "analysis_options.yaml" 22 | pull_request: 23 | branches: 24 | - "main" 25 | - "master" 26 | - "dev" 27 | - "develop" 28 | - "feature/**" 29 | - "bugfix/**" 30 | - "hotfix/**" 31 | - "support/**" 32 | paths: 33 | - "lib/**.dart" 34 | - "test/**.dart" 35 | - "example/**.dart" 36 | - ".github/workflows/*.yml" 37 | - "pubspec.yaml" 38 | - "analysis_options.yaml" 39 | 40 | permissions: 41 | contents: read 42 | actions: read 43 | checks: write 44 | 45 | concurrency: 46 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 47 | cancel-in-progress: true 48 | 49 | jobs: 50 | checkout: 51 | name: "Checkout" 52 | runs-on: ubuntu-latest 53 | defaults: 54 | run: 55 | working-directory: ./ 56 | container: 57 | image: dart:stable 58 | env: 59 | pub-cache: pub 60 | PUB_CACHE: /github/home/.pub-cache 61 | timeout-minutes: 10 62 | steps: 63 | - name: 🚂 Get latest code 64 | id: checkout 65 | uses: actions/checkout@v4 66 | with: 67 | sparse-checkout: | 68 | .github 69 | pubspec.yaml 70 | lib 71 | test 72 | analysis_options.yaml 73 | README.md 74 | CHANGELOG.md 75 | 76 | - name: 📤 Restore Pub modules 77 | id: cache-pub-restore 78 | uses: actions/cache/restore@v4 79 | with: 80 | path: | 81 | /home/runner/.pub-cache 82 | key: ${{ runner.os }}-pub-${{ env.pub-cache }}-${{ hashFiles('pubspec.yaml') }} 83 | 84 | - name: 👷 Install Dependencies 85 | id: install-dependencies 86 | timeout-minutes: 1 87 | run: | 88 | echo $PUB_CACHE/bin >> $GITHUB_PATH 89 | dart pub get --no-example 90 | 91 | - name: 📥 Save Pub modules 92 | id: cache-pub-save 93 | if: steps.cache-pub-restore.outputs.cache-hit != 'true' 94 | uses: actions/cache/save@v4 95 | with: 96 | path: | 97 | /home/runner/.pub-cache 98 | key: ${{ steps.cache-pub-restore.outputs.cache-primary-key }} 99 | 100 | - name: 🚦 Check code format 101 | id: check-format 102 | timeout-minutes: 1 103 | run: | 104 | find lib test -name "*.dart" ! -name "*.*.dart" -print0 | xargs -0 dart format --set-exit-if-changed --line-length 80 -o none lib/ test/ 105 | 106 | - name: 📈 Check analyzer 107 | id: check-analyzer 108 | timeout-minutes: 1 109 | run: dart analyze --fatal-infos --fatal-warnings lib/ test/ 110 | 111 | - name: 👀 Verify versions 112 | id: verify-versions 113 | timeout-minutes: 1 114 | run: | 115 | test -f pubspec.yaml && test -f lib/src/model/pubspec.yaml.g.dart && test -f CHANGELOG.md 116 | version_pubspec=$(grep '^version:' pubspec.yaml | awk '{print $2}' | sed 's/[^[:print:]]//g') 117 | version_dart=$(grep 'representation: r' lib/src/model/pubspec.yaml.g.dart | awk -F"'" '{print $2}' | sed 's/[^[:print:]]//g') 118 | test -n "$version_pubspec" && test -n "$version_dart" 119 | echo "Version from pubspec.yaml: '$version_pubspec'" 120 | echo "Version from pubspec.yaml.g.dart: '$version_dart'" 121 | echo "$version_pubspec" > /tmp/version_pubspec 122 | echo "$version_dart" > /tmp/version_dart 123 | diff /tmp/version_pubspec /tmp/version_dart 124 | grep -q "# $version_pubspec" CHANGELOG.md || (echo "Version not found in CHANGELOG.md" >&2; exit 1) 125 | 126 | - name: 🧪 Run unit tests 127 | id: run-unit-tests 128 | timeout-minutes: 2 129 | run: | 130 | dart test --color --platform=vm --concurrency=12 \ 131 | --timeout=60s --reporter=github --file-reporter=json:reports/tests.json \ 132 | --coverage=coverage -- test/unit_test.dart 133 | -------------------------------------------------------------------------------- /lib/src/util/mutex.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:meta/meta.dart'; 4 | 5 | /// A request for a mutex lock. 6 | class _Mutex$Request { 7 | /// Creates a new mutex request. 8 | _Mutex$Request._(Completer completer) 9 | : _completer = completer, 10 | future = completer.future; 11 | 12 | /// Creates a new mutex request with a synchronous completer. 13 | factory _Mutex$Request.sync() => _Mutex$Request._(Completer.sync()); 14 | 15 | /// Creates a new mutex request with a asynchronous completer. 16 | //factory _Mutex$Request.async() => _Mutex$Request._(Completer()); 17 | 18 | final Completer _completer; // The completer for the request. 19 | bool get isCompleted => _completer.isCompleted; // Is completed? 20 | bool get isNotCompleted => !_completer.isCompleted; // Is not completed? 21 | 22 | // Releases the lock. 23 | void release() { 24 | final completer = _completer; 25 | if (completer.isCompleted) return; 26 | completer.complete(); 27 | } 28 | 29 | final Future future; // The future for the request. 30 | _Mutex$Request? prev; // The previous request in the chain. 31 | } 32 | 33 | /// A mutual exclusion lock. 34 | @internal 35 | abstract interface class IMutex { 36 | /// The number of locks currently held. 37 | int get locks; 38 | 39 | /// The list of pending locks. 40 | List> get pending; 41 | 42 | /// Protects a callback with the mutex. 43 | Future protect(Future Function() callback); 44 | 45 | /// Locks the mutex. 46 | Future lock(); 47 | 48 | /// Unlocks the mutex. 49 | void unlock(); 50 | 51 | /// Waits for the last lock at the current moment to be released. 52 | /// This method do not add a new lock. 53 | Future wait(); 54 | } 55 | 56 | /// A mutual exclusion lock. 57 | @internal 58 | class MutexImpl implements IMutex { 59 | /// Creates a new mutex. 60 | MutexImpl(); 61 | 62 | _Mutex$Request? _last; // The last requested block 63 | _Mutex$Request? _current; // The first and current running block 64 | int _locks = 0; // The number of locks currently held 65 | 66 | /// The number of locks currently held. 67 | @override 68 | int get locks => _locks; 69 | 70 | /// The list of pending locks. 71 | @override 72 | List> get pending { 73 | final pending = List>.filled( 74 | _locks, 75 | Future.value(), 76 | growable: false, 77 | ); 78 | for (var i = _locks - 1, request = _last; 79 | i >= 0; 80 | i--, request = request?.prev) { 81 | final future = request?.future; 82 | if (future != null) 83 | pending[i] = future; 84 | else 85 | assert(false, 'Invalid lock state'); // coverage:ignore-line 86 | } 87 | return pending; 88 | } 89 | 90 | /// Protects a callback with the mutex. 91 | @override 92 | Future protect(Future Function() callback) async { 93 | await lock(); 94 | try { 95 | return await callback(); 96 | } finally { 97 | unlock(); 98 | } 99 | } 100 | 101 | /// Locks the mutex. 102 | @override 103 | Future lock() async { 104 | _locks++; 105 | final prev = _last; 106 | final current = _last = _Mutex$Request.sync()..prev = prev; 107 | // Wait for the previous lock to be released. 108 | if (prev != null && prev.isNotCompleted) await prev.future; 109 | _current = current..prev = null; // Set the current lock. 110 | } 111 | 112 | /// Unlocks the mutex. 113 | @override 114 | void unlock() { 115 | final current = _current; 116 | if (current == null) return; 117 | _locks--; 118 | _current = null; 119 | current.release(); 120 | } 121 | 122 | @override 123 | Future wait() async { 124 | final last = _last; 125 | if (last != null) await last.future; 126 | } 127 | } 128 | 129 | /// A fake mutex that does nothing. 130 | @internal 131 | class MutexDisabled implements IMutex { 132 | MutexDisabled(); 133 | 134 | static final Future _future = Future.value(); 135 | 136 | @override 137 | int get locks => 0; 138 | 139 | @override 140 | List> get pending => const []; 141 | 142 | @override 143 | Future protect(Future Function() callback) => callback(); 144 | 145 | @override 146 | Future lock() => _future; 147 | 148 | @override 149 | void unlock() {} 150 | 151 | @override 152 | Future wait() => _future; 153 | } 154 | -------------------------------------------------------------------------------- /lib/src/transport_adapter.dart: -------------------------------------------------------------------------------- 1 | import 'model/constant.dart'; 2 | import 'model/transport_interface.dart'; 3 | import 'web_socket_stub.dart' 4 | // ignore: uri_does_not_exist 5 | if (dart.library.js_interop) 'web_socket_js.dart' 6 | // ignore: uri_does_not_exist 7 | if (dart.library.io) 'web_socket_vm.dart'; 8 | 9 | /// Spinify transport adapter. 10 | /// Allow to pass additional options for WebSocket connection. 11 | final class SpinifyTransportAdapter { 12 | SpinifyTransportAdapter._({ 13 | // Timeout for connection. 14 | Duration? timeout, 15 | // Callback called after connection established. 16 | void Function(WebSocket client)? afterConnect, 17 | // Additional list of protocols. 18 | Iterable? protocols, 19 | // Binary type for JS platform (e.g. 'blob' or 'arraybuffer'). 20 | String? binaryType, 21 | // Compression options for VM platform. 22 | Object? /*CompressionOptions*/ compression, 23 | // Custom HTTP client for VM platform. 24 | Object? /*HttpClient*/ customClient, 25 | // User agent for VM platform. 26 | String? userAgent, 27 | }) : _options = { 28 | 'timeout': timeout, 29 | 'afterConnect': afterConnect, 30 | 'protocols': protocols, 31 | if (kIsWeb) 'binaryType': binaryType, // coverage:ignore-line 32 | if (!kIsWeb) 'compression': compression, 33 | if (!kIsWeb) 'customClient': customClient, 34 | if (!kIsWeb) 'userAgent': userAgent, 35 | }; 36 | 37 | /// Common options for VM and JS platforms. 38 | factory SpinifyTransportAdapter.common({ 39 | // Timeout for connection. 40 | Duration? timeout, 41 | // Callback called after connection established. 42 | void Function(WebSocket client)? afterConnect, 43 | // Additional list of protocols. 44 | Iterable? protocols, 45 | }) => 46 | SpinifyTransportAdapter._( 47 | timeout: timeout, 48 | afterConnect: afterConnect, 49 | protocols: protocols, 50 | ); 51 | 52 | /// Options for JS (Browser) platform. 53 | factory SpinifyTransportAdapter.js({ 54 | // Timeout for connection. 55 | Duration? timeout, 56 | // Callback called after connection established. 57 | void Function(WebSocket client)? afterConnect, 58 | // Additional list of protocols. 59 | Iterable? protocols, 60 | // Binary type for JS platform (e.g. 'blob' or 'arraybuffer'). 61 | String? binaryType, 62 | }) => 63 | SpinifyTransportAdapter._( 64 | timeout: timeout, 65 | afterConnect: afterConnect, 66 | protocols: protocols, 67 | binaryType: binaryType, 68 | ); 69 | 70 | /// Options for VM (Mobile, Desktop, Server, Console) platform. 71 | factory SpinifyTransportAdapter.vm({ 72 | // Timeout for connection. 73 | Duration? timeout, 74 | // Callback called after connection established. 75 | void Function(WebSocket client)? afterConnect, 76 | // Additional list of protocols. 77 | Iterable? protocols, 78 | // Compression options for VM platform. 79 | Object? /*CompressionOptions*/ compression, 80 | // Custom HTTP client for VM platform. 81 | Object? /*HttpClient*/ customClient, 82 | // User agent for VM platform. 83 | String? userAgent, 84 | }) => 85 | SpinifyTransportAdapter._( 86 | timeout: timeout, 87 | afterConnect: afterConnect, 88 | protocols: protocols, 89 | compression: compression, 90 | customClient: customClient, 91 | userAgent: userAgent, 92 | ); 93 | 94 | /// Construct WebSocket adapter for VM or JS platform 95 | /// depending on the current compile target. 96 | factory SpinifyTransportAdapter.selector({ 97 | required SpinifyTransportAdapter Function() vm, 98 | required SpinifyTransportAdapter Function() js, 99 | }) => 100 | kIsWeb ? js() : vm(); 101 | 102 | final Map _options; 103 | 104 | /// Create a Spinify transport 105 | /// (e.g. WebSocket or gRPC with JSON or Protocol Buffers). 106 | Future call({ 107 | required String url, // e.g. 'ws://localhost:8000/connection/websocket' 108 | Map? headers, // e.g. {'Authorization': 'Bearer '} 109 | Iterable? protocols, // e.g. {'centrifuge-protobuf'} 110 | }) => 111 | $webSocketConnect( 112 | url: url, 113 | headers: headers, 114 | protocols: protocols, 115 | options: _options, 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /example/benchmark/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(spinifybenchmark LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "spinifybenchmark") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | --------------------------------------------------------------------------------