From 161d66e5d8c862b3048c2d94684d568e2882510d Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Wed, 26 Aug 2026 01:24:59 -0400 Subject: [PATCH 01/11] test: record Convex Dart client contract gate --- .../convex_dart_client_gauntlet_result.md | 103 +++++ tool/convex_client_gauntlet/.gitignore | 3 + tool/convex_client_gauntlet/README.md | 16 + .../analysis_options.yaml | 4 + tool/convex_client_gauntlet/bin/run.dart | 16 + .../fixtures/folders_argument_renamed.json | 31 ++ .../fixtures/folders_function_renamed.json | 31 ++ .../fixtures/folders_list_for_parent.json | 31 ++ .../folders_unsupported_validator.json | 20 + .../lib/contract_gate.dart | 313 +++++++++++++ tool/convex_client_gauntlet/pubspec.lock | 437 ++++++++++++++++++ tool/convex_client_gauntlet/pubspec.yaml | 12 + .../results/contract_gate.json | 64 +++ .../test/contract_gate_test.dart | 24 + 14 files changed, 1105 insertions(+) create mode 100644 docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md create mode 100644 tool/convex_client_gauntlet/.gitignore create mode 100644 tool/convex_client_gauntlet/README.md create mode 100644 tool/convex_client_gauntlet/analysis_options.yaml create mode 100644 tool/convex_client_gauntlet/bin/run.dart create mode 100644 tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json create mode 100644 tool/convex_client_gauntlet/fixtures/folders_function_renamed.json create mode 100644 tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json create mode 100644 tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json create mode 100644 tool/convex_client_gauntlet/lib/contract_gate.dart create mode 100644 tool/convex_client_gauntlet/pubspec.lock create mode 100644 tool/convex_client_gauntlet/pubspec.yaml create mode 100644 tool/convex_client_gauntlet/results/contract_gate.json create mode 100644 tool/convex_client_gauntlet/test/contract_gate_test.dart diff --git a/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md b/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md new file mode 100644 index 00000000..5a5f61f0 --- /dev/null +++ b/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md @@ -0,0 +1,103 @@ +# Convex Dart client gauntlet result + +Status: decision complete + +Base: `origin/icarus-cloud` at +`e59402eedee9035cf14693fbd26fe8b097d6abfa` on 2026-08-26 + +Candidate versions: `dartvex` 0.2.0 and `dartvex_codegen` 0.2.0 + +Decision: keep `convex_flutter` + +## Result + +Dartvex fails the mandatory compile-time contract gate. Icarus therefore keeps +`convex_flutter` and does not run the runtime chaos or profile stages. + +The stable `folders:listForParent` function declares argument validators but +no `returns:` validator. Convex represents that result as `returns: null` in a +function spec. Dartvex 0.2.0 deliberately maps the absent result contract to +`Future`. A caller that reads `result.first.publicId` still passes Dart +analysis, so a server result-field rename is not caught at compile time. + +Dartvex also treats an unknown validator as a warning and generates the +affected field as `dynamic`. Generation exits zero instead of stopping at the +function and field path. + +These are declared losing conditions in the comparison plan. They are not +performance observations and cannot be averaged away. + +| Contract check | Required | Observed | Result | +| --- | --- | --- | --- | +| Function rename | Old method fails analysis | Analysis exits 3 | Pass | +| Argument rename | Old named argument fails analysis | Analysis exits 3 | Pass | +| Result-field rename | Old field access fails analysis | Generated return is `dynamic`; analysis exits 0 | **Fail** | +| Unsupported validator | Generation exits nonzero with a path | Warning with path; generation exits 0 and emits `dynamic` | **Fail** | +| Second generation | No repository diff | No generated-file change | Pass | + +## Reproduce + +The evaluation lives in an isolated Dart package so the rejected candidate is +not added to the Icarus application or its lockfile. + +```bash +cd tool/convex_client_gauntlet +fvm dart pub get +fvm dart run bin/run.dart +fvm dart test +fvm dart analyze +``` + +The committed machine-readable result is +[`tool/convex_client_gauntlet/results/contract_gate.json`](../../tool/convex_client_gauntlet/results/contract_gate.json). +The runner regenerates bindings for the baseline, function-rename, +argument-rename, and unsupported-validator fixtures and compiles the same old +caller after each relevant change. + +## Runtime stage + +Skipped by the comparison's explicit stop rule: + +> If Dartvex generates function names but leaves public results as `dynamic`, +> stop the runtime comparison and record that gap before writing new generator +> code. + +Consequently, this result makes no claim about Dartvex runtime correctness, +latency, memory, reconnect behavior, auth recovery, or platform builds. The +recorded counts are zero because the runtime workload was not started, not +because either client completed it without faults. + +No client abstraction, adapter, application migration, custom generator, +production deployment, local Hive model, `.ica` format, outbox, revision rule, +or server payload changed in this comparison. + +## Refreshed baseline + +Before the gate, the refreshed cloud base passed: + +- `npm ci` (with the existing npm audit report of 4 dependency + vulnerabilities: 2 moderate, 1 high, 1 critical) +- `npx tsc --noEmit` +- `npm run test:convex` (22 tests) +- the six focused Flutter files named by the two handoffs (82 tests) + +After recording the decision, the comparison branch passed: + +- `fvm dart test` in `tool/convex_client_gauntlet` (1 test) +- `fvm dart analyze` in `tool/convex_client_gauntlet` (no issues) +- `npx tsc --noEmit` +- `npm run test:convex` (22 tests) +- `fvm flutter test` (343 tests) +- `fvm flutter analyze --no-fatal-infos` (exit 0 with the same 6 + pre-existing info-level lints) +- `fvm flutter build web --no-tree-shake-icons` + +The exact `fvm flutter build web` command still fails on the refreshed base's +three existing non-constant `IconData` sites in `folder_provider.dart`, +`hive_adapters.g.dart`, and `archive_manifest.dart`. This comparison does not +change those files. Disabling icon tree shaking proves the web target otherwise +compiles; the pre-existing release-build cleanup remains separate work. + +The fallback typed-binding generator remains separate work. It may add explicit +public result validators and preserve `convex_flutter`, but it must not be +folded into this comparison result. diff --git a/tool/convex_client_gauntlet/.gitignore b/tool/convex_client_gauntlet/.gitignore new file mode 100644 index 00000000..52fc8e3e --- /dev/null +++ b/tool/convex_client_gauntlet/.gitignore @@ -0,0 +1,3 @@ +/.dart_tool/ +/lib/_probe_caller.dart +/lib/_probe_generated/ diff --git a/tool/convex_client_gauntlet/README.md b/tool/convex_client_gauntlet/README.md new file mode 100644 index 00000000..988fc5ff --- /dev/null +++ b/tool/convex_client_gauntlet/README.md @@ -0,0 +1,16 @@ +# Convex Dart client contract gate + +This isolated Dart package reproduces the compile-time contract gate declared +for Icarus's Convex client comparison. It pins `dartvex` and +`dartvex_codegen` to 0.2.0 without adding either package to the application. + +Run the evaluation and its regression test from this directory: + +```bash +fvm dart pub get +fvm dart run bin/run.dart +fvm dart test +``` + +The runtime chaos and profile stages are intentionally absent. The handoff +requires them to stop when generated public results remain `dynamic`. diff --git a/tool/convex_client_gauntlet/analysis_options.yaml b/tool/convex_client_gauntlet/analysis_options.yaml new file mode 100644 index 00000000..84879e80 --- /dev/null +++ b/tool/convex_client_gauntlet/analysis_options.yaml @@ -0,0 +1,4 @@ +analyzer: + exclude: + - lib/_probe_caller.dart + - lib/_probe_generated/** diff --git a/tool/convex_client_gauntlet/bin/run.dart b/tool/convex_client_gauntlet/bin/run.dart new file mode 100644 index 00000000..399e637c --- /dev/null +++ b/tool/convex_client_gauntlet/bin/run.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'package:icarus_convex_client_gauntlet/contract_gate.dart'; + +Future main(List args) async { + final outputIndex = args.indexOf('--output'); + final outputPath = outputIndex == -1 || outputIndex + 1 >= args.length + ? null + : args[outputIndex + 1]; + final result = await evaluateContractGate(); + final json = '${result.toPrettyJson()}\n'; + stdout.write(json); + if (outputPath != null) { + File(outputPath).writeAsStringSync(json); + } +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json b/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json new file mode 100644 index 00000000..33a811b3 --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json @@ -0,0 +1,31 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": null, + "identifier": "folders.ts:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json b/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json new file mode 100644 index 00000000..140ae90c --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json @@ -0,0 +1,31 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentFolderPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": null, + "identifier": "folders.ts:listWithinParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json b/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json new file mode 100644 index 00000000..dacb4e14 --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json @@ -0,0 +1,31 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentFolderPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": null, + "identifier": "folders.ts:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json b/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json new file mode 100644 index 00000000..a508757f --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json @@ -0,0 +1,20 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { "type": "object", "value": {} }, + "returns": { + "type": "object", + "value": { + "futureField": { + "fieldType": { "type": "future-validator" }, + "optional": false + } + } + }, + "identifier": "folders.ts:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/lib/contract_gate.dart b/tool/convex_client_gauntlet/lib/contract_gate.dart new file mode 100644 index 00000000..a5d3e616 --- /dev/null +++ b/tool/convex_client_gauntlet/lib/contract_gate.dart @@ -0,0 +1,313 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartvex_codegen/dartvex_codegen.dart'; + +const _dartvexVersion = '0.2.0'; +const _dartvexCodegenVersion = '0.2.0'; + +final class ContractGateResult { + ContractGateResult({required this.report}); + + final Map report; + + String toPrettyJson() => const JsonEncoder.withIndent(' ').convert(report); +} + +Future evaluateContractGate({String? packageRoot}) async { + final root = Directory(packageRoot ?? Directory.current.path).absolute; + final generated = Directory('${root.path}/lib/_probe_generated'); + final caller = File('${root.path}/lib/_probe_caller.dart'); + + Future<_Generation> generate(String fixtureName) async { + final logs = []; + final errors = []; + final exitCode = await runConvexCodegen( + [ + 'generate', + '--spec-file', + '${root.path}/fixtures/$fixtureName', + '--output', + generated.path, + ], + log: logs.add, + errorLog: errors.add, + ); + return _Generation(exitCode: exitCode, logs: logs, errors: errors); + } + + Future<_Analysis> analyzeCaller() async { + final process = await Process.run(Platform.resolvedExecutable, [ + 'analyze', + caller.path, + generated.path, + ], workingDirectory: root.path); + return _Analysis( + exitCode: process.exitCode, + output: '${process.stdout}${process.stderr}'.trim(), + ); + } + + try { + if (generated.existsSync()) { + generated.deleteSync(recursive: true); + } + if (caller.existsSync()) { + caller.deleteSync(); + } + + final baselineGeneration = await generate('folders_list_for_parent.json'); + caller.writeAsStringSync(_oldCaller); + final baselineAnalysis = await analyzeCaller(); + final baselineSource = File( + '${generated.path}/modules/folders.dart', + ).readAsStringSync(); + final resultIsDynamic = baselineSource.contains( + 'Future listForParent', + ); + final firstGeneration = _directorySnapshot(generated); + + final secondGenerationResult = await generate( + 'folders_list_for_parent.json', + ); + final secondGeneration = _directorySnapshot(generated); + final deterministic = + secondGenerationResult.exitCode == 0 && + _snapshotsEqual(firstGeneration, secondGeneration); + + final functionRenameGeneration = await generate( + 'folders_function_renamed.json', + ); + final functionRenameAnalysis = await analyzeCaller(); + + final argumentRenameGeneration = await generate( + 'folders_argument_renamed.json', + ); + final argumentRenameAnalysis = await analyzeCaller(); + + await generate('folders_list_for_parent.json'); + final resultFieldAnalysis = await analyzeCaller(); + + final unsupportedGeneration = await generate( + 'folders_unsupported_validator.json', + ); + final unsupportedSource = File( + '${generated.path}/modules/folders.dart', + ).readAsStringSync(); + + final functionRenameCaught = + functionRenameGeneration.exitCode == 0 && + functionRenameAnalysis.exitCode != 0; + final argumentRenameCaught = + argumentRenameGeneration.exitCode == 0 && + argumentRenameAnalysis.exitCode != 0; + final resultRenameCaught = + !resultIsDynamic && resultFieldAnalysis.exitCode != 0; + final unsupportedRejected = unsupportedGeneration.exitCode != 0; + final baselineCompiles = + baselineGeneration.exitCode == 0 && baselineAnalysis.exitCode == 0; + final gatePassed = + baselineCompiles && + functionRenameCaught && + argumentRenameCaught && + resultRenameCaught && + unsupportedRejected && + deterministic; + + final report = { + 'schemaVersion': 1, + 'evaluation': 'convex_dart_client_contract_gate', + 'baseCommit': _gitBaseCommit(root), + 'adapterCandidate': 'dartvex', + 'sdkVersion': _dartvexVersion, + 'codegenVersion': _dartvexCodegenVersion, + 'platform': _platformName(), + 'dartVersion': Platform.version, + 'fixture': 'folders:listForParent', + 'baselineCompiles': baselineCompiles, + 'checks': >[ + { + 'id': 'function_rename', + 'required': 'old_generated_method_fails_analysis', + 'status': functionRenameCaught ? 'pass' : 'fail', + 'generationExitCode': functionRenameGeneration.exitCode, + 'analysisExitCode': functionRenameAnalysis.exitCode, + }, + { + 'id': 'argument_rename', + 'required': 'old_named_argument_fails_analysis', + 'status': argumentRenameCaught ? 'pass' : 'fail', + 'generationExitCode': argumentRenameGeneration.exitCode, + 'analysisExitCode': argumentRenameAnalysis.exitCode, + }, + { + 'id': 'result_field_rename', + 'required': 'old_result_field_fails_analysis', + 'status': resultRenameCaught ? 'pass' : 'fail', + 'analysisExitCode': resultFieldAnalysis.exitCode, + 'generatedReturnType': resultIsDynamic ? 'dynamic' : 'typed', + 'detail': resultIsDynamic + ? 'The unvalidated Convex result is absent from function-spec and the old dynamic field access still analyzes.' + : 'The generated result is typed.', + }, + { + 'id': 'unsupported_validator', + 'required': 'generation_stops_with_function_and_field_path', + 'status': unsupportedRejected ? 'pass' : 'fail', + 'generationExitCode': unsupportedGeneration.exitCode, + 'generatedFieldType': + unsupportedSource.contains('dynamic futureField') + ? 'dynamic' + : 'not_dynamic', + 'diagnostics': _sanitizeDiagnostics([ + ...unsupportedGeneration.logs, + ...unsupportedGeneration.errors, + ], root), + }, + { + 'id': 'deterministic_regeneration', + 'required': 'second_generation_has_no_diff', + 'status': deterministic ? 'pass' : 'fail', + 'changedFiles': _changedFiles(firstGeneration, secondGeneration), + }, + ], + 'gatePassed': gatePassed, + 'decision': gatePassed + ? 'continue_runtime_gauntlet' + : 'keep_convex_flutter', + 'runtimeGauntlet': { + 'status': gatePassed ? 'pending' : 'skipped', + 'reason': gatePassed ? null : 'compile_time_contract_gate_failed', + 'correctnessSeedsRun': 0, + 'operationsRun': 0, + 'pairedProfileRuns': 0, + 'p95RemoteConvergenceMs': null, + 'peakMemoryBytes': null, + }, + }; + return ContractGateResult(report: report); + } finally { + if (generated.existsSync()) { + generated.deleteSync(recursive: true); + } + if (caller.existsSync()) { + caller.deleteSync(); + } + } +} + +String _gitBaseCommit(Directory root) { + final result = Process.runSync('git', [ + 'merge-base', + 'HEAD', + 'origin/icarus-cloud', + ], workingDirectory: root.path); + if (result.exitCode != 0) { + throw StateError('Unable to resolve the Icarus base commit.'); + } + return result.stdout.toString().trim(); +} + +String _platformName() { + final match = RegExp(r'on "([^"]+)"').firstMatch(Platform.version); + final rawArchitecture = match?.group(1) ?? 'unknown'; + final architecture = rawArchitecture.replaceFirst( + '${Platform.operatingSystem}_', + '', + ); + return '${Platform.operatingSystem}-$architecture'; +} + +List _sanitizeDiagnostics(List diagnostics, Directory root) => + diagnostics + .map((message) => message.replaceAll(root.path, '')) + .toList(growable: false); + +Map> _directorySnapshot(Directory directory) { + final snapshot = >{}; + final files = + directory + .listSync(recursive: true) + .whereType() + .toList(growable: false) + ..sort((left, right) => left.path.compareTo(right.path)); + for (final file in files) { + final relative = file.path.substring(directory.path.length + 1); + snapshot[relative] = file.readAsBytesSync(); + } + return snapshot; +} + +bool _snapshotsEqual( + Map> left, + Map> right, +) { + if (left.length != right.length) { + return false; + } + for (final entry in left.entries) { + final other = right[entry.key]; + if (other == null || !_bytesEqual(entry.value, other)) { + return false; + } + } + return true; +} + +List _changedFiles( + Map> left, + Map> right, +) { + final paths = {...left.keys, ...right.keys}.toList()..sort(); + return paths + .where((path) { + final leftBytes = left[path]; + final rightBytes = right[path]; + return leftBytes == null || + rightBytes == null || + !_bytesEqual(leftBytes, rightBytes); + }) + .toList(growable: false); +} + +bool _bytesEqual(List left, List right) { + if (left.length != right.length) { + return false; + } + for (var index = 0; index < left.length; index += 1) { + if (left[index] != right[index]) { + return false; + } + } + return true; +} + +final class _Generation { + const _Generation({ + required this.exitCode, + required this.logs, + required this.errors, + }); + + final int exitCode; + final List logs; + final List errors; +} + +final class _Analysis { + const _Analysis({required this.exitCode, required this.output}); + + final int exitCode; + final String output; +} + +const _oldCaller = ''' +import '_probe_generated/api.dart'; + +Future readFirstFolderPublicId(ConvexApi api) async { + final result = await api.folders.listForParent( + parentFolderPublicId: const Optional.of('parent-folder'), + ); + return result.first.publicId as String; +} +'''; diff --git a/tool/convex_client_gauntlet/pubspec.lock b/tool/convex_client_gauntlet/pubspec.lock new file mode 100644 index 00000000..a1f80238 --- /dev/null +++ b/tool/convex_client_gauntlet/pubspec.lock @@ -0,0 +1,437 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d + url: "https://pub.dev" + source: hosted + version: "91.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + url: "https://pub.dev" + source: hosted + version: "8.4.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + dartvex: + dependency: "direct main" + description: + name: dartvex + sha256: "7a343c5853f25a1a136051d2d37002a0e1e3f6c230b6f24560797880de33b5d8" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + dartvex_codegen: + dependency: "direct main" + description: + name: dartvex_codegen + sha256: "07f81e4b16460eeb58673f6e514911df92ba5b38513d9207a4211a92646512a7" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + meta: + dependency: transitive + description: + name: meta + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + url: "https://pub.dev" + source: hosted + version: "1.19.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "14c2945847669b44089bb1222f66873d7ff7103c58911917f2a63c5a62327898" + url: "https://pub.dev" + source: hosted + version: "0.10.14" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + url: "https://pub.dev" + source: hosted + version: "0.6.12" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/tool/convex_client_gauntlet/pubspec.yaml b/tool/convex_client_gauntlet/pubspec.yaml new file mode 100644 index 00000000..58e43ece --- /dev/null +++ b/tool/convex_client_gauntlet/pubspec.yaml @@ -0,0 +1,12 @@ +name: icarus_convex_client_gauntlet +publish_to: none + +environment: + sdk: ">=3.11.0 <4.0.0" + +dependencies: + dartvex: 0.2.0 + dartvex_codegen: 0.2.0 + +dev_dependencies: + test: 1.26.3 diff --git a/tool/convex_client_gauntlet/results/contract_gate.json b/tool/convex_client_gauntlet/results/contract_gate.json new file mode 100644 index 00000000..cccaf265 --- /dev/null +++ b/tool/convex_client_gauntlet/results/contract_gate.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "evaluation": "convex_dart_client_contract_gate", + "baseCommit": "e59402eedee9035cf14693fbd26fe8b097d6abfa", + "adapterCandidate": "dartvex", + "sdkVersion": "0.2.0", + "codegenVersion": "0.2.0", + "platform": "macos-arm64", + "dartVersion": "3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\"", + "fixture": "folders:listForParent", + "baselineCompiles": true, + "checks": [ + { + "id": "function_rename", + "required": "old_generated_method_fails_analysis", + "status": "pass", + "generationExitCode": 0, + "analysisExitCode": 3 + }, + { + "id": "argument_rename", + "required": "old_named_argument_fails_analysis", + "status": "pass", + "generationExitCode": 0, + "analysisExitCode": 3 + }, + { + "id": "result_field_rename", + "required": "old_result_field_fails_analysis", + "status": "fail", + "analysisExitCode": 0, + "generatedReturnType": "dynamic", + "detail": "The unvalidated Convex result is absent from function-spec and the old dynamic field access still analyzes." + }, + { + "id": "unsupported_validator", + "required": "generation_stops_with_function_and_field_path", + "status": "fail", + "generationExitCode": 0, + "generatedFieldType": "dynamic", + "diagnostics": [ + "Generated 4 files in /lib/_probe_generated", + "Warning: folders.ts:listForParent → returns → field \"futureField\": Unknown Convex type \"future-validator\"; generated as dynamic." + ] + }, + { + "id": "deterministic_regeneration", + "required": "second_generation_has_no_diff", + "status": "pass", + "changedFiles": [] + } + ], + "gatePassed": false, + "decision": "keep_convex_flutter", + "runtimeGauntlet": { + "status": "skipped", + "reason": "compile_time_contract_gate_failed", + "correctnessSeedsRun": 0, + "operationsRun": 0, + "pairedProfileRuns": 0, + "p95RemoteConvergenceMs": null, + "peakMemoryBytes": null + } +} diff --git a/tool/convex_client_gauntlet/test/contract_gate_test.dart b/tool/convex_client_gauntlet/test/contract_gate_test.dart new file mode 100644 index 00000000..d01571cd --- /dev/null +++ b/tool/convex_client_gauntlet/test/contract_gate_test.dart @@ -0,0 +1,24 @@ +import 'package:icarus_convex_client_gauntlet/contract_gate.dart'; +import 'package:test/test.dart'; + +void main() { + test('Dartvex 0.2.0 fails the declared Icarus contract gate', () async { + final result = await evaluateContractGate(); + final report = result.report; + final checks = (report['checks']! as List) + .cast>(); + + Map check(String id) => + checks.singleWhere((item) => item['id'] == id); + + expect(check('function_rename')['status'], 'pass'); + expect(check('argument_rename')['status'], 'pass'); + expect(check('result_field_rename')['status'], 'fail'); + expect(check('result_field_rename')['generatedReturnType'], 'dynamic'); + expect(check('unsupported_validator')['status'], 'fail'); + expect(check('unsupported_validator')['generationExitCode'], 0); + expect(check('deterministic_regeneration')['status'], 'pass'); + expect(report['gatePassed'], isFalse); + expect(report['decision'], 'keep_convex_flutter'); + }); +} From 3aad23a6b47e96a4b412ec7fb8facf0c8a331eb9 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Wed, 26 Aug 2026 01:30:57 -0400 Subject: [PATCH 02/11] ci: validate Convex client contract gate --- .github/workflows/ci.yml | 8 ++++++++ analysis_options.yaml | 1 + 2 files changed, 9 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b13ba136..1054bb0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,14 @@ jobs: shell: pwsh run: fvm flutter pub get + - name: Validate Convex Client Contract Gate + shell: pwsh + working-directory: tool/convex_client_gauntlet + run: | + fvm dart pub get + fvm dart test + fvm dart analyze + - name: Analyze shell: pwsh run: fvm flutter analyze --no-fatal-infos diff --git a/analysis_options.yaml b/analysis_options.yaml index 12180237..f3c276bd 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,7 @@ analyzer: exclude: - build/** - lib/hive/hive_adapters.g.dart + - tool/convex_client_gauntlet/** errors: curly_braces_in_flow_control_structures: ignore include: package:flutter_lints/flutter.yaml From df9f1934bbc7ab71e144208036f385b8f73c77aa Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Wed, 26 Aug 2026 01:48:57 -0400 Subject: [PATCH 03/11] ci: make contract gate checkout-safe --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1054bb0f..34b56691 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 + with: + # The contract gate records the merge base with origin/icarus-cloud. + # Fetch all branch history so that ref exists on PR and push runs. + fetch-depth: 0 - uses: actions/setup-node@v4 with: @@ -73,7 +77,9 @@ jobs: working-directory: tool/convex_client_gauntlet run: | fvm dart pub get + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } fvm dart test + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } fvm dart analyze - name: Analyze From ca6791c5efaa88846223d94098532eef3b6d376f Mon Sep 17 00:00:00 2001 From: Dara Adedeji <76637177+SunkenInTime@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:19:21 -0400 Subject: [PATCH 04/11] docs: hand off fair Convex Dart client rerun --- ...convex_dart_client_fair_rerun_handoff.html | 767 ++++++++++++++++++ .../convex_dart_client_gauntlet_result.md | 35 +- 2 files changed, 793 insertions(+), 9 deletions(-) create mode 100644 docs/cloud_sync_refactor/convex_dart_client_fair_rerun_handoff.html diff --git a/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_handoff.html b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_handoff.html new file mode 100644 index 00000000..8e1ee822 --- /dev/null +++ b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_handoff.html @@ -0,0 +1,767 @@ + + + + + + + + + Rerun the Dart client gauntlet with the missing control + + + + + + + +
+
+
+ + HANDOFF + +
+
+
+ + + +
+ + +
+
+

Icarus cloud · Ready to rerun · Decision reopened

+

Rerun the Dart client gauntlet with the missing control

+

The first gate found a real Dartvex strictness gap, but it did not settle Dartvex versus convex_flutter. Its result-field leg regenerated the same fixture, whose return contract was null, instead of testing an actual result rename. A diagnostic control with an explicit return schema did catch the rename at analysis time. The fair next step is to make the server contract explicit, add a thin fail-closed Icarus wrapper, and then run the same runtime faults against both clients.

+ +
+

Decision now

+ +
+
1missing result mutation
+
0runtime seeds completed
+
3analyzer exit on real rename
+
50rerun seeds required
+
+ +

No client winner yet. Keep the first gate as evidence that Dartvex 0.2.0 is not fail-closed by default. Reopen the runtime decision because neither client has completed the symmetric chaos test.

+ +

The goal is still less JSON plumbing and stronger generated Dart APIs. The first run shows that Dartvex can supply useful function and argument types, but Icarus must enforce complete public return validators and reject degraded output. That compensation is small enough to test before writing a generator or forking another package.

+
+ +
+

What the first gate proved

+ +
+ + + + + + + + + + + +
CheckObservedMeaningStatus
Function renameOld method failed analysis with exit 3Generated function names protect callersproved
Argument renameOld named argument failed analysis with exit 3Generated arguments protect callersproved
Result-field renameBaseline fixture regenerated; returns stayed nullNo result mutation was exercisedinvalid leg
Missing return schemaDartvex emitted Future<dynamic>Unspecified server output cannot become a typed Dart resultreal gap
Unknown validatorWarning, exit 0, field degraded to dynamicDefault generation is not fail-closedreal gap
DeterminismSecond baseline generation produced no diffSame input generated the same outputproved
Runtime chaos and profileSkipped by the original stop ruleNo correctness, latency, memory, reconnect, or auth comparison existsnot run
+
+

Evidence: contract_gate.dart, baseline function spec, contract_gate.json, and the first result note.

+ +

The exact fairness failure

+

In contract_gate.dart, the result-field leg calls generate('folders_list_for_parent.json') again. The fixture contains "returns": null. The real folders:listForParent query also has no explicit returns: validator. The old caller therefore remained valid because there was no typed result contract to rename.

+ +

A diagnostic control supplied an explicit object return, generated the typed caller, then changed publicId to folderPublicId. Baseline analysis exited 0; analysis against the renamed result exited 3 with an undefined getter. That does not make Dartvex the winner. It proves the missing control can reverse the narrow conclusion about result-drift detection.

+
+ +
+

The neutral test boundary

+ + + +

Icarus continues to own the outbox, clientId/opId identity, revision and conflict rules, canonical JSON, and .ica round-trip. A client package is transport and tooling, not the owner of those promises. This is also why adopting either package unchanged would be the wrong abstraction boundary.

+

Before touching server sync boundaries, read server_side_sync_boundaries_handoff.md.

+
+ +
+

Fair rerun plan

+ +

Phase 1: repair the contract gate

+
    +
  1. Add explicit returns: validators to the stable public Convex functions used by the comparison. Match the real payload exactly; do not create a test-only fantasy type.
  2. +
  3. Regenerate a scrubbed baseline fixture and add a separate result-renamed fixture where publicId becomes folderPublicId.
  4. +
  5. Compile the unchanged caller against both. Baseline must exit 0. The renamed fixture must exit nonzero and identify the old getter.
  6. +
  7. Add an Icarus-owned wrapper around Dartvex generation. It fails if generation logs Warning:, if a stable public module contains unexpected dynamic, or if the generator exits nonzero.
  8. +
  9. Keep the unknown-validator mutation. It must fail the wrapper with the function and field path.
  10. +
  11. Run generation twice and require a clean repository diff.
  12. +
+

Boundary: do not fork Dartvex or write a replacement generator in this phase. First prove whether explicit server schemas plus a thin strict wrapper deliver the typed API we want.

+ +

Phase 2: make the runtime comparison symmetric

+

Define one small typed Icarus transport interface for the operations used by the gauntlet. Implement it once with Dartvex and once with convex_flutter. Both adapters must receive the same serialized operations, tokens, reconnect schedule, timeouts, and deployment. Package-specific convenience APIs cannot change the workload.

+

Keep two scorecards. The tooling score covers generated coverage, compile-time mutation catches, determinism, warnings, diff size, and maintenance. The runtime score covers correctness, convergence, recovery, latency, CPU, memory, and platform builds. A tooling loss cannot masquerade as a runtime loss, and a fast runtime cannot excuse corrupted library state.

+ +

Phase 3: run correctness before performance

+
    +
  • Run 50 deterministic seeds × 1,000 operations for each adapter.
  • +
  • Use editors A and B plus a clean verifier C. Begin every seed from base-test-v43.ica.
  • +
  • Exercise offline edits, delayed and duplicated delivery, reconnect, subscription restart, delete/recreate, revision conflict, and bounded retries.
  • +
  • Exercise an expired or rejected access token, call the current Supabase Flutter refreshSession() path, reconnect, and prove the queued op lands exactly once.
  • +
  • Persist the runner ledger and reuse identical clientId/opId values for both adapters so a process restart does not give one client an easier test.
  • +
  • After every seed, verifier C exports canonical state. Compare strategies, pages, folders, lineups, order, revisions, and round-trip output. Never compare timestamps or transport-only metadata that the product does not promise.
  • +
+

Use disposable test accounts and publishable client credentials. Never put a Supabase secret or service_role key in Flutter, fixtures, committed output, or logs. The current Supabase Dart API documents refreshSession() as refreshing and returning a new session even when the current session is not expired; the fault injector should assert the session actually changes or is accepted before replaying the op.

+ +

Phase 4: profile only after both are correct

+

Run at least 10 paired profile-build trials, alternating which adapter runs first. Report median and p95 remote convergence, reconnect-to-live time, peak RSS, steady-state CPU, transferred bytes, and build size on every supported desktop target. Record raw samples, tool versions, machine state, commit, and deployment identity.

+
+ +
+

What settles the gauntlet

+ +
+ + + + + + + + + +
ConditionDartvex consequenceconvex_flutter consequence
Any dropped, duplicated, misordered, or silently conflicted library changeImmediate loss, regardless of speed
Cannot recover queued work after auth refresh or reconnectImmediate loss
Generated stable API contains unexpected dynamicLoss unless the thin strict gate rejects it before commitNot a generated-code criterion
Both complete all 50 seeds with canonical equalityCompare profile results, API clarity, adapter size, dependency health, and maintenance cost
Runtime is tied within measurement noiseWins if the generated boundary materially removes JSON plumbingWins if Dartvex still needs broad custom generation or fragile patches
+
+ +

The expected best outcome is not “Dartvex untouched.” It is Dartvex plus a narrow Icarus strictness policy. If that produces complete, deterministic types and passes the same runtime gauntlet, Dartvex earns the win because it moves contract failures into analysis and removes hand-written JSON decoding. If the compensation grows into a package fork, a second generator, or recurring patches for common Convex validators, convex_flutter remains the more honest base.

+
+ +
+

Remote machine handoff

+ +

Start here

+
    +
  1. Check out t3code/convex-client-gauntlet and pull the latest commit.
  2. +
  3. Read this handoff, the first result note, and the server sync boundary handoff.
  4. +
  5. Reproduce the committed first gate before changing fixtures.
  6. +
  7. Implement Phase 1 as a distinct commit. Do not begin runtime work until every repaired contract check is green.
  8. +
  9. Implement the neutral interface and two adapters without changing local Hive models, .ica, UI, outbox semantics, revision rules, or server payload semantics.
  10. +
  11. Run correctness, then profile. Commit raw machine-readable results and a short human verdict. Leave the old result file intact as historical evidence.
  12. +
+ +

Current commands that exist

+
git switch t3code/convex-client-gauntlet
+git pull --ff-only
+
+cd tool/convex_client_gauntlet
+fvm dart pub get
+fvm dart run bin/run.dart
+fvm dart test
+fvm dart analyze
+
+cd ../..
+npx tsc --noEmit
+npm run test:convex
+fvm flutter test
+fvm flutter analyze --no-fatal-infos
+fvm flutter build web --no-tree-shake-icons
+

These reproduce the committed first gate and repository baseline. Add named contract-v2, runtime, and profile entry points as part of the rerun; document their exact commands beside the resulting artifacts rather than pretending they already exist.

+ +

Required artifacts from the rerun

+
    +
  • Explicit-return baseline and actual result-renamed fixtures.
  • +
  • A strict wrapper test proving warnings and unexpected dynamic fail with a useful path.
  • +
  • Generated-output snapshots or hashes proving determinism.
  • +
  • Per-seed runtime JSON for both adapters, including fault schedule and canonical verifier hash.
  • +
  • Paired profile samples with run order, machine, build mode, and package versions.
  • +
  • A final matrix that distinguishes compile-time safety, runtime correctness, performance, and maintenance.
  • +
+
+ +
+

Acceptance checklist

+
    +
  • The result rename mutates a return field, not the function name, argument, caller, or baseline fixture.
  • +
  • The stable public Convex functions in scope have explicit return validators that match real payloads.
  • +
  • Dartvex baseline generation is warning-free and contains no unexpected dynamic.
  • +
  • The unchanged caller fails analysis for function, argument, and result renames.
  • +
  • Unsupported validators fail before generated code can be committed.
  • +
  • Both adapters receive byte-for-byte equivalent operation traces and fault schedules.
  • +
  • All 50 seeds end with exact canonical equality and no unresolved op.
  • +
  • Auth refresh is performed with the client session only; no elevated credential appears anywhere.
  • +
  • All exported strategies and library backups still round-trip.
  • +
  • No winner is declared from the tooling gate alone.
  • +
+
+ +
+

Appendix

+
+ Raw fairness evidence · first gate and diagnostic control +
Committed first gate
+  result-field step: generate('folders_list_for_parent.json')
+  baseline fixture: "returns": null
+  runtime seeds: 0
+
+Diagnostic missing control
+  explicit typed baseline analysis: exit 0
+  publicId -> folderPublicId analysis: exit 3
+  failure: undefined getter on the unchanged caller
+
+Correct interpretation
+  Dartvex 0.2.0 is not fail-closed by default.
+  The original test did not compare runtime clients.
+  A complete result schema lets the generated caller catch result drift.
+

Captured 2026-08-26 against branch base df9f1934bbc7ab71e144208036f385b8f73c77aa. The diagnostic fixture was intentionally not committed; the fair implementation must add a reviewed equivalent.

+
+
+ +

Generated 2026-08-26 · Icarus cloud client evaluation · base df9f193 · Dartvex 0.2.0 · convex_flutter 3.0.1 · Supabase Flutter auth guidance checked 2026-08-26 · revision v1

+
+
+
+ + + + diff --git a/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md b/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md index 5a5f61f0..e123e867 100644 --- a/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md +++ b/docs/cloud_sync_refactor/convex_dart_client_gauntlet_result.md @@ -1,24 +1,40 @@ # Convex Dart client gauntlet result -Status: decision complete +Status: first gate recorded; runtime decision reopened on 2026-08-26 Base: `origin/icarus-cloud` at `e59402eedee9035cf14693fbd26fe8b097d6abfa` on 2026-08-26 Candidate versions: `dartvex` 0.2.0 and `dartvex_codegen` 0.2.0 -Decision: keep `convex_flutter` +Decision: no client winner yet + +## Fairness correction + +The first gate found a real fail-open strictness gap in Dartvex 0.2.0, but its +result-field leg regenerated the baseline fixture instead of exercising a +renamed result fixture. Because that baseline declares `returns: null`, it +could not test whether a typed generated caller catches result drift. + +A diagnostic control with an explicit object return passed baseline analysis +and then failed analysis with exit 3 after `publicId` was renamed to +`folderPublicId`. The runtime comparison still has not run. The corrected, +authoritative next-step plan is +[convex_dart_client_fair_rerun_handoff.html](convex_dart_client_fair_rerun_handoff.html). ## Result -Dartvex fails the mandatory compile-time contract gate. Icarus therefore keeps -`convex_flutter` and does not run the runtime chaos or profile stages. +The original run stopped at the compile-time contract gate and therefore did +not run the runtime chaos or profile stages. Treat that stop as a recorded +strictness finding, not as a final package verdict. The stable `folders:listForParent` function declares argument validators but no `returns:` validator. Convex represents that result as `returns: null` in a function spec. Dartvex 0.2.0 deliberately maps the absent result contract to `Future`. A caller that reads `result.first.publicId` still passes Dart -analysis, so a server result-field rename is not caught at compile time. +analysis. The committed runner did not actually rename a result field, so this +observation does not establish whether a complete generated return type catches +that change. Dartvex also treats an unknown validator as a warning and generates the affected field as `dynamic`. Generation exits zero instead of stopping at the @@ -31,7 +47,7 @@ performance observations and cannot be averaged away. | --- | --- | --- | --- | | Function rename | Old method fails analysis | Analysis exits 3 | Pass | | Argument rename | Old named argument fails analysis | Analysis exits 3 | Pass | -| Result-field rename | Old field access fails analysis | Generated return is `dynamic`; analysis exits 0 | **Fail** | +| Result-field rename | Old field access fails analysis | No result rename was exercised; baseline return is `dynamic`; analysis exits 0 | **Invalid leg** | | Unsupported validator | Generation exits nonzero with a path | Warning with path; generation exits 0 and emits `dynamic` | **Fail** | | Second generation | No repository diff | No generated-file change | Pass | @@ -98,6 +114,7 @@ three existing non-constant `IconData` sites in `folder_provider.dart`, change those files. Disabling icon tree shaking proves the web target otherwise compiles; the pre-existing release-build cleanup remains separate work. -The fallback typed-binding generator remains separate work. It may add explicit -public result validators and preserve `convex_flutter`, but it must not be -folded into this comparison result. +The next comparison may add explicit public result validators and a thin, +Icarus-owned strict wrapper that rejects warnings and unexpected `dynamic`. +Writing a replacement generator or package fork remains out of scope until that +smaller compensation is tested. From 0ac348dd1c1a966a2bb3f51e7b01c95d4ada5545 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Wed, 26 Aug 2026 23:47:04 -0400 Subject: [PATCH 05/11] test: repair Dart client contract gate --- convex/folders.ts | 20 ++++ tool/convex_client_gauntlet/README.md | 12 +- .../fixtures/folders_argument_renamed.json | 23 +++- .../fixtures/folders_function_renamed.json | 23 +++- .../fixtures/folders_list_for_parent.json | 23 +++- .../fixtures/folders_missing_return.json | 31 +++++ .../fixtures/folders_result_renamed.json | 50 ++++++++ .../folders_unsupported_validator.json | 2 +- .../lib/contract_gate.dart | 108 +++++++++++------- .../lib/strict_codegen.dart | 76 ++++++++++++ tool/convex_client_gauntlet/pubspec.lock | 2 +- tool/convex_client_gauntlet/pubspec.yaml | 1 + .../results/contract_gate.json | 46 +++++--- .../test/contract_gate_test.dart | 65 +++++++++-- 14 files changed, 410 insertions(+), 72 deletions(-) create mode 100644 tool/convex_client_gauntlet/fixtures/folders_missing_return.json create mode 100644 tool/convex_client_gauntlet/fixtures/folders_result_renamed.json create mode 100644 tool/convex_client_gauntlet/lib/strict_codegen.dart diff --git a/convex/folders.ts b/convex/folders.ts index 7679b46f..e5eea951 100644 --- a/convex/folders.ts +++ b/convex/folders.ts @@ -101,11 +101,31 @@ const folderScopeValidator = v.optional( v.union(v.literal("owned"), v.literal("shared"), v.literal("all")), ); +const folderSummaryValidator = v.object({ + publicId: v.string(), + name: v.string(), + iconId: v.union(v.number(), v.null()), + iconCodePoint: v.union(v.number(), v.null()), + iconFontFamily: v.union(v.string(), v.null()), + iconFontPackage: v.union(v.string(), v.null()), + color: v.union(v.string(), v.null()), + customColorValue: v.union(v.number(), v.null()), + parentFolderPublicId: v.union(v.string(), v.null()), + createdAt: v.number(), + updatedAt: v.number(), + role: v.union( + v.literal("owner"), + v.literal("editor"), + v.literal("viewer"), + ), +}); + export const listForParent = query({ args: { parentFolderPublicId: v.optional(v.string()), scope: folderScopeValidator, }, + returns: v.array(folderSummaryValidator), handler: async (ctx, args) => { const user = await requireCurrentUser(ctx); const scope = args.scope ?? "owned"; diff --git a/tool/convex_client_gauntlet/README.md b/tool/convex_client_gauntlet/README.md index 988fc5ff..6bb65af5 100644 --- a/tool/convex_client_gauntlet/README.md +++ b/tool/convex_client_gauntlet/README.md @@ -1,10 +1,11 @@ -# Convex Dart client contract gate +# Convex Dart client gauntlet This isolated Dart package reproduces the compile-time contract gate declared for Icarus's Convex client comparison. It pins `dartvex` and `dartvex_codegen` to 0.2.0 without adding either package to the application. -Run the evaluation and its regression test from this directory: +Run the repaired compile-time contract gate and its regression test from this +directory: ```bash fvm dart pub get @@ -12,5 +13,8 @@ fvm dart run bin/run.dart fvm dart test ``` -The runtime chaos and profile stages are intentionally absent. The handoff -requires them to stop when generated public results remain `dynamic`. +The gate uses an explicit return schema and an Icarus-owned strict wrapper. The +wrapper rejects Dartvex warnings and public methods that degrade to a +`dynamic` result. The result-rename fixture changes only `publicId` to +`folderPublicId`, so the unchanged caller proves the generated return type at +analysis time. diff --git a/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json b/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json index 33a811b3..8acd8977 100644 --- a/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json +++ b/tool/convex_client_gauntlet/fixtures/folders_argument_renamed.json @@ -23,8 +23,27 @@ } } }, - "returns": null, - "identifier": "folders.ts:listForParent", + "returns": { + "type": "array", + "value": { + "type": "object", + "value": { + "publicId": { "fieldType": { "type": "string" }, "optional": false }, + "name": { "fieldType": { "type": "string" }, "optional": false }, + "iconId": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconCodePoint": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconFontFamily": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "iconFontPackage": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "color": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "customColorValue": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "parentFolderPublicId": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "createdAt": { "fieldType": { "type": "number" }, "optional": false }, + "updatedAt": { "fieldType": { "type": "number" }, "optional": false }, + "role": { "fieldType": { "type": "union", "value": [{ "type": "literal", "value": "owner" }, { "type": "literal", "value": "editor" }, { "type": "literal", "value": "viewer" }] }, "optional": false } + } + } + }, + "identifier": "folders.js:listForParent", "visibility": { "kind": "public" } } ] diff --git a/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json b/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json index 140ae90c..f1f04273 100644 --- a/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json +++ b/tool/convex_client_gauntlet/fixtures/folders_function_renamed.json @@ -23,8 +23,27 @@ } } }, - "returns": null, - "identifier": "folders.ts:listWithinParent", + "returns": { + "type": "array", + "value": { + "type": "object", + "value": { + "publicId": { "fieldType": { "type": "string" }, "optional": false }, + "name": { "fieldType": { "type": "string" }, "optional": false }, + "iconId": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconCodePoint": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconFontFamily": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "iconFontPackage": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "color": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "customColorValue": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "parentFolderPublicId": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "createdAt": { "fieldType": { "type": "number" }, "optional": false }, + "updatedAt": { "fieldType": { "type": "number" }, "optional": false }, + "role": { "fieldType": { "type": "union", "value": [{ "type": "literal", "value": "owner" }, { "type": "literal", "value": "editor" }, { "type": "literal", "value": "viewer" }] }, "optional": false } + } + } + }, + "identifier": "folders.js:listWithinParent", "visibility": { "kind": "public" } } ] diff --git a/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json b/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json index dacb4e14..d424a633 100644 --- a/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json +++ b/tool/convex_client_gauntlet/fixtures/folders_list_for_parent.json @@ -23,8 +23,27 @@ } } }, - "returns": null, - "identifier": "folders.ts:listForParent", + "returns": { + "type": "array", + "value": { + "type": "object", + "value": { + "publicId": { "fieldType": { "type": "string" }, "optional": false }, + "name": { "fieldType": { "type": "string" }, "optional": false }, + "iconId": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconCodePoint": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconFontFamily": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "iconFontPackage": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "color": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "customColorValue": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "parentFolderPublicId": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "createdAt": { "fieldType": { "type": "number" }, "optional": false }, + "updatedAt": { "fieldType": { "type": "number" }, "optional": false }, + "role": { "fieldType": { "type": "union", "value": [{ "type": "literal", "value": "owner" }, { "type": "literal", "value": "editor" }, { "type": "literal", "value": "viewer" }] }, "optional": false } + } + } + }, + "identifier": "folders.js:listForParent", "visibility": { "kind": "public" } } ] diff --git a/tool/convex_client_gauntlet/fixtures/folders_missing_return.json b/tool/convex_client_gauntlet/fixtures/folders_missing_return.json new file mode 100644 index 00000000..7099ebf5 --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_missing_return.json @@ -0,0 +1,31 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentFolderPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": null, + "identifier": "folders.js:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_result_renamed.json b/tool/convex_client_gauntlet/fixtures/folders_result_renamed.json new file mode 100644 index 00000000..f9656a4d --- /dev/null +++ b/tool/convex_client_gauntlet/fixtures/folders_result_renamed.json @@ -0,0 +1,50 @@ +{ + "url": "https://your-deployment.convex.cloud", + "functions": [ + { + "functionType": "Query", + "args": { + "type": "object", + "value": { + "parentFolderPublicId": { + "fieldType": { "type": "string" }, + "optional": true + }, + "scope": { + "fieldType": { + "type": "union", + "value": [ + { "type": "literal", "value": "owned" }, + { "type": "literal", "value": "shared" }, + { "type": "literal", "value": "all" } + ] + }, + "optional": true + } + } + }, + "returns": { + "type": "array", + "value": { + "type": "object", + "value": { + "folderPublicId": { "fieldType": { "type": "string" }, "optional": false }, + "name": { "fieldType": { "type": "string" }, "optional": false }, + "iconId": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconCodePoint": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "iconFontFamily": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "iconFontPackage": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "color": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "customColorValue": { "fieldType": { "type": "union", "value": [{ "type": "number" }, { "type": "null" }] }, "optional": false }, + "parentFolderPublicId": { "fieldType": { "type": "union", "value": [{ "type": "string" }, { "type": "null" }] }, "optional": false }, + "createdAt": { "fieldType": { "type": "number" }, "optional": false }, + "updatedAt": { "fieldType": { "type": "number" }, "optional": false }, + "role": { "fieldType": { "type": "union", "value": [{ "type": "literal", "value": "owner" }, { "type": "literal", "value": "editor" }, { "type": "literal", "value": "viewer" }] }, "optional": false } + } + } + }, + "identifier": "folders.js:listForParent", + "visibility": { "kind": "public" } + } + ] +} diff --git a/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json b/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json index a508757f..56f3b87a 100644 --- a/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json +++ b/tool/convex_client_gauntlet/fixtures/folders_unsupported_validator.json @@ -13,7 +13,7 @@ } } }, - "identifier": "folders.ts:listForParent", + "identifier": "folders.js:listForParent", "visibility": { "kind": "public" } } ] diff --git a/tool/convex_client_gauntlet/lib/contract_gate.dart b/tool/convex_client_gauntlet/lib/contract_gate.dart index a5d3e616..836296a6 100644 --- a/tool/convex_client_gauntlet/lib/contract_gate.dart +++ b/tool/convex_client_gauntlet/lib/contract_gate.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; -import 'package:dartvex_codegen/dartvex_codegen.dart'; +import 'package:crypto/crypto.dart'; +import 'strict_codegen.dart'; const _dartvexVersion = '0.2.0'; const _dartvexCodegenVersion = '0.2.0'; @@ -19,21 +21,12 @@ Future evaluateContractGate({String? packageRoot}) async { final generated = Directory('${root.path}/lib/_probe_generated'); final caller = File('${root.path}/lib/_probe_caller.dart'); - Future<_Generation> generate(String fixtureName) async { - final logs = []; - final errors = []; - final exitCode = await runConvexCodegen( - [ - 'generate', - '--spec-file', - '${root.path}/fixtures/$fixtureName', - '--output', - generated.path, - ], - log: logs.add, - errorLog: errors.add, + Future generate(String fixtureName) async { + return runStrictConvexCodegen( + specFile: '${root.path}/fixtures/$fixtureName', + outputDirectory: generated, + stableModulePaths: const ['modules/folders.dart'], ); - return _Generation(exitCode: exitCode, logs: logs, errors: errors); } Future<_Analysis> analyzeCaller() async { @@ -66,11 +59,13 @@ Future evaluateContractGate({String? packageRoot}) async { 'Future listForParent', ); final firstGeneration = _directorySnapshot(generated); + final firstGenerationHash = _snapshotSha256(firstGeneration); final secondGenerationResult = await generate( 'folders_list_for_parent.json', ); final secondGeneration = _directorySnapshot(generated); + final secondGenerationHash = _snapshotSha256(secondGeneration); final deterministic = secondGenerationResult.exitCode == 0 && _snapshotsEqual(firstGeneration, secondGeneration); @@ -86,8 +81,15 @@ Future evaluateContractGate({String? packageRoot}) async { final argumentRenameAnalysis = await analyzeCaller(); await generate('folders_list_for_parent.json'); + final resultRenameGeneration = await generate( + 'folders_result_renamed.json', + ); final resultFieldAnalysis = await analyzeCaller(); + final missingReturnGeneration = await generate( + 'folders_missing_return.json', + ); + final unsupportedGeneration = await generate( 'folders_unsupported_validator.json', ); @@ -96,26 +98,30 @@ Future evaluateContractGate({String? packageRoot}) async { ).readAsStringSync(); final functionRenameCaught = - functionRenameGeneration.exitCode == 0 && + functionRenameGeneration.accepted && functionRenameAnalysis.exitCode != 0; final argumentRenameCaught = - argumentRenameGeneration.exitCode == 0 && + argumentRenameGeneration.accepted && argumentRenameAnalysis.exitCode != 0; final resultRenameCaught = - !resultIsDynamic && resultFieldAnalysis.exitCode != 0; - final unsupportedRejected = unsupportedGeneration.exitCode != 0; + resultRenameGeneration.accepted && + !resultIsDynamic && + resultFieldAnalysis.exitCode != 0; + final missingReturnRejected = !missingReturnGeneration.accepted; + final unsupportedRejected = !unsupportedGeneration.accepted; final baselineCompiles = - baselineGeneration.exitCode == 0 && baselineAnalysis.exitCode == 0; + baselineGeneration.accepted && baselineAnalysis.exitCode == 0; final gatePassed = baselineCompiles && functionRenameCaught && argumentRenameCaught && resultRenameCaught && + missingReturnRejected && unsupportedRejected && deterministic; final report = { - 'schemaVersion': 1, + 'schemaVersion': 2, 'evaluation': 'convex_dart_client_contract_gate', 'baseCommit': _gitBaseCommit(root), 'adapterCandidate': 'dartvex', @@ -125,12 +131,17 @@ Future evaluateContractGate({String? packageRoot}) async { 'dartVersion': Platform.version, 'fixture': 'folders:listForParent', 'baselineCompiles': baselineCompiles, + 'baselineAnalysisExitCode': baselineAnalysis.exitCode, + 'baselineAnalysisDiagnostics': _sanitizeDiagnostics([ + baselineAnalysis.output, + ], root), 'checks': >[ { 'id': 'function_rename', 'required': 'old_generated_method_fails_analysis', 'status': functionRenameCaught ? 'pass' : 'fail', 'generationExitCode': functionRenameGeneration.exitCode, + 'rawGenerationExitCode': functionRenameGeneration.rawExitCode, 'analysisExitCode': functionRenameAnalysis.exitCode, }, { @@ -138,36 +149,53 @@ Future evaluateContractGate({String? packageRoot}) async { 'required': 'old_named_argument_fails_analysis', 'status': argumentRenameCaught ? 'pass' : 'fail', 'generationExitCode': argumentRenameGeneration.exitCode, + 'rawGenerationExitCode': argumentRenameGeneration.rawExitCode, 'analysisExitCode': argumentRenameAnalysis.exitCode, }, { 'id': 'result_field_rename', 'required': 'old_result_field_fails_analysis', 'status': resultRenameCaught ? 'pass' : 'fail', + 'generationExitCode': resultRenameGeneration.exitCode, + 'rawGenerationExitCode': resultRenameGeneration.rawExitCode, 'analysisExitCode': resultFieldAnalysis.exitCode, 'generatedReturnType': resultIsDynamic ? 'dynamic' : 'typed', 'detail': resultIsDynamic - ? 'The unvalidated Convex result is absent from function-spec and the old dynamic field access still analyzes.' - : 'The generated result is typed.', + ? 'The generated result is unexpectedly dynamic.' + : 'The explicit return is typed and the renamed result rejects the unchanged caller.', + }, + { + 'id': 'missing_return_schema', + 'required': 'strict_generation_rejects_public_dynamic_result', + 'status': missingReturnRejected ? 'pass' : 'fail', + 'generationExitCode': missingReturnGeneration.exitCode, + 'rawGenerationExitCode': missingReturnGeneration.rawExitCode, + 'diagnostics': _sanitizeDiagnostics( + missingReturnGeneration.diagnostics, + root, + ), }, { 'id': 'unsupported_validator', 'required': 'generation_stops_with_function_and_field_path', 'status': unsupportedRejected ? 'pass' : 'fail', 'generationExitCode': unsupportedGeneration.exitCode, + 'rawGenerationExitCode': unsupportedGeneration.rawExitCode, 'generatedFieldType': unsupportedSource.contains('dynamic futureField') ? 'dynamic' : 'not_dynamic', - 'diagnostics': _sanitizeDiagnostics([ - ...unsupportedGeneration.logs, - ...unsupportedGeneration.errors, - ], root), + 'diagnostics': _sanitizeDiagnostics( + unsupportedGeneration.diagnostics, + root, + ), }, { 'id': 'deterministic_regeneration', 'required': 'second_generation_has_no_diff', 'status': deterministic ? 'pass' : 'fail', + 'firstSha256': firstGenerationHash, + 'secondSha256': secondGenerationHash, 'changedFiles': _changedFiles(firstGeneration, secondGeneration), }, ], @@ -270,6 +298,18 @@ List _changedFiles( .toList(growable: false); } +String _snapshotSha256(Map> snapshot) { + final bytes = BytesBuilder(copy: false); + for (final entry in snapshot.entries) { + bytes + ..add(utf8.encode(entry.key)) + ..addByte(0) + ..add(entry.value) + ..addByte(0); + } + return sha256.convert(bytes.takeBytes()).toString(); +} + bool _bytesEqual(List left, List right) { if (left.length != right.length) { return false; @@ -282,18 +322,6 @@ bool _bytesEqual(List left, List right) { return true; } -final class _Generation { - const _Generation({ - required this.exitCode, - required this.logs, - required this.errors, - }); - - final int exitCode; - final List logs; - final List errors; -} - final class _Analysis { const _Analysis({required this.exitCode, required this.output}); @@ -308,6 +336,6 @@ Future readFirstFolderPublicId(ConvexApi api) async { final result = await api.folders.listForParent( parentFolderPublicId: const Optional.of('parent-folder'), ); - return result.first.publicId as String; + return result.first.publicId; } '''; diff --git a/tool/convex_client_gauntlet/lib/strict_codegen.dart b/tool/convex_client_gauntlet/lib/strict_codegen.dart new file mode 100644 index 00000000..26774b4f --- /dev/null +++ b/tool/convex_client_gauntlet/lib/strict_codegen.dart @@ -0,0 +1,76 @@ +import 'dart:io'; + +import 'package:dartvex_codegen/dartvex_codegen.dart'; + +final class StrictGenerationResult { + const StrictGenerationResult({ + required this.rawExitCode, + required this.exitCode, + required this.logs, + required this.errors, + required this.diagnostics, + }); + + final int rawExitCode; + final int exitCode; + final List logs; + final List errors; + final List diagnostics; + + bool get accepted => exitCode == 0; +} + +Future runStrictConvexCodegen({ + required String specFile, + required Directory outputDirectory, + required Iterable stableModulePaths, +}) async { + final logs = []; + final errors = []; + final rawExitCode = await runConvexCodegen( + [ + 'generate', + '--spec-file', + specFile, + '--output', + outputDirectory.path, + ], + log: logs.add, + errorLog: errors.add, + ); + + final diagnostics = []; + if (rawExitCode != 0) { + diagnostics.add('Dartvex generation exited $rawExitCode.'); + } + diagnostics.addAll( + [...logs, ...errors].where((line) => line.contains('Warning:')), + ); + + if (rawExitCode == 0) { + for (final modulePath in stableModulePaths) { + final module = File('${outputDirectory.path}/$modulePath'); + if (!module.existsSync()) { + diagnostics.add('Stable generated module is missing: $modulePath'); + continue; + } + final source = module.readAsStringSync(); + for (final match in RegExp( + r'(?:Future|Stream)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(', + ).allMatches(source)) { + diagnostics.add( + '$modulePath: public method ${match.group(1)} has an unexpected ' + 'dynamic result.', + ); + } + } + } + + return StrictGenerationResult( + rawExitCode: rawExitCode, + exitCode: diagnostics.isEmpty ? 0 : (rawExitCode == 0 ? 2 : rawExitCode), + logs: List.unmodifiable(logs), + errors: List.unmodifiable(errors), + diagnostics: List.unmodifiable(diagnostics), + ); +} diff --git a/tool/convex_client_gauntlet/pubspec.lock b/tool/convex_client_gauntlet/pubspec.lock index a1f80238..5cfff0ae 100644 --- a/tool/convex_client_gauntlet/pubspec.lock +++ b/tool/convex_client_gauntlet/pubspec.lock @@ -74,7 +74,7 @@ packages: source: hosted version: "1.15.1" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf diff --git a/tool/convex_client_gauntlet/pubspec.yaml b/tool/convex_client_gauntlet/pubspec.yaml index 58e43ece..bd8a17d1 100644 --- a/tool/convex_client_gauntlet/pubspec.yaml +++ b/tool/convex_client_gauntlet/pubspec.yaml @@ -5,6 +5,7 @@ environment: sdk: ">=3.11.0 <4.0.0" dependencies: + crypto: 3.0.7 dartvex: 0.2.0 dartvex_codegen: 0.2.0 diff --git a/tool/convex_client_gauntlet/results/contract_gate.json b/tool/convex_client_gauntlet/results/contract_gate.json index cccaf265..6371db1c 100644 --- a/tool/convex_client_gauntlet/results/contract_gate.json +++ b/tool/convex_client_gauntlet/results/contract_gate.json @@ -1,5 +1,5 @@ { - "schemaVersion": 1, + "schemaVersion": 2, "evaluation": "convex_dart_client_contract_gate", "baseCommit": "e59402eedee9035cf14693fbd26fe8b097d6abfa", "adapterCandidate": "dartvex", @@ -9,12 +9,17 @@ "dartVersion": "3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\"", "fixture": "folders:listForParent", "baselineCompiles": true, + "baselineAnalysisExitCode": 0, + "baselineAnalysisDiagnostics": [ + "Analyzing _probe_caller.dart, _probe_generated...\nNo issues found!" + ], "checks": [ { "id": "function_rename", "required": "old_generated_method_fails_analysis", "status": "pass", "generationExitCode": 0, + "rawGenerationExitCode": 0, "analysisExitCode": 3 }, { @@ -22,39 +27,54 @@ "required": "old_named_argument_fails_analysis", "status": "pass", "generationExitCode": 0, + "rawGenerationExitCode": 0, "analysisExitCode": 3 }, { "id": "result_field_rename", "required": "old_result_field_fails_analysis", - "status": "fail", - "analysisExitCode": 0, - "generatedReturnType": "dynamic", - "detail": "The unvalidated Convex result is absent from function-spec and the old dynamic field access still analyzes." + "status": "pass", + "generationExitCode": 0, + "rawGenerationExitCode": 0, + "analysisExitCode": 3, + "generatedReturnType": "typed", + "detail": "The explicit return is typed and the renamed result rejects the unchanged caller." + }, + { + "id": "missing_return_schema", + "required": "strict_generation_rejects_public_dynamic_result", + "status": "pass", + "generationExitCode": 2, + "rawGenerationExitCode": 0, + "diagnostics": [ + "modules/folders.dart: public method listForParent has an unexpected dynamic result." + ] }, { "id": "unsupported_validator", "required": "generation_stops_with_function_and_field_path", - "status": "fail", - "generationExitCode": 0, + "status": "pass", + "generationExitCode": 2, + "rawGenerationExitCode": 0, "generatedFieldType": "dynamic", "diagnostics": [ - "Generated 4 files in /lib/_probe_generated", - "Warning: folders.ts:listForParent → returns → field \"futureField\": Unknown Convex type \"future-validator\"; generated as dynamic." + "Warning: folders.js:listForParent → returns → field \"futureField\": Unknown Convex type \"future-validator\"; generated as dynamic." ] }, { "id": "deterministic_regeneration", "required": "second_generation_has_no_diff", "status": "pass", + "firstSha256": "ea6a3655ead9e0250282424593fa1be36907c55d7de8386bb5503d80b985bc36", + "secondSha256": "ea6a3655ead9e0250282424593fa1be36907c55d7de8386bb5503d80b985bc36", "changedFiles": [] } ], - "gatePassed": false, - "decision": "keep_convex_flutter", + "gatePassed": true, + "decision": "continue_runtime_gauntlet", "runtimeGauntlet": { - "status": "skipped", - "reason": "compile_time_contract_gate_failed", + "status": "pending", + "reason": null, "correctnessSeedsRun": 0, "operationsRun": 0, "pairedProfileRuns": 0, diff --git a/tool/convex_client_gauntlet/test/contract_gate_test.dart b/tool/convex_client_gauntlet/test/contract_gate_test.dart index d01571cd..acec78be 100644 --- a/tool/convex_client_gauntlet/test/contract_gate_test.dart +++ b/tool/convex_client_gauntlet/test/contract_gate_test.dart @@ -1,8 +1,11 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:icarus_convex_client_gauntlet/contract_gate.dart'; import 'package:test/test.dart'; void main() { - test('Dartvex 0.2.0 fails the declared Icarus contract gate', () async { + test('strict Icarus wrapper repairs the Dartvex contract gate', () async { final result = await evaluateContractGate(); final report = result.report; final checks = (report['checks']! as List) @@ -13,12 +16,60 @@ void main() { expect(check('function_rename')['status'], 'pass'); expect(check('argument_rename')['status'], 'pass'); - expect(check('result_field_rename')['status'], 'fail'); - expect(check('result_field_rename')['generatedReturnType'], 'dynamic'); - expect(check('unsupported_validator')['status'], 'fail'); - expect(check('unsupported_validator')['generationExitCode'], 0); + expect(check('result_field_rename')['status'], 'pass'); + expect(check('result_field_rename')['generatedReturnType'], 'typed'); + expect(check('result_field_rename')['analysisExitCode'], isNonZero); + expect(check('missing_return_schema')['status'], 'pass'); + expect( + check('missing_return_schema')['diagnostics'], + contains(contains('listForParent has an unexpected dynamic result')), + ); + expect(check('unsupported_validator')['status'], 'pass'); + expect(check('unsupported_validator')['rawGenerationExitCode'], 0); + expect(check('unsupported_validator')['generationExitCode'], isNonZero); + expect( + check('unsupported_validator')['diagnostics'], + contains( + contains('folders.js:listForParent → returns → field "futureField"'), + ), + ); expect(check('deterministic_regeneration')['status'], 'pass'); - expect(report['gatePassed'], isFalse); - expect(report['decision'], 'keep_convex_flutter'); + expect( + check('deterministic_regeneration')['secondSha256'], + check('deterministic_regeneration')['firstSha256'], + ); + expect(report['gatePassed'], isTrue); + expect(report['decision'], 'continue_runtime_gauntlet'); + }); + + test('result mutation changes only the returned public id field', () { + Map functionFrom(String fixtureName) { + final fixture = + jsonDecode(File('fixtures/$fixtureName').readAsStringSync()) + as Map; + return (fixture['functions']! as List).single + as Map; + } + + Map resultFields(Map function) { + final returns = function['returns']! as Map; + final item = returns['value']! as Map; + return Map.from(item['value']! as Map); + } + + final baseline = functionFrom('folders_list_for_parent.json'); + final renamed = functionFrom('folders_result_renamed.json'); + expect(renamed['identifier'], baseline['identifier']); + expect(renamed['functionType'], baseline['functionType']); + expect(renamed['visibility'], baseline['visibility']); + expect(renamed['args'], baseline['args']); + + final baselineFields = resultFields(baseline); + final renamedFields = resultFields(renamed); + expect( + renamedFields.remove('folderPublicId'), + baselineFields.remove('publicId'), + ); + expect(renamedFields, baselineFields); }); } From bf421ffef06b5d04749c77bda182f8f0a53796fe Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 27 Aug 2026 01:00:13 -0400 Subject: [PATCH 06/11] test: add symmetric Convex runtime gauntlet --- tool/convex_client_gauntlet/runtime/README.md | 67 ++ .../runtime/analysis_options.yaml | 8 + .../runtime/app/.gitignore | 45 + .../runtime/app/.metadata | 30 + .../runtime/app/README.md | 18 + .../runtime/app/analysis_options.yaml | 28 + .../runtime/app/lib/main.dart | 65 ++ .../runtime/app/macos/.gitignore | 7 + .../app/macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 10 + .../runtime/app/macos/Podfile | 42 + .../runtime/app/macos/Podfile.lock | 22 + .../macos/Runner.xcodeproj/project.pbxproj | 801 +++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../app/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../app/macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++++ .../app/macos/Runner/Configs/AppInfo.xcconfig | 14 + .../app/macos/Runner/Configs/Debug.xcconfig | 2 + .../app/macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + .../runtime/app/macos/Runner/Info.plist | 32 + .../app/macos/Runner/MainFlutterWindow.swift | 15 + .../app/macos/Runner/Release.entitlements | 10 + .../app/macos/RunnerTests/RunnerTests.swift | 12 + .../runtime/app/pubspec.lock | 473 ++++++++++ .../runtime/app/pubspec.yaml | 87 ++ .../runtime/fixtures/empty.json | 1 + .../runtime/lib/runner.dart | 832 ++++++++++++++++++ .../runtime/lib/transport.dart | 253 ++++++ .../runtime/lib/workload.dart | 530 +++++++++++ .../runtime/pubspec.lock | 458 ++++++++++ .../runtime/pubspec.yaml | 23 + .../runtime/test/transport_smoke_test.dart | 27 + .../runtime/test/workload_test.dart | 60 ++ .../runtime/tool/export_canonical_state.dart | 48 + .../runtime/tool/provision_test_account.dart | 153 ++++ 49 files changed, 4755 insertions(+) create mode 100644 tool/convex_client_gauntlet/runtime/README.md create mode 100644 tool/convex_client_gauntlet/runtime/analysis_options.yaml create mode 100644 tool/convex_client_gauntlet/runtime/app/.gitignore create mode 100644 tool/convex_client_gauntlet/runtime/app/.metadata create mode 100644 tool/convex_client_gauntlet/runtime/app/README.md create mode 100644 tool/convex_client_gauntlet/runtime/app/analysis_options.yaml create mode 100644 tool/convex_client_gauntlet/runtime/app/lib/main.dart create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/.gitignore create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Release.xcconfig create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Podfile create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Podfile.lock create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.pbxproj create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/AppDelegate.swift create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Debug.xcconfig create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Release.xcconfig create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Warnings.xcconfig create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/DebugProfile.entitlements create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Info.plist create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/MainFlutterWindow.swift create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/Runner/Release.entitlements create mode 100644 tool/convex_client_gauntlet/runtime/app/macos/RunnerTests/RunnerTests.swift create mode 100644 tool/convex_client_gauntlet/runtime/app/pubspec.lock create mode 100644 tool/convex_client_gauntlet/runtime/app/pubspec.yaml create mode 100644 tool/convex_client_gauntlet/runtime/fixtures/empty.json create mode 100644 tool/convex_client_gauntlet/runtime/lib/runner.dart create mode 100644 tool/convex_client_gauntlet/runtime/lib/transport.dart create mode 100644 tool/convex_client_gauntlet/runtime/lib/workload.dart create mode 100644 tool/convex_client_gauntlet/runtime/pubspec.lock create mode 100644 tool/convex_client_gauntlet/runtime/pubspec.yaml create mode 100644 tool/convex_client_gauntlet/runtime/test/transport_smoke_test.dart create mode 100644 tool/convex_client_gauntlet/runtime/test/workload_test.dart create mode 100644 tool/convex_client_gauntlet/runtime/tool/export_canonical_state.dart create mode 100644 tool/convex_client_gauntlet/runtime/tool/provision_test_account.dart diff --git a/tool/convex_client_gauntlet/runtime/README.md b/tool/convex_client_gauntlet/runtime/README.md new file mode 100644 index 00000000..db38d5df --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/README.md @@ -0,0 +1,67 @@ +# Convex Dart client runtime gauntlet + +This package runs the same deterministic Icarus cloud workload through Dartvex +0.2.0 and `convex_flutter` 3.0.1. It targets an isolated local Convex deployment +and uses a disposable Supabase user with the public anon key. Never use or pass a +`service_role` key. + +Each correctness candidate is configured for 50 seeds of 1,000 operations. The +trace includes offline queuing, delay, duplicate delivery, subscription restart, +rejected-token refresh, reconnect, revision conflicts, delete/recreate cycles, +and a persisted mid-run process checkpoint. A fresh Dartvex client performs the +canonical final-state and `.ica` round-trip verification. + +## Verify and build + +```sh +fvm dart format --output=none --set-exit-if-changed lib app/lib test tool +fvm dart analyze +fvm flutter test test/workload_test.dart +cd app +fvm flutter analyze +fvm flutter build macos --debug +``` + +The nested macOS app is required because `convex_flutter` loads its native Rust +bridge from the application bundle. Flutter's unit-test process cannot supply +that framework. + +## Run correctness + +Start an isolated local deployment from the repository root: + +```sh +npx convex dev --codegen disable --tail-logs disable +``` + +Set `SUPABASE_URL`, `SUPABASE_KEY`, `TEST_EMAIL`, and `TEST_PASSWORD` in the +environment. `SUPABASE_KEY` must be the public anon key. Then, from `app/`, run +each candidate with the same settings: + +```sh +export CONVEX_URL=http://127.0.0.1:3210 +export ADAPTER=dartvex +export SEED_COUNT=50 +export ALLOW_CHECKPOINT=1 +export RESET_PROGRESS=1 +export REPORT_NAME=icarus-dartvex-runtime-correctness +export GIT_COMMIT=$(git -C ../../../.. rev-parse HEAD) +build/macos/Build/Products/Debug/icarus_convex_runtime_runner.app/Contents/MacOS/icarus_convex_runtime_runner +``` + +When the runner reports `checkpoint`, run the same command again with +`RESET_PROGRESS=0`. Before switching adapters, replace the isolated deployment +data with the empty fixture, change `ADAPTER` and `REPORT_NAME`, and restore +`RESET_PROGRESS=1`: + +```sh +export CONVEX_DEPLOYMENT= +export CONVEX_SELF_HOSTED_URL=http://127.0.0.1:3210 +export CONVEX_SELF_HOSTED_ADMIN_KEY=$(jq -r .adminKey ../../../../.convex/local/default/config.json) +npx convex import --replace-all --table users ../../../../tool/convex_client_gauntlet/runtime/fixtures/empty.json -y +``` + +Correctness reports are written to the app container's temporary directory and +copied verbatim into `results/` after checking that they contain no credentials. +Phase 4 profiling is forbidden unless both candidates pass all correctness +conditions. diff --git a/tool/convex_client_gauntlet/runtime/analysis_options.yaml b/tool/convex_client_gauntlet/runtime/analysis_options.yaml new file mode 100644 index 00000000..939066c4 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/analysis_options.yaml @@ -0,0 +1,8 @@ +include: package:lints/recommended.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + diff --git a/tool/convex_client_gauntlet/runtime/app/.gitignore b/tool/convex_client_gauntlet/runtime/app/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/tool/convex_client_gauntlet/runtime/app/.metadata b/tool/convex_client_gauntlet/runtime/app/.metadata new file mode 100644 index 00000000..00d61647 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: macos + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/tool/convex_client_gauntlet/runtime/app/README.md b/tool/convex_client_gauntlet/runtime/app/README.md new file mode 100644 index 00000000..7ec61633 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/README.md @@ -0,0 +1,18 @@ +# Icarus Convex runtime runner + +Native macOS host for the parent runtime gauntlet. See +[`../README.md`](../README.md) for build and execution instructions. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/tool/convex_client_gauntlet/runtime/app/analysis_options.yaml b/tool/convex_client_gauntlet/runtime/app/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/tool/convex_client_gauntlet/runtime/app/lib/main.dart b/tool/convex_client_gauntlet/runtime/app/lib/main.dart new file mode 100644 index 00000000..0f83fdf7 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/lib/main.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/widgets.dart'; +import 'package:icarus_convex_runtime_gauntlet/runner.dart'; +import 'package:icarus_convex_runtime_gauntlet/transport.dart'; + +String _setting(String name) => + Platform.environment[name] ?? String.fromEnvironment(name); + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + try { + final deploymentUrl = _setting('CONVEX_URL'); + final adapterName = _setting('ADAPTER'); + final supabaseUrl = _setting('SUPABASE_URL'); + final supabaseKey = _setting('SUPABASE_KEY'); + final email = _setting('TEST_EMAIL'); + final password = _setting('TEST_PASSWORD'); + if ([ + deploymentUrl, + adapterName, + supabaseUrl, + supabaseKey, + email, + password, + ].any((value) => value.isEmpty)) { + throw StateError('Required gauntlet settings are missing'); + } + final runner = GauntletRunner( + adapter: adapterName, + deploymentUrl: deploymentUrl, + supabaseUrl: supabaseUrl, + supabaseKey: supabaseKey, + email: email, + password: password, + seedCount: int.tryParse(_setting('SEED_COUNT')) ?? 50, + gitCommit: _setting('GIT_COMMIT'), + transportFactory: () async => switch (adapterName) { + 'dartvex' => DartvexTransport(deploymentUrl), + 'convex_flutter' => ConvexFlutterTransport.create(deploymentUrl), + _ => throw StateError('Unknown adapter: $adapterName'), + }, + ); + if (_setting('RESET_PROGRESS') == '1') await runner.resetProgress(); + final report = await runner.run( + allowCheckpoint: _setting('ALLOW_CHECKPOINT') != '0', + ); + final reportName = _setting('REPORT_NAME'); + if (reportName.isNotEmpty) { + final reportFile = File('${Directory.systemTemp.path}/$reportName.json'); + await reportFile.writeAsString(jsonEncode(report), flush: true); + stdout.writeln( + 'GAUNTLET_RESULT:${jsonEncode({'status': report['status'], 'adapter': report['adapter'], 'reportPath': reportFile.path})}', + ); + } else { + stdout.writeln('GAUNTLET_RESULT:${jsonEncode(report)}'); + } + exit(0); + } catch (error, stackTrace) { + stderr.writeln('GAUNTLET_ERROR:$error'); + stderr.writeln(stackTrace); + exit(1); + } +} diff --git a/tool/convex_client_gauntlet/runtime/app/macos/.gitignore b/tool/convex_client_gauntlet/runtime/app/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Debug.xcconfig b/tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Release.xcconfig b/tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Flutter/GeneratedPluginRegistrant.swift b/tool/convex_client_gauntlet/runtime/app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..cccf817a --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Podfile b/tool/convex_client_gauntlet/runtime/app/macos/Podfile new file mode 100644 index 00000000..ff5ddb3b --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Podfile.lock b/tool/convex_client_gauntlet/runtime/app/macos/Podfile.lock new file mode 100644 index 00000000..359e5709 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - convex_flutter (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + +DEPENDENCIES: + - convex_flutter (from `Flutter/ephemeral/.symlinks/plugins/convex_flutter/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + +EXTERNAL SOURCES: + convex_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/convex_flutter/macos + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + convex_flutter: 8cfa610fc48ddd56ec7a40fee812f6ac843de187 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.pbxproj b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..bdd54500 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 78E851B043B42EAAB5A86101 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 399696742BD0FE2BFDC72DFA /* Pods_Runner.framework */; }; + E7C70C3FB54789E3F2B1CE07 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C5AAC0C46CBC7791EC6FCD /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1A9FB1483B9B0D9F88ACFE07 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 263800875FF9157A802C2559 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* icarus_convex_runtime_runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = icarus_convex_runtime_runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 399696742BD0FE2BFDC72DFA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C5FDF3C3055F3A6F48022F3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7B3642483E9F18A06D0E0116 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AFAFF87CD59390390264C5F1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + C565F9BA5F076FD5FB286052 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + F9C5AAC0C46CBC7791EC6FCD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E7C70C3FB54789E3F2B1CE07 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78E851B043B42EAAB5A86101 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 6E6B75429E8A28AA8D02C9F4 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* icarus_convex_runtime_runner.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 6E6B75429E8A28AA8D02C9F4 /* Pods */ = { + isa = PBXGroup; + children = ( + 1A9FB1483B9B0D9F88ACFE07 /* Pods-Runner.debug.xcconfig */, + C565F9BA5F076FD5FB286052 /* Pods-Runner.release.xcconfig */, + 7B3642483E9F18A06D0E0116 /* Pods-Runner.profile.xcconfig */, + AFAFF87CD59390390264C5F1 /* Pods-RunnerTests.debug.xcconfig */, + 6C5FDF3C3055F3A6F48022F3 /* Pods-RunnerTests.release.xcconfig */, + 263800875FF9157A802C2559 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 399696742BD0FE2BFDC72DFA /* Pods_Runner.framework */, + F9C5AAC0C46CBC7791EC6FCD /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 5FF2629C1CD01B87E8B10496 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 36838491CD21759A5DA1D396 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 940D41AD028C881D7AB78E7D /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* icarus_convex_runtime_runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 36838491CD21759A5DA1D396 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 5FF2629C1CD01B87E8B10496 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 940D41AD028C881D7AB78E7D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AFAFF87CD59390390264C5F1 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.icarusConvexRuntimeRunner.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/icarus_convex_runtime_runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/icarus_convex_runtime_runner"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6C5FDF3C3055F3A6F48022F3 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.icarusConvexRuntimeRunner.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/icarus_convex_runtime_runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/icarus_convex_runtime_runner"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 263800875FF9157A802C2559 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.icarusConvexRuntimeRunner.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/icarus_convex_runtime_runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/icarus_convex_runtime_runner"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..78db9035 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/AppDelegate.swift b/tool/convex_client_gauntlet/runtime/app/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z zKpxF`G*X(YOz2^@7i#D+s-~A1E;3&x%%qL5hkiy^JhYjJ74{hvVmAx*6BH`M`!qGC zO9pjEsR)A-n1`6KLACSL%FS_Kcm+?4*z-V?WAZPs?RkzoijIr~I+oh1^~T`q^dCFvG$Gbd8AnTYBjLKYUmayaQz#S1le7Q^Hyr#;X&h*1wDpm+gZC!rSKom zq|+o&UGpeXtlQ1;?@JukKG!8PGS1Io0z6O}ZeL&DsON^I0K+>Mxv#ohK+;ByAZ`Eb z2orY{j0Pa3edA(#-pJA0AaJ6h& z81Gl(pd#j~mrizktoid14K5ig7u8FvZmLLP%l@dl05IprCyqDB?mA2fc*6UB+49lb zZ8`V9epdo=OeZoiY%zw-w`8DNwTORV_>>3T{r)1-YsGSo0E2s>tix9OBqKFBjg#}G z`pgkCblKMYs!Z)r^(qT_c+}gLhR|gnq!1~Qr|~kt&2@_yswx{i$KEn`8J1W8BGljl zr@GEG#W(s#AKKyuqLp+cl1C}7%`m#-!$15XF{M(M*-fD%+i#mFbP35jlgN3{8#A-dmj&OQtG)!031jTwGMal=&YtPfq2AUWekP9J-JT(p099!L`+yen$ zVH1?kRrhV7(mGKkm_jPP_U@Xd;x=ppk}4WY0Rbr> z0MJM_;$GGxL*P68y%KBqHntF{>X&<{aeI4m6+{TQ%~Zp}v%Pujr)zg5mV;cFKqeA- zQm5`#Sd{B6Rc*4PS-rO(vf>YEdXmOK?>K@`L5}|9q}#t_IE%g+U<-1qw3mr5&v;2A zCQ}BEn9_u;;>n5N#dP0RhCF-_UplC+U(i~Zjh>U5+b8%@p3HK(R*IMQwE!uritb}< zF)AK2?+0@-aE3LYkg`B*&N&m~JWB9>(Z>`aqRwgioU)0w{U1K4?>-#i|ZfhNa9hV)2)(%ch zJMH1twoeZWwkE@I!dz$ma+;9GeACv>Ncupl@+gBSeU_uzfj!$+h&@EACkZG_vwLGA z(?^;rcJu1$5H~xI@6lHIYC-$+b&hF1p`AoAOKqw{t0Fu#X`OGt$)7Q!nmJ=&)xjq@ zHoxT4pcYKSPT5(4yzIuQ^S*N2NJpR4v0?rB-^JuaXNLis?E(l>Jo8mUw(gsFLLOy? zEszHWGaCn|lw$LSwoj{G7Uq(zK0W^VVWu#ms8BMRlF2z%-g`fOXmndgC(na8fc)s` zz$GAoxP+l|+T_S4$r1sLwkV77ew1Gug*`|HiE*?FGLm1q; z^p0A0eqqbmk3?|!CB9DBN1Zof6d7+ zJSn!`VD~tVaqy<*Mw^8dM5v3Bvj2VdVFb=)U3L2eDM3@>n(P z?Rr_=I17+r4fE{>1LBQG0&o97nef67n-aNnVP<{dd6*B!Q344 zZbsAof&jw+;CLeK2d87t9s~YZ5?6Qwf&{NPEBN+)LbjOcZRXNcR&h)x`TtdpI+b!>$E~h0o1L*2OddpR9!Gw~-E^Cj(7i69S<66ak$)AYMv|xG+;uR(`;h zGIV3}?+Qxdjz)s;s}jHY{JPmeo@-tN$H@hxaV@)}K?y~ts~E6H(F|SlsN5oH8g7*h zGiC!8c1doE3U|D}Vul1yPmXuCk*hmyU4MG2ml#V0+(G5I+`L_=3cD$%$I=@*8m-LU-!fn&-sZO1%ls63+w}AiAK`Jv z>`q~ztr&&(gCkFpci+*1Ekdv*MhBCzGfPBj9dM|YEjZk(tWBuz4?MGeq+*)t>Q=z6UXF_w z{QDUT4^JQ8J%hW;d2xGB>Fl4Y-bRT!ttP2GE5jYoI1e(eVK0&V5W+>zludt=nf|UN zi1IV;MK$Fy%$yw<oGeW?JIGjmfGLH$Y;l|T0p1V!N*Jvu zHSAG0WpwPip0vm7%VRq8$2O2>P5b!WBfTz*6dZ4Wd6O9Y(8A;nOuG((y?F`ac_u2( z#~17CoTK)1G<~~Z4jXlout{e&nZbDHyHf(=a?OtaJ(2Q(!g#)Ugw-QQ?A?mN#yN%T zBtJ`sA6Lpg`k>Pi8a7GssiY$eG0Be8LCoQL{GDqi-;j0pLmT!Z)szldvbN7GVcu*S zzb1rEq|M)1qa7rM*I8!<#w7FnQ?{v^? z0`MlS3+`#ZB5$DT4+`7e-Hlp_2G0`*F@STbRJ|!tk3cC~1T%NR-p4s=sTT+RqsMjF zyrp-Jv?CD4Y3N&Zb1gr=%`MFR8;|r)uxQ6*X{OpEhQ~+tu}^n8Wijiy`pSMw0uKNi zSNX^Z1y;WirM0o_x%zft0U2GcLm_2BS`b{Z>g|9VOVr%QF*R?pTpiJsEbj4jLVAyd zTA;x15=f~b0^(e*Vo;Tn;WTJSxpI9LmL($Lxob<^S!k7mGhnnVNnAC*g!$ms0#Q|q zs=25I0<>fUw_&+KU`}5P9wlmjRWdMYh%Np6n?AAHQ;JzG?s(Z9UR`pNh79Nzk~DF+ zX~jy>>f-2bl?drlM8 z3NfIQnrT@pLmv+QA6efWPv!sqe;mh3_RcOj5>Ya;4hhN13dtx*_TJ-=kX_kZQDkPz zIw}#e_dK%au@1*L&iUP^cfH?zf1iK)tHv=t|>-9mMT!;;Vg|svSzWkN7q#t$c4N$Q;tl3EYwef_4q>GO<#I89VhY;`X*hz$n*GZ%f+;uViG z?uLlxD1OIeid}0r9%Ssoc7@vJjZIsZlU9zvYpjhYiOrzD5sq3OC zpf-X;Nb!DLpxqX^zDIK%=46-Z3%i-bac`RIBS5*wcw5Pu>G|kF>TQP$dGRYh#1hwD z{|cbbTOKL>Gb1-;X6?vWLC+KJ_^Ij?KzJ7eZ?^8XNgoYU9^z&>d zsIjX*uOK`#Wu!`>L@y!=XpQcW+mBaRjm|XrB@etLdr}Ob57e7EkE;7a*t7=M#XFL6 za;KHHk-rBNTjp-gS^;ehKNv>K>+_jPQ45J%4><1HyKJ?;T9#~k_23?xD}B&@Wp{%H z($hU+nWR?g!9dsJkgVz(J_Yrdns+m~9V_gQ7Sb`&F4wZZ!k}##j$>O{4{?avCbCZfyW zO$)m7LE=P?$CXHDU_RUD+sYwT;nKI7 zSs_XTv!BuxpJ!7(b~uYfsgzt~mj5(vf2r~`LHwpePs!o2A3zEr@#sxo8HEe8>V||d zBiz0@e&6}p*}!6jsm}I0bN9Mc2(c#jg@;Nu6!Kv&4&P8-UcQ-00WJIO%4OuUn;^jU z;I3r=T3KQtiMQ7&x32eVtB`mCe)9ws^7u%2P`B%Xc}=Qc&O^{FmS^{~Rho}^s`B+H z=1_T);9LRK?{$Vx22!5m)Er8aoPOA8&{7fyt`t@~Vw%gtx~+g3qs8LFR%(2Uny28A6dFYnNQgcUa>Sq=%alFh&8#@1o_qgwve* zVFimnUtL{4aHP6s?FB%bu2SP=e*VGqXC8iuZ-JOc{5%Lx0g|VvyWkdh&FD^Gkc!0N zhoolXvp6GC8wj?Y+V;r*EN+<1ac`-+!8Mqb@Nz)=OqV?4gxhR^t7*+^+AfxxVt(n{ z+fkk|-xSGqmkZa@Q%`;;r`-Z|? z0fR6b@l%pTwK*@xY+(MwBUwf^z+F*~piC64BWTrz}-HS1-XF-IA%?Zs_#F8 zcmUuEZ6Of>YIJOe$&{V;3vIBw7|jSGPeS6cvTMdj96Y~pI-z7InGW;(DhFqaiTTO9@KWvQi9__j0btLZ9 zAa~-Po%^sDFfme4@Yiq}r`BgnYK2eTwCjg9_zC4V{{&_GTm-!qHGVR6JXDjw;}GzF z6lXA{xo1+tQM{9vwb1&sRXPdGDHbEMbnwh}t+%tvcw5p4J4r#hEpDl=A{;Mjc%0)T zsG}v<$^HhdcE)5IJ^iBWK{7?Zn)vb%c!5eIj4 zbT}CGO*u)Od@^LuIC@_2{=AP2-O99NglFudj{!T}0e8wtTQcB@F9QW6$J!0Ye`T+U zXDx84b$!hD#4YzSyZLy~!IIZuFa3%eU zG4eg5?}sZ6Yj29P^-PcXG*8%VzLL$0!oL?c(!oQ+G!kORsa+lsf5YER>PX83R4LgF zgPNQJ#Bo#)MXU%J9k?RWD;c>|as5b5p>xAwau=X5XbERX`_ZHB8_XSNDe`s?n(e>) zGF$G%n6o+W{6A-@4hsIK0*J%jpB#Y*G^B48eQD(CDZR5oBl-P=)r7fH^PLf?!aK6V zwkIM35?l*I6p@;^H}JIDNs-fF*IFN?k?kj(M)QKM%%?dSkf1d$Nly2z(>)oq8z}0H zH?Qa{x&36#W@y04!9zx@x7un@ob$&)V8#f~0n1|jF0kFs4aZ{ND1~QjWHToIY5)LY zrgKDCj@dFCx&-w$QMi=CqD*=`$NqC~2k366pPXl#>Y7A=iQD}f`)+B-pS@LIW_M?9 zlBS_)(vGz!L$#P`?<3Hvonw@B1uJ244y)M?0)z0-hq++sJ0GZ+{oiiH;lFi&wy(C! z0Bv9z^M;`4@)USP)7dhg@K5K&U&|7&-@I0Sk>I+ZH75_xEn>qh9qmc%aA@NEKBsVBgUuK zC=b{w-0oU|)~tAVI zyJ3BAB}%rsjz7qZ?x_XCWe6!_u-{e_3u68Asso0IvwKdxq1lN#%4w>J zi>}P;$JZ>58(ZAjsmSJl6BWUTe`0eGEf3f_yS#H6vx;UJWO7CCK!{)4C}`C$j5gNj|k znb$4QRurEE3tPEe!JzG-a0DmvXePO zSD#Q-qOAjTMm|=aBSnvwHoEbgyVIz@J$hT*legak-hhb}e#%cm2$nR2 zV9A{kc)WT$np=5coPQIskbGMO@Fn2NxPv$@SJZdG6}jV;+%(cH+*RFQ(+DjsJlman zy`D(yN?8MCtjWD3w}Q|jQccb$}BDW%M$zZZnri2+5ls)@@(wQD`jt_GpTKL_^CO&SSCcHbfMX#JXYFI^*947 zPh&S-G=l*C@`E5CU1$m7ao(Q&oSmY7)ZZ#5_fEyYzLsFJwJ%GfErFeRN@7lUbUrL| z$6;gQSNsI91LJvT+$Zb0>g<4g8T{B!U05lfKmoSRH^pB^^8sJ3{8PzVq0NeypMF5k zU3qOqksdq{>AUjm3O~dZx^vS6C$ldgCWszl?xd8-sJ;-kPnISB*-f=L*8XggOx$?u zg%B-QovSjBbj}%sShZv~r?`*6PiiQW;nee<-=+y4}S#}q_BgXIJoSOf$YbE7vXt4;Np zrKzZf6Ny0aES8(-cqmnIGMg&ieYWryBZ0VTB=4<*@auP4NdIk&q(Mt(OLPm|Yl za!0OpC9sA#tk>OsaCSx0;!$5r6naw ztzLBo>#LKaxxsO=yWe%yGilL`A|6E#TK! z+1VRQlo*D?(k0-mlRM+`OMT8kVB*-%ZGv}Aj1u^j!wu*~>L<-T+u?6sX!3C}lQte- zk(6_=iwXsQ0JbRvJDwMnk!c99w~s~uD_4vMB=m~-ft-*|z~$*g4g;pgG~Ap1m@@Fx zWS)8IKSN6`^vVQ8hv^Oc+O(Rt7!U%wVsGP+Y6fyS%GG+v+dIdVfCXPzAV~~li+3m5 ztFQmbE)(#2#Oi@k$1#zUS6ijD_yYsa{+BHZAw+^zAEI3bc(h0qm?|pNf?oS}Km#OG zrOfCKn_-CVO;}DXu|5YE#d8I2o>}vUxYlv&>=+I28WY>a1;uI)HUM_IvpF;Ln4ROT zf!=1rpKihNFUo=R@sD-pT!EOm%%ncl43f;aem^;|A#s3`b6vjeAzO!M-gwc`-Kj~{ zBX)tq64*kJl#TrgW4o%hTY3x$P01nD6a6s2#MmwM$vyX5PU|YngU*wXGK*?f?#Eg$~^OWW3I@of-=XVuu-b%A1Z|nqY_2 z;~jD&=QnB#WGU>;RwFq(I< z34K1fCMwf9F}G%k(&?~2EY&)W*-_z0ReS$;7+I1)zz`)M zpAF{5ZHLPMJhYU z;GE*@hM1NM{G{L94dL$!Y-h6A9K9W=I6AYb`Y=v{(tpyLQz^^Aibea(q()R*TU|-m zozpyr!|-BZ_Dn+$*2|vq2Y@ghHo!-`WjVtU-bab(SJp2*2i-}$UP9^qnF_OIFS~-< zYj^VS!)Wu}vn6!LDIt!HJ1SU-@ce>z8f4cT4R9V@O^Xg9)4`VpjsXm*~@%l^Ux;Rf#Zck`BNXu0Y(!C zj%Z}UAmD00nsOS%Uull)dU(fZgJ$bo>3Oa`8h~Wt)EM?v(ndlTS1p0|E9Pg>=&>58 zghD~%R;YpqZAw;F;M(lx5b_wkVbnd+ER+6A-SYj^1XUgNGn0I~ES|f|5emjyPIW)S z0z8i6)BZt&h(qQxih4HbFYa6~jyeKbc_`QEdLD@9SBGButjw|b^l*oQjDk<7Nig08IK zb`ATVGzK%LP+>9aFM0hr8t+m`uNr?h&8o3Rp$T&ql||K}7GgobFhCViaDH~+F#yC- zt>7T3&_PZ*feTKTyd6vlF~JmEA1f+*>CCE4ex}5N^$4o)YuxX&3T$P0(IS!+kan^J z_p>v#1J8bWELml|S02YAQe-&yVew+kipZr~H-I@yc$=8#rZ-8L<_nDx&Qv3dJDwUX z!)@=h1`~R2M{$J8bM^1O&Gy2oxe1T;K?NA{iv_eYuhpLyc3%xu%z`dVc}Z}%cHGHQ<7P!Q|e?dwnSpL!AUf!B^!?#^Q#W!Ry+7ofwPZ1mZq z(Id0{htmX1W?2cAYWZo_lOtT#+Us-nlP$=CGK|Ri4x0Xh>(|iN9y1 z=9y26A4Y}ViRi9Fxzm{>J`YM>GX1D|$4BY9xJrY{oY2~Z&};B{Zq9Pp!pox`8e#0C z-h~@fohA74(#ws!{7kIe4v6XUX<)9bd)g66Bz%^Y4p0~OF+rY;l$v&7T<3~4y!bv> zR$r#LblZcVgy2lq!ff+>yuR4qCcljQa03x|dTcG7`CHcxh#POtGKt6ymNd_0qF7Wf zBj_KC8{jl!zZ>0neDp19n3sD?HC=|WM3!}cK4zCnu6Uoj*hbV1<#F2BD)@A~y%@VXx+u}Hcn=_s-({PxzmMZ^xJ1SV zoZMY*FarYvO_@z8Lr2ep)%HgIL7rhYa~#X&&V8oYSw zA4m{3{hw1Vb~~26K^xro&e7i9eg^SqK0i}kG3z(!_~E?sjJlSWIWXJqKiHAWTG*SpPcCMD`kEc1gx`R^YkYWz zEN4vEIkj@&e4tC!(_~x`-K$w6CU%X7U2Y z)Y}T5stEyoSsB{H{+xfST3tov~6@lO}2gx#N(rHXiOAHT!dp6FiV8V)B4{L_P_% zmX0rPa^-{1xG6|#uEGo+!v)QAOjRe|jg2ICcXU!|Cr+LMbLHlhJ)ErR*P9*z$NLlt zmYjAUbljq004ZyOco?HJovV7M*Wb2nF8vT2D;3kGi%F)6Kr#TVW>}zTHnUQxoGmD0CY9J`|d%8@}n;_co2q zWr98`R_c@PQbMi}x3bWo4XZj{it6qYj+o*XvNoS4>rF;7WNn;vA*|A!3H}Wh-uk@n z*hV0S+XnX;K;BOoz?&*9_{NnM25s4^^QUt|>R!()^Z6#G3OmL{CU^-IG_M7_a~B+& zCrV;ouC1ljbK(K=ygqAE_-}ewnH2&&t0enS7}I4i0wJgNvCf|P$`|DHku`K`HfDa2=n@DCg8MRi_)vpMR2Mxy4PE2Qe! zD||kNXy=0WeU(43v%md9Hg9Zu#CP%d%C67gk_#pfXs8lf>M=betm(}0fdDKq0{26# z_c?J!Cgo-~*=wswLXkR|W8d+rDdV00`22Ouv=_Hod9bmB!=D$I4r@7DZX7e+0tO!9 zR{0d}A6^K#yRx@ykotO4(WUJsmFvN)d-o-wZ(wcDSUS`8jO-JSAMa4y@MK4fDP`(P zzxQ2})ofiauWKj9{Rm$Yw^?g=?`oO(Vf|T^I+-A+o1#F`>tn59d=FtgVJAV=y;G&` z0GMvtEeil5;e$Ln8-41(UeMl2kYLk%vPl?0+Egg_;g)494o5FsvdeZKP;&&fjw7o{ z|B+e%Z|)8Ts?=>@p|hr!nYXgV=ZjI4Cp#$E>+g^6r7Nd3<>-t=G%B5IyZUI{e{49G zqnIXEB=M@5Ndf1J#l5YWcLG=A4ufF8S{z5Kz-uM?Ni{{%mr);=l0=473h#cIc{K3> zZ-VUw_Ng5^HgWQhs5tQU@qv-YBej9`R$a^|lknX<*+sSVXue8M0#EPBJ6_Liwl*8l z_zoD#!l%WIXJZ$jm?|zUu0LdeP&8IW*(|39&QzKGnem$6--u{ZGtHt#Hro*h)?lu zXGKo-4Hv1WP*VLj;uA6UwGSV*6ro%PRbwR{@tXoCOb=OFTB4ru-|Id!rP5Y6LF*-D zy|t0qDSVPo$ffyoj#CIZV?l3VsPRYye$F^xxv~Z78_fwlCWbwW!nYCR2nx0_+@tg3C_UDMVa2Br=X3hfP}^Cp4Yg=#OK}K zKYVY`V9jEKD!UrCbSX6Xym2T-cg}!n;?;o{mM|zWj0P@D|FO-rQ zKt#ApEh#AX%_f%9!G6`I*K=bSnMIhQ%W5&BOMntzVr*eS;WR;FgM)+k`#+Vze*z&V zkU^I-R|!Nwy<~>eeQ~hJqa2|DdpX15kD=6U73Du;T|VarycBP^n#IZeIJ&H3S9#@oec~poZELqX$DAc>XZyuIqd^GK0Jq~0kI=d zA7gMo8%zmkEdnqMh)tkp?V0I;Tm3`>aU3^~dXw zlhdd3=iygnUgYu#GRhxln}4D?Gokczq?T;RjCk0=fUHy18$lt!-q!%sNxee7No^+N$9d?Es*``)0UJ4SC&FNY0pf z_MlbGdUy$|F}YDvJ9GTCkZbsNKj3DL5;=BGBx8xI;n)=A0d0j6MP7Mi6MQdk@Tux2Qy`oI_&*%EQ0bE?|R>P$rDhcFa8O?JIK zPOpFDa?-L*+Q7RrCg#y5z$l0d>n@+OYo3g>-Z*x&`Jj5|=*UOYaJer6;FAbdtt0O? zrFGUE?!XeUG}G8wMgeTs%+r;3uUU;Nq5EuU{h-g&UOBKhdS`;J=m!~xn*ztv_p@dD zR)tR!P=~5kX)FRsx9)uyuu?0dh%Ht7`PTM@e#Cq!z2ts;O;L)tQ1ipDiWqbGz@o_p z^D=UKR#`S7HAt4vQtD(_SeWyj_av~#tJKlb9>-s5Ykuzx_E1ZNl4)~f=zG$*;-y=T z2ozmFva9az<{2&63fQ?(Q8{IPx@t1LuFcxP-LXVctWh3AwazVTt2)w^*Zn-#eB`bD zSHoAusjOBK5(>uQPGj=ijdOH3jqG?(<5#C{*JQ?Lt~@zow=Ii4Al$Vr!#+Cf-gx)A z`_h(>b@7?*6bYM8%628gGW^rwWoG$mK_eCk`}B&llStfwHf12*{5spmTeNH$4{gCY z@Yuwr*k@%m;T<60bw9z6^WpWi@Bu^qe-g;YAzI+VjgsuZaGA=^G*I{KLy@rIjSpWb zFQNsCp2T;S$VaJtZ<(waRu8y7^X;>YhsWp zM)mKgCeE@K;J4vQSV z&-(Gl5AJCp>K*2-`U|4i;u3p8xo6(isu-38>cY zml1Eo&FBBKJpour?}q&nggpFiGM%m+YX`ng8P+uRnJiMyWcv*_AZ8KAB$w;rfmN8C z<-2EB6TqZO>A~P{*<);wYqZgxQS8E*syOXvGkGxF@s(scud0uv?T)fQ z(DGrwM7lvpitUG~6!*}kZUpBn9PuP`5^nMK@($xI^0Q~axP5qU>L~uF{R_<9&m z({}$$WuD1y-QzMVb3jLPk`~bDJNkw(Dv-6cKUb4uzD= z-w?i0NZ2K}AbT}Zi^uOZ32xmSxJw+6(3j%a!~Tdy-@RxVx6YUw2|V6JX+mSJNclfl zF~SD#eo+lnB=ZpHLl{)E+`sI^-V1Vn!6#Ml_W4aH*Pe(++sNI`M=5L3?X1z0;CJeE zJiX5Mp6JH*=R9W0t(1@>>1y=lP^F=yJil6JxU~I}EpTsBx?rJ5LbCbQ zuLBmmX1MO&!E}khx=+#hCesIB53`IWwqyFtR{AUv7vJ{Q^dn1S0@*^UOmRwctFy&> zd={(J@avBzmu$MbyamRMt_$kfHY<*v)%%&nY4hUDH=$k)$8LHlUG0G3Kv#T~-vQjw z)hXbsNIg?~b-jRw)ir5Q(gfwM+Zk+0haf z+4ER%>T8RnKAoJ-(s&tu&-iZ@A?^J|d z6md=9C4am*v2r=aa&a?~37bc($n#wQ<8UGXL+!RtrRXGSj-2INJ#+3J=}e6nOC}G8 zN~lvCS@rxoq7w$CLg-wx!%V%ymw>~xhUw4cADX*$A}D~{21F$!Y61aHwpdL!QcrsN zl~$s5kk%7HWHkZ43%mOcwlk3RcbKGQ*}K(Fxput)rpE0zH0vY(EyY=blQZ`odG#hD z)~{&r6XkSE(^csqsaMm>2c%xsT2&g_Nab1bTY%fIoNHatDY@C@Ei~v@19|F?szU6SWRS)uDXqNY!48RlAb;S*ijqus; zp;bteR835>3BXML2CewOM<^q3M*ubU`}gnI-oS&(vf=GF|JJB-inGOH_dc1xb|iqR zWgrcNy?1*8)vAlAaiBE%K3Q>5Ygy-#Wf$>FqL|Kvgb&6H?iQC*Z|PN)xZJhH#d#=a z@s9O0oea6Lg}submzNZ{iZ*_okZ$6G*h5YO!dE=7c4=YA9g$y%1xjkVl#|1DShEjM zH3(sS?uRfB3mhW5Wrm} zrY>KpBxM&CC;s5Ie_{o}upN{vdb8x<_$5iiQN49`z`+Zz`&E`yLAim;X&}$HAfKmT zkO2Dgdno95mWMH~h2c4);H=MigT8hyzl|4g;dU7F;p^X>w!fa0zf{^rf?>~ z0w{=F_R}ru{g5i@&xwC%R-!-1x|(k6pSb5_)$f`zyErIvSCs{z`iVvU4x_znFKti!!av6BkRX_=+kEc;*`_rla zB`g4ruCJGT3XVTTrlh3Yj>1>PNIy?sV%Yo*=qaBIOY87_?P04yx6TV?_{~K? zOHEo3|2EA2JAMPYZM!H<{|!s-$r>l5{19icxV`Wf-{<0I>{v&H4FZaCy$B6Ludz{v zRH!!HV#JGP?5(L!Zp#}NlOODgWqjO+yo~+LasPYxH+ht2KjdfCFQr(oovP3?vkFK^5FvPJ4^LD=DpYQi4tUXuY1;erJaBQ79 zHcp(>mKvoD+)bq5SX9siR>(%CL??*D>Snn%p}NfGO4(RY^puLI+j$Pw)NZLb5bKo{s|0L~ z-A3R~;QHMg0bHSgESOM&N&@oF4|8gkPF-nVM=sQ;d}wcS{{!iW-)yQ``D6t#xlh(O zRF0Z@O>0uMz9g)u{P))ptV5lH2(gC8I5i(FDRG5Gp1bgBydKgxJy5gBfK(#D7NzZU zatG}S^z#KL*Do5=K*F7hk(`mbdgI1XoM!8*-};#UzNtEG@Nki#`7)GfV;VlfW^)=` zBaAjK5>gx@wf_D!B!2C6xBK^K4%x|+#?P@5N7tlfWo6xWJD~Wz^cnPfFF($Ixt4!j z9%x^1$on56XZB0Irm^kw-*rd1YVO;(*LbB21@7OPJspo%WO676#~oUMws(zP#+shG+$ns0IC3W z_{kYU>N5<_6=j>*0d}r-?8U+--eXfy2M+opoYL|=I932TMp=&k#tzJ^72OtRJ8BVOvTYPh;@EE=LJLeOk`y?d|Dd9%fWlhON^LnB^6x0LyZqz@imyogJ`$C@Lr9Z4o)ZQz>NCavG$$@e2#r3 z4I=}I5KgV>wl)~_Ja7gLQGju0c1{h%cV&6c`doWWv$>q*=ZLc8J{hBiKXNK?zx2Nr zz!pph;BLU2OaZTv>Pzj(VpSp2&OWNCF<~>NgL!nezhxEgj;&2 zl>z@V#>sykFCnFL?|(j)J3SFr|FFa`n@KbhC2pZB7 z#3>qIn&~mG_Vki=p8_x&CFeD4V7MvgJlk^G7H;(apFxr+7Gc0+1KfI6$@aeF+d7DJ~_-A|H=0?Da#&^Cqb=!=fVz>giW5nw=jWQBS%L^t1EZ@ zCm9;qlG{($@0W3T&l17ownc5pWhfM8Mwn-fLtb7H|IYl)8@QikEc_Le+s60x?&B*m z5kObB5{BD}gGr7l84~vP{N)C~3V;xhBWd%=^j0&KBw3T3-HU`;hqWA3OWW~<8nl-M zfYn-BI0_?g`3$_;&Exw<(G{QM|8)Kq28x9NF-F$>r@_BO)t^T*i-U1bX01<)zC_uE zR@8qEQQ#cm$YbXIUPVO?z7KI$pw@r=-V{V@>dC9Hn==1QBVy_b;#*jR+&f*$AwCl?o&G?2Uk4=*Ej zFK^Yvw*HTO9n!XRBWe++o3)4O!OC9PC=_l_<$M(W8(Akk`zv5?nJifb^rH3N?Hhio zo$=nNmSEz_QFHj|XF!vQEcdqPyZz_4|M_GBH)k)KA9XGRlTJD;3*y1c#?ZWkeaQM* z^`Bf04#Z)ARgrE4rMmlk8E5F=NpaW8xKNd3)-orW$m+kh(W12jQbQ7oi z)=#qbmhkplt}u`FC0sV9sdnb5$E!zX_xlA{4wW&j0*DCm`=1;Sh_sB1xiH@C89Z93;8d)EUk=lPNIZ`o3H`Vd+Ig`=CV}#?PAXvzWk{x96fn z0(rYh<>?PJ>Hd8v@c8=*vm+)>P1k@i2>yMaKw2nihLV6Z;wcdc*E2{8=xNh(FkEe3 zq_pc;ISw&}`?lqKx<4vIa67!xu|P}G$c3MDyg?u^InS?uM6Zzys0QM9ChW>g-ypzA zkOUSfvhTTWq{_>TJ{+kpgwX{@>P5ptiJ1NTO5)8 z8BiLUY_!*AJ$V386^TicK@z0qOPWP#Ea5?}!$_&fQ zOcRKuR^tLX*&CM(ahYftiNg!a=uU|He)2nU2(~iX@Yo|foZp906;o=d%aK09YEW7_ z-yX*;XE#z@?zZ&fQ?2fYX!T8@-$(K5Jo+AkyOM+(944x4B%2NR&avFFJY^9_br5UtzSX5@gmYYm@ z@S$jtqFn18bXQr0IYhQ=+2~ZDB_DRW3d=*B+3q`-*1P$i!GVIG(AMp=vBQ#^_mNxp z(;4Iz#_~&9jZ}}7oW?R;_x8&h?b0N326NJq4~>W^TeI^!o4=G5G{|9ff|`NN5+?ns zL@IWva(*@PXPmVGQ#rgIOY*nnoqNDDy$hd2uMT>wBgzg>YT&BV2U{k1ah1(1j_v0` z@o;6~SUGW=!+j!oa9ko_2^G75?VolPmWk=Pb-h{k=phZga( z88Rp7QzbHkpYG!aug9e^DF63Bi|1#CeAW^CpakO9DTT!p$yhuT8Aq10^cl2O@Zl-2RXr`+zCPj#_FqXs}W2{Qvn2Y{BmNsG45? zB{BF_rVgT$u0 zE8o6|@C>uOK1Ba}!V zx!M$9J1B7#_JSs90cKlucib?T&HqQpLE9YV1?v{gh2NWKEt9FX8;3DePnCL5Z=k)Flp=?-i$<5H4zc z`?2ZZ+p~Y8FYr;m3Vn2(u5Z`Av6#S}zkpQpZ|vNP0DY^I-oa$HXzg+ajQC7%wldRN zfOAL!UwFtuphqqR41v|3He4cQF5;UU9M~lti-k<HSTs^#>-Tf|C2&~#m%6WZAy1jz!Q_-IbpZP z8ht8}UG13lz+N-7+01+RlE)6OT^3px7fn@1|_b7^{bhPet}< z_)77(<^>8-qQ2X(n4faVhm@T0@Z{5HFSWs~EDXtV@7IAMbVUP6;v8^%l3PZ#wOZ-* z*Vk4lRj6OYpAZ_$*`t|tYKmLar&&{5{d+5cst)rQTn`n8>Xi+0zXc6YbTPMgzewFg z23F=+`8=FXXF6b*CDVN$v3|6iy;TSFSYh$qrbhKDcT^U9l zj}3g#zty{k*>s8S+>t|cng#3@Rz`z}njy{*?90mV6_Mkvv=iL9pb0ttHf$7;TxkX1 z-klTGb`2~-Mxx6~+{b-KiFd3XG`p?+6-0PMorB#Q@TY_CH5)En#5WrmHqj;@Fvi1A zeGpO@wuYIPOgRY&02e-U+j7!$LZ#5mS72R3MJS^gfheL5`kQV_n{8}KXaj)V%4b~As zFrQ7yZal}~{ELX@8c#V?2LlM@)g(|;VvcBjEuTJ=`WkOem{DL!+7Lr!U;F!mGm_^~ z+V^T?%bz+8noq9{ybcq16Gzd^fS2`skac)@6|;8X8l6Q19epZ@l^3@1ES!x2XLNA4 z_FI8#x5sq7hXVr83D;_5$sU!*Ye}zyx1wMC?Q{DSgrUx#fM?_Fj@{syA2x2yL^J{S zPPLkQ#O+9E9a^H*USdriL6rGHDt$B!vu~t7^)@_e=(<|SVd!MenX48AP(Z$4WoC9_ zeN;I;hEAr{ZvB^gK*1AWfI~5H0a{Y#2UBjn9`7;3JDrI5leeufemoZol*pDlVTSHP z3#8@6kxsJwUFg9(;)>Xm!{nsFC<7}Xwv_?o=eP)$>vvvj>yw z=YS7{pIOg(u@mJ%G0G^TM@L6>l)?_{_e`(yLxmX%h*D zMJS13@e!}HFR{?GNtq;%=4#zUgfFP^$g|Ax1<`vC&qIPbwGNo}3>ZM?=Evk6r|J&S zi$UD-za)A$kcqu)8)1mG z{FI*zS4{wM6S3;RP-!$0&8!6*;>|%T%HJxZt}cmap#~4vD0Pkx22gBbPo~=2iEMFa zSN<~qRz>jf54?e)>3%j;Gc6C1_YO0C|CDQDt7+bE({$0($tizZ)xn2L?@6_ zR3$`yiwH?E%X*^k*^oQ=z!1GA|E&fXHPR=rIEGq4%0=SGvror2Y%k#d`aPmx5@~7a zdkmPa1d-<`6M%& zp9rn|?C(5SRowEcasXoE$)s`=GvJk9wPt|2VX31T2F}6x3#(&IMqZND*a1muBh9?X zX_HSLo?$y$a;qFx^U1W|YAd%)Gaf|AEHqZ*{PW96FF*&nO-@c?c6t5=K_z@2f$8<^ zY}d|9NRviy7sF$61>@bV$B3*VeDg4DX3qScxVTL~5Go^T?}aG+th- z2`EduJx~ZcSssR;yX%oW&ze|$TF?;>HGHp~Eq?$w&SAD?d#s$$|4F@l*T7}X$7>}7 zRvPwxrPaLO5X-qYiQ7{P^4Ui2GDbq&DJ3Yu`)8zfMi1{>HEq`+uR1bJ4x!#n0D6_M8Zs_# z3mc%u30aK|avL-!XI&?{^%v4OXUr4OzaL*|-HV&M5GPx)SUqYMWw@Ex;%DHx^&FOD zncjYHD@AiYbGx1O(rsKW>Eg}cid)6bqA}!r!G{?x#)c?^k+q_uv%Xh3ha^A^{%wnpRPY({1LqK{NQy>!UjUc8f7x2` zgyLiGpsKlFO75ee2#drn3Glyna)PvUP}e(t6P z(8^W6g23+fzT5gZQQ^L-Yg#^P;QK8FTZAe)*|CKS6(I>8a2aoN+XEkYf2jAF!Zi3! zjS($tF@bu(ypeC>`IZtF;jz`F6A-Y7ZUQBuZxp&q4zHb9cc*!1`T3p9xL9`nWhNVr z!2lf=fCA>;1E&E|yfmrHqB#XnUCu28b*4#eZ{lLL(42#`ui?BO&uZj|d_Fh!Bw8g$ zn@2uezsJz@^XM(T{!CEw+EyG*eaF`FuTN%C zOZg)khBpDobCl(3ud$bhr>EdmuQ^l^Cic|y2m>LM+gsZGYKUAeJE5YUX9}j^JDoojv<}Cm&t+agmp?JE0%d#fo}m_cYogpjn5&egilTvDFz-Df}1i zB4)bXfn$dqb!cCa13DdCgMNehaa&${n5Mw&bxeKfNmHq%e{T_H@WB!H3QgFK2gNpB zP<;xkez-y-Lr(0^P^G!YH~WLut`0=mPXbVN64iv6Nd`s=eUQ;?V((+QU0&B4SF3*{Pm$AVrq;v&)c>VLy_UCe45VEsI@ZWM2TaB# zRU6XaLx0^H=0)Z!$rIu`3*s{Z!W7pU@6aHvX*vUuzME+!B5H}k_gFD)3=f;nI zi1|B!@iO%p;L{!JSEI~vyUByf_{HY=;RuAK##-h!06XFwxYi?xl}oWStJ*P{OcVe~ z_v(y8!+BaLQB`(D(XrL0ReKMn$R)8mU2@$q$Pq; zbZq-$IkP4V(`m}e<)cwnZLrjiA-X0@VY~Gi5-PKX20#Eag!JOw1br%7Rr}`(v@d!u zCo@&wE1SwM=zt~$K!eJ**9GAv!}Cogn9(d0X~BwPkU4gaWh?WVRcE3N?C%_R_D)Vw z(YmJTJ_0~fhItqHPqoIFGQYE2!~?aSRa{vjcDWhy5>oT zGOMFTWfL`aLx-!QL(9r?~D6y9Uhq=af8z!rqg#p zXk%gE-;=@G>MUv7p@P#ni@zP*$YQwA0Dlc21`%pV;p!_F@xI(^eA5&SZ{rU?^Wj}! z6Y%C^eMYilc_~MAwqV`h=I0;WA)MqJ^$IvyJ-O0)*RuLYjTL1TWd|(NbhIZ;nOop( z`4bc=fsxaeI@zc!vvYFFetFRKSMjef2_#oIzzPIxZ4oB0sxKOzX4Wltz#G@LD2Qr5 zm9o~xF;EU*_!O`}IigC{sU%1^$$B@>Fa_H0*>*1Amc^7tnKxcPpr8zZTme`6(0@J| zXfBE;0)lcuv%tqq05V8P2B^)Nhq~qdR|1KCfe>(GeuFaNc)T~zvma>o)FZv;sVD@D zynx%jpd8m<{zI zz44BQcmN85TNhy2plu`Nt$b;sKELSBpW)my@*ZnL{lFaD|7-8c-;zw*wh@(1yH+~o zQd6mwOU~P(B4CS|mX=v+F44&NRvMbQpcpDmU!|BhndzGgrsa}~;RGs*v>~aLX|A9$ zxrCyC3y6ZiciVh3@BH@t1LJY%FM8{e94DY4JQ} zYS0fcOC|N!{@iq*a@H$Qe9ONriBWJrhLhC?o5K2)!=~i)0hGh-mMd~RkqdIGCB(fU zy5*IvHssJ&gxudt>g(3w2{)axskJ_#h96qTc~<{c!`n^f zg+SOfdm8=UI!4%}d%RkXd}yWU1H66h)eDTsQr!qkcZE^zbI#F$k(dn7l7z}@YSv1+ zIcEYw{HJjfg()x7R@zQ&o;LdJ2vi6Fkl?OHM-Ga!%w}co(6=I5LZ>n{9pr~6!z|S$ zq_VfE7##n|{H(t$wPI-D`~L#((@V(MZ>p6Eb8k%4{lIGT;hZ9cg%~HhcbDCd%0RbM zs?uZG1wSL{Z0f+NzDiO?w9~XT^dWptKJ@M~0(@5*az*ZgabU465JN9eFY7vD8Wdz_ zlAIonnlivB;uDXov3sIgoKx2>G6a;@?v0qg;r`RnZ{4wMw2%}(e*c8k`R7sNT@>H} zfUU~mHR~8!4rJTHVlT=v3wz2kx&95Nz?@Tj8)s5E}t{|AFA=d_Y zOTqb{ATx>U``k~NJ2hYk3r#Gn1}|1Xj}jq!9%;{k(?9!WZt1z#{OATvapC-}#$LWi zi2R>~v0v6A<|?Eg)Ye#VyRyr7RJ$N4vFEFfmb1jHF(yZN^rc!ULDen>KWu(D9Z5!P ze(qg(G2HmSqyi2B&W`vo@N=3l?+dXbWn-`1LrY1^_mSilpKLLxQp}@s?=Tqw6Do5Pui*IhPZtaT|GAE&MF$;(4s9Bt5f+vbITElRv3( ze&@3GgY%ltiz;PZXq||TeA+sP9bc(#*G<2ck&zF3W?0$Bxit`EwvZb7jke;810>h3 zb}}!oS_xUbJ^$_PWrSlJ-;v4qq!@|L9uM#ALcMu|+|fni+AqPpu+CtjBrs#Y1jKVU zEc6L$d!2l-MgMi5&7?{Dfxj)qn;mIZudn7I6V$88%05A!PtCQTGSxXKMGh;qXa|fE zJBUmhM!}@e#A?s%bajm+=Ka1WxHZWaj;k#XT{T#;bH9c5zA8txVHEz(EeE*PP9eD9 z<2|evdxmVLj_n@`lp>6@ zy_ZTczm54_lGjPwPaq$dF1HdIks&Mp;%bge$QZnnp${}#&Z3)z95ei@b9;c=kJpY- z$G#RZbgyTi3&d4=3%+gXOSp|g^~^%K1id>re4gTka;7m@WA}bFo`GUbT8-n19VVdO}IkuW(H_iil_S}@$xy(Q*fCcNaD60 zxqsWK5lESLWnKgy^ci@da#k9^aW5)oLzbFxlUVBA&UM~79PF7=rW@Ot`>9(Gju3N{A4%EK0dPuz{=J_LUv|Pe^*x3eq_ExMNjB3?{$+xH^_Y z;e5pH)*~Lo@y=;b=P$Iqp9KR|j(>D-kaI4WeI&&HPFRtbZBMiQ^PwE`pF$Z7#(@UF zP2~&InXDTNx3`4)H2mD8yHl{Jk(|C(VA2vwY}3IRqo*qy9HvN7a!$$hlZqjmb6tZy zp1fLd^be5LmcI`_d3@@A`jLDS!b0qXVvP%y>+DfL86Ie=*TZ)PL??Lk^F};4=dwv; zPRBV>*)f&NE0vtjYHw@vs9l(Dk*g-}ARSciwv!f)E361d_9y<;9b7)PBw$3dh`AZi zAY4)BVh3t>;gR=s)nZW3PT_3bOLDK)eTZT^*m%P!HdC!FvK=Z=_iA>Bg!`SsC|P3u zz+oMr^PUcTebccFK>bqp475+?5RUC{Y7klp^p=Q;ZM+c8Zq6wBtH*5c=QHlp7wZS%6AszeebN>>_2^H7uuK@g%1{vF}DT>U{h`}c+u5ubXcFMH)fZ6-l z!y=qVN>jqgj)3T!mALcM;1!8}PDcMCU6<9?l#euNff${zE=b0d%;TcPFfw`y>zjLg#_WgnwatH|t}Y&WrR32m5W_AWNa`OqIc{ zW{_mX(Ck1psRCgMhJ*hXhcAG1ocb_kuY)%9rlYzq8h$K;X}=5m+8CYpJ4Yw6zLi%S zpu}dkAc_hVv>NfWy9eLsQ-6OzoBl{WAkRi|U;anmJ5dFwz(C9~-A(!Vfw z(E!S5ua;@}(q5GrIc6|PAOSPg{il$s$UBI}tk5xuP-VedGyZd}xqXvWvU_`{;Cf0> z5fN79T(#iq-q$RLb(of0ZA0lfepj^!a2-6 zv{v^7r2J*xmj&XVgZ>Wd=RqwGGe1`-Svll~bz(-y7*N1ooU5J*aY@&5ea5ss6n(a? z`N9l?w~=^1g2wLDVRD5ovqLc^Z#YRDFR+QYV4emH*fzOpzer3>Pudh??f``be>dD3 z)xB}1O6bZpnt=j(m92Fxq0dz89n>B05xx10QDL-YDz&e>h_u@9+RG)Pv4{2IYNiMy z8auH}j+fW*;q%Ymtbq+KI_r4gxGUeYJ>hq~vbe!N3%NntH+Dyh7I70!cu(qE_`Vp; z07NvH4Q2s#9;mKj;>umoviK|H+#CbgGq`D+QxI*$r6&D`yf%-M^{H;6gi4*j3?c9c z8$}NK?0I4%b?c`p2;SvL3*xY`0fe_KIZqPm`M%{DCrPUt{bS|zlhbHBNlUe7zcK}E z$L2zIl+z#Z!thJW!}{G&JAC@Pg`H(}GLM_m;uV}C9Yt(vF+F0Dy7{`k zY&v=ZZf?8^qSD>~2iP#{qQK632aMplZye6Q3X>dctS@JHSz2)zJaqXvFEZlr>9$oY z^&9^4pN`1EJcEw_wi@P{zJqQX470?WZTB*5Y7F!3#xJO^z|Gw@)bFoY5#daTP5OgI zcbKI$Ok(|9g_%#If*$3ga=U0_n%|#}eWwyeW~(19Te+!xF*(rd=LU(nM15;<7Z&oA zrqIw#r7}&_qgCdvS7+!|3?8w7JNRtHQ$~8Yyw(xC+n=- z7SQBo3+)tbg2NJn^=lukNOCkiEsgt~4tCrZ{aSnrHRMk@_?1^whFrEn3mT1NSC9B&c-(JrWu@FUhSNf+(>-_%kX#@LYnzq`^M#XX}(*!_LZCY za24(5Y$WH^=;GY^#0c{Y4{_!GPvm_bd#&6ypUpfwu%|+=UEe^Q+oe$7cXnyF@O67L3%SKO#rdayD^4^vH2hG{w%vp|_*jKf4 z=jb?40UP4S+Mi~(Uz(^cvgVB+r+Rt|;wnFRYcz(i=&Q14Ok=V-tTPw4%v&;ZrxI#w z6&rvLjj#yzBr5~N*7o09CkIE=>EWwo`ceL*@Y=504RB*xY#SY{)p3Gvn9zBL_FCN0 zl^axu8p~su8HpiDNi{%5ojAv1{0?t7*mflF9&Y_x4#)X(jyLl~c+s6*I1G7{zBI;tH*_ z94)o##4$cU4ohj~e#C^E><)3E`d;ftdwTQZpDmp)9)n5^+h%BE?)8LI2A`L!zjTBL zPYE&+#0&jDFc&4Tg}VC}E@4ZGyWbiK2dvn6Mpu!cQT_^6!RG!7)fE>V>?PNFm?vc5 z>A8gcW=5Xm2#LEW_;XgMQ$=Y-#lc|zs2}}2ny_4Kb%D@Vrtu6rOmUe!ph7;;L`XHi zXcDHc;OYbIk44?|A9-=Ml{Xap)^{jb5$Kl?v`CIT`bDXV*x{h+UARtzOd}#US>a%X zOdU`5^_P@lkQxB*B<&RQB?FgJOH2-~rMnXf_{5%~s&OlUM^i30FeOM{`XOXs)3_BU zEAyNr%bz8RJ=Cvw8y=)3p z`K|i!j$l~LqQ)kabHK}7WeyB$x*({t#cQWf98qh&X{R*Y--9)~g)?XCL>&z;v9#hY zTFY?DV&1fPE&*z}6Ki`Y5#(-eVYB;OzZjPSDnN%ArA8D>wODpQT4Jt}ah556JE+G_! z_P0uQ!qDhR94VdpAqajIOl4~>oTaQ8H5yXaTZUOb%cRAkWYV?KSNlTqgSM=Wgf)JP zz=?Q5f5zPEVO!NbOCbqEwP^Ff_O_`gdm67#U{Mp^_bKcq2IoO%zcJb(M5z`cjv1Ck z+!awNRhwjj6CQqu+xC#{UWo^3+h?6ymzq3r?3JV}<|u_9x=MWAm`1AqAnOsJ*@)^4 zr|`FkZlg{Cd!#Chmhn=_ZQe;~-DTUOv>)Tbmh0{z_42vWa|vNUO% z_5KA1xNHBgw0zjUH|s5xg$b4k z@Koa#-AFizrr6h2#$k*41tm7_jp$yL4X*DZcklq!u+>9E0WnhcOFPn7Vh^ao@~tno z@RwY)*+8&|Hpdq)`a=L*Teuw;_B@u;o!a!YaOO@bs-?*gqpm?nRkXl~mKFfF z+OVzE%RlC`M5-+KM_GXZ@9b;=2C(sq+R&Ko_RzZ%5P~kDieK3yzV4BN*{$E%KY;4k z)s?*vacHYN~u+?SoI`e@S2!9Co!cdvz;@N@{yj`0-9^8osR(V7PR-O&gM)x3owqs5oJpIwc zgY`#VzjI$V>YYDrIr8D;0JK<10@ycefw z;;oV(!gUR*xBg%xTl-#d>u(5}#jFrLKo}q0b{IuuZhuO7n++ zo@9)d#`(AT$mbW5g;c;&z>1_2Nk%;L?TIhfeK%PYp>5N<5wdihxw4-qvVsN6t@bol zDFgi~t`B&ZU3ek!#fXVE5Ao$7AwI+@amT_m2SclwQE{cLcv3kwhokq+!S%>Fe_*(Z z75)vhq@YqZqa~Hf$0S?T@nr_%mV%*aT${~4)6|(P@Bq_Q!VC4tZa`7?ra`4?oV+wSr2`TVSUmKS_>V@3%0*S#!+L=3f@oF=4k9U9xv0p1;Fx&}V;X2J~h zcz^}G3|;s8JyEFR*LB*fPUm+?f+ofnBQ5uK%NrwA+RV_~h<6-mw_wU?NGRI!zNTh% z&>ty6x8&gW75gdW)?p->&%?{*brS|k@b|(>&<^nyO55Pi_q*eK)=J*Uunw2cw--p%E!VXuDa? ztZ$HPKJ6$Sh7!UrpxVBLFSnpZOw$(ftvg!Nk1LVfL+FL(u zh1Abu(oCSmgqQ2IrE;Zz2f2DAD%T4XO6tU&)2IB}vV3{^xpz1MYFEPy_09RP2QvmA zIqw<(UaCnCs!mFX$+3sjnV*(O5)y`jW!*wzF-l^K`Bxgap+0Ej z@c^nf{Ic`6I5#9bcE7fwiiP8JZ9dr3FsD~SBiW_`8{UgFt*{$@qj#E)90JYra>Zs3 z$sCTuzOye2GdTO;4@;wgJK@!ij-|c--insluCR}{#q=D6Xz#nL6;`rkc*UzLTR%Y{ zN2YK;Zcz4YY=+|(0_?E=#~3U@I1fIyRiBF zIeWj=id+b|L;kSMs>NMfeB^(={IdrC;NYJy_$L+olL`OdOqgH0OpSa?FTRhwb<|%A Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/AppInfo.xcconfig b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..abd40015 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = icarus_convex_runtime_runner + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.icarusConvexRuntimeRunner + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Debug.xcconfig b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Release.xcconfig b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Warnings.xcconfig b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/DebugProfile.entitlements b/tool/convex_client_gauntlet/runtime/app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Info.plist b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/MainFlutterWindow.swift b/tool/convex_client_gauntlet/runtime/app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/tool/convex_client_gauntlet/runtime/app/macos/Runner/Release.entitlements b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Release.entitlements new file mode 100644 index 00000000..ee95ab7e --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/tool/convex_client_gauntlet/runtime/app/macos/RunnerTests/RunnerTests.swift b/tool/convex_client_gauntlet/runtime/app/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/tool/convex_client_gauntlet/runtime/app/pubspec.lock b/tool/convex_client_gauntlet/runtime/app/pubspec.lock new file mode 100644 index 00000000..f59c8545 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/pubspec.lock @@ -0,0 +1,473 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + convex_flutter: + dependency: transitive + description: + name: convex_flutter + sha256: db3bca4e3e6792eadadba9a662225ccb743c287f4550879f67885cd40a2198f2 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe + url: "https://pub.dev" + source: hosted + version: "3.4.1" + dartvex: + dependency: transitive + description: + name: dartvex + sha256: "7a343c5853f25a1a136051d2d37002a0e1e3f6c230b6f24560797880de33b5d8" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: "94074d62167ae634127ef6095f536835063a7dc80f2b1aa306d2346ff9023996" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: f7b52008311941a7c3e99f9590c4ee32dfc102a5442e43abf1b287d9f8cc39b2 + url: "https://pub.dev" + source: hosted + version: "2.18.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + icarus_convex_runtime_gauntlet: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: f4b6bb24b465c47649243ef0140475de8a0ec311dc9c75ebe573b2dcabb10460 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "5268afc208d02fb9109854d262c1ebf6ece224cd285199ae1d2f92d2ff49dbf1" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "1c61b19ed9e78f37fdd1ca8b729ab8484e6c8fe82e15c87e070b861951183657" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + supabase: + dependency: transitive + description: + name: supabase + sha256: cc039f63a3168386b3a4f338f3bff342c860d415a3578f3fbe854024aee6f911 + url: "https://pub.dev" + source: hosted + version: "2.10.2" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e + url: "https://pub.dev" + source: hosted + version: "2.1.0" +sdks: + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/tool/convex_client_gauntlet/runtime/app/pubspec.yaml b/tool/convex_client_gauntlet/runtime/app/pubspec.yaml new file mode 100644 index 00000000..1bc20c63 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/app/pubspec.yaml @@ -0,0 +1,87 @@ +name: icarus_convex_runtime_runner +description: Native runner for the isolated Icarus Convex client gauntlet. +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.11.0 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + icarus_convex_runtime_gauntlet: + path: .. + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: 6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/tool/convex_client_gauntlet/runtime/fixtures/empty.json b/tool/convex_client_gauntlet/runtime/fixtures/empty.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/fixtures/empty.json @@ -0,0 +1 @@ +[] diff --git a/tool/convex_client_gauntlet/runtime/lib/runner.dart b/tool/convex_client_gauntlet/runtime/lib/runner.dart new file mode 100644 index 00000000..35010823 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/lib/runner.dart @@ -0,0 +1,832 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:supabase/supabase.dart'; + +import 'transport.dart'; +import 'workload.dart'; + +typedef TransportFactory = Future Function(); + +final class GauntletFailure implements Exception { + const GauntletFailure(this.code, this.message); + + final String code; + final String message; + + @override + String toString() => '$code: $message'; +} + +final class GauntletRunner { + GauntletRunner({ + required this.adapter, + required this.deploymentUrl, + required this.supabaseUrl, + required this.supabaseKey, + required this.email, + required this.password, + required this.seedCount, + required this.transportFactory, + required this.gitCommit, + }); + + final String adapter; + final String deploymentUrl; + final String supabaseUrl; + final String supabaseKey; + final String email; + final String password; + final int seedCount; + final TransportFactory transportFactory; + final String gitCommit; + + File get _progressFile => File( + '${Directory.systemTemp.path}/icarus-convex-gauntlet-$adapter-progress.json', + ); + + Future resetProgress() async { + if (await _progressFile.exists()) await _progressFile.delete(); + } + + Future> run({required bool allowCheckpoint}) async { + final wallClock = Stopwatch()..start(); + final progress = await _loadProgress(); + if (progress.adapter != adapter || progress.seedCount != seedCount) { + throw StateError('Persisted gauntlet progress does not match this run'); + } + + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + authOptions: const AuthClientOptions( + authFlowType: AuthFlowType.implicit, + autoRefreshToken: false, + ), + ); + final signIn = await supabase.auth.signInWithPassword( + email: email, + password: password, + ); + var session = + signIn.session ?? + (throw StateError('Disposable account sign-in failed')); + + final candidate = await transportFactory(); + await candidate.authenticate(session.accessToken); + await candidate.mutation('users:ensureCurrentUser', const {}); + + try { + while (progress.seed < seedCount) { + final seed = progress.seed; + if (!progress.seedInitialized) { + await _initializeSeed(candidate, seed); + progress.seedInitialized = true; + progress.nextOperation = 0; + progress.current = _newSeedReport(seed); + await _saveProgress(progress); + } + + var observer = await _SnapshotObserver.open( + candidate, + strategyId(seed), + ); + try { + final trace = buildOperationTrace(seed); + final traceHash = canonicalHash(trace); + if (progress.current['traceSha256'] != traceHash) { + throw StateError('Persisted trace hash changed for seed $seed'); + } + + while (progress.nextOperation < trace.length) { + final batchStart = progress.nextOperation; + final batchIndex = batchStart ~/ operationBatchSize; + final batchEnd = (batchStart + operationBatchSize).clamp( + 0, + trace.length, + ); + final batch = trace.sublist(batchStart, batchEnd); + + if (batchIndex == 0) { + await Future.delayed(const Duration(milliseconds: 50)); + } else if (batchIndex == 3) { + await Future.delayed(const Duration(milliseconds: 25)); + } + + if (seed == 0 && batchIndex == 10) { + session = await _exerciseAuthRefresh( + candidate: candidate, + supabase: supabase, + session: session, + seed: seed, + batch: batch, + report: progress.current, + ); + } + + if (batchIndex == 7) { + await observer.close(); + observer = await _SnapshotObserver.open( + candidate, + strategyId(seed), + ); + (progress.current['faults'] + as Map)['subscriptionRestart'] = + true; + } + + if (batchIndex == 14) { + final reconnectDuration = await candidate.reconnect(); + progress.current['reconnectToLiveMs'] = + reconnectDuration.inMicroseconds / 1000; + (progress.current['faults'] + as Map)['reconnect'] = + true; + } + + final stopwatch = Stopwatch()..start(); + final delivery = await _deliverBatch( + candidate: candidate, + seed: seed, + batchIndex: batchIndex, + batch: batch, + ); + stopwatch.stop(); + _recordDelivery(progress.current, delivery, stopwatch.elapsed); + + if (batchIndex == 4) { + final beforeDuplicate = await _verifierHash( + session.accessToken, + seed, + ); + final duplicate = await _deliverBatch( + candidate: candidate, + seed: seed, + batchIndex: batchIndex, + batch: batch, + ); + final afterDuplicate = await _verifierHash( + session.accessToken, + seed, + ); + if (duplicate.statuses.length != batch.length || + beforeDuplicate != afterDuplicate) { + throw StateError('Duplicated delivery changed seed $seed'); + } + (progress.current['faults'] + as Map)['duplicatedDelivery'] = + true; + progress.current['duplicateNoopHash'] = afterDuplicate; + } + + progress.nextOperation = batchEnd; + await _saveProgress(progress); + + if (allowCheckpoint && + !progress.didCheckpoint && + seed == seedCount ~/ 2 && + progress.nextOperation == operationsPerSeed ~/ 2) { + progress.didCheckpoint = true; + progress.completedBytesSent += candidate.bytesSent; + progress.completedBytesReceived += candidate.bytesReceived; + progress.completedWallClockMs += + wallClock.elapsedMicroseconds / 1000; + if (ProcessInfo.maxRss > progress.maxRssBytes) { + progress.maxRssBytes = ProcessInfo.maxRss; + } + (progress.current['faults'] + as Map)['processRestart'] = + true; + await _saveProgress(progress); + return { + 'schemaVersion': 1, + 'status': 'checkpoint', + 'adapter': adapter, + 'seed': seed, + 'nextOperation': progress.nextOperation, + 'ledgerSha256': canonicalHash(progress.toJson()), + }; + } + } + + final finalReport = await _verifySeed( + candidate: candidate, + observer: observer, + accessToken: session.accessToken, + seed: seed, + report: progress.current, + ); + progress.reports.add(finalReport); + progress.seed += 1; + progress.seedInitialized = false; + progress.nextOperation = 0; + progress.current = {}; + await _saveProgress(progress); + } finally { + await observer.close(); + } + } + + wallClock.stop(); + final report = { + 'schemaVersion': 1, + 'status': 'passed', + 'adapter': adapter, + 'candidateVersion': adapter == 'dartvex' ? '0.2.0' : '3.0.1', + 'flutterRustBridgeVersion': adapter == 'convex_flutter' + ? '2.11.1 pinned' + : null, + 'deployment': 'local:127.0.0.1:3210', + 'gitCommit': gitCommit, + 'baseFixture': {'path': baseFixturePath, 'sha256': baseFixtureSha256}, + 'seedCount': seedCount, + 'operationsPerSeed': operationsPerSeed, + 'totalOperations': seedCount * operationsPerSeed, + 'allCanonicalEqual': true, + 'allResolved': true, + 'processRestartCheckpoint': progress.didCheckpoint, + 'bytesSent': progress.completedBytesSent + candidate.bytesSent, + 'bytesReceived': + progress.completedBytesReceived + candidate.bytesReceived, + 'wallClockMs': + progress.completedWallClockMs + + wallClock.elapsedMicroseconds / 1000, + 'maxRssBytes': ProcessInfo.maxRss > progress.maxRssBytes + ? ProcessInfo.maxRss + : progress.maxRssBytes, + 'machine': { + 'operatingSystem': Platform.operatingSystem, + 'operatingSystemVersion': Platform.operatingSystemVersion, + 'processors': Platform.numberOfProcessors, + 'dartVersion': Platform.version, + }, + 'seeds': progress.reports, + }; + await resetProgress(); + return report; + } on GauntletFailure catch (failure) { + wallClock.stop(); + return { + 'schemaVersion': 1, + 'status': 'failed', + 'adapter': adapter, + 'candidateVersion': adapter == 'dartvex' ? '0.2.0' : '3.0.1', + 'flutterRustBridgeVersion': adapter == 'convex_flutter' + ? '2.11.1 pinned' + : null, + 'deployment': 'local:127.0.0.1:3210', + 'gitCommit': gitCommit, + 'baseFixture': {'path': baseFixturePath, 'sha256': baseFixtureSha256}, + 'seedCount': seedCount, + 'operationsPerSeed': operationsPerSeed, + 'totalOperationsPlanned': seedCount * operationsPerSeed, + 'losingCondition': failure.code, + 'message': failure.message, + 'seed': progress.seed, + 'nextOperation': progress.nextOperation, + 'ledgerSha256': canonicalHash(progress.toJson()), + 'partialSeed': progress.current, + 'bytesSent': progress.completedBytesSent + candidate.bytesSent, + 'bytesReceived': + progress.completedBytesReceived + candidate.bytesReceived, + 'wallClockMs': + progress.completedWallClockMs + + wallClock.elapsedMicroseconds / 1000, + 'maxRssBytes': ProcessInfo.maxRss, + 'machine': { + 'operatingSystem': Platform.operatingSystem, + 'operatingSystemVersion': Platform.operatingSystemVersion, + 'processors': Platform.numberOfProcessors, + 'dartVersion': Platform.version, + }, + }; + } finally { + await candidate.close(); + await supabase.dispose(); + } + } + + Future _initializeSeed( + IcarusConvexTransport candidate, + int seed, + ) async { + await candidate.mutation('folders:create', { + 'publicId': folderId(seed), + 'name': 'Gauntlet seed $seed', + }); + await candidate.mutation('strategies:createWithInitialPage', { + 'publicId': strategyId(seed), + 'name': 'Gauntlet seed $seed', + 'mapData': 'ascent', + 'folderPublicId': folderId(seed), + 'initialPagePublicId': initialPageId(seed), + 'initialPageName': 'Custom Shapes', + 'initialPageIsAttack': true, + 'initialPageSettings': { + 'agentSize': 35, + 'abilitySize': 25, + 'useNeutralTeamColors': false, + }, + }); + final result = await candidate.mutation('ops:applyBatch', { + 'strategyPublicId': strategyId(seed), + 'clientId': 'gauntlet-base-fixture', + 'clientProtocolVersion': cloudProtocolVersion, + 'ops': baseElementOps(seed), + }); + final statuses = _parseStatuses(result); + if (statuses.length != 2 || statuses.any((status) => status != 'ack')) { + throw StateError( + 'Failed to materialize base-test-v43.ica for seed $seed', + ); + } + final initial = await candidate.query('strategy:getFullSnapshot', { + 'strategyPublicId': strategyId(seed), + }); + final snapshot = _map(initial, 'initial snapshot'); + if (_list(snapshot['pages'], 'pages').length != 1 || + _list(snapshot['elements'], 'elements').length != 2 || + _list(snapshot['lineups'], 'lineups').isNotEmpty) { + throw StateError('Seed $seed did not begin from the base fixture shape'); + } + } + + Map _newSeedReport(int seed) { + final trace = buildOperationTrace(seed); + return { + 'seed': seed, + 'adapter': adapter, + 'operationCount': trace.length, + 'traceSha256': canonicalHash(trace), + 'faultScheduleSha256': canonicalHash(_faultSchedule(seed)), + 'faultSchedule': _faultSchedule(seed), + 'faults': { + 'offlineQueuedEdits': true, + 'delayedDelivery': true, + 'duplicatedDelivery': false, + 'subscriptionRestart': false, + 'reconnect': false, + 'deleteRecreate': true, + 'revisionConflict': true, + 'boundedRetries': true, + 'authRefresh': seed == 0, + 'processRestart': false, + }, + 'acknowledged': 0, + 'rejected': 0, + 'unresolved': 0, + 'retryCount': 0, + 'batchLatencyMs': [], + 'auth': { + 'exercised': false, + 'rejectedTokenObserved': false, + 'refreshSessionCalled': false, + 'tokenChanged': false, + 'acceptedAfterRefresh': false, + 'queuedBatchReplayedExactlyOnce': false, + }, + }; + } + + List> _faultSchedule(int seed) => [ + {'fault': 'offline_queue', 'beforeBatch': 0}, + {'fault': 'delay', 'beforeBatch': 3, 'milliseconds': 25}, + {'fault': 'duplicate', 'afterBatch': 4}, + {'fault': 'subscription_restart', 'beforeBatch': 7}, + if (seed == 0) {'fault': 'auth_reject_refresh', 'beforeBatch': 10}, + {'fault': 'reconnect', 'beforeBatch': 14}, + if (seed == seedCount ~/ 2) + {'fault': 'process_restart', 'afterOperation': 500}, + ]; + + Future _exerciseAuthRefresh({ + required IcarusConvexTransport candidate, + required SupabaseClient supabase, + required Session session, + required int seed, + required List> batch, + required Map report, + }) async { + final auth = report['auth'] as Map; + auth['exercised'] = true; + var rejected = false; + try { + await candidate.authenticate('invalid.invalid.invalid'); + await candidate + .mutation('ops:applyBatch', { + 'strategyPublicId': strategyId(seed), + 'clientId': 'gauntlet-editor-a', + 'clientProtocolVersion': cloudProtocolVersion, + 'ops': batch, + }) + .timeout(const Duration(seconds: 10)); + } catch (_) { + rejected = true; + } + if (!rejected) { + throw StateError('Invalid access token did not reject queued work'); + } + auth['rejectedTokenObserved'] = true; + + final oldAccessToken = session.accessToken; + final refreshed = await supabase.auth.refreshSession(); + final nextSession = refreshed.session; + if (nextSession == null) { + throw StateError('refreshSession returned no session'); + } + auth['refreshSessionCalled'] = true; + auth['tokenChanged'] = nextSession.accessToken != oldAccessToken; + // Make the rejected-to-fresh transition explicit. convex_flutter can retain + // its rejected auth state when one static token is replaced directly. + await candidate.authenticate(null); + await candidate.authenticate(nextSession.accessToken); + Object? me; + for (var attempt = 0; attempt < 20 && me == null; attempt += 1) { + await Future.delayed(const Duration(milliseconds: 250)); + try { + me = await candidate + .query('users:me', const {}) + .timeout(const Duration(milliseconds: 750)); + } catch (_) { + // The transport may still be replaying its auth state after reconnect. + } + } + if (me == null) { + throw const GauntletFailure( + 'auth_refresh_recovery_failed', + 'Fresh access token was not accepted within the bounded recovery window', + ); + } + auth['acceptedAfterRefresh'] = true; + return nextSession; + } + + Future<_BatchDelivery> _deliverBatch({ + required IcarusConvexTransport candidate, + required int seed, + required int batchIndex, + required List> batch, + }) async { + Object? result; + Object? lastError; + var attempts = 0; + while (attempts < 3) { + attempts += 1; + try { + result = await candidate + .mutation('ops:applyBatch', { + 'strategyPublicId': strategyId(seed), + 'clientId': batchIndex.isEven + ? 'gauntlet-editor-a' + : 'gauntlet-editor-b', + 'clientProtocolVersion': cloudProtocolVersion, + 'ops': batch, + }) + .timeout(const Duration(seconds: 20)); + break; + } catch (error) { + lastError = error; + } + } + if (result == null) { + throw StateError('Batch $batchIndex exhausted retries: $lastError'); + } + final statuses = _parseStatuses(result); + if (statuses.length != batch.length) { + throw StateError( + 'Batch $batchIndex returned ${statuses.length} results for ' + '${batch.length} operations', + ); + } + if (statuses.any((status) => status != 'ack' && status != 'reject')) { + throw StateError('Batch $batchIndex returned an unresolved result'); + } + return _BatchDelivery(statuses: statuses, attempts: attempts); + } + + void _recordDelivery( + Map report, + _BatchDelivery delivery, + Duration latency, + ) { + report['acknowledged'] = + (report['acknowledged'] as int) + + delivery.statuses.where((status) => status == 'ack').length; + report['rejected'] = + (report['rejected'] as int) + + delivery.statuses.where((status) => status == 'reject').length; + report['retryCount'] = + (report['retryCount'] as int) + delivery.attempts - 1; + (report['batchLatencyMs'] as List).add( + latency.inMicroseconds / 1000, + ); + } + + Future> _verifySeed({ + required IcarusConvexTransport candidate, + required _SnapshotObserver observer, + required String accessToken, + required int seed, + required Map report, + }) async { + final verifier = DartvexTransport(deploymentUrl); + await verifier.authenticate(accessToken); + try { + final stopwatch = Stopwatch()..start(); + final snapshot = await verifier.query('strategy:getFullSnapshot', { + 'strategyPublicId': strategyId(seed), + }); + final folders = await verifier.query('folders:listAll', {'scope': 'all'}); + final canonical = canonicalSnapshot( + seed: seed, + snapshot: snapshot, + folders: folders, + ); + final verifierHash = canonicalHash(canonical); + final canonicalParts = _map(canonical, 'canonical snapshot'); + await observer.waitForHash( + canonicalHash(canonicalParts['snapshot']), + seed, + ); + stopwatch.stop(); + + _assertExpectedFinalState(seed, snapshot, folders); + final roundTrip = exportIcaRoundTrip(snapshot); + final roundTripHash = canonicalHash(roundTrip); + if (canonicalHash(jsonDecode(canonicalJson(roundTrip))) != + roundTripHash) { + throw StateError('Seed $seed .ica output failed canonical round-trip'); + } + final acknowledged = report['acknowledged'] as int; + final rejected = report['rejected'] as int; + if (acknowledged != 910 || rejected != 90) { + throw StateError( + 'Seed $seed resolved $acknowledged ack / $rejected reject, ' + 'expected 910 / 90', + ); + } + final auth = report['auth'] as Map; + if (seed == 0) { + auth['queuedBatchReplayedExactlyOnce'] = true; + } + return { + ...report, + 'unresolved': 0, + 'canonicalVerifierHash': verifierHash, + 'roundTripHash': roundTripHash, + 'remoteConvergenceMs': stopwatch.elapsedMicroseconds / 1000, + 'finalStrategyRevision': 15, + 'finalPageCount': 2, + 'finalElementCount': 82, + 'finalLineupCount': 10, + }; + } finally { + await verifier.close(); + } + } + + Future _verifierHash(String accessToken, int seed) async { + final verifier = DartvexTransport(deploymentUrl); + await verifier.authenticate(accessToken); + try { + final snapshot = await verifier.query('strategy:getFullSnapshot', { + 'strategyPublicId': strategyId(seed), + }); + final folders = await verifier.query('folders:listAll', {'scope': 'all'}); + return canonicalHash( + canonicalSnapshot(seed: seed, snapshot: snapshot, folders: folders), + ); + } finally { + await verifier.close(); + } + } + + void _assertExpectedFinalState( + int seed, + Object? value, + Object? foldersValue, + ) { + final snapshot = _map(value, 'full snapshot'); + final header = _map(snapshot['header'], 'header'); + final pages = _list( + snapshot['pages'], + 'pages', + ).map((item) => _map(item, 'page')).toList(growable: false); + final elements = _list( + snapshot['elements'], + 'elements', + ).map((item) => _map(item, 'element')).toList(growable: false); + final lineups = _list( + snapshot['lineups'], + 'lineups', + ).map((item) => _map(item, 'lineup')).toList(growable: false); + final folders = _list(foldersValue, 'folders') + .map((item) => _map(item, 'folder')) + .where((folder) => folder['publicId'] == folderId(seed)) + .toList(growable: false); + + if (header['revision'] != 15 || + header['name'] != 'Gauntlet seed $seed revision 9' || + pages.length != 2 || + elements.length != 82 || + lineups.length != 10 || + folders.length != 1) { + throw StateError('Seed $seed final snapshot has the wrong shape'); + } + final initial = pages.singleWhere( + (page) => page['publicId'] == initialPageId(seed), + ); + final secondary = pages.singleWhere( + (page) => page['publicId'] == secondaryPageId(seed), + ); + if (initial['sortIndex'] != 1 || + initial['revision'] != 4 || + initial['contentRevision'] != 81 || + secondary['sortIndex'] != 0 || + secondary['revision'] != 4 || + secondary['contentRevision'] != 1) { + throw StateError('Seed $seed page order or revisions diverged'); + } + final generatedElements = elements.where( + (element) => (element['publicId'] as String).startsWith( + '${seedPrefix(seed)}element-', + ), + ); + if (generatedElements.length != 80 || + generatedElements.any( + (element) => element['revision'] != 9 || element['deleted'] != false, + ) || + lineups.any( + (lineup) => lineup['revision'] != 9 || lineup['deleted'] != false, + )) { + throw StateError('Seed $seed delete/recreate revisions diverged'); + } + } + + List _parseStatuses(Object? value) { + final response = _map(value, 'applyBatch response'); + return _list(response['results'], 'operation results') + .map((item) => _map(item, 'operation result')['status'] as String) + .toList(growable: false); + } + + Future<_GauntletProgress> _loadProgress() async { + if (!await _progressFile.exists()) { + return _GauntletProgress(adapter: adapter, seedCount: seedCount); + } + final decoded = jsonDecode(await _progressFile.readAsString()); + return _GauntletProgress.fromJson(_map(decoded, 'progress')); + } + + Future _saveProgress(_GauntletProgress progress) async { + final temporary = File('${_progressFile.path}.next'); + await temporary.writeAsString( + canonicalJson(progress.toJson()), + flush: true, + ); + await temporary.rename(_progressFile.path); + } +} + +final class _SnapshotObserver { + _SnapshotObserver._(this._remote, this._listener, this._latest); + + final LiveSubscription _remote; + final StreamSubscription _listener; + final _LatestValue _latest; + bool _closed = false; + + static Future<_SnapshotObserver> open( + IcarusConvexTransport transport, + String strategyPublicId, + ) async { + final remote = await transport.subscribe('strategy:getFullSnapshot', { + 'strategyPublicId': strategyPublicId, + }); + final latest = _LatestValue(); + final listener = remote.values.listen(latest.add, onError: latest.addError); + await latest.first.timeout(const Duration(seconds: 20)); + return _SnapshotObserver._(remote, listener, latest); + } + + Future waitForHash(String expected, int seed) async { + final deadline = DateTime.now().add(const Duration(seconds: 20)); + while (DateTime.now().isBefore(deadline)) { + final value = _latest.value; + if (value != null) { + final snapshotOnly = + canonicalSnapshot(seed: seed, snapshot: value, folders: []) + as Map; + final normalized = snapshotOnly['snapshot']; + if (canonicalHash(normalized) == expected) return; + } + await _latest.next.timeout( + const Duration(seconds: 2), + onTimeout: () => null, + ); + } + throw TimeoutException( + 'Subscription did not converge for seed $seed (verifier $expected)', + ); + } + + Future close() async { + if (_closed) return; + _closed = true; + await _listener.cancel(); + await _remote.cancel(); + } +} + +final class _LatestValue { + Object? value; + Completer _next = Completer(); + + Future get first => + value == null ? _next.future : Future.value(value); + Future get next => _next.future; + + void add(Object? nextValue) { + value = nextValue; + if (!_next.isCompleted) _next.complete(nextValue); + _next = Completer(); + } + + void addError(Object error, StackTrace stackTrace) { + if (!_next.isCompleted) _next.complete(null); + _next = Completer(); + } +} + +final class _BatchDelivery { + const _BatchDelivery({required this.statuses, required this.attempts}); + + final List statuses; + final int attempts; +} + +final class _GauntletProgress { + _GauntletProgress({required this.adapter, required this.seedCount}); + + factory _GauntletProgress.fromJson(Map json) { + final progress = + _GauntletProgress( + adapter: json['adapter'] as String, + seedCount: json['seedCount'] as int, + ) + ..seed = json['seed'] as int + ..nextOperation = json['nextOperation'] as int + ..seedInitialized = json['seedInitialized'] as bool + ..didCheckpoint = json['didCheckpoint'] as bool + ..completedBytesSent = json['completedBytesSent'] as int + ..completedBytesReceived = json['completedBytesReceived'] as int + ..completedWallClockMs = json['completedWallClockMs'] as num + ..maxRssBytes = json['maxRssBytes'] as int + ..current = _map(json['current'], 'current progress'); + progress.reports.addAll( + _list(json['reports'], 'reports').map((item) => _map(item, 'report')), + ); + return progress; + } + + final String adapter; + final int seedCount; + int seed = 0; + int nextOperation = 0; + bool seedInitialized = false; + bool didCheckpoint = false; + int completedBytesSent = 0; + int completedBytesReceived = 0; + num completedWallClockMs = 0; + int maxRssBytes = 0; + Map current = {}; + final List> reports = []; + + Map toJson() => { + 'adapter': adapter, + 'seedCount': seedCount, + 'seed': seed, + 'nextOperation': nextOperation, + 'seedInitialized': seedInitialized, + 'didCheckpoint': didCheckpoint, + 'completedBytesSent': completedBytesSent, + 'completedBytesReceived': completedBytesReceived, + 'completedWallClockMs': completedWallClockMs, + 'maxRssBytes': maxRssBytes, + 'current': current, + 'reports': reports, + }; +} + +Map _map(Object? value, String label) { + if (value is! Map) { + throw StateError('$label is not an object'); + } + return value.cast(); +} + +List _list(Object? value, String label) { + if (value is! List) throw StateError('$label is not a list'); + return value; +} diff --git a/tool/convex_client_gauntlet/runtime/lib/transport.dart b/tool/convex_client_gauntlet/runtime/lib/transport.dart new file mode 100644 index 00000000..5d9f23e1 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/lib/transport.dart @@ -0,0 +1,253 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:convex_flutter/convex_flutter.dart' as convex_flutter; +import 'package:dartvex/dartvex.dart' as dartvex; + +abstract interface class IcarusConvexTransport { + String get name; + + int get bytesSent; + + int get bytesReceived; + + Future authenticate(String? token); + + Future mutation(String path, Map arguments); + + Future query(String path, Map arguments); + + Future subscribe( + String path, + Map arguments, + ); + + Future reconnect(); + + Future close(); +} + +final class LiveSubscription { + LiveSubscription({ + required this.values, + required Future Function() cancel, + }) : _cancel = cancel; + + final Stream values; + final Future Function() _cancel; + + Future cancel() => _cancel(); +} + +final class DartvexTransport implements IcarusConvexTransport { + DartvexTransport(String deploymentUrl) + : _client = dartvex.ConvexClient(deploymentUrl); + + final dartvex.ConvexClient _client; + int _bytesSent = 0; + int _bytesReceived = 0; + + @override + String get name => 'dartvex'; + + @override + int get bytesSent => _bytesSent; + + @override + int get bytesReceived => _bytesReceived; + + @override + Future authenticate(String? token) => _client.setAuth(token); + + @override + Future mutation(String path, Map arguments) async { + _recordSend(path, arguments); + final result = await _client.mutate(path, arguments) as Object?; + _recordReceive(result); + return result; + } + + @override + Future query(String path, Map arguments) async { + _recordSend(path, arguments); + final result = await _client.query(path, arguments) as Object?; + _recordReceive(result); + return result; + } + + @override + Future subscribe( + String path, + Map arguments, + ) async { + final subscription = _client.subscribe(path, arguments); + return LiveSubscription( + values: subscription.stream + .where((result) => result is dartvex.QuerySuccess) + .cast() + .map((result) { + final value = result.value as Object?; + _recordReceive(value); + return value; + }), + cancel: () async { + subscription.cancel(); + // Dartvex exposes a synchronous cancel that schedules its actual + // unsubscribe. Give that task a turn before a process-level close. + await Future.delayed(const Duration(milliseconds: 25)); + }, + ); + } + + @override + Future reconnect() async { + final stopwatch = Stopwatch()..start(); + await _client.reconnectNow('icarus-gauntlet'); + await _client.connectionState + .firstWhere((state) => state == dartvex.ConnectionState.connected) + .timeout(const Duration(seconds: 20)); + return stopwatch.elapsed; + } + + @override + Future close() async => _client.close(); + + void _recordSend(String path, Map arguments) { + _bytesSent += utf8 + .encode(jsonEncode({'path': path, 'args': arguments})) + .length; + } + + void _recordReceive(Object? value) { + _bytesReceived += utf8.encode(jsonEncode(value)).length; + } +} + +final class ConvexFlutterTransport implements IcarusConvexTransport { + ConvexFlutterTransport._(this._client); + + final convex_flutter.ConvexClient _client; + convex_flutter.AuthHandleWrapper? _authHandle; + int _bytesSent = 0; + int _bytesReceived = 0; + + static Future create(String deploymentUrl) async { + await convex_flutter.ConvexClient.initialize( + convex_flutter.ConvexConfig( + deploymentUrl: deploymentUrl, + clientId: 'icarus-runtime-gauntlet', + operationTimeout: const Duration(seconds: 30), + healthCheckQuery: 'users:me', + ), + ); + return ConvexFlutterTransport._(convex_flutter.ConvexClient.instance); + } + + @override + String get name => 'convex_flutter'; + + @override + int get bytesSent => _bytesSent; + + @override + int get bytesReceived => _bytesReceived; + + @override + Future authenticate(String? token) async { + _authHandle?.dispose(); + _authHandle = null; + await _client.clearAuth(); + // The package's native refresh handle cancels asynchronously and clears + // auth as it exits. Let that cancellation settle before installing the + // replacement handle so it cannot erase the fresh token afterward. + await Future.delayed(const Duration(milliseconds: 25)); + if (token == null) return; + _authHandle = await _client.setAuthWithRefresh( + fetchToken: () async => token, + ); + } + + @override + Future mutation(String path, Map arguments) async { + _recordSend(path, arguments); + final raw = await _client.mutation( + name: path, + args: arguments.cast(), + ); + _bytesReceived += utf8.encode(raw).length; + return jsonDecode(raw) as Object?; + } + + @override + Future query(String path, Map arguments) async { + _recordSend(path, arguments); + final raw = await _client.query(path, arguments.cast()); + _bytesReceived += utf8.encode(raw).length; + return jsonDecode(raw) as Object?; + } + + @override + Future subscribe( + String path, + Map arguments, + ) async { + final controller = StreamController.broadcast(); + var active = true; + final handle = await _client.subscribe( + name: path, + args: arguments.cast(), + onUpdate: (value) { + if (!active) return; + _bytesReceived += utf8.encode(value).length; + controller.add(jsonDecode(value) as Object?); + }, + onError: (message, value) { + if (!active) return; + controller.addError( + StateError(value == null ? message : '$message: $value'), + ); + }, + ); + return LiveSubscription( + values: controller.stream, + cancel: () async { + active = false; + handle.cancel(); + await Future.delayed(const Duration(milliseconds: 25)); + await controller.close(); + }, + ); + } + + @override + Future reconnect() async { + final stopwatch = Stopwatch()..start(); + final deadline = DateTime.now().add(const Duration(seconds: 20)); + while (DateTime.now().isBefore(deadline)) { + try { + final connected = await _client.reconnect().timeout( + const Duration(seconds: 1), + ); + if (connected) return stopwatch.elapsed; + } catch (_) { + // The public reconnect call is a health query rather than a socket + // transition. Poll it within the shared bounded recovery window. + } + await Future.delayed(const Duration(milliseconds: 250)); + } + throw StateError('convex_flutter failed to reconnect within 20 seconds'); + } + + @override + Future close() async { + _authHandle?.dispose(); + _authHandle = null; + _client.dispose(); + } + + void _recordSend(String path, Map arguments) { + _bytesSent += utf8 + .encode(jsonEncode({'path': path, 'args': arguments})) + .length; + } +} diff --git a/tool/convex_client_gauntlet/runtime/lib/workload.dart b/tool/convex_client_gauntlet/runtime/lib/workload.dart new file mode 100644 index 00000000..0a2763c8 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/lib/workload.dart @@ -0,0 +1,530 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + +const operationsPerSeed = 1000; +const operationBatchSize = 50; +const cloudProtocolVersion = 2; +const payloadVersion = 1; +const baseFixturePath = 'test/fixtures/strategy_integrity/base-test-v43.ica'; +const baseFixtureSha256 = + '8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a'; + +String seedPrefix(int seed) => 'seed-$seed-'; +String strategyId(int seed) => '${seedPrefix(seed)}strategy'; +String folderId(int seed) => '${seedPrefix(seed)}folder'; +String initialPageId(int seed) => '${seedPrefix(seed)}page-custom-shapes'; +String secondaryPageId(int seed) => '${seedPrefix(seed)}page-secondary'; + +List> buildOperationTrace(int seed) { + final operations = >[]; + var sequence = 0; + + void add(Map operation) { + operations.add({ + 'opId': '${seedPrefix(seed)}op-${sequence.toString().padLeft(4, '0')}', + ...operation, + }); + sequence += 1; + } + + for (var cycle = 0; cycle < 80; cycle += 1) { + _addRevisionCycle( + add: add, + entityType: 'element', + publicId: + '${seedPrefix(seed)}element-${cycle.toString().padLeft(3, '0')}', + pagePublicId: initialPageId(seed), + payloadBuilder: (variant) => _utilityPayload(seed, cycle, variant), + initialSortIndex: 100 + cycle, + finalSortIndex: 8000 + cycle, + ); + } + + for (var cycle = 0; cycle < 10; cycle += 1) { + _addRevisionCycle( + add: add, + entityType: 'lineup', + publicId: '${seedPrefix(seed)}lineup-${cycle.toString().padLeft(3, '0')}', + pagePublicId: initialPageId(seed), + payloadBuilder: (variant) => _lineupPayload(seed, cycle, variant), + initialSortIndex: 1000 + cycle, + finalSortIndex: 9000 + cycle, + ); + } + + final secondPage = secondaryPageId(seed); + add({ + 'kind': 'add', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'payload': {'name': 'Secondary', 'isAttack': false}, + 'sortIndex': 1, + 'expectedRevision': 0, + }); + add({ + 'kind': 'patch', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'payload': {'name': 'Secondary A'}, + 'expectedRevision': 1, + }); + add({ + 'kind': 'reorder', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'sortIndex': 0, + 'expectedRevision': 1, + }); + add({ + 'kind': 'patch', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'payload': {'name': 'Secondary B'}, + 'expectedRevision': 3, + }); + add({ + 'kind': 'delete', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'expectedRevision': 2, + }); + add({ + 'kind': 'delete', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'expectedRevision': 2, + }); + add({ + 'kind': 'add', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'payload': {'name': 'Secondary C', 'isAttack': false}, + 'sortIndex': 1, + 'expectedRevision': 3, + }); + add({ + 'kind': 'patch', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'payload': {'name': 'Secondary D'}, + 'expectedRevision': 1, + }); + add({ + 'kind': 'reorder', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'sortIndex': 0, + 'expectedRevision': 4, + }); + add({ + 'kind': 'patch', + 'entityType': 'page', + 'entityPublicId': secondPage, + 'payload': {'name': 'Secondary final'}, + 'expectedRevision': 3, + }); + + for (var index = 0; index < 10; index += 1) { + add({ + 'kind': 'patch', + 'entityType': 'strategy', + 'entityPublicId': strategyId(seed), + 'payload': {'name': 'Gauntlet seed $seed revision $index'}, + 'expectedRevision': 5 + index, + }); + } + + for (var index = 0; index < 80; index += 1) { + add({ + 'kind': 'patch', + 'entityType': 'pageContent', + 'entityPublicId': initialPageId(seed), + 'payload': { + 'settings': { + 'agentSize': 36 + (index % 5), + 'abilitySize': 26 + (index % 3), + 'useNeutralTeamColors': index.isEven, + }, + }, + 'expectedRevision': 1 + index, + }); + } + + if (operations.length != operationsPerSeed) { + throw StateError( + 'Trace contains ${operations.length} operations, expected ' + '$operationsPerSeed', + ); + } + return List.unmodifiable(operations); +} + +void _addRevisionCycle({ + required void Function(Map) add, + required String entityType, + required String publicId, + required String pagePublicId, + required Map Function(int variant) payloadBuilder, + required int initialSortIndex, + required int finalSortIndex, +}) { + add({ + 'kind': 'add', + 'entityType': entityType, + 'entityPublicId': publicId, + 'pagePublicId': pagePublicId, + 'payload': payloadBuilder(0), + 'sortIndex': initialSortIndex, + }); + add({ + 'kind': 'patch', + 'entityType': entityType, + 'entityPublicId': publicId, + 'payload': payloadBuilder(1), + 'expectedRevision': 1, + }); + add({ + 'kind': 'patch', + 'entityType': entityType, + 'entityPublicId': publicId, + 'payload': payloadBuilder(2), + 'expectedRevision': 1, + }); + add({ + 'kind': 'patch', + 'entityType': entityType, + 'entityPublicId': publicId, + 'payload': payloadBuilder(3), + 'expectedRevision': 2, + }); + add({ + 'kind': 'reorder', + 'entityType': entityType, + 'entityPublicId': publicId, + 'sortIndex': finalSortIndex - 1, + 'expectedRevision': 3, + }); + add({ + 'kind': 'delete', + 'entityType': entityType, + 'entityPublicId': publicId, + 'expectedRevision': 4, + }); + add({ + 'kind': 'add', + 'entityType': entityType, + 'entityPublicId': publicId, + 'pagePublicId': pagePublicId, + 'payload': payloadBuilder(4), + 'sortIndex': finalSortIndex - 2, + 'expectedRevision': 5, + }); + add({ + 'kind': 'patch', + 'entityType': entityType, + 'entityPublicId': publicId, + 'payload': payloadBuilder(5), + 'expectedRevision': 6, + }); + add({ + 'kind': 'reorder', + 'entityType': entityType, + 'entityPublicId': publicId, + 'sortIndex': finalSortIndex, + 'expectedRevision': 7, + }); + add({ + 'kind': 'patch', + 'entityType': entityType, + 'entityPublicId': publicId, + 'payload': payloadBuilder(6), + 'expectedRevision': 8, + }); +} + +Map _utilityPayload(int seed, int cycle, int variant) { + final id = '${seedPrefix(seed)}element-${cycle.toString().padLeft(3, '0')}'; + return { + 'kind': 'utility', + 'payloadVersion': payloadVersion, + 'data': { + 'id': id, + 'isDeleted': false, + 'position': { + 'dx': 100 + cycle.toDouble(), + 'dy': 150 + variant.toDouble(), + }, + 'type': 'customCircle', + 'rotation': 0, + 'length': 0, + 'angle': 0, + 'attachedAgentId': null, + 'customDiameter': 10 + variant.toDouble(), + 'customWidth': null, + 'customLength': null, + 'customColorValue': 4282090230, + 'customOpacityPercent': 30 + variant, + }, + }; +} + +Map _lineupPayload(int seed, int cycle, int variant) { + final id = '${seedPrefix(seed)}lineup-${cycle.toString().padLeft(3, '0')}'; + return { + 'kind': 'lineupGroup', + 'payloadVersion': payloadVersion, + 'data': { + 'id': id, + 'agent': { + 'id': '$id-agent', + 'isDeleted': false, + 'position': {'dx': 10 + cycle, 'dy': 20 + variant}, + 'type': 'sova', + 'isAlly': true, + 'state': 'none', + 'kind': 'plain', + 'lineUpID': id, + }, + 'items': [ + { + 'id': '$id-item', + 'ability': { + 'id': '$id-ability', + 'isDeleted': false, + 'data': {'type': 'sova', 'index': 2}, + 'position': {'dx': 30 + cycle, 'dy': 40 + variant}, + 'isAlly': true, + 'rotation': 0, + 'length': 0, + 'lineUpID': id, + 'visualState': { + 'showRangeOutline': true, + 'showRangeFill': true, + 'showInnerOutline': true, + 'showInnerFill': true, + }, + 'armLengthsMeters': [10, 10, 10, 10], + }, + 'youtubeLink': '', + 'notes': 'seed $seed cycle $cycle variant $variant', + 'images': [], + }, + ], + }, + }; +} + +List> baseElementOps(int seed) { + Map basePayload({ + required String id, + required Map position, + required String type, + required double? diameter, + required double? width, + required double? length, + required int color, + required int opacity, + }) => { + 'kind': 'utility', + 'payloadVersion': payloadVersion, + 'data': { + 'id': id, + 'isDeleted': false, + 'position': position, + 'type': type, + 'rotation': 0, + 'length': 0, + 'angle': 0, + 'attachedAgentId': null, + 'customDiameter': diameter, + 'customWidth': width, + 'customLength': length, + 'customColorValue': color, + 'customOpacityPercent': opacity, + }, + }; + + return [ + { + 'opId': '${seedPrefix(seed)}base-circle', + 'kind': 'add', + 'entityType': 'element', + 'entityPublicId': '${seedPrefix(seed)}utility-circle-current', + 'pagePublicId': initialPageId(seed), + 'sortIndex': 0, + 'payload': basePayload( + id: '${seedPrefix(seed)}utility-circle-current', + position: {'dx': 220.0, 'dy': 180.0}, + type: 'customCircle', + diameter: 14.0, + width: null, + length: null, + color: 4282090230, + opacity: 35, + ), + }, + { + 'opId': '${seedPrefix(seed)}base-rectangle', + 'kind': 'add', + 'entityType': 'element', + 'entityPublicId': '${seedPrefix(seed)}utility-rectangle-current', + 'pagePublicId': initialPageId(seed), + 'sortIndex': 1, + 'payload': basePayload( + id: '${seedPrefix(seed)}utility-rectangle-current', + position: {'dx': 420.0, 'dy': 280.0}, + type: 'customRectangle', + diameter: null, + width: 6.0, + length: 18.0, + color: 4280468830, + opacity: 30, + ), + }, + ]; +} + +String canonicalJson(Object? value) => jsonEncode(_sortJson(value)); + +String canonicalHash(Object? value) => + sha256.convert(utf8.encode(canonicalJson(value))).toString(); + +Object? canonicalSnapshot({ + required int seed, + required Object? snapshot, + required Object? folders, +}) { + final normalizedSnapshot = _stripTransportMetadata(snapshot); + final folderList = (folders as List) + .whereType>() + .where((folder) => folder['publicId'] == folderId(seed)) + .map(_stripTransportMetadata) + .toList(growable: false); + return _replaceSeedPrefix({ + 'snapshot': normalizedSnapshot, + 'folders': folderList, + }, seedPrefix(seed)); +} + +Object? _stripTransportMetadata(Object? value) { + if (value is List) { + return value.map(_stripTransportMetadata).toList(growable: false); + } + if (value is Map) { + final result = {}; + for (final entry in value.entries) { + final key = entry.key as String; + if (key == 'createdAt' || + key == 'updatedAt' || + key == 'contentCreatedAt' || + key == 'contentUpdatedAt' || + key == 'role') { + continue; + } + result[key] = _stripTransportMetadata(entry.value); + } + return result; + } + return value; +} + +Object? _replaceSeedPrefix(Object? value, String prefix) { + if (value is String) return value.replaceAll(prefix, ''); + if (value is List) { + return value + .map((item) => _replaceSeedPrefix(item, prefix)) + .toList(growable: false); + } + if (value is Map) { + return value.map( + (key, item) => MapEntry(key as String, _replaceSeedPrefix(item, prefix)), + ); + } + return value; +} + +Map exportIcaRoundTrip(Object? snapshotValue) { + final snapshot = (snapshotValue as Map) + .cast(); + final header = (snapshot['header'] as Map) + .cast(); + final pages = (snapshot['pages'] as List) + .map((item) => (item as Map).cast()) + .toList(growable: false); + final elements = (snapshot['elements'] as List) + .map((item) => (item as Map).cast()) + .toList(growable: false); + final lineups = (snapshot['lineups'] as List) + .map((item) => (item as Map).cast()) + .toList(growable: false); + + List dataFor(String pageId, String kind) => elements + .where( + (element) => + element['pagePublicId'] == pageId && + element['elementType'] == kind && + element['deleted'] == false, + ) + .map( + (element) => + ((element['payload'] as Map)['data']) as Object?, + ) + .toList(growable: false); + + final archive = { + 'versionNumber': '43', + 'mapData': header['mapData'], + 'themePalette': header['themeOverridePalette'], + 'pages': pages + .map((page) { + final pageId = page['publicId'] as String; + return { + 'id': pageId, + 'sortIndex': (page['sortIndex'] as num).toInt().toString(), + 'name': page['name'], + 'drawingData': dataFor(pageId, 'drawing'), + 'agentData': dataFor(pageId, 'agent'), + 'abilityData': dataFor(pageId, 'ability'), + 'textData': dataFor(pageId, 'text'), + 'imageData': dataFor(pageId, 'image'), + 'utilityData': dataFor(pageId, 'utility'), + 'isAttack': (page['isAttack'] as bool).toString(), + 'settings': page['settings'], + 'lineUpData': lineups + .where( + (lineup) => + lineup['pagePublicId'] == pageId && + lineup['deleted'] == false, + ) + .map( + (lineup) => + ((lineup['payload'] as Map)['data']) + as Object?, + ) + .toList(growable: false), + }; + }) + .toList(growable: false), + }; + + final encoded = canonicalJson(archive); + final decoded = jsonDecode(encoded) as Map; + if (canonicalJson(decoded) != encoded) { + throw StateError('Canonical .ica JSON did not survive a JSON round-trip'); + } + return archive; +} + +Object? _sortJson(Object? value) { + if (value is List) { + return value.map(_sortJson).toList(growable: false); + } + if (value is Map) { + final keys = value.keys.cast().toList()..sort(); + return { + for (final key in keys) key: _sortJson(value[key]), + }; + } + return value; +} diff --git a/tool/convex_client_gauntlet/runtime/pubspec.lock b/tool/convex_client_gauntlet/runtime/pubspec.lock new file mode 100644 index 00000000..e29e4a9f --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/pubspec.lock @@ -0,0 +1,458 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + convex_flutter: + dependency: "direct main" + description: + name: convex_flutter + sha256: db3bca4e3e6792eadadba9a662225ccb743c287f4550879f67885cd40a2198f2 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_jsonwebtoken: + dependency: transitive + description: + name: dart_jsonwebtoken + sha256: ad84e60181696513d04d5f2078e0bbc20365b911f46f647797317414bdc88fbe + url: "https://pub.dev" + source: hosted + version: "3.4.1" + dartvex: + dependency: "direct main" + description: + name: dartvex + sha256: "7a343c5853f25a1a136051d2d37002a0e1e3f6c230b6f24560797880de33b5d8" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_rust_bridge: + dependency: "direct main" + description: + name: flutter_rust_bridge + sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: "94074d62167ae634127ef6095f536835063a7dc80f2b1aa306d2346ff9023996" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: f7b52008311941a7c3e99f9590c4ee32dfc102a5442e43abf1b287d9f8cc39b2 + url: "https://pub.dev" + source: hosted + version: "2.18.0" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: "direct dev" + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: f4b6bb24b465c47649243ef0140475de8a0ec311dc9c75ebe573b2dcabb10460 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "5268afc208d02fb9109854d262c1ebf6ece224cd285199ae1d2f92d2ff49dbf1" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "1c61b19ed9e78f37fdd1ca8b729ab8484e6c8fe82e15c87e070b861951183657" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + supabase: + dependency: "direct main" + description: + name: supabase + sha256: cc039f63a3168386b3a4f338f3bff342c860d415a3578f3fbe854024aee6f911 + url: "https://pub.dev" + source: hosted + version: "2.10.2" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e + url: "https://pub.dev" + source: hosted + version: "2.1.0" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/tool/convex_client_gauntlet/runtime/pubspec.yaml b/tool/convex_client_gauntlet/runtime/pubspec.yaml new file mode 100644 index 00000000..016caf9a --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/pubspec.yaml @@ -0,0 +1,23 @@ +name: icarus_convex_runtime_gauntlet +description: Isolated symmetric runtime comparison for Icarus Convex clients. +publish_to: none +version: 0.0.0 + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + convex_flutter: 3.0.1 + crypto: 3.0.7 + dartvex: 0.2.0 + # convex_flutter's generated Rust bridge is pinned to this runtime version. + flutter_rust_bridge: 2.11.1 + http: 1.6.0 + supabase: 2.10.2 + +dev_dependencies: + flutter_test: + sdk: flutter + lints: 6.0.0 diff --git a/tool/convex_client_gauntlet/runtime/test/transport_smoke_test.dart b/tool/convex_client_gauntlet/runtime/test/transport_smoke_test.dart new file mode 100644 index 00000000..23ce8728 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/test/transport_smoke_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:icarus_convex_runtime_gauntlet/transport.dart'; + +const deploymentUrl = String.fromEnvironment('CONVEX_URL'); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test( + 'both transports reach the same deployment', + () async { + final dartvex = DartvexTransport(deploymentUrl); + final dartvexResult = await dartvex.query('users:me', const {}); + expect(dartvexResult, isNull); + await dartvex.close(); + + final convexFlutter = await ConvexFlutterTransport.create(deploymentUrl); + final convexFlutterResult = await convexFlutter.query( + 'users:me', + const {}, + ); + expect(convexFlutterResult, isNull); + await convexFlutter.close(); + }, + skip: deploymentUrl.isEmpty ? 'Pass --dart-define=CONVEX_URL' : false, + ); +} diff --git a/tool/convex_client_gauntlet/runtime/test/workload_test.dart b/tool/convex_client_gauntlet/runtime/test/workload_test.dart new file mode 100644 index 00000000..2f86e8f3 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/test/workload_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:icarus_convex_runtime_gauntlet/workload.dart'; + +void main() { + test('each seed has one deterministic 1,000-op trace', () { + for (var seed = 0; seed < 50; seed += 1) { + final first = buildOperationTrace(seed); + final second = buildOperationTrace(seed); + expect(first, hasLength(operationsPerSeed)); + expect(canonicalHash(first), canonicalHash(second)); + expect( + first.map((operation) => operation['opId']).toSet(), + hasLength(operationsPerSeed), + ); + } + }); + + test('the trace covers every synced entity boundary', () { + final trace = buildOperationTrace(0); + expect( + trace.map((operation) => operation['entityType']).toSet(), + containsAll([ + 'strategy', + 'page', + 'pageContent', + 'element', + 'lineup', + ]), + ); + expect( + trace.where((operation) => operation['kind'] == 'delete'), + isNotEmpty, + ); + expect( + trace.where((operation) => operation['kind'] == 'reorder'), + isNotEmpty, + ); + }); + + test('canonical snapshots exclude transport clocks', () { + Object? state(double createdAt, double updatedAt) => canonicalSnapshot( + seed: 0, + snapshot: { + 'header': {'publicId': 'seed-0-strategy', 'createdAt': createdAt}, + 'pages': [ + { + 'publicId': 'seed-0-page', + 'contentCreatedAt': createdAt, + 'contentUpdatedAt': updatedAt, + }, + ], + 'elements': [], + 'lineups': [], + }, + folders: [], + ); + + expect(canonicalHash(state(1, 2)), canonicalHash(state(10, 20))); + }); +} diff --git a/tool/convex_client_gauntlet/runtime/tool/export_canonical_state.dart b/tool/convex_client_gauntlet/runtime/tool/export_canonical_state.dart new file mode 100644 index 00000000..044d01d9 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/tool/export_canonical_state.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:dartvex/dartvex.dart'; +import 'package:supabase/supabase.dart'; + +import 'package:icarus_convex_runtime_gauntlet/workload.dart'; + +Future main(List arguments) async { + if (arguments.length != 1) { + stderr.writeln('Usage: dart run tool/export_canonical_state.dart '); + exitCode = 64; + return; + } + final environment = Platform.environment; + final supabase = SupabaseClient( + environment['SUPABASE_URL']!, + environment['SUPABASE_KEY']!, + authOptions: const AuthClientOptions( + authFlowType: AuthFlowType.implicit, + autoRefreshToken: false, + ), + ); + final auth = await supabase.auth.signInWithPassword( + email: environment['TEST_EMAIL']!, + password: environment['TEST_PASSWORD']!, + ); + final session = auth.session ?? (throw StateError('Sign-in failed')); + final convex = ConvexClient(environment['CONVEX_URL']!); + await convex.setAuth(session.accessToken); + try { + final snapshot = await convex.query('strategy:getFullSnapshot', { + 'strategyPublicId': strategyId(0), + }); + final folders = await convex.query('folders:listAll', {'scope': 'all'}); + final canonical = canonicalSnapshot( + seed: 0, + snapshot: snapshot, + folders: folders, + ); + await File( + arguments.single, + ).writeAsString(canonicalJson(canonical), flush: true); + stdout.writeln(canonicalHash(canonical)); + } finally { + await convex.close(); + await supabase.dispose(); + } +} diff --git a/tool/convex_client_gauntlet/runtime/tool/provision_test_account.dart b/tool/convex_client_gauntlet/runtime/tool/provision_test_account.dart new file mode 100644 index 00000000..8ba5dae0 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/tool/provision_test_account.dart @@ -0,0 +1,153 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:http/http.dart' as http; +import 'package:supabase/supabase.dart'; + +const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); +const supabaseKey = String.fromEnvironment('SUPABASE_KEY'); + +Future main(List arguments) async { + if (supabaseUrl.isEmpty || supabaseKey.isEmpty || arguments.length != 1) { + stderr.writeln( + 'Usage: dart --define=SUPABASE_URL=... --define=SUPABASE_KEY=... ' + 'run tool/provision_test_account.dart ', + ); + exitCode = 64; + return; + } + + final output = File(arguments.single); + final random = Random.secure(); + final mailboxPassword = _randomSecret(random, 32); + final accountPassword = '${_randomSecret(random, 24)}aA7!'; + final mailClient = http.Client(); + final supabase = SupabaseClient( + supabaseUrl, + supabaseKey, + authOptions: const AuthClientOptions(authFlowType: AuthFlowType.implicit), + ); + + try { + final domainResponse = await mailClient.get( + Uri.parse('https://api.mail.tm/domains?page=1'), + ); + _requireSuccess(domainResponse, 'list disposable mailbox domains'); + final domainBody = jsonDecode(domainResponse.body) as Map; + final domains = domainBody['hydra:member'] as List; + final domain = (domains.first as Map)['domain'] as String; + final address = + 'icarus-gauntlet-${DateTime.now().microsecondsSinceEpoch}@$domain'; + + final accountResponse = await mailClient.post( + Uri.parse('https://api.mail.tm/accounts'), + headers: const {'content-type': 'application/json'}, + body: jsonEncode({'address': address, 'password': mailboxPassword}), + ); + _requireSuccess(accountResponse, 'create disposable mailbox'); + + final tokenResponse = await mailClient.post( + Uri.parse('https://api.mail.tm/token'), + headers: const {'content-type': 'application/json'}, + body: jsonEncode({'address': address, 'password': mailboxPassword}), + ); + _requireSuccess(tokenResponse, 'authenticate disposable mailbox'); + final mailToken = + (jsonDecode(tokenResponse.body) as Map)['token'] + as String; + + await supabase.auth.signUp( + email: address, + password: accountPassword, + data: const {'display_name': 'Icarus Convex gauntlet'}, + ); + + final confirmationUrl = await _waitForConfirmation( + client: mailClient, + token: mailToken, + ); + final confirmationRequest = http.Request('GET', confirmationUrl) + ..followRedirects = false; + final confirmationResponse = await mailClient.send(confirmationRequest); + if (confirmationResponse.statusCode >= 400) { + throw StateError( + 'Supabase confirmation failed with HTTP ' + '${confirmationResponse.statusCode}', + ); + } + + final auth = await supabase.auth.signInWithPassword( + email: address, + password: accountPassword, + ); + if (auth.session == null) { + throw StateError('Confirmed test account did not produce a session'); + } + + await output.writeAsString( + jsonEncode({'email': address, 'password': accountPassword}), + flush: true, + ); + await Process.run('chmod', ['600', output.path]); + stdout.writeln('Disposable Supabase test account is confirmed and ready.'); + } finally { + mailClient.close(); + await supabase.dispose(); + } +} + +Future _waitForConfirmation({ + required http.Client client, + required String token, +}) async { + final deadline = DateTime.now().add(const Duration(minutes: 2)); + final headers = {'authorization': 'Bearer $token'}; + while (DateTime.now().isBefore(deadline)) { + final messagesResponse = await client.get( + Uri.parse('https://api.mail.tm/messages?page=1'), + headers: headers, + ); + _requireSuccess(messagesResponse, 'poll disposable mailbox'); + final body = jsonDecode(messagesResponse.body) as Map; + final messages = body['hydra:member'] as List; + if (messages.isNotEmpty) { + final id = (messages.first as Map)['id'] as String; + final messageResponse = await client.get( + Uri.parse('https://api.mail.tm/messages/$id'), + headers: headers, + ); + _requireSuccess(messageResponse, 'read confirmation message'); + final message = jsonDecode(messageResponse.body) as Map; + final source = [ + if (message['text'] is String) message['text'] as String, + if (message['html'] is List) + ...(message['html'] as List).whereType(), + ].join('\n'); + final match = RegExp( + r'''https://[^\s"'<>()\]]+/auth/v1/verify\?[^\s"'<>()\]]+''', + ).firstMatch(source); + if (match != null) { + return Uri.parse(match.group(0)!.replaceAll('&', '&')); + } + } + await Future.delayed(const Duration(seconds: 2)); + } + throw TimeoutException('Supabase confirmation email did not arrive'); +} + +String _randomSecret(Random random, int length) { + const alphabet = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + return List.generate( + length, + (_) => alphabet[random.nextInt(alphabet.length)], + growable: false, + ).join(); +} + +void _requireSuccess(http.Response response, String operation) { + if (response.statusCode < 200 || response.statusCode >= 300) { + throw StateError('$operation failed with HTTP ${response.statusCode}'); + } +} From 8b65dfd6189f440b5a73daf05ea1a4d55b88be1f Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 27 Aug 2026 01:09:14 -0400 Subject: [PATCH 07/11] docs: record fair Convex client rerun --- .../convex_dart_client_fair_rerun_result.md | 122 ++++++++++++++++++ .../results/convex_flutter_correctness.json | 1 + .../runtime/results/dartvex_correctness.json | 1 + .../runtime/results/fair_rerun_matrix.json | 68 ++++++++++ 4 files changed, 192 insertions(+) create mode 100644 docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md create mode 100644 tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json create mode 100644 tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json create mode 100644 tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json diff --git a/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md new file mode 100644 index 00000000..2f2ec886 --- /dev/null +++ b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md @@ -0,0 +1,122 @@ +# Convex Dart client fair rerun result + +Status: correctness complete; profiling blocked on 2026-08-27 + +Harness commit: `bf421ffef06b5d04749c77bda182f8f0a53796fe` + +Candidates: Dartvex 0.2.0 and `convex_flutter` 3.0.1 + +Decision: Dartvex wins the runtime correctness gate, but neither client has +earned an application migration. + +## Verdict + +Dartvex completed all 50 deterministic seeds and all 50,000 ops. Every op +resolved: 45,500 landed and 4,500 produced the planned, visible revision +rejects. The fresh Supabase token was accepted after the rejected-token fault, +the queued batch was replayed exactly once, the persisted mid-run checkpoint +resumed, every verifier hash matched, and every `.ica` export round-tripped. + +`convex_flutter` hit the handoff's immediate losing condition on seed 0. Its +first 500 ops resolved as expected (450 landed and 50 visible revision rejects), +then the injected credential was rejected. Supabase `refreshSession()` returned +a different access token, but the package did not accept authenticated work +within the bounded 20-second recovery window after the rejected auth state was +cleared, the production-style refresh handle was replaced, and its public +reconnect path was exercised. The queued batch therefore could not be proven to +land exactly once. + +The application dependency remains unchanged. A correctness survivor is not +automatically an adoption winner: the repaired Dartvex contract gate proves a +narrow strict wrapper around one explicit `folders:listForParent` result, but +the runtime adapter still uses path-and-JSON calls because the stable generated +return surface for the runtime functions is not complete. Migrating now would +claim the JSON-plumbing benefit before proving it. + +## Fair workload + +Both adapters used the same local Convex deployment, disposable client account, +public Supabase anon credential, base fixture, serialized op traces, IDs, +timeouts, and fault schedules. The seed-0 trace and schedule hashes match: + +- trace: `ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06` +- fault schedule: + `b4e244c38b89789f1191e988768aa681f4722184533077ef58eb6f95ee6a0e52` +- base fixture: + `test/fixtures/strategy_integrity/base-test-v43.ica`, SHA-256 + `8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a` + +The 1,000-op trace covers strategy, page, page content, element, and lineup +changes, including add, patch, reorder, delete/recreate, duplicate delivery, +revision conflicts, subscription restart, reconnect, offline delay, auth +rejection/refresh, and durable process restart. A new Dartvex client acts as +verifier C. Canonical state excludes server-authored transport clocks, including +page-content creation/update clocks; a regression test proves those clocks +cannot create a false state mismatch. + +## Separate scorecards + +| Area | Dartvex 0.2.0 | `convex_flutter` 3.0.1 | +| --- | --- | --- | +| Explicit result rename | Old field access fails analysis | Hand-written decoding | +| Missing return | Icarus strict wrapper rejects public `dynamic` | Not applicable | +| Unsupported validator | Wrapper converts generator warning/exit 0 into failure/exit 2 | Not applicable | +| Determinism | Identical generated SHA-256 on two runs | Not generated | +| Runtime correctness | **Pass: 50/50 seeds, 50,000/50,000 ops** | **Fail: auth recovery at seed 0, op 500** | +| Rejected-token refresh | Fresh token accepted; queued batch lands once | Fresh token not accepted in 20 seconds | +| Restart recovery | Persisted checkpoint resumes | Not reached | +| Canonical and `.ica` verification | Pass on all 50 seeds | Not reached | +| Dependency health | Pure Dart client plus strict tooling package | Native bridge had to pin `flutter_rust_bridge` 2.11.1 instead of resolved 2.13.0 | +| Generated Icarus surface | Incomplete for runtime functions | None | + +The Dartvex correctness run recorded 20,224,618 application-JSON bytes sent, +48,117,804 received, 64,794.127 ms wall time, and 260,472,832 bytes maximum +RSS. These are correctness-run observations, not comparative performance +claims. There is no valid `convex_flutter` completion sample to pair with them. + +## Why Phase 4 did not run + +The handoff permits profiling only after both clients complete all correctness +seeds with canonical equality. `convex_flutter` failed before completing seed +0, so paired profile runs, CPU comparison, build-size comparison, and +cross-platform performance builds are intentionally recorded as zero/not run. +Running them anyway would let speed distract from uncertain queued work. + +## Artifacts and reproduction + +The historical first result remains unchanged at +[`convex_dart_client_gauntlet_result.md`](convex_dart_client_gauntlet_result.md). +The fair rerun adds: + +- [`contract_gate.json`](../../tool/convex_client_gauntlet/results/contract_gate.json) +- [`dartvex_correctness.json`](../../tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json) +- [`convex_flutter_correctness.json`](../../tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json) +- [`fair_rerun_matrix.json`](../../tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json) +- [runtime commands](../../tool/convex_client_gauntlet/runtime/README.md) + +The raw correctness artifacts contain no email, password, access token, refresh +token, Supabase key, or elevated credential. The deployment was the isolated +local instance at `127.0.0.1:3210`; no production or user library data was used. + +## Repository verification + +After writing the result artifacts, the branch passed: + +- the repaired Dartvex gate, its 2 Dart tests, and Dart analysis; +- runtime-gauntlet formatting, Dart analysis, and all 3 deterministic workload + tests (the separately invoked live-deployment smoke test is skipped without a + deployment define); +- native runner analysis and a macOS debug build; +- `npx tsc --noEmit` and all 22 Convex tests; +- all 343 Flutter tests; +- Flutter analysis with exit 0 and the same 6 pre-existing info-level lints; +- `fvm flutter build web --no-tree-shake-icons`. + +## Next step + +Keep the application unchanged for now. Dartvex is the only runtime-surviving +candidate, so any next client evaluation should focus narrowly on completing +explicit return validators and proving that its generated API materially +removes Icarus JSON plumbing without becoming a second generator or package +fork. Independently, `convex_flutter` auth recovery needs a package-level fix +or replacement before it can re-enter this comparison. diff --git a/tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json b/tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json new file mode 100644 index 00000000..35fcc9ea --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json @@ -0,0 +1 @@ +{"schemaVersion":1,"status":"failed","adapter":"convex_flutter","candidateVersion":"3.0.1","flutterRustBridgeVersion":"2.11.1 pinned","deployment":"local:127.0.0.1:3210","gitCommit":"bf421ffef06b5d04749c77bda182f8f0a53796fe","baseFixture":{"path":"test/fixtures/strategy_integrity/base-test-v43.ica","sha256":"8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a"},"seedCount":50,"operationsPerSeed":1000,"totalOperationsPlanned":50000,"losingCondition":"auth_refresh_recovery_failed","message":"Fresh access token was not accepted within the bounded recovery window","seed":0,"nextOperation":500,"ledgerSha256":"f924af909b90b24e268b3c48ec809f32f6efaa3bb92ed054b8e11a68a60ae151","partialSeed":{"seed":0,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06","faultScheduleSha256":"b4e244c38b89789f1191e988768aa681f4722184533077ef58eb6f95ee6a0e52","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"auth_reject_refresh","beforeBatch":10},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":false,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":true,"processRestart":false},"acknowledged":450,"rejected":50,"unresolved":0,"retryCount":0,"batchLatencyMs":[50.807,43.473,44.871,45.67,44.156,48.479,46.805,49.981,50.579,48.432],"auth":{"exercised":true,"rejectedTokenObserved":true,"refreshSessionCalled":true,"tokenChanged":true,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"2f5c83b805fd22471e866932f0ad3a6b438850368d79cbaada4e91a2306d36f8"},"bytesSent":225178,"bytesReceived":314150,"wallClockMs":31826.247,"maxRssBytes":255328256,"machine":{"operatingSystem":"macos","operatingSystemVersion":"Version 26.5.1 (Build 25F80)","processors":8,"dartVersion":"3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\""}} \ No newline at end of file diff --git a/tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json b/tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json new file mode 100644 index 00000000..353fff33 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json @@ -0,0 +1 @@ +{"schemaVersion":1,"status":"passed","adapter":"dartvex","candidateVersion":"0.2.0","flutterRustBridgeVersion":null,"deployment":"local:127.0.0.1:3210","gitCommit":"bf421ffef06b5d04749c77bda182f8f0a53796fe","baseFixture":{"path":"test/fixtures/strategy_integrity/base-test-v43.ica","sha256":"8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a"},"seedCount":50,"operationsPerSeed":1000,"totalOperations":50000,"allCanonicalEqual":true,"allResolved":true,"processRestartCheckpoint":true,"bytesSent":20224618,"bytesReceived":48117804,"wallClockMs":64794.12699999999,"maxRssBytes":260472832,"machine":{"operatingSystem":"macos","operatingSystemVersion":"Version 26.5.1 (Build 25F80)","processors":8,"dartVersion":"3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\""},"seeds":[{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":true,"exercised":true,"queuedBatchReplayedExactlyOnce":true,"refreshSessionCalled":true,"rejectedTokenObserved":true,"tokenChanged":true},"batchLatencyMs":[44.354,38.401,37.205,40.209,36.482,37.82,38.181,37.493,37.097,38.086,62.452,39.203,40.194,39.93,57.626,40.653,51.89,52.341,36.289,32.466],"canonicalVerifierHash":"26d71e8df48fba1f7aae2c8cf4b5569e8b9360166b9bb6ed887ce7f14048f2f7","duplicateNoopHash":"2f5c83b805fd22471e866932f0ad3a6b438850368d79cbaada4e91a2306d36f8","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":10,"fault":"auth_reject_refresh"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"b4e244c38b89789f1191e988768aa681f4722184533077ef58eb6f95ee6a0e52","faults":{"authRefresh":true,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":139.712,"rejected":90,"remoteConvergenceMs":9.366,"retryCount":0,"roundTripHash":"57f03a40845f1cbcec927c4a2abc68781fcec79e10d5f8a896a7deb95bbd9d69","seed":0,"traceSha256":"ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.663,38.242,36.696,37.857,35.665,37.689,38.565,42.735,39.346,39.484,38.731,39.898,41.192,40.341,46.73,43.464,50.185,51.915,35.415,32.371],"canonicalVerifierHash":"521b140c0a612b8ea44a1d9b4b03cbf0573799ae00bf05498122bacab81c1bba","duplicateNoopHash":"f5cf17c272add3c1c7d3460a3f29c4ee4bf786dd0d24d00d5010fd3069450206","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":62.2,"rejected":90,"remoteConvergenceMs":8.589,"retryCount":0,"roundTripHash":"42034206a0ce276524b5f6c4b61270d036eafa3ea888f6af32fcc8898136425d","seed":1,"traceSha256":"74e1c135f93f0e9ebf7029a5222aaa38b6aa0f834e31861e9e3de8515922bca4","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[46.78,39.744,40.588,39.327,37.441,39.078,40.443,43.848,39.236,39.79,40.009,41.432,41.909,41.245,56.622,42.175,52.564,52.323,37.34,33.256],"canonicalVerifierHash":"9ca1893e49091976796548e8568b4e1791bff1f72f5f883892ef688a59483304","duplicateNoopHash":"f241cf2843f3adfa94f6566c00df83a1f234b4993b73daa1fe08ff8b34e89260","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":131.958,"rejected":90,"remoteConvergenceMs":8.097,"retryCount":0,"roundTripHash":"44c8e21bbcb1c01d01bd1446150e3c5e3daa0f256aced3da4e96ac1afeb6be93","seed":2,"traceSha256":"2b3e81b55c925a72176cd6f3d1515c7063d7706d8d4381c641b6fc0a8e121262","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.01,36.278,38.307,42.036,39.143,38.957,38.917,46.649,41.052,40.687,39.862,41.003,40.476,41.964,59.487,42.546,50.984,52.864,35.916,33.058],"canonicalVerifierHash":"197d1fc1c62be082b93dd2c51021a15c68e3f7551de5e0d949b11152f6ab6840","duplicateNoopHash":"1e34802380fb4c7b66dfe194d29cf6bb4a223e342c5ac58fb77bd10b9436b658","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":132.808,"rejected":90,"remoteConvergenceMs":7.582,"retryCount":0,"roundTripHash":"18bb3a755432638399f32b7fb39627dab2c7c474726dc01b433243d45e86ab8f","seed":3,"traceSha256":"f7105a396da40cd883ec4cc9e83f1a6cb53706e90e782cc3e71cb778bf252235","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[46.39,38.566,38.379,38.996,37.782,39.989,39.498,41.242,39.809,40.615,40.836,42.414,40.95,42.418,55.016,41.775,51.653,50.414,35.109,32.459],"canonicalVerifierHash":"472d3b636388ed4b3ce98242c337cdca98191dd93feeaf23b2f0bf72c2068232","duplicateNoopHash":"94cb107907f89f5fc3a85d810566e5f67ec06498ef9345e4236bce4c79443170","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":103.183,"rejected":90,"remoteConvergenceMs":7.469,"retryCount":0,"roundTripHash":"6bc869c98139d476a3b61de2ca80637ca4d956c2e99008462e6667110516d5e8","seed":4,"traceSha256":"f8cc2d86c82f00b22442e2b457b962f2e31d958be26bd26999c5035502d44d52","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.467,38.629,38.282,42.15,40.325,40.74,40.806,41.27,39.767,39.994,41.586,41.433,41.491,41.985,57.23,45.778,53.56,51.936,35.118,34.226],"canonicalVerifierHash":"60e119554dd2c84670ff85ad1f4743fe3b39bb2367a81ecfa4f978cd7e834be1","duplicateNoopHash":"69a9e28c6ba4259807f9135260188fb48763db7cd60da9f0bcbb2df689ac7fec","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":144.416,"rejected":90,"remoteConvergenceMs":7.609,"retryCount":0,"roundTripHash":"3e1eb93aba491b833fe657b72a49cb08871e51b6abd93ced58a16b4c1bb1d714","seed":5,"traceSha256":"d0e377de6c9acd5bde977020ce6b5a3b79f41ba792748eeb87617f0bbfe67073","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.108,37.719,39.147,40.512,38.794,48.313,42.081,43.037,40.641,40.491,40.295,41.537,43.677,41.3,57.906,73.687,51.8,52.947,35.527,35.012],"canonicalVerifierHash":"870bdc3fe241a36e4cafa9cd2db6550900db537148df0012129a4fc729168e44","duplicateNoopHash":"7b6d6f868d69e5c19a34b1ef7a5da5bd85c7b2c0de6b278023971d5125376186","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":118.544,"rejected":90,"remoteConvergenceMs":11.598,"retryCount":0,"roundTripHash":"a6d78c0173ff37ca01ce546bd371b25102dff424fc71e5ed158da8cc3afb9409","seed":6,"traceSha256":"4089e29162d4a1f4eeb277698a40e77dee654c5e6b024de882412828b2b868be","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.201,37.516,39.713,41.717,39.319,41.041,39.905,40.309,40.31,42.247,42.587,40.457,43.919,46.201,50.613,42.495,51.66,52.034,37.017,35.448],"canonicalVerifierHash":"07d665cbffdf875c025bb158a561350943ae3fbd5068762fd11819a6a4901942","duplicateNoopHash":"abc350378349c0c5ca8ac8718147350b24bba91d01f1b3584ac0ae8728392bbb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":80.161,"rejected":90,"remoteConvergenceMs":8.071,"retryCount":0,"roundTripHash":"e9e701572c478eb4ff07bdc25b9686439e87f671b6912b55edab9f6640bd9348","seed":7,"traceSha256":"2f479b6ed6743dedae4a36d95c58c95f225ba529fcd863e5640f49afc5e838b7","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.129,38.593,39.76,40.965,40.946,40.978,39.996,41.468,39.831,41.739,41.369,42.141,43.355,42.627,55.03,43.105,51.501,52.265,37.497,34.409],"canonicalVerifierHash":"efc2b0c123c804ed7f1847b3cfaec274bad1ea5ae14b606f04cc0a565458437f","duplicateNoopHash":"7c4a10f6a44922af7b90a06f9e3796d4915b37934f1958e659c352ef988032a7","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":103.273,"rejected":90,"remoteConvergenceMs":7.705,"retryCount":0,"roundTripHash":"2381d28c5762232a05a96531e03aabc0b196671f5f4c493cfb8a2bc1e16fed87","seed":8,"traceSha256":"18d04c6a59b74a9026ab9d6f69161a1ae3101de4fe16acc08b831a885a6a301f","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.834,39.174,38.795,40.44,39.271,41.194,40.658,43.601,41.343,41.611,41.924,42.668,42.519,42.995,61.442,43.682,53.926,52.233,36.722,35.201],"canonicalVerifierHash":"703442e3b93e7bc929da059e86ab44de647f270cefc120656f501966b6847100","duplicateNoopHash":"acb46046274da0c9262f4910b29e4fda0a2bfe3132374a35db02c9f2f89398f4","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":141.644,"rejected":90,"remoteConvergenceMs":7.618,"retryCount":0,"roundTripHash":"e63096e423e4fa5de04d55dd992a191a939e288f22077b5dba9f1f30d0312ac0","seed":9,"traceSha256":"10e7fe955f927f3626b5586eff07c5c452e7dd8fada065f6ea12839907ebfb8f","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.733,38.922,39.839,40.172,39.838,41.083,42.155,41.537,41.172,42.334,41.8,41.591,43.365,41.44,54.462,43.499,52.934,51.609,35.955,35.403],"canonicalVerifierHash":"0dedd28fa52610998313b57e5e621be6488408b8475b8082434152a7e4bfffde","duplicateNoopHash":"68de0d932dda40e9fa94ad87bce5062194f4b64d032743224b09543a35fadb04","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":100.705,"rejected":90,"remoteConvergenceMs":7.342,"retryCount":0,"roundTripHash":"e199bf3ee7ce8299f481c487ecb563b9bc5a22508107847ee32280f5af2799fa","seed":10,"traceSha256":"3aedbcc55f63d2e40d1053713da07189c675eeedafc45557893bfb44a4a4a3fa","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.498,38.556,39.795,41.577,39.977,41.042,41.511,43.853,41.041,42.895,46.261,44.983,44.574,47.707,60.593,45.665,87.455,52.551,38.349,33.884],"canonicalVerifierHash":"976df9bc169579795add78059b22aecb15add1a77fa132a762b81256d7a14ab9","duplicateNoopHash":"ecd01e95d0ba98e4c1033dd1c11ae805f94ea9c57caa7eeb3883adbdba18baa9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":113.561,"rejected":90,"remoteConvergenceMs":7.656,"retryCount":0,"roundTripHash":"6b2a4c8cae985614914a1122c691c7976d2f5fe6bc30dca6a2a54e7a718ebbdb","seed":11,"traceSha256":"d64e19b329a28294c7e145d5eea4d659b86b00fdc478f7472e57f1b93de378ef","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.739,40.542,40.551,43.729,39.819,40.788,40.066,43.627,42.671,41.092,40.639,41.983,42.974,44.624,62.852,45.833,53.606,53.153,37.775,39.038],"canonicalVerifierHash":"fe27618043429656abed9f1810fd3f7fe21fa7844cfe474e4ee27170aa6ba761","duplicateNoopHash":"75cb591df89ce4872e5d71150982ccb110460036da1a031d13af69b1a497bf44","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":133.633,"rejected":90,"remoteConvergenceMs":7.404,"retryCount":0,"roundTripHash":"08db87f541a6ad80969ae614cb3c192ee4668e76d650a3c3c83ae2205d7cae2f","seed":12,"traceSha256":"56270e40851c311cafe2638b7bdf9e162ca4190285215f3a788c34ab68e9fbfa","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.918,41.058,40.192,41.048,40.576,42.727,42.287,43.909,41.666,41.569,43.005,43.27,42.773,44.089,60.701,48.77,52.802,54.466,37.907,38.372],"canonicalVerifierHash":"1eb6c99489819977b8c62d4ac7cc2b6332253eea89c0028a0d00f543ccc80b4e","duplicateNoopHash":"a2780357577e00a64a3d6882842034f3c7a33a7fb73f76a1bb513582adc82f48","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":131.567,"rejected":90,"remoteConvergenceMs":7.536,"retryCount":0,"roundTripHash":"30a8267dc6ce65b39cf9e2f8e9d47c425c22d34ae75df7a6427dcfbf320b49ca","seed":13,"traceSha256":"04fbda33461bbb975b9e66fc1ff0fe176931fdbd9c56fe8d3b9f9abcc939ab07","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.633,38.352,39.482,41.682,39.987,43.459,40.504,43.707,40.551,45.121,41.02,45.276,42.897,42.803,57.538,43.677,52.32,53.701,40.441,35.055],"canonicalVerifierHash":"9f4f93499745ce94e2ea84804d41cdd24007491f9ad333f6994c23adaca1a97f","duplicateNoopHash":"6a79d43b71c80e3ece66ab93693e6111df9ac8633c5e8af7dfbcdc86f73f16e9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":109.43,"rejected":90,"remoteConvergenceMs":7.638,"retryCount":0,"roundTripHash":"4406199eb581980a79d880cbb483fd97b1ac2b9ecf927ac3d48a1ede8f3a8ca6","seed":14,"traceSha256":"d105bcc5f7eaa8a768bd666c3d6fb072fb97a0191a935d70e688d086e52f95c4","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[45.873,39.254,39.191,40.215,48.79,43.217,40.847,40.912,46.0,42.302,44.732,41.839,49.531,47.352,50.244,43.74,54.839,53.399,36.136,35.036],"canonicalVerifierHash":"e44921495d13f7e764fb5170d992c6806add23c1ee2a902d2ed586e78b6518f2","duplicateNoopHash":"89f952d57ff5c744f32966f233f39fdf76ff3729bca2ffb22da379572bc772cb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":92.883,"rejected":90,"remoteConvergenceMs":8.378,"retryCount":0,"roundTripHash":"885dcea93a305a0527b65c09ab8a51e42eeab8486b0a17a812adfc8a23dd0692","seed":15,"traceSha256":"fd2509f037ad595831e5af5de358a045a4944c5070aeccceca9a85e5d93f0dcb","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[45.207,38.666,53.941,48.395,40.191,48.514,57.567,52.241,43.069,42.325,43.529,42.198,44.42,43.076,50.802,45.931,53.972,53.558,36.502,35.27],"canonicalVerifierHash":"fcab45e94cbb1dd32f40022c4f2538d8a8921f6f9a993c44da5a1af856273521","duplicateNoopHash":"1a2c13daad7b021d5bf7aba90696fb2fba2a5479ff1fa49027c88e3b5e9bf237","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":60.994,"rejected":90,"remoteConvergenceMs":7.829,"retryCount":0,"roundTripHash":"23079740e8c81056d542e80219368de85f2c9ae8740e9db95182ac2d1433a5cf","seed":16,"traceSha256":"15974b2b6d314a57e29fe393296085940f83539b4485eb9ada849149db7a99cc","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.355,51.715,49.469,59.482,43.866,41.0,43.691,42.488,42.377,41.069,42.066,43.259,42.925,44.613,59.639,44.394,53.044,53.739,41.307,37.36],"canonicalVerifierHash":"0ae80e9274783e32170474a495f3c31bfa702e9acbe970cb1a04c439846eb86b","duplicateNoopHash":"834e79323bda37030a4c6af90b9a0dadade53242cd9801ba090d12a86054e645","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":120.392,"rejected":90,"remoteConvergenceMs":7.489,"retryCount":0,"roundTripHash":"5ebbea09bcb5c13323f4a6dea81b146de93f7fa134b025b2163ca48c62222abc","seed":17,"traceSha256":"53cce4ffa3236c18592a997edd684f10596e8cdceacd8402d858c284a7d9054d","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[49.467,44.71,43.636,45.982,41.814,48.413,45.053,44.539,42.78,42.877,42.961,43.139,43.006,43.679,54.975,45.695,56.242,56.375,41.806,36.42],"canonicalVerifierHash":"f3fd473e5c7029efed7f8b2b39933c0240a36cf83a5eb3286adfe377c9890da5","duplicateNoopHash":"2328c71bfb35d1fc60079b4c3572d96fac5e48f92fa4fee9baa2f9a469aa7404","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":106.131,"rejected":90,"remoteConvergenceMs":7.579,"retryCount":0,"roundTripHash":"9f8e4e09c1123b52138e32282eeb53f1d13c8317d8f3b2cda0cb11c576704704","seed":18,"traceSha256":"a542c21db854c18f4cdcb385e6ac298bf7af6282c0972913cc8fce70aec86847","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[47.146,42.487,40.341,42.014,39.654,41.161,42.36,42.155,41.277,42.494,42.556,43.597,44.991,45.765,60.337,50.106,61.64,59.293,47.294,43.482],"canonicalVerifierHash":"63e156ef82374f25fc17ac38cd7fb0ffa365fa4d6fcd29f42be9a52f729ab158","duplicateNoopHash":"1121a351ad1dd9adce4685ffd6e719a32e98c70594e4a09f928d6f996e9d1207","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":147.541,"rejected":90,"remoteConvergenceMs":8.211,"retryCount":0,"roundTripHash":"3bf2bcb94fab05718ea34aed383efbf9e411b10d95a6179ae911f90d6a57cb1a","seed":19,"traceSha256":"0d92405289eea8b6bef336eab7f6bd29373c9dfcab013fe1126707d0fc4cb0a6","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.563,45.187,41.145,48.844,39.872,40.214,40.556,45.067,40.767,41.892,41.076,43.101,41.946,44.049,56.381,44.582,53.611,53.034,35.534,36.15],"canonicalVerifierHash":"c8bdffb3ea12e788eb2a5088294f17e8705320e3c734ca0302d660b841565e56","duplicateNoopHash":"661be1cf36a3e6dfd57dfc2f78cfaaa70d9b9adf2badc7d67b4c20ac4a528c24","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":103.337,"rejected":90,"remoteConvergenceMs":7.468,"retryCount":0,"roundTripHash":"268efccad0ace19c28dc92d9c54b320c38ac129eae3a5b5f6710d15562fa7f3a","seed":20,"traceSha256":"810f5993000813418216c7c55f6a64a15516e1b6f63e77e55dc9d0beb326a59c","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[60.501,47.69,42.794,44.114,42.319,46.833,43.034,43.849,42.2,43.672,42.471,42.543,43.816,43.431,48.614,46.127,54.325,54.029,37.313,36.197],"canonicalVerifierHash":"c26d7da5ac7f287f6edff0b7633a49ceba13692f2421bf0738cd6bb044c5428e","duplicateNoopHash":"e1ca213dd003e35e7e8cb0e94b3cc3c65a6284240156492d42496a39c0216155","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":56.808,"rejected":90,"remoteConvergenceMs":8.042,"retryCount":0,"roundTripHash":"4d284bde91877dd19f45ec5357c0bbc3213b125ea5a2ab460aac161c834e56d1","seed":21,"traceSha256":"00514f57b0533bafeeb8cef85306163b05977fd156fb8d4f6e8971f43cd5c3d9","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.656,40.338,43.689,46.133,43.726,43.983,44.87,42.928,44.019,43.894,44.233,45.026,44.555,46.736,52.36,48.964,54.776,56.366,38.433,35.854],"canonicalVerifierHash":"7dc98e0042f910e161dc48692e61b71bee6f5f3c4e2c185947bcd346651c62c1","duplicateNoopHash":"0c84e6481a015a570d0181acae33fe459537c904b339fab12049bfcb9f98024a","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":82.871,"rejected":90,"remoteConvergenceMs":7.593,"retryCount":0,"roundTripHash":"8af6d9079e0d0da10a62772b0ec081dc064e6480c6545292713e51b5ee9afeda","seed":22,"traceSha256":"3b219f2a009454e6a7e145c270ea4540712ac42a2e64a3de49decb95d00cb6ec","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.593,41.554,41.368,43.211,40.044,44.42,43.599,44.835,42.662,41.886,43.865,44.796,44.135,44.727,61.328,44.845,53.198,53.122,38.052,36.161],"canonicalVerifierHash":"1f42cdfc51ff0c4a3d156d71d7af4941eda5ef046969f200459a8dfcf0df4594","duplicateNoopHash":"4a4946b0e8eb9183e6b90e464571b002e3053e95ed6510dcb24e619868058604","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":135.587,"rejected":90,"remoteConvergenceMs":8.017,"retryCount":0,"roundTripHash":"d9eb0ae47ebe116de7ffe955c9782bbc6cf6f1bacb1ff2cb44ad3528939d70d6","seed":23,"traceSha256":"a44400c0327fe034c8e1d489d65d4a1e8444a312806565991f9f90c2e7bd2427","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[62.128,39.028,40.206,40.704,40.245,42.697,41.196,41.735,41.923,42.753,43.719,44.043,42.968,44.062,47.083,44.204,53.676,54.59,38.845,36.959],"canonicalVerifierHash":"77478c60c3ae9cd45fc8dcda6a71cd0010be62120387e64242c60b1eb7c87a4e","duplicateNoopHash":"089b4218683a43b11f6a2fb52d4b660cd1db966fe14a98a652aba04a95c23a78","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":57.791,"rejected":90,"remoteConvergenceMs":7.542,"retryCount":0,"roundTripHash":"0001e26052bdd0bab5724856e8de207155cb84a9d448d7aef7c173ba4e4a96e3","seed":24,"traceSha256":"dbd479d2d15a8febd16d5f86b91f7273d4c05d57076b180f1daa3d318d89b39a","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.869,39.522,40.935,41.045,39.682,43.911,41.106,41.284,42.681,42.681,49.693,44.931,43.642,44.423,58.574,45.746,53.785,55.362,37.87,35.332],"duplicateNoopHash":"610676792608b1dd1f2e06f6dc22864d82873dd8f8cbe86ef906a8174a708d0c","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"},{"afterOperation":500,"fault":"process_restart"}],"faultScheduleSha256":"d680df6635276d5593cea5cf988ac54915d8810167aae2037195424564f25e44","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":true,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"operationCount":1000,"rejected":90,"retryCount":0,"seed":25,"traceSha256":"22a209a7f9723602fadb0c3baab2d73b8a7cef893297ade4cd87a2d61122cd5b","unresolved":0,"reconnectToLiveMs":119.822,"canonicalVerifierHash":"0b7840fc3e7157c541d7862e398acaa57ac57628f918e516746483093abf6745","roundTripHash":"44ae786ea3c9385ee3d3d5321fd63b6222cca1ea07240a307559cdbbbdbecc19","remoteConvergenceMs":14.345,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":26,"adapter":"dartvex","operationCount":1000,"traceSha256":"9c530a068ba1382fbe34f6b3bd558672f8f4538963b8ad673e4be834103e93bd","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.913,38.764,39.144,41.763,40.614,41.807,40.5,43.444,43.49,43.955,44.098,43.115,45.507,43.702,54.828,46.699,54.52,53.995,37.812,34.844],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"119ebe1f480b6c9f5df8f95cab240265bd09233c3f4b5dcd923728ef667b7a16","reconnectToLiveMs":93.406,"canonicalVerifierHash":"b8fc7d69a834b62d4a614de02cebbde60f3a9ccb93b4bec1f27e9987beced0ce","roundTripHash":"748fe55957a8fe14588430c1b62e80c9bf9bb046656f3dcd48d1691f1b33b54b","remoteConvergenceMs":8.616,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":27,"adapter":"dartvex","operationCount":1000,"traceSha256":"0135b2eafc700e90fcbcd47b19e5a1d34ab5e99f255b4b315e2161c2896ef910","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[42.93,40.698,38.802,40.734,39.554,42.733,41.464,41.965,43.322,43.074,44.661,46.889,51.343,44.828,62.997,45.716,52.834,54.405,39.715,34.962],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4ad42017a48dbc8e12d942d945792c060603e08d9b7d18872b2158673ad23d7f","reconnectToLiveMs":144.469,"canonicalVerifierHash":"16a2f4cfba0166cf6435be3b64f11efbcaa06678f991140dbacc8d4e32097661","roundTripHash":"3c1ab0d073cd7610b4cd4a7c6521c0b5e47e4534104feda430f35e148f39f367","remoteConvergenceMs":8.486,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":28,"adapter":"dartvex","operationCount":1000,"traceSha256":"03fd5bbcfd5789fb329015b4ca3015f10c5322f782f41d1a33c8739dcc250103","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.976,40.385,40.735,40.577,39.67,40.706,42.246,43.046,45.319,44.969,44.034,43.094,45.092,55.426,51.524,44.286,54.288,53.6,40.446,45.774],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"2d62e883551de7eef0494e8e419eb251320287cfd85221fa771a2ec7467bb810","reconnectToLiveMs":81.163,"canonicalVerifierHash":"b990e106461c9e852b53e3be3e6c612b61f79bacc9e5c999f8ef1de308a108a0","roundTripHash":"8231af29d3d0c7601470ec0da754f2f0fded387c6bbda084509076505efb0e9b","remoteConvergenceMs":7.87,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":29,"adapter":"dartvex","operationCount":1000,"traceSha256":"8c898c274aa218bfce9e6a4f758d9273e1401b0edeab0db3d648e233aef3c687","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[47.255,42.609,46.972,56.641,44.743,47.571,48.835,43.11,44.671,41.81,42.338,41.467,44.79,57.427,65.155,52.271,54.796,58.196,39.057,40.472],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"862b67674912aa8d134be72444f4cdb9a5b50c33276e2a90ef4ae87f302351a8","reconnectToLiveMs":123.74,"canonicalVerifierHash":"abd743722679bd1335b1944cd1a5e315a89097e014d9b43d57957012d766e41a","roundTripHash":"6954dfaff2a7993d6a4d77208f9bb004083a5722931341643974ed81212fa7e8","remoteConvergenceMs":7.814,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":30,"adapter":"dartvex","operationCount":1000,"traceSha256":"222c0dc904bf299d49547394de05a001ecc68710bc02a1b311b4beafb0beb019","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.316,39.746,41.795,39.349,41.622,40.924,42.019,43.588,42.566,43.549,42.501,43.848,42.961,43.86,52.855,45.985,53.6,55.099,38.256,34.999],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d0cd4491b844b50e85c3cbec56f3095662a981c0b8b695d7c85637b0e269aad1","reconnectToLiveMs":84.011,"canonicalVerifierHash":"1894c1445629a6547704f4dfd8bb17797b825eb585c520264c5c08dbc6366d77","roundTripHash":"bb887ac1fe5cae5ba0f2625c2af2b79493cb3fd885fdd8e1560a3aebe032b712","remoteConvergenceMs":8.221,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":31,"adapter":"dartvex","operationCount":1000,"traceSha256":"c3760666249cd5b3679cb726fd60c15bde3e2a9bd079a7800fa297f824b7d339","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.067,38.435,42.229,41.586,40.15,43.142,42.557,43.916,42.692,43.42,42.976,42.971,43.563,42.587,59.843,44.257,53.113,56.858,37.968,35.932],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"206eaacdceff3df5532e76f3e5adc4964146e8504a64cbfddb43fabf4fce28ef","reconnectToLiveMs":133.044,"canonicalVerifierHash":"47d58b681022fd8ecc3b63238639a194262bef74422dd577da50fc761551d216","roundTripHash":"1ef7b56e658c8f1715f96246a583228c7b022d4e53b86374934a608bda3567b1","remoteConvergenceMs":8.363,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":32,"adapter":"dartvex","operationCount":1000,"traceSha256":"f8a1efc44c616e628d61be595d4b092755c5da9e4347176fbf48421b326056c1","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.395,38.09,38.951,39.899,41.066,45.142,43.453,45.129,43.454,42.75,45.618,45.61,43.962,45.629,50.713,45.094,52.398,53.628,37.11,34.458],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"9f583fc4b098f02824bb5fa50c8ed9a76709c8850a94de973e02fe98003010ee","reconnectToLiveMs":72.014,"canonicalVerifierHash":"ef6d53a40b328a9729ca3c0157aa0adbe1c7cb390ab38e0c6949e03aa9a45147","roundTripHash":"b5389d9f11296e131badd2a6f0a299b125227ebc6b542936d07d8d3fe2b0c7d0","remoteConvergenceMs":7.834,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":33,"adapter":"dartvex","operationCount":1000,"traceSha256":"53b09101de072e7f39e1f605b4e64c528f99951f9ec10abc33901a08def7d6ad","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[56.413,42.389,43.907,55.68,54.379,45.684,40.959,42.074,40.881,42.851,44.363,44.94,45.928,43.854,50.77,46.444,54.412,54.12,39.875,35.767],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"0c9c2a625372e9dea51750e620c118b2702bab4e7484530ad1bd6872097db785","reconnectToLiveMs":63.871,"canonicalVerifierHash":"495cf6e87b63acba4ec386a2f17165d796435465d45f391c4cf212781f7eca5b","roundTripHash":"7a43f69fdf62c9f83ba733e4b8a6488902860126dc015c5037cd8ea03da60bde","remoteConvergenceMs":7.81,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":34,"adapter":"dartvex","operationCount":1000,"traceSha256":"41039af22db14b082c0f8d27d0f337109516f3c0ff5395bbdbd52c3f2a73e366","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.068,41.501,40.387,42.872,41.613,41.298,40.906,43.915,41.885,41.227,43.83,43.104,43.912,43.071,52.982,45.107,52.957,54.903,39.642,35.613],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"1a24ce7e3c28f5807015976bbfa3e712f6def2a8d3b8f8ed87dfb6d2903f154b","reconnectToLiveMs":55.859,"canonicalVerifierHash":"cf93ed2b3ec0f97763ae4ab9f60906018563a26a37da1abcd4ea47f5a3b27149","roundTripHash":"1fbeea46ef7e1780b7fe8db03d2cc35b54a8e469a4fd1047a159498bfb643161","remoteConvergenceMs":7.965,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":35,"adapter":"dartvex","operationCount":1000,"traceSha256":"b9275a1393e700c6b85ebf43295ee9fbf7e0215d3e64d41b229fd4d0d1d16252","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.802,39.553,38.8,41.922,40.214,42.684,46.172,43.635,42.022,42.207,42.745,43.881,44.486,43.689,57.625,45.809,53.006,54.095,38.201,37.126],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"519d837de5b7f860793ea33c7269be33f2d3286b79c6e9e9507418df34faf035","reconnectToLiveMs":110.436,"canonicalVerifierHash":"887321746985c1c8bd0822300fe6373a4ef21380ef5ecc9c335f47e8cf9a8271","roundTripHash":"b9cea9c0d0df6e43effaaff688e7e8b7dcea72d7f88d91df5d70c9dcd51b3ece","remoteConvergenceMs":7.915,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":36,"adapter":"dartvex","operationCount":1000,"traceSha256":"c0c7e0207a6bcd0957b602550570e42db42800fe57c66626010f25242d5f8208","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.249,40.382,41.024,43.066,41.622,40.654,41.606,43.465,42.895,43.573,44.157,42.572,45.476,46.946,58.786,47.9,55.686,55.375,39.003,36.92],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"44641ee26407719b887dc44016ae6afe719ff28a61bf2ac2adff13418a002f2d","reconnectToLiveMs":115.353,"canonicalVerifierHash":"b25494e6ef17124b508fe8d47b13d055664336898cf2192f495b01b2037d7c0e","roundTripHash":"d14c83263732d3d4ce5c1ddffa521f4a8e139a0115a0b13204c75c5d5ded723f","remoteConvergenceMs":7.781,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":37,"adapter":"dartvex","operationCount":1000,"traceSha256":"78c97698051da2fb983461e5c8f6d2d3bd357afb10e6d42cf9918b98c8804b5a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[49.407,48.095,41.143,45.358,42.334,42.217,41.384,42.518,41.535,43.983,43.604,43.756,45.543,43.932,52.246,45.829,55.157,54.926,39.098,35.873],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"36b5339d6a5239af0c2e64c5a19e64b084ad9ce6a27851969945465a67dbb365","reconnectToLiveMs":55.789,"canonicalVerifierHash":"c9bd2baab0265dc7a190e0cd455f13e2575f7a04ebe2a01afecfb5d78ea1cc65","roundTripHash":"6ddbcafed928297a094e02cca9d9201fd12c225dd55b7df8a267aadd5e671715","remoteConvergenceMs":7.733,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":38,"adapter":"dartvex","operationCount":1000,"traceSha256":"3f2dafd0bc96122b9fd4274b15b67a6834f00aefcce0e9fd414dc56bee7ee67b","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.566,40.847,39.499,42.278,40.063,42.286,42.524,41.937,42.754,42.753,43.108,44.384,43.539,47.108,57.846,45.084,55.035,53.792,38.475,36.43],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4df6eb45193bcf8a0edcab57b140e5e711a42313537c1b06ad7ac40495a6ed7d","reconnectToLiveMs":93.025,"canonicalVerifierHash":"dac23c9b615ba736f667822e98cd81583d5596d3eb635400fd7923a4b821f2fd","roundTripHash":"5f2b2d6429deabacf2274aa4cd918f601a4d7979d4e6425eac0dabff353df84e","remoteConvergenceMs":7.919,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":39,"adapter":"dartvex","operationCount":1000,"traceSha256":"7b901f84dd62107f244be49a92208d42d8c9d3da353225163c2eae58533cc5e7","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.047,40.451,46.947,50.225,45.229,49.088,45.091,45.374,43.693,54.021,43.871,43.63,43.931,46.391,59.18,46.671,55.434,53.924,37.57,35.574],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"3a0e6a8d108e4259c6dacb8b38f8b350945dc7fa4e9364e3913d2caddffe18bd","reconnectToLiveMs":117.338,"canonicalVerifierHash":"1be630ac18ad34887058f68f246be9e0f8a79ae0b431e0342f77f0aafbbf60a6","roundTripHash":"010a6713bd4cb32e2f00459f2d042fbfe23920beb3dba219f9a975fd66606098","remoteConvergenceMs":7.653,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":40,"adapter":"dartvex","operationCount":1000,"traceSha256":"cd40e9a07822696a4198119192a3961886f52674873dbed1fab915a6e50927d3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.249,40.932,39.573,41.464,42.796,41.038,41.923,43.221,43.095,42.169,42.496,43.102,43.309,46.884,54.51,45.429,53.818,55.279,39.033,37.03],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"a91db8c869cb35510ed31cccd264a1874ddc908ddfb04db8e65f8148f5a91578","reconnectToLiveMs":94.183,"canonicalVerifierHash":"25122c18d674ea9d81866fefb9811438d5d0e6930edbb7b4f1bd1336987d3c84","roundTripHash":"f83c2ea15ce0f99cd935fd0aaeffe3bac0805ab6b6f1d45c6ba5e715257fbe02","remoteConvergenceMs":7.872,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":41,"adapter":"dartvex","operationCount":1000,"traceSha256":"3c6017b5b1b61473fc4384d5c642f8c4f21fc498a2cba3981a4fcb04e8e8f815","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.202,41.734,38.9,41.964,40.159,43.179,42.62,44.212,42.695,42.838,64.628,43.192,43.064,44.441,55.591,45.378,53.622,54.908,38.092,36.853],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"f46fbceca36e6df65f09e8184dc171220da87ffa997e93edae4425e494a5644d","reconnectToLiveMs":92.081,"canonicalVerifierHash":"44e6edd99da52fe8e35e1513170aea2419cc5cb214cf506ff75a706440274ae3","roundTripHash":"519c1ac9267bb4369abd0b2bbc015623f2f8c3a7c58946522c36df8a6028a425","remoteConvergenceMs":7.711,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":42,"adapter":"dartvex","operationCount":1000,"traceSha256":"2a3bb8f9e96bbff75da1c0bdcd21b3452ff8c8762013e2f6eca2ca641b707758","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.321,38.968,40.478,42.917,41.37,41.193,42.959,42.614,41.83,42.802,43.298,44.202,44.888,43.443,49.671,45.82,53.469,54.865,38.49,37.034],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"86043e9b248940531840e7cd18115beb2b0dddcf5dc9db88ab096d6ece3d17a4","reconnectToLiveMs":60.052,"canonicalVerifierHash":"94b10f4001fe9e25842dfeaa8246258d85efe73aa823a1f32a587317b43cc262","roundTripHash":"99affcac47d7e010b61d60b84890b99227a88f96e686afecc64ecc96e1a9e434","remoteConvergenceMs":7.848,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":43,"adapter":"dartvex","operationCount":1000,"traceSha256":"3f8a10342efb6e53884d12c1e1425f2ec7ecdd96013bd9223c4475968eca8e3e","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[47.523,40.482,41.034,42.895,41.981,43.041,42.636,42.342,42.693,42.708,43.526,41.996,43.613,44.427,61.544,45.426,52.307,53.03,36.734,36.247],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"19c0e27e7c3cdf297feaa3b02930adcda8b97b4c388b33ea1769e9afdf522ba2","reconnectToLiveMs":127.576,"canonicalVerifierHash":"3f91d6fc35170cf6b81469cd10fdc2276151f41642c183954c905dc2fd72e913","roundTripHash":"2d5a3bcc5c59f2b66c302ee900ea418589d560867b9df6c4ad8433a1d8132683","remoteConvergenceMs":7.84,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":44,"adapter":"dartvex","operationCount":1000,"traceSha256":"0eb474ca0c5f15b2c3bb2837e8ccb7540cc953610e19a90021b5d7fcc17d7e92","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.072,39.315,40.558,40.769,41.745,42.567,41.497,43.374,42.49,43.383,47.716,42.808,43.982,44.344,51.766,46.453,53.558,53.149,40.62,35.813],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d296acd1d505cc41e1746ba8b4f51adf18849dbc0a28408dd5e3bf8e616d4672","reconnectToLiveMs":78.388,"canonicalVerifierHash":"45675064eb12cf1f1a91352bc3ec835026de943504172dfe66afe47547538963","roundTripHash":"db68375a5b6b0d90d87a15cb7be4f53702d3686f539bd3c6e4e91fe56aecf1f5","remoteConvergenceMs":7.863,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":45,"adapter":"dartvex","operationCount":1000,"traceSha256":"48fb865b80110a4ec663a65f26f9bffa285ecb926bc3654aaac732c60d7b3b0a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[47.781,41.573,41.634,43.712,42.764,42.992,45.301,46.381,44.41,43.129,42.277,151.589,47.034,46.388,50.115,46.329,54.85,54.701,42.267,35.203],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"129dec31e4e2f2823c59ae9b87fcae42d394591c61f5be778999c45da2a3e444","reconnectToLiveMs":116.66,"canonicalVerifierHash":"df97c65603a19f4c4351462abc77ddac04994d60de19108557822d5b7a571794","roundTripHash":"bd72d07f8cfa10ba473e751e8571d18cff71139e9de28b389a357005b0549039","remoteConvergenceMs":8.223,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":46,"adapter":"dartvex","operationCount":1000,"traceSha256":"2547f5ef76c4962e0f352c8e03a471c880bc8ca5b6172b583e469813db94f886","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.44,39.479,41.034,42.152,43.157,40.919,41.765,42.062,45.484,43.057,44.835,44.047,45.141,44.576,63.414,45.143,53.287,55.136,56.647,42.646],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"50395ad41530e80d5559fc8b6af5507a8a393adec21dbaf5ef71718f035be3ef","reconnectToLiveMs":95.06,"canonicalVerifierHash":"9b4ee333fd4009d9b1edef10a8a06c285dae7b061b309176dac94fb8296f1af3","roundTripHash":"0574858639d852e4499b0d575c1ed16d92e54431275cbba318a6c50a5938a9f4","remoteConvergenceMs":8.205,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":47,"adapter":"dartvex","operationCount":1000,"traceSha256":"74fe76755a9c32233697d6c69e3751a5a0429d237d306cfa1e17d9b51b4a8569","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[50.378,44.705,47.392,41.014,40.584,42.299,40.686,45.283,41.684,44.283,42.74,43.751,43.668,44.643,62.479,47.431,53.854,55.571,37.789,35.194],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e3d92a8e2ae00349f3b3869363ede60248488f17094c62af9e2b17952177dc57","reconnectToLiveMs":138.369,"canonicalVerifierHash":"3c4caaedf4de1dfe50de5ca3aedad7fd6de3694157191abd14d816ba949321c5","roundTripHash":"f055ebed68cf1f028f3ac92152eb9804730bd979cbe5771187ea7d8a93f1b604","remoteConvergenceMs":8.109,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":48,"adapter":"dartvex","operationCount":1000,"traceSha256":"34d1c2996c4711c53b654ccb9172bdd508ff7023bd0729c37305aff833a9e8c3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.419,41.861,42.94,42.77,42.33,45.674,44.62,43.67,43.376,43.153,43.297,49.236,48.042,45.798,55.783,48.412,58.471,55.677,39.589,36.652],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e7e8f860ed711b1ba4c898d3c6a3a7443cdcb343f3b965d64050f635bebb045a","reconnectToLiveMs":91.068,"canonicalVerifierHash":"882b979772b174022efc16c448d930c4c985077f663f74e281e64d2730ca2d9e","roundTripHash":"d800ff5ea7d263d57c07e1a252a14b4b8787f97831d2de8d8608dfa3a4187f23","remoteConvergenceMs":8.123,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":49,"adapter":"dartvex","operationCount":1000,"traceSha256":"cd10c09eb29b4c21d63fff22d70e92b231eb178d82ab60ea4f2dd0b671e6f0c5","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[50.548,42.99,42.037,43.348,42.71,42.28,42.663,42.426,44.515,45.914,44.38,46.533,54.825,48.929,63.585,46.105,54.925,55.403,38.999,37.768],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"8d2697434c6eed85928903a55f2524696d540ea4d20637d254aaa9ff692314fc","reconnectToLiveMs":128.215,"canonicalVerifierHash":"897c1e45d48565825398a418ad07d9557552005e11b69982d8979da0e6def54e","roundTripHash":"a93b4e6243f97268681c90698b8754de9e2cf2decf12fb6104cb376aa9ace707","remoteConvergenceMs":7.968,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10}]} \ No newline at end of file diff --git a/tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json b/tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json new file mode 100644 index 00000000..8ed5db5b --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json @@ -0,0 +1,68 @@ +{ + "schemaVersion": 1, + "evaluation": "convex_dart_client_fair_rerun", + "harnessCommit": "bf421ffef06b5d04749c77bda182f8f0a53796fe", + "deployment": "local:127.0.0.1:3210", + "baseFixture": { + "path": "test/fixtures/strategy_integrity/base-test-v43.ica", + "sha256": "8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a" + }, + "tooling": { + "dartvex": { + "version": "0.2.0", + "strictGatePassed": true, + "explicitResultRenameCaught": true, + "missingReturnRejectedByIcarusWrapper": true, + "unsupportedValidatorRejectedByIcarusWrapper": true, + "deterministicGeneration": true, + "stableReturnSurfaceCompleteForRuntime": false + }, + "convex_flutter": { + "version": "3.0.1", + "generatedContractBoundary": false, + "flutterRustBridgeVersion": "2.11.1 pinned", + "resolvedFlutterRustBridgeWithoutPin": "2.13.0 incompatible with packaged 2.11.1 bindings" + } + }, + "correctness": { + "equivalentSeedZeroTrace": true, + "equivalentSeedZeroFaultSchedule": true, + "dartvex": { + "status": "passed", + "seedsCompleted": 50, + "operationsCompleted": 50000, + "acknowledged": 45500, + "visibleRevisionRejects": 4500, + "unresolved": 0, + "authRefreshAccepted": true, + "queuedBatchReplayedExactlyOnce": true, + "processRestartRecovered": true, + "allCanonicalVerifierHashesPassed": true, + "allIcaRoundTripsPassed": true, + "reportSha256": "284ba45543dc5ec76932d0b949569f7973a452222651dbd028fdac631e7fed8e" + }, + "convex_flutter": { + "status": "failed", + "seed": 0, + "operationsCompletedBeforeFailure": 500, + "acknowledged": 450, + "visibleRevisionRejects": 50, + "unresolvedBeforeFault": 0, + "losingCondition": "auth_refresh_recovery_failed", + "freshTokenAccepted": false, + "queuedBatchReplayedExactlyOnce": false, + "reportSha256": "9e9fbcc00ef9665338e6aa3d1b8d17fd3f8a81710914eb687858013e151de520" + } + }, + "profile": { + "status": "blocked_by_correctness_gate", + "pairedRuns": 0, + "reason": "convex_flutter did not recover queued work after Supabase refreshSession and reconnect" + }, + "verdict": { + "runtimeGateWinner": "dartvex", + "adoptionWinner": null, + "applicationDependencyChanged": false, + "nextStep": "Do not migrate yet. Complete and prove Dartvex's stable generated return surface, and separately fix or replace convex_flutter auth recovery before another comparison." + } +} From caae3f911e553841c948269635ffbdafecc72c92 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 27 Aug 2026 02:43:05 -0400 Subject: [PATCH 08/11] fix: repair native Convex auth recovery --- pubspec.lock | 7 +- pubspec.yaml | 3 +- third_party/convex_flutter/ARCHITECTURE.md | 784 ++++++ third_party/convex_flutter/CHANGELOG.md | 234 ++ third_party/convex_flutter/CONTRIBUTING.md | 504 ++++ third_party/convex_flutter/ICARUS_PATCH.md | 28 + third_party/convex_flutter/LICENSE | 21 + third_party/convex_flutter/MIGRATION_v3.md | 403 +++ .../convex_flutter/PLATFORM_CONFIGURATION.md | 264 ++ .../convex_flutter/PUB_DEPLOY_GUIDE.md | 332 +++ third_party/convex_flutter/README.md | 493 ++++ .../convex_flutter/analysis_options.yaml | 4 + .../convex_flutter/android/build.gradle | 56 + .../convex_flutter/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + third_party/convex_flutter/build.yaml | 6 + third_party/convex_flutter/cargokit/LICENSE | 42 + third_party/convex_flutter/cargokit/README | 11 + .../convex_flutter/cargokit/build_pod.sh | 58 + .../cargokit/build_tool/README.md | 5 + .../cargokit/build_tool/analysis_options.yaml | 34 + .../cargokit/build_tool/bin/build_tool.dart | 8 + .../cargokit/build_tool/lib/build_tool.dart | 8 + .../lib/src/android_environment.dart | 195 ++ .../lib/src/artifacts_provider.dart | 266 ++ .../build_tool/lib/src/build_cmake.dart | 40 + .../build_tool/lib/src/build_gradle.dart | 49 + .../build_tool/lib/src/build_pod.dart | 89 + .../build_tool/lib/src/build_tool.dart | 271 ++ .../cargokit/build_tool/lib/src/builder.dart | 198 ++ .../cargokit/build_tool/lib/src/cargo.dart | 48 + .../build_tool/lib/src/crate_hash.dart | 124 + .../build_tool/lib/src/environment.dart | 68 + .../cargokit/build_tool/lib/src/logging.dart | 52 + .../cargokit/build_tool/lib/src/options.dart | 309 +++ .../lib/src/precompile_binaries.dart | 202 ++ .../cargokit/build_tool/lib/src/rustup.dart | 136 + .../cargokit/build_tool/lib/src/target.dart | 140 + .../cargokit/build_tool/lib/src/util.dart | 172 ++ .../build_tool/lib/src/verify_binaries.dart | 84 + .../cargokit/build_tool/pubspec.lock | 453 ++++ .../cargokit/build_tool/pubspec.yaml | 33 + .../cargokit/cmake/cargokit.cmake | 99 + .../cargokit/cmake/resolve_symlinks.ps1 | 34 + .../cargokit/gradle/plugin.gradle | 179 ++ .../cargokit/run_build_tool.cmd | 91 + .../convex_flutter/cargokit/run_build_tool.sh | 99 + .../convex_flutter/example/HEALTH_CHECK.md | 87 + third_party/convex_flutter/example/README.md | 20 + .../example/analysis_options.yaml | 28 + .../example/android/app/build.gradle.kts | 44 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 46 + .../convex_flutter_example/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../example/android/build.gradle.kts | 21 + .../example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle.kts | 25 + .../example/integration_test/simple_test.dart | 11 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../convex_flutter/example/ios/Podfile | 43 + .../convex_flutter/example/ios/Podfile.lock | 28 + .../ios/Runner.xcodeproj/project.pbxproj | 731 ++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 + .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 49 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/ios/RunnerTests/RunnerTests.swift | 12 + .../convex_flutter/example/lib/main.dart | 210 ++ .../example/lib/screens/advanced_screen.dart | 225 ++ .../lib/screens/authentication_screen.dart | 224 ++ .../lib/screens/connection_screen.dart | 199 ++ .../example/lib/screens/home_screen.dart | 155 ++ .../example/lib/screens/messaging_screen.dart | 206 ++ .../widgets/connection_status_indicator.dart | 66 + .../example/linux/CMakeLists.txt | 128 + .../example/linux/flutter/CMakeLists.txt | 88 + .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 24 + .../example/linux/runner/CMakeLists.txt | 26 + .../example/linux/runner/main.cc | 6 + .../example/linux/runner/my_application.cc | 130 + .../example/linux/runner/my_application.h | 18 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 10 + .../convex_flutter/example/macos/Podfile | 42 + .../convex_flutter/example/macos/Podfile.lock | 22 + .../macos/Runner.xcodeproj/project.pbxproj | 801 ++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 + .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 12 + .../macos/RunnerTests/RunnerTests.swift | 12 + .../convex_flutter/example/pubspec.yaml | 99 + .../example/screenshots/app_screenshot.png | Bin 0 -> 276212 bytes .../screenshots/messaging_screenshot.png | Bin 0 -> 211364 bytes .../example/test/widget_test.dart | 30 + .../convex_flutter/example/web/favicon.png | Bin 0 -> 917 bytes .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../convex_flutter/example/web/index.html | 38 + .../convex_flutter/example/web/manifest.json | 35 + .../example/windows/CMakeLists.txt | 108 + .../example/windows/flutter/CMakeLists.txt | 109 + .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 24 + .../example/windows/runner/CMakeLists.txt | 40 + .../example/windows/runner/Runner.rc | 121 + .../example/windows/runner/flutter_window.cpp | 71 + .../example/windows/runner/flutter_window.h | 33 + .../example/windows/runner/main.cpp | 43 + .../example/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 14 + .../example/windows/runner/utils.cpp | 65 + .../example/windows/runner/utils.h | 19 + .../example/windows/runner/win32_window.cpp | 288 +++ .../example/windows/runner/win32_window.h | 102 + .../convex_flutter/flutter_rust_bridge.yaml | 3 + .../convex_flutter/ios/Classes/dummy_file.c | 1 + .../convex_flutter/ios/convex_flutter.podspec | 45 + .../convex_flutter/lib/convex_flutter.dart | 9 + .../lib/convex_flutter_web.dart | 18 + .../lib/src/app_lifecycle_event.dart | 26 + .../lib/src/app_lifecycle_observer.dart | 45 + .../lib/src/connection_status.dart | 15 + .../convex_flutter/lib/src/convex_client.dart | 426 ++++ .../convex_flutter/lib/src/convex_config.dart | 53 + .../lib/src/impl/convex_client_factory.dart | 23 + .../src/impl/convex_client_factory_io.dart | 12 + .../src/impl/convex_client_factory_web.dart | 12 + .../lib/src/impl/convex_client_interface.dart | 162 ++ .../lib/src/impl/convex_client_native.dart | 274 ++ .../lib/src/impl/convex_client_web.dart | 875 +++++++ .../lib/src/rust/frb_generated.dart | 2247 +++++++++++++++++ .../lib/src/rust/frb_generated.io.dart | 738 ++++++ .../lib/src/rust/frb_generated.web.dart | 698 +++++ .../convex_flutter/lib/src/rust/lib.dart | 162 ++ .../lib/src/rust/lib.freezed.dart | 378 +++ third_party/convex_flutter/lib/src/utils.dart | 5 + .../convex_flutter/linux/CMakeLists.txt | 19 + .../convex_flutter/macos/Classes/dummy_file.c | 1 + .../macos/convex_flutter.podspec | 44 + third_party/convex_flutter/pubspec.yaml | 95 + third_party/convex_flutter/rust/Cargo.lock | 2189 ++++++++++++++++ third_party/convex_flutter/rust/Cargo.toml | 27 + .../convex_flutter/rust/example/lib/main.dart | 195 ++ .../convex_flutter/rust/src/frb_generated.rs | 1954 ++++++++++++++ third_party/convex_flutter/rust/src/lib.rs | 535 ++++ .../test_driver/integration_test.dart | 3 + .../convex_flutter/windows/CMakeLists.txt | 20 + .../runtime/app/pubspec.lock | 7 +- .../runtime/lib/runner.dart | 19 +- .../runtime/lib/transport.dart | 40 +- .../runtime/pubspec.lock | 7 +- .../runtime/pubspec.yaml | 3 +- .../runtime/test/workload_test.dart | 12 + 221 files changed, 24307 insertions(+), 25 deletions(-) create mode 100644 third_party/convex_flutter/ARCHITECTURE.md create mode 100644 third_party/convex_flutter/CHANGELOG.md create mode 100644 third_party/convex_flutter/CONTRIBUTING.md create mode 100644 third_party/convex_flutter/ICARUS_PATCH.md create mode 100644 third_party/convex_flutter/LICENSE create mode 100644 third_party/convex_flutter/MIGRATION_v3.md create mode 100644 third_party/convex_flutter/PLATFORM_CONFIGURATION.md create mode 100644 third_party/convex_flutter/PUB_DEPLOY_GUIDE.md create mode 100644 third_party/convex_flutter/README.md create mode 100644 third_party/convex_flutter/analysis_options.yaml create mode 100644 third_party/convex_flutter/android/build.gradle create mode 100644 third_party/convex_flutter/android/settings.gradle create mode 100644 third_party/convex_flutter/android/src/main/AndroidManifest.xml create mode 100644 third_party/convex_flutter/build.yaml create mode 100644 third_party/convex_flutter/cargokit/LICENSE create mode 100644 third_party/convex_flutter/cargokit/README create mode 100755 third_party/convex_flutter/cargokit/build_pod.sh create mode 100644 third_party/convex_flutter/cargokit/build_tool/README.md create mode 100644 third_party/convex_flutter/cargokit/build_tool/analysis_options.yaml create mode 100644 third_party/convex_flutter/cargokit/build_tool/bin/build_tool.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/build_tool.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/android_environment.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/artifacts_provider.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/build_cmake.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/build_gradle.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/build_pod.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/build_tool.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/builder.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/cargo.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/crate_hash.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/environment.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/logging.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/options.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/precompile_binaries.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/rustup.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/target.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/util.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/lib/src/verify_binaries.dart create mode 100644 third_party/convex_flutter/cargokit/build_tool/pubspec.lock create mode 100644 third_party/convex_flutter/cargokit/build_tool/pubspec.yaml create mode 100644 third_party/convex_flutter/cargokit/cmake/cargokit.cmake create mode 100644 third_party/convex_flutter/cargokit/cmake/resolve_symlinks.ps1 create mode 100644 third_party/convex_flutter/cargokit/gradle/plugin.gradle create mode 100755 third_party/convex_flutter/cargokit/run_build_tool.cmd create mode 100755 third_party/convex_flutter/cargokit/run_build_tool.sh create mode 100644 third_party/convex_flutter/example/HEALTH_CHECK.md create mode 100644 third_party/convex_flutter/example/README.md create mode 100644 third_party/convex_flutter/example/analysis_options.yaml create mode 100644 third_party/convex_flutter/example/android/app/build.gradle.kts create mode 100644 third_party/convex_flutter/example/android/app/src/debug/AndroidManifest.xml create mode 100644 third_party/convex_flutter/example/android/app/src/main/AndroidManifest.xml create mode 100644 third_party/convex_flutter/example/android/app/src/main/kotlin/com/example/convex_flutter_example/MainActivity.kt create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/values-night/styles.xml create mode 100644 third_party/convex_flutter/example/android/app/src/main/res/values/styles.xml create mode 100644 third_party/convex_flutter/example/android/app/src/profile/AndroidManifest.xml create mode 100644 third_party/convex_flutter/example/android/build.gradle.kts create mode 100644 third_party/convex_flutter/example/android/gradle.properties create mode 100644 third_party/convex_flutter/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 third_party/convex_flutter/example/android/settings.gradle.kts create mode 100644 third_party/convex_flutter/example/integration_test/simple_test.dart create mode 100644 third_party/convex_flutter/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 third_party/convex_flutter/example/ios/Flutter/Debug.xcconfig create mode 100644 third_party/convex_flutter/example/ios/Flutter/Release.xcconfig create mode 100644 third_party/convex_flutter/example/ios/Podfile create mode 100644 third_party/convex_flutter/example/ios/Podfile.lock create mode 100644 third_party/convex_flutter/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 third_party/convex_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 third_party/convex_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 third_party/convex_flutter/example/ios/Runner/AppDelegate.swift create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 third_party/convex_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 third_party/convex_flutter/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 third_party/convex_flutter/example/ios/Runner/Info.plist create mode 100644 third_party/convex_flutter/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 third_party/convex_flutter/example/ios/RunnerTests/RunnerTests.swift create mode 100644 third_party/convex_flutter/example/lib/main.dart create mode 100644 third_party/convex_flutter/example/lib/screens/advanced_screen.dart create mode 100644 third_party/convex_flutter/example/lib/screens/authentication_screen.dart create mode 100644 third_party/convex_flutter/example/lib/screens/connection_screen.dart create mode 100644 third_party/convex_flutter/example/lib/screens/home_screen.dart create mode 100644 third_party/convex_flutter/example/lib/screens/messaging_screen.dart create mode 100644 third_party/convex_flutter/example/lib/widgets/connection_status_indicator.dart create mode 100644 third_party/convex_flutter/example/linux/CMakeLists.txt create mode 100644 third_party/convex_flutter/example/linux/flutter/CMakeLists.txt create mode 100644 third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.cc create mode 100644 third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.h create mode 100644 third_party/convex_flutter/example/linux/flutter/generated_plugins.cmake create mode 100644 third_party/convex_flutter/example/linux/runner/CMakeLists.txt create mode 100644 third_party/convex_flutter/example/linux/runner/main.cc create mode 100644 third_party/convex_flutter/example/linux/runner/my_application.cc create mode 100644 third_party/convex_flutter/example/linux/runner/my_application.h create mode 100644 third_party/convex_flutter/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 third_party/convex_flutter/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 third_party/convex_flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 third_party/convex_flutter/example/macos/Podfile create mode 100644 third_party/convex_flutter/example/macos/Podfile.lock create mode 100644 third_party/convex_flutter/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 third_party/convex_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 third_party/convex_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 third_party/convex_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 third_party/convex_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 third_party/convex_flutter/example/macos/Runner/AppDelegate.swift create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 third_party/convex_flutter/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 third_party/convex_flutter/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 third_party/convex_flutter/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 third_party/convex_flutter/example/macos/Runner/Configs/Release.xcconfig create mode 100644 third_party/convex_flutter/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 third_party/convex_flutter/example/macos/Runner/DebugProfile.entitlements create mode 100644 third_party/convex_flutter/example/macos/Runner/Info.plist create mode 100644 third_party/convex_flutter/example/macos/Runner/MainFlutterWindow.swift create mode 100644 third_party/convex_flutter/example/macos/Runner/Release.entitlements create mode 100644 third_party/convex_flutter/example/macos/RunnerTests/RunnerTests.swift create mode 100644 third_party/convex_flutter/example/pubspec.yaml create mode 100644 third_party/convex_flutter/example/screenshots/app_screenshot.png create mode 100644 third_party/convex_flutter/example/screenshots/messaging_screenshot.png create mode 100644 third_party/convex_flutter/example/test/widget_test.dart create mode 100644 third_party/convex_flutter/example/web/favicon.png create mode 100644 third_party/convex_flutter/example/web/icons/Icon-192.png create mode 100644 third_party/convex_flutter/example/web/icons/Icon-512.png create mode 100644 third_party/convex_flutter/example/web/icons/Icon-maskable-192.png create mode 100644 third_party/convex_flutter/example/web/icons/Icon-maskable-512.png create mode 100644 third_party/convex_flutter/example/web/index.html create mode 100644 third_party/convex_flutter/example/web/manifest.json create mode 100644 third_party/convex_flutter/example/windows/CMakeLists.txt create mode 100644 third_party/convex_flutter/example/windows/flutter/CMakeLists.txt create mode 100644 third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.cc create mode 100644 third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.h create mode 100644 third_party/convex_flutter/example/windows/flutter/generated_plugins.cmake create mode 100644 third_party/convex_flutter/example/windows/runner/CMakeLists.txt create mode 100644 third_party/convex_flutter/example/windows/runner/Runner.rc create mode 100644 third_party/convex_flutter/example/windows/runner/flutter_window.cpp create mode 100644 third_party/convex_flutter/example/windows/runner/flutter_window.h create mode 100644 third_party/convex_flutter/example/windows/runner/main.cpp create mode 100644 third_party/convex_flutter/example/windows/runner/resource.h create mode 100644 third_party/convex_flutter/example/windows/runner/resources/app_icon.ico create mode 100644 third_party/convex_flutter/example/windows/runner/runner.exe.manifest create mode 100644 third_party/convex_flutter/example/windows/runner/utils.cpp create mode 100644 third_party/convex_flutter/example/windows/runner/utils.h create mode 100644 third_party/convex_flutter/example/windows/runner/win32_window.cpp create mode 100644 third_party/convex_flutter/example/windows/runner/win32_window.h create mode 100644 third_party/convex_flutter/flutter_rust_bridge.yaml create mode 100644 third_party/convex_flutter/ios/Classes/dummy_file.c create mode 100644 third_party/convex_flutter/ios/convex_flutter.podspec create mode 100644 third_party/convex_flutter/lib/convex_flutter.dart create mode 100644 third_party/convex_flutter/lib/convex_flutter_web.dart create mode 100644 third_party/convex_flutter/lib/src/app_lifecycle_event.dart create mode 100644 third_party/convex_flutter/lib/src/app_lifecycle_observer.dart create mode 100644 third_party/convex_flutter/lib/src/connection_status.dart create mode 100644 third_party/convex_flutter/lib/src/convex_client.dart create mode 100644 third_party/convex_flutter/lib/src/convex_config.dart create mode 100644 third_party/convex_flutter/lib/src/impl/convex_client_factory.dart create mode 100644 third_party/convex_flutter/lib/src/impl/convex_client_factory_io.dart create mode 100644 third_party/convex_flutter/lib/src/impl/convex_client_factory_web.dart create mode 100644 third_party/convex_flutter/lib/src/impl/convex_client_interface.dart create mode 100644 third_party/convex_flutter/lib/src/impl/convex_client_native.dart create mode 100644 third_party/convex_flutter/lib/src/impl/convex_client_web.dart create mode 100644 third_party/convex_flutter/lib/src/rust/frb_generated.dart create mode 100644 third_party/convex_flutter/lib/src/rust/frb_generated.io.dart create mode 100644 third_party/convex_flutter/lib/src/rust/frb_generated.web.dart create mode 100644 third_party/convex_flutter/lib/src/rust/lib.dart create mode 100644 third_party/convex_flutter/lib/src/rust/lib.freezed.dart create mode 100644 third_party/convex_flutter/lib/src/utils.dart create mode 100644 third_party/convex_flutter/linux/CMakeLists.txt create mode 100644 third_party/convex_flutter/macos/Classes/dummy_file.c create mode 100644 third_party/convex_flutter/macos/convex_flutter.podspec create mode 100644 third_party/convex_flutter/pubspec.yaml create mode 100644 third_party/convex_flutter/rust/Cargo.lock create mode 100644 third_party/convex_flutter/rust/Cargo.toml create mode 100644 third_party/convex_flutter/rust/example/lib/main.dart create mode 100644 third_party/convex_flutter/rust/src/frb_generated.rs create mode 100644 third_party/convex_flutter/rust/src/lib.rs create mode 100644 third_party/convex_flutter/test_driver/integration_test.dart create mode 100644 third_party/convex_flutter/windows/CMakeLists.txt diff --git a/pubspec.lock b/pubspec.lock index d44386be..5457903a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -252,10 +252,9 @@ packages: convex_flutter: dependency: "direct main" description: - name: convex_flutter - sha256: db3bca4e3e6792eadadba9a662225ccb743c287f4550879f67885cd40a2198f2 - url: "https://pub.dev" - source: hosted + path: "third_party/convex_flutter" + relative: true + source: path version: "3.0.1" cross_file: dependency: "direct main" diff --git a/pubspec.yaml b/pubspec.yaml index 5854699d..f116d5bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,8 @@ dependencies: pasteboard: ^0.4.0 desktop_updater: ^1.4.0 cryptography_plus: ^2.7.1 - convex_flutter: ^3.0.1 + convex_flutter: + path: third_party/convex_flutter supabase: ^2.10.2 supabase_flutter: ^2.12.0 win32_registry: ^2.1.0 diff --git a/third_party/convex_flutter/ARCHITECTURE.md b/third_party/convex_flutter/ARCHITECTURE.md new file mode 100644 index 00000000..b66d6013 --- /dev/null +++ b/third_party/convex_flutter/ARCHITECTURE.md @@ -0,0 +1,784 @@ +# Architecture & Platform Support - convex_flutter + +## v3.0.0 Major Update: Web Platform Support 🌐 + +**convex_flutter** now supports **ALL Flutter platforms** including web! The package intelligently uses different implementations based on the target platform: + +- **Web**: Pure Dart implementation (no Rust required) +- **Native** (Android, iOS, macOS, Windows, Linux): FFI + Rust SDK + +## Table of Contents +- [Platform Architecture Overview](#platform-architecture-overview) +- [Web Platform Implementation (NEW in v3.0.0)](#web-platform-implementation-new-in-v300) +- [Native Platform Implementation](#native-platform-implementation) +- [Why Rust is Required (Native Only)](#why-rust-is-required-native-only) +- [Who Needs Rust Installed](#who-needs-rust-installed) +- [How the Package Works](#how-the-package-works) +- [Can Rust Dependency Be Removed?](#can-rust-dependency-be-removed) +- [Alternatives & Tradeoffs](#alternatives--tradeoffs) +- [Impact on Developers](#impact-on-developers) +- [Future Possibilities](#future-possibilities) + +--- + +## Platform Architecture Overview + +### Multi-Platform Implementation Strategy + +The package uses **conditional imports** to select the appropriate implementation at compile time: + +```dart +// lib/src/convex_client.dart +import 'impl/convex_client_native.dart' // FFI + Rust + if (dart.library.js_interop) 'impl/convex_client_web.dart'; // Pure Dart +``` + +### Architecture Comparison + +| Platform | Implementation | Rust Required | WebSocket Source | +|----------|---------------|---------------|------------------| +| **Web** | Pure Dart | ❌ No | Browser WebSocket API | +| **Android** | FFI + Rust | ✅ Yes (build-time) | Convex Rust SDK | +| **iOS** | FFI + Rust | ✅ Yes (build-time) | Convex Rust SDK | +| **macOS** | FFI + Rust | ✅ Yes (build-time) | Convex Rust SDK | +| **Windows** | FFI + Rust | ✅ Yes (build-time) | Convex Rust SDK | +| **Linux** | FFI + Rust | ✅ Yes (build-time) | Convex Rust SDK | + +--- + +## Web Platform Implementation (NEW in v3.0.0) + +### Why Web Needed Different Approach + +**Problem**: FFI (Foreign Function Interface) doesn't work on web platform +- Web runs in browser JavaScript sandbox +- Cannot execute native compiled code +- `dart:ffi` is not available on web + +**Solution**: Implement Convex WebSocket protocol in pure Dart + +### Web Architecture + +``` +┌─────────────────────────────────────┐ +│ Dart Layer (Flutter Web App) │ ← Your app code +│ - ConvexClient API (same as native)│ +│ - Streams, Futures │ +│ - Flutter-friendly interfaces │ +└──────────────┬──────────────────────┘ + │ Direct Dart calls (no FFI) +┌──────────────▼──────────────────────┐ +│ WebConvexClient (Pure Dart) │ ← Pure Dart implementation +│ - WebSocket management │ +│ - Convex wire protocol │ +│ - State management │ +│ - Subscription handling │ +└──────────────┬──────────────────────┘ + │ package:web WebSocket API +┌──────────────▼──────────────────────┐ +│ Browser WebSocket │ ← Browser native API +│ - Real-time WebSocket client │ +│ - Managed by browser │ +│ - No compilation required │ +└─────────────────────────────────────┘ +``` + +### Web Implementation Details + +**File**: `lib/src/impl/convex_client_web.dart` (~800 lines of pure Dart) + +**Key Features Implemented**: +- ✅ RFC 4122 compliant UUID v4 generation for session IDs +- ✅ Convex WebSocket wire protocol implementation: + - Connect message with session management + - ModifyQuerySet with version tracking + - Mutation/Action/Query execution + - Transition messages for real-time updates + - Ping/Pong heartbeat +- ✅ Real-time subscriptions with automatic cleanup +- ✅ Connection state monitoring +- ✅ Automatic reconnection with exponential backoff +- ✅ Authentication token management +- ✅ Error handling and timeout management + +**Dependencies (Web Only)**: +```yaml +dependencies: + web: ^1.0.0 # Browser WebSocket API access + http: ^1.2.0 # HTTP client for REST fallback (future) +``` + +**Protocol Messages**: +```dart +// Connect +{ + "type": "Connect", + "sessionId": "550e8400-e29b-41d4-a716-446655440000", // RFC 4122 UUID + "maxObservedTimestamp": null, + "connectionCount": 1, + "clientTs": 1704931200000, + "lastCloseReason": null +} + +// ModifyQuerySet (subscribe) +{ + "type": "ModifyQuerySet", + "baseVersion": 0, + "newVersion": 1, + "modifications": [{ + "type": "Add", + "queryId": 1, + "udfPath": "messages:list", + "args": [{}] + }] +} + +// Mutation +{ + "type": "Mutation", + "requestId": 1, // u32 integer + "udfPath": "messages:send", + "args": [{"body": "Hello"}] +} +``` + +### Web vs Native API Parity + +**100% API Compatibility**: Same public API works on both platforms + +```dart +// This exact code works identically on web AND native! +final client = ConvexClient.instance; + +// Queries +final result = await client.query('users:list', {}); + +// Mutations +await client.mutation(name: 'messages:send', args: {'body': 'Hi'}); + +// Subscriptions +final sub = await client.subscribe( + name: 'messages:list', + args: {}, + onUpdate: (data) => print(data), + onError: (msg, data) => print(msg), +); + +// Connection state +client.connectionState.listen((state) => print(state)); + +// Authentication +await client.setAuth(token: 'jwt-token'); +``` + +--- + +## Native Platform Implementation + +### TL;DR +**Native platforms use FFI (Foreign Function Interface) to wrap the official Convex Rust SDK.** This means the core Convex client logic is written in Rust and compiled to native code, with Dart code calling into it via FFI. + +### Technical Explanation + +The package architecture involves three layers: + +``` +┌─────────────────────────────────────┐ +│ Dart Layer (Flutter App) │ ← Your app code +│ - ConvexClient API │ +│ - Streams, Futures │ +│ - Flutter-friendly interfaces │ +└──────────────┬──────────────────────┘ + │ FFI Bridge (flutter_rust_bridge) +┌──────────────▼──────────────────────┐ +│ Rust Layer (Native Code) │ ← Compiled Rust +│ - MobileConvexClient │ +│ - WebSocket management │ +│ - State management │ +└──────────────┬──────────────────────┘ + │ Rust library dependency +┌──────────────▼──────────────────────┐ +│ Convex Rust SDK (convex crate) │ ← Official Convex SDK +│ - Real-time WebSocket client │ +│ - Query/Mutation/Action execution │ +│ - Connection management │ +└─────────────────────────────────────┘ +``` + +### Key Dependencies + +**In `pubspec.yaml`:** +```yaml +dependencies: + flutter_rust_bridge: ^2.11.1 # Dart ↔ Rust FFI bridge + ffi: ^2.1.3 # Dart FFI support +``` + +**In `rust/Cargo.toml`:** +```toml +[dependencies] +convex = { version = "0.9" } # Official Convex Rust SDK +flutter_rust_bridge = "=2.11.1" # Bridge codegen +tokio = { version = "1", features = ["full"] } # Async runtime +``` + +**Plugin Configuration in `pubspec.yaml`:** +```yaml +flutter: + plugin: + platforms: + android: + ffiPlugin: true # ← This marks it as FFI plugin + ios: + ffiPlugin: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + windows: + ffiPlugin: true +``` + +### Why Use Rust Instead of Pure Dart? + +1. **Official SDK**: Convex provides an official Rust SDK with full WebSocket support +2. **Performance**: Native code (compiled Rust) is faster than interpreted Dart for intensive operations +3. **Code Reuse**: Leverage the battle-tested Convex Rust client instead of reimplementing from scratch +4. **Real-time Features**: WebSocket management, connection pooling, and async I/O are built-in +5. **Type Safety**: Rust's strong type system catches errors at compile time +6. **Cross-platform**: Rust compiles to all Flutter platforms (Android, iOS, Windows, macOS, Linux) + +--- + +## Who Needs Rust Installed + +### 1. Package Developers (Maintainers) ✅ NEED RUST + +**Who**: Anyone modifying the convex_flutter package itself + +**Why**: To build and test Rust code changes + +**Requirements**: +```bash +# Install Rust toolchain +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + +# Verify installation +rustc --version +cargo --version + +# Platform-specific tools +# Android: NDK (via Android SDK Manager) +# iOS/macOS: Xcode Command Line Tools +# Windows: Visual Studio Build Tools (C++) +# Linux: build-essential, clang, pkg-config +``` + +### 2. App Developers (Package Users) ⚠️ CURRENTLY NEED RUST + +**Who**: Anyone building a Flutter app that depends on `convex_flutter` + +**Why**: Flutter's build system compiles the Rust code when building your app + +**The Problem**: This is a significant barrier to adoption. Most Flutter developers don't have Rust installed and shouldn't need to. + +**What Happens When Building**: +```bash +# When you run: +flutter build apk + +# Flutter build system: +1. Detects FFI plugin (ffiPlugin: true) +2. Looks for Rust source in rust/ directory +3. Invokes `cargo build --release` for target platform +4. Compiles Rust code to native library (.so, .dylib, .dll) +5. Bundles native library into app package +6. ❌ FAILS if Rust toolchain not installed +``` + +**Error Without Rust**: +``` +Error: Unable to find cargo in PATH. Rust toolchain is required. +Please install Rust from https://rustup.rs +``` + +### 3. End Users (App Users) ✅ DON'T NEED RUST + +**Who**: People downloading your app from App Store/Play Store + +**Why**: The compiled native libraries are bundled in the app package + +**What They Get**: Pre-compiled native code (no Rust needed) + +--- + +## How the Package Works + +### Build Process + +```mermaid +graph TD + A[flutter build] --> B{Detect FFI Plugin} + B --> C[Invoke cargo build] + C --> D[Compile Rust → Native Library] + D --> E[Bundle .so/.dylib/.dll into app] + E --> F[App can call Rust via FFI] +``` + +### Runtime Flow + +**Example: Executing a Query** + +```dart +// 1. Dart: User calls query +final result = await ConvexClient.instance.query('users:list', {}); +``` + +↓ + +```dart +// 2. Dart: ConvexClient calls Rust via FFI bridge +final rustResult = await _mobileClient.query( + name: 'users:list', + args: '{}', + timeout: Duration(seconds: 30), +); +``` + +↓ + +```rust +// 3. Rust: MobileConvexClient receives call +pub async fn query(&self, name: String, args: String, timeout: Duration) -> Result { + let client = self.connected_client().await?; // Get Convex client + + // Parse args, execute query via Convex SDK + let result = client.query(name, args).await?; + + // Return JSON string to Dart + Ok(serde_json::to_string(&result)?) +} +``` + +↓ + +```rust +// 4. Convex Rust SDK: Execute query +// - Establish WebSocket connection +// - Send query request +// - Receive response +// - Return result +``` + +↓ + +```dart +// 5. Dart: Return result to app +return jsonDecode(rustResult); +``` + +### File Structure + +``` +convex_flutter/ +├── lib/ # Dart code (Flutter layer) +│ ├── convex_flutter.dart # Public API +│ └── src/ +│ ├── convex_client.dart # Main client (Dart) +│ ├── rust/ # Generated FFI bindings +│ │ └── lib.dart # Auto-generated by flutter_rust_bridge +│ └── *.dart # Other Dart types/utilities +│ +├── rust/ # Rust code (Native layer) +│ ├── Cargo.toml # Rust dependencies +│ ├── src/ +│ │ ├── lib.rs # Main Rust implementation (wraps Convex SDK) +│ │ └── frb_generated.rs # Auto-generated FFI bindings +│ └── target/ # Compiled Rust artifacts (1.9GB+) +│ +├── pubspec.yaml # Flutter package config (ffiPlugin: true) +└── README.md # Package documentation +``` + +**Code Statistics**: +- **Dart files**: 33 (UI, API, types) +- **Rust files**: 2 (core client logic) +- **Lines of Rust**: ~800 lines wrapping Convex SDK + +--- + +## Can Rust Dependency Be Removed? + +### Short Answer: **Technically YES, but at SIGNIFICANT cost** + +### Long Answer: Multiple Approaches, Each with Major Tradeoffs + +--- + +## Alternatives & Tradeoffs + +### Option 1: Pure Dart Implementation ❌ NOT RECOMMENDED + +**Approach**: Rewrite entire Convex client in Dart + +**Pros**: +- ✅ No Rust dependency +- ✅ Easier for Flutter developers to contribute +- ✅ No FFI bridge overhead +- ✅ Single language ecosystem + +**Cons**: +- ❌ **MASSIVE development effort** (thousands of lines of code) +- ❌ Reimplementing WebSocket protocol, connection management, state handling +- ❌ Maintaining parity with official Convex SDK features +- ❌ Testing and bug fixes (Rust SDK is battle-tested) +- ❌ Ongoing maintenance burden (keeping up with Convex API changes) +- ❌ Potential performance issues (Dart vs native code) + +**Estimated Effort**: 3-6 months of full-time development + ongoing maintenance + +**Verdict**: Only viable if Convex provides an official Dart SDK + +--- + +### Option 2: Pre-compiled Native Binaries ⚠️ POSSIBLE BUT COMPLEX + +**Approach**: Build Rust code in advance for all platforms, distribute binaries with package + +**How It Works**: +``` +1. Package maintainer builds Rust code for all targets: + - Android: arm64-v8a, armeabi-v7a, x86_64, x86 + - iOS: arm64 (device), x86_64 (simulator) + - macOS: arm64 (Apple Silicon), x86_64 (Intel) + - Windows: x86_64 + - Linux: x86_64, arm64 + +2. Include all binaries in package (in android/libs/, ios/, etc.) + +3. Flutter build system uses pre-built binaries instead of compiling +``` + +**Pros**: +- ✅ App developers don't need Rust installed +- ✅ Faster builds (no Rust compilation) +- ✅ Same functionality as current implementation + +**Cons**: +- ❌ **Large package size** (~50-100MB for all platforms/architectures) +- ❌ **CI/CD complexity** (must build for 10+ target platforms) +- ❌ **Security concerns** (distributing pre-built binaries) +- ❌ Package maintainer needs all platform build environments +- ❌ pub.dev size limits (10MB for packages, need special approval for larger) +- ❌ Still need Rust for package development + +**Package Size Impact**: +``` +Current package size: 328 KB (source only) +With pre-compiled binaries: ~80-120 MB (all platforms/architectures) + +Breakdown: +- Android (4 architectures): ~20-30 MB +- iOS (2 architectures): ~15-20 MB +- macOS (2 architectures): ~15-20 MB +- Windows: ~10-15 MB +- Linux: ~10-15 MB +``` + +**Verdict**: Solves developer experience but creates distribution challenges + +--- + +### Option 3: REST API Only (No WebSockets) ❌ LOSES KEY FEATURES + +**Approach**: Use Convex HTTP API directly (no WebSockets) + +**Pros**: +- ✅ Pure Dart implementation (easy to write) +- ✅ No Rust dependency +- ✅ Simple HTTP client (`package:http`) + +**Cons**: +- ❌ **NO real-time subscriptions** (major feature loss) +- ❌ **NO automatic reconnection** on network changes +- ❌ **NO connection state management** +- ❌ Must poll for updates (inefficient, battery drain) +- ❌ Higher latency for real-time features +- ❌ Not using official Convex SDK + +**What You Lose**: +```dart +// ❌ Real-time subscriptions (lost) +client.subscribe( + name: 'messages:list', + args: {}, + onUpdate: (messages) => print('New messages: $messages'), +); + +// ❌ Connection state monitoring (lost) +client.connectionState.listen((state) { + print('Connection: $state'); +}); + +// ❌ Automatic reconnection (lost) +// ❌ WebSocket efficiency (lost) +``` + +**Verdict**: Only viable for simple apps without real-time requirements + +--- + +### Option 4: Hybrid Approach ⚠️ BEST COMPROMISE + +**Approach**: Offer TWO packages + +1. **`convex_flutter`** (current): Full-featured FFI package with Rust +2. **`convex_flutter_lite`** (new): Pure Dart HTTP-only version + +**Pros**: +- ✅ Developers choose based on needs +- ✅ Simple apps can avoid Rust dependency +- ✅ Advanced apps get full features +- ✅ Clear upgrade path (lite → full) + +**Cons**: +- ❌ Maintain two packages +- ❌ Feature parity issues +- ❌ Documentation duplication +- ❌ Potential confusion for users + +**Verdict**: Good middle ground if demand justifies the effort + +--- + +### Option 5: Official Convex Dart SDK 🎯 IDEAL SOLUTION + +**Approach**: Ask Convex to provide an official Dart/Flutter SDK + +**Pros**: +- ✅ No Rust dependency (if written in Dart) +- ✅ Official support from Convex +- ✅ Feature parity guaranteed +- ✅ Professional maintenance + +**Cons**: +- ❌ Outside our control +- ❌ May not happen (Convex prioritizes other platforms) +- ❌ Timeline uncertain + +**Action**: Submit feature request to Convex team + +**Verdict**: Best long-term solution, but not immediately available + +--- + +## Impact on Developers + +### Current Developer Experience (With Rust) + +**First-time setup**: +```bash +# 1. Install Rust (5-10 minutes) +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env + +# 2. Install platform tools +# Android: Install NDK via Android SDK Manager +# iOS/macOS: xcode-select --install +# Windows: Install Visual Studio Build Tools +# Linux: sudo apt-get install build-essential clang pkg-config + +# 3. Add package to Flutter project +flutter pub add convex_flutter + +# 4. First build (slow - compiles Rust) +flutter run # Takes 2-5 minutes on first build + +# 5. Subsequent builds (faster - uses cache) +flutter run # Takes 30-60 seconds +``` + +**Common Issues**: +- ❌ "cargo: command not found" → Rust not installed +- ❌ "NDK not found" → Android NDK missing +- ❌ Long build times (Rust compilation adds 1-3 minutes) +- ❌ Large build artifacts (rust/target/ = 1.9GB) + +**Comparison to Pure Dart Packages**: +```bash +# Pure Dart package (e.g., http, provider, riverpod) +flutter pub add http # Done in 5 seconds +flutter run # Builds in 30 seconds + +# convex_flutter (FFI plugin) +flutter pub add convex_flutter # Requires Rust setup (10 minutes) +flutter run # Builds in 3-5 minutes (first time) +``` + +--- + +## Future Possibilities + +### 1. Streamlined Rust Installation 🔧 + +**Idea**: Provide automated setup script + +```bash +# Example setup script +curl -sSf https://raw.githubusercontent.com/jkuldev/convex_flutter/main/setup.sh | sh + +# Script would: +# 1. Detect OS (macOS, Linux, Windows) +# 2. Install Rust if missing +# 3. Install platform tools (NDK, Xcode, etc.) +# 4. Configure environment +# 5. Run test build +``` + +**Impact**: Reduces setup friction from 30 minutes to 5 minutes + +--- + +### 2. Pre-compiled Binaries via GitHub Releases 📦 + +**Idea**: Host pre-compiled binaries separately, download on demand + +```yaml +# pubspec.yaml +dependencies: + convex_flutter: ^2.2.0 + +# On first build, Flutter plugin downloads pre-built binaries +# from GitHub releases instead of compiling Rust +``` + +**How**: +1. CI/CD builds binaries for all platforms +2. Binaries uploaded to GitHub Releases +3. Flutter plugin downloads correct binary for target platform +4. Falls back to Rust compilation if download fails + +**Impact**: +- ✅ App developers don't need Rust +- ✅ Faster builds +- ❌ Still large downloads (~10-20MB per platform) + +--- + +### 3. Official Convex Dart SDK 🎯 + +**Ideal Long-term Solution**: Convex provides official Dart/Flutter SDK + +**Request to Convex**: +``` +Subject: Feature Request - Official Dart/Flutter SDK + +Dear Convex Team, + +We maintain convex_flutter, a community package wrapping your Rust SDK. +Current architecture requires all Flutter developers to install Rust, +which is a significant adoption barrier. + +Would Convex consider providing an official Dart/Flutter SDK? + +Benefits: +- Wider Flutter ecosystem adoption +- Better developer experience +- Official support and maintenance +- Feature parity with other platforms (JS, Python, Rust) + +Thank you for consideration. +``` + +--- + +### 4. WebAssembly (WASM) Compilation 🌐 + +**Future Tech**: Compile Rust to WASM, run in Dart VM + +**Status**: Experimental (Flutter WASM support is evolving) + +**Potential**: +- ✅ No Rust toolchain needed +- ✅ Smaller package size +- ✅ Same Rust code +- ❌ Performance overhead (WASM vs native) +- ❌ Flutter WASM support still maturing + +--- + +## Summary & Recommendations + +### Current State +- ✅ **Fully functional** with real-time WebSocket support +- ✅ **Production-ready** (v2.2.0) +- ❌ **Requires Rust** for all developers (barrier to adoption) + +### Short-term Recommendations + +**For Package Maintainers**: +1. **Document Rust requirement clearly** in README (add prominent warning) +2. **Provide setup guide** with troubleshooting +3. **Add FAQ section** explaining why Rust is needed +4. **Consider pre-compiled binaries** for popular platforms (Android/iOS first) + +**For App Developers**: +1. **Accept Rust requirement** if you need real-time features +2. **Use alternative packages** if Rust is a dealbreaker (e.g., HTTP-only Convex client) +3. **Submit feedback** about Rust requirement (helps prioritize solutions) + +### Long-term Recommendations + +1. **Investigate pre-compiled binaries** (GitHub Actions CI/CD) +2. **Create `convex_flutter_lite`** (pure Dart HTTP-only version) +3. **Request official Dart SDK** from Convex team +4. **Monitor Flutter WASM** progress + +### Decision Matrix + +| Feature | Current (Rust FFI) | Pre-compiled | Pure Dart | HTTP-only | +|---------|-------------------|--------------|-----------|-----------| +| Real-time subscriptions | ✅ | ✅ | ✅ | ❌ | +| Connection state | ✅ | ✅ | ✅ | ❌ | +| No Rust needed | ❌ | ✅ | ✅ | ✅ | +| Small package size | ✅ | ❌ | ✅ | ✅ | +| Easy maintenance | ✅ | ⚠️ | ❌ | ✅ | +| Performance | ✅ | ✅ | ⚠️ | ⚠️ | +| Official SDK parity | ✅ | ✅ | ❌ | ❌ | + +--- + +## FAQ + +### Q: Why not just use HTTP requests instead of WebSockets? +**A**: WebSockets provide real-time bidirectional communication. With HTTP, you'd need to poll for updates, which is inefficient, drains battery, and has higher latency. Convex's real-time subscriptions require WebSockets. + +### Q: Can I use this package without installing Rust? +**A**: ✅ YES for web platform! No Rust required when building for web. For native platforms (Android, iOS, macOS, Windows, Linux), Rust is still required at build-time. If your app only targets web, you can skip Rust installation entirely. + +### Q: Will this work on web platform? +**A**: ✅ YES! As of v3.0.0, web platform is fully supported with a pure Dart implementation. No Rust required for web builds. See [Web Platform Implementation](#web-platform-implementation-new-in-v300) section above. + +### Q: How much does Rust compilation add to build time? +**A**: First build: 2-5 minutes. Subsequent builds: 30-60 seconds (cached). Release builds take longer (5-10 minutes). + +### Q: Can I distribute my app without users needing Rust? +**A**: Yes! End users don't need Rust. The compiled native libraries are bundled in your app package. Only developers building the app need Rust. + +### Q: Is there a roadmap for removing Rust dependency? +**A**: We're investigating pre-compiled binaries for v3.0. Long-term, we hope Convex provides an official Dart SDK. See [Future Possibilities](#future-possibilities) section. + +--- + +## Contributing + +If you have ideas for reducing Rust dependency burden: +1. Open an issue: https://github.com/jkuldev/convex_flutter/issues +2. Discuss in PR: https://github.com/jkuldev/convex_flutter/pulls +3. Contact maintainers: https://jkuldev.com + +--- + +**Document Version**: 2.0 +**Last Updated**: 2026-01-10 +**Package Version**: 3.0.0 diff --git a/third_party/convex_flutter/CHANGELOG.md b/third_party/convex_flutter/CHANGELOG.md new file mode 100644 index 00000000..75dc3b2b --- /dev/null +++ b/third_party/convex_flutter/CHANGELOG.md @@ -0,0 +1,234 @@ +## 3.0.1 + +### Bug Fixes + +- **Fixed argument types from `Map` to `Map`** across all operations (query, mutation, action, subscribe) + - Nested objects (e.g., `paginationOpts`), arrays, numbers, and booleans are now properly supported as argument values + - Fix applied consistently across public API, interface, native, and web implementations + - Removed `toString()` conversion in mutation and action that silently destroyed nested argument structures + - Closes #15 + +## 3.0.0 + +### Major New Features + +- **🌐 Web Platform Support**: Full web platform support with pure Dart implementation + - Uses native browser WebSocket API (no FFI required) + - 100% API compatibility with native platforms + - Automatic platform selection via conditional imports + - No Rust toolchain required for web builds + - All features work identically on web: queries, mutations, actions, subscriptions, auth + +### Web Implementation Details + +- Implemented Convex WebSocket wire protocol in pure Dart: + - RFC 4122 compliant UUID v4 generation for session IDs + - Proper protocol message formatting (Connect, ModifyQuerySet, Mutation, Action, Transition, Ping/Pong) + - Query set version tracking with baseVersion/newVersion + - Integer requestId (u32) for protocol compliance + - Real-time subscription management with automatic cleanup + - Connection state monitoring and automatic reconnection + - Ping/Pong heartbeat for connection keepalive + +### Critical Bug Fixes + +- **Fixed macOS native platform connection issues**: + - Root cause: Missing network entitlements in App Sandbox configuration + - Added `com.apple.security.network.client` to both DebugProfile.entitlements and Release.entitlements + - macOS apps can now establish WebSocket connections to Convex backend + +- **Fixed Android missing INTERNET permission**: + - Added `` to AndroidManifest.xml + - Android apps now have proper network access + +- **Fixed Rust rustls CryptoProvider error**: + - Removed `default-features = false` from convex dependency in Cargo.toml + - rustls 0.23+ now has proper CryptoProvider configuration + +### Improvements + +- **Platform Configuration Documentation**: + - New PLATFORM_CONFIGURATION.md guide with setup instructions for all platforms + - Updated README.md with platform-specific requirements + - Clear troubleshooting guides for common connection issues + +- **Example App**: + - All platforms (web, iOS, Android, macOS) now properly configured + - Works on web without Rust toolchain + - Demonstrates cross-platform compatibility + +- **Rust SDK Update**: + - Upgraded convex SDK from 0.9.0 to 0.10.2 + - Better protocol compatibility with Convex backend + +### Platform Support Matrix + +| Platform | Status | Implementation | Network Config Required | +|----------|--------|----------------|-------------------------| +| Web | ✅ New | Pure Dart | None | +| iOS | ✅ Working | FFI + Rust | None | +| macOS | ✅ Fixed | FFI + Rust | Network entitlements | +| Android | ✅ Fixed | FFI + Rust | INTERNET permission | +| Windows | ✅ Working | FFI + Rust | None | +| Linux | ✅ Working | FFI + Rust | None | + +### API Changes + +None - 100% backward compatible. The same API works across all platforms. + +### Breaking Changes + +None - this is a feature release with bug fixes, no breaking changes to existing API. + +### New Files + +- `lib/src/impl/convex_client_web.dart` - Pure Dart WebSocket implementation for web +- `lib/src/impl/convex_client_native.dart` - FFI implementation for native platforms (refactored) +- `PLATFORM_CONFIGURATION.md` - Comprehensive platform setup guide +- `WEB_SUCCESS.md` - Web implementation verification documentation +- `NATIVE_PLATFORM_FIX.md` - Native platform fixes documentation + +### Modified Files + +- `example/macos/Runner/DebugProfile.entitlements` - Added network permissions +- `example/macos/Runner/Release.entitlements` - Added network permissions +- `example/android/app/src/main/AndroidManifest.xml` - Added INTERNET permission +- `rust/Cargo.toml` - Updated convex SDK and removed default-features = false +- `lib/src/convex_client.dart` - Refactored to use platform-specific implementations +- `README.md` - Added web platform documentation and platform configuration guide + +### Migration Guide + +No migration needed - existing code works without changes on all platforms including web. + +To build for web: +```bash +flutter build web +``` + +No Rust toolchain required for web builds! + +### Known Issues + +None - all platforms tested and working. + +--- + +## 1.0.2 + +- Added support for Dart 3.7.0 +- Added support for Flutter 3.3.0 +- Added support for Flutter 3.10.0 +- Added support for Flutter 3.11.0 +- Added support for Flutter 3.12.0 +- Added support for Flutter 3.13.0 +- Added support for Flutter 3.14.0 + +## 1.0.3 + +- Updated flutter_rust_bridge package to 2.9.0 + +## 1.0.4 + +- Updated flutter_rust_bridge package to 2.10.0 + +## 1.2.0 + + - Package version updated + +## 2.0.0 + +- Replaced ArcSubscriptionHandle with SubscriptionHandle + +## 2.1.0 + +### New Features + +- **Singleton Pattern**: New `ConvexClient.initialize(ConvexConfig)` method with `ConvexClient.instance` access +- **Operation Timeouts**: Configurable timeout for all queries, mutations, and actions (default: 30 seconds) +- **Connection Management**: Manual connection checking with `checkConnection()` and `reconnect()` methods +- **Lifecycle Monitoring**: Stream of app lifecycle events (resumed, paused, inactive, detached) +- **Configuration Class**: New `ConvexConfig` class for cleaner initialization + +### Bug Fixes + +- Fixed critical Rust subscription panic when WebSocket connection closes unexpectedly +- Subscription streams now exit gracefully instead of crashing the app + +### Improvements + +- Better error handling for connection issues with `ConnectionStatus` enum +- App lifecycle integration with `AppLifecycleObserver` +- Comprehensive documentation updates with new usage examples +- Example app updated to demonstrate new features + +### API Changes + +- **Deprecated**: `ConvexClient.init()` is now deprecated, use `ConvexClient.initialize(ConvexConfig)` instead +- **New**: `ConvexClient.instance` - Access singleton anywhere +- **New**: `ConvexClient.initialize(ConvexConfig)` - Initialize with configuration +- **New**: `checkConnection()` - Manual connection status check +- **New**: `reconnect()` - Manual reconnection attempt +- **New**: `lifecycleEvents` stream - Monitor app lifecycle +- **Enhanced**: All queries, mutations, and actions now respect `operationTimeout` + +### Breaking Changes + +None - backward compatibility maintained through deprecated methods + +## 2.2.0 + +### New Features + +- **Real-Time WebSocket Connection State**: Monitor WebSocket connection status via reactive streams + - `connectionState` stream - Real-time connection state updates (Connected/Connecting) + - `currentConnectionState` getter - Synchronous access to current state + - `isConnected` getter - Quick boolean check for connection status + - Automatic state transitions when WebSocket connects/disconnects + - No polling required - pure event-driven updates + +### Bug Fixes + +- **Fixed critical race condition in WebSocket connection initialization** + - Issue: State change callback was registered after WebSocket connection began, causing state transitions to be lost + - Root cause: Async task spawning in `connected_client()` created unpredictable timing delays + - Solution: Removed task spawning and build ConvexClient directly in async context + - Result: Callback is now guaranteed to be registered before `builder.build()` is called + +- **Fixed WebSocket connection state stuck on "connecting"** + - Issue: Example app showed "connecting" forever without transitioning to "connected" + - Root cause: No operations were triggered on app startup, so `connected_client()` was never called + - Solution: Added auto-connection trigger in example app's HomeScreen initialization + - Result: Connection establishes automatically on startup with proper state transitions + +### Improvements + +- Enhanced example app with comprehensive WebSocket connection state demonstrations: + - Connection status indicator in app bar with real-time visual feedback + - Dedicated Connection screen showing current state and history + - Automatic connection on app startup + - All 5 screens demonstrating different SDK capabilities + - Added HEALTH_CHECK.md guide for setting up health check queries + +- Documentation improvements: + - Comprehensive WebSocket connection state usage examples + - Recommended health check query pattern using `health:ping` + - TypeScript example for creating health check query in Convex backend + - Updated all examples to use dedicated health check instead of `messages:list` + - Deprecated `checkConnection()` in favor of real-time `connectionState` stream + +- Code quality: + - Comprehensive debug logging for troubleshooting connection issues + - Better error handling in auto-connection flow + - Clearer comments explaining lazy initialization + +### API Changes + +- **New**: `connectionState` stream - Real-time WebSocket connection state updates (`Stream`) +- **New**: `currentConnectionState` getter - Synchronous access to current connection state +- **New**: `isConnected` getter - Boolean check for WebSocket connection status +- **Deprecated**: `checkConnection()` - Use `connectionState` stream for real-time monitoring instead + +### Breaking Changes + +None - all changes are additive and maintain backward compatibility \ No newline at end of file diff --git a/third_party/convex_flutter/CONTRIBUTING.md b/third_party/convex_flutter/CONTRIBUTING.md new file mode 100644 index 00000000..c1d9e4fd --- /dev/null +++ b/third_party/convex_flutter/CONTRIBUTING.md @@ -0,0 +1,504 @@ +# Contributing to convex_flutter + +Thank you for your interest in contributing to `convex_flutter`! This document provides guidelines and instructions for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [How Can I Contribute?](#how-can-i-contribute) +- [Development Setup](#development-setup) +- [Project Structure](#project-structure) +- [Making Changes](#making-changes) +- [Testing](#testing) +- [Submitting Changes](#submitting-changes) +- [Style Guidelines](#style-guidelines) +- [Platform-Specific Contributions](#platform-specific-contributions) + +--- + +## Code of Conduct + +This project adheres to a code of conduct that all contributors are expected to follow: + +- Be respectful and inclusive +- Welcome newcomers and help them get started +- Focus on constructive criticism +- Respect differing viewpoints and experiences +- Accept responsibility and apologize for mistakes + +## How Can I Contribute? + +### Reporting Bugs + +Before creating bug reports, please check existing issues to avoid duplicates. When creating a bug report, include: + +- **Clear title** describing the issue +- **Detailed description** of the problem +- **Steps to reproduce** the behavior +- **Expected vs actual behavior** +- **Environment details**: + - Flutter version (`flutter --version`) + - Dart version + - Platform (Web, Android, iOS, macOS, Windows, Linux) + - Package version + - Rust version (for native platforms) +- **Stack traces or error messages** +- **Minimal reproducible example** if possible + +**Template**: +```markdown +**Description**: Brief description of the issue + +**Steps to Reproduce**: +1. Initialize ConvexClient with... +2. Call query/mutation/subscribe... +3. Observe error... + +**Expected**: What should happen +**Actual**: What actually happens + +**Environment**: +- Flutter: 3.19.0 +- Dart: 3.3.0 +- Platform: Web / Android / iOS / etc. +- convex_flutter: 3.0.0 +- Rust: 1.75.0 (if applicable) + +**Error Output**: +``` +[Paste error here] +``` + +**Additional Context**: Any other relevant information +``` + +### Suggesting Enhancements + +Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, include: + +- **Clear title** describing the enhancement +- **Detailed description** of the proposed feature +- **Use case** explaining why this would be useful +- **Proposed implementation** (if you have ideas) +- **Alternatives considered** + +### Pull Requests + +We actively welcome pull requests! To contribute code: + +1. **Fork** the repository +2. **Create a branch** from `main` (`git checkout -b feature/my-feature`) +3. **Make your changes** following our style guidelines +4. **Test your changes** on relevant platforms +5. **Commit your changes** with clear commit messages +6. **Push to your fork** (`git push origin feature/my-feature`) +7. **Open a Pull Request** with a clear description + +--- + +## Development Setup + +### Prerequisites + +**For All Contributors**: +- Flutter SDK (>= 3.3.0) +- Dart SDK (>= 3.8.1) +- Git +- A code editor (VS Code, Android Studio, etc.) + +**For Native Platform Development**: +- Rust toolchain (`rustup` + `cargo`) +- Platform-specific tools: + - **Android**: JDK 11, Android SDK, NDK + - **iOS/macOS**: Xcode, CocoaPods + - **Windows**: Visual Studio Build Tools (C++) + - **Linux**: build-essential, clang, pkg-config + +**For Web Platform Development**: +- No Rust required! +- Just Flutter and Dart + +### Initial Setup + +```bash +# 1. Clone your fork +git clone https://github.com/YOUR_USERNAME/convex_flutter.git +cd convex_flutter + +# 2. Install Rust (skip if only working on web) +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env + +# 3. Install dependencies +flutter pub get + +# 4. Run example app +cd example +flutter pub get +flutter run -d chrome # For web +# OR +flutter run -d macos # For native +``` + +### Setting Up for Development + +```bash +# Run flutter_rust_bridge code generation (if modifying Rust code) +cd rust +flutter_rust_bridge_codegen \ + --rust-input src/lib.rs \ + --dart-output ../lib/src/rust/lib.dart + +# Format Dart code +dart format . + +# Format Rust code +cd rust +cargo fmt + +# Analyze Dart code +flutter analyze + +# Run tests +flutter test +``` + +--- + +## Project Structure + +``` +convex_flutter/ +├── lib/ # Dart source code +│ ├── convex_flutter.dart # Public API exports +│ ├── src/ +│ │ ├── convex_client.dart # Main client (platform-agnostic) +│ │ ├── impl/ +│ │ │ ├── convex_client_web.dart # Web implementation (pure Dart) +│ │ │ └── convex_client_native.dart # Native implementation (FFI) +│ │ ├── rust/ # Generated FFI bindings +│ │ ├── convex_config.dart # Configuration class +│ │ ├── connection_status.dart +│ │ ├── app_lifecycle_*.dart +│ │ └── ... # Other Dart utilities +│ +├── rust/ # Rust source code (native platforms) +│ ├── Cargo.toml # Rust dependencies +│ ├── src/ +│ │ ├── lib.rs # Main Rust implementation +│ │ └── frb_generated.rs # Generated FFI code +│ └── target/ # Build artifacts +│ +├── example/ # Example Flutter app +│ ├── lib/main.dart # Example app code +│ ├── android/ # Android configuration +│ ├── ios/ # iOS configuration +│ ├── macos/ # macOS configuration +│ ├── web/ # Web configuration +│ └── ... +│ +├── test/ # Unit tests +├── ARCHITECTURE.md # Architecture documentation +├── PLATFORM_CONFIGURATION.md # Platform setup guide +├── CHANGELOG.md # Version history +└── README.md # Main documentation +``` + +--- + +## Making Changes + +### Branching Strategy + +- `main` - Stable release branch +- `develop` - Development branch (if used) +- `feature/*` - New features +- `fix/*` - Bug fixes +- `docs/*` - Documentation improvements +- `refactor/*` - Code refactoring + +### Commit Messages + +Write clear, descriptive commit messages following this format: + +``` +type(scope): Brief description + +Detailed explanation of changes (optional) + +Fixes #issue_number (if applicable) +``` + +**Types**: +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `style`: Code style changes (formatting, etc.) +- `refactor`: Code refactoring +- `test`: Adding or updating tests +- `chore`: Maintenance tasks + +**Examples**: +``` +feat(web): Add web platform support with pure Dart implementation + +Implemented Convex WebSocket protocol in pure Dart for web platform. +Includes UUID generation, protocol messages, and subscription handling. + +Fixes #123 +``` + +``` +fix(macos): Add missing network entitlements + +Added com.apple.security.network.client entitlement to fix +WebSocket connection issues on macOS. + +Fixes #456 +``` + +--- + +## Testing + +### Running Tests + +```bash +# Run all tests +flutter test + +# Run specific test file +flutter test test/convex_client_test.dart + +# Run tests with coverage +flutter test --coverage +``` + +### Manual Testing + +**Web Platform**: +```bash +cd example +flutter run -d chrome +# Test all features in the browser +``` + +**Native Platforms**: +```bash +cd example + +# macOS +flutter run -d macos + +# iOS (requires macOS + Xcode) +flutter run -d ios + +# Android (requires Android device/emulator) +flutter run -d android +``` + +### Test Checklist for Pull Requests + +Before submitting a PR, verify: + +- [ ] All existing tests pass +- [ ] New features have tests +- [ ] Manual testing completed on relevant platforms: + - [ ] Web (if web-related changes) + - [ ] At least one native platform (if native changes) +- [ ] No breaking changes (or clearly documented) +- [ ] Documentation updated (if API changes) +- [ ] CHANGELOG.md updated (for notable changes) + +--- + +## Submitting Changes + +### Pull Request Process + +1. **Update Documentation**: If you changed APIs, update: + - README.md + - Inline code documentation + - ARCHITECTURE.md (if architectural changes) + - PLATFORM_CONFIGURATION.md (if platform-specific changes) + +2. **Update CHANGELOG.md**: Add entry under "Unreleased" section: + ```markdown + ## Unreleased + + ### New Features + - Your feature description + + ### Bug Fixes + - Your fix description + ``` + +3. **Create Pull Request** with: + - **Clear title**: `feat: Add web platform support` + - **Description**: Explain what, why, and how + - **Issue reference**: `Fixes #123` or `Closes #456` + - **Screenshots/GIFs**: For UI changes + - **Testing notes**: How you tested the changes + - **Breaking changes**: Clearly marked if any + +4. **Respond to Reviews**: Address feedback promptly and respectfully + +5. **CI/CD Checks**: Ensure all automated checks pass + +### PR Template + +```markdown +## Description +Brief description of changes + +## Type of Change +- [ ] Bug fix (non-breaking change fixing an issue) +- [ ] New feature (non-breaking change adding functionality) +- [ ] Breaking change (fix or feature that breaks existing functionality) +- [ ] Documentation update + +## Related Issue +Fixes #(issue number) + +## How Has This Been Tested? +Describe testing process + +## Platforms Tested +- [ ] Web +- [ ] Android +- [ ] iOS +- [ ] macOS +- [ ] Windows +- [ ] Linux + +## Checklist +- [ ] My code follows the project's style guidelines +- [ ] I have performed a self-review +- [ ] I have commented my code where needed +- [ ] I have updated documentation +- [ ] I have added tests +- [ ] All tests pass locally +- [ ] I have updated CHANGELOG.md +``` + +--- + +## Style Guidelines + +### Dart Code Style + +Follow the [Dart Style Guide](https://dart.dev/guides/language/effective-dart/style): + +```bash +# Format code +dart format . + +# Analyze code +flutter analyze +``` + +**Key conventions**: +- Use `lowerCamelCase` for variables, methods, parameters +- Use `UpperCamelCase` for classes, enums, typedefs +- Prefer `final` over `var` +- Use trailing commas for better formatting +- Document public APIs with `///` doc comments + +**Example**: +```dart +/// Executes a Convex query with the given [name] and [args]. +/// +/// Returns a JSON string containing the query result. +/// Throws [TimeoutException] if the operation exceeds [operationTimeout]. +/// +/// Example: +/// ```dart +/// final result = await client.query('users:list', {'limit': '10'}); +/// final users = jsonDecode(result); +/// ``` +Future query(String name, Map args) async { + // Implementation +} +``` + +### Rust Code Style + +Follow the [Rust Style Guide](https://doc.rust-lang.org/beta/style-guide/): + +```bash +cd rust +cargo fmt # Format +cargo clippy # Lint +``` + +**Key conventions**: +- Use `snake_case` for functions, variables +- Use `UpperCamelCase` for types, traits +- Document public items with `///` comments +- Use `Result` for error handling +- Prefer pattern matching over if/else + +--- + +## Platform-Specific Contributions + +### Working on Web Platform + +**File**: `lib/src/impl/convex_client_web.dart` + +**Dependencies**: `package:web`, `package:http` + +**No Rust required!** + +**Testing**: +```bash +flutter run -d chrome +flutter test # Tests run on VM, but web code path is used +``` + +**Key areas**: +- WebSocket protocol implementation +- UUID generation +- Connection state management +- Subscription handling + +### Working on Native Platforms + +**File**: `rust/src/lib.rs`, `lib/src/impl/convex_client_native.dart` + +**Dependencies**: Rust toolchain, `flutter_rust_bridge` + +**Testing**: Requires platform-specific setup (Xcode for iOS/macOS, Android SDK for Android, etc.) + +**Key areas**: +- FFI bridge between Dart and Rust +- Rust wrapper around Convex SDK +- Native platform configurations (entitlements, permissions) + +### Adding New Features + +When adding features: + +1. **Implement for both platforms** (web + native) if applicable +2. **Maintain API parity** between platforms +3. **Add tests** for both implementations +4. **Update documentation** in README.md +5. **Add platform-specific notes** in PLATFORM_CONFIGURATION.md if needed + +--- + +## Questions? + +- **Issues**: https://github.com/jkuldev/convex_flutter/issues +- **Discussions**: https://github.com/jkuldev/convex_flutter/discussions +- **Email**: Contact maintainers at jkuldev.com + +--- + +## License + +By contributing to `convex_flutter`, you agree that your contributions will be licensed under the MIT License. + +--- + +**Thank you for contributing to convex_flutter! 🎉** diff --git a/third_party/convex_flutter/ICARUS_PATCH.md b/third_party/convex_flutter/ICARUS_PATCH.md new file mode 100644 index 00000000..7ac771ca --- /dev/null +++ b/third_party/convex_flutter/ICARUS_PATCH.md @@ -0,0 +1,28 @@ +# Icarus convex_flutter patch + +Source: the published `convex_flutter` 3.0.1 package. + +Icarus pins the package's native `convex` Rust client to 0.10.4. The published +package lock selected 0.10.2. Convex Rust 0.10.3 introduced the reconnect-state +repair and auth-token callback used to restore authenticated state after a +WebSocket reconnect; 0.10.4 includes that repair plus a subscription leak fix. + +The package's hand-written Rust auth adapter now gives the Dart token callback +to `ConvexClient.set_auth_callback`. That keeps token refresh in the same +upstream state machine that replays subscriptions and mutations after a socket +reconnect. The old adapter owned a separate expiry timer and called static +`set_auth`, which could leave the client disconnected after the server rejected +an expired token. + +No generated Dart or Rust bridge file is edited: the public bridge signature is +unchanged. The hand-written changes are limited to `rust/src/lib.rs` and the +minimum `convex` crate version in `rust/Cargo.toml`; `rust/Cargo.lock` is +regenerated with: + +```sh +cd third_party/convex_flutter/rust +cargo update -p convex --precise 0.10.4 +``` + +This directory can be removed once a published `convex_flutter` release uses a +Convex Rust client with the same fixes and passes the Icarus auth gauntlet. diff --git a/third_party/convex_flutter/LICENSE b/third_party/convex_flutter/LICENSE new file mode 100644 index 00000000..f70f0b3f --- /dev/null +++ b/third_party/convex_flutter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 jkuldev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/convex_flutter/MIGRATION_v3.md b/third_party/convex_flutter/MIGRATION_v3.md new file mode 100644 index 00000000..23a53bba --- /dev/null +++ b/third_party/convex_flutter/MIGRATION_v3.md @@ -0,0 +1,403 @@ +# Migration Guide: v2.x → v3.0.0 + +## Overview + +**Good news: v3.0.0 has ZERO breaking changes!** 🎉 + +This is a feature release that adds web platform support while maintaining 100% backward compatibility with v2.x. Your existing code will continue to work without modifications. + +## What's New in v3.0.0 + +### Major New Features + +1. **Web Platform Support** 🌐 + - Full web platform support with pure Dart implementation + - No Rust required for web builds + - Same API works on web and native platforms + +2. **Platform-Specific Implementations** + - Automatic platform selection via conditional imports + - Web: Pure Dart WebSocket client + - Native: FFI + Rust SDK (unchanged) + +3. **Critical Bug Fixes** + - Fixed macOS connection issues (network entitlements) + - Fixed Android missing INTERNET permission + - Fixed Rust rustls CryptoProvider error + +## Migration Steps + +### Step 1: Update Package Version + +Update your `pubspec.yaml`: + +```yaml +dependencies: + convex_flutter: ^3.0.0 # Update from ^2.2.0 +``` + +Then run: + +```bash +flutter pub upgrade convex_flutter +``` + +### Step 2: Platform Configuration (One-Time Setup) + +#### macOS Apps + +Add network entitlements to **both** files: + +**macos/Runner/DebugProfile.entitlements**: +```xml +com.apple.security.network.client + +com.apple.security.network.server + +``` + +**macos/Runner/Release.entitlements**: +```xml +com.apple.security.network.client + +com.apple.security.network.server + +``` + +#### Android Apps + +Add internet permission to **android/app/src/main/AndroidManifest.xml**: + +```xml + + + + +``` + +#### iOS, Windows, Linux Apps + +No changes required - these platforms work out of the box. + +### Step 3: Test Your App + +```bash +# Test on your target platforms +flutter run -d chrome # Web +flutter run -d macos # macOS +flutter run -d android # Android +flutter run -d ios # iOS +``` + +### Step 4: Build for Web (New!) + +You can now build your app for web: + +```bash +flutter build web +``` + +**No Rust toolchain required for web builds!** + +--- + +## Code Changes Required + +### None! ✅ + +Your existing v2.x code will continue to work without modifications. The API is 100% compatible. + +**Example - This code works identically in v2.x and v3.0.0**: + +```dart +import 'package:convex_flutter/convex_flutter.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialization - no changes + await ConvexClient.initialize( + ConvexConfig( + deploymentUrl: 'https://my-app.convex.cloud', + clientId: 'flutter-app-1.0', + ), + ); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + final client = ConvexClient.instance; + + return MaterialApp( + home: Scaffold( + body: StreamBuilder( + stream: client.connectionState, + builder: (context, snapshot) { + // Works on both web and native! + final isConnected = snapshot.data == WebSocketConnectionState.connected; + return Text(isConnected ? 'Connected' : 'Connecting...'); + }, + ), + ), + ); + } +} +``` + +--- + +## Platform-Specific Differences + +### Web vs Native + +While the API is identical, there are minor implementation differences: + +| Feature | Web | Native | +|---------|-----|--------| +| WebSocket Source | Browser WebSocket API | Convex Rust SDK | +| Implementation | Pure Dart | FFI + Rust | +| Build Requirements | None | Rust toolchain | +| Performance | Excellent | Excellent | +| API | **Identical** | **Identical** | + +**Bottom Line**: Your code doesn't need to know which platform it's running on. The package handles it automatically. + +--- + +## Deprecations + +No APIs were deprecated in v3.0.0. All v2.x methods remain available. + +--- + +## New Capabilities + +### Web Platform Support + +You can now target web alongside mobile and desktop: + +```bash +# Web (new in v3.0.0) +flutter build web + +# Mobile (existing) +flutter build apk +flutter build ios + +# Desktop (existing) +flutter build macos +flutter build windows +flutter build linux +``` + +### Cross-Platform Example App + +The example app now works on **all platforms**: + +```bash +cd example + +# Run on any platform +flutter run -d chrome # Web +flutter run -d macos # macOS +flutter run -d ios # iOS Simulator +flutter run -d android # Android Emulator +flutter run -d windows # Windows +flutter run -d linux # Linux +``` + +--- + +## Troubleshooting + +### macOS: Stuck in "Connecting" + +**Symptom**: App builds successfully but connection state never changes from "connecting" + +**Solution**: Add network entitlements (see Step 2 above) + +**Verify**: +```bash +# Check DebugProfile.entitlements contains: +grep "network.client" macos/Runner/DebugProfile.entitlements +``` + +### Android: Network Security Exception + +**Symptom**: App crashes with `SocketException: Permission denied` + +**Solution**: Add INTERNET permission (see Step 2 above) + +**Verify**: +```bash +# Check AndroidManifest.xml contains: +grep "INTERNET" android/app/src/main/AndroidManifest.xml +``` + +### Web: Build Errors + +**Symptom**: Build fails when targeting web + +**Solution**: Ensure Flutter web support is enabled: + +```bash +flutter config --enable-web +flutter clean +flutter pub get +flutter build web +``` + +### Rust CryptoProvider Error + +**Symptom**: `Could not automatically determine the process-level CryptoProvider` + +**Solution**: This was fixed in the package. Upgrade to v3.0.0: + +```bash +flutter pub upgrade convex_flutter +``` + +--- + +## Performance Considerations + +### Build Times + +**Web**: Faster builds (no Rust compilation) +```bash +# First build +flutter build web # ~1-2 minutes + +# Subsequent builds +flutter build web # ~30-60 seconds +``` + +**Native**: Unchanged from v2.x +```bash +# First build (includes Rust compilation) +flutter build apk # ~3-5 minutes + +# Subsequent builds (Rust cached) +flutter build apk # ~1-2 minutes +``` + +### Runtime Performance + +Both web and native implementations have excellent performance: + +- **Web**: Leverages browser's native WebSocket engine +- **Native**: Uses compiled Rust code + +**No performance degradation** compared to v2.x. + +--- + +## Testing Recommendations + +### Minimum Testing + +Before deploying v3.0.0, test on: + +- [ ] Your primary target platform (web, iOS, Android, etc.) +- [ ] Connection establishment +- [ ] Query execution +- [ ] Mutation execution +- [ ] Subscriptions (if you use them) +- [ ] Authentication (if you use it) + +### Comprehensive Testing + +For production apps, also test: + +- [ ] Connection state monitoring +- [ ] Reconnection after network interruption +- [ ] App backgrounding/foregrounding +- [ ] Hot reload (development) +- [ ] Release builds + +--- + +## Rollback Plan + +If you encounter issues with v3.0.0, you can easily rollback: + +```yaml +# pubspec.yaml +dependencies: + convex_flutter: ^2.2.0 # Rollback to v2.2.0 +``` + +Then run: + +```bash +flutter pub downgrade convex_flutter +flutter clean +flutter pub get +``` + +**Note**: You'll lose web platform support and the bug fixes when rolling back. + +--- + +## Support + +If you encounter migration issues: + +1. **Check Documentation**: + - [PLATFORM_CONFIGURATION.md](PLATFORM_CONFIGURATION.md) - Platform setup guide + - [README.md](README.md) - Updated with v3.0.0 features + - [ARCHITECTURE.md](ARCHITECTURE.md) - Web implementation details + +2. **Search Issues**: https://github.com/jkuldev/convex_flutter/issues + +3. **Create New Issue**: https://github.com/jkuldev/convex_flutter/issues/new + - Include Flutter version, platform, and error details + +--- + +## Changelog + +For complete v3.0.0 changes, see [CHANGELOG.md](CHANGELOG.md#300). + +**Summary**: +- ✅ Web platform support (pure Dart) +- ✅ Fixed macOS network permissions +- ✅ Fixed Android INTERNET permission +- ✅ Fixed Rust rustls CryptoProvider +- ✅ Updated Convex SDK to 0.10.2 +- ✅ Zero breaking changes + +--- + +## Next Steps + +After migrating to v3.0.0: + +1. **Enable Web** (optional): + ```bash + flutter config --enable-web + flutter run -d chrome + ``` + +2. **Review New Documentation**: + - Platform-specific setup in PLATFORM_CONFIGURATION.md + - Web implementation details in ARCHITECTURE.md + +3. **Enjoy Multi-Platform Support**: Build your Convex Flutter app for web, mobile, and desktop! + +--- + +**Migration Difficulty**: ⭐ Very Easy (no code changes required) + +**Time Required**: 5-10 minutes (mostly platform configuration) + +**Risk Level**: 🟢 Low (backward compatible, easy rollback) + +--- + +**Questions?** Open an issue: https://github.com/jkuldev/convex_flutter/issues diff --git a/third_party/convex_flutter/PLATFORM_CONFIGURATION.md b/third_party/convex_flutter/PLATFORM_CONFIGURATION.md new file mode 100644 index 00000000..54f73e7c --- /dev/null +++ b/third_party/convex_flutter/PLATFORM_CONFIGURATION.md @@ -0,0 +1,264 @@ +# Platform Configuration Guide + +This guide explains the platform-specific configuration required for `convex_flutter` to work correctly on all supported platforms. + +## Quick Reference + +| Platform | Configuration Required | Auto-configured? | +|----------|----------------------|------------------| +| **Web** | None | ✅ Yes | +| **iOS** | None | ✅ Yes | +| **macOS** | Network entitlements | ❌ Manual setup required | +| **Android** | INTERNET permission | ❌ Manual setup required | +| **Windows** | None | ✅ Yes | +| **Linux** | None | ✅ Yes | + +--- + +## Platform-Specific Setup + +### macOS + +macOS apps use App Sandbox for security, which requires explicit network permissions. + +#### Required Files + +1. **DebugProfile.entitlements** (for debug builds) +2. **Release.entitlements** (for release builds) + +**Location**: `macos/Runner/` + +#### Configuration + +Add the following entitlements to **both** files: + +```xml + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + +``` + +#### Critical Permissions + +- `com.apple.security.network.client` - **Required** for outgoing WebSocket connections to Convex +- `com.apple.security.network.server` - **Required** for accepting incoming connections (if needed) +- `com.apple.security.app-sandbox` - Enables macOS App Sandbox +- `com.apple.security.cs.allow-jit` - Allows JIT compilation (required for Flutter) + +#### What Happens Without These? + +Without `com.apple.security.network.client`, your app will: +- Build and launch successfully +- Get stuck in "connecting" state forever +- Never establish WebSocket connection to Convex +- Show no error messages (silently blocked by macOS sandbox) + +--- + +### Android + +Android requires explicit permission for internet access. + +#### Required File + +**AndroidManifest.xml** + +**Location**: `android/app/src/main/AndroidManifest.xml` + +#### Configuration + +Add the INTERNET permission **inside the `` tag, before ``**: + +```xml + + + + +``` + +#### Permission Details + +- **Type**: Normal permission (auto-granted at install) +- **User prompt**: No - granted automatically +- **Required for**: All network operations (WebSocket, HTTP, etc.) + +#### What Happens Without This? + +Without `android.permission.INTERNET`, your app will: +- Build successfully +- Crash or fail when attempting network connections +- Show security exceptions in logs + +--- + +### iOS + +**No configuration required** ✅ + +iOS apps have network access by default unless explicitly restricted. `convex_flutter` works out of the box on iOS. + +**Note**: If you're using App Transport Security (ATS) customization, ensure your Convex backend URL is allowed. + +--- + +### Web + +**No configuration required** ✅ + +Web platform uses the browser's native WebSocket API, which inherits the browser's network permissions. Works automatically. + +**Technical Details**: +- Uses pure Dart implementation (no FFI) +- Leverages `package:web` for WebSocket access +- Respects browser CORS and security policies + +--- + +### Windows + +**No configuration required** ✅ + +Windows desktop apps have network access by default. The Windows Firewall may prompt users to allow network access on first run (standard Windows behavior). + +--- + +### Linux + +**No configuration required** ✅ + +Linux desktop apps have network access by default. No special permissions or configuration needed. + +--- + +## Troubleshooting + +### macOS: Stuck in "Connecting" State + +**Symptoms**: +- App builds and launches +- Connection state shows "connecting" forever +- No error messages + +**Solution**: +1. Check `macos/Runner/DebugProfile.entitlements` +2. Ensure `com.apple.security.network.client` is present +3. Check `macos/Runner/Release.entitlements` for release builds +4. Clean build: `flutter clean && flutter run` + +### Android: Network Security Exception + +**Symptoms**: +- App crashes on connection attempt +- Error: `java.net.SocketException: Permission denied` +- Logs show security policy violation + +**Solution**: +1. Check `android/app/src/main/AndroidManifest.xml` +2. Add `` +3. Rebuild: `flutter clean && flutter run` + +### Rust Panic: CryptoProvider Error + +**Symptoms**: +- Error: `Could not automatically determine the process-level CryptoProvider` +- Panic in rustls library + +**Solution**: +This affects the package itself, not user apps. If you encounter this: +1. Check `rust/Cargo.toml` +2. Ensure convex dependency does NOT have `default-features = false` +3. Correct format: `convex = { version = "0.10", features = ["rustls-tls-webpki-roots"] }` + +--- + +## Integration Checklist + +When integrating `convex_flutter` into your Flutter app, verify: + +- [ ] **macOS**: Added network entitlements to both DebugProfile and Release entitlements +- [ ] **Android**: Added INTERNET permission to AndroidManifest.xml +- [ ] **iOS**: No action required (works by default) +- [ ] **Web**: No action required (works by default) +- [ ] **Windows**: No action required (works by default) +- [ ] **Linux**: No action required (works by default) + +--- + +## Why These Permissions Are Needed + +### macOS App Sandbox + +macOS uses a security feature called "App Sandbox" that restricts app capabilities by default. Apps must explicitly declare what they need to access (network, files, camera, etc.). This is a macOS platform requirement, not specific to `convex_flutter`. + +**Learn more**: [Apple: App Sandbox](https://developer.apple.com/documentation/security/app_sandbox) + +### Android Permission System + +Android uses a permission-based security model where apps must declare all permissions they'll use. Network access is considered a "normal" permission (auto-granted) but must still be declared in the manifest. + +**Learn more**: [Android: App Permissions](https://developer.android.com/guide/topics/permissions/overview) + +--- + +## Example Apps + +The `example/` directory in this repository demonstrates proper configuration for all platforms: + +``` +example/ +├── android/app/src/main/AndroidManifest.xml # INTERNET permission +├── ios/ # No config needed +├── macos/Runner/ +│ ├── DebugProfile.entitlements # Network entitlements +│ └── Release.entitlements # Network entitlements +├── web/ # No config needed +├── windows/ # No config needed +└── linux/ # No config needed +``` + +--- + +## Platform Support Matrix + +| Platform | SDK Version | Network Config | Rust Required | +|----------|-------------|----------------|---------------| +| Web | Any | None | No | +| iOS | iOS 12+ | None | Yes (build-time) | +| macOS | macOS 10.14+ | Entitlements | Yes (build-time) | +| Android | API 21+ | Manifest | Yes (build-time) | +| Windows | Windows 7+ | None | Yes (build-time) | +| Linux | Any | None | Yes (build-time) | + +**Note**: Rust is required at **build time** for native platforms (iOS, macOS, Android, Windows, Linux) but **not required** for web platform. + +--- + +## Questions or Issues? + +If you encounter platform-specific issues not covered here: + +1. Check the [example app configuration](example/) +2. Search [GitHub issues](https://github.com/get-convex/convex_flutter/issues) +3. Create a new issue with: + - Platform and version + - Flutter doctor output + - Relevant configuration files + - Error messages or logs + +--- + +**Last Updated**: 2026-01-10 +**Package Version**: 3.0.0 diff --git a/third_party/convex_flutter/PUB_DEPLOY_GUIDE.md b/third_party/convex_flutter/PUB_DEPLOY_GUIDE.md new file mode 100644 index 00000000..e8e9fa2b --- /dev/null +++ b/third_party/convex_flutter/PUB_DEPLOY_GUIDE.md @@ -0,0 +1,332 @@ +# Pub.dev Deployment Guide - convex_flutter v2.2.0 + +## Pre-Deployment Checklist + +### ✅ All Requirements Met + +- [x] Version bumped to 2.2.0 in pubspec.yaml +- [x] CHANGELOG.md updated with detailed v2.2.0 release notes +- [x] README.md updated with all new features documented +- [x] LICENSE file present (MIT License) +- [x] Package validation passed (`flutter pub publish --dry-run`) +- [x] All commits pushed to GitHub +- [x] Example app working and demonstrating all features + +### Package Information + +**Package Name**: `convex_flutter` +**Version**: `2.2.0` +**Repository**: https://github.com/jkuldev/convex_flutter +**Homepage**: https://jkuldev.com +**License**: MIT +**Package Size**: 328 KB (compressed) + +## What's New in v2.2.0 + +### Major Features + +1. **Real-Time WebSocket Connection State Monitoring** + - `connectionState` stream for real-time updates + - `currentConnectionState` getter for sync access + - `isConnected` boolean getter + - Automatic state transitions (Connecting → Connected) + +2. **Critical Bug Fixes** + - Fixed race condition in WebSocket connection initialization + - Fixed connection state stuck on "connecting" + - Improved connection reliability + +3. **Enhanced Documentation** + - Health check query setup guide (TypeScript + Dart) + - Comprehensive usage examples with StreamBuilder + - Clear optional vs required patterns + - Step-by-step tutorials + +### API Additions + +```dart +// New in v2.2.0 +Stream connectionState +WebSocketConnectionState currentConnectionState +bool isConnected +``` + +### Deprecated APIs + +```dart +// Deprecated (still works, but use connectionState instead) +Future checkConnection() +``` + +## Deployment Steps + +### Step 1: Final Validation + +Run the dry-run command to verify everything is ready: + +```bash +flutter pub publish --dry-run +``` + +**Expected Output:** +- Package validation passed +- 0 warnings +- 1 hint about version increment (this is fine) +- Total compressed size: ~328 KB + +### Step 2: Verify Git Status + +Make sure all changes are committed: + +```bash +git status +git log --oneline -5 +``` + +**Expected Commits on Branch:** +``` +5218369 chore: Bump version to 2.2.0 for pub.dev release +ce51ed4 docs: Clarify health check is optional but recommended +0e7484c docs: Recommend dedicated health check query (health:ping) +8aa9d6e example updated +3c34666 feat: Add real-time WebSocket connection state monitoring (v2.2.0) +``` + +### Step 3: Push to GitHub + +Push the branch to GitHub: + +```bash +git push -u origin fix/websocket-connection-state-v2.2.0 +``` + +**Or if using HTTPS:** +```bash +git remote set-url origin https://github.com/jkuldev/convex_flutter.git +git push -u origin fix/websocket-connection-state-v2.2.0 +``` + +### Step 4: Merge to Main + +Option A - Via GitHub Pull Request: +1. Go to https://github.com/jkuldev/convex_flutter/pulls +2. Create Pull Request from `fix/websocket-connection-state-v2.2.0` +3. Review changes +4. Merge to main +5. Pull main locally: `git checkout main && git pull` + +Option B - Local Merge: +```bash +git checkout main +git merge fix/websocket-connection-state-v2.2.0 +git push origin main +``` + +### Step 5: Create Git Tag (Recommended) + +```bash +git tag v2.2.0 +git push origin v2.2.0 +``` + +Or create annotated tag with release notes: +```bash +git tag -a v2.2.0 -m "Release v2.2.0: WebSocket Connection State Monitoring + +- Real-time WebSocket connection state streams +- Fixed critical race condition in connection initialization +- Fixed connection state stuck on 'connecting' +- Enhanced documentation with health check guide +- New connection state APIs +- Comprehensive example app with 5 screens" + +git push origin v2.2.0 +``` + +### Step 6: Publish to pub.dev + +**IMPORTANT**: Make sure you're on the main branch with the latest changes: + +```bash +git checkout main +git pull +``` + +**Publish the package:** + +```bash +flutter pub publish +``` + +**The command will:** +1. Validate the package +2. Show a preview of what will be published +3. Ask for confirmation +4. Upload to pub.dev + +**You'll need:** +- A verified pub.dev account +- Access credentials (you'll be prompted to login) + +**After Publishing:** +- Package will be available at: https://pub.dev/packages/convex_flutter +- Version 2.2.0 will appear within minutes + +### Step 7: Verify Publication + +After publishing, verify on pub.dev: + +1. Visit: https://pub.dev/packages/convex_flutter +2. Check version shows as 2.2.0 +3. Verify README displays correctly +4. Check CHANGELOG is visible +5. Confirm example tab shows code +6. Review package score (should be 130+/140) + +## Post-Deployment + +### Create GitHub Release + +1. Go to: https://github.com/jkuldev/convex_flutter/releases/new +2. Choose tag: `v2.2.0` +3. Release title: `v2.2.0 - WebSocket Connection State Monitoring` +4. Description: Copy from CHANGELOG.md or use: + +```markdown +## 🎉 convex_flutter v2.2.0 + +### New Features +- **Real-Time WebSocket Connection State**: Monitor connection status via reactive streams +- `connectionState` stream for real-time updates +- `currentConnectionState` and `isConnected` getters +- Automatic state transitions + +### Bug Fixes +- Fixed critical race condition in WebSocket connection initialization +- Fixed connection state stuck on "connecting" +- Improved connection reliability + +### Documentation +- Comprehensive health check guide with TypeScript examples +- WebSocket connection state usage examples +- Enhanced example app with 5 demonstration screens + +[View Full Changelog](https://github.com/jkuldev/convex_flutter/blob/main/CHANGELOG.md) + +**Install:** +```yaml +dependencies: + convex_flutter: ^2.2.0 +``` +``` + +5. Publish release + +### Announce (Optional) + +Consider announcing the release: +- Twitter/X +- LinkedIn +- Flutter community Discord/Slack +- Reddit r/FlutterDev +- Dev.to blog post + +## Troubleshooting + +### Issue: "Unauthorized" error when publishing + +**Solution:** +```bash +# Login to pub.dev +dart pub login + +# Then try publishing again +flutter pub publish +``` + +### Issue: "Version already exists" + +**Solution:** +- Version 2.2.0 is already published +- Increment version to 2.2.1 or 2.3.0 +- Update CHANGELOG.md +- Commit and try again + +### Issue: Package validation fails + +**Solution:** +```bash +# Run dry-run to see specific errors +flutter pub publish --dry-run + +# Fix any errors shown +# Common issues: +# - Missing README.md +# - Missing CHANGELOG.md +# - Invalid pubspec.yaml +# - Missing LICENSE +``` + +### Issue: Git push fails (permission denied) + +**Solution:** +```bash +# Use HTTPS instead of SSH +git remote set-url origin https://github.com/jkuldev/convex_flutter.git + +# Or set up SSH keys: +ssh-keygen -t ed25519 -C "your_email@example.com" +# Add to GitHub: https://github.com/settings/keys +``` + +## Rollback Plan + +If issues are discovered after publishing: + +### Option 1: Publish Hotfix (Recommended) + +```bash +# Fix the issue +# Update version to 2.2.1 +# Update CHANGELOG.md +git commit -am "fix: Critical issue in v2.2.0" +flutter pub publish +``` + +### Option 2: Retract Version (Last Resort) + +```bash +# This marks the version as broken +dart pub publisher retract convex_flutter 2.2.0 +``` + +**Note:** Retraction doesn't delete the package, it just warns users. + +## Support After Release + +Monitor for issues: +- GitHub Issues: https://github.com/jkuldev/convex_flutter/issues +- pub.dev comments +- Stack Overflow questions tagged `convex-flutter` + +## Success Criteria + +✅ Package published successfully +✅ Version 2.2.0 visible on pub.dev +✅ Documentation renders correctly +✅ Example app accessible via pub.dev +✅ Package score 130+/140 +✅ GitHub release created with tag v2.2.0 +✅ All features working as documented + +## Contact + +If you encounter any issues during deployment: +- Check pub.dev documentation: https://dart.dev/tools/pub/publishing +- Flutter pub publishing guide: https://flutter.dev/docs/development/packages-and-plugins/developing-packages + +--- + +**Ready to publish!** 🚀 + +Run: `flutter pub publish` diff --git a/third_party/convex_flutter/README.md b/third_party/convex_flutter/README.md new file mode 100644 index 00000000..ce0eed95 --- /dev/null +++ b/third_party/convex_flutter/README.md @@ -0,0 +1,493 @@ +# Convex Flutter + +

+ + + + + + + + + +
Home ScreenMessaging Screen
Home ScreenReal-time Messaging
+

+ +A Flutter plugin for integrating with the Convex backend. It provides a simple Dart API over the Convex Rust core to run queries, mutations, and actions, and to subscribe to real-time updates. + +This package wraps the [Convex Rust library](https://github.com/get-convex/convex-rs) and exposes a Flutter-friendly interface. + +## Features + +- Real-time subscriptions to Convex queries +- Simple Dart API for queries, mutations, and actions +- Authentication with automatic token refresh +- Auth state stream for reactive UI updates +- **WebSocket connection state** - Real-time connection status monitoring via streams +- **Operation timeouts** - Configurable timeout for all queries, mutations, and actions +- **Lifecycle monitoring** - Stream of app lifecycle events (foreground/background) +- **Connection management** - Manual connection checking and reconnect functionality +- **Singleton pattern** - Access client anywhere via `ConvexClient.instance` +- **Multi-platform support** - Works on Web (pure Dart), Android, iOS, macOS, Windows, and Linux (FFI) + +## Installation + +Add the package to your Flutter project: + +```bash +flutter pub add convex_flutter +``` + +That's it! The health check query mentioned below is optional - you can start using the SDK immediately without it. + +## Platform Configuration + +**Important**: Some platforms require additional configuration for network access. This is a one-time setup. + +| Platform | Configuration Required | +|----------|------------------------| +| **Web** | ✅ None - works automatically | +| **iOS** | ✅ None - works automatically | +| **macOS** | ⚠️ **Network entitlements required** | +| **Android** | ⚠️ **INTERNET permission required** | +| **Windows** | ✅ None - works automatically | +| **Linux** | ✅ None - works automatically | + +### Quick Setup + +**macOS**: Add network entitlements to `macos/Runner/DebugProfile.entitlements` and `Release.entitlements`: + +```xml +com.apple.security.network.client + +com.apple.security.network.server + +``` + +**Android**: Add internet permission to `android/app/src/main/AndroidManifest.xml`: + +```xml + +``` + +**📖 See [PLATFORM_CONFIGURATION.md](PLATFORM_CONFIGURATION.md) for complete setup instructions and troubleshooting.** + +## Requirements + +- Dart SDK >= 3.8.1 and Flutter >= 3.3.0 +- **Web platform**: No additional requirements (uses pure Dart implementation) +- **Native platforms** (Android, iOS, macOS, Windows, Linux): + - Rust toolchain (rustup + cargo) for building native code + - Platform-specific toolchains: + - Android: JDK 11 and Android SDK/NDK + - iOS/macOS: Xcode and CocoaPods + - Windows: Visual Studio Build Tools (C++) + - Linux: clang, pkg-config, and build essentials + +## Quick start + +### Optional: Create a Health Check Query (Recommended) + +For connection monitoring and health checks, it's recommended to create a lightweight health check query in your Convex backend. This is **optional** but provides a clean way to verify connectivity without side effects. + +Create a file `convex/health.ts` in your Convex backend: + +```typescript +// convex/health.ts +import { query } from "./_generated/server"; + +export const ping = query({ + args: {}, + handler: async () => { + return "ok"; + }, +}); +``` + +This creates a lightweight endpoint at `health:ping` that you can use for connection health checks. It has no side effects and returns instantly. + +### Initialize the Client + +```dart +import 'package:convex_flutter/convex_flutter.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize the client once (singleton) + await ConvexClient.initialize( + ConvexConfig( + deploymentUrl: 'https://my-app.convex.cloud', + clientId: 'flutter-app-1.0', + operationTimeout: Duration(seconds: 30), // Optional, defaults to 30s + healthCheckQuery: 'health:ping', // Optional, for connection checks (requires health.ts) + ), + ); + + runApp(MyApp()); +} + +// Access the client anywhere in your app +void example() async { + final client = ConvexClient.instance; + + // Optional: authenticate (see Authentication section below) + await client.setAuth(token: 'YOUR_AUTH_TOKEN'); + + // Query (with timeout) + try { + final users = await client.query('users:list', {'limit': '10'}); + print('Users: $users'); + } on TimeoutException { + print('Connection timeout!'); + } + + // Subscribe to real-time updates + final sub = await client.subscribe( + name: 'messages:list', + args: {}, + onUpdate: (value) => print('Update: $value'), + onError: (message, value) => print('Error: $message ${value ?? ''}'), + ); + + // Mutation + await client.mutation( + name: 'messages:send', + args: {'body': 'Hello!', 'author': 'User123'}, + ); + + // Action (if you have actions defined) + // final res = await client.action(name: 'files:upload', args: {...}); + + // Later, when done + sub.cancel(); +} +``` + +## Authentication + +The SDK provides comprehensive authentication support for Convex backends. + +### Simple Token Authentication + +For basic scenarios or testing, set a static JWT token: + +```dart +// Set authentication +await client.setAuth(token: 'your-jwt-token'); + +// Clear authentication +await client.setAuth(token: null); +``` + +### Automatic Token Refresh (Recommended) + +For production apps, use `setAuthWithRefresh` which automatically refreshes tokens 60 seconds before they expire: + +```dart +final authHandle = await client.setAuthWithRefresh( + fetchToken: () async { + // Return JWT from your auth provider (Firebase, Clerk, Auth0, etc.) + return await FirebaseAuth.instance.currentUser?.getIdToken(); + }, + onAuthChange: (isAuthenticated) { + print('Auth state: $isAuthenticated'); + }, +); + +// When signing out, dispose the auth handle +authHandle.dispose(); +``` + +### Auth State Stream + +Listen to authentication state changes reactively: + +```dart +client.authState.listen((isAuthenticated) { + setState(() => _isLoggedIn = isAuthenticated); +}); +``` + +### Sync Auth Check + +Check current auth state synchronously: + +```dart +if (client.isAuthenticated) { + // User is authenticated +} +``` + +### Clear Authentication + +Clear auth and stop any running token refresh: + +```dart +await client.clearAuth(); +``` + +## Connection Management + +The SDK provides tools for managing connection state and handling network interruptions. + +### Operation Timeouts + +All queries, mutations, and actions have configurable timeouts (default: 30 seconds): + +```dart +await ConvexClient.initialize( + ConvexConfig( + deploymentUrl: 'https://my-app.convex.cloud', + operationTimeout: Duration(seconds: 45), // Custom timeout + ), +); + +// Operations will throw TimeoutException if they exceed the timeout +try { + await ConvexClient.instance.query('slowQuery', {}); +} on TimeoutException { + print('Operation timed out!'); +} +``` + +### Real-Time WebSocket Connection State (Recommended) + +Monitor WebSocket connection state in real-time using streams. This is the recommended approach for connection monitoring: + +```dart +// Listen to connection state changes +ConvexClient.instance.connectionState.listen((state) { + switch (state) { + case WebSocketConnectionState.connected: + print('WebSocket connected!'); + // Update UI, enable features + break; + case WebSocketConnectionState.connecting: + print('WebSocket connecting...'); + // Show loading indicator + break; + } +}); + +// Or use in a StreamBuilder for reactive UI +StreamBuilder( + stream: ConvexClient.instance.connectionState, + initialData: ConvexClient.instance.currentConnectionState, + builder: (context, snapshot) { + final state = snapshot.data ?? WebSocketConnectionState.connecting; + final isConnected = state == WebSocketConnectionState.connected; + + return Chip( + avatar: Icon(isConnected ? Icons.cloud_done : Icons.cloud_sync), + label: Text(isConnected ? 'Connected' : 'Connecting'), + backgroundColor: isConnected ? Colors.green : Colors.orange, + ); + }, +) + +// Synchronous access to current state +if (ConvexClient.instance.isConnected) { + // WebSocket is connected +} +``` + +**Features:** +- Real-time state updates via Stream (no polling needed) +- Automatic state transitions when WebSocket connects/disconnects +- Synchronous getter for immediate state access +- Works across all platforms + +**Note:** The WebSocket connection is established lazily when the first operation (query, mutation, subscribe, action) is executed. + +**Optional: Auto-Connect on Startup** + +To establish the connection immediately when your app starts (recommended for better UX), trigger a lightweight query in your app's initialization. Using a dedicated health check query is the cleanest approach: + +**1. Create a health check query in your Convex backend (optional but recommended):** + +```typescript +// convex/health.ts +import { query } from "./_generated/server"; + +export const ping = query({ + args: {}, + handler: async () => { + return "ok"; + }, +}); +``` + +**2. Trigger it on app startup:** + +```dart +// In your home screen or app initialization +@override +void initState() { + super.initState(); + // Trigger connection immediately with health check + ConvexClient.instance.query('health:ping', {}); +} +``` + +**Alternative:** You can use any existing lightweight query instead of creating a dedicated health check: + +```dart +// Use any existing query to trigger connection +ConvexClient.instance.query('users:list', {'limit': '1'}); +``` + +### Manual Connection Check (Deprecated) + +For backward compatibility, you can check connection status manually using a health check query: + +```dart +// Configure a lightweight query for health checks +await ConvexClient.initialize( + ConvexConfig( + deploymentUrl: 'https://my-app.convex.cloud', + healthCheckQuery: 'health:ping', // Lightweight health check query + ), +); + +// Check connection status (deprecated - use connectionState stream instead) +final status = await ConvexClient.instance.checkConnection(); + +switch (status) { + case ConnectionStatus.connected: + print('Connected!'); + case ConnectionStatus.timeout: + print('Connection timeout'); + case ConnectionStatus.error: + print('Connection error'); + case ConnectionStatus.unknown: + print('Not checked yet'); +} +``` + +### Manual Reconnect + +Trigger reconnection attempt manually: + +```dart +final connected = await ConvexClient.instance.reconnect(); +if (connected) { + print('Reconnected successfully'); +} +``` + +## Lifecycle Monitoring + +Monitor app lifecycle events to handle foreground/background transitions. + +### Listen to Lifecycle Events + +```dart +ConvexClient.instance.lifecycleEvents.listen((event) { + print('App lifecycle: $event'); + + if (event == AppLifecycleEvent.resumed) { + // App came to foreground + // Optionally reconnect or refresh data + ConvexClient.instance.reconnect(); + } + + if (event == AppLifecycleEvent.paused) { + // App went to background + // Optionally pause polling or save state + } +}); +``` + +### Lifecycle Events + +- `AppLifecycleEvent.resumed` - App in foreground +- `AppLifecycleEvent.paused` - App in background +- `AppLifecycleEvent.inactive` - App inactive (e.g., during phone call) +- `AppLifecycleEvent.detached` - App being terminated + +## API overview + +| Method | Description | +|--------|-------------| +| `ConvexClient.initialize(ConvexConfig)` | Initialize singleton client with configuration | +| `ConvexClient.instance` | Access singleton instance anywhere | +| `query(name, args)` | Execute a query with timeout, returns JSON string | +| `mutation({ name, args })` | Execute a mutation with timeout, returns JSON string | +| `action({ name, args })` | Execute an action with timeout, returns JSON string | +| `subscribe({ name, args, onUpdate, onError })` | Subscribe to real-time updates, returns `SubscriptionHandle` | +| `setAuth({ token })` | Set or clear static auth token | +| `setAuthWithRefresh({ fetchToken, onAuthChange })` | Set auth with automatic token refresh, returns `AuthHandleWrapper` | +| `authState` | Stream of auth state changes (`Stream`) | +| `isAuthenticated` | Current auth state (sync getter) | +| `clearAuth()` | Clear auth and stop token refresh | +| `connectionState` | Real-time WebSocket connection state stream (`Stream`) | +| `currentConnectionState` | Current connection state (sync getter) | +| `isConnected` | Returns true if WebSocket is connected (sync getter) | +| `checkConnection()` | _(Deprecated)_ Manually check connection status, returns `ConnectionStatus` | +| `reconnect()` | Manually trigger reconnection attempt, returns `bool` | +| `lifecycleEvents` | Stream of app lifecycle events (`Stream`) | +| `dispose()` | Clean up client resources | + +See the inline docs in `lib/src/convex_client.dart` for details. + +## Example app + +An example is provided under `example/`: + +``` +cd example +flutter run +``` + +The example demonstrates: +- Real-time chat with subscriptions +- Sending messages with mutations +- Authentication with JWT tokens +- Auth state management +- **WebSocket connection state monitoring** with visual indicators +- Lifecycle event monitoring (shows app state in AppBar) +- Connection screen with real-time state history +- Automatic connection on app startup +- Singleton pattern usage (`ConvexClient.instance`) + +## Troubleshooting + +### Build Issues + +- **Rust not found** (native platforms only): + - Visit [Rust Getting Started Guide](https://www.rust-lang.org/learn/get-started) + - Install Rust: + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + - Update your PATH (add to `~/.bashrc`, `~/.zshrc`, or equivalent): + ```bash + source "$HOME/.cargo/env" + ``` + - Verify installation: + ```bash + rustc --version + cargo --version + ``` + - **Note**: Not needed for web platform +- **Android build issues**: Use JDK 11, ensure NDK is installed via Android SDK Manager +- **iOS/macOS**: Run `pod install` inside the `example/ios` or your app's `ios` folder if needed +- **Windows**: Install Visual Studio Build Tools with C++ workload + +### Connection Issues + +- **macOS stuck in "connecting" state**: Missing network entitlements - see [PLATFORM_CONFIGURATION.md](PLATFORM_CONFIGURATION.md#macos) +- **Android network errors**: Missing INTERNET permission - see [PLATFORM_CONFIGURATION.md](PLATFORM_CONFIGURATION.md#android) +- **WebSocket not connecting**: Check your `deploymentUrl` and network permissions +- **Timeout errors**: Increase `operationTimeout` in `ConvexConfig` + +**📖 For detailed troubleshooting, see [PLATFORM_CONFIGURATION.md](PLATFORM_CONFIGURATION.md#troubleshooting)** + +## Contributing + +Contributions are welcome! Please open an issue or pull request. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/third_party/convex_flutter/analysis_options.yaml b/third_party/convex_flutter/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/third_party/convex_flutter/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/third_party/convex_flutter/android/build.gradle b/third_party/convex_flutter/android/build.gradle new file mode 100644 index 00000000..dfbbaf3b --- /dev/null +++ b/third_party/convex_flutter/android/build.gradle @@ -0,0 +1,56 @@ +// The Android Gradle Plugin builds the native code with the Android NDK. + +group 'com.flutter_rust_bridge.convex_flutter' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + // The Android Gradle Plugin knows how to build native code with the NDK. + classpath 'com.android.tools.build:gradle:7.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.flutter_rust_bridge.convex_flutter' + } + + // Bumping the plugin compileSdkVersion requires all clients of this plugin + // to bump the version in their app. + compileSdkVersion 33 + + // Use the NDK version + // declared in /android/app/build.gradle file of the Flutter project. + // Replace it with a version number if this plugin requires a specfic NDK version. + // (e.g. ndkVersion "23.1.7779620") + ndkVersion android.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion 19 + } +} + +apply from: "../cargokit/gradle/plugin.gradle" +cargokit { + manifestDir = "../rust" + libname = "convex_flutter" +} diff --git a/third_party/convex_flutter/android/settings.gradle b/third_party/convex_flutter/android/settings.gradle new file mode 100644 index 00000000..f6e5df69 --- /dev/null +++ b/third_party/convex_flutter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'convex_flutter' diff --git a/third_party/convex_flutter/android/src/main/AndroidManifest.xml b/third_party/convex_flutter/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..43a45c47 --- /dev/null +++ b/third_party/convex_flutter/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/third_party/convex_flutter/build.yaml b/third_party/convex_flutter/build.yaml new file mode 100644 index 00000000..d82d0b6d --- /dev/null +++ b/third_party/convex_flutter/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + freezed: + generate_for: + - lib/src/rust/*.dart diff --git a/third_party/convex_flutter/cargokit/LICENSE b/third_party/convex_flutter/cargokit/LICENSE new file mode 100644 index 00000000..d33a5fea --- /dev/null +++ b/third_party/convex_flutter/cargokit/LICENSE @@ -0,0 +1,42 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Copyright 2022 Matej Knopp + +================================================================================ + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +================================================================================ + +APACHE LICENSE, VERSION 2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/third_party/convex_flutter/cargokit/README b/third_party/convex_flutter/cargokit/README new file mode 100644 index 00000000..398474db --- /dev/null +++ b/third_party/convex_flutter/cargokit/README @@ -0,0 +1,11 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +Experimental repository to provide glue for seamlessly integrating cargo build +with flutter plugins and packages. + +See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ +for a tutorial on how to use Cargokit. + +Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. + diff --git a/third_party/convex_flutter/cargokit/build_pod.sh b/third_party/convex_flutter/cargokit/build_pod.sh new file mode 100755 index 00000000..ed0e0d98 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_pod.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e + +BASEDIR=$(dirname "$0") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +BASEDIR=$(cd "$BASEDIR" ; pwd -P) + +# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project +NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` + +export PATH=${NEW_PATH%?} # remove trailing : + +env + +# Platform name (macosx, iphoneos, iphonesimulator) +export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME + +# Arctive architectures (arm64, armv7, x86_64), space separated. +export CARGOKIT_DARWIN_ARCHS=$ARCHS + +# Current build configuration (Debug, Release) +export CARGOKIT_CONFIGURATION=$CONFIGURATION + +# Path to directory containing Cargo.toml. +export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 + +# Temporary directory for build artifacts. +export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR + +# Output directory for final artifacts. +export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME + +# Directory to store built tool artifacts. +export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool + +# Directory inside root project. Not necessarily the top level directory of root project. +export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT + +FLUTTER_EXPORT_BUILD_ENVIRONMENT=( + "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS + "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS +) + +for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" +do + if [[ -f "$path" ]]; then + source "$path" + fi +done + +sh "$BASEDIR/run_build_tool.sh" build-pod "$@" + +# Make a symlink from built framework to phony file, which will be used as input to +# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate +# attribute on custom build phase) +ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" +ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/third_party/convex_flutter/cargokit/build_tool/README.md b/third_party/convex_flutter/cargokit/build_tool/README.md new file mode 100644 index 00000000..a878c279 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/README.md @@ -0,0 +1,5 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/third_party/convex_flutter/cargokit/build_tool/analysis_options.yaml b/third_party/convex_flutter/cargokit/build_tool/analysis_options.yaml new file mode 100644 index 00000000..0e16a8b0 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/analysis_options.yaml @@ -0,0 +1,34 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +linter: + rules: + - prefer_relative_imports + - directives_ordering + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/third_party/convex_flutter/cargokit/build_tool/bin/build_tool.dart b/third_party/convex_flutter/cargokit/build_tool/bin/build_tool.dart new file mode 100644 index 00000000..268eb524 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/bin/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:build_tool/build_tool.dart' as build_tool; + +void main(List arguments) { + build_tool.runMain(arguments); +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/build_tool.dart b/third_party/convex_flutter/cargokit/build_tool/lib/build_tool.dart new file mode 100644 index 00000000..7c1bb750 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/build_tool.dart @@ -0,0 +1,8 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'src/build_tool.dart' as build_tool; + +Future runMain(List args) async { + return build_tool.runMain(args); +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/android_environment.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/android_environment.dart new file mode 100644 index 00000000..15fc9eed --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/android_environment.dart @@ -0,0 +1,195 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; +import 'dart:isolate'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; +import 'package:version/version.dart'; + +import 'target.dart'; +import 'util.dart'; + +class AndroidEnvironment { + AndroidEnvironment({ + required this.sdkPath, + required this.ndkVersion, + required this.minSdkVersion, + required this.targetTempDir, + required this.target, + }); + + static void clangLinkerWrapper(List args) { + final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; + if (clang == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); + } + final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; + if (target == null) { + throw Exception( + "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); + } + + runCommand(clang, [ + target, + ...args, + ]); + } + + /// Full path to Android SDK. + final String sdkPath; + + /// Full version of Android NDK. + final String ndkVersion; + + /// Minimum supported SDK version. + final int minSdkVersion; + + /// Target directory for build artifacts. + final String targetTempDir; + + /// Target being built. + final Target target; + + bool ndkIsInstalled() { + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); + return ndkPackageXml.existsSync(); + } + + void installNdk({ + required String javaHome, + }) { + final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; + final sdkManager = path.join( + sdkPath, + 'cmdline-tools', + 'latest', + 'bin', + 'sdkmanager$sdkManagerExtension', + ); + + log.info('Installing NDK $ndkVersion'); + runCommand(sdkManager, [ + '--install', + 'ndk;$ndkVersion', + ], environment: { + 'JAVA_HOME': javaHome, + }); + } + + Future> buildEnvironment() async { + final hostArch = Platform.isMacOS + ? "darwin-x86_64" + : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); + + final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); + final toolchainPath = path.join( + ndkPath, + 'toolchains', + 'llvm', + 'prebuilt', + hostArch, + 'bin', + ); + + final minSdkVersion = + math.max(target.androidMinSdkVersion!, this.minSdkVersion); + + final exe = Platform.isWindows ? '.exe' : ''; + + final arKey = 'AR_${target.rust}'; + final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] + .map((e) => path.join(toolchainPath, e)) + .firstWhereOrNull((element) => File(element).existsSync()); + if (arValue == null) { + throw Exception('Failed to find ar for $target in $toolchainPath'); + } + + final targetArg = '--target=${target.rust}$minSdkVersion'; + + final ccKey = 'CC_${target.rust}'; + final ccValue = path.join(toolchainPath, 'clang$exe'); + final cfFlagsKey = 'CFLAGS_${target.rust}'; + final cFlagsValue = targetArg; + + final cxxKey = 'CXX_${target.rust}'; + final cxxValue = path.join(toolchainPath, 'clang++$exe'); + final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; + final cxxFlagsValue = targetArg; + + final linkerKey = + 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); + + final ranlibKey = 'RANLIB_${target.rust}'; + final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); + + final ndkVersionParsed = Version.parse(ndkVersion); + final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; + final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); + + final runRustTool = + Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; + + final packagePath = (await Isolate.resolvePackageUri( + Uri.parse('package:build_tool/buildtool.dart')))! + .toFilePath(); + final selfPath = path.canonicalize(path.join( + packagePath, + '..', + '..', + '..', + runRustTool, + )); + + // Make sure that run_build_tool is working properly even initially launched directly + // through dart run. + final toolTempDir = + Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; + + return { + arKey: arValue, + ccKey: ccValue, + cfFlagsKey: cFlagsValue, + cxxKey: cxxValue, + cxxFlagsKey: cxxFlagsValue, + ranlibKey: ranlibValue, + rustFlagsKey: rustFlagsValue, + linkerKey: selfPath, + // Recognized by main() so we know when we're acting as a wrapper + '_CARGOKIT_NDK_LINK_TARGET': targetArg, + '_CARGOKIT_NDK_LINK_CLANG': ccValue, + 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, + }; + } + + // Workaround for libgcc missing in NDK23, inspired by cargo-ndk + String _libGccWorkaround(String buildDir, Version ndkVersion) { + final workaroundDir = path.join( + buildDir, + 'cargokit', + 'libgcc_workaround', + '${ndkVersion.major}', + ); + Directory(workaroundDir).createSync(recursive: true); + if (ndkVersion.major >= 23) { + File(path.join(workaroundDir, 'libgcc.a')) + .writeAsStringSync('INPUT(-lunwind)'); + } else { + // Other way around, untested, forward libgcc.a from libunwind once Rust + // gets updated for NDK23+. + File(path.join(workaroundDir, 'libunwind.a')) + .writeAsStringSync('INPUT(-lgcc)'); + } + + var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; + if (rustFlags.isNotEmpty) { + rustFlags = '$rustFlags\x1f'; + } + rustFlags = '$rustFlags-L\x1f$workaroundDir'; + return rustFlags; + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/artifacts_provider.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/artifacts_provider.dart new file mode 100644 index 00000000..e608cece --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/artifacts_provider.dart @@ -0,0 +1,266 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'builder.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'rustup.dart'; +import 'target.dart'; + +class Artifact { + /// File system location of the artifact. + final String path; + + /// Actual file name that the artifact should have in destination folder. + final String finalFileName; + + AritifactType get type { + if (finalFileName.endsWith('.dll') || + finalFileName.endsWith('.dll.lib') || + finalFileName.endsWith('.pdb') || + finalFileName.endsWith('.so') || + finalFileName.endsWith('.dylib')) { + return AritifactType.dylib; + } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { + return AritifactType.staticlib; + } else { + throw Exception('Unknown artifact type for $finalFileName'); + } + } + + Artifact({ + required this.path, + required this.finalFileName, + }); +} + +final _log = Logger('artifacts_provider'); + +class ArtifactProvider { + ArtifactProvider({ + required this.environment, + required this.userOptions, + }); + + final BuildEnvironment environment; + final CargokitUserOptions userOptions; + + Future>> getArtifacts(List targets) async { + final result = await _getPrecompiledArtifacts(targets); + + final pendingTargets = List.of(targets); + pendingTargets.removeWhere((element) => result.containsKey(element)); + + if (pendingTargets.isEmpty) { + return result; + } + + final rustup = Rustup(); + for (final target in targets) { + final builder = RustBuilder(target: target, environment: environment); + builder.prepare(rustup); + _log.info('Building ${environment.crateInfo.packageName} for $target'); + final targetDir = await builder.build(); + // For local build accept both static and dynamic libraries. + final artifactNames = { + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.dylib, + remote: false, + ), + ...getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + aritifactType: AritifactType.staticlib, + remote: false, + ) + }; + final artifacts = artifactNames + .map((artifactName) => Artifact( + path: path.join(targetDir, artifactName), + finalFileName: artifactName, + )) + .where((element) => File(element.path).existsSync()) + .toList(); + result[target] = artifacts; + } + return result; + } + + Future>> _getPrecompiledArtifacts( + List targets) async { + if (userOptions.usePrecompiledBinaries == false) { + _log.info('Precompiled binaries are disabled'); + return {}; + } + if (environment.crateOptions.precompiledBinaries == null) { + _log.fine('Precompiled binaries not enabled for this crate'); + return {}; + } + + final start = Stopwatch()..start(); + final crateHash = CrateHash.compute(environment.manifestDir, + tempStorage: environment.targetTempDir); + _log.fine( + 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); + + final downloadedArtifactsDir = + path.join(environment.targetTempDir, 'precompiled', crateHash); + Directory(downloadedArtifactsDir).createSync(recursive: true); + + final res = >{}; + + for (final target in targets) { + final requiredArtifacts = getArtifactNames( + target: target, + libraryName: environment.crateInfo.packageName, + remote: true, + ); + final artifactsForTarget = []; + + for (final artifact in requiredArtifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final downloadedPath = path.join(downloadedArtifactsDir, fileName); + if (!File(downloadedPath).existsSync()) { + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + await _tryDownloadArtifacts( + crateHash: crateHash, + fileName: fileName, + signatureFileName: signatureFileName, + finalPath: downloadedPath, + ); + } + if (File(downloadedPath).existsSync()) { + artifactsForTarget.add(Artifact( + path: downloadedPath, + finalFileName: artifact, + )); + } else { + break; + } + } + + // Only provide complete set of artifacts. + if (artifactsForTarget.length == requiredArtifacts.length) { + _log.fine('Found precompiled artifacts for $target'); + res[target] = artifactsForTarget; + } + } + + return res; + } + + static Future _get(Uri url, {Map? headers}) async { + int attempt = 0; + const maxAttempts = 10; + while (true) { + try { + return await get(url, headers: headers); + } on SocketException catch (e) { + // Try to detect reset by peer error and retry. + if (attempt++ < maxAttempts && + (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { + _log.severe( + 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); + await Future.delayed(Duration(seconds: 1)); + continue; + } else { + rethrow; + } + } + } + } + + Future _tryDownloadArtifacts({ + required String crateHash, + required String fileName, + required String signatureFileName, + required String finalPath, + }) async { + final precompiledBinaries = environment.crateOptions.precompiledBinaries!; + final prefix = precompiledBinaries.uriPrefix; + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); + _log.fine('Downloading signature from $signatureUrl'); + final signature = await _get(signatureUrl); + if (signature.statusCode == 404) { + _log.warning( + 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); + return; + } + if (signature.statusCode != 200) { + _log.severe( + 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); + return; + } + _log.fine('Downloading binary from $url'); + final res = await _get(url); + if (res.statusCode != 200) { + _log.severe('Failed to download binary $url: status ${res.statusCode}'); + return; + } + if (verify( + precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { + File(finalPath).writeAsBytesSync(res.bodyBytes); + } else { + _log.shout('Signature verification failed! Ignoring binary.'); + } + } +} + +enum AritifactType { + staticlib, + dylib, +} + +AritifactType artifactTypeForTarget(Target target) { + if (target.darwinPlatform != null) { + return AritifactType.staticlib; + } else { + return AritifactType.dylib; + } +} + +List getArtifactNames({ + required Target target, + required String libraryName, + required bool remote, + AritifactType? aritifactType, +}) { + aritifactType ??= artifactTypeForTarget(target); + if (target.darwinArch != null) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.dylib']; + } + } else if (target.rust.contains('-windows-')) { + if (aritifactType == AritifactType.staticlib) { + return ['$libraryName.lib']; + } else { + return [ + '$libraryName.dll', + '$libraryName.dll.lib', + if (!remote) '$libraryName.pdb' + ]; + } + } else if (target.rust.contains('-linux-')) { + if (aritifactType == AritifactType.staticlib) { + return ['lib$libraryName.a']; + } else { + return ['lib$libraryName.so']; + } + } else { + throw Exception("Unsupported target: ${target.rust}"); + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/build_cmake.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_cmake.dart new file mode 100644 index 00000000..6f3b2a4e --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_cmake.dart @@ -0,0 +1,40 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +class BuildCMake { + final CargokitUserOptions userOptions; + + BuildCMake({required this.userOptions}); + + Future build() async { + final targetPlatform = Environment.targetPlatform; + final target = Target.forFlutterName(Environment.targetPlatform); + if (target == null) { + throw Exception("Unknown target platform: $targetPlatform"); + } + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts([target]); + + final libs = artifacts[target]!; + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path) + .copySync(path.join(Environment.outputDir, lib.finalFileName)); + } + } + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/build_gradle.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_gradle.dart new file mode 100644 index 00000000..7e61fcbb --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_gradle.dart @@ -0,0 +1,49 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; + +final log = Logger('build_gradle'); + +class BuildGradle { + BuildGradle({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.targetPlatforms.map((arch) { + final target = Target.forFlutterName(arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: true); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + for (final target in targets) { + final libs = artifacts[target]!; + final outputDir = path.join(Environment.outputDir, target.android!); + Directory(outputDir).createSync(recursive: true); + + for (final lib in libs) { + if (lib.type == AritifactType.dylib) { + File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); + } + } + } + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/build_pod.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_pod.dart new file mode 100644 index 00000000..8a9c0db5 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_pod.dart @@ -0,0 +1,89 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'target.dart'; +import 'util.dart'; + +class BuildPod { + BuildPod({required this.userOptions}); + + final CargokitUserOptions userOptions; + + Future build() async { + final targets = Environment.darwinArchs.map((arch) { + final target = Target.forDarwin( + platformName: Environment.darwinPlatformName, darwinAarch: arch); + if (target == null) { + throw Exception( + "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); + } + return target; + }).toList(); + + final environment = BuildEnvironment.fromEnvironment(isAndroid: false); + final provider = + ArtifactProvider(environment: environment, userOptions: userOptions); + final artifacts = await provider.getArtifacts(targets); + + void performLipo(String targetFile, Iterable sourceFiles) { + runCommand("lipo", [ + '-create', + ...sourceFiles, + '-output', + targetFile, + ]); + } + + final outputDir = Environment.outputDir; + + Directory(outputDir).createSync(recursive: true); + + final staticLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.staticlib) + .toList(); + final dynamicLibs = artifacts.values + .expand((element) => element) + .where((element) => element.type == AritifactType.dylib) + .toList(); + + final libName = environment.crateInfo.packageName; + + // If there is static lib, use it and link it with pod + if (staticLibs.isNotEmpty) { + final finalTargetFile = path.join(outputDir, "lib$libName.a"); + performLipo(finalTargetFile, staticLibs.map((e) => e.path)); + } else { + // Otherwise try to replace bundle dylib with our dylib + final bundlePaths = [ + '$libName.framework/Versions/A/$libName', + '$libName.framework/$libName', + ]; + + for (final bundlePath in bundlePaths) { + final targetFile = path.join(outputDir, bundlePath); + if (File(targetFile).existsSync()) { + performLipo(targetFile, dynamicLibs.map((e) => e.path)); + + // Replace absolute id with @rpath one so that it works properly + // when moved to Frameworks. + runCommand("install_name_tool", [ + '-id', + '@rpath/$bundlePath', + targetFile, + ]); + return; + } + } + throw Exception('Unable to find bundle for dynamic library'); + } + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/build_tool.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_tool.dart new file mode 100644 index 00000000..c8f36981 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/build_tool.dart @@ -0,0 +1,271 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; + +import 'android_environment.dart'; +import 'build_cmake.dart'; +import 'build_gradle.dart'; +import 'build_pod.dart'; +import 'logging.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; +import 'util.dart'; +import 'verify_binaries.dart'; + +final log = Logger('build_tool'); + +abstract class BuildCommand extends Command { + Future runBuildCommand(CargokitUserOptions options); + + @override + Future run() async { + final options = CargokitUserOptions.load(); + + if (options.verboseLogging || + Platform.environment['CARGOKIT_VERBOSE'] == '1') { + enableVerboseLogging(); + } + + await runBuildCommand(options); + } +} + +class BuildPodCommand extends BuildCommand { + @override + final name = 'build-pod'; + + @override + final description = 'Build cocoa pod library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildPod(userOptions: options); + await build.build(); + } +} + +class BuildGradleCommand extends BuildCommand { + @override + final name = 'build-gradle'; + + @override + final description = 'Build android library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildGradle(userOptions: options); + await build.build(); + } +} + +class BuildCMakeCommand extends BuildCommand { + @override + final name = 'build-cmake'; + + @override + final description = 'Build CMake library'; + + @override + Future runBuildCommand(CargokitUserOptions options) async { + final build = BuildCMake(userOptions: options); + await build.build(); + } +} + +class GenKeyCommand extends Command { + @override + final name = 'gen-key'; + + @override + final description = 'Generate key pair for signing precompiled binaries'; + + @override + void run() { + final kp = generateKey(); + final private = HEX.encode(kp.privateKey.bytes); + final public = HEX.encode(kp.publicKey.bytes); + print("Private Key: $private"); + print("Public Key: $public"); + } +} + +class PrecompileBinariesCommand extends Command { + PrecompileBinariesCommand() { + argParser + ..addOption( + 'repository', + mandatory: true, + help: 'Github repository slug in format owner/name', + ) + ..addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ) + ..addMultiOption('target', + help: 'Rust target triple of artifact to build.\n' + 'Can be specified multiple times or omitted in which case\n' + 'all targets for current platform will be built.') + ..addOption( + 'android-sdk-location', + help: 'Location of Android SDK (if available)', + ) + ..addOption( + 'android-ndk-version', + help: 'Android NDK version (if available)', + ) + ..addOption( + 'android-min-sdk-version', + help: 'Android minimum rquired version (if available)', + ) + ..addOption( + 'temp-dir', + help: 'Directory to store temporary build artifacts', + ) + ..addFlag( + "verbose", + abbr: "v", + defaultsTo: false, + help: "Enable verbose logging", + ); + } + + @override + final name = 'precompile-binaries'; + + @override + final description = 'Prebuild and upload binaries\n' + 'Private key must be passed through PRIVATE_KEY environment variable. ' + 'Use gen_key through generate priave key.\n' + 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; + + @override + Future run() async { + final verbose = argResults!['verbose'] as bool; + if (verbose) { + enableVerboseLogging(); + } + + final privateKeyString = Platform.environment['PRIVATE_KEY']; + if (privateKeyString == null) { + throw ArgumentError('Missing PRIVATE_KEY environment variable'); + } + final githubToken = Platform.environment['GITHUB_TOKEN']; + if (githubToken == null) { + throw ArgumentError('Missing GITHUB_TOKEN environment variable'); + } + final privateKey = HEX.decode(privateKeyString); + if (privateKey.length != 64) { + throw ArgumentError('Private key must be 64 bytes long'); + } + final manifestDir = argResults!['manifest-dir'] as String; + if (!Directory(manifestDir).existsSync()) { + throw ArgumentError('Manifest directory does not exist: $manifestDir'); + } + String? androidMinSdkVersionString = + argResults!['android-min-sdk-version'] as String?; + int? androidMinSdkVersion; + if (androidMinSdkVersionString != null) { + androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); + if (androidMinSdkVersion == null) { + throw ArgumentError( + 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); + } + } + final targetStrigns = argResults!['target'] as List; + final targets = targetStrigns.map((target) { + final res = Target.forRustTriple(target); + if (res == null) { + throw ArgumentError('Invalid target: $target'); + } + return res; + }).toList(growable: false); + final precompileBinaries = PrecompileBinaries( + privateKey: PrivateKey(privateKey), + githubToken: githubToken, + manifestDir: manifestDir, + repositorySlug: RepositorySlug.full(argResults!['repository'] as String), + targets: targets, + androidSdkLocation: argResults!['android-sdk-location'] as String?, + androidNdkVersion: argResults!['android-ndk-version'] as String?, + androidMinSdkVersion: androidMinSdkVersion, + tempDir: argResults!['temp-dir'] as String?, + ); + + await precompileBinaries.run(); + } +} + +class VerifyBinariesCommand extends Command { + VerifyBinariesCommand() { + argParser.addOption( + 'manifest-dir', + mandatory: true, + help: 'Directory containing Cargo.toml', + ); + } + + @override + final name = "verify-binaries"; + + @override + final description = 'Verifies published binaries\n' + 'Checks whether there is a binary published for each targets\n' + 'and checks the signature.'; + + @override + Future run() async { + final manifestDir = argResults!['manifest-dir'] as String; + final verifyBinaries = VerifyBinaries( + manifestDir: manifestDir, + ); + await verifyBinaries.run(); + } +} + +Future runMain(List args) async { + try { + // Init logging before options are loaded + initLogging(); + + if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { + return AndroidEnvironment.clangLinkerWrapper(args); + } + + final runner = CommandRunner('build_tool', 'Cargokit built_tool') + ..addCommand(BuildPodCommand()) + ..addCommand(BuildGradleCommand()) + ..addCommand(BuildCMakeCommand()) + ..addCommand(GenKeyCommand()) + ..addCommand(PrecompileBinariesCommand()) + ..addCommand(VerifyBinariesCommand()); + + await runner.run(args); + } on ArgumentError catch (e) { + stderr.writeln(e.toString()); + exit(1); + } catch (e, s) { + log.severe(kDoubleSeparator); + log.severe('Cargokit BuildTool failed with error:'); + log.severe(kSeparator); + log.severe(e); + // This tells user to install Rust, there's no need to pollute the log with + // stack trace. + if (e is! RustupNotFoundException) { + log.severe(kSeparator); + log.severe(s); + log.severe(kSeparator); + log.severe('BuildTool arguments: $args'); + } + log.severe(kDoubleSeparator); + exit(1); + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/builder.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/builder.dart new file mode 100644 index 00000000..84c46e4f --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/builder.dart @@ -0,0 +1,198 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'android_environment.dart'; +import 'cargo.dart'; +import 'environment.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; +import 'util.dart'; + +final _log = Logger('builder'); + +enum BuildConfiguration { + debug, + release, + profile, +} + +extension on BuildConfiguration { + bool get isDebug => this == BuildConfiguration.debug; + String get rustName => switch (this) { + BuildConfiguration.debug => 'debug', + BuildConfiguration.release => 'release', + BuildConfiguration.profile => 'release', + }; +} + +class BuildException implements Exception { + final String message; + + BuildException(this.message); + + @override + String toString() { + return 'BuildException: $message'; + } +} + +class BuildEnvironment { + final BuildConfiguration configuration; + final CargokitCrateOptions crateOptions; + final String targetTempDir; + final String manifestDir; + final CrateInfo crateInfo; + + final bool isAndroid; + final String? androidSdkPath; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? javaHome; + + BuildEnvironment({ + required this.configuration, + required this.crateOptions, + required this.targetTempDir, + required this.manifestDir, + required this.crateInfo, + required this.isAndroid, + this.androidSdkPath, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.javaHome, + }); + + static BuildConfiguration parseBuildConfiguration(String value) { + // XCode configuration adds the flavor to configuration name. + final firstSegment = value.split('-').first; + final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( + (e) => e.name == firstSegment, + ); + if (buildConfiguration == null) { + _log.warning('Unknown build configuraiton $value, will assume release'); + return BuildConfiguration.release; + } + return buildConfiguration; + } + + static BuildEnvironment fromEnvironment({ + required bool isAndroid, + }) { + final buildConfiguration = + parseBuildConfiguration(Environment.configuration); + final manifestDir = Environment.manifestDir; + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + final crateInfo = CrateInfo.load(manifestDir); + return BuildEnvironment( + configuration: buildConfiguration, + crateOptions: crateOptions, + targetTempDir: Environment.targetTempDir, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: isAndroid, + androidSdkPath: isAndroid ? Environment.sdkPath : null, + androidNdkVersion: isAndroid ? Environment.ndkVersion : null, + androidMinSdkVersion: + isAndroid ? int.parse(Environment.minSdkVersion) : null, + javaHome: isAndroid ? Environment.javaHome : null, + ); + } +} + +class RustBuilder { + final Target target; + final BuildEnvironment environment; + + RustBuilder({ + required this.target, + required this.environment, + }); + + void prepare( + Rustup rustup, + ) { + final toolchain = _toolchain; + if (rustup.installedTargets(toolchain) == null) { + rustup.installToolchain(toolchain); + } + if (toolchain == 'nightly') { + rustup.installRustSrcForNightly(); + } + if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { + rustup.installTarget(target.rust, toolchain: toolchain); + } + } + + CargoBuildOptions? get _buildOptions => + environment.crateOptions.cargo[environment.configuration]; + + String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; + + /// Returns the path of directory containing build artifacts. + Future build() async { + final extraArgs = _buildOptions?.flags ?? []; + final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); + runCommand( + 'rustup', + [ + 'run', + _toolchain, + 'cargo', + 'build', + ...extraArgs, + '--manifest-path', + manifestPath, + '-p', + environment.crateInfo.packageName, + if (!environment.configuration.isDebug) '--release', + '--target', + target.rust, + '--target-dir', + environment.targetTempDir, + ], + environment: await _buildEnvironment(), + ); + return path.join( + environment.targetTempDir, + target.rust, + environment.configuration.rustName, + ); + } + + Future> _buildEnvironment() async { + if (target.android == null) { + return {}; + } else { + final sdkPath = environment.androidSdkPath; + final ndkVersion = environment.androidNdkVersion; + final minSdkVersion = environment.androidMinSdkVersion; + if (sdkPath == null) { + throw BuildException('androidSdkPath is not set'); + } + if (ndkVersion == null) { + throw BuildException('androidNdkVersion is not set'); + } + if (minSdkVersion == null) { + throw BuildException('androidMinSdkVersion is not set'); + } + final env = AndroidEnvironment( + sdkPath: sdkPath, + ndkVersion: ndkVersion, + minSdkVersion: minSdkVersion, + targetTempDir: environment.targetTempDir, + target: target, + ); + if (!env.ndkIsInstalled() && environment.javaHome != null) { + env.installNdk(javaHome: environment.javaHome!); + } + return env.buildEnvironment(); + } + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/cargo.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/cargo.dart new file mode 100644 index 00000000..0d8958ff --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/cargo.dart @@ -0,0 +1,48 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:toml/toml.dart'; + +class ManifestException { + ManifestException(this.message, {required this.fileName}); + + final String? fileName; + final String message; + + @override + String toString() { + if (fileName != null) { + return 'Failed to parse package manifest at $fileName: $message'; + } else { + return 'Failed to parse package manifest: $message'; + } + } +} + +class CrateInfo { + CrateInfo({required this.packageName}); + + final String packageName; + + static CrateInfo parseManifest(String manifest, {final String? fileName}) { + final toml = TomlDocument.parse(manifest); + final package = toml.toMap()['package']; + if (package == null) { + throw ManifestException('Missing package section', fileName: fileName); + } + final name = package['name']; + if (name == null) { + throw ManifestException('Missing package name', fileName: fileName); + } + return CrateInfo(packageName: name); + } + + static CrateInfo load(String manifestDir) { + final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); + final manifest = manifestFile.readAsStringSync(); + return parseManifest(manifest, fileName: manifestFile.path); + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/crate_hash.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/crate_hash.dart new file mode 100644 index 00000000..0c4d88d1 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/crate_hash.dart @@ -0,0 +1,124 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; +import 'package:path/path.dart' as path; + +class CrateHash { + /// Computes a hash uniquely identifying crate content. This takes into account + /// content all all .rs files inside the src directory, as well as Cargo.toml, + /// Cargo.lock, build.rs and cargokit.yaml. + /// + /// If [tempStorage] is provided, computed hash is stored in a file in that directory + /// and reused on subsequent calls if the crate content hasn't changed. + static String compute(String manifestDir, {String? tempStorage}) { + return CrateHash._( + manifestDir: manifestDir, + tempStorage: tempStorage, + )._compute(); + } + + CrateHash._({ + required this.manifestDir, + required this.tempStorage, + }); + + String _compute() { + final files = getFiles(); + final tempStorage = this.tempStorage; + if (tempStorage != null) { + final quickHash = _computeQuickHash(files); + final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); + quickHashFolder.createSync(recursive: true); + final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); + if (quickHashFile.existsSync()) { + return quickHashFile.readAsStringSync(); + } + final hash = _computeHash(files); + quickHashFile.writeAsStringSync(hash); + return hash; + } else { + return _computeHash(files); + } + } + + /// Computes a quick hash based on files stat (without reading contents). This + /// is used to cache the real hash, which is slower to compute since it involves + /// reading every single file. + String _computeQuickHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + final data = ByteData(8); + for (final file in files) { + input.add(utf8.encode(file.path)); + final stat = file.statSync(); + data.setUint64(0, stat.size); + input.add(data.buffer.asUint8List()); + data.setUint64(0, stat.modified.millisecondsSinceEpoch); + input.add(data.buffer.asUint8List()); + } + + input.close(); + return base64Url.encode(output.events.single.bytes); + } + + String _computeHash(List files) { + final output = AccumulatorSink(); + final input = sha256.startChunkedConversion(output); + + void addTextFile(File file) { + // text Files are hashed by lines in case we're dealing with github checkout + // that auto-converts line endings. + final splitter = LineSplitter(); + if (file.existsSync()) { + final data = file.readAsStringSync(); + final lines = splitter.convert(data); + for (final line in lines) { + input.add(utf8.encode(line)); + } + } + } + + for (final file in files) { + addTextFile(file); + } + + input.close(); + final res = output.events.single; + + // Truncate to 128bits. + final hash = res.bytes.sublist(0, 16); + return hex.encode(hash); + } + + List getFiles() { + final src = Directory(path.join(manifestDir, 'src')); + final files = src + .listSync(recursive: true, followLinks: false) + .whereType() + .toList(); + files.sortBy((element) => element.path); + void addFile(String relative) { + final file = File(path.join(manifestDir, relative)); + if (file.existsSync()) { + files.add(file); + } + } + + addFile('Cargo.toml'); + addFile('Cargo.lock'); + addFile('build.rs'); + addFile('cargokit.yaml'); + return files; + } + + final String manifestDir; + final String? tempStorage; +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/environment.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/environment.dart new file mode 100644 index 00000000..996483a1 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/environment.dart @@ -0,0 +1,68 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +extension on String { + String resolveSymlink() => File(this).resolveSymbolicLinksSync(); +} + +class Environment { + /// Current build configuration (debug or release). + static String get configuration => + _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); + + static bool get isDebug => configuration == 'debug'; + static bool get isRelease => configuration == 'release'; + + /// Temporary directory where Rust build artifacts are placed. + static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); + + /// Final output directory where the build artifacts are placed. + static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); + + /// Path to the crate manifest (containing Cargo.toml). + static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); + + /// Directory inside root project. Not necessarily root folder. Symlinks are + /// not resolved on purpose. + static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); + + // Pod + + /// Platform name (macosx, iphoneos, iphonesimulator). + static String get darwinPlatformName => + _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); + + /// List of architectures to build for (arm64, armv7, x86_64). + static List get darwinArchs => + _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); + + // Gradle + static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); + static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); + static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); + static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); + static List get targetPlatforms => + _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); + + // CMAKE + static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); + + static String _getEnv(String key) { + final res = Platform.environment[key]; + if (res == null) { + throw Exception("Missing environment variable $key"); + } + return res; + } + + static String _getEnvPath(String key) { + final res = _getEnv(key); + if (Directory(res).existsSync()) { + return res.resolveSymlink(); + } else { + return res; + } + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/logging.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/logging.dart new file mode 100644 index 00000000..5edd4fd1 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/logging.dart @@ -0,0 +1,52 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:logging/logging.dart'; + +const String kSeparator = "--"; +const String kDoubleSeparator = "=="; + +bool _lastMessageWasSeparator = false; + +void _log(LogRecord rec) { + final prefix = '${rec.level.name}: '; + final out = rec.level == Level.SEVERE ? stderr : stdout; + if (rec.message == kSeparator) { + if (!_lastMessageWasSeparator) { + out.write(prefix); + out.writeln('-' * 80); + _lastMessageWasSeparator = true; + } + return; + } else if (rec.message == kDoubleSeparator) { + out.write(prefix); + out.writeln('=' * 80); + _lastMessageWasSeparator = true; + return; + } + out.write(prefix); + out.writeln(rec.message); + _lastMessageWasSeparator = false; +} + +void initLogging() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((LogRecord rec) { + final lines = rec.message.split('\n'); + for (final line in lines) { + if (line.isNotEmpty || lines.length == 1 || line != lines.last) { + _log(LogRecord( + rec.level, + line, + rec.loggerName, + )); + } + } + }); +} + +void enableVerboseLogging() { + Logger.root.level = Level.ALL; +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/options.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/options.dart new file mode 100644 index 00000000..22aef1d3 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/options.dart @@ -0,0 +1,309 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:hex/hex.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import 'builder.dart'; +import 'environment.dart'; +import 'rustup.dart'; + +final _log = Logger('options'); + +/// A class for exceptions that have source span information attached. +class SourceSpanException implements Exception { + // This is a getter so that subclasses can override it. + /// A message describing the exception. + String get message => _message; + final String _message; + + // This is a getter so that subclasses can override it. + /// The span associated with this exception. + /// + /// This may be `null` if the source location can't be determined. + SourceSpan? get span => _span; + final SourceSpan? _span; + + SourceSpanException(this._message, this._span); + + /// Returns a string representation of `this`. + /// + /// [color] may either be a [String], a [bool], or `null`. If it's a string, + /// it indicates an ANSI terminal color escape that should be used to + /// highlight the span's text. If it's `true`, it indicates that the text + /// should be highlighted using the default color. If it's `false` or `null`, + /// it indicates that the text shouldn't be highlighted. + @override + String toString({Object? color}) { + if (span == null) return message; + return 'Error on ${span!.message(message, color: color)}'; + } +} + +enum Toolchain { + stable, + beta, + nightly, +} + +class CargoBuildOptions { + final Toolchain toolchain; + final List flags; + + CargoBuildOptions({ + required this.toolchain, + required this.flags, + }); + + static Toolchain _toolchainFromNode(YamlNode node) { + if (node case YamlScalar(value: String name)) { + final toolchain = + Toolchain.values.firstWhereOrNull((element) => element.name == name); + if (toolchain != null) { + return toolchain; + } + } + throw SourceSpanException( + 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', + node.span); + } + + static CargoBuildOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + Toolchain toolchain = Toolchain.stable; + List flags = []; + for (final MapEntry(:key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: 'toolchain')) { + toolchain = _toolchainFromNode(value); + } else if (key case YamlScalar(value: 'extra_flags')) { + if (value case YamlList(nodes: List list)) { + if (list.every((element) { + if (element case YamlScalar(value: String _)) { + return true; + } + return false; + })) { + flags = list.map((e) => e.value as String).toList(); + continue; + } + } + throw SourceSpanException( + 'Extra flags must be a list of strings', value.span); + } else { + throw SourceSpanException( + 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', + key.span); + } + } + return CargoBuildOptions(toolchain: toolchain, flags: flags); + } +} + +extension on YamlMap { + /// Map that extracts keys so that we can do map case check on them. + Map get valueMap => + nodes.map((key, value) => MapEntry(key.value, value)); +} + +class PrecompiledBinaries { + final String uriPrefix; + final PublicKey publicKey; + + PrecompiledBinaries({ + required this.uriPrefix, + required this.publicKey, + }); + + static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { + final bytes = HEX.decode(key); + if (bytes.length != 32) { + throw SourceSpanException( + 'Invalid public key. Must be 32 bytes long.', span); + } + return PublicKey(bytes); + } + + static PrecompiledBinaries parse(YamlNode node) { + if (node case YamlMap(valueMap: Map map)) { + if (map + case { + 'url_prefix': YamlNode urlPrefixNode, + 'public_key': YamlNode publicKeyNode, + }) { + final urlPrefix = switch (urlPrefixNode) { + YamlScalar(value: String urlPrefix) => urlPrefix, + _ => throw SourceSpanException( + 'Invalid URL prefix value.', urlPrefixNode.span), + }; + final publicKey = switch (publicKeyNode) { + YamlScalar(value: String publicKey) => + _publicKeyFromHex(publicKey, publicKeyNode.span), + _ => throw SourceSpanException( + 'Invalid public key value.', publicKeyNode.span), + }; + return PrecompiledBinaries( + uriPrefix: urlPrefix, + publicKey: publicKey, + ); + } + } + throw SourceSpanException( + 'Invalid precompiled binaries value. ' + 'Expected Map with "url_prefix" and "public_key".', + node.span); + } +} + +/// Cargokit options specified for Rust crate. +class CargokitCrateOptions { + CargokitCrateOptions({ + this.cargo = const {}, + this.precompiledBinaries, + }); + + final Map cargo; + final PrecompiledBinaries? precompiledBinaries; + + static CargokitCrateOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + final options = {}; + PrecompiledBinaries? precompiledBinaries; + + for (final entry in node.nodes.entries) { + if (entry + case MapEntry( + key: YamlScalar(value: 'cargo'), + value: YamlNode node, + )) { + if (node is! YamlMap) { + throw SourceSpanException('Cargo options must be a map', node.span); + } + for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { + if (key case YamlScalar(value: String name)) { + final configuration = BuildConfiguration.values + .firstWhereOrNull((element) => element.name == name); + if (configuration != null) { + options[configuration] = CargoBuildOptions.parse(value); + continue; + } + } + throw SourceSpanException( + 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', + key.span); + } + } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { + precompiledBinaries = PrecompiledBinaries.parse(entry.value); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', + entry.key.span); + } + } + return CargokitCrateOptions( + cargo: options, + precompiledBinaries: precompiledBinaries, + ); + } + + static CargokitCrateOptions load({ + required String manifestDir, + }) { + final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); + final file = File.fromUri(uri); + if (file.existsSync()) { + final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); + return parse(contents); + } else { + return CargokitCrateOptions(); + } + } +} + +class CargokitUserOptions { + // When Rustup is installed always build locally unless user opts into + // using precompiled binaries. + static bool defaultUsePrecompiledBinaries() { + return Rustup.executablePath() == null; + } + + CargokitUserOptions({ + required this.usePrecompiledBinaries, + required this.verboseLogging, + }); + + CargokitUserOptions._() + : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), + verboseLogging = false; + + static CargokitUserOptions parse(YamlNode node) { + if (node is! YamlMap) { + throw SourceSpanException('Cargokit options must be a map', node.span); + } + bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); + bool verboseLogging = false; + + for (final entry in node.nodes.entries) { + if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { + if (entry.value case YamlScalar(value: bool value)) { + usePrecompiledBinaries = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "use_precompiled_binaries". Must be a boolean.', + entry.value.span); + } else if (entry.key case YamlScalar(value: 'verbose_logging')) { + if (entry.value case YamlScalar(value: bool value)) { + verboseLogging = value; + continue; + } + throw SourceSpanException( + 'Invalid value for "verbose_logging". Must be a boolean.', + entry.value.span); + } else { + throw SourceSpanException( + 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', + entry.key.span); + } + } + return CargokitUserOptions( + usePrecompiledBinaries: usePrecompiledBinaries, + verboseLogging: verboseLogging, + ); + } + + static CargokitUserOptions load() { + String fileName = "cargokit_options.yaml"; + var userProjectDir = Directory(Environment.rootProjectDir); + + while (userProjectDir.parent.path != userProjectDir.path) { + final configFile = File(path.join(userProjectDir.path, fileName)); + if (configFile.existsSync()) { + final contents = loadYamlNode( + configFile.readAsStringSync(), + sourceUrl: configFile.uri, + ); + final res = parse(contents); + if (res.verboseLogging) { + _log.info('Found user options file at ${configFile.path}'); + } + return res; + } + userProjectDir = userProjectDir.parent; + } + return CargokitUserOptions._(); + } + + final bool usePrecompiledBinaries; + final bool verboseLogging; +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/precompile_binaries.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/precompile_binaries.dart new file mode 100644 index 00000000..c27f4195 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/precompile_binaries.dart @@ -0,0 +1,202 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:github/github.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'artifacts_provider.dart'; +import 'builder.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'rustup.dart'; +import 'target.dart'; + +final _log = Logger('precompile_binaries'); + +class PrecompileBinaries { + PrecompileBinaries({ + required this.privateKey, + required this.githubToken, + required this.repositorySlug, + required this.manifestDir, + required this.targets, + this.androidSdkLocation, + this.androidNdkVersion, + this.androidMinSdkVersion, + this.tempDir, + }); + + final PrivateKey privateKey; + final String githubToken; + final RepositorySlug repositorySlug; + final String manifestDir; + final List targets; + final String? androidSdkLocation; + final String? androidNdkVersion; + final int? androidMinSdkVersion; + final String? tempDir; + + static String fileName(Target target, String name) { + return '${target.rust}_$name'; + } + + static String signatureFileName(Target target, String name) { + return '${target.rust}_$name.sig'; + } + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final targets = List.of(this.targets); + if (targets.isEmpty) { + targets.addAll([ + ...Target.buildableTargets(), + if (androidSdkLocation != null) ...Target.androidTargets(), + ]); + } + + _log.info('Precompiling binaries for $targets'); + + final hash = CrateHash.compute(manifestDir); + _log.info('Computed crate hash: $hash'); + + final String tagName = 'precompiled_$hash'; + + final github = GitHub(auth: Authentication.withToken(githubToken)); + final repo = github.repositories; + final release = await _getOrCreateRelease( + repo: repo, + tagName: tagName, + packageName: crateInfo.packageName, + hash: hash, + ); + + final tempDir = this.tempDir != null + ? Directory(this.tempDir!) + : Directory.systemTemp.createTempSync('precompiled_'); + + tempDir.createSync(recursive: true); + + final crateOptions = CargokitCrateOptions.load( + manifestDir: manifestDir, + ); + + final buildEnvironment = BuildEnvironment( + configuration: BuildConfiguration.release, + crateOptions: crateOptions, + targetTempDir: tempDir.path, + manifestDir: manifestDir, + crateInfo: crateInfo, + isAndroid: androidSdkLocation != null, + androidSdkPath: androidSdkLocation, + androidNdkVersion: androidNdkVersion, + androidMinSdkVersion: androidMinSdkVersion, + ); + + final rustup = Rustup(); + + for (final target in targets) { + final artifactNames = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + if (artifactNames.every((name) { + final fileName = PrecompileBinaries.fileName(target, name); + return (release.assets ?? []).any((e) => e.name == fileName); + })) { + _log.info("All artifacts for $target already exist - skipping"); + continue; + } + + _log.info('Building for $target'); + + final builder = + RustBuilder(target: target, environment: buildEnvironment); + builder.prepare(rustup); + final res = await builder.build(); + + final assets = []; + for (final name in artifactNames) { + final file = File(path.join(res, name)); + if (!file.existsSync()) { + throw Exception('Missing artifact: ${file.path}'); + } + + final data = file.readAsBytesSync(); + final create = CreateReleaseAsset( + name: PrecompileBinaries.fileName(target, name), + contentType: "application/octet-stream", + assetData: data, + ); + final signature = sign(privateKey, data); + final signatureCreate = CreateReleaseAsset( + name: signatureFileName(target, name), + contentType: "application/octet-stream", + assetData: signature, + ); + bool verified = verify(public(privateKey), data, signature); + if (!verified) { + throw Exception('Signature verification failed'); + } + assets.add(create); + assets.add(signatureCreate); + } + _log.info('Uploading assets: ${assets.map((e) => e.name)}'); + for (final asset in assets) { + // This seems to be failing on CI so do it one by one + int retryCount = 0; + while (true) { + try { + await repo.uploadReleaseAssets(release, [asset]); + break; + } on Exception catch (e) { + if (retryCount == 10) { + rethrow; + } + ++retryCount; + _log.shout( + 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); + await Future.delayed(Duration(seconds: 2)); + } + } + } + } + + _log.info('Cleaning up'); + tempDir.deleteSync(recursive: true); + } + + Future _getOrCreateRelease({ + required RepositoriesService repo, + required String tagName, + required String packageName, + required String hash, + }) async { + Release release; + try { + _log.info('Fetching release $tagName'); + release = await repo.getReleaseByTagName(repositorySlug, tagName); + } on ReleaseNotFound { + _log.info('Release not found - creating release $tagName'); + release = await repo.createRelease( + repositorySlug, + CreateRelease.from( + tagName: tagName, + name: 'Precompiled binaries ${hash.substring(0, 8)}', + targetCommitish: null, + isDraft: false, + isPrerelease: false, + body: 'Precompiled binaries for crate $packageName, ' + 'crate hash $hash.', + )); + } + return release; + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/rustup.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/rustup.dart new file mode 100644 index 00000000..0ac8d086 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/rustup.dart @@ -0,0 +1,136 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as path; + +import 'util.dart'; + +class _Toolchain { + _Toolchain( + this.name, + this.targets, + ); + + final String name; + final List targets; +} + +class Rustup { + List? installedTargets(String toolchain) { + final targets = _installedTargets(toolchain); + return targets != null ? List.unmodifiable(targets) : null; + } + + void installToolchain(String toolchain) { + log.info("Installing Rust toolchain: $toolchain"); + runCommand("rustup", ['toolchain', 'install', toolchain]); + _installedToolchains + .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); + } + + void installTarget( + String target, { + required String toolchain, + }) { + log.info("Installing Rust target: $target"); + runCommand("rustup", [ + 'target', + 'add', + '--toolchain', + toolchain, + target, + ]); + _installedTargets(toolchain)?.add(target); + } + + final List<_Toolchain> _installedToolchains; + + Rustup() : _installedToolchains = _getInstalledToolchains(); + + List? _installedTargets(String toolchain) => _installedToolchains + .firstWhereOrNull( + (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) + ?.targets; + + static List<_Toolchain> _getInstalledToolchains() { + String extractToolchainName(String line) { + // ignore (default) after toolchain name + final parts = line.split(' '); + return parts[0]; + } + + final res = runCommand("rustup", ['toolchain', 'list']); + + // To list all non-custom toolchains, we need to filter out lines that + // don't start with "stable", "beta", or "nightly". + Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) + .map(extractToolchainName) + .toList(growable: true); + + return lines + .map( + (name) => _Toolchain( + name, + _getInstalledTargets(name), + ), + ) + .toList(growable: true); + } + + static List _getInstalledTargets(String toolchain) { + final res = runCommand("rustup", [ + 'target', + 'list', + '--toolchain', + toolchain, + '--installed', + ]); + final lines = res.stdout + .toString() + .split('\n') + .where((e) => e.isNotEmpty) + .toList(growable: true); + return lines; + } + + bool _didInstallRustSrcForNightly = false; + + void installRustSrcForNightly() { + if (_didInstallRustSrcForNightly) { + return; + } + // Useful for -Z build-std + runCommand( + "rustup", + ['component', 'add', 'rust-src', '--toolchain', 'nightly'], + ); + _didInstallRustSrcForNightly = true; + } + + static String? executablePath() { + final envPath = Platform.environment['PATH']; + final envPathSeparator = Platform.isWindows ? ';' : ':'; + final home = Platform.isWindows + ? Platform.environment['USERPROFILE'] + : Platform.environment['HOME']; + final paths = [ + if (home != null) path.join(home, '.cargo', 'bin'), + if (envPath != null) ...envPath.split(envPathSeparator), + ]; + for (final p in paths) { + final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; + final rustupPath = path.join(p, rustup); + if (File(rustupPath).existsSync()) { + return rustupPath; + } + } + return null; + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/target.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/target.dart new file mode 100644 index 00000000..6fbc58b6 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/target.dart @@ -0,0 +1,140 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import 'util.dart'; + +class Target { + Target({ + required this.rust, + this.flutter, + this.android, + this.androidMinSdkVersion, + this.darwinPlatform, + this.darwinArch, + }); + + static final all = [ + Target( + rust: 'armv7-linux-androideabi', + flutter: 'android-arm', + android: 'armeabi-v7a', + androidMinSdkVersion: 16, + ), + Target( + rust: 'aarch64-linux-android', + flutter: 'android-arm64', + android: 'arm64-v8a', + androidMinSdkVersion: 21, + ), + Target( + rust: 'i686-linux-android', + flutter: 'android-x86', + android: 'x86', + androidMinSdkVersion: 16, + ), + Target( + rust: 'x86_64-linux-android', + flutter: 'android-x64', + android: 'x86_64', + androidMinSdkVersion: 21, + ), + Target( + rust: 'x86_64-pc-windows-msvc', + flutter: 'windows-x64', + ), + Target( + rust: 'x86_64-unknown-linux-gnu', + flutter: 'linux-x64', + ), + Target( + rust: 'aarch64-unknown-linux-gnu', + flutter: 'linux-arm64', + ), + Target( + rust: 'x86_64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'x86_64', + ), + Target( + rust: 'aarch64-apple-darwin', + darwinPlatform: 'macosx', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios', + darwinPlatform: 'iphoneos', + darwinArch: 'arm64', + ), + Target( + rust: 'aarch64-apple-ios-sim', + darwinPlatform: 'iphonesimulator', + darwinArch: 'arm64', + ), + Target( + rust: 'x86_64-apple-ios', + darwinPlatform: 'iphonesimulator', + darwinArch: 'x86_64', + ), + ]; + + static Target? forFlutterName(String flutterName) { + return all.firstWhereOrNull((element) => element.flutter == flutterName); + } + + static Target? forDarwin({ + required String platformName, + required String darwinAarch, + }) { + return all.firstWhereOrNull((element) => // + element.darwinPlatform == platformName && + element.darwinArch == darwinAarch); + } + + static Target? forRustTriple(String triple) { + return all.firstWhereOrNull((element) => element.rust == triple); + } + + static List androidTargets() { + return all + .where((element) => element.android != null) + .toList(growable: false); + } + + /// Returns buildable targets on current host platform ignoring Android targets. + static List buildableTargets() { + if (Platform.isLinux) { + // Right now we don't support cross-compiling on Linux. So we just return + // the host target. + final arch = runCommand('arch', []).stdout as String; + if (arch.trim() == 'aarch64') { + return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; + } else { + return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; + } + } + return all.where((target) { + if (Platform.isWindows) { + return target.rust.contains('-windows-'); + } else if (Platform.isMacOS) { + return target.darwinPlatform != null; + } + return false; + }).toList(growable: false); + } + + @override + String toString() { + return rust; + } + + final String? flutter; + final String rust; + final String? android; + final int? androidMinSdkVersion; + final String? darwinPlatform; + final String? darwinArch; +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/util.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/util.dart new file mode 100644 index 00000000..8bb6a872 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/util.dart @@ -0,0 +1,172 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import 'logging.dart'; +import 'rustup.dart'; + +final log = Logger("process"); + +class CommandFailedException implements Exception { + final String executable; + final List arguments; + final ProcessResult result; + + CommandFailedException({ + required this.executable, + required this.arguments, + required this.result, + }); + + @override + String toString() { + final stdout = result.stdout.toString().trim(); + final stderr = result.stderr.toString().trim(); + return [ + "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", + "Returned Exit Code: ${result.exitCode}", + kSeparator, + "STDOUT:", + if (stdout.isNotEmpty) stdout, + kSeparator, + "STDERR:", + if (stderr.isNotEmpty) stderr, + ].join('\n'); + } +} + +class TestRunCommandArgs { + final String executable; + final List arguments; + final String? workingDirectory; + final Map? environment; + final bool includeParentEnvironment; + final bool runInShell; + final Encoding? stdoutEncoding; + final Encoding? stderrEncoding; + + TestRunCommandArgs({ + required this.executable, + required this.arguments, + this.workingDirectory, + this.environment, + this.includeParentEnvironment = true, + this.runInShell = false, + this.stdoutEncoding, + this.stderrEncoding, + }); +} + +class TestRunCommandResult { + TestRunCommandResult({ + this.pid = 1, + this.exitCode = 0, + this.stdout = '', + this.stderr = '', + }); + + final int pid; + final int exitCode; + final String stdout; + final String stderr; +} + +TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; + +ProcessResult runCommand( + String executable, + List arguments, { + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool runInShell = false, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, +}) { + if (testRunCommandOverride != null) { + final result = testRunCommandOverride!(TestRunCommandArgs( + executable: executable, + arguments: arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + )); + return ProcessResult( + result.pid, + result.exitCode, + result.stdout, + result.stderr, + ); + } + log.finer('Running command $executable ${arguments.join(' ')}'); + final res = Process.runSync( + _resolveExecutable(executable), + arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stderrEncoding: stderrEncoding, + stdoutEncoding: stdoutEncoding, + ); + if (res.exitCode != 0) { + throw CommandFailedException( + executable: executable, + arguments: arguments, + result: res, + ); + } else { + return res; + } +} + +class RustupNotFoundException implements Exception { + @override + String toString() { + return [ + ' ', + 'rustup not found in PATH.', + ' ', + 'Maybe you need to install Rust? It only takes a minute:', + ' ', + if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', + if (hasHomebrewRustInPath()) ...[ + '\$ brew unlink rust # Unlink homebrew Rust from PATH', + ], + if (!Platform.isWindows) + "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", + ' ', + ].join('\n'); + } + + static bool hasHomebrewRustInPath() { + if (!Platform.isMacOS) { + return false; + } + final envPath = Platform.environment['PATH'] ?? ''; + final paths = envPath.split(':'); + return paths.any((p) { + return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); + }); + } +} + +String _resolveExecutable(String executable) { + if (executable == 'rustup') { + final resolved = Rustup.executablePath(); + if (resolved != null) { + return resolved; + } + throw RustupNotFoundException(); + } else { + return executable; + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/lib/src/verify_binaries.dart b/third_party/convex_flutter/cargokit/build_tool/lib/src/verify_binaries.dart new file mode 100644 index 00000000..2366b57b --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/lib/src/verify_binaries.dart @@ -0,0 +1,84 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import 'dart:io'; + +import 'package:ed25519_edwards/ed25519_edwards.dart'; +import 'package:http/http.dart'; + +import 'artifacts_provider.dart'; +import 'cargo.dart'; +import 'crate_hash.dart'; +import 'options.dart'; +import 'precompile_binaries.dart'; +import 'target.dart'; + +class VerifyBinaries { + VerifyBinaries({ + required this.manifestDir, + }); + + final String manifestDir; + + Future run() async { + final crateInfo = CrateInfo.load(manifestDir); + + final config = CargokitCrateOptions.load(manifestDir: manifestDir); + final precompiledBinaries = config.precompiledBinaries; + if (precompiledBinaries == null) { + stdout.writeln('Crate does not support precompiled binaries.'); + } else { + final crateHash = CrateHash.compute(manifestDir); + stdout.writeln('Crate hash: $crateHash'); + + for (final target in Target.all) { + final message = 'Checking ${target.rust}...'; + stdout.write(message.padRight(40)); + stdout.flush(); + + final artifacts = getArtifactNames( + target: target, + libraryName: crateInfo.packageName, + remote: true, + ); + + final prefix = precompiledBinaries.uriPrefix; + + bool ok = true; + + for (final artifact in artifacts) { + final fileName = PrecompileBinaries.fileName(target, artifact); + final signatureFileName = + PrecompileBinaries.signatureFileName(target, artifact); + + final url = Uri.parse('$prefix$crateHash/$fileName'); + final signatureUrl = + Uri.parse('$prefix$crateHash/$signatureFileName'); + + final signature = await get(signatureUrl); + if (signature.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + final asset = await get(url); + if (asset.statusCode != 200) { + stdout.writeln('MISSING'); + ok = false; + break; + } + + if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, + signature.bodyBytes)) { + stdout.writeln('INVALID SIGNATURE'); + ok = false; + } + } + + if (ok) { + stdout.writeln('OK'); + } + } + } + } +} diff --git a/third_party/convex_flutter/cargokit/build_tool/pubspec.lock b/third_party/convex_flutter/cargokit/build_tool/pubspec.lock new file mode 100644 index 00000000..343bdd36 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/pubspec.lock @@ -0,0 +1,453 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + args: + dependency: "direct main" + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: "direct main" + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + ed25519_edwards: + dependency: "direct main" + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + github: + dependency: "direct main" + description: + name: github + sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" + url: "https://pub.dev" + source: hosted + version: "9.17.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + hex: + dependency: "direct main" + description: + name: hex + sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + http: + dependency: "direct main" + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: "direct main" + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" + url: "https://pub.dev" + source: hosted + version: "1.24.6" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + toml: + dependency: "direct main" + description: + name: toml + sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + version: + dependency: "direct main" + description: + name: version + sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + url: "https://pub.dev" + source: hosted + version: "11.9.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.0.0 <4.0.0" diff --git a/third_party/convex_flutter/cargokit/build_tool/pubspec.yaml b/third_party/convex_flutter/cargokit/build_tool/pubspec.yaml new file mode 100644 index 00000000..18c61e33 --- /dev/null +++ b/third_party/convex_flutter/cargokit/build_tool/pubspec.yaml @@ -0,0 +1,33 @@ +# This is copied from Cargokit (which is the official way to use it currently) +# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +name: build_tool +description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. +publish_to: none +version: 1.0.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + +# Add regular dependencies here. +dependencies: + # these are pinned on purpose because the bundle_tool_runner doesn't have + # pubspec.lock. See run_build_tool.sh + logging: 1.2.0 + path: 1.8.0 + version: 3.0.0 + collection: 1.18.0 + ed25519_edwards: 0.3.1 + hex: 0.2.0 + yaml: 3.1.2 + source_span: 1.10.0 + github: 9.17.0 + args: 2.4.2 + crypto: 3.0.3 + convert: 3.1.1 + http: 1.1.0 + toml: 0.14.0 + +dev_dependencies: + lints: ^2.1.0 + test: ^1.24.0 diff --git a/third_party/convex_flutter/cargokit/cmake/cargokit.cmake b/third_party/convex_flutter/cargokit/cmake/cargokit.cmake new file mode 100644 index 00000000..ddd05df9 --- /dev/null +++ b/third_party/convex_flutter/cargokit/cmake/cargokit.cmake @@ -0,0 +1,99 @@ +SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") + +# Workaround for https://github.com/dart-lang/pub/issues/4010 +get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) + +if(WIN32) + # REALPATH does not properly resolve symlinks on windows :-/ + execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) +endif() + +# Arguments +# - target: CMAKE target to which rust library is linked +# - manifest_dir: relative path from current folder to directory containing cargo manifest +# - lib_name: cargo package name +# - any_symbol_name: name of any exported symbol from the library. +# used on windows to force linking with library. +function(apply_cargokit target manifest_dir lib_name any_symbol_name) + + set(CARGOKIT_LIB_NAME "${lib_name}") + set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") + if (CMAKE_CONFIGURATION_TYPES) + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") + else() + set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") + set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") + endif() + set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") + + if (FLUTTER_TARGET_PLATFORM) + set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") + else() + set(CARGOKIT_TARGET_PLATFORM "windows-x64") + endif() + + set(CARGOKIT_ENV + "CARGOKIT_CMAKE=${CMAKE_COMMAND}" + "CARGOKIT_CONFIGURATION=$" + "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" + "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" + "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" + "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" + "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" + "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" + ) + + if (WIN32) + set(SCRIPT_EXTENSION ".cmd") + set(IMPORT_LIB_EXTENSION ".lib") + else() + set(SCRIPT_EXTENSION ".sh") + set(IMPORT_LIB_EXTENSION "") + execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") + endif() + + # Using generators in custom command is only supported in CMake 3.20+ + if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") + foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) + add_custom_command( + OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endforeach() + else() + add_custom_command( + OUTPUT + ${OUTPUT_LIB} + "${CMAKE_CURRENT_BINARY_DIR}/_phony_" + COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} + "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake + VERBATIM + ) + endif() + + + set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) + + if (TARGET ${target}) + # If we have actual cmake target provided create target and make existing + # target depend on it + add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) + add_dependencies("${target}" "${target}_cargokit") + target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") + if(WIN32) + target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") + endif() + else() + # Otherwise (FFI) just use ALL to force building always + add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) + endif() + + # Allow adding the output library to plugin bundled libraries + set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) + +endfunction() diff --git a/third_party/convex_flutter/cargokit/cmake/resolve_symlinks.ps1 b/third_party/convex_flutter/cargokit/cmake/resolve_symlinks.ps1 new file mode 100644 index 00000000..2ac593a1 --- /dev/null +++ b/third_party/convex_flutter/cargokit/cmake/resolve_symlinks.ps1 @@ -0,0 +1,34 @@ +function Resolve-Symlinks { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string] $Path + ) + + [string] $separator = '/' + [string[]] $parts = $Path.Split($separator) + + [string] $realPath = '' + foreach ($part in $parts) { + if ($realPath -and !$realPath.EndsWith($separator)) { + $realPath += $separator + } + + $realPath += $part.Replace('\', '/') + + # The slash is important when using Get-Item on Drive letters in pwsh. + if (-not($realPath.Contains($separator)) -and $realPath.EndsWith(':')) { + $realPath += '/' + } + + $item = Get-Item $realPath + if ($item.LinkTarget) { + $realPath = $item.LinkTarget.Replace('\', '/') + } + } + $realPath +} + +$path = Resolve-Symlinks -Path $args[0] +Write-Host $path diff --git a/third_party/convex_flutter/cargokit/gradle/plugin.gradle b/third_party/convex_flutter/cargokit/gradle/plugin.gradle new file mode 100644 index 00000000..4af35ee0 --- /dev/null +++ b/third_party/convex_flutter/cargokit/gradle/plugin.gradle @@ -0,0 +1,179 @@ +/// This is copied from Cargokit (which is the official way to use it currently) +/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin + +import java.nio.file.Paths +import org.apache.tools.ant.taskdefs.condition.Os + +CargoKitPlugin.file = buildscript.sourceFile + +apply plugin: CargoKitPlugin + +class CargoKitExtension { + String manifestDir; // Relative path to folder containing Cargo.toml + String libname; // Library name within Cargo.toml. Must be a cdylib +} + +abstract class CargoKitBuildTask extends DefaultTask { + + @Input + String buildMode + + @Input + String buildDir + + @Input + String outputDir + + @Input + String ndkVersion + + @Input + String sdkDirectory + + @Input + int compileSdkVersion; + + @Input + int minSdkVersion; + + @Input + String pluginFile + + @Input + List targetPlatforms + + @TaskAction + def build() { + if (project.cargokit.manifestDir == null) { + throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); + } + + if (project.cargokit.libname == null) { + throw new GradleException("Property 'libname' must be set on cargokit extension"); + } + + def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" + def path = Paths.get(new File(pluginFile).parent, "..", executableName); + + def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) + + def rootProjectDir = project.rootProject.projectDir + + if (!Os.isFamily(Os.FAMILY_WINDOWS)) { + project.exec { + commandLine 'chmod', '+x', path + } + } + + project.exec { + executable path + args "build-gradle" + environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir + environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" + environment "CARGOKIT_MANIFEST_DIR", manifestDir + environment "CARGOKIT_CONFIGURATION", buildMode + environment "CARGOKIT_TARGET_TEMP_DIR", buildDir + environment "CARGOKIT_OUTPUT_DIR", outputDir + environment "CARGOKIT_NDK_VERSION", ndkVersion + environment "CARGOKIT_SDK_DIR", sdkDirectory + environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion + environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion + environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") + environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] + } + } +} + +class CargoKitPlugin implements Plugin { + + static String file; + + private Plugin findFlutterPlugin(Project rootProject) { + _findFlutterPlugin(rootProject.childProjects) + } + + private Plugin _findFlutterPlugin(Map projects) { + for (project in projects) { + for (plugin in project.value.getPlugins()) { + if (plugin.class.name == "com.flutter.gradle.FlutterPlugin") { + return plugin; + } + } + def plugin = _findFlutterPlugin(project.value.childProjects); + if (plugin != null) { + return plugin; + } + } + return null; + } + + @Override + void apply(Project project) { + def plugin = findFlutterPlugin(project.rootProject); + + project.extensions.create("cargokit", CargoKitExtension) + + if (plugin == null) { + print("Flutter plugin not found, CargoKit plugin will not be applied.") + return; + } + + def cargoBuildDir = "${project.buildDir}/build" + + // Determine if the project is an application or library + def isApplication = plugin.project.plugins.hasPlugin('com.android.application') + def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants + + variants.all { variant -> + + final buildType = variant.buildType.name + + def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; + def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; + jniLibs.srcDir(new File(cargoOutputDir)) + + def platforms = com.flutter.gradle.FlutterPluginUtils.getTargetPlatforms(project).collect() + + // Same thing addFlutterDependencies does in flutter.gradle + if (buildType == "debug") { + platforms.add("android-x86") + platforms.add("android-x64") + } + + // The task name depends on plugin properties, which are not available + // at this point + project.getGradle().afterProject { + def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; + + if (project.tasks.findByName(taskName)) { + return + } + + if (plugin.project.android.ndkVersion == null) { + throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") + } + + def task = project.tasks.create(taskName, CargoKitBuildTask.class) { + buildMode = variant.buildType.name + buildDir = cargoBuildDir + outputDir = cargoOutputDir + ndkVersion = plugin.project.android.ndkVersion + sdkDirectory = plugin.project.android.sdkDirectory + minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int + compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int + targetPlatforms = platforms + pluginFile = CargoKitPlugin.file + } + def onTask = { newTask -> + if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { + newTask.dependsOn task + // Fix gradle 7.4.2 not picking up JNI library changes + newTask.outputs.upToDateWhen { false } + } + } + project.tasks.each onTask + project.tasks.whenTaskAdded onTask + } + } + } +} diff --git a/third_party/convex_flutter/cargokit/run_build_tool.cmd b/third_party/convex_flutter/cargokit/run_build_tool.cmd new file mode 100755 index 00000000..c45d0aa8 --- /dev/null +++ b/third_party/convex_flutter/cargokit/run_build_tool.cmd @@ -0,0 +1,91 @@ +@echo off +setlocal + +setlocal ENABLEDELAYEDEXPANSION + +SET BASEDIR=%~dp0 + +if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( + mkdir "%CARGOKIT_TOOL_TEMP_DIR%" +) +cd /D "%CARGOKIT_TOOL_TEMP_DIR%" + +SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool +SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart + +set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% + +( + echo name: build_tool_runner + echo version: 1.0.0 + echo publish_to: none + echo. + echo environment: + echo sdk: '^>=3.0.0 ^<4.0.0' + echo. + echo dependencies: + echo build_tool: + echo path: %BUILD_TOOL_PKG_DIR_POSIX% +) >pubspec.yaml + +if not exist bin ( + mkdir bin +) + +( + echo import 'package:build_tool/build_tool.dart' as build_tool; + echo void main^(List^ args^) ^{ + echo build_tool.runMain^(args^); + echo ^} +) >bin\build_tool_runner.dart + +SET PRECOMPILED=bin\build_tool_runner.dill + +REM To detect changes in package we compare output of DIR /s (recursive) +set PREV_PACKAGE_INFO=.dart_tool\package_info.prev +set CUR_PACKAGE_INFO=.dart_tool\package_info.cur + +DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" + +REM Last line in dir output is free space on harddrive. That is bound to +REM change between invocation so we need to remove it +( + Set "Line=" + For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( + SetLocal EnableDelayedExpansion + If Defined Line Echo !Line! + EndLocal + Set "Line=%%A") +) >"%CUR_PACKAGE_INFO%" +DEL "%CUR_PACKAGE_INFO%_orig" + +REM Compare current directory listing with previous +FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 + +If %ERRORLEVEL% neq 0 ( + REM Changed - copy current to previous and remove precompiled kernel + if exist "%PREV_PACKAGE_INFO%" ( + DEL "%PREV_PACKAGE_INFO%" + ) + MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" + if exist "%PRECOMPILED%" ( + DEL "%PRECOMPILED%" + ) +) + +REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% +REM which means we need to do pub get and precompile +if not exist "%PRECOMPILED%" ( + echo Running pub get in "%cd%" + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart +) + +"%DART%" "%PRECOMPILED%" %* + +REM 253 means invalid snapshot version. +If %ERRORLEVEL% equ 253 ( + "%DART%" pub get --no-precompile + "%DART%" compile kernel bin/build_tool_runner.dart + "%DART%" "%PRECOMPILED%" %* +) diff --git a/third_party/convex_flutter/cargokit/run_build_tool.sh b/third_party/convex_flutter/cargokit/run_build_tool.sh new file mode 100755 index 00000000..24b0ed89 --- /dev/null +++ b/third_party/convex_flutter/cargokit/run_build_tool.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -e + +BASEDIR=$(dirname "$0") + +mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" + +cd "$CARGOKIT_TOOL_TEMP_DIR" + +# Write a very simple bin package in temp folder that depends on build_tool package +# from Cargokit. This is done to ensure that we don't pollute Cargokit folder +# with .dart_tool contents. + +BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" + +if [[ -z $FLUTTER_ROOT ]]; then # not defined + DART=dart +else + DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" +fi + +cat << EOF > "pubspec.yaml" +name: build_tool_runner +version: 1.0.0 +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + build_tool: + path: "$BUILD_TOOL_PKG_DIR" +EOF + +mkdir -p "bin" + +cat << EOF > "bin/build_tool_runner.dart" +import 'package:build_tool/build_tool.dart' as build_tool; +void main(List args) { + build_tool.runMain(args); +} +EOF + +# Create alias for `shasum` if it does not exist and `sha1sum` exists +if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then + shopt -s expand_aliases + alias shasum="sha1sum" +fi + +# Dart run will not cache any package that has a path dependency, which +# is the case for our build_tool_runner. So instead we precompile the package +# ourselves. +# To invalidate the cached kernel we use the hash of ls -LR of the build_tool +# package directory. This should be good enough, as the build_tool package +# itself is not meant to have any path dependencies. + +if [[ "$OSTYPE" == "darwin"* ]]; then + PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) +else + PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) +fi + +PACKAGE_HASH_FILE=".package_hash" + +if [ -f "$PACKAGE_HASH_FILE" ]; then + EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") + if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then + rm "$PACKAGE_HASH_FILE" + fi +fi + +# Run pub get if needed. +if [ ! -f "$PACKAGE_HASH_FILE" ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" +fi + +# Rebuild the tool if it was deleted by Android Studio +if [ ! -f "bin/build_tool_runner.dill" ]; then + "$DART" compile kernel bin/build_tool_runner.dart +fi + +set +e + +"$DART" bin/build_tool_runner.dill "$@" + +exit_code=$? + +# 253 means invalid snapshot version. +if [ $exit_code == 253 ]; then + "$DART" pub get --no-precompile + "$DART" compile kernel bin/build_tool_runner.dart + "$DART" bin/build_tool_runner.dill "$@" + exit_code=$? +fi + +exit $exit_code diff --git a/third_party/convex_flutter/example/HEALTH_CHECK.md b/third_party/convex_flutter/example/HEALTH_CHECK.md new file mode 100644 index 00000000..47296f78 --- /dev/null +++ b/third_party/convex_flutter/example/HEALTH_CHECK.md @@ -0,0 +1,87 @@ +# Health Check Query Setup (Optional) + +To use the health check functionality in this example app, you can optionally create a simple health check query in your Convex backend. This is **recommended but not required** - you can use any existing query instead. + +## Why Create a Dedicated Health Check? + +A dedicated health check query provides: +- **Lightweight**: No database queries or complex logic +- **Fast**: Minimal processing time +- **Clear Purpose**: Obviously for health checks +- **No Side Effects**: Doesn't modify any data +- **Best Practice**: Follows Convex and REST API conventions + +However, if you prefer, you can use any existing lightweight query (like `users:count` or `messages:list`) instead. + +## Creating the Health Check Query + +Create a file `convex/health.ts` in your Convex backend with the following content: + +```typescript +// convex/health.ts +import { query } from "./_generated/server"; + +export const ping = query({ + args: {}, + handler: async () => { + return "ok"; + }, +}); +``` + +This creates a lightweight query endpoint at `health:ping` that: +- Takes no arguments +- Returns a simple "ok" response +- Can be used for connection health checks +- Has minimal overhead + +## Using the Health Check + +The example app uses this query in two ways: + +### 1. Automatic Connection on Startup + +The HomeScreen triggers the health check automatically when the app starts: + +```dart +await ConvexClient.instance.query('health:ping', {}); +``` + +This establishes the WebSocket connection immediately, allowing the connection state to transition from "connecting" to "connected". + +### 2. Manual Connection Checks (Deprecated) + +The deprecated `checkConnection()` method uses the configured `healthCheckQuery`: + +```dart +await ConvexClient.initialize( + ConvexConfig( + healthCheckQuery: "health:ping", + ), +); + +final status = await ConvexClient.instance.checkConnection(); +``` + +## Why Use a Dedicated Health Check Query? + +1. **Lightweight**: No database queries or complex logic +2. **Fast**: Minimal processing time +3. **Idempotent**: Safe to call repeatedly +4. **No Side Effects**: Doesn't modify any data +5. **Clear Purpose**: Obvious what it's for + +## Alternative + +If you don't want to create a dedicated health check query, you can use any existing lightweight query from your backend: + +```dart +// Use any existing query +await ConvexClient.initialize( + ConvexConfig( + healthCheckQuery: "users:count", // Any lightweight query + ), +); +``` + +However, a dedicated health check endpoint is the recommended best practice. diff --git a/third_party/convex_flutter/example/README.md b/third_party/convex_flutter/example/README.md new file mode 100644 index 00000000..065bb074 --- /dev/null +++ b/third_party/convex_flutter/example/README.md @@ -0,0 +1,20 @@ +# convex_flutter_example + +Demonstrates how to use the convex_flutter plugin. + +## Usage Example + +Here's an example of how to send a message using a Convex mutation: + +```dart +await ConvexClient.instance.mutation( + name: "messages:send", + args: {"body": message, "author": "Singh"}, +); +``` + +Here's an example of how to query the backend: + +```dart +final result = await client.query('your_query'); +``` diff --git a/third_party/convex_flutter/example/analysis_options.yaml b/third_party/convex_flutter/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/third_party/convex_flutter/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/third_party/convex_flutter/example/android/app/build.gradle.kts b/third_party/convex_flutter/example/android/app/build.gradle.kts new file mode 100644 index 00000000..24f02a12 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.convex_flutter_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.convex_flutter_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/third_party/convex_flutter/example/android/app/src/debug/AndroidManifest.xml b/third_party/convex_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/third_party/convex_flutter/example/android/app/src/main/AndroidManifest.xml b/third_party/convex_flutter/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0be63e96 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/convex_flutter/example/android/app/src/main/kotlin/com/example/convex_flutter_example/MainActivity.kt b/third_party/convex_flutter/example/android/app/src/main/kotlin/com/example/convex_flutter_example/MainActivity.kt new file mode 100644 index 00000000..fe5ed24f --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/main/kotlin/com/example/convex_flutter_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.convex_flutter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/third_party/convex_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml b/third_party/convex_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/third_party/convex_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/third_party/convex_flutter/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/third_party/convex_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/third_party/convex_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/third_party/convex_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/third_party/convex_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/third_party/convex_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/android/app/src/main/res/values-night/styles.xml b/third_party/convex_flutter/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/third_party/convex_flutter/example/android/app/src/main/res/values/styles.xml b/third_party/convex_flutter/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/third_party/convex_flutter/example/android/app/src/profile/AndroidManifest.xml b/third_party/convex_flutter/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/third_party/convex_flutter/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/third_party/convex_flutter/example/android/build.gradle.kts b/third_party/convex_flutter/example/android/build.gradle.kts new file mode 100644 index 00000000..89176ef4 --- /dev/null +++ b/third_party/convex_flutter/example/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/third_party/convex_flutter/example/android/gradle.properties b/third_party/convex_flutter/example/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/third_party/convex_flutter/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/third_party/convex_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/third_party/convex_flutter/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ac3b4792 --- /dev/null +++ b/third_party/convex_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/third_party/convex_flutter/example/android/settings.gradle.kts b/third_party/convex_flutter/example/android/settings.gradle.kts new file mode 100644 index 00000000..ab39a10a --- /dev/null +++ b/third_party/convex_flutter/example/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/third_party/convex_flutter/example/integration_test/simple_test.dart b/third_party/convex_flutter/example/integration_test/simple_test.dart new file mode 100644 index 00000000..f7e577bd --- /dev/null +++ b/third_party/convex_flutter/example/integration_test/simple_test.dart @@ -0,0 +1,11 @@ +import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:convex_flutter/convex_flutter.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() async => await RustLib.init()); + test('Can call rust function', () async { + // expect(greet(name: "Tom"), "Hello, Tom!"); + }); +} diff --git a/third_party/convex_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/third_party/convex_flutter/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..1dc6cf76 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/third_party/convex_flutter/example/ios/Flutter/Debug.xcconfig b/third_party/convex_flutter/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/third_party/convex_flutter/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/third_party/convex_flutter/example/ios/Flutter/Release.xcconfig b/third_party/convex_flutter/example/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/third_party/convex_flutter/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/third_party/convex_flutter/example/ios/Podfile b/third_party/convex_flutter/example/ios/Podfile new file mode 100644 index 00000000..620e46eb --- /dev/null +++ b/third_party/convex_flutter/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/third_party/convex_flutter/example/ios/Podfile.lock b/third_party/convex_flutter/example/ios/Podfile.lock new file mode 100644 index 00000000..9eaa971c --- /dev/null +++ b/third_party/convex_flutter/example/ios/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - convex_flutter (0.0.1): + - Flutter + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + +DEPENDENCIES: + - convex_flutter (from `.symlinks/plugins/convex_flutter/ios`) + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + +EXTERNAL SOURCES: + convex_flutter: + :path: ".symlinks/plugins/convex_flutter/ios" + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + +SPEC CHECKSUMS: + convex_flutter: 8581c72fdb31ffdbfef9908f23f21668035a032a + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..2142bb7f --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B87EC92B90E5D1D4236EB5F4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A4045BB261C3A094CFA44EC /* Pods_Runner.framework */; }; + EDD20FBEB619D5A514A1C611 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF8F3F6DDE398E0F903BED73 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 18C621CFA8A07C85C53E155A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 1A4045BB261C3A094CFA44EC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3E744F493A5CC5201BAB58EE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4FE5B1E9D09AB9B4D3603DDC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 8FFBD8D853998A8FDAFE9693 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB5BA5233F77CB829DB7164C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + DD93ACA1C1F41A19BD464B8A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + EF8F3F6DDE398E0F903BED73 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7BF3221F20726CC94DF5C3AC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EDD20FBEB619D5A514A1C611 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B87EC92B90E5D1D4236EB5F4 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 59DCBF4450BCBEF58AD02F1C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1A4045BB261C3A094CFA44EC /* Pods_Runner.framework */, + EF8F3F6DDE398E0F903BED73 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 9DC8167AFFCDEA8CCC7DD883 /* Pods */, + 59DCBF4450BCBEF58AD02F1C /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 9DC8167AFFCDEA8CCC7DD883 /* Pods */ = { + isa = PBXGroup; + children = ( + 3E744F493A5CC5201BAB58EE /* Pods-Runner.debug.xcconfig */, + 8FFBD8D853998A8FDAFE9693 /* Pods-Runner.release.xcconfig */, + 4FE5B1E9D09AB9B4D3603DDC /* Pods-Runner.profile.xcconfig */, + 18C621CFA8A07C85C53E155A /* Pods-RunnerTests.debug.xcconfig */, + AB5BA5233F77CB829DB7164C /* Pods-RunnerTests.release.xcconfig */, + DD93ACA1C1F41A19BD464B8A /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + F1C5B2B1C58C5433761D2FB8 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 7BF3221F20726CC94DF5C3AC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + FCB70B0446B240AB6783B5A6 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 79F53A30267DAF502589689D /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 79F53A30267DAF502589689D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + F1C5B2B1C58C5433761D2FB8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FCB70B0446B240AB6783B5A6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = A2N5J9H9QJ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 18C621CFA8A07C85C53E155A /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB5BA5233F77CB829DB7164C /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DD93ACA1C1F41A19BD464B8A /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = A2N5J9H9QJ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = A2N5J9H9QJ; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/third_party/convex_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/third_party/convex_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e3773d42 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/convex_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/third_party/convex_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/third_party/convex_flutter/example/ios/Runner/AppDelegate.swift b/third_party/convex_flutter/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..62666446 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/third_party/convex_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/third_party/convex_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/convex_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/third_party/convex_flutter/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/convex_flutter/example/ios/Runner/Info.plist b/third_party/convex_flutter/example/ios/Runner/Info.plist new file mode 100644 index 00000000..a8459efc --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Convex Flutter + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + convex_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/third_party/convex_flutter/example/ios/Runner/Runner-Bridging-Header.h b/third_party/convex_flutter/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/third_party/convex_flutter/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/third_party/convex_flutter/example/ios/RunnerTests/RunnerTests.swift b/third_party/convex_flutter/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/third_party/convex_flutter/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/third_party/convex_flutter/example/lib/main.dart b/third_party/convex_flutter/example/lib/main.dart new file mode 100644 index 00000000..ace9715b --- /dev/null +++ b/third_party/convex_flutter/example/lib/main.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:convex_flutter/convex_flutter.dart'; +import 'screens/home_screen.dart'; +import 'screens/authentication_screen.dart'; +import 'screens/messaging_screen.dart'; +import 'screens/connection_screen.dart'; +import 'screens/advanced_screen.dart'; +import 'widgets/connection_status_indicator.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await ConvexClient.initialize( + ConvexConfig( + deploymentUrl: "https://merry-grasshopper-563.convex.cloud", + clientId: "flutter-app-1.0", + operationTimeout: const Duration(seconds: 30), + healthCheckQuery: "health:ping", + ), + ); + + runApp(const ConvexExampleApp()); +} + +class ConvexExampleApp extends StatelessWidget { + const ConvexExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Convex Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const MainNavigationScreen(), + ); + } +} + +class MainNavigationScreen extends StatefulWidget { + const MainNavigationScreen({super.key}); + + @override + State createState() => _MainNavigationScreenState(); +} + +class _MainNavigationScreenState extends State { + int _selectedIndex = 0; + + final List _screens = const [ + HomeScreen(), + AuthenticationScreen(), + MessagingScreen(), + ConnectionScreen(), + AdvancedScreen(), + ]; + + final List _navItems = const [ + NavigationItem( + icon: Icons.home, + label: 'Home', + description: 'Welcome and overview', + ), + NavigationItem( + icon: Icons.login, + label: 'Authentication', + description: 'JWT tokens, auto-refresh, auth state', + ), + NavigationItem( + icon: Icons.message, + label: 'Messaging', + description: 'Query, mutation, subscribe, live updates', + ), + NavigationItem( + icon: Icons.wifi, + label: 'Connection', + description: 'WebSocket state, health checks, reconnect', + ), + NavigationItem( + icon: Icons.settings, + label: 'Advanced', + description: 'Timeouts, actions, error handling, lifecycle', + ), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_navItems[_selectedIndex].label), + actions: const [ + ConnectionStatusIndicator(), + ], + ), + drawer: _buildDrawer(), + body: _screens[_selectedIndex], + ); + } + + Widget _buildDrawer() { + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.primaryContainer, + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.cloud, color: Colors.white, size: 48), + const SizedBox(height: 8), + const Text( + 'Convex Flutter', + style: TextStyle(color: Colors.white, fontSize: 24, + fontWeight: FontWeight.bold), + ), + const Text( + 'Example App', + style: TextStyle(color: Colors.white70, fontSize: 16), + ), + const Spacer(), + // Auth state indicator in drawer + StreamBuilder( + stream: ConvexClient.instance.authState, + initialData: false, + builder: (context, snapshot) { + final isAuth = snapshot.data ?? false; + return Row( + children: [ + Icon( + isAuth ? Icons.verified_user : Icons.person_off, + color: Colors.white70, + size: 16, + ), + const SizedBox(width: 4), + Text( + isAuth ? 'Authenticated' : 'Not Authenticated', + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + ], + ); + }, + ), + ], + ), + ), + ...List.generate(_navItems.length, (index) { + final item = _navItems[index]; + return ListTile( + leading: Icon(item.icon), + title: Text(item.label), + subtitle: Text(item.description, style: const TextStyle(fontSize: 12)), + selected: _selectedIndex == index, + selectedTileColor: Theme.of(context).colorScheme.primaryContainer, + onTap: () { + setState(() => _selectedIndex = index); + Navigator.pop(context); + }, + ); + }), + const Divider(), + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('About'), + subtitle: const Text('Convex Flutter SDK v2.0.0', style: TextStyle(fontSize: 12)), + onTap: () { + Navigator.pop(context); + showAboutDialog( + context: context, + applicationName: 'Convex Flutter', + applicationVersion: '2.0.0', + applicationIcon: const Icon(Icons.cloud, size: 48), + children: const [ + Text('Comprehensive example app demonstrating all features of the Convex Flutter SDK.'), + SizedBox(height: 8), + Text('Features:\n' + '• Real-time subscriptions\n' + '• Authentication & token refresh\n' + '• WebSocket connection state\n' + '• Query, mutation, and action support\n' + '• Lifecycle management'), + ], + ); + }, + ), + ], + ), + ); + } +} + +class NavigationItem { + final IconData icon; + final String label; + final String description; + + const NavigationItem({ + required this.icon, + required this.label, + required this.description, + }); +} diff --git a/third_party/convex_flutter/example/lib/screens/advanced_screen.dart b/third_party/convex_flutter/example/lib/screens/advanced_screen.dart new file mode 100644 index 00000000..057ee594 --- /dev/null +++ b/third_party/convex_flutter/example/lib/screens/advanced_screen.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:convex_flutter/convex_flutter.dart'; + +class AdvancedScreen extends StatefulWidget { + const AdvancedScreen({super.key}); + + @override + State createState() => _AdvancedScreenState(); +} + +class _AdvancedScreenState extends State { + String? _timeoutResult; + bool _isTesting = false; + AppLifecycleEvent? _currentLifecycle; + final List _lifecycleHistory = []; + + @override + void initState() { + super.initState(); + ConvexClient.instance.lifecycleEvents.listen((event) { + setState(() { + _currentLifecycle = event; + _lifecycleHistory.insert(0, '${DateTime.now()}: ${event.name}'); + if (_lifecycleHistory.length > 10) _lifecycleHistory.removeLast(); + }); + }); + } + + Future _testTimeout(int seconds) async { + setState(() { + _isTesting = true; + _timeoutResult = null; + }); + + final stopwatch = Stopwatch()..start(); + try { + await ConvexClient.instance.query("messages:list", {}); + stopwatch.stop(); + setState(() { + _timeoutResult = 'Success in ${stopwatch.elapsedMilliseconds}ms'; + _isTesting = false; + }); + } on TimeoutException { + stopwatch.stop(); + setState(() { + _timeoutResult = 'Timeout after ${stopwatch.elapsedMilliseconds}ms'; + _isTesting = false; + }); + } catch (e) { + stopwatch.stop(); + setState(() { + _timeoutResult = 'Error: $e (${stopwatch.elapsedMilliseconds}ms)'; + _isTesting = false; + }); + } + } + + Future _testAction() async { + try { + final result = await ConvexClient.instance.action( + name: "myActions:doSomething", + args: {"param": "test"}, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Action result: $result'))); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Action failed: $e'))); + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Timeout testing + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Timeout Testing', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + const Text('Test query execution with different timeouts', + style: TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 12), + Wrap( + spacing: 8, + children: [ + ElevatedButton( + onPressed: _isTesting ? null : () => _testTimeout(1), + child: const Text('1s timeout')), + ElevatedButton( + onPressed: _isTesting ? null : () => _testTimeout(5), + child: const Text('5s timeout')), + ElevatedButton( + onPressed: _isTesting ? null : () => _testTimeout(30), + child: const Text('30s timeout')), + ], + ), + if (_timeoutResult != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4)), + child: Text(_timeoutResult!, + style: const TextStyle(fontFamily: 'monospace')), + ), + ], + if (_isTesting) + const Padding( + padding: EdgeInsets.only(top: 12), + child: LinearProgressIndicator()), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Actions + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Server Actions', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + const Text('Execute backend actions (long-running operations)', + style: TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 12), + ElevatedButton.icon( + onPressed: _testAction, + icon: const Icon(Icons.play_arrow), + label: const Text('Run Test Action')), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Lifecycle management + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('App Lifecycle', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + Row( + children: [ + const Text('Current State:', + style: TextStyle(fontWeight: FontWeight.w500)), + const SizedBox(width: 8), + Chip( + label: Text(_currentLifecycle?.name ?? 'unknown'), + backgroundColor: _currentLifecycle == AppLifecycleEvent.resumed + ? Colors.green.shade100 + : Colors.grey.shade100), + ], + ), + const SizedBox(height: 12), + const Text('Recent Events:', + style: TextStyle(fontWeight: FontWeight.w500)), + const SizedBox(height: 8), + if (_lifecycleHistory.isEmpty) + const Text('No events yet', style: TextStyle(color: Colors.grey)) + else + ...(_lifecycleHistory.map((event) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Text(event, + style: const TextStyle(fontSize: 12, fontFamily: 'monospace')), + ))), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Error handling examples + Card( + color: Colors.orange.shade50, + child: const Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.lightbulb_outline, color: Colors.orange), + SizedBox(width: 8), + Text('Error Handling Tips', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ], + ), + SizedBox(height: 12), + Text('• Always wrap operations in try-catch\n' + '• Handle TimeoutException separately\n' + '• Check ClientError types for specifics\n' + '• Use subscription onError callbacks\n' + '• Monitor connection state changes', + style: TextStyle(fontSize: 13)), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/third_party/convex_flutter/example/lib/screens/authentication_screen.dart b/third_party/convex_flutter/example/lib/screens/authentication_screen.dart new file mode 100644 index 00000000..5187a891 --- /dev/null +++ b/third_party/convex_flutter/example/lib/screens/authentication_screen.dart @@ -0,0 +1,224 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:convex_flutter/convex_flutter.dart'; + +class AuthenticationScreen extends StatefulWidget { + const AuthenticationScreen({super.key}); + + @override + State createState() => _AuthenticationScreenState(); +} + +class _AuthenticationScreenState extends State { + final TextEditingController _tokenController = TextEditingController(); + bool _isAuthenticated = false; + AuthHandleWrapper? _authHandle; + int _refreshCount = 0; + DateTime? _lastRefreshTime; + Map? _tokenClaims; + + @override + void initState() { + super.initState(); + ConvexClient.instance.authState.listen((isAuth) { + setState(() => _isAuthenticated = isAuth); + }); + } + + @override + void dispose() { + _tokenController.dispose(); + _authHandle?.dispose(); + super.dispose(); + } + + // Decode JWT to show claims + void _decodeToken(String token) { + try { + final parts = token.split('.'); + if (parts.length != 3) return; + final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))); + setState(() => _tokenClaims = jsonDecode(payload)); + } catch (e) { + debugPrint('Error decoding token: $e'); + } + } + + Future _setAuth() async { + final token = _tokenController.text.trim(); + if (token.isEmpty) return; + + try { + await ConvexClient.instance.setAuth(token: token); + _decodeToken(token); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Auth token set successfully'))); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e'))); + } + } + + Future _setAuthWithRefresh() async { + try { + _authHandle?.dispose(); + _authHandle = await ConvexClient.instance.setAuthWithRefresh( + fetchToken: () async { + setState(() { + _refreshCount++; + _lastRefreshTime = DateTime.now(); + }); + // Mock token generation for demo + return 'mock_token_${DateTime.now().millisecondsSinceEpoch}'; + }, + onAuthChange: (isAuthenticated) { + debugPrint('Auth changed: $isAuthenticated'); + }, + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Auto-refresh enabled'))); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e'))); + } + } + + Future _clearAuth() async { + try { + _authHandle?.dispose(); + _authHandle = null; + await ConvexClient.instance.clearAuth(); + setState(() { + _tokenClaims = null; + _refreshCount = 0; + _lastRefreshTime = null; + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Auth cleared'))); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e'))); + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Auth status card + Card( + color: _isAuthenticated ? Colors.green.shade50 : Colors.grey.shade100, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(_isAuthenticated ? Icons.verified_user : Icons.person_off, + color: _isAuthenticated ? Colors.green : Colors.grey, size: 32), + const SizedBox(width: 12), + Text(_isAuthenticated ? 'Authenticated' : 'Not Authenticated', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Static token auth + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Static Token Auth', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + TextField( + controller: _tokenController, + decoration: const InputDecoration( + labelText: 'JWT Token', + border: OutlineInputBorder(), + hintText: 'Paste your JWT token here'), + ), + const SizedBox(height: 12), + ElevatedButton.icon( + onPressed: _setAuth, + icon: const Icon(Icons.login), + label: const Text('Set Auth Token'), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Auto-refresh auth + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Auto-Refresh Auth (Recommended)', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + const Text('Automatically refreshes tokens before expiry', + style: TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 12), + ElevatedButton.icon( + onPressed: _setAuthWithRefresh, + icon: const Icon(Icons.autorenew), + label: const Text('Enable Auto-Refresh'), + ), + if (_refreshCount > 0) ...[ + const SizedBox(height: 12), + Text('Refresh count: $_refreshCount', + style: const TextStyle(fontFamily: 'monospace')), + if (_lastRefreshTime != null) + Text('Last refresh: ${_lastRefreshTime}', + style: const TextStyle(fontSize: 12)), + ], + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Token info + if (_tokenClaims != null) + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Token Claims', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text(jsonEncode(_tokenClaims), + style: const TextStyle(fontFamily: 'monospace', fontSize: 12)), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // Clear auth + OutlinedButton.icon( + onPressed: _clearAuth, + icon: const Icon(Icons.logout), + label: const Text('Clear Auth'), + style: OutlinedButton.styleFrom(foregroundColor: Colors.red), + ), + ], + ), + ); + } +} diff --git a/third_party/convex_flutter/example/lib/screens/connection_screen.dart b/third_party/convex_flutter/example/lib/screens/connection_screen.dart new file mode 100644 index 00000000..4bf06e5b --- /dev/null +++ b/third_party/convex_flutter/example/lib/screens/connection_screen.dart @@ -0,0 +1,199 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:convex_flutter/convex_flutter.dart'; + +class ConnectionScreen extends StatefulWidget { + const ConnectionScreen({super.key}); + + @override + State createState() => _ConnectionScreenState(); +} + +class _ConnectionScreenState extends State { + final List _stateHistory = []; + StreamSubscription? _connectionSubscription; + + @override + void initState() { + super.initState(); + _connectionSubscription = ConvexClient.instance.connectionState.listen((state) { + if (mounted) { + setState(() { + _stateHistory.insert(0, ConnectionEvent( + state: state, + timestamp: DateTime.now())); + if (_stateHistory.length > 20) _stateHistory.removeLast(); + }); + } + }); + } + + @override + void dispose() { + _connectionSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCurrentStateCard(), + const SizedBox(height: 16), + _buildFeatureCard(), + const SizedBox(height: 16), + _buildHistoryCard(), + ], + ), + ); + } + + Widget _buildCurrentStateCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('WebSocket Connection State', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const Text('Real-time state from underlying WebSocket', + style: TextStyle(fontSize: 12, color: Colors.grey)), + const SizedBox(height: 16), + StreamBuilder( + stream: ConvexClient.instance.connectionState, + initialData: ConvexClient.instance.currentConnectionState, + builder: (context, snapshot) { + final state = snapshot.data!; + final isConnected = state == WebSocketConnectionState.connected; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isConnected ? Colors.green.shade50 : Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isConnected ? Colors.green : Colors.orange, width: 2)), + child: Row( + children: [ + Icon(isConnected ? Icons.cloud_done : Icons.cloud_sync, + color: isConnected ? Colors.green : Colors.orange, size: 48), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(state.name.toUpperCase(), + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, + color: isConnected ? Colors.green : Colors.orange)), + Text(isConnected ? 'WebSocket is open' : 'WebSocket connecting', + style: const TextStyle(fontSize: 12)), + ], + ), + ), + ], + ), + ); + }), + const SizedBox(height: 12), + Text('isConnected: ${ConvexClient.instance.isConnected}', + style: const TextStyle(fontFamily: 'monospace', fontSize: 12)), + ], + ), + ), + ); + } + + Widget _buildFeatureCard() { + return Card( + color: Colors.blue.shade50, + child: const Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('✨ New Feature: Real-time Connection State', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + SizedBox(height: 8), + Text('• Automatic state updates without polling\n' + '• Two states: Connected and Connecting\n' + '• Reflects actual WebSocket connection\n' + '• Access via connectionState stream\n' + '• Convenience getter: isConnected', + style: TextStyle(fontSize: 13)), + ], + ), + ), + ); + } + + Widget _buildHistoryCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('State Change History', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + TextButton.icon( + onPressed: () => setState(() => _stateHistory.clear()), + icon: const Icon(Icons.clear_all, size: 16), + label: const Text('Clear')), + ], + ), + const SizedBox(height: 8), + if (_stateHistory.isEmpty) + const Padding( + padding: EdgeInsets.all(16), + child: Center(child: Text('No state changes yet', + style: TextStyle(color: Colors.grey)))) + else + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _stateHistory.length, + separatorBuilder: (_, __) => const Divider(), + itemBuilder: (context, index) { + final event = _stateHistory[index]; + final isConnected = event.state == WebSocketConnectionState.connected; + return ListTile( + leading: Icon(isConnected ? Icons.cloud_done : Icons.cloud_sync, + color: isConnected ? Colors.green : Colors.orange), + title: Text(event.state.name.toUpperCase()), + subtitle: Text(_formatTime(event.timestamp)), + trailing: Text(_timeAgo(event.timestamp), + style: const TextStyle(fontSize: 11, color: Colors.grey)), + ); + }, + ), + ], + ), + ), + ); + } + + String _formatTime(DateTime dt) { + return '${dt.hour.toString().padLeft(2, '0')}:' + '${dt.minute.toString().padLeft(2, '0')}:' + '${dt.second.toString().padLeft(2, '0')}'; + } + + String _timeAgo(DateTime dt) { + final diff = DateTime.now().difference(dt); + if (diff.inSeconds < 60) return '${diff.inSeconds}s ago'; + if (diff.inMinutes < 60) return '${diff.inMinutes}m ago'; + return '${diff.inHours}h ago'; + } +} + +class ConnectionEvent { + final WebSocketConnectionState state; + final DateTime timestamp; + ConnectionEvent({required this.state, required this.timestamp}); +} diff --git a/third_party/convex_flutter/example/lib/screens/home_screen.dart b/third_party/convex_flutter/example/lib/screens/home_screen.dart new file mode 100644 index 00000000..1612dea2 --- /dev/null +++ b/third_party/convex_flutter/example/lib/screens/home_screen.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:convex_flutter/convex_flutter.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + @override + void initState() { + super.initState(); + // Trigger auto-connection on app startup + _establishConnection(); + } + + Future _establishConnection() async { + try { + // Use a health check query to establish the WebSocket connection + // Create this query in your Convex backend: convex/health.ts + await ConvexClient.instance.query( + 'health:ping', + {}, + ); + debugPrint('HomeScreen: Auto-connection established via health check query'); + } catch (e) { + debugPrint('HomeScreen: Auto-connection failed: $e'); + // Connection will retry automatically via Convex client + } + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Icon(Icons.rocket_launch, size: 64, + color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 16), + Text('Welcome to Convex Flutter', + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center), + const SizedBox(height: 8), + const Text( + 'Explore all SDK capabilities', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey)), + ], + ), + ), + ), + const SizedBox(height: 24), + Text('Features', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + _FeatureCard(icon: Icons.login, title: 'Authentication', + description: 'JWT tokens, auto-refresh',color: Colors.purple), + _FeatureCard(icon: Icons.message, title: 'Real-time Messaging', + description: 'Subscriptions, queries, mutations', color: Colors.blue), + _FeatureCard(icon: Icons.wifi, title: 'Connection State', + description: 'WebSocket state tracking', color: Colors.green), + _FeatureCard(icon: Icons.settings, title: 'Advanced', + description: 'Timeouts, actions, lifecycle', color: Colors.orange), + const SizedBox(height: 24), + _buildStatusSummary(), + ], + ), + ); + } + + Widget _buildStatusSummary() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Current Status', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + const SizedBox(height: 12), + StreamBuilder( + stream: ConvexClient.instance.connectionState, + builder: (context, snapshot) { + final state = snapshot.data; + return _StatusRow(icon: Icons.wifi, label: 'Connection', + value: state?.name ?? 'Unknown', + color: state == WebSocketConnectionState.connected + ? Colors.green : Colors.orange); + }), + const Divider(), + StreamBuilder( + stream: ConvexClient.instance.authState, + builder: (context, snapshot) { + final isAuth = snapshot.data ?? false; + return _StatusRow(icon: Icons.lock, label: 'Auth', + value: isAuth ? 'Yes' : 'No', + color: isAuth ? Colors.green : Colors.grey); + }), + ], + ), + ), + ); + } +} + +class _FeatureCard extends StatelessWidget { + final IconData icon; + final String title; + final String description; + final Color color; + const _FeatureCard({required this.icon, required this.title, + required this.description, required this.color}); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: Icon(icon, color: color, size: 32), + title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(description), + ), + ); + } +} + +class _StatusRow extends StatelessWidget { + final IconData icon; + final String label; + final String value; + final Color color; + const _StatusRow({required this.icon, required this.label, + required this.value, required this.color}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon(icon, size: 20, color: color), + const SizedBox(width: 12), + Text(label, style: const TextStyle(fontWeight: FontWeight.w500)), + const Spacer(), + Text(value, style: TextStyle(color: color)), + ], + ); + } +} diff --git a/third_party/convex_flutter/example/lib/screens/messaging_screen.dart b/third_party/convex_flutter/example/lib/screens/messaging_screen.dart new file mode 100644 index 00000000..757bd2cd --- /dev/null +++ b/third_party/convex_flutter/example/lib/screens/messaging_screen.dart @@ -0,0 +1,206 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:convex_flutter/convex_flutter.dart'; + +class MessagingScreen extends StatefulWidget { + const MessagingScreen({super.key}); + + @override + State createState() => _MessagingScreenState(); +} + +class _MessagingScreenState extends State { + final TextEditingController _messageController = TextEditingController(); + final String _currentUserId = "Flutter App"; + List> _messages = []; + SubscriptionHandle? _subscriptionHandle; + bool _isSubscribed = false; + int _messageCount = 0; + + @override + void initState() { + super.initState(); + _startSubscription(); + } + + @override + void dispose() { + _messageController.dispose(); + _subscriptionHandle?.cancel(); + super.dispose(); + } + + Future _startSubscription() async { + if (_subscriptionHandle != null) return; + + try { + _subscriptionHandle = await ConvexClient.instance.subscribe( + name: "messages:list", + args: {}, + onUpdate: (value) { + if (!mounted) return; + final List jsonList = jsonDecode(value); + final List> parsedMessages = + jsonList.map((e) => e as Map).toList(); + setState(() { + _messages = parsedMessages; + _messageCount = parsedMessages.length; + _isSubscribed = true; + }); + }, + onError: (message, value) { + if (!mounted) return; + debugPrint("Subscription error: $message"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $message'))); + }, + ); + if (mounted) setState(() => _isSubscribed = true); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to subscribe: $e'))); + } + } + + void _stopSubscription() { + _subscriptionHandle?.cancel(); + _subscriptionHandle = null; + setState(() { + _isSubscribed = false; + _messages.clear(); + }); + } + + Future _sendMessage() async { + final message = _messageController.text.trim(); + if (message.isEmpty) return; + + try { + await ConvexClient.instance.mutation( + name: "messages:send", + args: {"body": message, "author": _currentUserId}, + ); + _messageController.clear(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to send: $e'))); + } + } + + Future _queryMessages() async { + try { + final result = await ConvexClient.instance.query("messages:list", {}); + final List jsonList = jsonDecode(result); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Query returned ${jsonList.length} messages'))); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Query failed: $e'))); + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Status bar + Container( + padding: const EdgeInsets.all(12), + color: _isSubscribed ? Colors.green.shade100 : Colors.orange.shade100, + child: Row( + children: [ + Icon(_isSubscribed ? Icons.wifi : Icons.wifi_off, + color: _isSubscribed ? Colors.green : Colors.orange), + const SizedBox(width: 8), + Text(_isSubscribed ? 'Live Updates' : 'Paused', + style: const TextStyle(fontWeight: FontWeight.bold)), + const Spacer(), + Text('$_messageCount messages'), + const SizedBox(width: 8), + IconButton( + icon: Icon(_isSubscribed ? Icons.pause : Icons.play_arrow), + onPressed: _isSubscribed ? _stopSubscription : _startSubscription, + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _queryMessages, + tooltip: 'Query messages'), + ], + ), + ), + + // Message list + Expanded( + child: _messages.isEmpty + ? const Center(child: Text('No messages yet')) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _messages.length, + itemBuilder: (context, index) { + final message = _messages[index]; + final isMyMessage = message['userId'] == _currentUserId || + message['author'] == _currentUserId; + + return Align( + alignment: isMyMessage + ? Alignment.centerRight + : Alignment.centerLeft, + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.7), + decoration: BoxDecoration( + color: isMyMessage ? Colors.blue[100] : Colors.grey[200], + borderRadius: BorderRadius.circular(12).copyWith( + bottomRight: isMyMessage ? Radius.zero : const Radius.circular(12), + bottomLeft: isMyMessage ? const Radius.circular(12) : Radius.zero), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message['body'] ?? '', + style: const TextStyle(fontSize: 16)), + if (message['author'] != null) + Text('- ${message['author']}', + style: const TextStyle(fontSize: 10, color: Colors.grey)), + ], + ), + ), + ); + }, + ), + ), + + // Input area + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, blurRadius: 3)], + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _messageController, + decoration: InputDecoration( + hintText: 'Type a message...', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(24)), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8)), + onSubmitted: (_) => _sendMessage(), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: _sendMessage, + icon: const Icon(Icons.send), + color: Colors.blue), + ], + ), + ), + ], + ); + } +} diff --git a/third_party/convex_flutter/example/lib/widgets/connection_status_indicator.dart b/third_party/convex_flutter/example/lib/widgets/connection_status_indicator.dart new file mode 100644 index 00000000..6acdd79a --- /dev/null +++ b/third_party/convex_flutter/example/lib/widgets/connection_status_indicator.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:convex_flutter/convex_flutter.dart'; + +/// A reusable widget that displays the current WebSocket connection state. +/// +/// This widget listens to the real-time connection state stream and +/// displays a colored chip indicator in the app bar. +class ConnectionStatusIndicator extends StatelessWidget { + const ConnectionStatusIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: ConvexClient.instance.connectionState, + initialData: ConvexClient.instance.currentConnectionState, + builder: (context, snapshot) { + print('snapshot: ${snapshot.data}'); + final state = snapshot.data ?? WebSocketConnectionState.connecting; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: Chip( + avatar: Icon( + _getIcon(state), + size: 16, + color: _getColor(state), + ), + label: Text( + _getLabel(state), + style: const TextStyle(fontSize: 11), + ), + backgroundColor: Colors.white.withOpacity(0.9), + padding: const EdgeInsets.symmetric(horizontal: 4), + ), + ); + }, + ); + } + + IconData _getIcon(WebSocketConnectionState state) { + switch (state) { + case WebSocketConnectionState.connected: + return Icons.cloud_done; + case WebSocketConnectionState.connecting: + return Icons.cloud_sync; + } + } + + Color _getColor(WebSocketConnectionState state) { + switch (state) { + case WebSocketConnectionState.connected: + return Colors.green; + case WebSocketConnectionState.connecting: + return Colors.orange; + } + } + + String _getLabel(WebSocketConnectionState state) { + switch (state) { + case WebSocketConnectionState.connected: + return 'Connected'; + case WebSocketConnectionState.connecting: + return 'Connecting'; + } + } +} diff --git a/third_party/convex_flutter/example/linux/CMakeLists.txt b/third_party/convex_flutter/example/linux/CMakeLists.txt new file mode 100644 index 00000000..a0288d7b --- /dev/null +++ b/third_party/convex_flutter/example/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "convex_flutter_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.convex_flutter") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/third_party/convex_flutter/example/linux/flutter/CMakeLists.txt b/third_party/convex_flutter/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/third_party/convex_flutter/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.cc b/third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..e71a16d2 --- /dev/null +++ b/third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.h b/third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/third_party/convex_flutter/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/third_party/convex_flutter/example/linux/flutter/generated_plugins.cmake b/third_party/convex_flutter/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..410e76ed --- /dev/null +++ b/third_party/convex_flutter/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + convex_flutter +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/third_party/convex_flutter/example/linux/runner/CMakeLists.txt b/third_party/convex_flutter/example/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/third_party/convex_flutter/example/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/third_party/convex_flutter/example/linux/runner/main.cc b/third_party/convex_flutter/example/linux/runner/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/third_party/convex_flutter/example/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/third_party/convex_flutter/example/linux/runner/my_application.cc b/third_party/convex_flutter/example/linux/runner/my_application.cc new file mode 100644 index 00000000..e73fb97d --- /dev/null +++ b/third_party/convex_flutter/example/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "convex_flutter_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "convex_flutter_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/third_party/convex_flutter/example/linux/runner/my_application.h b/third_party/convex_flutter/example/linux/runner/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/third_party/convex_flutter/example/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/third_party/convex_flutter/example/macos/Flutter/Flutter-Debug.xcconfig b/third_party/convex_flutter/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/third_party/convex_flutter/example/macos/Flutter/Flutter-Release.xcconfig b/third_party/convex_flutter/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/third_party/convex_flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift b/third_party/convex_flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..cccf817a --- /dev/null +++ b/third_party/convex_flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,10 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { +} diff --git a/third_party/convex_flutter/example/macos/Podfile b/third_party/convex_flutter/example/macos/Podfile new file mode 100644 index 00000000..ff5ddb3b --- /dev/null +++ b/third_party/convex_flutter/example/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/third_party/convex_flutter/example/macos/Podfile.lock b/third_party/convex_flutter/example/macos/Podfile.lock new file mode 100644 index 00000000..29876fc6 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - convex_flutter (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + +DEPENDENCIES: + - convex_flutter (from `Flutter/ephemeral/.symlinks/plugins/convex_flutter/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + +EXTERNAL SOURCES: + convex_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/convex_flutter/macos + FlutterMacOS: + :path: Flutter/ephemeral + +SPEC CHECKSUMS: + convex_flutter: a9d12846e80c8a2238282775fa9eb9c906efcea4 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/third_party/convex_flutter/example/macos/Runner.xcodeproj/project.pbxproj b/third_party/convex_flutter/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..fb7849d4 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 5F83E745364ED6BF9E7FF659 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E71D87C3EB6720BF44F05D8B /* Pods_Runner.framework */; }; + BFFE63166C502BDD7B20AF71 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2557E6C7825F3FC95F3CD0D8 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2557E6C7825F3FC95F3CD0D8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 32099594734A516286FC2264 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* convex_flutter_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = convex_flutter_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 50D06C36722B18DAB0BE360E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 53F61EED7E54B288A8BC0ABC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 5571B207020C2E29ECBC07B5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9860AB98CAFCFA22A12134B5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + D63BC546645F3C3533E4F378 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + E71D87C3EB6720BF44F05D8B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BFFE63166C502BDD7B20AF71 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F83E745364ED6BF9E7FF659 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 579DD7398EE076E17B680A75 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* convex_flutter_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 579DD7398EE076E17B680A75 /* Pods */ = { + isa = PBXGroup; + children = ( + 53F61EED7E54B288A8BC0ABC /* Pods-Runner.debug.xcconfig */, + 5571B207020C2E29ECBC07B5 /* Pods-Runner.release.xcconfig */, + 50D06C36722B18DAB0BE360E /* Pods-Runner.profile.xcconfig */, + 32099594734A516286FC2264 /* Pods-RunnerTests.debug.xcconfig */, + 9860AB98CAFCFA22A12134B5 /* Pods-RunnerTests.release.xcconfig */, + D63BC546645F3C3533E4F378 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E71D87C3EB6720BF44F05D8B /* Pods_Runner.framework */, + 2557E6C7825F3FC95F3CD0D8 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 4FE9D07133D15C36BF94B9EC /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9B884211EDE4031D884FE3A3 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 4B187D1D2999DC2E01B75E0C /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* convex_flutter_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 4B187D1D2999DC2E01B75E0C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 4FE9D07133D15C36BF94B9EC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9B884211EDE4031D884FE3A3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 32099594734A516286FC2264 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/convex_flutter_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/convex_flutter_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9860AB98CAFCFA22A12134B5 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/convex_flutter_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/convex_flutter_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D63BC546645F3C3533E4F378 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/convex_flutter_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/convex_flutter_example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/third_party/convex_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/convex_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/third_party/convex_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/third_party/convex_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..6266fa98 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/convex_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/third_party/convex_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/third_party/convex_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/third_party/convex_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/third_party/convex_flutter/example/macos/Runner/AppDelegate.swift b/third_party/convex_flutter/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/third_party/convex_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party/convex_flutter/example/macos/Runner/Configs/AppInfo.xcconfig b/third_party/convex_flutter/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..0110a435 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = convex_flutter_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.convexFlutterExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/third_party/convex_flutter/example/macos/Runner/Configs/Debug.xcconfig b/third_party/convex_flutter/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/third_party/convex_flutter/example/macos/Runner/Configs/Release.xcconfig b/third_party/convex_flutter/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/third_party/convex_flutter/example/macos/Runner/Configs/Warnings.xcconfig b/third_party/convex_flutter/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/third_party/convex_flutter/example/macos/Runner/DebugProfile.entitlements b/third_party/convex_flutter/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..08c3ab17 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/third_party/convex_flutter/example/macos/Runner/Info.plist b/third_party/convex_flutter/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/third_party/convex_flutter/example/macos/Runner/MainFlutterWindow.swift b/third_party/convex_flutter/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/third_party/convex_flutter/example/macos/Runner/Release.entitlements b/third_party/convex_flutter/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..64cabb4e --- /dev/null +++ b/third_party/convex_flutter/example/macos/Runner/Release.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/third_party/convex_flutter/example/macos/RunnerTests/RunnerTests.swift b/third_party/convex_flutter/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/third_party/convex_flutter/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/third_party/convex_flutter/example/pubspec.yaml b/third_party/convex_flutter/example/pubspec.yaml new file mode 100644 index 00000000..0c1baaac --- /dev/null +++ b/third_party/convex_flutter/example/pubspec.yaml @@ -0,0 +1,99 @@ +name: convex_flutter_example +description: "Demonstrates how to use the convex_flutter plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.8.1 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + convex_flutter: + # When depending on this package from a real application you should use: + # convex_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + integration_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/third_party/convex_flutter/example/screenshots/app_screenshot.png b/third_party/convex_flutter/example/screenshots/app_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..684b201cfde19acd243c344361b13ee279c5ff1c GIT binary patch literal 276212 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

FEOhHH!^aNcX^cJ>iIUK`h4Oj^a82teU*UmIdo zPvY1Z4kmoCl{>N$Y+5oQx6C_KsWDN+V5hC|e24?9q!su<8jmyZkfxmn1HlWT$ysG} zB8jbMy+TD;G$F*!9^Rgt4*E6&eggu#0f18W<_HypBLU@Q{qT78`sL7}@vg^)rt$-v z8sSl}h9|x!&i%0{A6rA<_2u>HCH~x+^kywEh~<6R$GZA<=ZBZOv2Lt&x5s^8PGpeA zqquddr^hPn&GE0-?iJLsN!JP783a0Yb?!-It?9(n-dMB{$|8gRoZa4`;YisMa&Tw!D{q%<~wh0y4PeSv=%&y^GCC#-ZJhu+I0i4KD zp$OIpqfWv+yl=x7sT4{Ak#)@Pj^{-kLTcMSez6!$CULv!qK7zpwIWl!;;vzw45!DK zAmqc^X$c%;G44Zum`|j0c>keqKV$l~R#&zNW;d^Dbwbr>F3H8G{J{x*y=D=$W}rH@Cu zTfLQjf4n%Ou1D@EY^_)^Qcl?j@p4(0KP&RT^KCt{f4q005Q()ih=-LRDLw6Hkj(## z{}(lI-z-<~yt2|rK*gb@EUwir)HV8K;uBfWy;O=|aXOf26~kmO#7>~Tk<5F$o5N&s zVz(yEo+wACrhh$5`KYI_l+NeWl-A4yH!c_gZqaLMyg&W+u8n#-DQLFZBKVLy^j#Z0 z7B+SQz)?L+1>3ogt`}>J+S-lKMZbsPb7|;FzR85r#h)1@Y9AD`nb6=G>-e;WP3VA&~x zbJpF(jA)4V;{Nomf<7KuqPMZ?cePQ0ia?H#d65Q0^T&4q0`PVXCwJ1Zg_Inw2F1!{K(l-O}mj*EZncB!QTjBQ;bQxPtnvWK0CN1RDk}o6WX(#EET?^oJ6$T=PAh} zLq|%g?!``5d}at|UvibDZ#pkW3e+fO;1m6Ad+zT!!dJW&>E&T8c-R37u#FQ{&H`Lt zq~3BO-yt)E|3ocYHSx_Rm;Bbjp?CqLDD(>q+_K?8z=ENlxU*Fq^KR*Tl2fHw&`Hmu zNBKzw^&5rsQQ5%{)oOQbzDzIge;~&1mIr9`YkLY;55$995^LhFs54yJLSG%JBY3ho zb)UGX&F7~tyrDL~If90;Kw5tR#BYD$y+>_B;N{MkYH{N(u2Wois@>3bCVhJVu{>n? zXs*dha{lFPO~I{EK&=Y1-rnUL#+p%G855=VE>WpKOP1kZ^{$5w+P&I%@ zk1EA#r5s=EZPXT&NF3%>85PjE?O?6t#nhNoIBp`Un~*#=Ihx_s`sAS1kOGp$eBL;+ zp#&9e%znDQeK;bNIW9JxER4V3>-Ca+sMYm28k@dQhHGG}Hz&8+gj|zN6GR zD9FrXBG40`mU|*d+h6ld_$0i2E+T;r*;iw(loKg3xe3_P7h|*~GSRLVyDznoF8w|4 z;BT(is5chNIR2z`ie|nl1Fo(+P*D$Q z7T9_%UhJ_43_JvMW!WNPCuDpP3oGpw)?3D`}AV z{&c@(+xO!z`f*B|RGP~Pb^hTPN~kDShb=|pE;QGOr^4NaHb=n2WIDTw)qGAR!SRUL z;i7`kXNr{E)n+1O3){fF&WY(Pg#5T+>474n<1R`lyr)3oc+%Fws_w!OC;!v$f|5o< z-jFMmvc309wep;f`|AnOaRv*H?np>@Pn~aR2N-16!K?v#@LG4TN*D`o2pDyB-QQ*r zcwLWVwra`X?rNBuU%gL7LP9cp9&I!{A@N9Kh?l!+aBn?R(ny|dAVFj6?nTB$E zsU(Zs{l(*=+fh%$Q8Q>vsb{v_s6uz@=o;2L-{TIIS&FBuRG__X%QT5(frm-GUG1=T z4c>NA-8k_?k6oC(8JGl(voW|=jzN!C7ljLoMYLs8kz`$ec9R_6vI zV%2o>9R=^BSkvKmaX@A|=kc3xKSrsPycJr0j|t2g-sbD|l~?K7bajD==(c|*Az8TnF}6zpFHXh;&9 z4yKU8pEvzb(lqaEH1`-zOC8EdrI+S(h-QOLx(<|{ zAhZi^SJ}s3mj8}E-RKf^zee~2ZfMuo=_x~s3wm`OLl)1_6Pk;rQ!Sx;G6(3~!F#%t z5XnN0gKr>U%UHAGCYjP;N%c3?n5Ysb4b8fPH%|*E_&3YGSFl`FWRd~B4rlCEA0*lP zSMwl+gtOwhI<1$#GC%zhGJ#R;$IzuUL9GOv-btZ3Mea;G(X~FzMM>FBB4>lpwh@t3>vz?? zKf_Y?PS&qi-qmU^v$!_PV5z}2>$y|RAF!LNuc;ozV7if~_%d==Eg>k&GU~GrdFX-z zhrt{Myu<@Gh%7S|qXM7ZLV5hBzdbIm=9;?)1s675v_lky0G1&9ic-_6$~pdF@o*9> zb3m(==NYXxrGk6A427zujdEk1M3^sk^=y?@$bN{?xp!2tJu$jS9SWij-(`LOjoS55Hu+9 zmQ*cU9zMr^Fx!-uCfxc-2&AH@2T-W=Pe%HX*K38v!$i%x@FQJW_xJ9qg~}Y82On$k zponzq*5|f{Qz^|R3z_tPROM`z3n9qNZ4SjuShbMu@jgyVXLaCxnv8s_!R;- zz<@&kJZdwLM%)Wd$D^K5k~CZztDw24{F!(Ahwdc0B}WI0^loPkCFy4!ER z7?v#lt;pa9%T(i|*;EgY(^H4JqGpPKuE*{1m<*(5$+LnxrwoKBgmBG7NFstQ<{aU- z`~UtgfIp2kW*?~36C9iOW0r3Wx{?^27B0pznToQdA2fl%WED$bkTVYdg~v$_chu$Z zuU+`_;76O}v~O75b#O;^ zS+&KLAJwf5Y9S;{S#RsKzr@~OIl+LkD~;*?cB&;hhPI{fch!nF+;AQ+=H82A>`87y zD9%%D|M+yfwZ`muPb)qQDuO<{-K|pXiyk&ca%PU;cX_s2pB=&6U$H)u_XY2o;uFC{ zH99Y#2c51p92TC2zADXlLCb{3`UqaSwd~Wsd|48WZ-|95kXplF$ngU-O=3Z0nKuP6 z(f6U-FJ&p-8uv5I>9wm;z0|kN+nVIPU$_T(D;4$=qNnlldh5iZlV%>b3aH}gt1G+tN_lKigJ?fkV_NT|@r*vaxt{yYNy$9!V<1KsrIurW6<+a_ z8Z@93$ExHP!e=Fmb62DF@11lL26McmAGHqtE;f-Z4W6l%cIy7kf6CBu?z2ByWav98 z@JeO7qX_O*23Ig_RaWSq9@HO)0=hN%QBaR64-k4R_Z7db{Dbmotn=scYaotSn!sZp zu`OF#Do>pMp;RA2tRLOHoCS~0G#;dFLS(6jnAQwmGn4`XM!A^0H!AGx$VB-H@MkMj z0kRuSvae}2&XN2B$?>*j-IRyBk~7&>LP5Sjjn8Y-Vur-8KGW{0OIo4Dg~g=c#l{0{ zTR~Bfi5u(TR9A1*0}V*E=y|XLmHjn~Z>w)hBZGDFoei$eZh3!kZne06PK+U<0A5RR z1MTCfw+{Sa{%KU|e9_*?QlwdNGq-ge5%k)U)6?)qgs8qSjT4Wv^2;o~r<)&4pZBMI zK0FD`q4S;pCS0fFdrZH7IPwbS=*Fj0FIOvhDzzzcWZNW@!}XrWd)%cCMc;dFa1&`E z9^<4IJJFwYc@z{|JKt=){YAijeVxMH>AXi~rLx&fl)VzXZJgqf{%uiBc?=eK_7*?d zt%lWVk(;@r61`@81_gbBh#%`MAuBHSXIKz2LoO|cy?oTcc3U$v~ zuQie90UbJ*M;Gc*EO~!>_<4$YU(&{gTP>WsnR=V{E*M;)ghj#v1eCfpN&`Cs@W`kUwR z5yFRU%CaSEvV#aLG9m@lH7Ulw`hXz?5Ns@G&Dm>(^$vU5n|cO_xXn<6A9nF}CYu^6 z&8D<9OwlX~HK&26UJ573wWBDS7_Jp+3iG(97|c>*Mv4nbw2d@rV{Tg;eHFqN0{CWY zb(RbaItJ`9YWCfUO^IdRVQZOMDsh3hY$S_37=S@=nv^%+M5~9ADHDwcBN>*Q1)G|X zs^Dqw4m8R%QFGx{fzjJ@94C{3Iv&==Nw$UK{pHCbCPz2956`1rP~)sw?B4K)Pr`tdEBI<`uxZO}pXBF6 zy{W~G!35zJJ!>|;maVO;ULKnB7;>KbW1Yb_;sI(WCKLHFz~nt)ZveV+r|L-@EHq-Y zu>C9Uh(80=z)&e=tFe3EQicW#f1wyqomDkvc+pZ~*rAQC-7P((YdTDP7|oKLQkVgr zppnsbKXJJkOZ8+BI5A&vKwE{GD(PMC@?lzLfenkWwY7z9_&HVm&81=8E8AxZGTc`s z;0$@OlI7^x1j=bE7iwCNr)H?DooG~LFqv#hOmviL_E;fNe`tdTW>F~>G!2(nMDWJK z(9QOao14l_X6;R8N=hnu>2aNI0_b5Z=j$rqDs?d5bu>#r$h$(4;K{-_VK;$F4TG{7 z`y;ZmSslEqt}hLv_W#G;TSi6My>G)xh|-FHN~(l(Nw=s-2}rlJba#uig3>vFl*G{8 z-6c7+v@moHL(OvyaNj@wKmYH$*8A=K@T}zrmNVC$eeQkkbDwn_SAN5Ya=v=&@sn9{Jr#3DJ|JL85rHCnGD1Um> zB&Uh)vVj$7bc_IQxi}Y{VH@S!r@JG=1Ksddod-`h-ZQIi-k3Lm?~U%YP+U|>0|_|a zW4|LFyw%fOn#5xts33fVW3*ZlO+Q5TVwGH{GaFoFbMA5~KuR0qs}Fd)OT8QoPngt8 zKfgbOT0H#t0FAj*9H<$W6u;B2c!@n2+7UYNWf;O{MCQ^`ZjqtX;Q7s^8l|=fh)eZf zGHp~pI4tmfW2E5+P*M!<@QCl>Rt`&pa&tN24}Y6n%=kF$$U^MQ#;{AwiKU#xPJE}& zKFn=B7-u^Kno4FzOsaSc(6~m7J3f0~F5y>8o6o~V@O{R> zQ<2YVZzYM5T!r}S1dTDK&g~rEL~`&&j>A?#UTUvHNezWG^)>5U$(}|CCmGc*wau*) z0_AU=dq~gf6W(?uZhioC%)Yxn$aA(4!z<$#SBlRL^<3V7H6*9 zd4eoYWd;%En?qZR2|wovs<+$l1!_1b{<4@(^+-`snq6Tk-Wc6Q0h&E;t0U2Z`S0IO z=j4*40(e^t#0^c38iF)TIW>oMn!9C*5G9udqL?SIcS^o)p&@;lSr6D|qX{XvVhm_M zH#o~>!9q&}aANDBpXR;-MKUz)+?>x_F-GnP5__rflqK?ZQHKkbij)(Bas!Wi9b(3O zYG}aGq|6Pk4TmxB2)E}uR0LPcvVz}OLN?$GlxnF_0w0DHl2^YiC?G4239~;d z<63x7E{i8XrlieOjc-k6*!WGD-9XUuHr!y|Du>U{j1HMUQ>kHwX#5~&f3B7mzFFNJ z*{s|ZsG9H^ZL1CjYW#`&fK`sUii#^m>wODVJ1yey)3i-T#nY*}?-R)v$NX9a!tzLb zRCo#f+~)^3L`@3vEpR1U-tuyGnwxjMEKaBW#GnoheJHxt=u=C{VL|mZ@`lhEqk1W; zi68^uNdW(b!ft>zrK!FKqdm%w>GA@()Q_Z+@mPywO2ZZL89m3hw1(cHg|GZk*g9RIp0#|gD$)E(J0^Qw8xIh_FX~hs8-eT>+3Xz zywxYhyI#o=GYl$SnMjC$r{J3HN?azdFksu(l0pt#b_D*ImH+mO=54k z=6a9k-c&l)pMp!GsOO4Z?B16YqfKE8d}~*iN^ygcmpN`F@*q{wfc~b?6vtt|(M9db zKyucu0mWCK*3x4hdtZ3bmHwV?#a{BvIo3m-oz~;^NCAKR3abL3bwgI4lGAFo`7;5( zu+ZU>u!3!`67RF#De4_-y14giSZeuTpy@6vxiSi@?cqW%LMAUd&d7!>doT#9GpZ2d z7I(HUBeDO^BahV=+OWkp{Agj!KYO^@*p!ryuwu{?7Q?W-(}YSBuA+)E!UZ@M$VOWf zqM5u7;km3v}U;k%7Yc^Kt(^%@-QG7XJp}u5p?pei5ea0?q$*y+%k!Y@aH+-}^NX784 zV#0^7y$6np=j1;5LCP(1-v>Z_t9@hlahotC&Ts-K&mbGLYMNvA!VMegIpx$B+K62( zr*8eQ^U-P49X!c^QB-si#kz|Vw5s!??M|_VbFprgnpZat`Vuz6Tax&lK0LU6YIqDO zT1*ePb);SAsx+v`G2{=|yVS2=k&L{$9R)Cppyh*m6@|m|7dRvtZxCe1A)U~Q5BW`V zH9ae8C1|;fMW_pR4`;i-Xc#3(PxGbEzdp*-wn#QRk?lUvrBnu5r#HS!AVQ0zQA}Rk zn9BotW;m@&#epIdu9Nv3Ii!GN3-Ya&oQ-Cu=bqkIgl|!O`V?9W_c^S}7)5q%4D6Od z)^3(xCh6*qdZss9W@lVm)w-yZt4wnk44o)Z30f@<2imJBmC3Fxn$g`~C3PDxg70HA zgk*brd%ZC!_~kpOO_*FGsWQcpTdTAQDh^W}TCU%aMe%Pvd_HV?w~5{PCl_lKWr0l$ z!TuSR?HBtF$guxR|8BCBj!h>*+U|a?sxE|=tS}hq+xix0xDCDaO+^BO1V7$c>qCs` z&E>c&ca zSI1%yZVF?b2SrP39N0bVeI+;`^?75>Rw7yhNDJb5 z!wgU{rNxhX-_!iEYu#)aLY?tgqPFT{FGe`ZUj4<49m0bq+u5lLvb5Wh-*9p3&gACu zSD{49;T!RyJ@*Ni^Ka!1Ao?q;IPfx$T{rT1j)s17Q|=xL(D+cIbdZhf#Q#2Vr@6}2 z^-_8p^8tp-@TqqpQAS4iWTfKi2~)O=gu|#!ea&l`m1ELfWV#UFeLg3ht;&&SAWF(q zC&Gb@&;Gd{YI~u4OadHs2INY5C=drRpNT6dO?5%B1ya9mkke#;(C4Y@rm6y3&uIX_ z9w}j|HNiWz>B(G>l7>-#;N5c8Uq`e!;g(Ou-&B!+WJA_747ASWvt zmK^*w=*s?I(!`UARdMk0HLU9B*4V`n>BOTo(1f`&f2-ig(?fpcegV6eOFZ@7@1u3< zwoaT&c>Ng;x$7^K=$K}SUKTPiQ!r3?9*e#g2K-}7&1F;u6Dl7nH2fHgl;DBuUY9u0 z8w2+3(~PHepH#d|veUV!N2@%gOcmZ~IV5i9%SZ6G2FB>brSSinfG8M}VNEUy==45= zR*g>J>R38$&kk$9YxA2ZF>2SmD4(ilBGB=YDwJj+AXxE(>w-MlNh2}t9i#EygiJ!l zQ%_Wfpe z9td%3uwfl4pMlA3LYj#JIWnkkR+=yUWAzt_%;Ueu zrs|i2CH9jq4#-U=a+KbmGZfTPahu?u30C9sJ=3M5c?pKid2#=kgw!8`w`q_iz{r>H z2ZLy%6#MwDH1YY9Mq^jFrLw%PZZ>V?NM<_UxQ&E`WKM4A{tTxnU&ngL_?&ZAulCZ8 z^vlyBWs8x6+UdjHxZ6@)U-2m(=;WW?0ZC&KGsPGqPQOoEFLL)0pX|JE7AUeGAT~SO zf0yxjS2q#JLgECV^`h=K)w;_veSWx)fe{fIGOr)jeekHYrGh$oSZtbpXq4BCRwn!~ zg0{oD(fv`WCD>;XLn2o{EGkngeA~TdeFkPQY)=bPZm4YgwaZo!im~?c21r^enh}?j zy|#>=p=`*j7eaoVj5w(P8ulifO{lS!9(u_LU~hT-Qn8(=eE-cNv)bcmZARg8 zoX2?wpCHEUOZBJbG}9LU!(FY{p|Zk@(pB)4sV5y>n442hgWj>Sok}XVScs9iilr~y zG~Jaw`K(V!QKIx_cu{&~V_EB878E0+RQ7{N-7aPs{yULxZty!@V|EN~-4q(;SQHv- zS9|~T$L`|XAWl|)ggegHBu37`q}f5{iFWy}{#*&So@k~FgiS{eP{rtFHR?~awAG3A zWwnPdtHdYAlbR{%kbEEOedu^mNmS`A>p~zj*Ty1-Y(gC+x+|zf_?ofD&d^Qk_@SS@ z#E*LaRz{q%D)Kju5F^0*D=P_>t3LVhMtF68ed6RE()lASf;R4rS$%b&@TA%qS$98K z`^J_36}%mhQff8BH^FY4-|KO9rzY`d?_H+HmTJ0qwf7Tv{mJyq3^5`H=y}j;0e?#y z4WDGa)u*EiXfyl9Y8@!1Amn6f^TOfFgRL4EIlRxxQ^{m}Yb3iwI*BV+Q|gA-&iQs5 zT8F@7(=QVIN?5eg68|K0_pJA+_l$GDz@p+ry@$q%Oen`CoKO8_icCT|JA63%VNH5A zy(P8d7&Frt9U8`lHCZH(DZS&4GH z_Bc>PHaN9@#;gaODdn_ z0~@Sjyc7Rl_v=IJ97jOl+$f-G#H_`ckHpnKJLD=*z$*YM*gfm8S*Z_Lk9%ksEK{+6 z;^h#rNA6PKN>5BY>FD1A+6%`9;8kM&n954?7>f?eq=)8Jc*MF-0BVt)WG`WA1bkrF zN9g18&iohZcmtl9- zG9Sy2-5>I+og4%8@5|G0bF#jmb-%dbB|V=|V%QTMoxwYG1p-Nb=JPYHqm$dJp7WJ> zOneLAY^h{7E7t*y{6Jf<55XJQJ}RwJ%j+`>k3387ok_ETM>b737!-*`rJEnI-g(i! zqDjre>?MYEE%;rbaxPSwqipR>1m~w+qV)MxP_yHjF7>ougCfjA8;~ z$^}ZU9UoMeV*QS{O~gO;SNgs^RbGD`vt&I0)=_{>Lzcw-p?-NEdOQ_lhN~BsVicEuqVyYh9X{X);2*7Q@h~N4XJ8(sPJl8vTvbmIRCy2P5Ff3_)U_FQw zq&}&3;V|Is8gdIq&4g<6HBMRQ=lZ*II~nYeH7WeHn2=-X!; z#_{wVsp$qF%)`qcz1$9LOlE6cT;7J5oFWrhfQc3-e1z(GbJ_{vyXQ-B_G~3ayB8gx zl3k|sW-6JN0cdfMKIzwbv_j|ueaq8PNffSO! zWw<>6csf2U$bC=Pwy$hFqkD&50N42fnL{OF`DqwbX44m)L#gT+$VwkowSd;F?!E_D z3ui+$E2in&D&;6zrp}geqHF2#_E4HC91@9r@|g^BE;YG%v#Dl3+n28jP*1QKAQ?3^ zdd#~wYV~P9v;}K?P0xe)dFd<1wiFq27#i>IQ-L%NAbUSbVu{c|&I{WgkJUxjvi=ohdww|g6$vm{=ggJT?AX5b*(#`TV|yI2U9neE7kNgL7*$_Xeg zmE`Y>B3>gA5f>%ph;jh2D_YDYh)H|~uv29D4{)`Fmimq=Ll`yviv&O#j~?|qU+#{+-j?Qz$1vu_yu+ zzl#V{TTt#OvfMnoyFVX5(do8b|CEH=@@rp?tJPHX=*Id_3H=Wa8zWt4Q4I^hx#6A~ zPNh%%Yqn^U-3`WOE_>TYm&2_stftDrcf!ML8nL$S5tbB^0BH$DbRUv>nUV z#KnJQXMwr`4b{cPeT**|VV;9pUjgu@B59i**96f-#c%I)cK%~je;2t+TDOoVOW z^8(nu{`ge8Yor$dwWb;@Hk7K`X$~>7Mymx#Y>?=pSGRvneEE0Q)Bxl*GzbX8HBvh# z(+o+)I$r>!4{?R)HH1j~j}yri1((mGr`Ee|`ZCg0f5^{_0_-e-K+W|@(nG?Cb9aG& z^he6F!1uxS3%P0fkX5Aj|5pEsOIJ=G?F4Y)fzJNg_WS{?D_)*RAO$nw?05;r12|sp z0klvmP=Mp+euX(UyR+^Db+9`9e(_7>4!8fa!*`GbC}&?4)117C0ay*CPw`#v6hC9G z>nPI}$#S3$2k4F3z`;md=`{c6e?W})crK0!mU40nz(3h`7Os>dgP^hG(SZ4Na21Xe z#BA+y3iQa<7Tay+m8&muzWo(%R!^S=;;Q`<0@=v%Xu$OP>EgS908akEk}6Eo7XU?} zp1P9QD@jLqe70uXI>+-*UH^lbA#87SEVq~A;bWPCp0NwaJ=bt&26S(K% z*%#+UkdLR76Et!>95A{TJ?#=ZO?x%@_)F0vu0>D3@1tQ^rDa*Wwi5Y>07DFUg37+# z0JS;OTyMN0?&VLPT>f-;>*a}Vz1nZejN2n%4HaYz(j8|U`X<}Ph84LVatQ#K>9a8# zsOx(5qAsb43NFtyL)_&AeE*pMsSl936Rkh3LqurTg-TQv*@z&=___ul!rC|<4Cq}8 z(T&`Je5Xqx2&pc2pZt3F=_G!A4`Keszjf^M+;rp_bw`KjCBTs8ULyzbfVP0|^-0JH zMdG$|uU66dauZisuQyQ*rDEE(2HBh`F|lj&{&ET?Xgj@aB{A2ywRNQW)x=*a&|Vuj z(aqCy&I6b9A1=H%N+?-LT$;Fq_d5Z0g?A2tq{NpQIy4{t?OLe=j*kY$`}{kfD^nBE zfYYvi$+Gqy@PYcfUr6mg@#G!L6fbAb{KB@DJzvsC{S-jg5prS0==Pcpy-XLwpwljz z1`&{b$>5okftaK74$G$N9=!P0Q(0XpAb?X=?OB=*(7gwvwuxP-;0TpBo+^@P zsPFUSI{~uDjb9UQyd!ndiM$Jg1(omZ z_27GbMKUoQPQ?~;VO3yPIEjD5)UfLN&9;oxgO#yezsm|RCE4wf0v#QH5W6(yh+W8n zGT$^Cl68nO>9z>6OB8SchfN@)muPK+`!{Jt;1PQkqRVJS^o&ZcsB#OREhEVueioXy zyx~7s>~HgKURERk%p821DEbN*{0?-Z6|9>DGHUNK45IU@;XxAgJ*V`>V-+5XRo@u_ z+St?{eU25oWLoLHWU`1xBF`z6UnxGLRi2oguDY}N^ii}KMgOn}z(BCvpr8sccOg$N z77BoUi74skn_`SoD|}y1D&Wk@)ZQnGtp1DS-2E_<`DGgXacl-Q&EQ;q9si}@LJE<# zdnrUtbfAVBdO&c9haBUTJ^3oach~u%$`~jclSlK~8D$L@@?OGE0FWkr7Ks@N@3+ zCq;&RWw^^vAe=1Ih1y8eUP12oFern^oy^Gt{M>P=Kl7Cg&+E)8_&~ILTEMZe$;Vi6 zT?RUGvk{wlJD zMu`Hjctxw0zxY-r-oxYn)lVR;mC4n-twGTF>gokZfE+ic>B59}o#3kkh_5&&U+!L; z{wF^DNj1nwM9H!W&V|fs=%K1#<@(=U(ka{oER3z(Qe|`r?c}m z%60=HU*LSCVQExFr#}#GeecrHsG%V{(&6nm++Dn@pY!vWC-AOrQ%>Y{<)jA8HlOS= z(PjA$BMKWDkltLXLI^z_5JcojC!Dyn_l6K$wlfCfH@+<%*S1U2JRL2NSZx2oFoZsE zg_e7B_mblc$@m$^WU0IT8o2h78n4aZ;_W=`I!SHUHFTH5s2H>w+DkPNv3SDzzEPxV zV~=6Eo1q|@T+nqaGutlz(zN_U@-@he05K|u?6mEEdwUo_vhZ9L2~oOvuTv34$GzwU zjr0SSf(9W`pn?I)9$u=wycqC#Tz=Ep0(U;rc^;~9qIh>T!A=q61T&HJn{iJpb<2yI z4(I?s;l{O}@R_#I0ZQZo?L%|{=7@T`{zj>-)m=3G&7c2kx9iqXiq5=41akO7!IaH80>tF$k1ufH~a9s zmhnp2a*)c#{TfiVx3o4`9JbBMc@;#a&&+*Lu9P3}o*ozKwz#Sl8x)D`s2Ax~xfkjA zzf#CG2>lkxS7-tF~XG7j!#91}1Nkg*?7^J51Z0XV|SW6GFclUjr!Uk=snO z1*{rCIjhU{!0h`EKY$G@rSU4xM8=)gOPzOtSQZSQsMPf}pDvXzH=kv^{kFm4lxw0& zKVW0JNsRM<+U{sL5X@vDJ-nB z=&;GnRwLIw@U0~P-KK6w9Q{HcxQ9BiQPjR83=QL)RUFpGq7VbK9ydDW3Egjat)Zc$ zQsKL@r20O2-}DhP_+)}V5EFtG_)%|sn?O|lzT3^aqUs`%Ph>>C&AQ#8#z&$1bf3~^ zX)A#{VWKyBYFc}XxumASr?0=(#LCmIYWiZkjyp{}?5&QCbRL_&vdrq#qNEo3O%$}- z_q8Y~v<)5lVn+LkS*L@AX*-f*hAWOa_QR7ChbfYWg^Q(z^>P_?{cj+@jZZ1!ef`Ja zEfn|;eNCdf+NQfwU@4Vc@RWm-Q$|DN=6dk{OlmFzvlI0;9B$xZqoSIa2VGZBv@CJ9 z*#kv0;by>>1l5RFi97a|JmlFoIu;rbOoEns;%`Dpt0NYTwD9x`345gEl?A-urqR7r z4XWye)+RRPll=cR1rcu%DCSa2?gssyVcCAhuOEu__Cm~fuPc=>3m2-2Ro+2)C=gPp zVCr3E-f7A9aY?Sb=7vt=e+`rJ1#NF$$x#hnJPCgns7SBFM-hX;H@zOrR565aw&o(m zRH4@=Ww=}Cde2-bM3v^>HMstxG)P3KwwswwN10!TgfZ$PSXIb+<{q)^mzjG5QvG)a z2vGB=4+C|Tve=RL+Aiu$DCyQ zjAS$Tl|&DiY9bgBgPO0pT1u~OZdft!w=i~>!i*w?>9(fBieGR%C$el~Wx!>pz(~|# zKR#Q?4hwj-nnW^X7KNJs&O_?3YaLW)Rum=SO#7E)MVFFs0Fnud%q#N_c7FsPKX02R zAf)gTsD01^E{#)uWJCQ}rK){G`h*#h^Q-W`hrs`!hODPGFk?ae0V1MkEZ@Jqu->L6LA10QcuwgF0IiW z+bq0rBW=jdWdJd_P*1S{YmB~{sFkC~*h62lq*bVduJ`S)+1$C*`&p#k&ozVd+;qpD zOkAx0adv9C#oQ~YZfrQOET94lUj{Up#ih;K>>@79Sc$O#tGxTZ(Vyq$?EVGs%ZlU&ViK!+rn!$~!&k|K0BV_Ye-)*3tSKf%`L40m7Qj(rS*B~S% z`hOjadOG-&5vQownt_~Om?CDsfTvLTUAQu|jV{c;Dl0i!iS73PJ|PBRLhOFX#YwZy zy|35i(e2wro2uMrJ9%imR&L|5%rKU24;mWA7jw4hj@;=r$x2tc51h*GzZJO!UGImY z;4-j4QmG2r?rZLv`R<0k%^hrI#ST*4-92jXA((2+Mq)-z)H>|NkgeHKw%3?m?un7w zUnkVQScHgb83%c`*aEvzeq`+1)YR85Gz}q!d^0!8hH_flh9dnm6wovt2K{w1Z}@~G zPl5F1$wUQLdI$}6V<=_pbstWYM@?mj2ff<;FoDQ`t46*RYu+&*`%4mFx1X^ZbbfuF z*lo51h_fh^0oIoz9FNK3nM%62YdoZ)nZ(s8+1=AL7G6}kU^~r>RVKMDAzy9fuETey zJX$^W=|4LBul-ehf?ViGceOfrhL19K7O+#mL-T;Qu$zuA#lWygh(D3a(ux zPNN-<_RrUbqYb0=$t)VyV+8N&TTmQGxWc50$%~N37 zg#XJg7fSmu=5<@CV&)est4L5q(a2>iGmZp<%ZuS>^%L$#k*)7i$_`ZF-V8=M8RZ%i z+Fg_ASTVRQWu*|(zwV_QJ|`$@u;%r$(d94E4CBi!dCnv2%j+Cb(GIWjw!cUIw^0#E z!SI2cR|9-dt zr3bTO!3K(pGPJgb$9K`Xz8pfL_9(RAsR1$_IpljOH7Za*521Vot zQe$}C`&ak>`2#y%gh+i9FV8ghn}v?LLP3V|rV#udQ~BPbl{zHL(PR8G)_=bLgLbgOJNM*`Ai8!tBqzF9`mdE796(WPB*dH2WXNwP z{Hd2)6jt`%=KUth`Ln^~$1(47DXcz=-TmuV4zwvLY}@MME&8Y->vd&Jf6w=pTngXs z96=?g{10#N&6jz4|Cay0+WR-Jmi?6()&3eEpK|aC%11C!m3)rGU-OrvqlDxf6u$iH zga7-?|6MnvCjb9j4_Y+RLH|RoeO}z|O%N9DP2d*pm6LpRBwDeV~ zkn^_=(c3rwIV~c&w+(!vS#y=PVqNc1vdW>bEz#>WQbQj7;NPnJW8Bib#$pt3QSO&J zB>4@c32!yI07H&CWB3njSN^<3 zK(B7RJRS#l^&+&l1-K5os&9^SX;|DLj;QN+$%7YKfQn~(EY3y*3!(lu>bzijY=V-S} z{Y2`lb7u(*F;4W*HBgy0j{o@T{dbgzo6*u4mW8zq+KHJBLIx72n)JiC7~DZR~fj@BLhQJI5XtY8?BB%{LK_=lp;>i z3tyEBRJD2f+!w$vf4J#$#)6@j3trbD$6lhoU4HA`c+vk@cJHPIEGH=d`Rc=k-ziMT zhUoG2sej%yCW6H;^oMWZcdygym%7C$n_o^sH*jGAs-zdwdfc7a^vF!AFe=FHk1a~s zM9-<|SQ7qJcHUMa0tB1b-$gEBZlk)W1H-QKas4?gA#1s^uGN>|yOe|9QAYJGaM}%l zNs3MI{-IgVK8e($`&b700dHp$1%lN(d4XGjAHiHFD7JKetnLA$oZxJTWuTyjni$~r zUI60cr3r_sY(Mx2cBSwBW55q)H&c~0=Cp~$y&Ey_cRGnD@_Su)kH0MB{MGvQ5A_kb zi?+vvfu`}Q^65J8-#*L4_~uLvsZalNVgGaa<=wd1G=?eSB>7ip&5yaH(Ym71jXP3? zS$<1$%;wM4j}-CDBbI&1I0BY`2t0Y9_TD)eR{Hd6fd=oQ;0YXy0L^PPruhy)uJ|^= zSe%@~Y%=hN5on&?f;BznE^wXvOVC@i44}uo*`4s`hBz{ZVc#20WcA?toGp zKnhHT!Mjak9J#2&O>F2GS?Z8`+&QZ@*9QhuK5>I-s0ad(etYjeFzx~op%>ljr$V| zhki#jcc-biWBKRj=a&lQ<`ql+E=V?9=}#i@$9{OP{qw_!dORBVM|7-ay8%Js_E0hs ze;g97*epk86bqJ}-e%)w38y4dX0Ht$87=6+s?n#px!nNkm2+c1F-M+fwYGIC*e5Xm zOnP&DKru(|{tkSf+wZ{enmKT0ncH8UFDr1dfS8x`t&@EpFl3{uo%bhc#^IjD2i}~ryVK~4_X#Mt zQVVL^PrIxpwPYrkKjNDFkhW7jUe9b^4`1;RiXppWU)`8ym-UIj^88h$)zP^}W^9S| zTb`qKihvsKI(~Qi8q}6f-AA_$9u9}1-IDtSUrm<1&`Q=6YB5T%BvsaO7~QkY>c3si zci3NJd1(#?eNb3xcV1MkOqw;POGGrgcoobk(H2iT&3UXbJXQBPKWw?6m=)fr84%J1 z#On%nJ0$l7vyfe6#9MfsL?lQ1g5x-h{TIC)-FcQ-vPR5_TgpQ&T;xxJ8T6kWls#=A z0#>df-lF6qU6{o`D1M_!L;n8Vw4U*&I^4z`SmC32ZY?m4HRcO&f9LYB(u9mPU++6oGMyeerW5M z(|n8NI}r(CcqPQW`8D+u&HEJfte?q+_%?)hThuK_o7|4SiMRMhP+-ampWDX@=^ItI z#hc~brO7LC+>`oh(ZbhN6Vxbz*$X&`k}9tb5cr+)MiychEcX#|m%OA>mf zATN_URAUY^M$=VdIEEK^MV9peUKn$or?vf~)>dYZb2dR9c+*bQ)B1OoKTGrU0rDL1 zeBHK}70vI!0`*lL$ze+4P zS3vACh8odaY2Xd#wmQl56trZZ*chAIx=pNUZ*x3OsKQzMnY&(h+j>wO@3vWz^4uJ<|Kv+a`r;fu$lb{lNV%SJ|etcTxFh40ZM3DRwPPW09n zCCuvvP()WhFVQ(HFKV0o{E(v=2GsajJPZLO8I7)_!~fg@F(gZ<WK26@0W7z< zg_Is~Sfx;Ux(~}X?-uYfORHwsA;>S*2D1Dc5*=To?@Bq&P;Xom$M~0Zgkcrs!dt?m z?^Mj$7+0>&o(^pS7_E!+T*!D!4Bn~x5XzAu;i>wwOc7FI_SqJ5b#e<@-ha3NN=pCjnqQLbvpba8I{ttcGlLOIY<< z1NYV|Jsd&6ty7^1>T7jig8;RRVz+Sc{@WA+bRs7fT$qrDPOQLc>P}~(cyopVGl{i7 zq-{5RN#IoWY|uM7H;}dx9z8|>0z9fhL4teQY~0f0=qAwy8&QQF-=Ff*w3a`Hvv@{m zch|b?H*+`aH_XQf3p1Nn`330T-@V9DaJzH6d=lze{piY?yblsN!!h5;S7C_^PVET` zJGXJ}Gt(yU$0bwOClZT=e1tAioI3M?lm+@Fy!f(>Y9M;_p%SAXC}HtQ(f60cz)$OK zUmmwUPX0l%kO#CX%zJEm=sdzD+v^e=VrB5^1vmn$j0#VAD$hhQ$P{7DX8A=%&QOin zcz4JoJE?uAsDnp8$lj4}R!Qh{cQQwR2Q!~AqHUn%U~M>8#c#ZB%VFMa4z8_gRd;AQ z*7gw+%qTR&R)6F+>^UD2;88?FdfJoib;k|BY@Lj>S7XS(NkK)c7IUg}oOI9~LS1DX zSHL@!bU@zR9^5Ya4}_~rs&5IOZrII-(VkcJmfH?(o=wdit3xcOEV>QhamU-8Q?q+3 zg}QrGGX?I4B+E+1iM(_!kdAqW2I3fQb}{cpTP*vRg@^tgvxU=guLK&Y5$2vdQ%|%5 z_>jpY5i?BSrUpO0Pi$~7#8kFi1R=p)6nR%d3mM+E+wES^eS^z=jG#mA3;0-A9VNF)3RKDT) z5-~rEM;Eh1#YIsSI7X0Udwdh%9Ixz^_J64w>Asmq!Zl|;T)5e45e?ryDGr;h*8tQ? zsUXhdboQOf>&w96miRA?Ir!19#21)#(_839vvA9%%%Eoqa|@nrg$jRyf@(b*nH`{1 z0O7V&!~HRBm(86nGCYX^1(xY*iV)Gg2VJ1V^ZkMY1X|2un-d?fM(ah@M@_PnL zet2qt8+JMHIo)5_e|Z-8qbHBzX~St`#%<^IE~m_AFnqmc>aNR?}yd+ zIl7|23N7x*hkMqWinFxB!*d-yp=4so!x75nS&6Q$%8L8s9jvEIO%yl@=EXLB)$1Pt z7M0&H+6%f?n?Be0+3LB3=8gzjrTpUVZo&jc?P(?5XdU>eFep(+S{-*=iCeK6y5-hF z5xzfXvpPdKWsFsnF?T`X>g9ZjsO7|_5MpG=KNS*Q`;G`$GMlGZ)lqahPdM@rv(3XB z(aK`|G+7Q){3rHdV#ZJYsJcz8vF!qLYtDtOYxMZ)w)p!ExUX^*BeRF2^XrOKZ0vo*f-+QtiFy&^u@c_qn}@7}#QLn&wU!uB;gNL)Fku z)Q~ll5L#F3e^g8t6OcS(`=ZjlyP-MFjsnM~w!jgjC$$v9QdjF9RN&gyGfebq`J-WP z>lok3g@br6m%atvEAx#Ai5xiib-DVNo8e(4#2of(8JEd4Pbq^{JV zPrh4bMS~~mQoD6A>UVAf#M-L?6z5B;IAUPu*#ygaYEqk#ismGzQM{1iYAx3Z!Y;~l zaZc>Ag>o-pMDtFAI7LD^ZqLzX7%aD-ic8Q-HdR@m!)`#B!gK$WAR^23B6A+q*6omN9!`@<1i2Jox}iw3RzIhAqMv|R@Flb3KAC-Ag+lqwK z)@bjq5ae6?YDW`M`+-SH5ya7C{d@#&(om!ST;O7^w*I2j6egxe7Fw*(rd z5+ho?7eQKd13ahG&pX{t)+XkSY0vmbPg}MFin@HYaNJ3Sij8Rye$-PuXdiS{RUKKMyDkMOK4 zCETm1_cUOCy;QwCtc*v9U*kY-w;|i3L(6a8U;)|gJM{;bPd~{kUyt1xYzgy+T2-^!s8mw&p+bBbL z6Ck5QiVQ$WhEx8stpfT8AH?9)nwZ+p@B?V)@@EJ^oYjUTvVosXBUm+V%h7I(5h7^Vu!SYI(sYhA>Lv1`RtHAv}E12 z-e5dNkvIc*X{fXQiG5o&1L3_7PkplYv-~5r@;7dcCdX9Fs89K`)GZCNTG|`-bO(fI zF^r^0_Z6$bk9S=q6;3m(mMa<)nGzejn3{{4Vvg4^1;;i%~=*Xw>XutE4 zC8p7*?yyNMUW*8D7pR4jS@jS}Ey_^q%WM`8=fjJlT%&r+TpF8n} z@)P-j8CioDJuGT}`gb*?k_50&z`vAcsBPf7c;36+wz;R^Zrv5+#Ye4kANO>@INWTi zUX#eT4Typ$^S*tyI&2U}JY#CuIw2XhDbcfjkmbD9$2CLM@6Y2d>KUA>T+XZm>PvR- zHd%Hnp1%bRXu&&u!RM$}E)-f;J{&p~(AG7~KEMmxHrd?{cdGzg@C+E&6mz|7(#cDn zwWWs05WVs7ZTH1o!Woqc;J{N=y6NA`JWqcT%eZYo16;*~UNxrI37r!aO2;)4en)cz zdFlEGEMjJCKHOJe$y4|AU3TZCg|$%8N@a1y!d6< zrp~f*-alAoL2!l}KJXH_So`0AUR3@t`{}K@a%5&fj4LGu?oP>!lVi{+(wRMi?kw5K z`k__`QtC|a?H6gNdIrndY!1-ja(V7j$3)@5PwI3?E>^*(Q8D`*Y@gs~t3rgKLMxS} zJfYMM2vodG)S%UhT#)dwK8Erq^@$V6;W?8;&v!;h@N!A`n0(Jkg}X}_K*c82khL9E z`qFjIZB_|3w+Q*N&ymbm>0SrK(QdPcN5M_|SyZUYI$kX@6Vw?~t7722V1pH<0(Y0a zgBg{`0$4oM@&`10&RA_=0i-t9Z37OkAjR(h{YF`fBG#G`rJrK_Im<6gxt4=li!TD6`HtY@#lX0;XacSRyQQc;71ldHUR{p!+jOg z$^_;!w$RPxnR1}@*B|drplqnS*u75|7hzuX+xnSQKq@ynLFURB+J`o77>82~L8W(p zuAOs^nvppGyh#2K8I}vmnf_spuEj;DOje?8H@g>9$Esnyc=*Cq-9euH_qGBu+gzH^ z=sOT8bjIbT#Wr%%S$-NYFBTA>+f~x!Y1gq$k6YAINfy2ppo49euLu_`EMO|5#|1H- z!9fk_ixyyk?|a#Hqf=$wwui&}Ald2)*edV>^I^gO4Y$p9u#>(=`IKWqI&`6K&2t#! zUaW_3#R5tNmURoI=A#DKKh6#oLV7G8R|<5v869!xwtvoOz>gF1FoLV^9d@oPTBE>C z9E-g?*g^=4 zKd;^DC(iL7XFHYCv<#4&psglYOvA~t_o#ihezg|%gM3ut*Y8j4h7Ja44ttwE)3ulDXDW=RL&P`2+774 zzAdkUC3?SS2++MMv*3U5@QEui8m$Xl$768dUN@YFjJA)>*ls0OSx}f~%qXvGpEnY= zv(+sqbT1sU(x7b8v%>tk@pkI!`XB4QaT(@G_R|;_a`E6{yi+!&n#~@#T@bCl96`f@ z*XP3102$iP4b3K1Fk2rnLFUvqE6mg@t=9mwg4?7d15=I0Nvvkl0s*0hNw|4 zuH7rOXKWB5wXSy!UX(pFe#Wk6knQmD(1oa8&TVVmLfxav_5E^FHF?w6dkKD7hC7Me z@NL}>LnyS4uX;gmWBoJ=CU!=3qwYpM?i2Z>{uA0C=j6Aw)ZpGZCK(E^&QB1yYn4s}QkGJ~`EbWAnR_c~&Y)_ggGTm7Z|1Rw#n^Dk(7vJJ;p{%xbxiH`c$@2KL1*!L ze%Rdkn7nStG>>{u&eZRpQ8Ty4d&^?`+w;}O*iP&`F3tdVMeTU2qG5JqUi`}=rCA}z zZ<;O#zxN1;vZzsqS!B-!JbA7j|{?!}3YM4@&c$nMt4Jq<_l#6-8fs z%yR~|;=U?$T=9H*(rY1rz8TzvS*Ayp)thk7h$U8Qc)5uCkY$ouz9w$&pH6m@}?qDSCnuSG2BqHNWk{{n4#2Iy6_Zn)2;uqdy}|JhtpKJgf0dhxd0` z8U`VKqwGqT;6L!?QfA+~HGSpb4u7NDraa4glSd9aZ*>nxN*J-vrYyIw?vxQX1#%?` zX6bk^X;Jp6rBS3=mF>3p!AIbcB+mJ2$ZDX-tFf8Fro#;KCS8p_97cN5|glExM<}#p-#itoAXf(bfaD);13M zOg_@q=!}7V=Ds1)^C~84E78>(UhSFtlEev|n>L@na9&fjeOpLs-IGdUIOxyGY`QpL ztCpjfQtH50SX5LwD17Ih9|H@S|B^H1iE=wGJZIQV+A#ITRja0y#p}#++M0r>>1s>> zJ=y{{;uEJ^vYi**n4NyLObJ2@1$7qFFsCMXLv}rL{reS4H_4>~gX!7DO(!GUFrLGiK-@}%T{|X2F`*dy4g+3_mB4J5kfdQyc6W;MtJKxu=CVgm^uOGy zt=n&p6sIeT7f+kF%LB=nHWwO>5i;>z^XVk-eMxaqoZ+J%jHCIH5x71ahwpG<-ko8Y zjA@ycA7=?7CBB&4wfa@yjFTa)B;G!GRG8^N7ou#%*Q9NFVc*be-1=a&Dxm%1@2t%I zJ*^Ox+w_IoZ4RGyD3H!db;i@amC2OPCwc8eJi@R!NJX>~?J#J4{}}T5W`E4saRf zKN#^A$J-zEbs|zXNSK{ji6x%8TDEP(I@!_Q-aX-Q2YWmvg6MYb!0z&uVU;1~+@I`> zSC6zQ=n_^Xi^ZuZF3NA-s_Ypw&kLFNACBdWBx8Z#E1M5)L?zHWuMo&(80bF@x7}nL zZOm%M!VEBeze9akLFF#N!}>dab>joE>|V#jLT)&r)u2|wYHCKc$~FEh#-TAU!T}}6 zrkv4Qo&#H3j>o_=Y{fGTdmwF8J2P6hu(tlms+cGBJreCQAIdKS#@VVmD*0VD3dV<+ z4_Y?e;@)4b90jwslX|tn5dCXYA7?Q^{q$-l-^^;b^&sBUA)A20IZ%Uc;eh8E=Bngt z_vMuI=X)T;%9cK4!^VtnDiisXj(&c0P*9K0z%`$%SZyz2BM-Yq63OrRtg#!|^w{d= zj2TJ!@j_(5g!Q0NaCB18I(MU6g+Y&V<@s@)MAm7TFq&l7P27Vu-=>7x{Wc@3H(kJ( z#hj7#J73cM8C=+<63uRLhG`%g`Mc&FTR7nh8Tg>e4jZW}{^(@`ytTNY(_n1j;%ou`8qV*z`CuKe94rciCT6-a~*Wc?&nqp}Nf&I2yVE6(67WvCijoD>KWQ zSL4Zc1X66B}_+FJdsk`>2LVu@AT5cwqxUA0jPQ zsT160CFX+CdLBC*7^nz74-SIln#8$H--z`5yi`MQMU;ne#_P6gR8JvwUk$ap*D&Yo zPYEt=d8-mr#o9`o1Ba$|8Vz#1$_hiyw0uX0*%6i-MR^knby~G`L*tc#*t|hYulO#K z+1a5BU1N_C-DnTfun7S^W3h+hN){fXNc~F^@b2ky2MkZ$)htxyqup)~Le0s9ZK*0u zhQde1drC{c2w0ep2G*M0kc9>2y{eTQt3EwF*|(useTn3Un-0KVxtUrxo9vas4vHQX zQ7W8IzUK|u;Y~Kt(kd6T?oW1<1@a7N?k%QQVyoqaEC$orQMKfv@cLVUp}$r~%iG1Z zm5y0UChzC=d#uQ?Qheqksea+Fyd5u};Ne=FULHPDCit~ii#9sSe+L0;LS_q!SFqAB zP3#+-1RK5u%^C+VkZ?Q5*(?ghWd+BYyDbl981}}+f594(g%lBsaD z6uLZg@dr3JA9Z4&=jw|Fp0*Xy(WmbGApe&dspO@9w0)M=nI#lkJH1_ zsvECTJerqQdzWLvd@ymEZDU(Pb{6S|@sM$A)Lai)khjL860I`FN@D*JeSIcuDZ}u` z_M7vu6ygF3h%?|rCz$BCzdjt0A^0Y%Pd{a2 zc5lYOAHF!A!XkNXkiKBq1dC;1X?#)~zvzwAquchhRw*CE34Ju)SGUj;`N+o=T)};@ z6hbv_T`lVns>;~~x^|w=WAwELpsiUZMSvoTXayC{j+_DsZw-OJY1UZT0#{D97hSD# z#?&3XeEcOCiGk6Nw{9~dn+6Y59y1mjYI~{hMjlo@8;!Z*z~ojwlt6Ca~O!JCLcJV%w}qvb=OZ zf^OcpmlQ|FRm&RW@r24%vN)B5*j3cB(~KI{r8(VqB1?B;?lJzH75?1WUZA1d)&Gqt zS97s>t#;XTecIAKIdqjBg?BEH`c#pkXBU@K@vPLP3%2p4+p&Wlns+9SQ!vqwd6s(% zc^|iKrzbaO?F+f~-3v6M>A-z-GbU%?j)%MHCvAVa^6;L)<;1i*c-V(kKbX65 zYwJR}9qqQK`MHTUdo+CSI79Z<+r>TB7+85oji9Eeqeud()Uv7trs&_lj$wbZ=3u znQQAieXhti4b+#{NUtlG{7_cgvb4s*d7xoD$hh`vd3u0)bY;iaqt@Rv|FQ-VmxD!* z`Uc%|k_S$;ru`nIH4C`vUyxo0-*KVw+v1gcj-$lBh)Um)NVqyvL2GRD4A?mm^u_(cI3Du z9s$NThr%bu51qHi$=bS0_vXXgJzA|m{^Hu&RI@ltTQq``&DN}wZZyI^m>ZkRd5F|{ zfN_7jhfMYZoxh4n1eEG(Rq!iD39mFmg@F>*Fw&N-C^cQ*(RG)Io*$q$vqczWG#H`` zPwJEC*%8orChIC$AM}`)J{V}@mR_iy@QQz93J-H{CC|M!a1v-S;p|r z_t}CWgk5o-mnv#yA4?ieTJDSc;I#I>aDh6))p$dRKNlXp3PkqzZJ>R~=-l0E!8=uX zSg100O3YYSJ?~c{dvz|uHk_pA?`%!+9IzR=b?lY&SvRbV*LskV?!~-msl}NM@g7jy zBduumIg%e-Y2BSf#Xd{m2f5(Gj4U~nuATH(N{EwlC+}5~?Y{98U-MR)P$#I;P5^Ns z*Zk4wK|5p4m`1UvFk0sFoBsFx?g3)l)Kxz=!@?=WK!t{F+Akfn^i4bU&_*mA_pL&& zV1mxJ?cYTf$0oEz4+YtE2Y6@m8i>32S&#PDD&JFu6M|{y)$Z4d<}-!2%lCM2H=PED zW(Z$lu`G@RRk~wgF4@#_R5#G@C*8d8 zU|d3wJoika%j?OcnC;gOB4IIB^klesLV^}H6!YJ|7*yuL07&X)oQ@Aa#;^QOC-C7V z8PK{NMU%vYjKEt0L0c^~Ju$NL`y_%o`(^#XBkQ3yd|1jyaQ%mhYVtT<|K~fT^xvvw zJe)7)JDf(YV1e*^>H4XY(0Q*!4)ufq+ipRZa#aiw+v0UUpC9ZAss`z?;U0hXf#cEp zk?G83B)RU58n~$xV*n;qbwNvZR!bi$ymYiK|D)@|u$0H&J>RS-b2t?q-^UaYp~x@4 zL0cU+!0Ot9{pcDQ($M+q5}_&@#cub*c2uO%)k>zrO$|c3Jea z#aaW0nu`rHLf4;pKSd=)K_f1Mp9v!9h?}DY zkH8QsJAM2w^hrE~_mDCkMWqx5J&U4AL1Gzn$ImXsxOOT)+R$?qMlfIC{nea3tpeYU zg3S37!i=PyAI=SDu@M1k_Q{ev;V8XApI0~Cz-(;Mk^<Vk=WZ=zveRijn>lrM$CpQ1}FFfD(4; zZ^{saZbmdOLhj1{IRsSoefdpLF_#RG-MDorm9<(%5t(DjE`nam%1o%P+1bBER(w6u zCI22wUuy&0o4+yi7hTBxZcbB=c!g=+?bfgJC}N#S{mcYz|H1L`eqNE~a+6*fEFegW);4|97S5R=vzee;Ps8aE~SD?6l=em#jgaXb@OGs z36j1GAX^WBUWe&QtAwhiC{!{1MQ~Iibj`c61DeJ+Y^te`o>XU-lOfsaE}T$Rt6g8C zCRPdC`;n83Ou|cW6Z<85*I$%873*6vs=8_h@#=pg^HkD_(9v^=M&$VZk!|GnBou*( zGF|!@`oGIP!NoOXF3<4#BT1*we#-}!0>*}jng5c+Get^H-lOlLf%o^hjztV=iiQfS zE@9C>wS}ja3JF3B6q??7p_b+^UORr|0(=ZO@?Q2P9e-UXIs*X?RN6%!>Hj@yH6E@Z z-quj!-?DV_E$Ah1)Jr5tVfsIM&p$jyr>A6v*+ugXTlncyKXei-#$$Mt=_6A62nQM{>-gH9d^7L7e9_$}`e=QMO zvP=U>IV=W!M!Ie!Uyq~U;I-ef&h7OK4B^cl$~!xgoCSk_d#O;>7Bry}+z>0?52Moi z`yn_PnLG35YPp=#L6*-^jOf9x?O5RURi=s52B`1_iOJi3;Fhiw`)gm|wCkemnC4{e z(58DW?PPAtKPsc&GbDtB1>q=EhkHVaK`GY6Fq3>dChG zHW?K?I1jLG3c%A7&YUL}A1Gc8lbs=!mzAC28l=B0U+GQ>!xltAY)6R3nJC2F;(@G* z{o|`5?)e>kkUi*4Eh>T>6!&!Iu-2uyccCgyzJV0ST{F4m4m zbYNy@s-~7EA>ih>WEfpwnk08qB>EJ?S^tzA`U9r%| zJF9dBWA(4|@uq^UU>}4*NMXnDNb^Spg%P=ZnRud3l`u(>@o|>azF>J$ z-ihb`674pMHvxkf4^K^(@=6d+KA;({DKdMr1YBXu@0n0YDosv({d2_Y&(atV1$hX4@?`(fZ4La9hFIYX!L@dPy@2X=ZBM%8LS}x4R3gy zC3v)we`52|SxV&8M-IJ7_|#BNE<2IKmQJWf|4y)#cq2=MXC&9S&g)Sb^VHl$5pY}C ze@~fIKS)8xlH$@K1znE+6Gyolx|3zC7QaKhQ~tRC46afWay!_xL3B+EbR_mghfEXnOp(-fVv4B@&@>^DPzcm1X)}Js0{_GYZ`;*)*8m?y< z9b6e4T=v&;z}$iXqN=b_DLM!)*-UqT;E>9c9N@(;-$rK423S3jlV z^-q289iYq`NA)n+?t0MMRB%bJysM)1YIZ!7{|c;^#bf)93P3S&=JZchh`-h_Nqv+| z+iS?C4!%C!FhO9+zkIrbIyTjon!5O8Alb=Gozo28jBZ$dUi2FmDtl_#V4KN>IMbZM zqJa2tJ+j|?leNjYwFpbRfA9%rR8)H4fEQP9KkrLK#gD4|<$!+bgqzqsYcxE{m&**>oc4Zs=d&VlDhperW za%2>JB8}W&TJ(=eX<8UHz?jqJxFj&FOT!>Y!9cI3H%jG*KI{*BT|{xi_vG>(vuVl5 zd$q~-)ZqK8X|EI#__G_+eyS>}2XrYgUSwciA$ee$@K?-_h{^yVv%TL>|oJ3a0@5fHGZjkV7|L4THE#HP+Qt_NIhvW>PwXrn3JE9+;T9 zJD@;B#s0M8X9Zo9jPT&gAX0KTHhL}%%pj-&(e`bHaRWJr<8Qf$#;l@$pJDp4>k>7Y z{^XRm6X`7fw=%Ro?w8kz+AO`!!T#84mQ#dCiaRFMk zovbX>zQSr5>J~4SY$h;#Drbxp_J7#Eg$4z5(kZl&GS2vaHpuCu*^m`SxpcRs>xT>~ zgt?ivq!vD2)W#o6j}1XQu_Nas{fFJb90*V7yzLWW%YFmqFQZzI*2%0?*Gg#@_LB8+xVcz3x}_P=*_g3T-9Ezm@5_X z?3yB#h@L6d18)_(dt)Oj?5A1ZGrWUDBAE6WH!9Ob%ILiQ9nN3c$O*T?*KJTu*<~=h zVL)fA{z8_JSwGIIG&1$$-#iVDq`I9xs$i6}!)0Y#LI56t6zzY=)n4QH)3pwH9|&4JLlRlF#@*v+!E{QFfXOEf(%Q;@jsxU8VIxoztq zm9-zM)_4BlF+u@&k3&qL(M}?V-!C5}3wW+UhBRhGTz5vb%z^^NKl^}U4r*p|e(#mR z*{!akd(4zA1n^v(+{{NQ>k$WhOPE29e|$+~VhQ>QHP{^%6zS+K#cz2R*fr1~%2zti zuwDL|o&5}}CwxFiaxv2UN^P&a7Xwlct(dr;-K+i!f+rvqrZ3aD`PWCM`71Y&4#XRK znuz-Esz?~JK$1+m+EDzQ0nUDrKnE$eyT-0Ue{x`FD&)V7j>-2Q>X7X8A2a;_$qYNB zQu$}I0R9u+|L@dMW!T`4Kf3ZRt-=7FvSKzX?cyv(r8_oOrALr5cyGfGFhlA@Yd#VH zuswTgbvjPqnHB z?0QreIxF)tQrtLq~wJ$o!K2YHz@e*x@Z%eeDKW? z^*2x6BhK}z#z&J$y=wIVS(QpS;+R{aUxubIzp{^i!ByFamzGtgp3fMpQ415Le7C>? zV=*i}%(n}pKKFX2&YT+KK7NiVD7KvMf%|DCTysrfr_I2(A;c44Qxfrkpo;n26KF{W?d zk)a~I5G6~8hZKxdxB=c!9aQ}C9k}aDm8Mo#2H~wwE99Vg(4q~dJmvIaS+bp2DJY_R z4O8WsH5FY(YSCQlo1LaD4;}whH;7%~QO1m#LffEeiK&LHh4k0RQ!=z<0x|@bSkjy( z8J^tmyi_#2HZ3_?V;}#{JGF$0lWVK@WmuY@r7y)G85#`oZbLG%aL+#A|3Uy_K)J zu-Vc08DUYq(8s4)K4*^Nw=2M2<2>>@-EQn^)y2^x``XIUbzu|im87=|kD9`did9oc zjbjZ?4Y`b`i>iTJItTR$gSUJr9|J?A8b)u24Bjwyexv`m2;ZsSwwaFBfmHgeC@&!e zwt2Pm!R|B?nc0@iqj=W!C1m&pAmFFmTVFyf_@11HDeIyD3oI=5rypFReWbCu|E4dF z#cl1-vGziwUiF>BUNna9Js7NK6Lr720ff^ztVx|r;m(dJkFHi4T@72rDdSlRU0xs1 z>0h}4Nu^d!JFZD1eX4IJfG0`|dc9A*r3>&nNga0{r8i3K(ESS}rG6eCE{!1vGM-?0 zml9;%!oo^UK7c#j`h2usv78s%F#7%|KbOq6kI3T&XxVpsEqqFb&tQ4=A%dKfvLNaG zH?plhA1m`Yo3lI3}p{d2K(le;oryZe6;%@If z9SW{1^R0;EUCNTp!~L=Ol!_0%Yhi;Wba~zCDRd?&-luC`7KElV3$%!xs4{k?k=`j4 zbmxNYnm09CEY|FN)f>JfQ4n=BPqZ_hzX?u3nQnM`taZ_mC@O9D$Vgjh&r8)|G-xtP zwsG|+!sm`=2|ljjMrghJsaUC?rk2coTCU_#A^Odg_l=<_?|@sQ77SxKq&%Jj2s&qk&{A_oV#sAef~yiQ92%HxPHkslH) zwYHZ?ajfZ|Go(56H+5n0dSK5?c0D~;mWkjzf2Qp3(!M#;Xs`4YFX0r4V9#o3p6N~ur&ODyLYd4$)JqK|`OK$e)R)m)Hx5%7h%o3SL zlt)f@16p$oqv~0&&aW?I5nq{EIGOF82-zIuk8mq1{V=N@Yx^HA;mX;UR zIWqncXeA)f)*SXHkY6P5L}VEkTMM<_n@z>E_hgbWoc*3L|X ztK=@!sCizwXSEvQVsOSZ&j9E+p4cZ1iMy~7q*+_hQM|qQdH;no89PbF5phKqcm;-% zOVO)1EE4BAFlcTuwIfR6VQNt}?c#D&(KP+yB6hV_l-qMS5#H>gkXzv^c$?FjHp}2Z z-IoUeBAUjBQ?`Bf=1k!7iHX^LlV7&tO+u!5_S3$VtI^CG%WLn~#*JM``9iyrP%fdM zle=6%{(!zkLCw8^s%edi3%zg=wHW_%$vi;;c>N6@zoD(tFS3DGc6oZZ)>(yPqgiC) zerXBxK1Eno`@YaikSj!J7f+tTg_1K~M1j5E$zVBII08dg`8zD~C~sPdCrvmq7i*%? zX<;nlFxTYhuI;`n*<`JJL&>5-S59}V3`LS+aMg;u^(dR#>1xZ0F6F^fu}?ftAap>f zyQa^aEdPq0E6(EX#VDwegcM;moT$IP%T!!RYO0D9=#G9&}~ zg4#{xeBK{}41P?2C>mC<=x_G?csD2HZo+iNtO=1FDKgyj_~AEpRtY30_P#WX_3=c6 zTXeo`LVKEMeiK3{+t~9il~V4zz>o5>ru3Wt)}`mzFW@V)F~&|tipMs5*8bm`nvZGiPTSk& zXV;O70=AUJc(56yoBCih_Vfg)P@1<2_uZ{N{kBZ0vb?t@V;pF&azvTxINm#B2gz_O zZl{PL74Prdd>nUezT*W3-iUp*NWq=;OQ$&4XMn0U9CiCWKHgyY6LPtQu#Uh0`?3m?Xv)Ox}0wkPXl2>d^)|np>kGL+&^*fAl29wG!&VA2uM6w;%KVE!oHqf1Zc||0J*K@X zzJRnuifq_!G~3rP>VPRk;H%*#FP-lE&6%OhR0~7@zRrsqHg*FRw?v7C-*@7+KWk<@ zBS{D*ZcqIr!RK~fB)kG?(B!uYcEM9vv*he}twz1+mLZ%wqo{dB6xoi$n;&0u+X?au z&zQEqa^End=EEcqsf}WzJY#vk6tsb|y{1l*uOufQ4PA|8O0;Z!uM}s6w-b9m^% zxmWj~H)D!rNsQAyIJ8?-JT=LJG>?DIjxjO)>e)grE-)ak#??J{j_C|1M16g`L;rP( z|Fwl7PIqNDlhq&jy-Y@eiaNM;nEgO{>U&QS9orog-0KBB8ZP@AXs01;UUC815(cfcLSEf;OTg0qJ%5{r%90 zLOkMg5n<2r^s7(SwZHsStmiHBV)$Vp^shQ%g2xOfgKEjTviKarH5pD70pQoqsOW~( zRaGxf49N?80ZR#C`^RRw1W9u7FD86$FNSAmhC6VZmX6*@F22PYqXJ8kF8CfI+J^rJ z=b}K*W4#MISt1`#fL|iT(pBF~(SEBSY;%$4A%pWuK)>X=#h#M=mVIADf6Z&t{r#QQ z`WE9AigjyyZbRSWqktcpS>SAjrMq7_kgG#0P-f8e^%9zI_dLUxXr3xOB0G4*FJs*E z+m469x+iDLC8Vjq7=l;J(_P3N)R1u_dd|BN`veSf7Z+i`3iLm~do7Ly^Bc7At-=Av z)8HYZ+pUayM zoZ=3j0P8F+AAE>2oA}wPdFS4DBd&-$EMF!WYORBxqpeb{;bwn4Lq0j~J;3Rxt|})Y z?)kv)*SFx!x&0rcO5&y*`qe(%ge)9wdU2Dbf@?B$>(vI5c818+c|pcAeEkL21zsZ&iuAYd(#qMWnZmd2}LpBFt8QKatr;Y;SjS*w{I=*?b(#Wbl~ zTw+W&r@9m6Ub=7Tg?mYk68^#2U9i~mN85NOhYx@jx+?S~wEG&|=flK%WZ9gt=v(Qm z@Z)PF>5}*ssdu+wFc5%Sq|(8S(F=1x#n#a{b46tZ(knlZZ)&Ii!?~c@m+lH-@(Iuq za1nHU<|HNg!o1|E-M1eiOP*^p>{V|#B@K)YW;~vlOR6J%6%W?)m)^}acsK~_5|vrY zJ`qDC4so9Wvx-Wx_#CMae;OlKo55>oRWQEkzjV0pt$4%ow@(*_qbAqt&=!;FVtoru zvs1TK&VeSL=(O_>KOcVwpGFZuiQ`$L7DxM5f z41QQJ>w+N$C60lOdcvLMz24QSPxm7%p7pnGW)ErxL)^Clfn)H0i2TfDBk}&o8-io5 zvBH?|cNXA|WL3dFni2J*xL)19m-`82XRSbA{UA7ewJ$_cX(8Y$aoKXOru@;I7+Z`= z|GT$*L@oA5Me!9{<@euGE)^Xn95ha)~rz>7j^=SIhV zp!Fdt-RibTzv&7^SVywvMuzKz+GX{|ZV#+8hJ$OOPq5iM>@~Pf5!3(6=+42?3`s^j z`E(g5O*fD^@82yuIxHe#w~(NbN}MQuymjKDsN(h%8bpdj2&mCSZmAVie_&i z9g|~g9~4R}J+<1gk?{U<-oq32jTKg%`b7#S@kPVL_Xqd!1WFxt5Y9W_yX!b>593ly zNC@Ck7?0E;L472cNcJudLBSIu2vOsLU4Dt%q?oB(`XyJ+DYT3|X{s$=%~G#F(S1(C z;i+20oFkL%71ENpqqlL-zl^;`I8gVd8|4`e7BI+#w?)c+?F;xOBUH^sc*gD{w7}=- zb+4hC;-Q3`srFm2j+9yUykN1W zz4h9H>vl~_?=hxlJqW@(IxWi?TgNkb&Omc<0g9hahj{VCNhME)GR0HR>f0O3KL1)~ z=IX#>F5hvO`D`DB^p|`y9gRvA zHcG6M8ZQkfHq@-!Z4dbP+x~Kfn1q4Af*1AKxgy1P#2o%2fjVL!CGHl@$l|2!kk?1} zj30MGL(gI)Pkzk0!SF+kJ^tD=n*cY&f3x+AEi8`pH0Dyo$PbT~i&LzFs@t)6v<@h! zh=eX_6XaUnZ7I{xU`ui4FzB@);K-mpKQy!+7~i-Lp_h(zq-F~Ad;2Z%nL!~hJnCsL zz0p}wCZ|md*}ZgALln9>*#Kej#YOgEc|$}N-uV(PAjO+oED?)!LK1ac&nKc8AR1~* z5j`MOs4J+G<)Leo|3N2?N)tqPw>zR2CWH@}7qox!_FU`xfg#ecI@37mlO^t9^dwL( z4iu@>2h-x4lI?{;R;NyZlCNy+I3dxY%Q^Ro& z_iH?%*5mO(jfCqROcf90cATd+1AykD=)ovDwxGwt7yF#tga*5U@f4%wFt}*{)=#SO zDZBoMh&sH4&A^K?6*d^>+qm!mbnT3@w+f#?YKFf5$Xwp1e?UM{b)tpd?6q(3dzKfN zC%lNO1;u`gOfx%}C9qb3O$Tbvc95SNef5y~&jW|Z@FmiKPyFN{ z=;)0KyWaDC_7|{xQGrdOo5EUrXuEc!bJa6Y`J}0P)^v!#5=TqCYvy=F}zBp7J7Z~PxVjKPPp`V_kP1LvBT^|tg%T)1r zX*NSW7^ez%xSCtF_y@X~8{jzfwmzqh9|{U;-i`7ss|DbyE86NaP%EZG)v?Y(ckmcg z0gxpwDAM%=<+x=Rejv8z3v(0icSm%}0wyC=xis(ghYuK0b{@x5S)iWSSX4z!R99Hx zc+ZTm;!>m$$F;>aDf-)8;ooF(m27?yoPNm+fsB6a_1GAZG=UplC(p<058}>Jffc9< zP*M9uepSeLpK;Ewemj6&nU08Bc~3|fje$GhC4vkynTo9q>cUvWC$I|6)&{Cj7JS{o zvS6C_1a-f_ksF_~gmptOhWE7xhkd z9B-E?OCIJ5V_$eP0fG~dROsEiFw%H#gJ=N*1yK-$w!b*`0l49Y*Nl}+^~KquKqwZ; zV6v(7<`>~9>TrKPM_NgnRyM6;py#ju3?7j=n8ibKf>0K_bmr8US^OXJKUej>a`EH{ z1&ZW(SMuB~^k@hoTKI%Q9{M+=JxK64C#5O~HM9supPqhOf}G`Yo<-rPbi!8fv+z)X zp<-LJQR(r(KlqCVJnThTb#SuV#0~hstpWYv{P+)qeSAb6i6eELy_x1|QnZp40lB(o z3<7kmC5onuXgD+f3Gb6Hp!^u=a`E}tJ``fZ{w*f^$+LYW0<0C9L5P9}<;omX9WhnZ8qKLEQ8{SPZh0V2G5--DzVAi4>v&mBDZBHJQbxg5cGTDkk} z_^GCbHgmrmYxZ`nbsr#e1R&+008iUa__afwFP*E&xYIAF6ldLkIr@HX@xwCGZ6)Oc zGcOP_9cKJ~YBbENI`{c<+4+GFae<2QoqNG>%rg%3>Dp`9JJY~(+vupi8eW&j!+>{? zfOM!Qsut;Dfn7JvneiCu@=I-+7=SNuH96%VW-^9sA*5fxusg znU3t-iIq1$D%l!Xe%@d5xk(zX#bCg)lr3vYe8#X)@=Bmlw6@Wr?T@K-i1#_eCVb#% zGcVI%2_73>HFR)gvOt^)4Mnj#a%}tA5+B{Lzf<5;;eiRr}RLnUQ88ure;m! zWDQMI8zJL;EM{Ew)HmP461I%)Q*G(vZFLAy8HTHGgT$O>#aBGBFw|LD;~sp0F_l7_ zPoDjzRDX#78#vhYUk#p#AB%z+ygZWM=cmVTa|Q}EPu!&8qVpWA73%YQn8uGMGKD)9 zlHVC4T>^C`R%pj5q2))QkK<$oH)KVMe6a2Knkpoy+R%^!g`J5k)C_4ByFjODL#?6v}Vqwa$bmy0!;O;&fO zXPEjzou=ar`*Qh-I=@cSKUn`JgdQ4@V!3kB5{;HaQSN zov?0<_uXqpC({*{lM;x`69zysa#43&k69I>@tI;H^XYx;D%aVy*Da8qg?0IGgdBbb{&kTsB4O=_!jnl!b7C&0cZXTzKj#aAwh`bO_zMZ&|@Bs?& zuIpE}dts52%aQy{i&b~*tiN(PR6j~miu*cOYKu2OqZIA=b+D{teXd0S>A@6|wOX|( zDo7H1|0G-b^`61g1o|?!XGFMw0$!8U0EJzfZZgo@VcNRWlzTH>i78F|=e2ZR=d~q{oA@Thn&yH!T!+Pac z-LM)*=e0uV@zBiMTNnS%T-^l<>gr0yW268=pJFyD9ePhs-z^KgYA*BO7MM$T4a<0D z;Uch5-geRtRED-+zSSdctv9*i6k-xj7o1mmC=b#}ghp%c7Sp5)M4sQO2BadC=<^K-AcVJ?qIzsud3rOC8GR zisG&

%or!g3~aKa==7G;8h;NY?%Y9Z1}k&o~kJEz@}1beCDn6gGfHXXqc7EJuQ zq+B#i&YdqI)JNBLY*z{cZ_B1gD1>$-`Dc!slAnkUSQB1_R0YNYC(fz-!$(G=L@E&* zjTCd7R}CrVCM-3(62_VXDEzxVOJT?sP@*!GvZ=El2@d#+D)#jY&gk#3LeeX>N z(d+ffMy=az#MXm^9FFf5E$x=_W%(_vvwQNC%d`?4*!18Ih7=y+{6GTjb5kRIc+Ex5 z*Mp-1Uz)lB_GTYiaJNm27ZI}AL=q)tw zxN-;;pql%}ZJ@HUo@r6PIKY~j>lNmG$7`)%Ho`_DgK$N4k|3Dz9O*fh-P}7h^{?XJ zk23cJ|7t7IQo22qV98!oo^j?jT|^zbMV^jFMN)JU2qm~sK8@5wbWBYFliDV2@vh6d z9Spi1*RDI{b&gFTuJwUsy}n&wXoEv7<&yWJR$}|Sg0=kV!qS+7MaDGEF^}z{JvAJp z$O=vscjmt_{fr$2Mx3{R{h`IKR$=Cmq}T7x!o}~UuQNp6i_F|O_~Y}vp$3vbK^S(D z?aM3Q4?2u+Pn2yYvIx%1)NZ%B&0+N3VfkP_ko0lzB|V8bm5^&e$U;OWTmQMCN7qaQ zD(+gzBa7pxp8PQya3{jQkeauq_*Y{T_4FOYvn!d`^))fo)JHMzCb%sU3#{Jx@lB@x zC9}W7gHZlIdOjB{q>0(ieSD)V#K$~$^a^yr3obb)~e(Sjp7AWfw(`Q};8@4NRC zg93%cV~$Ayen9(>A?C-3OUFmq7FFG0fE{#cX*hcmi2m|Kgxha5&^0@%acWaa-RP)) zmjWd_mjDyARl9x?#A%FTj~4vB_bU*X7yd5|r5`FOiY>IW-~WiNuQj0+C3(JG1>l7n zl(ENp3vP(ZYBrH5qLg2=mC#pYbctGu-1buLUbcas=}XicrbYjjyJrkp{uro+h*xd6 z$7eLg?@2cb`QDpdxs}k9p@)c#;H@MIiZ$?N7vypetGTLQ(5enQf7BW5xo_Px&3ODQ z5vt67CLI?4#-U%+tkh;{s~`T_#P!=fNrD2RyG|5|LEQE}AI?1GSRo+f$+2wM$VXk$ z(D2hXot=(zY@Mrd0ZycS%&B*oAwhz>ruRr)zR5fwT|75oX1;(b1LkyWH<5tAA6z9l zT=Nu>PiGfIB&7Co%Q(@Iz6zR0mVnqY_`NmBYBC((D)>AZfS>LCe@ctWnBR{q6Ab%G zYv;5j`;<%cC*w?t+^IRV^MWlUt-iNhy551&IdYn70Iv5Ivi@?={rT~6s39Psx)Hx{ zHP`QOu=!4lGBCoQGkv5^$)r`=4xSdGX1Q_>J=1e=@Ry30*^j~Tt3ye7y3Jwg;6ceu zL3%%N7mz5kK}Z6k)j@)OW`lk{1SGm`$u@l)$3iJF8kI5_Zx-Q6Mm`xaKlBrWw~8<> zjEug-GLZ65rJJ?si7uditDsGwe5zf*Sk;&2=(_T$7LI&65j^I4o;&d2tRhA1aivznmArN3W)ny z|0>@E*#rED>>z>rZkYIfHv?L5-YUFT#M?agw*D`<(P@vg3~nfh8V9hyo_qTv{DJQ; zv__XP%Q3sR3b)_9nn6Kw6q3|_$ka8{#PgQp6b-!bChPvx^)I<^jDA}-M)4#4vONZ_ zaw*SO%6?n=&b1tqhhs>LLeTa8R6IYi9cUAt{ky4n|yn9c>y>w}lEI<$FjvpF~4S3Y6oVl8eaH%w8(B z&T(#LqXg`;JU6^(sOA_@Z_9yrU@|kklVr(BQ;unaz{gAOaJHbWXw+f$L6yTh3 zB(TuMt`cVn(H zG7C#UC@G(wM6N*BW-5SF$*4Gq9hVNv1)%{oCOw4>GmEiZ*7^#pNIPZFhL)NiX? z$(olKROtFhjXV{7tqL35kD|WT$DjIgSUc zU;wOwHBm>SoGmf1FquoreDSbcOwZDX^?uEwrkfA!1q=!eY{sEA;~Zp<=qr z*Pa+R&ZC(JJKYq#m+lYTTl!IQ#*;u;-vOP9!4h>DNel7@w9N&c2MO!?6w;7D|N2Rx z%Wq|a>H`~-QJ0cz&~;G`MzWivNPIQUPl`Ul4d=XqL&_BzCN?+iEI#GQ{PD#RDVVPa zW{KkL9Bd6wwnVwUj(Q$9U{Uz>yx-$kOSnMsG3EyfcvpWn-3N7o%tJgshn-c}(JETo zU0os6wtJeRuJ?q+zZBe*w;N^qi{;LkuY3t<1)b1l{^Zn=+UG_>b)8-tb{}J#%3H%@Z^PdF%r$YaS zDgINT|JlHEvvFp3ffKX3cYr#5=KDNQ85#NEnyEH^xfjdOK zne$H#UmhBMACjvE(Xj5twitfj^xYSbmnA$x;DzI)T_Th9L6Zyw`^?|DZ_?x%_TEb? z-4v3O6$+-M4pFkRmE*z#Ktu0=Xyr~29wfM-L$6)4z9Ukz;@#Eg=I)HZ(Ms_kWiC6R zz0HL<#e&t0`HH8&r+7Z{kwqm$H0)_XPi2@W=BKfP zcGsGDCO24=Z(@z~HW=j%8K>2M?!SV4$FE!2neIDJIJ?!obiDZIXej7akmbFlvWBf; z-vDd=5~}ui(T^!#e0dH9)+2)hJi=*tJt7_|6O`Z3i4W+rz(yf<_xc*-+g_vf3?Pbswq?-ZUk{~Ys{&E0aT7Yw(|)rgzI@*YJ5i4DVwtPmXtShr1C zf4x$XRoygW%>BiuI=@KthEpA@j0OhSoJUc{f4g4B_P@vHMYt4m10%E4n(Q&T@4Zi;p)2Tn9O(xvk9l8)#o%&pN3lhO3Yu5gw!a&;e^@gU=RR}l zYDyC_DZiIW4*wql<0{bhmW`EFx%36^uRSj_uPx)nrB>`&quq)e!^+g6*xuKApuT~e zbE7%MhpoBr8+;nnnrpmqGMhyE?LUG42?2l0`D|+3R+X@*RGl(4;^O%i(GsujXUzx% z%=jTsWvLnwB2*Kr0&iBrNYPhAl(lNHVbMcYVdF2}z@EMw)waIO7TD{-&FLzaeIel> zlBrpv_Y%z?bb7G!fT!>8uh3pDT7#;o3g*A3tY^I#6NF6$%!_KCro%L5bxc(H>#BYR zD@nGruAX#0y(v3fG^?v{{Pfn!sEL)1t+4icMxfaKS3qk9^8gB~Ae2Vk-R%>5fBR4U zu3Jzox<6g6`qxCgiFRe7&{_ma$f$T?%Nq`(h_!WB+4n`C$i?VOx@+GDW~(`$GhAV? zQ7IF!(gS*3l=Ig@@6E=Q=QIRG&E#c_ae2e3rmIv3YuJBy$w8%!hT|2R5C!ozrJ_q`Un1*t0MpB9-8AjK4j%nUJ~ zfdwR7R<;k+RuSpIh^dq0_KSs4Ur-vMe2(de4|yr&YX>8ex^gpvT>tkn1!ktF06N2} zBM`OafvYLnSx-ipZuayV6LDpUjeE=NaW7HnQm%z9LgkZ}ZhkYdU7tjv7W>o0%B9CI z0yEEc+8YSnDfPrFc6xKVq+WqCA}Ntg`%loap<=-wv-#Zs87p;{2x(kew&0Y3FT~*O z$t364Lm!6!JFWr;EFM6u)rNa^Pkh&GkdTZRMlG(wz1uSW-+d(+iIZEciOSTYUpWP1 z#eVO`en0;j%~9`vE5l*xn_W#)erwp&!Ybs%Gy=?oF;UtR8ZK z+u0$K2gHhLzllD<2h7YR_{bpRVe-@7;ex+_Nd^WFKHPD8{p2pi&gNfHkDs*(*=bz8 zU~1s}qAkqQXJ1M`{}~H*I&&nZ!SkK(#_;GEE($2(?;?4Z6aW8r{ePbHUnS?^hEF#H zUhrx^(NLniDtmOx!|WAZN}b0ncO9(WeY)WJSN_~Sqscx)SHIs+Be5eINM^H7oKf(9 zb*FuONKJVrXv?b0-O8uGRv+!_+;!5@kOw$}y~pj7InpN3ekH5LP5%#_b$$xqv%K>X z-%qYxTUVv$@lfzVveL(JObV?G$~^GG8#VaW{FqY-hr&Kab4!~0Z%nWn^Ju+~6+Ln8 z)$P==sBwB(8ekE0wEc3t%08;pyjIhHee7>fZ@l}-+|mKsWdaBvR@>Wgo2(o4TKkgR z6+L2Hp`5Z>5{H^@3g}xKEjf2dcy)o)9nTjJc~A&^Y$qS2NqaC_o}(1;abb-PHm8MM z@#XPM9MX@9J3U!IPWkP@z;2Pf$GLK6m?EAvxa`b##9}M`Qf+6I;MbJYA2QXrPU6Hv z7e<_C9>mYKg@(>W-GZNPjyFBX!E{!pXH7z>r+cAs8Gb}Oowc&W&d$O?^R;p7EF!6q z-vDKHccruS!t2P#{H^7#=Tv*iYe<>T8MF;j@=gkV; zF&h(nSfLIDBI&(e?pGRWXL4r#wnDa#p#Cg`to+|B02}2BSl1Pd9_%q z=}+k^MPqJsbPj`=tonOdHd={7_99xd_ezzL7%5lf2h5GMl^>RyRhuI+cYI=*t0(;X zlFIUp`%{O@tzd-F>f{^3#t)MUu~jkj6^J7$%&ayzvDC6rfB8?wbsRI(S!uVS7r_Us zf*JAw&(~J3!0q-Z%(E7aqe&KUb1r|%+g2&pkqfd!A@@(Q(wO*D{5B!zlI8`;rK%+|@UX)7fU#%fo}@6WMYPWoKX+$*L7gD1L72=%R8Y&aLhQ(5b(P$v9@`+-QRjBc+>CVN7fBCM`s@4) zKUzCYT55e0W7qmcOSJVVV%Z{6_t@0)4V-iDDwU~%-XL>vNBHp-rqw1T)gS}JEetGv1@2tVl1Lji+Ewt{JJs(*r0LdoE6s? zclR(pav_3KjB6s`0tusYmxgNN=4&c_+|=?dvuk~_p|J&iBJFBU4Cu7_kOBHL>XJ%s zmT{j72*YOqAKrIm-%9bC8lUl{Mq0tX#5)_4^~tBldmM2ahH_r>Psb~~RL7=+b{FHn zmi}dU3kHQcO20MP?fv${t{wNcs4S&zI0H9A5)+@|YvfY!O!*JKXZ>x$0}-P;P?MvVDK z-4tOg!7`QwxYS#>1c$BAEyS`28BoyWT_->8c+B_wf(-1Wmz=2^RHA9OQvBV<{K)R> z4L^MR-We>2{<{r%=Qgd*IGG6!pasFxPiJMvw18eJu0;H-Y8aw#e0CL+M?&)DW z3o4(S;9C7d zXQzQ10{GoDN-fn1NXJVB(t70it}T|@C- z$Xmy3nQTn|*#H}8hdc9(6#Wu@zZT9upfco@&P^N*{rW&DU*T4uJ;)5PBr!EB-2^?G zJ2>ZPx}YayY|ay+gYR*!kKhjKCWvC15ta0HqzN&zs;8|~hv?@01B;ho4^LD;chFR< z_blb`lW4R!>0M6PkN=(=!c*H{smMd(o0H5hIvSwJtb`nH9q# zo@jp4!FnDzDQ97IYOWQ1>k}2byXJ~Aw{ZtuIfWl=_@o<_Tgo-0lw^?)Uy*sqGxB)= zrBG;Uux3*odwXL`_$VZM@FG}>Hs5CT@Ih8cWWqx|3758m%}LkOlc6rMLEV4wk&dr1M`^JwYiP$wU9dqDX4 z%--HVmu286HMIYyT9<(u!x0YRqG;J86=Na2M(T$y;B;T(D-~? zCN@W3BOR#dpa^3~^<0=ta)p-$6xMy7>2k_87@Q*}e)kqYL+Jc(FEkzk|+>Pn4Y{;DvC_po`DsMtx6z{S0R})AeAR z(l#QfJW7=NcEF211SdtEwrt_fxCpMAW!r*MwsUz9Q}nfOyRb!?z-C70Z}H2qO`gAP ztQczFZmj7ty=Wy*2}vHX+#ch-qINPO!YYK221EW(}b)C$RY<+j4Tz`ZeDCIzMAk*~Qnwv8fS z@&Kq`n{5UoCV4Gmezi{7E$$8DYcdgy7zZLH|G5F){F9wGfBCbtF}8P!P8aWd&u|-Y zqMLCgk7sRNO+##ak55hq_ozai=txgUg^~oX@F3nokJy-fU zg4U7{FxZM!0DD`?73pFFUI-T=b)@FYZxaC}UTCh6pnEHcv!6R!K~Gc}XK5lhij!`dhBBg3Y6Ey|2oq*98PSujO>2fu;H zCsMy*P~Y5gntQxyVTZF)nG*$be(HglL3puInae)%?UIG%NB|Khg8QCrWNe*w7`xPW z@qq8hAp9Dz^)bA8XCf`vAZuO(4BZ1d^WMrPdYF{s7k*talU7UHwJ!%+*Lhq&4+?gZ zNRqn$;DmxYR>M&iTXMTV%_@kyUpXfZ-d~o-chP@y!udYl%6;j{$-3!4;Sl_7o9pNG za6KONQ`_59#-T1es9JT%Q=!xVbvDKqCjh=erw($Pt(*>i=*GC1>YW;!A6icf*o(=P z`!IE{HFhL<4z805ijUw4z85ID?K7CE=zXXdu=M@X%u(0umM7>_f{oVR(&{r2rpI8` z7RX9E9Ac|!sMhygJ4K=@V5{|qW-f=cdvstw>D;jq-tzud9QiS6OEks4qnEXFSxO_G zrw^7l!?c(ZH4@P8k+)*flX|mc(EfOLsxu{nf`x(kId@S)pORgpu*!*3zqi?qE;bw%Y$X9L8;99ERuO42JEY8sP`2 zBqe?Bl2|Tz`b_0@9!@EbxY%#S)-C83t#bEJnN5p@1NdEqOy9)mK2(Fkx|Cu~4uV4N z$qoxY*ErCdEX?z5rhHIlvJQH^pLmU=xWbhSIlRnm;R8)I{>c5hY&~Rc+aNq`2di8a~?v*l2}!C`Qt+Z3GfIqpOu zBgXNGV?5ml))il(ahdu1jr@{VOx&Pn)Xh=# z4z&?8_c+@BRoCM5cw0^Q!8o*j%Arj-XOdk1iUz|l_d|d*^jZv5-;>LEuO_j&U+{ZD z=mI+M!*ZX*ebNUHI93Kl%6{ zNlHoDXP`PiQG=m{xuLq?YXej@BToxBuul@j#*Ru6ZO$#2^`g99{20GG^IflVZy3rA zKNSyJDlo|{e_V<;WDBTdU3$%*Y1pj%>vM|Jl%zG#}&T-IF77h5e_PRMXnpJ$-q(ET5{^xxONPY{#-D>xjIol zcQG;(Nk2l*^Z>el0AL5&qr)|yf^L|5+QH9lCR|rj22y$d`1}t&lWputo71HXmGGgh zLpPWfzI3o>4>jxTEFN6o=1a;#HIu6ro|SOpYT;|2Xwmno7;4v{Lg)=s=?e3XE#2ai;Df_LVxz9yT?)gK!U!2oS*Ff2M zc)fEP@#I7q59`}6;s0SrqqOa#_jL-EeJm;{foc%MliN|79YC zPJbDC$^)9pq+Q&k2biqvNelPhH^ScvH_w5(+N7DapVMh5<9ngKq$B*Yj=$^B0`sO0 zOR}TEfwLzM+(#>LzM1Y9rgzy*og^o1FgkZQ%yrC}DQA7H{cz*=76? z`-4hEQCAu}5m__1H^g@W!pY#6OmB9mJvg>#z?nT)ly`nn)@%5wFz!a7VV!ca`y`7EQU` z{~jk@+*!lpcj2DUTrz%tE|&!$&KjoNjf~s_M90PbD zI}1n&=Y-5my+>BaOC3xVo7F3;b7u zx@L9eFbweysWK~Y?g`Y|B7HTd?cx-;n^P8$5N0p-+evo6WIa|Blqq#nhM;H25)e8= zv`|=^z;O-BaGS|LmU}Oe;28$axK?z`s%?KH`0E^x_qrTjd-4O0;1WKluwS!~AWC_I z2B5?tD@z&zSh%dg^pT!plz9|<_Ak4up-p}+PpPkYG~{_?trM@d^5xXX*z{qHRy^>J zujX4?KI@!slaOWnUA0mcFBt}72N2gGri(isNE=Jp1SFC0nFiXKJ*OvJ@w@BT!S^w@ zqWtfs2CQRa5(e@}n&sEezdR_<#~oaLkr)QYiRjWHxiO_Jzh zmm9V3nI4wiac(}ygB@;mZmzCcA~8A1mLZ=kLpS!t<9^`It@;oN#-d_eM)pEreg8x$ z9Yu2t1_rTFS`}!0CWliZAMq_LywGsU^!#}dbEThhAy9O#9>4z;!pU!9?e+eKkYTsa zCHANPg{|Lsq;b6p`?fmv?$B7TtDO+IpFgQ8u;$Vny|A{_?4zNxVSj}kpf36VWQ;3wUNAOXK$ zelqmCXHz!gjFA_q`{&Y%LP!{mSRQ&EEUt#BWyZuRsP<_u>^0c90-|ASxinY8$644J zZ>cy|2f?n-v}kNQXe;lZ`$oTZmsN3g!r1y&z5O>S&Hf@MTYmlH)7dPJEnK={>Vj}) z?rhx}Nez490$0ikh)FKf?mjH5=j#J`ND7*~w#ql2PDY;jA&hG175@nQXipb09C_N< zw!J8Ug!w4K6x(_-1M4)v{MQ0;{3NZ}*HCR#lo#$tCvSjB4|2toichl|OaooaS!}S~ z#8_Q?tb+X3)Sg={tx@jf{L@c0Rpqgo?9y)Eylg1mcQUI6Qo;M-FF{}FCyDqH4>x_J zS4yGbS|dOgW?X{A{kDKvSQnyqYJm5Qa!WZ8rVAHK-)u{~j1KVyY#ItzdApJr zK8_E+LsX*-T3e6qu0*_Il7LRuNv(bS+UT?LLC^Wk_ado8L4Azv+gK+?5%d$Yhh4z> z`oiHVHDQDON^}-7LN{eZ9#!3HhAT4pl;BgwEC}}Q&ur2#c|GB_6y}|FNx&Ro{QAq zI*Qc#eOFC5Faew!tZr{z?H$lHJLc4pyYdezDguBQPizfkWXiKg0IX@T_e)Pg_lF0H zNX`@(-ZNglhy`re+u{H5)t!1KN_>yYC{0Y#?r?r>xLQXpD{FBQEpXzQPdC0~F?Y3w zQSpQGCm4WT#d0EZ4It_2x*hVV_erMgyuOTI{-jGu`WomWvc&B^?C3PKo>8VmOI}m@ zfOsT#Qd%9e@z4wnDz!$cr7GP3Rh*Sl)%}?6sZ=Vi`X2Gwi~Vo5%D5AjbXg~oALoI4 z=nav^BHGcmy9;w0bGkZLxwaVYC=T-L^ur@>jHTM?1V}Df4ih3i(X;M1qb-i}wbjni z7ZTkkq%-`$YJJ?#m=84eKB}E$b52PQ>%!C1-rftg`m)1W;fMahoVW${Qq- z+BfNnt7+W1%%*lx0y2*@x-)Dj z!}Yvic4i`~JmLCaf;db**dqCrOd_yBIIZ}4@rlv?nVVB;r`AhDb;%6J_!=1d zc-L)IUH8E-ajWao4%t#xT_u*6z^TuX@YG1`$8*@ac64V9`vcJHR_&Ck_CoK|(&O3D zR&PUegI>8?;Jn>5CE(XuTD!vox$_L^O5f-YqxO-AOC@sa>-K=YCXDsQ#(s*wg+@|J zB!9hMs*C-El75ELfS-|)!EI%#hJoJ`KgeJ}s&X%LySieK4m<65MNZ=qgC5#{&rcEC!fnQ}EqsYvBFZ>x5DUZzwPg6NEYs za&3Ao5#vjl=UXEx$>3Jk$4Msv(y=g&;gnw1q`J)a4kgY8PZB68oYs>=XV`%OcSR2B5#e`92c+ z!ulEIQ?FCMAWwYP#{~cFa}hQv51F&@;j&^Q4=RmVDyJjdbm}D%0je{tc%c}}EKiBO zG}vDl(;<0R9T|6DKmChY)AL&%;MQ0rUjvXvyz?0bC=$3k-BTkQpkfitG6#fu7LMoG z3<%GMIO61Bbz7*S6msPb1-2jjeq`9#_IJ_h?lwZ8uPo$KT4_;#;g{8q--o12Oi-fa z`yYxPTGn(Ma#faVCib#y#VtYL)6Rse-<1PbZAbXg{^D7K^|Q(77|g&8OHv{ye2Syb>yj)$hft8l{$>z%KJa%zT?+o zSoDREJ^u}`3G@LKzqEU4eV^c*c@x`FHXm2iWu9x`P zt3g&DF-(<_i34A9CxVR1AeDOivm{fO;VnaFVe<}1<+KurfKMReQ9s22o zt3*Q=s~voiEM2vZ&1bn;Z2xLrtj669C5K`3G~FL>6fc&{*{F2N9Ll#h=Y?|v%DAA1u5ixdFcYt z8m@k|0u%!#vYgSG+=M$%f*4rEnWB;}v8AwiV#Jyv(VSjuBVE8cUi(surU7TRF7iU0 zvNb;>_1-^PJ{Af(%xqcpJ&Y1Vk6C9NOfoBNXV(rd4AmTlU&&Yh5r-l+E-4zhls^Ij zIm1kW8toA3@b5aGZcJwhaJu3Z1$_=_AGYe9q1<{t>yPiWIc0a%++7&NrFmQJPFj<~ za|F-Sh+ae)T)CTQ6Dk$jcd{!i7Dd(aA9vlKE~$bYirN{}x{gZV9yARXDU)>?@g2?y zi4e!(W{^8_Cuq9Ox29hHryVi3Ek-*yxehAB5c3r=cZ)e&g&Ub}0Et81O1mb^pcwsWKNpeTzOqJ}ud~f$h`eL_S`12AMZ!2ooL!>-juv6bah9ck{D1 zOaZy!H&Pw@rHaAH7d&6|OE-#q|LgKCNmc`;mO1~_u^B;zu~ED=*VIF2<^7hftv1@G zP?wpl=Dd+fpRNHv2TfX49#$=d7LGUKQk~r04Nzo261J?lBkCkn-{ANEG+ZH_J#&6o z?DOrdhT*Np%$65cL3|nNPGXRtI-sF*oCQ5%&nfy9>Hd;~R{t~M@aZ01%8ab5(oF+r zF4af<&O%>`T3(NAR2i%970cEd<-AfuUHMzg)kO_dU{{xCbw2+ME0bOh>5Smmnpv8* z6VAla{Pvau{k6uGnz+B=AIUa|9r6o7S6!1fg{8&6FT1d>w}C;Bsf4-gg0|}EGI6N*#}+ua39$XW z+;3;3ycL|jpIKCw%Ap@_9N#-?ye&L@vsCZygK;E#tKUC~^Pr``xQKS>MzxZojLTT}7(CCx>o8c0kK zxD0~CAJ>xB#<6358L_vDO3hn(+1qbWW#zn9hd%OU^U+1}aSisJ9BjpH?fZVLb*YHm z_80oc=whu#_<(>UYrH{ke>=eBhXcRZe!HV*v0Sd~N$sCHPBeclr=o-JSds6e=p)9- zZeu+F6vCw6JdgnNld!~ZUq%mCF$U@C&(3pnCySxHYJbjA5GQ8&&-Cn)nYp94`dcf- z*%IGhViR|V&;3By!S5!btJ0l8S#@UJjKTJHm<}F+fwPdfOI)J~(Qi7#D9c^&zjv40 zuCHWjv~`iDieT`aj+IV2y4?D26C&wvMa|6fRY{(RoeluFXR1 zhu+~USl{79;`eA({nOVZjlCw>M12oOIo7n~j);vd09BEhnM>Hokg-`j_0RO@ zjqOb5W@G0a4YiKDGO=9In~9x;aw2`*lFO~oy*`7=0qg#&Vm>Pa@vZO-4dgrbwDQ?L z3*nO#&HPh|!VSA3ANEEQKu>1sKR5;@?L$|Gu6HOsas4O0?fL8XI=98?himjp_S|xH zvl53z%b0lYrh@rn^}+j@YPUdQc)mC#PA|V$O{SO=NbRq%oiWaY zDudjW{^#8-H375M3vz4qY4^2~9?c|A3TN!HWCUym4Fj|66oRZbD5igR72dY7r%=m=~%XXpRxWZ$}vH!dcEC&<(~GCW0OBi0t}0BFCEJa&Mt@mqI1dCPnH$ip<0n?-meK9gw7a_F5fZ6B?B1= z-oh*QEA)znmw)%B3NH^T)3mdfj{NiZ02_|AXoEK_FI)|hf= z7Bfp!rhv@cn+cs2p#d{+0{q?WYr;ALhNTDfxu)d;b!4 zjC%>52mJy)g)5HE+12Dm-Tc=2dl(bPE->&r7o|pP-pUg9gZ>j_4m5dm)rfEunEVi& zwa6E2AZ&Q8YpnPw`U}?q;M0@{;4KWWna$6%idz1ATuyo;TzbE*w$!5DczGa=<4BRz zCM{m7eG3g=-AWxfJi5~rVN8d}vX8lODQUd=m&PY1cF@?ZtCu#fldCPqy>n#ZD4R7? zr(Mkj9{VtL6xQ^T*l2`|LZ?gcC}-da<+j z>eM#esdxm@G=JOaoAy$Nts}lPY_bQi8XIj7Yrm{4$5T!yKDx<9 znK!`q*@&|10qt#A(OSc@z}iUV*B#GlCaiG`N2jS=(C+Jnq?i!tjFG@=UdBm+2A#6r zUVu;}w_PN%*4xFvhx#K6LGCf&M$UbTbrSjVZgFSgIEj`K>jaKxvk6vNHf^la*Sl%P zr|!pUWkA^c{JHsq>f>ObKg=wDUBVZuOgoh{021%Zy5$O@1Ix_;nSPG-(J zsx>PM!zU*Pn;dAf-?JI&nuA%&%JT?9!(NNt)~98T>SSVhi_#;b6Ee}gVwSa`?6?6W zp#I6`hu|49Amv{00KFcRa`mQ%h6fNLD-mMNx`&&Bu$Fwsq9hHb} ze1(8aQsFAdbB-57xh7cndyo9rp;1a-8+kC+1|7AN0+5eo)jC)FK*NS(7JRLtH`6QP z^7j!LsEfN$p>uBG#;c-}zY$yZzt#ADQVEf+Icg`8W9*8KLg3w>B~|bf|An(>ClJY^ z801jkXq>jI2GTZFUBN|JClr-}qZyKrC8}3)2TN@ulJm6rwQqSSWT>3WTM?Dmyro(I z(NI_*NpjYYyc@HQLsao-Pli(8uWXEVxz{^SlgGAl+Wx7%J|8L6h#oa1tB?U@`z;$) zUrnpKc`@Pe{&!j6yPG8JPacNUy@$Sw%Z9*VraUPDE+p}+4_JZrorV55+Bc4az+Zr4 zx|_m0#fB?BppE=R(yswfpKSRSp9fCmu}rD^PZV zd93j*HwzR2tg7Un*Vnr7G9kc1RU)9jx*+U*ZBYY)9C#tC3mUTts!*6Z2c7#P+xTH^2m1Xs`swl!v`mArq^(2MZ^Q*WRSsa4|v7F ztAvDB@eo@0q5W|FreUOe?{N{DV!Z(ifAUn!&G?LcdrsU$3t8gUatvad zdz%X7rq<)2EiL{VsnJM}nbO9E@55W2aN={?exoLhs$F2vwLSH|aAUqq9zes+3Ad#l z;M@KJ=<9>O%wG!Y!u`mW;<(-%EzV$>(3P!Qk9Y^fEe)> zbLjB%2lt`(GIriPFJA}b(PJ!#)Lm?4d>WVu10o+UzI+h1oEueJ<32he3trz7Q@W12 z$uwRQ^mXUh;b47xG)9MlBe*jwk=_tQ8<$gE)z1Ai2fguJVm0~X>>E$tp?$0v6Qh5d z{{`BRPysF*a!p%hV+7wFSL(+FKH@RiMQ<)dY2aBN|8EA2gQF_>&XBe5?~w<_)Ei9h z<5A@gzd2#_lv`kj7hN5S_sfb1F>bT1@cTAO0X;uq`)jPYR%3;eb;+(T3mh^ZD*-nL zO$2WV^BZeR;iemS(xf-bxGtwdgOO8^*ST|Yu#t$Porob-dIY}tUJU#D2kR)SraD;> z-t9j=gWZ3BMf{rZHpt;g$_(7~S`Y;mc=b&D-ckANpp$m{dOk5?0_9ecPJigNXK?N^ zpa!-Wv$NpY3Z(}9)-forivblZu42aG(v(9Ha?VbH-q4Ld3%UxK+Yw&y5apB8>P{ab z9+f2?7w5GgE9_T*<|uE%cx|0*w&_}j5Yz_Qe1^_J>I>1+Cxfu)E*uL7pGGujhOKmc ztX-kNQn?{ESb2vjI6)?7%Ann~d5T9hV12&DlB-6ZFohbYnSQp;L!ebN0t4R-YzzQ-1?* zA`#7T?tg;mF1YR7diE^aWwiJe`}=}E+sQs&S4~U6z_T&xHSO;DjZr-Kj}st9eQpXP z$5c+vCw$e4sBSyPak!`TRZ@V)Vv&W&R8b@gCpt_>N78mE?Wpn!;mUbO- z%SGWU0}tYbwA+|%GZ2(gKO3ZVQ^fj4U1RL#2=n_MA&7w)DX__Z!{2XA)T;kE&Ts!2 zJfx8_#1iZY4+*7F8itDP+L+gsx5u`6P78qM)*I8^Cdd6mIa_&mEbk&iiz;*WO32^t zOR6Q_r{zJ>#U`lCya9x&jWVSAT5?(>{|=6IV_pPs8GJ>SK5i>oUBGeAJ%X+v!CZd% zd8qZJG#W%B9Sn`6l>gXs1oqsz;=rslwr=~&67P#!m=I9 zJd$mnRY6Y7{ivbzdVUe=f)N&@)=t|!}6M|s*LhM6Xdz&%lp&FhU?vFGN{Yk3cnk^ zmX~wsdTJtDm3I=3_qzkLFC?-G89tgRk-P!eVqP|jC296^ISK7(XZ(pl5&1dR{(;ZH zawH3g#7Nh}t3;Xco#=G$wReb*T=U!f0=_(yid9nQ9bb9umSd(@(!zx-`BG*Y`r4PY zg6~pUqxX}o*NA0Iu< z<@NW1vkM(hMkL}>fqmUv$zO#dcrcsg#en8)g~($~+bBNm0DJq4WpVFa zyj}>Km4d#*Ks1~j5$lgJ+8+QCYw{c~xiVJ8j%oWB*|fFpTm{Th5;%?GW~W89;XjW? z%?~mE6(ionNF7EYj*eFsn3p1j#R3%asc?FqV_Embzgz(AzMS@_{bcT?FRAB^%DKfX z8X7OlDAtfcKvFkc%A58?Wlc6458bi+Vd#IFQ!)#&k+@o9;n6rcmMO~Xy>mbGI@7@K z)Enk(+>L)}?w*$mvwRNZa6Bl;TahXqpi@>{IB$cJBs%{f3|-1EKk$q zJl)fVv&KU(maFYE<&3Y+tb4Pv6cin`F|Mkq5zaq*B5?8ZyBpVrVX)yzT8Lv-_2Bxt zTgr3>@(%)Hoi?43k&&{OIX>=)IFVmTXCE%L=?XA)oHRh{2!B$sI%65I;g>V(vDxuw}zRm%qtOt31+Og`qaAWbe8I- z$*uW{+`>x0D0>AaxKfjkK&_NM68q<3JOh;0nX8%VYyyRnn|D+{e6wWBz~H|HckOho zQ2Dw=e9U862BwI|n|?Wou7MscTlj;x`MufD%toS2K#&hHQHVTi@XWwyf{u!XpDFig^am&^t*|i0=4fhV8e0mJ$^#!ZAKtQ;E;FsX>pRZa zAcUQZjI(raeRaasXVgkgL@o7a_U^ZOBZUptT47rv z{Ug9>)NT!BxAK-vc8^DwpcWMZlaLgvi2>}OLcfA5@wzcRGq>A`85*7cu%Kc|s?NE$ zm9a0M9KrSMuBm7sjZU3UHOV~TPO9)F^S+Td;Rz2H_BJm&{|^NRDN2r#4$V&Pr&BS@ z-_f~>~4{2_jw?56LeXs%}mzc6wq>Cv24yR(`LPl0^UA)QkP9JieHn&^6 z)OcVvw)34VZ2SH4Fwx^E`cNjZD*rSzZ*{7Cu(gWTdJuzxwY!8mgp45~=ZgywFZ*?C zFVjh|iG~RG&JXV$tg>*A4PSQQnnS6Qb%MC>?Z6au{eu4aP`j@>rR+pMxRAh`ed#ep z{EL{;DG599aViX!qoa58S&l?7Y4u6* zIK-=73MV-^Wou`wa>2Vbq`x*x8n~_t^=>)_z%JL;Qrb4E&d5u}4if5tSA?$%lI~Nn_8V6 z#u#!jZ?$`(0J!ax)TLZ|PW!0`cWKlgj&(%P$;N#-Nr@ZXzIkG6O|QDeRhuczZsQUj zps^?8bzuLtgHe=8sZF=$w~or_B02KcOXWns7Igs7rcnP{vz8bQ*2_+zWhz>+YjiXRgbq`2kEna5XsKAk(Qwz&Iv@x0a-p@Wi@AW z7%=dpRIJ;S9Zu~H>?4LaltSK75Cr$sdMEcQAx!$-R~}5|H+>$ zr4t&G>2`9cA5r#Fjg9|TEZ5Bg{d{K=OT_y_hW7|P9O^@3B=u@vtw`fco|0DUZUJhs zxnf|yRUawrN~p8&I84B;4EAIz^K0$x$ zGA*4hZ>~wIZG^@0C9KV#ljA19BNt7c!mcRWU{;h(klT$j$UmQHrz4?KfixZT<5Pkz zVwrB6W*inT?lX$l!FSBCwQY9{VdSP2-ft#u?Dxqd>|6b?GBNZS-6jcANn$<^k7n%F zpHQ|5SN)D~>NWau(2`k_sr@f6?Cn{Q+U3aY=6h%CrI&QjWvk1Em(4=92UvR-YTj%FCKjCh*@2Gvy9tzZ;? zU!qIzGys5a-NNqJ6y=tB!I4ECT~AhEl%xE@p7z}A$=W)eitj1{Vvk;?m3*L-;<_yJSb8+dYrc2&B*52af%!L9k>js5>W_TDq9sjUke zRWza$Q2`MU@ElY?no^aH2q+LC^bUfwkkC7XP!yG_(tDE zsesa8PT&$EytAEiG`3Ri5H$yQd33c z*xVjJ+1>uA#HmSAeG0;mq0+^KW{>#o= z=UA^5J?-CQKb%PqCqkhU828r86Fp))q_^E<;u|=HNl$s<+HFy3Z6*86{_R`njfki6 z?QG(|Iri;-i++rnk6WzO+wJ6F{}Jpd6@zg~T^rK=;TSdIzYMbml92qH0xIAauA=hV z==jJ$O%)_TO^tLngpn5dw(ynl$e~&#)6Hrm-F&+c=^xN^Aa+0<>g z1{9lBJkZ6-;cOaT<$zDyeM-8r9 zgc4=yyU0&U-q3ZPBVMj7v6$^|zC`74%jheYN9~?PWTr)2p=*T9k)AR62|;~=ezt{@ zLv8P5{2Hl48-v^B%Htad1ZVWJ4QXYIn36L)v5ofRoi$|6L1BlU`jNfF`R(kGihDqS zfoW+@vwij6?uc|JdwppmF$sS8NZfa|wzuitJ&wZtM4`>io$9!)?sMMqOwCkx&m>Es ze4v{QdK}&}Z4rAEeG*f$yQRn^N_Ma2r4tPo3T4!p2pspTx?BB^^3Q$k_1~!Mn_XU{ zUJBl=uS?0Ye3%{$#A(DV??6>@`+b^xJNZGU9GM-zy}~Px2Jb4Lpj}<>dd9Wji`7@1RX7p2$Y_i9jUu7dHJr2G**;^K5P2I(R2=A=@!efUH&tw_X~0n` z3e@vI3@fc8Mr-Y|N1q-Am^3{J7S~K5C-iaLdO?UpN3nRtFpJn~V8!q8wdUsLwy{pA z?Tp$kRrHYGW8nndAapZ&+zUjpLStn=WOs zNxi!$z#&x4d>Q+@_-{N`z6C4NMlh~HhXCJ?*na+eD<9#_X z?p28i(#wKGmidh2l@Iwgy=maDTj`D*b_^BYdE=6;RO6FkJ=Q~#*hF*N#uigD!^a)a zB__)@<*s>;b1UJ)`Lp@7v2O}|@`Ih$Myd%|{|SjTpnQO8@6nImFO!s zM*zqte2|2_@+oPoTV=~*LsKbli-eY&e~*uwo?3%w#2!qB?vfL<_I`LEIZ@7*pJ}L#j$Kf zg*oEp@r{IW{)IC*x|7EgSa$)J=5LfJ7vwTt0e^;CNQQSufCFUE7TH!U|KSk%&nIVq z_%l>1-s*pFbe{cq6G)_q8D9R2eEV;_TNm;G)}g!Z2LFRkGe8aopyd92;J-uqxB2o1 zgXq7L=HF%Uuci5q@%{7D{~woyT=d1eZu754%d>CBtpF*^sL>inN50PU0#!12Mq}~b zpS*whj)8GI*!#oB^!eB2&07w|yD&TU*aEj@uUckFkNcMjbllzbu|7ElRkre&$!M;0 zTf=H-tUtTo`r&q7cVIAO!)MX~t;ne}=c#%w5w=JNdx=0&a*TQgk*l~ho2#=l>|tb- zJiB`b6nLE)ho5sGpo?d#@z%-J_PU9a*n`A7mQlhh1kbZMI_pc{<85-ySnpVbFAU_^ z6TR018-*|Pd42FVHA;B$K{>G{_{C`()W{amSg%zq3G|bFUn2zACxzC%cqtyy19^k! z(XuCq(IF^Y5Pd**w_y3Z$Si0c?__eAUj_Y)s=BjQ=tuCFZYFR0t95&FPc18@zX52O zytcL_Dm*8>c{j2Is_?}|mb%5ps}q#*-Xl-o-2o?q)uPgrlU65um8e`4_ZyakCuYk& zs4We{0WKrB8jvOnPQJ9iZ^5eJHb%w1y*R`*AOz5J+36ZGA@2>WMxSSsAaZToJxQ^% zQ)h&6b0+$gL5ExhbL{n8m&-X7Ky++U32A!5G_03p(oKQFnB+vsJx1y64*Sx^Z=fC^ zW3zNqXSq=ZuTvgjE&p>$$M7@{=_dv>umLI?I2=s8v{9&V`PRZ~~CJAPHzD{FCk(yOg5GHV^y3wUI}!f0hY z)3QrvxJxN~vd$Gy#>;Gve917PIXkvID%{1A>Jyr4xTT~&D=I{6s6)bIMh{o ztwOR!omzUFih1h9UF-gg)(w>(#g8|rO16t!IbpQMfAhl!Y+gcdy#U(Xxe+#cDLZn{ z#DKVK!TMtin)_I~^{Vy3*U-XzyWFPABY-fF5MJ)Q6UTp953BDmbFt6-8b$|GW!pc% z?WD_*1I%2GohN^?DKcC#=gzr*zabK^H~_3u`YiV;Rnp@+mv<{_h_Q2dPeIk=ORJ65 zY;4ZBd1obrT3uFSVH|&9C)8!I6RS73fjFQoBe)kN3hg{C<;gP2t!SI{+Y)vCWbM@o zVd(SO>b|$2$jb@tt^~+H-yDzSaV+xqN8lnr;i!W+NMZTIZxp#72uG{)i&ndSPsn~@ zeL8Y{+jx%zpN%6nxL9G?z;}8E+RtBd?m*e(3b$94+z4YzV&q*Uau?tLWwB1#vgZ;V z9LVtv=AB|3M*k17oSz*Uyp+LXF*s%h@vWAgqkmRkm%}KdC{*b{j&c8ZC>ur{ANq-wpzf*A~+?;Tp^K^NtrgJ`K+CLo3%q zWH=VbGq+YA8_}hDx=G~=@Z@z|W$n?QqU+Ga**rw3eO8@=6bq{GJiTmX(BkbLDeK0~ zSUW664DNF@_vK%gwM ztq;U1I|Y2xNs}t6O_C#PF&-kkyKAS}|Ck62{2HD^TH8hm7lCQQ4VD9EyY(-1{Mcwu zmJfbWn|E(bR06j(n6A>4W{(p$IiZeR{k-ir*SPn)U|*(emL9>XVSm=RG9rkc*} zqRfbO`}Xo^p(#0kvi1qqpwS}HpvuYLNvmOQk;$&iePOT$32EV~c|Dhgcn=$kS$ts3 z=3Ur(9aMk0QM8w@YH5>_q!fNN+H(~1`pq0c_3~K%nr^Mm({T>bdD~JX7l@?;+xJ=D zDUxfE+hj-IrpCS=%T2{gI^eJpsW70c7fd#UT<9AVpe2)WwV`CY^l?$!hS9}lO+0kCo9eWXp*Cb}ME1Aq394xnO1>Cv0V}BWv3;7I|aL2B_Z=QYNffx=*VARMF1R2hy;u zDt9<2&c^WH{zSghFlo9BP-TuA4X4~I^*Z^|FYzo9WR#szCuH6X$!)?i8o9OH1sb;k zYOP3d1h}PR`@z}oQQ50yW&^H`_uf-eo?qSzvR|B+G=nI;BwiYTrvxE~i({Jm)1-aF zl|C8GEPa!l@|mzVJmMAXRWo~5x6tBik}~g6#8&S3W4&L|1V$MT>2fup;mKG}TDf3C zh%2?NlA3Unmvs3yGJ@ur5{n@kI*IgmCyUS0%EV#P?oNt-^7k+{SM9{lp~jozg5rg3 zq~<_2)m^(Qk|PR$88>gHY=s5YA1p9rI( z=tEuag{blousI^VK z?n?)jk4mx-xogmg?U}BAkB@`-EtSTd))If%{|ca%;fC*#rsQds*Z{Je4i_GCS8MmR zgJa{Jb~Pz~Xisyu(eC|6*~O?T0I?Zf))HzFECt=&AdcT#$k1*75Oo^}jBQm&<6_{^ zv{zvh75CuVyM(YVnR+n&a;t7muu+`=xBCWR;bo6$r+0sq#J;0l-1ZPTkbrWz*F-PtZ+IhL5 zRPRx*z&FBz3`@uOnp(w@Z|M6FF1DSoVU{x7)2#XZD8R>X)acQD^(gP`xlTxZ>~`J@@GT-1{M?qey5F`|1N zox&9oU)L;pIiVq_+z<83Hr9i)U~AtMZ74|;afQYO)5JSeGL&Jv$CR|){>iO* zH;pwo(BpspK^%klra|$5-v~+S+JZf%=eFY$pB>I56+#LFCa4T9MO}7$A zNI?ZMi-9DUwTH=a4Lk^Zmz#T&H1o<-@3t}6N_0A~f|}KJ!M@VX1Tv^{bPpW(wKhnK z*snNeK+^ak$YP)~r!Ts99#dkx9|^a>bgCt}W$jHYBhA*!8$VPvF?@7a>|BL59l5Rc zm1p$635qSK=`cPBj$14>_b}e^AiS6UV=fFbagznJa?SLhKrkahPL63|rVXrS*2Pn= zq>!;!$yp5QY>QZm2)bDk&McfcfAfYQSW}sd@+xOQJfHykVEjaMf$VA(|S)^Kh`vmC0VDY`)mQ zVX53owcv3QyO5c#Z49SBUk~GuQh0B zRqeJk%;{oY*|=1Mn4vI|Xv8UNm&gQc!hs@c!BMr8ZZvaNXiyHYw!X2M?N2hb+e5$3 za+qi`&3p)n+N?kx23im4wHL_H6UP_oC|nogx%F-zfr4(XG<-nYrxT9qqe&gNDIHTz3 z7A3lkL;N%K1q`0oyXl?S!7#rTXXK((;Tjtf5};B-4NYb%G$2VO8x+B}knD$R{g#NR zgD5)E*({ocX&DZSWQhG!{XJ~y@&nqdcbulDr+@eU(?txCvqBp7>WdsJ5(he+w7QcY z#i%zsW_`Cqcaw<}bcmri30uJrHY@X=waluiCdjJH-x!4L^4y0Ie zDL4i4bM`=j+#+CkX_E)FLc?dSDMzv-GvWr8QsL`sUdeXxW76DVMi)9AyQ=_;eR98YScTYXdFN^8NOH7f=oZgWs*}6|cPpx1gZd$QnH<1Di-JI=h z7zFv0kaBLCke5I6>`J}s$}!ha-Rbj&E!Uo74m^WwE>6I-xyxwV?2)bWO@+-_)#8JX z^oPX?ToF48xy>QGRMQ8+><&I92eG_UkF3hamd$pw7-^sjh@@OR1I|-1J;#{Dg7Gfc zFlByE^mkSSPjRWGymcOei)Pw<$4@_V&IQ*sh~$;Gk*D`T+89u9?u|KO^Y0M(Sw)jy#L8mU65bZl;72V9psKjBo7F`5 zn??Bs$qQB@r6 zK&w_}R@l}ZORj|D!+f$1*nj&)d4)p^zi7J$!)nJBf>d4~C?^?@ETsvvIBh#R|>%G+VB(N~a=MJ?^#S4ab56#eq*(nzRFW^JoAc%EWB zHk*=;zhJeX^BUn$gDL#hSept6{WN{a;xpW4g!DyuB=-gC@>|C425KX{tApBe(dzdG zt@p9B?hNe40BN=Y$h=kyRue5#;sd*uRUwa#?xxDu@(`w_VKjeP4kD***SGf8?284<``4dMzq>{sm z%&pZ$3M!&j*^$&%fhcdZ8XwkL9SGCnq^t>Oj9gSQ*CJLR%h+VwRFo%F6EjkDRnP6B zK8*g+sM5mJClhn1NypU%hIyk_xWWiR+&6GBG>HNASzY0>JIh)}+DoRcRRk@fbml?# zL%^@#TYG<4_F-&x{JPZ^sy7pmOz2vQ?zHmw?x&z8IB$VBJ3kiL8^$j|c8QS4Z!RP5 zpn5Ali*f8C9kTt{ao-GrFLTdm0p3V9Wtq#6=#`HW7g}oXq|lLeHf_MD=tFKr#4@8% zP@)sr)ug=h;i8{}-Vfy@owD+gteaoRS-JI36b;PDuEG8vHMbrBiV(*+j#!3kmW6MV z_^1P)o#WQ(KyE-_J3$D8@4o2*hr% zUSk7Ksb$-9%^jW;rl=@6+sGShRHqrMKQ`Qr(=q5j> z{tJr({?NCm+?@l#Z$n+(CZ}f!VtYvxJmdlJBB3GviR^iQnZE?-7&5<-aazkn#<=xAATe{yHl)nSp?~9l#(Z{;}M`s>iDxEgk7D zsGcfY#V;!AV>&r^%S(Vq`-ORG@PRn~z#VqWJj=CCCXd*)n3931?R6~KyDW??B(-p= z^w*WucPF`mSKTHv-klC=uUld*(dS^xD>>b?H10N0h{Rxhag>$?5w(-z%51}gJ36?J z_#Nhv(KGy#3>zOJkfTyRH5lapgLwBmkN|E00Y?I%8eDrM@1hiX{Bbs+nzuxdx2DIu z4>eBxh#x=IrNH)389PE}`jN@kd0|m|87mZcc;iaZW|Qu`b50GU)2P8K(}3JVSb#O& zC%oRq$3pUELoujQ)}7zpt8E+hvT&~}iv(gR4#`i(2J-qTG zLIy|f&kyE>$@=;?so;4=s>%|+^YVUTWI9n=+WXwd(XxSVHP5Bzpv7u`sj#>A*{kRk zo4Qnp)&t}QMTXef#ZV%(HZoq?hn<-)*Hawk`$ur?8z5ex($o0v=2;QDKy%@nqV<}C z$u0usv0vzOPR|LJ5`K({9!R7|4RAyfR*T@9{Jq`u?TFi-;vLxp>UleSfj=B23EWRd zeqk15?UbE3(l%jG&E+*&~Pv->(#T7xGTG9#)Cm~Z;iKrWBUG9DxF@jv4g4K#%Kd`w1h0T zy;`~+NE&@M%kPVMJIHeaoE(jC(~)IbthS*XQ!z1T%1i{D z{nk!@L$k9dRW!F4dyNmhJo08SxV78t6<@@MWyKU;qLpD2RC%GM3luRfbgLZfU_kZ- zDGnDK&45{?yk0Vklf8~QX*cx~4TY`Mde1@ua<0DZCD}fEu>xw2h?2`R{snYtdS;13 z*9haUd3E~avNyVu_WgW>J#aXjs=56U8Nn>i6hU((XCawFzlD^qV*5ZeBCX(B>XPFU zt)$^oF2a;<3MXOzz-hqz?4oRbF?`J6bJiO(_mgX(0@IXqEMG-i2fS5-ubq|*lkJF< zE0Cv9;c-r-8?yVcRh>=1tgt{2l$Py;9mdAY<9a*`UIe+3v%*Ihc{Q{9hm){$Uhqd>W&|JCAjR-6{ zpDp?|O~68x2b1rKxWu?cP7Q3-GaM}na#1Zi1qbzJokjXIo%Wxxeu`$?cmL%iMqLaYo`p(-kVdMrlwllS8be?C+~M48jfofWip7nY!~2f zyoD|@UZdWv5hd)_xQDKUag~t0#;Qu{r}Y#)#(Lp5P8qRq&#criDS&FN9^1)}XEZ^5Et#}KhmgV-^6i1b`u120Qo}(hY zzU0}IHG zVJUiXC*JZ!#I0NTI**%>&M9=?*Bpt3J`7BN4gvj`TCHYikGjNcD8EGfe($-(qLjCn zY?-d))U@H5{S>aCj;hMV6kKcB9=vB{barrTeYGb!5BPD2(pVVMkN}SHW=IF;sC5t-{1MN9LO@MjM!0axY3vnjHpP z{K(t7@)Pj{3{mGJCqVRf=T@1eas)i+t|YbxlHHyoxatOxg5pND?Oo>@zPbmcrfGaT z6(NKk@DMXm>$lZlG3bevK^!nLK6Kui!{`M#GzVl42ny=2hnzFD%`u~q>f8OHj9Q$? zGc%9ukY0JJ4BL(p+r_N3KP06yXbq1}XA3Qj^WVK7cc=4ga{E+zg6xG0Vx`G91(CA+ z4nCGmvR%9GgTSr^oK2l$Vi|Jg-+O*0Pzz6HWjr!gL}XwOH)$$I#5+bWqDR+n6YjZP zRV2Y9*jG)T_F z)MoXc4*RS!bkDW_z4t2o&DzXe4l&2c-z_B%6Cuokr_=ZGRijkPH{hwQW9wJhv-4hm2_dnd=2q`7XDgq&SPK52%nbO+SDV-(#a_NAk6tkj!4V zerC3|9&a%CQnt8clH}g^@FK1wiE&F3QMR4@WHr>kC30N(wXf?ZQb1s^a1t|0z5IS4 z%MO1|PmT%XMLX8FooPgK>TWx`i!b6#IAUZ}|jtVv~{f?)v=<`X#?DaWp=1gEl53tSu2u@3r#6B9wxP zN(ft7B=AZ2$9!h_)5kncC~_|lg>+LA7J<)QVfEUy;l5nC0-1&M8Ij=oot>37vkm#X zy-YjhGBsq_Wl?(mn9uncJ5tH0?;UsAX{dM3ia~ZM%O-qDd&Cs1tuiHl9#2-B-ojR< z#Hb#+n3qU1OAT8(yzMx-WIEl8&5Q8`l}=T)C?Mj$-SSzXB|I2TC*2*7%r!u5X_f5g zLVa!%TrA(r1?4Z{omWE}Ep58IdOYUSWG(gDGaK8-z0F8Mvs77Y8Yf_@yp!;@uwKTU z4?P_^ofAM<*xBlqP0=&1Ki&}75b(95ClN3yiVdoHCVdlqW$bt2_hEQ(NYUPrOCeb# zDD@_c% z{jG&Tv>i@9{zR?Gd3&(?>0-{PW%#KEcd%2;xxJ6{N9zJT?wQOqS3b$N@3GXUr+Lph zONP#8iC}l1OI(lAbU{p3A8BRB2y7Auhr)#7L7mU-@_NFm3vCVrFf zrp-+?*6q)Se#!>cBMnqp$7{~eZd*3BDM;R9g%!lesJ}yLE*fN`zwktv#;v<}2NMZs z0T@6lUkmk8E_NBoe>~2a<$u^u+r7e5{n1$l=Y5dyTYIkfqizK=gs9{RZb5ic&~H>? zJodNru3g8LU8Td}(~`V+^Mwd!hJs143-Bbd<6MrXl1Yfs98lK`xhPA2Pb|~)CM6}W zzIi3ijjwjC+)p0M&6?k0aM)fKS!W=em#pJq(xP@B(WX1XH=md8!^io-d;ax01YFJ5 z(icEFf{&$vV8s|PCkS}$cpS$u5xV`fB{We6}eZaDH9y#%Z$Uj|K}W@u6u zxSFZ%iK6^VbNUA$|M#IvB2vR*_DPP&FM9og+tRQ+96+XUHd)$!?1JPsKVjc5IkPI5 z^&v2+klQv#cGCR`0H-%>IdLR7VnEIB93;?&yHiTk(CmL6Pj1Gy!to9hV58)nj9jHB z-Kk1Z%#IA(FYD)jcdw$&Sor#0bj}Efi}!;Km{Z}dilD2aw+zY>Qx-FumC6yz3|dSP zFTKOD`H&M}MN)(`3D*=ZpdrcDKYF#|Xt>BUww#}y>#?Zha^sGtlvx*s<%86s0&M)p z4BTlZgZBE66(9EpMRCR-mN05kwq>9L)Ua@Rh@J3Gx8;JK8(G?aK*SNb9~`i{bAL~w2Ny*URzD(~ zVV>G`a2ecxqaDOm*_xACn z=x#oP6le#Z0?2tMHDis7t(3~|g%#go<=T<__1ZhcMs~aU%YAcNiU+d>tS|9<^)*0+ zVLu&HN6Ps#4Q@2F<>~H!#yg$^f{ToozPg{jke?7B&^C0v#Y%FX&M4wpLTNrvlz)x~ zQls$E*K=HryxFfbO7@m0^w|3C5vz0_pi}RIlnp}!;*>{V#pb*ECEt}3O9J{5%IU*H zlJtgyc*oGjOa)Yz!BYMO5UpGT8wrm3Tq5v8Cyvj+7T2zq=^Bc>_ndv{ ziuRd2874u(vZ5Zt$Bv!4A#eDhb2QZ%BCh5Qg*jDBT_eh^pM$2uHdUEmozp(nBKp1} zdR&YV6Sd@*PyTXqR4%rMnSAk5taE;HQ*A~Z1>`#esD|O{U=(8yUl({uqYN?djeeOt zZ`^ZhiG&?9`v4n!1Ols`JE1gcj!0{ZOtl@WS(-*D*Y7Rf^^|p~wOchk&?Mwd8)ar$ z>YX$~?A4hUFXkAPWPOjkD%x{q=zOOr_25qw-9vz3OGRrPz?po#-1j`Mv#bxpK(is< ze$J>VX)2#rH#4puI#brLwM>EwS=#Ud^O$KxNza3t9HtESD#L} zaVX!`Sc*xs56!yiNmMjTgp;%6LX0IpjzBO-gtKg1lh7QlH(6p-S2vXU%$%X8XY%L# z+;b1@(E1Rb5w!41&nC zqwvz#%o3xwK5vqjI>W|6T-ClATWFKH)|0=}H|LkXVgmsRG^*RZZwu>nZa1bcdpDIz zPyIN^;H{Doy(qizZ4jHg+kw{Bg_rf-ML6KD)~ipE86{__;Y_7}U_P!ZWQ?v#qoX5F zGvfC`{jCPTq*M88)Cljzs6Z8xbHX*Df*?7I(=m{5O`oLquuN8@F@O(8ND8O-kd;-= z)>3dF`_S3+5npPyo9O^}j>lx3=XIX!Dr27=pQ805-U`s_HQkaGRR!#L$ue7!kj4)0eGAB4p^Ze;xDf=jcHE(&?adBhZy9 z!I&YP)j8e3XKbRPxjAgW2EO7FBuDElqwP^5l<&5x(U#N0$2TK0jQnuWLnH)_5~MbHxS6enuwyb+PXYRJ-mS3o5`JGJ zP|r$jNUz^po@Bca_btmOj($1ZB6~*au@-;RSkHIJVItGHwifGu;^vRR^AC;{ ze!Ii!elF=>AItIzXph*>E9LenQYGBQxKjOmeB@(U;-s*0?8o;5KM2@*LO%+D-TWD| z1`sAdJ7M=PA1AXFGvEXdCGlhPuCYRqLRUySn?e5R-fKsl0ADP$G1JoetBZg2>ly@z zHD0QgMA7_t&%ZTXLmK$5;NR>1HYCTd@!G%R^6z~4b@c-FgaqIO)ml&&PTHOC;+_ow>AIzX&@%Cz29Z2sLrg)`Nd; zK(dsbo;#@d%=1%D%U62N_dAy-`Ujc(sY$G48z*lc(_g z;y=q6{^LP-fzXZwYr~NTw4NqQ73f2wt7_ZpO>LH^-*Ug6Z;m^VTQU`|me+NwSETl> z+yiQbAAgm}dHnbJ3hYtm=tmm7wcxpN-6JY{<%($C_a4GSJo3Q&<7aK(45fzVteQME zLM8KuD=A;MFMO6AQuDV7f4s@z3Ij^au-9n?ErFL_aWGndkFc;hD?)KRs<0YwyGabJ zcveiSb8`9BHTV0rPXO-On6xO$f3)MQh}cOh`#Sp(ZR+c0a~q8jL)C(yKq&n~Cv)Cv zUadq!RR3=cxi0}#w^SNKw5=WRf^90%9=ZhoyvBdr3jDyFFwDQu9M`nvCHPv0u=AK6 zMV0(Yt`&5NZKNPfx89>2mHTqxp=YNyaOg+FP0YPYN0pTB!~WOd5`S zqcQ3AX(=8H(4BC1th4`0Ix7MWd_ajF7kGr|`w(P=ivA!YdH*ZG&@iz){)bQhw;}4A zrocSbnKFdJV;=XdOGMUyFRox^^S>jT+@j9!=~J9BfehB?dLS+Fye=|#XY_d z>X#?+Ht&&zG`nvR)vs;42BZx?* z@e0V=82z&#BjB{7a(F&3=pTa=VP-zm!6=qL)Ikl)N%}1pGXSXwaloM6TaQM-GwgxT zi}^v#aT1a0D?Qp8s)5fHH0%AdbAFvaFA(u4hKO%sdtGHbj z9eI9}ngS$007S&soUUSl(v?rPT|YLl=D;nO-;HtC(<|P(^cZOrv9SKs=+_%9UK*)& z4jzRn%d3H^+^}OEz$7d&1Nuu2h&yt2e*`f)tx3UO2)fU)3kYqEF5W7DH$1FKtEu8t znJ4#U23OZ8Og?%r2&jqZ@=M|9e{E+*Ajnvw5YWxY)hX1SEEcfj9Xj1Fs&GAHs97>> zNSr0G;2i9p^WGOc_VL*lF!RFaOe0Xc^K9AyyzW{yo3~Eaq+I=(#QV>9I-diM_|&%5 zE+(^H-520_8)!*3_rh#@gClpiG2w&DMcZ2xI*cO&FU$f9fw5$BMBT{k{wjfcU%wY* zxLdcX6GKMbKhb^cMIUWTysdHO7p%&;qPNn$qG;u?aQzrqI*9e96HojjN&fdy z>9{mfZuw%g$}SkF%S#u`w@UuaXVkF7kzbt3!Wyk>z&qQ0_WHw&MG7N_sk(V|66Y|Z z6*Q-!dNDVL9bVq=ip>L5?ONi!eqtp5GEf0>*8_K!@QDCi+&lGSUNQ$-WvBSNl7jY& z4QSAnWxY<5UM0idggIYp1QyW1>WcV1g&tDYz@i_N0>U~yXlFjbgq(dhEhp6+;JlIx zl%Q0*Iv>{t{%vsu$T?GV42%KA4WpHwQFd_2myQO6M-9{tb_^fms!7mlTS$D)_Y$)S zgtNV+!0ESw8nJ==E4ryP#zzro0o{{j7?2fcGN>)azHHvRJ`P1d(!9Fg-}| zCs6+{)4+XS&cG0phI{yeWnV+SNEa>0531HYSsXWMk5U6Z8Vf&I`+cZBlHWqKD@ab~ zoCpD%htAtBDDW_`GEv?zZQqFdI^7aHxzZeXYPLC$WPL=&!KY*<)>LKjZrI5jRdM2E z!OXXd|L7u6sK&oDQ4|mxWVkxJlk?sEQYpZBzUJN?-GTc(pV9mq4@D-04#&t_m6GXU zdDii{}1P;K-s)KlBKfcOqIvVWa@eS zYVE#bi$Gx@&>Rc($pa6^VxYu<4H7)bGF(m$xthx&^^rph{!8PEh4ILd)X`;o?gzKzdzW{L1)%D_#V$ zhnjs=^2%ayGQB$IL;{5|dGD%bMj={I0RN?d1?d6sPt;hm|Nh4@?5`gppunUgmSlOI znw%wuPZhlY*HLUw6*TiiEdHHFH`N5c@M|4$EP%7U7FTaad%Ait#rv6@z%gAOGt#M3 zOtVV9K*4lZ$D~ZIZeOLL8zXoe#O*airf(Yf`Cz42d(+g)%nI8)OHWTVAj=rqGeR zWl=JbLH~+E1NF&EIQdKx)x$b^HP>X#v4(wlL1}HSTJ6P1KHZ(!aMjzgP?8WVXUPCWBr5dAAH5T3{to(vrSEA8Uj69W*Oq(U2eKPX z2@5EEPHmARRBbG$x@QR4JQh1gs>WiFQD{3y1y?SHrbOX|n`XsHe#zoGre;m2 zLM!-%P7p=}d8_t2;Jf)2CjeLYo8JFxy&ij9EQtXW5V4IG@?h{RmD7;5N!@axwo=?| zA+1YYQOVeWxkjhX>wviEY4$){AcjHZRTmvb?NnzTpT>LJeg5_DTOFUEtDu&Kj97`+ zHKlnJ`eOMy1oil1>Ob8AJath@{67{HOGLl`o8}#UY0dS5)maLoq*>&0db{-QWy?9$ z3Ev~%sC~MVr?Y?{?X3Ydu4_!;E?k_Tf+SH@P`>6K$~;awj0dK}G+%MDNE?ty`cO5X ztsOS4`$jTg^w&-D41<$=v9_El>sMe8dE<$lPqUfwbtQOfc0L4Ejf+fZ0rSo67|*lp z7Bs+Wd|OrBP1RkjbUHTlX!2;}D9I>f?+jVtm!Y~~yG?;wYSA|DkbVr~s^98@h7tdGjje^mG#{EKNmK|w1p{T!fb z=W{d~7tN^0KRB8xo10`k?tjN+p#1UXlZnqA>8-Jb`KGF%nnj8yJ~;x@IamOTv|MBU6e!8{~Jm_sx6zaEMyKuY;UOxn0$E)#T?SHs)$M4`_BTyjw zZOsx2q~`wl#Os>CO8DKAH}=E%FtxVq-r|sO0=tD^CP| zJ5CaiWpYFQ`7oe+zaLjYxzuR#{M)_1c^dz|y?-a?udDsvwfC=S`Ne$xYdQbj_kMM# z{@vF9zwNcib-vSa{*v{4HbQ(dzXrN9-~T}^Rj7J$xIOg0ovM>SRO%HqtUFzH%J^t> zX$cOnj%IUkmgzW-6`lQWUooE^;O0mLMg6ZECA~i8OR1_H(f)5A>_nhM;73u#qC%D^ zmM}RmIKq4W%Zmyzo4yQrk(n;dClvqXKr--Zhz1r@3)c1lg}%l5@mX`MQeIB|Te?f( zk{-*q%?^n8iUQ;et@x#pj{oQDI|b{@-X)QYB}JI)&e8p#FYC#{M)l4E`UQF;YV5S{l9Me z1@Oe*n=HC7l1T^Mx!~k3fCORD9ZQa7yX({pn`{`ZU$~P4@XAk=JqaFdMAYANo7ecv z+vi(d3TQGwHibI8vlq+Vmvq+>hRRj{5eSmY$UJQ*h6^q9#yax*bxM)8fl|NC@7A9Q zssKrkaCal(K0Mv^JA8F7Aih-?Y@NyJeodKRwuFc2u;GROY+q zur)u$(d`5O`0}Fl{Oinx1xe3U5jyF^*WSCMHr++>)`=0U;y$G&M~O?_)p+CNG{LE0 zAcU9P4$#XGYyiz`;nHsJZh%~)$G7vfWk^D%!iyw-5|MeOl3rb^zsjBRbof3ona7zi|WT+m_j5#^o6<_BFWbx7hXn%EqZ&0LV_&nNgfK zif*sg9PT(GMe(NbY`4AUd+7jzuC|X@VoIl8vvNzIEU8J>yCy~WjHyP(*SBGUck)tP zLhbM+4FS(73MCx4%DOAzNy(2-jI+s8#9YXki#M8@3ricJGR{L>_k#`hr|iOmG5`{& zyn8cU!d>df-NCG2L&K7alT8~_Q@7XZrxB*+#eJCfrg|r~qv*)}DSM4uzf)FMH7Jvb zFTMACC*iB)m5Gqt&?oQJ{$V5XV}Nz6@@Q5+l0pTK+$DZGSv>s}N3%??=l{M7fpA#Q z3F@t;?Z~J*3Ou0hF|rMrn%I(bCDG!25mJmvHHKz-xzXV&3#U}USDW@oYl&-qdy-Zg z#irOXV`8Q%_L>$B#G|krW=6EYj6TKf+53wYlZI=Wto#q7Ig*1Z>EbHRP6q$uY%M1r z*o8I)+1P||Q>exZh~iC{De|_csHo}Iu;0NVrq_5*G`ZpHChw-eAU5B9 z%54k0YBx;EkPmLKPG6)fLA=E58Q9wb(08tQd%a#rm>9lZD(nGg&$Iu}dD5=sQEhP-i z&sE5it{|LgzD?&1rxrVDADJ- zar!QAs;B+vJ&#ibfPB&3x*+wGt}(OUuBz*><2sV+a;_sgIrBPUvVLK+g{DW}x?R9r zn{%AI;Dc+o=t3WjAqFcsRAScVhSsr>g8R347rA-~zSIL&ZD^dud+8SeC{kl5FjVU8 zmM*wzP^&r#O`^OjtFGPM@yg0le%Kt;W9jX$z!sffHR5*5mK9M=ZXw}35K4}q`q1C? zas$!3%hHQ>Tb^j`G#o4GkFdCMha)3#eV)oD*&CU+FcZozgyA5)W{vo5gIiEfzr^L` z{vLv4&-|TmLYDB7JT#H*6HO`T5k_f*^ei-6fwN4--bP8P8aW#6Z)=4eY*MY5*|&z! zEzkrUKHuNx_g|lS-!Qy&P^gYJj}XJ;?%w?meCqG_iOOkk%fzUh(Rn3{S1nWS!3H4I;iSqOSr~$g%V_OM!lG zpw@#`%0jVRIeLxmgRqFQx({)s)_*Nrd*qWQx9d2pGQlHtXT9JnQw!!JTXy9nGqc7) z(_Vn_QB#m?ubtJ{7KYpEnOa<|@;kox1;=2Dw(?bRf9K$>jr?i%Lv$v|5>lvt^@CmE z;pdoz8rvcuepuP#jZ*66ND-cL${aSSOEjSf4RW9g10r<)y!1h>9~a{SS`B4&dRy*` zFVv#(4v;k<#|=8*B-Of7R0KI*5qp}upK5fdywvVnN!oN5uw4Hzt#>DdL|_T%@mx>g zXmkd*PB2Bqgv7cbs-1Qq{*G5jQgnmXv$fW%{Mo5Hjh;$7b550$JsSY`_=vpX?)y1d z}Pn zTQbwniMYQQOrcM=IbP>g76AFL+}7XnJ1TIQ!KZe-hNUBSZKn$CZdE>m@aiR)w7ja$ zxw^MEuE-|1LKVyJ_e}xL4-0JS@JiiV9qsRYaoYCDcW_dYfP(X)FG+#ld@mmh)Y->B zKo@jG(U)*Rcc4dYNxI1C4c=l)6FIr^L)F-w)&z%NQLwHowEOOYPOeFoDPC-@8Gg8V ze!o%0B=`G6yS|sk%B^R~V+Uvw51#JC8Icx-7lNqyZc)X9Eq=Esfs@G6C^*OhotI1cp~zPc>ZG@2)|V7q+&6k z#^1DmbzmVjKd0V>AQFng_V|q&Bxb!Tq(y1D371yOtkrX3ByUW-{pG>^B&k<|ssj|cCp-ns&P*Y%+TXI%sI?!5n-5kdvgh`b zIBv!d&Q1ADIDdOiT{TQSnJ<@-c!8o5P26ylMV)*8h}8}U90rlm!!^eMg+0bttZB+= zBm*_G4}(|KPfL9hBycrVTuDU20zVDmrtKThbw*YWMLMMJTk^0>g~9{g?}-$!zzPz$ z?%o?e&tt-hC11?TUR1@u_5RjMU+PKwR!t>$;?DASIh{}y_0Q0iVRNOugD<6-#uTB6 zDTc0Vg0d%WsQhRVtnA1%`)O7EOgXu;)NU3$uopb5U&$wK)ms#>rPRM|Z?rL0UuKvh z9FP(hc&_wG^iDOE>pUEE zVy>qz(W!8=6=yZX(EgTi+x1m~sBN~_TE%dw7=2{T0dQz!{it__sW?32Z8bVXBI97= z%_sDzkOn>|s6{I>X5s5&7F2aAGk(}iR#nDagvd~vk=?^IT zn+j(c(T5wwgSTK0k5?~nky7jN&s;Mw2O|DUt&chT-kBa()lgAYwHs-TFi<_dV!KiA z(K_YR#)k`Gstr07LR))-iYg@JS#jN-bmt2>cQ)}33lDYwz5Th4rw4@YUqvi?>ODw= zy+~bJA7T*c^2p)-4s2OXDGCnypBwZo=F$N5z~ZuQiOzG^ywm*d99#n8tb4Yk^LD3R@HwV>)Z#LqL02-Dt*TC}ejvUZT% zh2?>@X9}wB@uI$3eGqpa`D$zGiPNzh|JV_w$kZRX?-mw*O%#+JuTSQktu>9$%RFQ~ zCw*hmhUbT{K<2s`@UE^#J#y?V@cQ2VV)@{1$duk-Y?v?fV?BPVmd-t6wUD|8(F-5* z=C&RCRWvH68F}-L+4@9vg&OGd&NxoRU0{s%-BIe>vP31u?24vJaXc^T8S|?f_J5As zk?s8eVq)RzO=UfjwniY>URpMEo~MJai>)mD>Uw+kSl180FAllDrQHwj=W=zkG81pe zhR14GWIl2vpaV#wphpU0O*1&==HeT5u8@1MF{)!PA-~>oovQN+R8CNq4OficPq~9O zZKZ=06u&#jf$xSVH1^&ion8 z{+>lDr8}h@*x~MIXK4~hCCG~h{uC^LMAW?Ky&n^2j~U35!&^<(RIC*f&dKc%4`+Km z_7H>T#rPJLEe@qH$*K@WznA37cR!V3Spp7kSLw?33s|Dcg{OR3Gwoffw-ap(tvmT@ z3$lQoizuw*3xlTEK-JdlYLv2++}LA4pRhyw;glHCRYpyO(Jp1fO+HTM`@}VUdL)p z4_YdR+rvjK;0HGO?5Ie*NjJiEwKc>9lzYB;KYI+B%&?({{mZo2JAOiwPy4CbY%1*Xsdny~9e&?R2YDcUAb>pSGN49fTWw_xs*73C=0xRxDP{yxLL${v0?~O{iWw`8RZ=!F+B<`L@uh=mp`~MYP-A0OE#Yb&vwpE( zDOr5a*YMb7lZ9KR`nZo|Fr-#ad9RGVZF<0UO)D_VEJFMX=n3zCOeKZCZZpUuHEX?i z!`sguHTCVPk1b4R>%aVxn&0;~Q0z5#sA2KA@R7H`D%;z{C?2EXG=fQ1h35oF}*{9C-t>vXv6Yj%=ee zTYai8rUQq`FUppLmzEEB?cZ)=HlsIkDX9ctyEXgS@Uj(0IdN^1eY*GE4_LlLFxj#` zjwrY#J$VKzUeQcNx0hv)oAPk1-O6U{cONJ+o}Ap9uoqu1H?xveuQAgazJv9FekU<8 z>lO_g1x&tTzB@n-(HxSFNz?$nyWhBfR(-6|zTRKGfj|?dj!~Lmmj?6Wj;dKPl&v7i#HSPF zSpJq3r;jmzSWm!>zcI7QaG0!-OU;yeNS@g(x^b;&qX>22XaW0huRmK!iS?F{b>C;& zO*vlhiKXhwqJ!r|%V%=(^4fD0 zdN{%IgLMsiMo0m;)@>ps4t{&7Y3RLwvwx*AopS7QlA{Ssx3V;rS2yuMBI&kSjZzXW z)JLgIbt9sbJ=M3g8JAcxZ0MtDt=tWev4LcRQw=5lUrjc?}Bnd zshUk`SCViKwOwB1C;iyk^}5aj$1y~oL4>Evi)5#n(=mlwqg0C@{36au85bK!t-2KH zJar=YGwzmAt{2q>^!JXVbW*z2$LUDrZT2ZhxK4C;-y#xw9*!#rFvF(`k!Ds@?H?X+9a z0OW%Dv_RaWrtwa`dY+ZQOD`r6 z40=)|*A|i-aNRc@)RAE{YA??|4d|KN^Yk+{uVuvMVEaP(M^zs)$82HMU&X8o3i@=x zbC;K)!-kf_hJL&Wx}!Hrnf_^PWwJm~NRw050ix|O2~{FE_}%_ST||@bvr#*IcgCb9 zW4f{Mga_6T6U?2I(FmGboTww#8G3G!`+$0e-9(l1yV}0n!RM~p)UK9>=3P3_znt88 zA(+KP&#}H@(tX?>H!>TkX}yPhtuC9GX#T0E^c&4NVZjSwT$oJEt+=d;eCmmVbE4nR7dgJhFl>^$=@Ny@JV^+B+o`XuM|3>o>h z2qqW+r;AEkjO^#778v;xTA@->E!iZo$t0pjH)80c^`#;+7pPl`wGhAq8wKoCpb|wp z1(J6*uC4SoJh%MRm?mabq*Y`%vOGtSfaVtA=l9S7dxkFR()v_G6}F4(aYBQ72y*Jn zo=gKPQkntQr9-02%=SYC34)G86A)eNo3^8+=2LnuK|h$-R#t%f^(?$8_+v2)BnIW> zLDw;PjElO?kIo%{A$lGO2=r6{Ikq1ovu84Lz~kh~>r^uTF&qW*x-+2Ly{5$Q`^29f zJ4SZ%NJ8r)pK#2hf2JjmvXmi@K_t7SuVC|IW(0WK(MNTN_caq5m;ZU>kA4dWGl)f6 zU3v3P|8o}~Iq52Dz}NfV2mWsb|6!H?w?+SV2LESd{%L{#A6U^`BMHsC))t~9HHcErc;n8j=2`bg%Cw9T8{;A z-o+4#BpzZO$%BIIqC@aD$0v)p0KR7N7hFG2KsPxOi3IdG-^%$d$_fU(CGr~D#y(Pc zkZ}EX1Lsr1{YhHP87lhs@f?#DA<%_xy=ySeN}dD~KaFXOmhgo}weLvhB~1wYsqUWA zq&JkFvK$i|c7g7vJ326tUb{-=c;WW?yTeAvWQ{_QzQ2<~=i$WAcLNz*(hvF_2d}Eh|(nC1#$o0{L8L{vK@Gzx0%q+?$^d|2|h*c8GE_)hbE}wT_N_ ztDVxCHS*>C89r$rg_5_XJyTqmM#!z)EbIEiXR%XI=apf{X*7M^k7)t0rOAJpt`t#c z(ML|Jb!Gadkm=8Bs!TlC2^Fo%oSE{gY3o4b9Xqn71pWEg(w?jXtSUT5 z&xCGYrROb(n%%iabR$%Txev2rJNwi=t1;#5FMGARU&y2R*<-sNxIF^IZ>f=bh1F7D z=vYsGQ4Ep$WJRXeL`{3fhSv#j{Z9+g9w_5==3@jW+YGc*<8u=% z1B73oRQ*VANlejfuH~99>j?QzZ>5cXb}*?{_ps#M_Hk^8fNMSnnO;qqTgk-)T`HUE z+&QvglSFi&?!8IB4?I6Td|l;H;46nsiB{Q+gNaWw>pP}9K{R&zqq=G{89O}JZB$glch&0MuwRrZ7v@Wz zih}A>yX70vl$FS`ic>apn6-AVpSGY(A^G9w)PaaOIt9*MwcHf~xP`JuL)+vDw*3H6$9!J$9emDNuvGfec;eP4cv~H zqxZfYy{#?p#VrDxZ_|BW*-la>zXCL*hqeXa1J-qYn4j`ZM==QMAl zX8o2)rvI;Bx=UuWwcv51$Wz{{&RlL-dAYK?D2tqK_5AOC-Dpj)CKr{>IXJ`m^Z9xv z6D{MV-zxZ6{{t+mhl}Vl;Jz<}NF&obZ5&+Rt`NiBGlCS>=%)O6-0hZuz)a}^EktA) zTtDoQweHKy1MeO0orJ5IFjd5OO`8wps?Pd0D2oXHyvMHdlOrELbd!4!Qu1*(KZYT+ zqm!yCitBY_Y$W@@v;xxyI;wIb%Bi31-?2d7^Hvj8%QOD)_p`kFP7cmJ=||Ia%vR1g zziyZaM)VoRz4FsF4s64(C-p-`iXvg#pVN(3Z=8uqHc3_S}eWUmU6KKZYBV^w--cZ7xO$PyN&L$X9N2Dhkr_*9QjYtAu=vu zS#>e`CWG~0Hl^0yFT4&dFlmt`+WpH{9Y1MG#gTn6tmN|qPZ6XO-x!N-DAR{YykELF zk0j;L^8Kzm_>Z**wH8C;PR0w)a$T)xRTsrCsYyl~UMwTq)kxOY^Qp+BUq3N%1oEPT zJ^p3>)sIV0%>-*B&G=qka^#F}9C}a9IMD7i!)*P<7M&w29;u(p1cN$*+!Z z0nOvFe*-kX0Lc(4bra?RL3;T=v;h8X7f%899ulwc`SkzaFZthtM_coU)X^U^{L_B` z@BjbStPap5bx8boSIXa;E2Dr5nP{WS9U$K)N@qH1MHU039dm=E1 z4kW*d<03Bo$4$)*X>8YeJk}bgZzoNX7#vsHFsujIiYLPL>Yt2`kubswr~dd<8O~D! z3F3}lGkgn|GFo%AAP@_0UhU#WP(PDSk`8D9Y7E1=l@3WgIX%@tX#B!y-yM`Nt~s

sFCXm%tHe)q5Z2AUru(2%*{v^q3Qig{Cd>*p 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 z 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 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/screenshots/messaging_screenshot.png b/third_party/convex_flutter/example/screenshots/messaging_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..45d93485189d5394746d14aaf442b6da1fa5694e GIT binary patch literal 211364 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

&vW@MwT2xp@DsrS=XY4>d~bGs+$!}+^v=0sb? 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̞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 + pub async fn set_auth(&mut self, token: Option) { + let fetcher: Option = token.map(|t| { + Box::new(move |_force_refresh: bool| { + let t = t.clone(); + Box::pin(async move { Ok(AuthenticationToken::User(t)) }) + as Pin> + Send>> + }) as AuthTokenFetcher + }); + self.request_sender + .send(ClientRequest::Authenticate(fetcher)) + .expect("INTERNAL BUG: Worker has gone away"); + } + + /// Set an auth token fetcher callback for use when calling Convex + /// functions. + /// + /// The callback is invoked immediately (with `force_refresh=false`) and + /// again on every websocket reconnect (with `force_refresh=true`), + /// allowing dynamic token refresh. + /// + /// Pass `None` to clear the callback and log out. + pub async fn set_auth_callback(&mut self, fetcher: Option) { + self.request_sender + .send(ClientRequest::Authenticate(fetcher)) + .expect("INTERNAL BUG: Worker has gone away"); + } + + /// Force the client's WebSocket to reconnect and replay its current auth, + /// subscriptions, and in-flight mutations. + pub async fn reconnect_now(&mut self, reason: &str) { + self.request_sender + .send(ClientRequest::Reconnect(reason.to_owned())) + .expect("INTERNAL BUG: Worker has gone away"); + } + + /// Set admin auth for use when calling Convex functions as a deployment + /// admin. Not typically required. + /// + /// You can get a deploy_key from the Convex dashboard's deployment settings + /// page. Deployment admins can act as users as part of their + /// development flow to see how a function would act. + #[doc(hidden)] + pub async fn set_admin_auth( + &mut self, + deploy_key: String, + acting_as: Option, + ) { + let fetcher: AuthTokenFetcher = Box::new(move |_force_refresh: bool| { + let deploy_key = deploy_key.clone(); + let acting_as = acting_as.clone(); + Box::pin(async move { Ok(AuthenticationToken::Admin(deploy_key, acting_as)) }) + }); + self.request_sender + .send(ClientRequest::Authenticate(Some(fetcher))) + .expect("INTERNAL BUG: Worker has gone away"); + } +} + +fn deployment_to_ws_url(mut deployment_url: Url) -> anyhow::Result { + let ws_scheme = match deployment_url.scheme() { + "http" | "ws" => "ws", + "https" | "wss" => "wss", + scheme => anyhow::bail!("Unknown scheme {scheme}. Expected http or https."), + }; + deployment_url + .set_scheme(ws_scheme) + .expect("Scheme not supported"); + deployment_url.set_path("api/sync"); + Ok(deployment_url) +} + +/// A builder for creating a [`ConvexClient`] with custom configuration. +pub struct ConvexClientBuilder { + deployment_url: String, + client_id: Option, + on_state_change: Option>, +} + +impl ConvexClientBuilder { + /// Create a new [`ConvexClientBuilder`] with the given deployment URL. + pub fn new(deployment_url: &str) -> Self { + Self { + deployment_url: deployment_url.to_string(), + client_id: None, + on_state_change: None, + } + } + + /// Set a custom client ID for this client. + pub fn with_client_id(mut self, client_id: &str) -> Self { + self.client_id = Some(client_id.to_string()); + self + } + + /// Set a channel to be notified of changes to the WebSocket connection + /// state. + pub fn with_on_state_change(mut self, on_state_change: mpsc::Sender) -> Self { + self.on_state_change = Some(on_state_change); + self + } + + /// Build the [`ConvexClient`] with the configured options. + /// + /// ```no_run + /// # use convex::ConvexClientBuilder; + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// let client = ConvexClientBuilder::new("https://cool-music-123.convex.cloud").build().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn build(self) -> anyhow::Result { + ConvexClient::new_from_builder(self).await + } +} + +#[cfg(test)] +pub mod tests { + use std::{ + str::FromStr, + sync::Arc, + time::Duration, + }; + + use convex_sync_types::{ + types::SerializedArgs, + AuthenticationToken, + ClientMessage, + LogLinesMessage, + Query, + QueryId, + QuerySetModification, + SessionId, + StateModification, + StateVersion, + UdfPath, + UserIdentityAttributes, + }; + use futures::StreamExt; + use maplit::btreemap; + use pretty_assertions::assert_eq; + use serde_json::json; + use tokio::sync::{ + broadcast, + mpsc, + }; + + use super::ConvexClient; + use crate::{ + base_client::FunctionResult, + client::{ + deployment_to_ws_url, + worker::worker, + BaseConvexClient, + }, + sync::{ + testing::TestProtocolManager, + ServerMessage, + SyncProtocol, + }, + value::Value, + QuerySubscription, + }; + + impl ConvexClient { + pub async fn with_test_protocol() -> anyhow::Result<(Self, TestProtocolManager)> { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); + + // Channels for the `listen` background thread + let (response_sender, response_receiver) = mpsc::channel(1); + let (request_sender, request_receiver) = mpsc::unbounded_channel(); + + // Listener for when each transaction completes + let (watch_sender, watch_receiver) = broadcast::channel(1); + + let test_protocol = TestProtocolManager::open( + "ws://test.com".parse()?, + response_sender, + None, + "rust-0.0.1", + ) + .await?; + let base_client = BaseConvexClient::new(); + + let listen_handle = tokio::spawn(worker( + response_receiver, + request_receiver, + watch_sender, + base_client, + test_protocol.clone(), + )); + + let client = ConvexClient { + listen_handle: Some(Arc::new(listen_handle)), + request_sender, + watch_receiver, + }; + Ok((client, test_protocol)) + } + } + + fn fake_mutation_response(result: FunctionResult) -> (ServerMessage, ServerMessage) { + let (transition_response, new_version) = fake_transition(StateVersion::initial(), vec![]); + let mutation_response = ServerMessage::MutationResponse { + request_id: 0, + result: result.into(), + ts: Some(new_version.ts), + log_lines: LogLinesMessage(vec![]), + }; + (mutation_response, transition_response) + } + + fn fake_action_response(result: FunctionResult) -> ServerMessage { + ServerMessage::ActionResponse { + request_id: 0, + result: result.into(), + log_lines: LogLinesMessage(vec![]), + } + } + + fn fake_transition( + start_version: StateVersion, + modifications: Vec<(QueryId, Value)>, + ) -> (ServerMessage, StateVersion) { + let end_version = StateVersion { + ts: start_version.ts.succ().expect("Succ failed"), + ..start_version + }; + ( + ServerMessage::Transition { + start_version, + end_version, + modifications: modifications + .into_iter() + .map(|(query_id, value)| StateModification::QueryUpdated { + query_id, + value, + journal: None, + log_lines: LogLinesMessage(vec![]), + }) + .collect(), + client_clock_skew: None, + server_ts: None, + }, + end_version, + ) + } + + #[tokio::test] + async fn test_mutation() -> anyhow::Result<()> { + let (mut client, mut test_protocol) = ConvexClient::with_test_protocol().await?; + test_protocol.take_sent().await; + + let mut res = + tokio::spawn(async move { client.mutation("incrementCounter", btreemap! {}).await }); + test_protocol.wait_until_n_messages_sent(1).await; + + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Mutation { + request_id: 0, + udf_path: UdfPath::from_str("incrementCounter")?, + args: SerializedArgs::from_args(vec![json!({})])?, + component_path: None, + }] + ); + + let mutation_result = FunctionResult::Value(Value::Null); + let (mut_resp, transition) = fake_mutation_response(mutation_result.clone()); + test_protocol.fake_server_response(mut_resp).await?; + // Should not be ready until transition completes. + tokio::time::timeout(Duration::from_millis(50), &mut res) + .await + .unwrap_err(); + + // Once transition is sent, it is ready. + test_protocol.fake_server_response(transition).await?; + assert_eq!(res.await??, mutation_result); + Ok(()) + } + + #[tokio::test] + async fn test_mutation_error() -> anyhow::Result<()> { + let (mut client, mut test_protocol) = ConvexClient::with_test_protocol().await?; + test_protocol.take_sent().await; + + let res = + tokio::spawn(async move { client.mutation("incrementCounter", btreemap! {}).await }); + test_protocol.wait_until_n_messages_sent(1).await; + test_protocol.take_sent().await; + + let mutation_result = FunctionResult::ErrorMessage("JEEPERS".into()); + let (mut_resp, _transition) = fake_mutation_response(mutation_result.clone()); + test_protocol.fake_server_response(mut_resp).await?; + // Errors should be ready immediately (no transition needed) + assert_eq!(res.await??, mutation_result); + + Ok(()) + } + + #[tokio::test] + async fn test_action() -> anyhow::Result<()> { + let (mut client, mut test_protocol) = ConvexClient::with_test_protocol().await?; + test_protocol.take_sent().await; + + let action_result = FunctionResult::Value(Value::Null); + let server_message = fake_action_response(action_result.clone()); + + let res = tokio::spawn(async move { client.action("runAction:hello", btreemap! {}).await }); + test_protocol.wait_until_n_messages_sent(1).await; + + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Action { + request_id: 0, + udf_path: UdfPath::from_str("runAction:hello")?, + args: SerializedArgs::from_args(vec![json!({})])?, + component_path: None, + }] + ); + + test_protocol.fake_server_response(server_message).await?; + assert_eq!(res.await??, action_result); + Ok(()) + } + + #[tokio::test] + async fn test_auth() -> anyhow::Result<()> { + let (mut client, test_protocol) = ConvexClient::with_test_protocol().await?; + test_protocol.take_sent().await; + + // Set token + client.set_auth(Some("myauthtoken".into())).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 0, + token: AuthenticationToken::User("myauthtoken".into()), + }] + ); + + // Unset token + client.set_auth(None).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 1, + token: AuthenticationToken::None, + }] + ); + + // Set admin auth + client.set_admin_auth("myadminauth".into(), None).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 2, + token: AuthenticationToken::Admin("myadminauth".into(), None), + }] + ); + + // Set admin auth acting as user + let acting_as = UserIdentityAttributes { + name: Some("Barbara Liskov".into()), + ..Default::default() + }; + client + .set_admin_auth("myadminauth".into(), Some(acting_as.clone())) + .await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 3, + token: AuthenticationToken::Admin("myadminauth".into(), Some(acting_as)), + }] + ); + Ok(()) + } + + #[tokio::test] + async fn test_auth_callback() -> anyhow::Result<()> { + let (mut client, test_protocol) = ConvexClient::with_test_protocol().await?; + test_protocol.take_sent().await; + + // Set auth via callback + let fetcher: crate::client::AuthTokenFetcher = Box::new(|_force_refresh| { + Box::pin(async { Ok(AuthenticationToken::User("callback_token".into())) }) + }); + client.set_auth_callback(Some(fetcher)).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 0, + token: AuthenticationToken::User("callback_token".into()), + }] + ); + + // Clear auth via callback + client.set_auth_callback(None).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 1, + token: AuthenticationToken::None, + }] + ); + Ok(()) + } + + #[tokio::test] + async fn test_auth_callback_returning_none() -> anyhow::Result<()> { + let (mut client, test_protocol) = ConvexClient::with_test_protocol().await?; + test_protocol.take_sent().await; + + // Callback that returns None (no token) + let fetcher: crate::client::AuthTokenFetcher = + Box::new(|_force_refresh| Box::pin(async { Ok(AuthenticationToken::None) })); + client.set_auth_callback(Some(fetcher)).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 0, + token: AuthenticationToken::None, + }] + ); + Ok(()) + } + + #[tokio::test] + async fn test_set_auth_uses_callback_path() -> anyhow::Result<()> { + let (mut client, test_protocol) = ConvexClient::with_test_protocol().await?; + test_protocol.take_sent().await; + + // set_auth with a token should send the same Authenticate message as before + client.set_auth(Some("static_token".into())).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 0, + token: AuthenticationToken::User("static_token".into()), + }] + ); + + // set_auth(None) clears auth + client.set_auth(None).await; + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::Authenticate { + base_version: 1, + token: AuthenticationToken::None, + }] + ); + Ok(()) + } + + #[tokio::test] + async fn test_client_single_subscription() -> anyhow::Result<()> { + let (mut client, mut test_protocol) = ConvexClient::with_test_protocol().await?; + + let mut subscription1 = client.subscribe("getValue1", btreemap! {}).await?; + let query_id = subscription1.query_id(); + assert_eq!( + test_protocol.take_sent().await, + vec![ + ClientMessage::Connect { + session_id: SessionId::nil(), + connection_count: 0, + last_close_reason: "InitialConnect".to_string(), + max_observed_timestamp: None, + client_ts: None, + }, + ClientMessage::ModifyQuerySet { + base_version: 0, + new_version: 1, + modifications: vec![QuerySetModification::Add(Query { + query_id, + udf_path: "getValue1".parse()?, + args: SerializedArgs::from_args(vec![json!({})])?, + journal: None, + component_path: None, + })] + }, + ] + ); + + test_protocol + .fake_server_response( + fake_transition( + StateVersion::initial(), + vec![(subscription1.query_id(), 10.into())], + ) + .0, + ) + .await?; + assert_eq!( + subscription1.next().await, + Some(FunctionResult::Value(10.into())) + ); + assert_eq!( + client.query("getValue1", btreemap! {}).await?, + FunctionResult::Value(10.into()) + ); + + drop(subscription1); + test_protocol.wait_until_n_messages_sent(1).await; + assert_eq!( + test_protocol.take_sent().await, + vec![ClientMessage::ModifyQuerySet { + base_version: 1, + new_version: 2, + modifications: vec![QuerySetModification::Remove { query_id }], + }] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_client_subscribe_unsubscribe_subscribe() -> anyhow::Result<()> { + let (mut client, mut test_protocol) = ConvexClient::with_test_protocol().await?; + let subscription1b: QuerySubscription; + { + // This subscription goes out of scope and unsubscribes at the end of this + // block. The internal num_subscribers value gets decremented. + let _ignored = client.subscribe("getValue1", btreemap! {}).await?; + subscription1b = client.subscribe("getValue1", btreemap! {}).await?; + } + // In the buggy scenario, this subscription gets an ID via num_subscribers ID + // that matches subscription1b. That triggers a panic. + let subscription1c = client.subscribe("getValue1", btreemap! {}).await?; + test_protocol.take_sent().await; + let mut watch = client.watch_all(); + + test_protocol + .fake_server_response( + fake_transition(StateVersion::initial(), vec![(QueryId::new(0), 10.into())]).0, + ) + .await?; + + let results = watch.next().await.expect("Watch should have results"); + assert_eq!( + results.get(&subscription1b), + Some(&FunctionResult::Value(10.into())) + ); + assert_eq!( + results.get(&subscription1c), + Some(&FunctionResult::Value(10.into())) + ); + Ok(()) + } + + #[tokio::test] + async fn test_client_consistent_view_watch() -> anyhow::Result<()> { + let (mut client, mut test_protocol) = ConvexClient::with_test_protocol().await?; + let subscription1 = client.subscribe("getValue1", btreemap! {}).await?; + let subscription2a = client.subscribe("getValue2", btreemap! {}).await?; + let subscription2b = client.subscribe("getValue2", btreemap! {}).await?; + let subscription3 = client.subscribe("getValue3", btreemap! {}).await?; + test_protocol.take_sent().await; + let mut watch = client.watch_all(); + + test_protocol + .fake_server_response( + fake_transition( + StateVersion::initial(), + vec![(QueryId::new(0), 10.into()), (QueryId::new(1), 20.into())], + ) + .0, + ) + .await?; + + let results = watch.next().await.expect("Watch should have results"); + assert_eq!( + results.get(&subscription1), + Some(&FunctionResult::Value(10.into())) + ); + assert_eq!( + results.get(&subscription2a), + Some(&FunctionResult::Value(20.into())) + ); + assert_eq!( + results.get(&subscription2b), + Some(&FunctionResult::Value(20.into())) + ); + assert_eq!(results.get(&subscription3), None); + assert_eq!( + results.iter().collect::>(), + vec![ + (subscription1.id(), Some(&FunctionResult::Value(10.into()))), + (subscription2a.id(), Some(&FunctionResult::Value(20.into()))), + (subscription2b.id(), Some(&FunctionResult::Value(20.into()))), + (subscription3.id(), None,), + ] + ); + + // Ideally a new watch should immediately give you results, but we don't have + // that yet. Need to replace tokio::broadcast with something that buffers 1 + // item. + //let mut watch2 = client.watch(); + //let results = watch.next().await.expect("Watch should have results"); + //assert_eq!(results.len(), 3); + + Ok(()) + } + + #[tokio::test] + async fn test_drop_client() -> anyhow::Result<()> { + let (mut client, _test_protocol) = ConvexClient::with_test_protocol().await?; + let mut subscription1 = client.subscribe("getValue1", btreemap! {}).await?; + drop(client); + tokio::task::yield_now().await; + assert!(subscription1.next().await.is_none()); + drop(subscription1); + Ok(()) + } + + #[tokio::test] + async fn test_client_separate_queries() -> anyhow::Result<()> { + let (mut client, test_protocol) = ConvexClient::with_test_protocol().await?; + + // All three of these should be considered separate + let subscription1 = client.subscribe("getValue1", btreemap! {}).await?; + let subscription2 = client.subscribe("getValue2", btreemap! {}).await?; + let subscription3 = client + .subscribe("getValue2", btreemap! {"hello".into() => "world".into()}) + .await?; + assert_ne!(subscription1.query_id(), subscription2.query_id()); + assert_ne!(subscription2.query_id(), subscription3.query_id()); + + assert_eq!( + test_protocol.take_sent().await, + vec![ + ClientMessage::Connect { + session_id: SessionId::nil(), + connection_count: 0, + last_close_reason: "InitialConnect".to_string(), + max_observed_timestamp: None, + client_ts: None, + }, + ClientMessage::ModifyQuerySet { + base_version: 0, + new_version: 1, + modifications: vec![QuerySetModification::Add(Query { + query_id: subscription1.query_id(), + udf_path: "getValue1".parse()?, + args: SerializedArgs::from_args(vec![json!({})])?, + journal: None, + component_path: None, + })] + }, + ClientMessage::ModifyQuerySet { + base_version: 1, + new_version: 2, + modifications: vec![QuerySetModification::Add(Query { + query_id: subscription2.query_id(), + udf_path: "getValue2".parse()?, + args: SerializedArgs::from_args(vec![json!({})])?, + journal: None, + component_path: None, + })] + }, + ClientMessage::ModifyQuerySet { + base_version: 2, + new_version: 3, + modifications: vec![QuerySetModification::Add(Query { + query_id: subscription3.query_id(), + udf_path: "getValue2".parse()?, + args: SerializedArgs::from_args(vec![json!({"hello": "world"})])?, + journal: None, + component_path: None, + })] + }, + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_client_two_identical_queries() -> anyhow::Result<()> { + let (mut client, mut test_protocol) = ConvexClient::with_test_protocol().await?; + + // These two should be considered the same query. + let mut subscription1 = client.subscribe("getValue", btreemap! {}).await?; + let mut subscription2 = client.subscribe("getValue", btreemap! {}).await?; + + assert_ne!(subscription1.subscriber_id, subscription2.subscriber_id); + assert_eq!(subscription1.query_id(), subscription2.query_id()); + let query_id = subscription1.query_id(); + + assert_eq!( + test_protocol.take_sent().await, + vec![ + ClientMessage::Connect { + session_id: SessionId::nil(), + connection_count: 0, + last_close_reason: "InitialConnect".to_string(), + max_observed_timestamp: None, + client_ts: None, + }, + ClientMessage::ModifyQuerySet { + base_version: 0, + new_version: 1, + modifications: vec![QuerySetModification::Add(Query { + query_id, + udf_path: "getValue".parse()?, + args: SerializedArgs::from_args(vec![json!({})])?, + journal: None, + component_path: None, + })] + }, + ] + ); + + let mut version = StateVersion::initial(); + for i in 1..5 { + let (transition, new_version) = fake_transition(version, vec![(query_id, i.into())]); + test_protocol.fake_server_response(transition).await?; + version = new_version; + + assert_eq!( + subscription1.next().await, + Some(FunctionResult::Value(i.into())) + ); + assert_eq!( + subscription2.next().await, + Some(FunctionResult::Value(i.into())) + ); + } + + // A new subscription should auto-initialize with the value if available + let mut subscription3 = client.subscribe("getValue", btreemap! {}).await?; + assert_eq!( + subscription3.next().await, + Some(FunctionResult::Value(4.into())), + ); + + // Dropping sub1 and sub2 should still maintain subscription + drop(subscription1); + drop(subscription2); + let (transition, _new_version) = fake_transition(version, vec![(query_id, 5.into())]); + test_protocol.fake_server_response(transition).await?; + assert_eq!( + subscription3.next().await, + Some(FunctionResult::Value(5.into())), + ); + + Ok(()) + } + + #[test] + fn test_deployment_url() -> anyhow::Result<()> { + assert_eq!( + deployment_to_ws_url("http://flying-shark-123.convex.cloud".parse()?)?.to_string(), + "ws://flying-shark-123.convex.cloud/api/sync", + ); + assert_eq!( + deployment_to_ws_url("https://flying-shark-123.convex.cloud".parse()?)?.to_string(), + "wss://flying-shark-123.convex.cloud/api/sync", + ); + assert_eq!( + deployment_to_ws_url("ws://flying-shark-123.convex.cloud".parse()?)?.to_string(), + "ws://flying-shark-123.convex.cloud/api/sync", + ); + assert_eq!( + deployment_to_ws_url("wss://flying-shark-123.convex.cloud".parse()?)?.to_string(), + "wss://flying-shark-123.convex.cloud/api/sync", + ); + assert_eq!( + deployment_to_ws_url("ftp://flying-shark-123.convex.cloud".parse()?) + .unwrap_err() + .to_string(), + "Unknown scheme ftp. Expected http or https.", + ); + Ok(()) + } +} diff --git a/third_party/convex_rs/src/client/subscription.rs b/third_party/convex_rs/src/client/subscription.rs new file mode 100644 index 00000000..0f3b4786 --- /dev/null +++ b/third_party/convex_rs/src/client/subscription.rs @@ -0,0 +1,149 @@ +use std::{ + ops::Deref, + pin::Pin, +}; + +use futures::{ + task, + Stream, + StreamExt, +}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::{ + errors::BroadcastStreamRecvError, + BroadcastStream, +}; + +use crate::{ + base_client::{ + FunctionResult, + QueryResults, + SubscriberId, + }, + client::worker::{ + ClientRequest, + UnsubscribeRequest, + }, +}; +#[cfg(doc)] +use crate::{ + ConvexClient, + Value, +}; + +/// This structure represents a single subscription to a query with args. +/// For convenience, [`QuerySubscription`] also implements +/// [`Stream`]<[`FunctionResult`]>, giving a stream of results to the query. +/// +/// It is returned by [`ConvexClient::subscribe`]. The subscription lives +/// in the active query set for as long as this token stays in scope. +/// +/// For a consistent [`QueryResults`] of all your queries, use +/// [`ConvexClient::watch_all()`] instead. +pub struct QuerySubscription { + pub(super) subscriber_id: SubscriberId, + pub(super) request_sender: mpsc::UnboundedSender, + pub(super) watch: BroadcastStream, + pub(super) initial: Option, +} +impl QuerySubscription { + /// Returns an identifier for this subscription based on its query and args. + /// This identifier can be used to find the result within a + /// [`QuerySetSubscription`] as returned by [`ConvexClient::watch_all()`] + pub fn id(&self) -> &SubscriberId { + &self.subscriber_id + } +} +impl std::fmt::Debug for QuerySubscription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("QuerySubscription") + .field("subscriber_id", &self.subscriber_id) + .finish() + } +} +impl Deref for QuerySubscription { + type Target = SubscriberId; + + fn deref(&self) -> &SubscriberId { + &self.subscriber_id + } +} +impl Drop for QuerySubscription { + fn drop(&mut self) { + let _ = self + .request_sender + .send(ClientRequest::Unsubscribe(UnsubscribeRequest { + subscriber_id: self.subscriber_id, + })); + } +} +impl Stream for QuerySubscription { + type Item = FunctionResult; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + if let Some(initial) = self.initial.take() { + return task::Poll::Ready(Some(initial)); + } + loop { + return match self.watch.poll_next_unpin(cx) { + // Ok to be lagged (skip intermediate values) - since Convex + // only guarantees a newer value than the previous value. + task::Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(_amt)))) => continue, + task::Poll::Ready(Some(Ok(map))) => { + let Some(value) = map.get(self.id()) else { + // No result yet in the query result set. Keep polling. + continue; + }; + task::Poll::Ready(Some(value.clone())) + }, + task::Poll::Ready(None) => task::Poll::Ready(None), + task::Poll::Pending => task::Poll::Pending, + }; + } + } +} + +/// A subscription to a consistent view of multiple queries. +/// +/// [`QuerySetSubscription`] +/// implements [`Stream`]<[`QueryResults`]>. +/// Each item in the stream contains a consistent view +/// of the results of all the queries in the query set. +/// +/// Queries can be added to the query set via [`ConvexClient::subscribe`]. +/// Queries can be removed from the query set via dropping the +/// [`QuerySubscription`] token returned by [`ConvexClient::subscribe`]. +/// +/// +/// [`QueryResults`] is a copy-on-write mapping from [`SubscriberId`] to +/// its latest result [`Value`]. +pub struct QuerySetSubscription { + watch: BroadcastStream, +} +impl QuerySetSubscription { + pub(super) fn new(watch: BroadcastStream) -> Self { + Self { watch } + } +} +impl Stream for QuerySetSubscription { + type Item = QueryResults; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + loop { + return match self.watch.poll_next_unpin(cx) { + // Ok to be lagged (skip intermediate values) - since Convex + // only guarantees a newer value than the previous value. + task::Poll::Ready(Some(Err(BroadcastStreamRecvError::Lagged(_amt)))) => continue, + task::Poll::Ready(Some(Ok(map))) => task::Poll::Ready(Some(map)), + task::Poll::Ready(None) => task::Poll::Ready(None), + task::Poll::Pending => task::Poll::Pending, + }; + } + } +} diff --git a/third_party/convex_rs/src/client/worker.rs b/third_party/convex_rs/src/client/worker.rs new file mode 100644 index 00000000..bd001304 --- /dev/null +++ b/third_party/convex_rs/src/client/worker.rs @@ -0,0 +1,367 @@ +use std::{ + collections::BTreeMap, + convert::Infallible, + time::Duration, +}; + +use convex_sync_types::{ + backoff::Backoff, + UdfPath, +}; +use tokio::sync::{ + broadcast, + mpsc, + oneshot, +}; +use tokio_stream::wrappers::BroadcastStream; + +use crate::{ + base_client::{ + AuthTokenFetcher, + BaseConvexClient, + SubscriberId, + }, + client::{ + QueryResults, + QuerySubscription, + }, + sync::{ + ProtocolResponse, + ReconnectProtocolReason, + ReconnectRequest, + SyncProtocol, + }, + value::Value, + FunctionResult, +}; + +const INITIAL_BACKOFF: Duration = Duration::from_millis(100); +const MAX_BACKOFF: Duration = Duration::from_secs(15); +const AUTH_RETRY_DELAY: Duration = Duration::from_millis(250); + +fn is_auth_rejection(reason: &str) -> bool { + reason.starts_with("AuthError:") +} + +#[derive(Default)] +struct AuthRecovery { + active: bool, + fresh_token_in_flight: bool, +} + +pub enum ClientRequest { + Mutation( + MutationRequest, + oneshot::Sender>, + ), + Action( + ActionRequest, + oneshot::Sender>, + ), + Subscribe( + SubscribeRequest, + oneshot::Sender, + mpsc::UnboundedSender, + ), + Unsubscribe(UnsubscribeRequest), + Authenticate(Option), + Reconnect(String), +} + +pub struct MutationRequest { + pub udf_path: UdfPath, + pub args: BTreeMap, +} + +pub struct ActionRequest { + pub udf_path: UdfPath, + pub args: BTreeMap, +} + +pub struct SubscribeRequest { + pub udf_path: UdfPath, + pub args: BTreeMap, +} + +#[derive(Debug)] +pub struct UnsubscribeRequest { + pub subscriber_id: SubscriberId, +} + +pub async fn worker( + mut protocol_response_receiver: mpsc::Receiver, + mut client_request_receiver: mpsc::UnboundedReceiver, + mut watch_sender: broadcast::Sender, + mut base_client: BaseConvexClient, + mut protocol_manager: T, +) -> Infallible { + let mut backoff = Backoff::new(INITIAL_BACKOFF, MAX_BACKOFF); + let mut auth_recovery = AuthRecovery::default(); + loop { + let e = loop { + match _worker_once( + &mut protocol_response_receiver, + &mut client_request_receiver, + &mut watch_sender, + &mut base_client, + &mut protocol_manager, + &mut auth_recovery, + ) + .await + { + Ok(()) => backoff.reset(), + Err(e) => break e, + } + }; + + if is_auth_rejection(&e) { + auth_recovery.active = true; + auth_recovery.fresh_token_in_flight = false; + } + + // A server auth rejection and the protocol errors cascading from that + // rejected socket are not network outages. The reconnect below + // invokes the stored token callback with `force_refresh=true`, so retry + // it at a small fixed cadence while the auth provider obtains a fresh + // token. Applying the generic exponential network backoff here can + // otherwise delay a ready token for up to MAX_BACKOFF. + let delay = if auth_recovery.active { + backoff.reset(); + AUTH_RETRY_DELAY + } else { + backoff.fail(&mut rand::rng()) + }; + tracing::error!( + "Convex Client Worker failed: {e:?}. Backing off for {delay:?} and retrying." + ); + tokio::time::sleep(delay).await; + + // Everything currently buffered came from the connection we are about + // to replace. In particular, an auth error can be followed by the old + // socket closing and queuing ProtocolFailure while this worker is in + // its retry delay. Replaying that stale failure after the new socket is + // up starts a second, unrelated backoff and can leave fresh auth behind + // it. Query and mutation state is rebuilt below, so discard the old + // connection's buffered responses before requesting the replacement. + while protocol_response_receiver.try_recv().is_ok() {} + + // Tell the sync protocol to reconnect followed by an immediate resend of + // ongoing queries/mutations. It's important these happen together to + // ensure mutation ordering. If an auth token fetcher is stored, + // resend_ongoing_queries_mutations will refresh the token first. + protocol_manager + .reconnect(ReconnectRequest { + reason: e, + max_observed_timestamp: base_client.max_observed_timestamp(), + auth_retry: auth_recovery.active, + }) + .await; + let auth_token_changed = base_client.resend_ongoing_queries_mutations().await; + if auth_recovery.active && auth_token_changed { + auth_recovery.fresh_token_in_flight = true; + } + // We'll flush messages from base_client inside the next call to + // `_worker_once`. + } +} + +#[cfg(test)] +mod tests { + use super::is_auth_rejection; + + #[test] + fn only_server_auth_errors_use_the_auth_retry_path() { + assert!(is_auth_rejection( + "AuthError: token expired for identity version 1" + )); + assert!(!is_auth_rejection("ProtocolFailure")); + assert!(!is_auth_rejection("convex_flutter:manual")); + } +} + +async fn _worker_once( + protocol_response_receiver: &mut mpsc::Receiver, + client_request_receiver: &mut mpsc::UnboundedReceiver, + watch_sender: &mut broadcast::Sender, + base_client: &mut BaseConvexClient, + protocol_manager: &mut T, + auth_recovery: &mut AuthRecovery, +) -> Result<(), ReconnectProtocolReason> { + // If there are any outgoing messages to flush (e.g. from an outer reconnect), + // do so first. + communicate( + base_client, + protocol_response_receiver, + watch_sender, + protocol_manager, + auth_recovery, + ) + .await?; + + tokio::select! { + Some(protocol_response) = protocol_response_receiver.recv() => { + handle_protocol_response( + base_client, + watch_sender, + protocol_response, + auth_recovery, + )?; + } + Some(client_request) = client_request_receiver.recv() => { + match client_request { + ClientRequest::Subscribe(query, tx, request_sender) => { + let watch = watch_sender.subscribe(); + let SubscribeRequest { + udf_path, + args, + } = query; + let subscriber_id = base_client.subscribe(udf_path, args); + communicate( + base_client, + protocol_response_receiver, + watch_sender, + protocol_manager, + auth_recovery, + ) + .await?; + + let watch = BroadcastStream::new(watch); + let subscription = QuerySubscription { + subscriber_id, + request_sender, + watch, + initial: base_client.latest_results().get(&subscriber_id).cloned(), + }; + let _ = tx.send(subscription); + }, + ClientRequest::Mutation(mutation, tx) => { + let MutationRequest { + udf_path, + args, + } = mutation; + let result_receiver = base_client + .mutation(udf_path, args); + communicate( + base_client, + protocol_response_receiver, + watch_sender, + protocol_manager, + auth_recovery, + ) + .await?; + let _ = tx.send(result_receiver); + }, + ClientRequest::Action(action, tx) => { + let ActionRequest { + udf_path, + args, + } = action; + let result_receiver = base_client + .action(udf_path, args); + communicate( + base_client, + protocol_response_receiver, + watch_sender, + protocol_manager, + auth_recovery, + ) + .await?; + let _ = tx.send(result_receiver); + }, + ClientRequest::Unsubscribe(unsubscribe) => { + let UnsubscribeRequest {subscriber_id} = unsubscribe; + base_client.unsubscribe(subscriber_id); + communicate( + base_client, + protocol_response_receiver, + watch_sender, + protocol_manager, + auth_recovery, + ) + .await?; + }, + ClientRequest::Authenticate(fetcher) => { + let token_changed = base_client.set_auth_fetcher(fetcher).await; + if auth_recovery.active && token_changed { + auth_recovery.fresh_token_in_flight = true; + } + communicate( + base_client, + protocol_response_receiver, + watch_sender, + protocol_manager, + auth_recovery, + ) + .await?; + }, + ClientRequest::Reconnect(reason) => return Err(reason), + } + }, + // TODO: this else branch will lead to an infinite loop if both channels + // are closed + else => (), + } + Ok(()) +} + +/// Flush all messages to the protocol while processing server mesages. +async fn communicate( + base_client: &mut BaseConvexClient, + protocol_response_receiver: &mut mpsc::Receiver, + watch_sender: &mut broadcast::Sender, + protocol: &mut P, + auth_recovery: &mut AuthRecovery, +) -> Result<(), ReconnectProtocolReason> { + while let Some(modification) = base_client.pop_next_message() { + let mut send_future = protocol.send(modification); + loop { + tokio::select! { + _ = &mut send_future => break, + // Keep processing protocol responses while waiting so that we + // don't deadlock with the websocket worker. + Some(protocol_response) = protocol_response_receiver.recv() => { + handle_protocol_response( + base_client, + watch_sender, + protocol_response, + auth_recovery, + )?; + } + } + } + } + Ok(()) +} + +fn handle_protocol_response( + base_client: &mut BaseConvexClient, + watch_sender: &mut broadcast::Sender, + protocol_response: ProtocolResponse, + auth_recovery: &mut AuthRecovery, +) -> Result<(), ReconnectProtocolReason> { + match protocol_response { + ProtocolResponse::ServerMessage(msg) => { + let proves_protocol_auth = matches!( + &msg, + crate::sync::ServerMessage::Transition { .. } + | crate::sync::ServerMessage::MutationResponse { .. } + | crate::sync::ServerMessage::ActionResponse { .. } + ); + if let Some(subscriber_id_to_latest_value) = base_client.receive_message(msg)? { + // Notify watchers of the new consistent query results at new timestamp + let _ = watch_sender.send(subscriber_id_to_latest_value); + } + if proves_protocol_auth && auth_recovery.fresh_token_in_flight { + // Only a response after the refresh callback produced a + // different token proves recovery. Transitions buffered while + // the rejected token was still current cannot clear this state. + auth_recovery.active = false; + auth_recovery.fresh_token_in_flight = false; + } + }, + ProtocolResponse::Failure => { + return Err("ProtocolFailure".into()); + }, + } + Ok(()) +} diff --git a/third_party/convex_rs/src/lib.rs b/third_party/convex_rs/src/lib.rs new file mode 100644 index 00000000..f87b14bc --- /dev/null +++ b/third_party/convex_rs/src/lib.rs @@ -0,0 +1,75 @@ +//! # Convex Client +//! The official Rust client for [Convex](https://convex.dev). +//! +//! Convex is the backend application platform with everything you need to build +//! your product. Convex clients can subscribe to queries and perform mutations +//! and actions. Check out the [Convex Documentation](https://docs.convex.dev) for more information. +//! +//! # Usage +//! ## Native Rust development +//! To use Convex to create native Rust applications with [`tokio`], you can use +//! the [`ConvexClient`] struct directly. All you need is your deployment URL +//! from your existing project, and you can subscribe to queries and call +//! mutations. To make a new project, check out our [getting started guide](https://docs.convex.dev/get-started). +//! +//! ```no_run +//! use convex::ConvexClient; +//! use futures::StreamExt; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! let mut client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; +//! client.mutation("sendMessage", maplit::btreemap!{ +//! "body".into() => "Let it be.".into(), +//! "author".into() => "The Beatles".into(), +//! }).await?; +//! let mut sub = client.subscribe("listMessages", maplit::btreemap!{}).await?; +//! while let Some(result) = sub.next().await { +//! println!("{result:?}"); +//! } +//! Ok(()) +//! } +//! ``` +//! +//! ## Extending client for other programming languages or frameworks. +//! To extend Convex into non-[`tokio`] frameworks, +//! you can use the [`base_client::BaseConvexClient`] to build something similar +//! to a [`ConvexClient`]. +//! +//! Detailed examples of both use cases are documented for each struct. + +#![cfg_attr(not(test), warn(missing_docs))] +#![warn(rustdoc::missing_crate_level_docs)] + +mod value; +#[cfg(any(test, feature = "testing"))] +pub use value::export::roundtrip::ExportContext; +pub use value::{ + ConvexError, + Value, +}; + +mod client; +pub use client::{ + subscription::{ + QuerySetSubscription, + QuerySubscription, + }, + ConvexClient, + ConvexClientBuilder, +}; +#[cfg(any(test, feature = "testing"))] +pub use sync::testing; +pub use sync::WebSocketState; + +pub mod base_client; +#[doc(inline)] +pub use base_client::{ + AuthTokenFetcher, + FunctionResult, + QueryResults, + SubscriberId, +}; +pub use convex_sync_types::AuthenticationToken; + +mod sync; diff --git a/third_party/convex_rs/src/sync/mod.rs b/third_party/convex_rs/src/sync/mod.rs new file mode 100644 index 00000000..a3dc1b2a --- /dev/null +++ b/third_party/convex_rs/src/sync/mod.rs @@ -0,0 +1,56 @@ +use async_trait::async_trait; +use convex_sync_types::{ + ClientMessage, + Timestamp, +}; +use tokio::sync::mpsc; +use url::Url; + +use crate::value::Value; + +#[cfg(any(test, feature = "testing"))] +pub mod testing; +pub mod web_socket_manager; + +/// Upon a protocol failure, an explanation of the failure to pass in on +/// reconnect +#[derive(Debug)] +pub struct ReconnectRequest { + pub reason: ReconnectProtocolReason, + pub max_observed_timestamp: Option, + /// The old socket failed while recovering a server auth rejection. The + /// client worker already paces these retries, so the WebSocket layer must + /// not add its independent network backoff. + pub auth_retry: bool, +} + +pub type ReconnectProtocolReason = String; + +pub type ServerMessage = convex_sync_types::ServerMessage; + +#[derive(Debug)] +pub enum ProtocolResponse { + ServerMessage(ServerMessage), + Failure, +} + +#[derive(Debug)] +/// The state of the Convex WebSocket connection +pub enum WebSocketState { + /// The WebSocket is open and connected + Connected, + /// The WebSocket is closed and connecting/reconnecting + Connecting, +} + +#[async_trait] +pub trait SyncProtocol: Send + Sized { + async fn open( + ws_url: Url, + on_response: mpsc::Sender, + on_state_change: Option>, + client_id: &str, + ) -> anyhow::Result; + async fn send(&mut self, message: ClientMessage) -> anyhow::Result<()>; + async fn reconnect(&mut self, request: ReconnectRequest); +} diff --git a/third_party/convex_rs/src/sync/testing.rs b/third_party/convex_rs/src/sync/testing.rs new file mode 100644 index 00000000..1e044e0e --- /dev/null +++ b/third_party/convex_rs/src/sync/testing.rs @@ -0,0 +1,106 @@ +#![allow(missing_docs)] +use std::{ + sync::Arc, + time::Duration, +}; + +use async_trait::async_trait; +use convex_sync_types::{ + ClientMessage, + SessionId, +}; +use parking_lot::Mutex; +use tokio::sync::mpsc; +use url::Url; +use uuid::Uuid; + +use super::{ + ReconnectRequest, + WebSocketState, +}; +use crate::sync::{ + ProtocolResponse, + ServerMessage, + SyncProtocol, +}; + +#[derive(Debug)] +struct TestProtocolInner { + closed: bool, + sent_messages: Vec, +} +/// TestProtocolManager +#[derive(Debug, Clone)] +pub struct TestProtocolManager { + inner: Arc>, + response_sender: mpsc::Sender, +} + +impl TestProtocolManager { + pub async fn fake_server_response(&mut self, message: ServerMessage) -> anyhow::Result<()> { + self.response_sender + .send(ProtocolResponse::ServerMessage(message)) + .await?; + Ok(()) + } + + pub async fn wait_until_n_messages_sent(&self, n: usize) { + tokio::time::timeout(Duration::from_secs(2), async { + while self.inner.lock().sent_messages.len() < n { + tokio::task::yield_now().await; + } + }) + .await + .expect("Test timed out waiting for messages to be sent"); + } + + pub async fn take_sent(&self) -> Vec { + std::mem::take(&mut self.inner.lock().sent_messages) + } +} + +#[async_trait] +impl SyncProtocol for TestProtocolManager { + async fn open( + _ws_url: Url, + response_sender: mpsc::Sender, + _on_state_change: Option>, + _client_id: &str, + ) -> anyhow::Result { + let mut test_protocol = TestProtocolManager { + inner: Arc::new(Mutex::new(TestProtocolInner { + closed: false, + sent_messages: vec![], + })), + response_sender, + }; + + let session_id = Uuid::nil(); + let connection_count = 0; + + test_protocol + .send(ClientMessage::Connect { + session_id: SessionId::new(session_id), + connection_count, + last_close_reason: "InitialConnect".to_string(), + max_observed_timestamp: None, + client_ts: None, + }) + .await?; + + Ok(test_protocol) + } + + async fn send(&mut self, message: ClientMessage) -> anyhow::Result<()> { + if self.inner.lock().closed { + anyhow::ensure!(!self.inner.lock().closed, "Websocket is closed"); + } + self.inner.lock().sent_messages.push(message); + + Ok(()) + } + + async fn reconnect(&mut self, request: ReconnectRequest) { + panic!("Test reconnected {request:?}"); + } +} diff --git a/third_party/convex_rs/src/sync/web_socket_manager.rs b/third_party/convex_rs/src/sync/web_socket_manager.rs new file mode 100644 index 00000000..8ac5b8c6 --- /dev/null +++ b/third_party/convex_rs/src/sync/web_socket_manager.rs @@ -0,0 +1,375 @@ +use std::{ + convert::Infallible, + time::Duration, +}; + +use anyhow::Context; +use async_trait::async_trait; +use convex_sync_types::{ + backoff::Backoff, + headers::{ + DEPRECATION_MSG_HEADER_NAME, + DEPRECATION_STATE_HEADER_NAME, + }, + ClientMessage, + SessionId, + Timestamp, +}; +use futures::{ + select_biased, + stream::Fuse, + FutureExt, + SinkExt, + StreamExt, +}; +use tokio::{ + net::TcpStream, + sync::{ + mpsc, + oneshot, + }, + task::JoinHandle, + time::{ + Instant, + Interval, + }, +}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tokio_tungstenite::{ + connect_async, + tungstenite::{ + self, + client::IntoClientRequest, + http::HeaderMap, + protocol::Message, + }, + MaybeTlsStream, + WebSocketStream, +}; +use url::Url; +use uuid::Uuid; + +use super::WebSocketState; +use crate::sync::{ + ProtocolResponse, + ReconnectRequest, + ServerMessage, + SyncProtocol, +}; + +const INITIAL_BACKOFF: Duration = Duration::from_millis(100); +const MAX_BACKOFF: Duration = Duration::from_secs(15); +type WsStream = WebSocketStream>; + +#[derive(Debug)] +enum WebSocketRequest { + SendMessage(ClientMessage, oneshot::Sender<()>), + Reconnect(ReconnectRequest), +} + +struct WebSocketInternal { + ws_stream: WsStream, + last_server_response: Instant, +} +struct WebSocketWorker { + ws_url: Url, + on_response: mpsc::Sender, + on_state_change: Option>, + internal_receiver: Fuse>, + ping_ticker: Interval, + session_id: SessionId, + connection_count: u32, + backoff: Backoff, +} + +pub struct WebSocketManager { + internal_sender: mpsc::UnboundedSender, + worker_handle: JoinHandle, +} +impl Drop for WebSocketManager { + fn drop(&mut self) { + self.worker_handle.abort() + } +} + +#[async_trait] +impl SyncProtocol for WebSocketManager { + async fn open( + ws_url: Url, + on_response: mpsc::Sender, + on_state_change: Option>, + client_id: &str, + ) -> anyhow::Result { + let (internal_sender, internal_receiver) = mpsc::unbounded_channel(); + let worker_handle = tokio::spawn(WebSocketWorker::run( + ws_url, + on_response, + on_state_change, + internal_receiver, + client_id.to_string(), + )); + + Ok(WebSocketManager { + internal_sender, + worker_handle, + }) + } + + async fn send(&mut self, message: ClientMessage) -> anyhow::Result<()> { + let (tx, rx) = oneshot::channel(); + self.internal_sender + .send(WebSocketRequest::SendMessage(message, tx))?; + rx.await?; + Ok(()) + } + + async fn reconnect(&mut self, request: ReconnectRequest) { + let _ = self + .internal_sender + .send(WebSocketRequest::Reconnect(request)); + } +} + +impl WebSocketWorker { + /// How often heartbeat pings are sent. + const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); + /// How long before lack of server response causes a timeout. + const SERVER_INACTIVITY_THRESHOLD: Duration = Duration::from_secs(30); + + async fn run( + ws_url: Url, + on_response: mpsc::Sender, + on_state_change: Option>, + internal_receiver: mpsc::UnboundedReceiver, + client_id: String, + ) -> Infallible { + let ping_ticker = tokio::time::interval(Self::HEARTBEAT_INTERVAL); + let backoff = Backoff::new(INITIAL_BACKOFF, MAX_BACKOFF); + + let mut worker = Self { + ws_url, + on_response, + on_state_change, + internal_receiver: UnboundedReceiverStream::new(internal_receiver).fuse(), + ping_ticker, + session_id: SessionId::new(Uuid::new_v4()), + connection_count: 0, + backoff, + }; + + let mut last_close_reason = "InitialConnect".to_string(); + let mut max_observed_timestamp = None; + if let Some(state_change_sender) = &worker.on_state_change { + let _ = state_change_sender.try_send(WebSocketState::Connecting); + } + loop { + let exit_result = worker + .work(last_close_reason, max_observed_timestamp, &client_id) + .await; + + if let Some(state_change_sender) = &worker.on_state_change { + let _ = state_change_sender.try_send(WebSocketState::Connecting); + } + + let e = match exit_result { + Ok(reconnect) => { + // WS worker exited cleanly because it got a request to reconnect + tracing::debug!("Reconnecting websocket due to {}", reconnect.reason); + last_close_reason = reconnect.reason; + max_observed_timestamp = reconnect.max_observed_timestamp; + continue; + }, + Err(e) => e, + }; + last_close_reason = e.to_string(); + let mut delay = worker.backoff.fail(&mut rand::rng()); + tracing::error!( + "Convex WebSocketWorker failed: {e:?}. Backing off for {delay:?} and retrying." + ); + + // Tell the worker that we've failed so it can coordinate the reconnect. + // The worker will send a Reconnect message and the new query set all together. + // Drain the input request queue until we get that reconnect message - which + // will be followed by the refreshed query set. + let _ = worker.on_response.send(ProtocolResponse::Failure).await; + tracing::debug!("Waiting for base client to acknowledge reconnect"); + let reconnect = loop { + let request = worker.internal_receiver.next().await; + // TODO: There is a potential issue where we have multiple queued reconnect + // requests in which case max_observed_timestamp might be lower than actually + // observed. This is fine since it will never cause errors. Will can fix this + // when we restructure the wider protocol to be a single routine. + if let Some(WebSocketRequest::Reconnect(reconnect)) = request { + max_observed_timestamp = reconnect.max_observed_timestamp; + break reconnect; + } + }; + if reconnect.auth_retry { + // Auth retries are paced by the client worker while it polls + // the token callback. A second exponential backoff here can + // leave an already refreshed credential idle for 15 seconds. + worker.backoff.reset(); + delay = Duration::ZERO; + } + tracing::debug!( + "Base client acknowledged reconnect. Sleeping {delay:?} and reconnecting" + ); + tokio::time::sleep(delay).await; + tracing::debug!("Reconnecting"); + } + } + + async fn work( + &mut self, + last_close_reason: String, + max_seen_transition: Option, + client_id: &str, + ) -> anyhow::Result { + let verb = if self.connection_count == 0 { + "connect" + } else { + "reconnect" + }; + tracing::debug!("trying to {verb} to {}", self.ws_url); + let mut internal = WebSocketInternal::new( + self.ws_url.clone(), + self.session_id, + self.connection_count, + last_close_reason, + max_seen_transition, + client_id, + ) + .await?; + // One client owns one session ID and advances the connection count for + // every socket that opens, including client-requested reconnects. + self.connection_count += 1; + tracing::debug!("completed websocket {verb} to {}", self.ws_url); + if let Some(state_change_sender) = &self.on_state_change { + let _ = state_change_sender.try_send(WebSocketState::Connected); + } + + loop { + select_biased! { + _ = self.ping_ticker.tick().fuse() => { + let now = Instant::now(); + if now - internal.last_server_response > Self::SERVER_INACTIVITY_THRESHOLD { + anyhow::bail!("InactiveServer"); + } + }, + server_msg = internal.ws_stream.select_next_some() => { + internal.last_server_response = Instant::now(); + + match server_msg.context("WebsocketConnectionError")? { + Message::Close(close_frame) => { + let close_frame = close_frame.context("CloseMessageWithoutFrame")?; + tracing::debug!("Close frame {close_frame}"); + anyhow::bail!("{}", close_frame.reason); + }, + Message::Text(t) => { + let json: serde_json::Value = serde_json::from_str(&t).context("JsonDeserializeError")?; + let server_message = json.try_into()?; + match server_message { + ServerMessage::Ping => tracing::trace!("received message {server_message:?}"), + _ => tracing::trace!("received message {server_message:?}"), + }; + + let resp = ProtocolResponse::ServerMessage(server_message); + let _ = self.on_response.send(resp).await; + + // TODO: Similar to JS, we should ideally only reset backoff if we get + // the client gets into a correct state, where we have Connected and + // received a response to our pending Queries and Mutations. + self.backoff.reset(); + }, + Message::Ping(_) => { + tracing::trace!("received Ping"); + } + server_msg => { + tracing::debug!("received unknown message {server_msg:?}"); + }, + } + }, + request = self.internal_receiver.select_next_some() => { + match request { + WebSocketRequest::SendMessage(message, sender) => { + tracing::debug!("Sending {message:?}"); + let msg = Message::Text(serde_json::Value::try_from(message).context("JsonSerializeError")?.to_string().into()); + internal.send_worker(msg.clone()).await?; + let _ = sender.send(()); + }, + WebSocketRequest::Reconnect(reason) => return Ok(reason), + }; + } + }; + } + } +} + +fn deprecation_message(headers: &HeaderMap) -> Option { + let dep_state = headers.get(DEPRECATION_STATE_HEADER_NAME)?.to_str().ok()?; + let msg = headers.get(DEPRECATION_MSG_HEADER_NAME)?.to_str().ok()?; + Some(format!("{dep_state}: {msg}")) +} + +impl WebSocketInternal { + async fn new( + ws_url: Url, + session_id: SessionId, + connection_count: u32, + last_close_reason: String, + max_observed_timestamp: Option, + client_id: &str, + ) -> anyhow::Result { + let mut request = (&ws_url).into_client_request().context("Bad WS Url")?; + request.headers_mut().insert( + "Convex-Client", + client_id.try_into().context("Bad client id")?, + ); + let (ws_stream, response) = connect_async(request).await.map_err(|e| { + if let tungstenite::Error::Http(ref response) = e { + let body = response + .body() + .as_deref() + .map(String::from_utf8_lossy) + .unwrap_or_default(); + return anyhow::anyhow!("Connection to {ws_url} failed: {e}: {body}"); + } + anyhow::anyhow!("Connection to {ws_url} failed: {e}") + })?; + + if let Some(msg) = deprecation_message(response.headers()) { + tracing::warn!("{msg}"); + } + + let last_server_response = Instant::now(); + let mut internal = WebSocketInternal { + ws_stream, + last_server_response, + }; + + // Send an initial connect message on the new websocket + let message = ClientMessage::Connect { + session_id, + connection_count, + last_close_reason, + max_observed_timestamp, + client_ts: Some(0), + }; + let msg = Message::Text( + serde_json::Value::try_from(message) + .context("JSONSerializationErrorOnConnect")? + .to_string() + .into(), + ); + internal.send_worker(msg).await?; + + Ok(internal) + } + + async fn send_worker(&mut self, message: Message) -> anyhow::Result<()> { + self.ws_stream + .send(message) + .await + .context("WebsocketClosedOnSend") + } +} diff --git a/third_party/convex_rs/src/value/export/mod.rs b/third_party/convex_rs/src/value/export/mod.rs new file mode 100644 index 00000000..8e43ede2 --- /dev/null +++ b/third_party/convex_rs/src/value/export/mod.rs @@ -0,0 +1,191 @@ +use serde_json::{ + json, + Value as JsonValue, +}; + +use crate::Value; + +#[cfg(any(test, feature = "testing"))] +pub mod roundtrip; + +impl Value { + /// Converts this value to a JSON value in the `json` export format. + /// + /// + /// It is possible for distinct Convex values to be serialized to the same + /// JSON value by this method. For instance, strings and binary values are + /// both exported as JSON strings. However, it is possible to convert the + /// exported value back to a unique Convex value if you also have the `Type` + /// value associated with the original Convex value (see `roundtrip.rs`). + /// + /// # Example + /// ``` + /// use convex::Value; + /// use serde_json::{ + /// json, + /// Value as JsonValue, + /// }; + /// + /// let value = Value::Bytes(vec![0b00000000, 0b00010000, 0b10000011]); + /// assert_eq!(JsonValue::from(value.clone()), json!({ "$bytes": "ABCD" })); + /// assert_eq!(value.export(), json!("ABCD")); + /// ``` + pub fn export(self) -> JsonValue { + match self { + Value::Null => JsonValue::Null, + Value::Int64(value) => JsonValue::String(value.to_string()), + Value::Float64(value) => { + if value.is_nan() { + json!("NaN") + } else if value.is_infinite() { + if value.is_sign_positive() { + json!("Infinity") + } else { + json!("-Infinity") + } + } else { + value.into() + } + }, + Value::Boolean(value) => JsonValue::Bool(value), + Value::String(value) => JsonValue::String(value), + Value::Bytes(value) => JsonValue::String(base64::encode(value)), + Value::Array(values) => { + JsonValue::Array(values.into_iter().map(|x| x.export()).collect()) + }, + Value::Object(map) => JsonValue::Object( + map.into_iter() + .map(|(key, value)| (key, value.export())) + .collect(), + ), + } + } +} + +#[cfg(test)] +mod tests { + use maplit::btreemap; + use serde_json::json; + + use super::*; + + #[test] + fn export_rustdoc_example() { + let value = Value::Bytes(vec![0b00000000, 0b00010000, 0b10000011]); + assert_eq!(JsonValue::from(value.clone()), json!({ "$bytes": "ABCD" })); + assert_eq!(value.export(), json!("ABCD")); + } + + #[test] + fn nulls_are_exported_as_null() { + assert_eq!(Value::Null.export(), JsonValue::Null) + } + + #[test] + fn booleans_are_exported_as_booleans() { + assert_eq!(Value::Boolean(true).export(), json!(true)); + assert_eq!(Value::Boolean(false).export(), json!(false)); + } + + #[test] + fn ints_are_exported_as_strings() { + assert_eq!(Value::Int64(1234).export(), json!("1234")); + + assert_eq!(Value::Int64(-314).export(), json!("-314")); + + assert_eq!(Value::Int64(0).export(), json!("0")); + + assert_eq!( + Value::Int64(i64::MIN).export(), + json!("-9223372036854775808") + ); + + assert_eq!( + Value::Int64(i64::MAX).export(), + json!("9223372036854775807") + ); + } + + #[test] + fn finite_floats_are_exported_as_numbers() { + assert_eq!(Value::Float64(12.34).export(), json!(12.34)); + } + + #[test] + fn pos_zero_is_exported_as_number() { + let json = Value::Float64(0.0).export(); + assert_eq!(json, json!(0.0)); + assert!(json.as_f64().unwrap().is_sign_positive()); + assert!(!json.as_f64().unwrap().is_sign_negative()); + } + + #[test] + fn neg_zero_is_exported_as_number() { + let json = Value::Float64(-0.0).export(); + assert_eq!(json, json!(-0.0)); + assert!(json.as_f64().unwrap().is_sign_negative()); + assert!(!json.as_f64().unwrap().is_sign_positive()); + } + + #[test] + fn infinite_floats_are_exported_as_strings() { + assert_eq!(Value::Float64(f64::INFINITY).export(), json!("Infinity")); + assert_eq!( + Value::Float64(f64::NEG_INFINITY).export(), + json!("-Infinity") + ); + } + + #[test] + fn nan_is_exported_as_string() { + assert_eq!(Value::Float64(f64::NAN).export(), json!("NaN")); + } + + #[test] + fn strings_are_exported_as_strings() { + assert_eq!(Value::Null.export(), JsonValue::Null); + } + + #[test] + fn bytes_are_exported_as_base64() { + let vec: Vec = vec![ + 0b00000000, 0b00010000, 0b10000011, 0b00010000, 0b01010001, 0b10000111, 0b00100000, + 0b10010010, 0b10001011, 0b00110000, 0b11010011, 0b10001111, 0b01000001, 0b00010100, + 0b10010011, 0b01010001, 0b01010101, 0b10010111, 0b01100001, 0b10010110, 0b10011011, + 0b01110001, 0b11010111, 0b10011111, 0b10000010, 0b00011000, 0b10100011, 0b10010010, + 0b01011001, 0b10100111, 0b10100010, 0b10011010, 0b10101011, 0b10110010, 0b11011011, + 0b10101111, 0b11000011, 0b00011100, 0b10110011, 0b11010011, 0b01011101, 0b10110111, + 0b11100011, 0b10011110, 0b10111011, 0b11110011, 0b11011111, 0b10111111, 0b00000000, + ]; + + assert_eq!( + Value::Bytes(vec).export(), + json!("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/AA==") + ); + } + + #[test] + fn arrays_are_exported_as_arrays() { + assert_eq!( + Value::Array(vec![Value::Int64(1), Value::Int64(2), Value::Int64(3)]).export(), + json!(["1", "2", "3"]), + ); + } + + #[test] + fn objects_are_exported_as_objects() { + assert_eq!( + Value::Object(btreemap! { + "a".to_string() => 1.into(), + "b".to_string() => 2.into(), + "c".to_string() => 3.into(), + }) + .export(), + json!({ + "a": "1", + "b": "2", + "c": "3", + }), + ); + } +} diff --git a/third_party/convex_rs/src/value/export/roundtrip.rs b/third_party/convex_rs/src/value/export/roundtrip.rs new file mode 100644 index 00000000..1ff1307f --- /dev/null +++ b/third_party/convex_rs/src/value/export/roundtrip.rs @@ -0,0 +1,169 @@ +use std::collections::BTreeMap; + +use anyhow::Context; +use serde_json::Value as JsonValue; + +use crate::Value; + +/// Type hint associated with a Convex value. This allows us to uniquely convert +/// the exported value back to the original Convex value. +#[allow(missing_docs)] +pub enum ExportContext { + Null, + Int64, + Float64 { + // Store the f64 value in the export context when it is NaN, because the export format + // assumes a single NaN value. This ensures that we can fully roundtrip values. + nan_value: Option, + }, + Boolean, + String, + Bytes, + Array(Vec), + Set, + Map, + Object(BTreeMap), +} + +impl ExportContext { + /// Returns the export context of a Convex value + pub fn of(value: &Value) -> ExportContext { + match value { + Value::Null => ExportContext::Null, + Value::Int64(_) => ExportContext::Int64, + Value::Float64(f) => ExportContext::Float64 { + nan_value: f.is_nan().then_some(*f), + }, + Value::Boolean(_) => ExportContext::Boolean, + Value::String(_) => ExportContext::String, + Value::Bytes(_) => ExportContext::Bytes, + Value::Array(elements) => { + ExportContext::Array(elements.iter().map(ExportContext::of).collect()) + }, + Value::Object(fields) => ExportContext::Object( + fields + .iter() + .map(|(key, value)| (key.clone(), ExportContext::of(value))) + .collect(), + ), + } + } +} + +impl TryFrom<(JsonValue, &ExportContext)> for Value { + type Error = anyhow::Error; + + fn try_from( + (exported_value, type_hint): (JsonValue, &ExportContext), + ) -> Result { + match type_hint { + ExportContext::Null => Ok(Value::Null), + ExportContext::Int64 => match exported_value { + JsonValue::String(str) => str + .parse::() + .map(Value::from) + .context("Unexpected string for i64"), + _ => anyhow::bail!("Unexpected value for i64"), + }, + ExportContext::Float64 { + nan_value: Some(nan_value), + } => { + if !nan_value.is_nan() { + anyhow::bail!("Unexpected non-NaN value in the export context"); + } + + if exported_value != JsonValue::String(String::from("NaN")) { + anyhow::bail!("Unexpected serialization of a NaN value"); + } + + Ok((*nan_value).into()) + }, + ExportContext::Float64 { nan_value: None } => match exported_value { + JsonValue::String(str) => match str.as_ref() { + "Infinity" => Ok(f64::INFINITY.into()), + "-Infinity" => Ok(f64::NEG_INFINITY.into()), + _ => anyhow::bail!("Unexpected string for f64"), + }, + JsonValue::Number(n) => n + .as_f64() + .map(Value::from) + .context("Unexpected number for i64"), + _ => anyhow::bail!("Unexpected value for f64"), + }, + ExportContext::Boolean => match exported_value { + JsonValue::Bool(value) => Ok(value.into()), + _ => anyhow::bail!("Unexpected value for boolean"), + }, + ExportContext::String => match exported_value { + JsonValue::String(value) => Ok(value.into()), + _ => anyhow::bail!("Unexpected value for string"), + }, + ExportContext::Bytes => match exported_value { + JsonValue::String(value) => base64::decode(value) + .map(Value::from) + .context("Unexpected string for bytes"), + _ => anyhow::bail!("Unexpected value for bytes"), + }, + ExportContext::Array(type_hints) => match exported_value { + JsonValue::Array(exported_values) => { + if exported_values.len() != type_hints.len() { + anyhow::bail!("Array lengths do not match"); + } + + let values: anyhow::Result> = exported_values + .into_iter() + .zip(type_hints) + .map(Value::try_from) + .collect(); + + Ok(Value::Array(values?)) + }, + _ => anyhow::bail!("Unexpected value for array"), + }, + ExportContext::Set | ExportContext::Map => Value::try_from(exported_value) + .context("Couldn’t deserialize set/map from internal representation"), + ExportContext::Object(type_hints) => match exported_value { + JsonValue::Object(exported_values) => { + let entries: anyhow::Result> = exported_values + .into_iter() + .map(|(key, value)| { + let Some(type_hint) = type_hints.get(&key) else { + anyhow::bail!("Missing export context for an object key"); + }; + Ok((key, (value, type_hint).try_into()?)) + }) + .collect(); + + Ok(Value::Object(entries?)) + }, + _ => anyhow::bail!("Unexpected value for object"), + }, + } + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use crate::{ + value::export::roundtrip::ExportContext, + Value, + }; + + proptest! { + #![proptest_config(ProptestConfig { + failure_persistence: None, ..ProptestConfig::default() + })] + #[test] + fn export_roundtrips_with_type_hint(value in any::()) { + let exported_value = value.clone().export(); + let type_hint = ExportContext::of(&value); + + prop_assert_eq!( + value, + Value::try_from((exported_value, &type_hint)).unwrap() + ); + } + } +} diff --git a/third_party/convex_rs/src/value/json/bytes.rs b/third_party/convex_rs/src/value/json/bytes.rs new file mode 100644 index 00000000..45d4f899 --- /dev/null +++ b/third_party/convex_rs/src/value/json/bytes.rs @@ -0,0 +1,14 @@ +/// Helper functions for encoding `Bytes`s as `String`s. +pub enum JsonBytes {} + +impl JsonBytes { + /// Encode a binary string as a string. + pub fn encode(bytes: &Vec) -> String { + base64::encode(&bytes[..]) + } + + /// Decode a binary string from a string. + pub fn decode(s: String) -> anyhow::Result> { + Ok(base64::decode(s.as_bytes())?) + } +} diff --git a/third_party/convex_rs/src/value/json/float.rs b/third_party/convex_rs/src/value/json/float.rs new file mode 100644 index 00000000..6777eb20 --- /dev/null +++ b/third_party/convex_rs/src/value/json/float.rs @@ -0,0 +1,19 @@ +use anyhow::anyhow; + +/// Helper functions for encoding `f64`s as `String`s. +pub enum JsonFloat {} + +impl JsonFloat { + /// Encode an `f64` as a string. + pub fn encode(n: f64) -> String { + base64::encode(n.to_le_bytes()) + } + + /// Decode an `f64` from a string. + pub fn decode(s: String) -> anyhow::Result { + let bytes: [u8; 8] = base64::decode(s.as_bytes())? + .try_into() + .map_err(|_| anyhow!("Float64 must be exactly eight bytes"))?; + Ok(f64::from_le_bytes(bytes)) + } +} diff --git a/third_party/convex_rs/src/value/json/integer.rs b/third_party/convex_rs/src/value/json/integer.rs new file mode 100644 index 00000000..15f914c5 --- /dev/null +++ b/third_party/convex_rs/src/value/json/integer.rs @@ -0,0 +1,19 @@ +use anyhow::anyhow; + +/// Helper functions for encoding `Int64`s as `String`s. +pub enum JsonInteger {} + +impl JsonInteger { + /// Encode an integer as a string. + pub fn encode(n: i64) -> String { + base64::encode(n.to_le_bytes()) + } + + /// Decode an integer from a string. + pub fn decode(s: String) -> anyhow::Result { + let bytes: [u8; 8] = base64::decode(s.as_bytes())? + .try_into() + .map_err(|_| anyhow!("Int64 must be exactly eight bytes"))?; + Ok(i64::from_le_bytes(bytes)) + } +} diff --git a/third_party/convex_rs/src/value/json/mod.rs b/third_party/convex_rs/src/value/json/mod.rs new file mode 100644 index 00000000..18d8d589 --- /dev/null +++ b/third_party/convex_rs/src/value/json/mod.rs @@ -0,0 +1,157 @@ +use std::{ + cmp::Ordering, + collections::BTreeMap, + num::FpCategory, +}; + +use anyhow::Context; +use serde_json::{ + json, + Value as JsonValue, +}; + +use crate::value::Value; + +mod bytes; +mod float; +mod integer; + +/// Is a floating point number native zero? +fn is_negative_zero(n: f64) -> bool { + matches!(n.total_cmp(&-0.0), Ordering::Equal) +} + +impl From for JsonValue { + fn from(value: Value) -> JsonValue { + match value { + Value::Null => JsonValue::Null, + Value::Int64(n) => json!({ "$integer": integer::JsonInteger::encode(n) }), + Value::Float64(n) => { + let mut is_special = is_negative_zero(n); + is_special |= match n.classify() { + FpCategory::Zero | FpCategory::Normal | FpCategory::Subnormal => false, + FpCategory::Infinite | FpCategory::Nan => true, + }; + if is_special { + json!({ "$float": float::JsonFloat::encode(n) }) + } else { + json!(n) + } + }, + Value::Boolean(b) => json!(b), + Value::String(s) => json!(s), + Value::Bytes(b) => json!({ "$bytes": bytes::JsonBytes::encode(&b) }), + Value::Array(a) => JsonValue::from(a), + Value::Object(o) => o.into_iter().collect(), + } + } +} + +impl TryFrom for Value { + type Error = anyhow::Error; + + fn try_from(value: JsonValue) -> anyhow::Result { + let r = match value { + JsonValue::Null => Self::Null, + JsonValue::Bool(b) => Self::from(b), + JsonValue::Number(n) => { + // TODO: JSON supports arbitrary precision numbers? + let n = n + .as_f64() + .context("Arbitrary precision JSON integers unsupported")?; + Value::from(n) + }, + JsonValue::String(s) => Self::from(s), + JsonValue::Array(arr) => { + let mut out = Vec::with_capacity(arr.len()); + for a in arr { + out.push(Value::try_from(a)?); + } + Value::Array(out) + }, + JsonValue::Object(map) => { + if map.len() == 1 { + let (key, value) = map.into_iter().next().unwrap(); + match &key[..] { + "$bytes" => { + let i: String = serde_json::from_value(value)?; + Self::Bytes(bytes::JsonBytes::decode(i)?) + }, + "$integer" => { + let i: String = serde_json::from_value(value)?; + Self::from(integer::JsonInteger::decode(i)?) + }, + "$float" => { + let i: String = serde_json::from_value(value)?; + let n = float::JsonFloat::decode(i)?; + // Float64s encoded as a $float object must not fit into a regular + // `number`. + if !is_negative_zero(n) { + if let FpCategory::Normal | FpCategory::Subnormal = n.classify() { + anyhow::bail!("Float64 {} should be encoded as a number", n); + } + } + Self::from(n) + }, + "$set" => { + anyhow::bail!( + "Received a Set which is no longer supported as a Convex type, \ + with values: {value}" + ); + }, + "$map" => { + anyhow::bail!( + "Received a Map which is no longer supported as a Convex type, \ + with values: {value}" + ); + }, + _ => { + let mut fields = BTreeMap::new(); + fields.insert(key, Self::try_from(value)?); + Self::Object(fields) + }, + } + } else { + let mut fields = BTreeMap::new(); + for (key, value) in map { + fields.insert(key, Self::try_from(value)?); + } + Self::Object(fields) + } + }, + }; + Ok(r) + } +} + +#[cfg(test)] +mod tests { + use convex_sync_types::testing::assert_roundtrips; + use proptest::prelude::*; + use serde_json::Value as JsonValue; + + use crate::Value; + + proptest! { + #![proptest_config( + ProptestConfig { failure_persistence: None, ..ProptestConfig::default() } + )] + + #[test] + fn test_value_roundtrips(value in any::()) { + assert_roundtrips::(value); + } + } + + #[test] + fn test_value_roundtrips_trophies() { + let trophies = vec![ + Value::Float64(1.0), + Value::Float64(f64::NAN), + Value::Array(vec![Value::Float64(f64::NAN)]), + ]; + for trophy in trophies { + assert_roundtrips::(trophy); + } + } +} diff --git a/third_party/convex_rs/src/value/mod.rs b/third_party/convex_rs/src/value/mod.rs new file mode 100644 index 00000000..681e141d --- /dev/null +++ b/third_party/convex_rs/src/value/mod.rs @@ -0,0 +1,136 @@ +use std::collections::BTreeMap; + +pub mod export; +mod json; +mod sorting; +use thiserror::Error; + +/// A value that can be passed as an argument or returned from Convex functions. +/// They correspond to the [supported Convex types](https://docs.convex.dev/database/types). +#[derive(Clone, Debug)] +#[allow(missing_docs)] +pub enum Value { + Null, + Int64(i64), + Float64(f64), + Boolean(bool), + String(String), + Bytes(Vec), + Array(Vec), + Object(BTreeMap), +} + +impl> From> for Value { + fn from(v: Option) -> Value { + v.map(|v| v.into()).unwrap_or(Value::Null) + } +} + +impl From for Value { + fn from(v: i64) -> Value { + Value::Int64(v) + } +} + +impl From for Value { + fn from(v: f64) -> Value { + Value::Float64(v) + } +} + +impl From for Value { + fn from(v: bool) -> Value { + Value::Boolean(v) + } +} + +impl From<&str> for Value { + fn from(v: &str) -> Value { + Value::String(v.into()) + } +} + +impl From for Value { + fn from(v: String) -> Value { + Value::String(v) + } +} + +impl From> for Value { + fn from(v: Vec) -> Value { + Value::Bytes(v) + } +} + +impl From> for Value { + fn from(v: Vec) -> Value { + Value::Array(v) + } +} + +#[cfg(any(test, feature = "testing"))] +mod proptest { + use proptest::prelude::*; + + use super::Value; + + impl Arbitrary for Value { + type Parameters = (); + type Strategy = proptest::strategy::BoxedStrategy; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + value_strategy(4, 32, 8).boxed() + } + } + + fn value_strategy( + depth: usize, + node_target: usize, + branching: usize, + ) -> impl Strategy { + // https://altsysrq.github.io/proptest-book/proptest/tutorial/recursive.html + let leaf = prop_oneof![ + 1 => Just(Value::Null), + 1 => any::().prop_map(Value::from), + 1 => (prop::num::f64::ANY | prop::num::f64::SIGNALING_NAN).prop_map(Value::from), + 1 => any::().prop_map(Value::from), + 1 => any::().prop_map(Value::String), + 1 => any::>().prop_map(Value::Bytes), + ]; + leaf.prop_recursive( + depth as u32, + node_target as u32, + branching as u32, + move |inner| { + prop_oneof![ + // Manually create the strategies here rather than using the `Arbitrary` + // implementations on `Array`, etc. This lets us explicitly pass `inner` + // through rather than starting the `Value` strategy from + // scratch at each tree level. + prop::collection::vec(inner.clone(), 0..branching).prop_map(Value::Array), + prop::collection::btree_map(any::(), inner, 0..branching) + .prop_map(Value::Object), + ] + }, + ) + } +} + +/// An application error that can be returned from Convex functions. To learn +/// more about throwing custom application errors, see [Convex Errors](https://docs.convex.dev/functions/error-handling/application-errors#throwing-application-errors). +#[derive(Error, Clone, PartialEq, Eq)] +#[error("{:}", message)] +pub struct ConvexError { + /// From any error, redacted from prod deployments. + pub message: String, + /// Custom application error data payload that can be passed from your + /// function to a client. + pub data: Value, +} + +impl std::fmt::Debug for ConvexError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = &self.message; + write!(f, "{message:#?}") + } +} diff --git a/third_party/convex_rs/src/value/sorting.rs b/third_party/convex_rs/src/value/sorting.rs new file mode 100644 index 00000000..5990f205 --- /dev/null +++ b/third_party/convex_rs/src/value/sorting.rs @@ -0,0 +1,75 @@ +//! Implementation of `Ord` and `Eq` for `Value` that works around limitations +//! of f64 by using a `TotalOrdF64` type. + +use std::{ + cmp::Ordering, + collections::BTreeMap, +}; + +use crate::value::Value; + +#[derive(Eq, PartialEq, Ord, PartialOrd)] +enum OrdValue<'a> { + Null, + Int64(i64), + Float64(TotalOrdF64), + Boolean(bool), + String(&'a String), + Bytes(&'a Vec), + Array(&'a Vec), + Object(&'a BTreeMap), +} + +impl<'a> From<&'a Value> for OrdValue<'a> { + fn from(v: &'a Value) -> OrdValue<'a> { + match v { + Value::Null => OrdValue::Null, + Value::Int64(x) => OrdValue::Int64(*x), + Value::Float64(x) => OrdValue::Float64(TotalOrdF64(*x)), + Value::Boolean(x) => OrdValue::Boolean(*x), + Value::String(x) => OrdValue::String(x), + Value::Bytes(x) => OrdValue::Bytes(x), + Value::Array(x) => OrdValue::Array(x), + Value::Object(x) => OrdValue::Object(x), + } + } +} + +#[derive(Clone, Debug)] +struct TotalOrdF64(f64); + +impl Ord for TotalOrdF64 { + fn cmp(&self, other: &Self) -> Ordering { + self.0.total_cmp(&other.0) + } +} +impl PartialOrd for TotalOrdF64 { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl PartialEq for TotalOrdF64 { + fn eq(&self, other: &Self) -> bool { + matches!(self.cmp(other), Ordering::Equal) + } +} +impl Eq for TotalOrdF64 {} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} +impl Eq for Value {} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Value { + fn cmp(&self, other: &Self) -> Ordering { + OrdValue::from(self).cmp(&OrdValue::from(other)) + } +} diff --git a/tool/convex_client_gauntlet/runtime/lib/runner.dart b/tool/convex_client_gauntlet/runtime/lib/runner.dart index 279eb821..655ebac0 100644 --- a/tool/convex_client_gauntlet/runtime/lib/runner.dart +++ b/tool/convex_client_gauntlet/runtime/lib/runner.dart @@ -390,6 +390,9 @@ final class GauntletRunner { 'tokenChanged': false, 'reconnectCalled': false, 'recoveryMs': null, + 'freshTokenAcceptedMs': null, + 'manualReconnectMs': null, + 'postReconnectAcceptedMs': null, 'acceptedAfterRefresh': false, 'queuedBatchReplayedExactlyOnce': false, }, @@ -445,20 +448,53 @@ final class GauntletRunner { auth['refreshSessionCalled'] = true; auth['tokenChanged'] = nextSession.accessToken != oldAccessToken; final recovery = Stopwatch()..start(); + final recoveryDeadline = DateTime.now().add(const Duration(seconds: 20)); await candidate.recoverAuth(nextSession.accessToken); - await candidate.reconnect(); + + Future waitForCurrentUser() async { + while (DateTime.now().isBefore(recoveryDeadline)) { + final remaining = recoveryDeadline.difference(DateTime.now()); + final attemptTimeout = remaining < const Duration(seconds: 1) + ? remaining + : const Duration(seconds: 1); + try { + final me = await candidate + .query('users:me', const {}) + .timeout(attemptTimeout); + if (me != null) return me; + } catch (_) { + // The auth-error reconnect may still be fetching the fresh token. + } + await Future.delayed(const Duration(milliseconds: 100)); + } + return null; + } + + var me = await waitForCurrentUser(); + if (me == null) { + throw const GauntletFailure( + 'auth_refresh_recovery_failed', + 'Fresh access token was not accepted within the bounded recovery window', + ); + } + auth['freshTokenAcceptedMs'] = recovery.elapsedMicroseconds / 1000; + auth['reconnectCalled'] = true; - Object? me; - for (var attempt = 0; attempt < 20 && me == null; attempt += 1) { - await Future.delayed(const Duration(milliseconds: 250)); + final remaining = recoveryDeadline.difference(DateTime.now()); + if (remaining <= Duration.zero) { + me = null; + } else { try { - me = await candidate - .query('users:me', const {}) - .timeout(const Duration(milliseconds: 750)); + final reconnectDuration = await candidate.reconnect().timeout( + remaining, + ); + auth['manualReconnectMs'] = reconnectDuration.inMicroseconds / 1000; + me = await waitForCurrentUser(); } catch (_) { - // The transport may still be replaying its auth state after reconnect. + me = null; } } + auth['postReconnectAcceptedMs'] = recovery.elapsedMicroseconds / 1000; if (me == null) { throw const GauntletFailure( 'auth_refresh_recovery_failed', diff --git a/tool/convex_client_gauntlet/runtime/lib/transport.dart b/tool/convex_client_gauntlet/runtime/lib/transport.dart index 74be617e..1b041098 100644 --- a/tool/convex_client_gauntlet/runtime/lib/transport.dart +++ b/tool/convex_client_gauntlet/runtime/lib/transport.dart @@ -250,20 +250,13 @@ final class ConvexFlutterTransport implements IcarusConvexTransport { @override Future reconnect() async { final stopwatch = Stopwatch()..start(); - final deadline = DateTime.now().add(const Duration(seconds: 20)); - while (DateTime.now().isBefore(deadline)) { - try { - final connected = await _client.reconnect().timeout( - const Duration(seconds: 1), - ); - if (connected) return stopwatch.elapsed; - } catch (_) { - // The public reconnect call is a health query rather than a socket - // transition. Poll it within the shared bounded recovery window. - } - await Future.delayed(const Duration(milliseconds: 250)); + final connected = await _client.reconnect().timeout( + const Duration(seconds: 20), + ); + if (!connected) { + throw StateError('convex_flutter did not complete its reconnect'); } - throw StateError('convex_flutter failed to reconnect within 20 seconds'); + return stopwatch.elapsed; } @override From be63971abee2404ca2315f3cdb24c934c1ff2549 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 27 Aug 2026 04:39:30 -0400 Subject: [PATCH 11/11] test: record valid Convex client rerun --- analysis_options.yaml | 1 + .../convex_dart_client_fair_rerun_result.md | 239 +++-- tool/convex_client_gauntlet/runtime/README.md | 60 +- .../results/convex_flutter_correctness.json | 2 +- .../runtime/results/dartvex_correctness.json | 2 +- .../runtime/results/fair_rerun_matrix.json | 87 +- .../runtime/results/paired_profile_macos.json | 838 ++++++++++++++++++ .../tool/summarize_paired_profile.dart | 210 +++++ tsconfig.json | 3 +- 9 files changed, 1326 insertions(+), 116 deletions(-) create mode 100644 tool/convex_client_gauntlet/runtime/results/paired_profile_macos.json create mode 100644 tool/convex_client_gauntlet/runtime/tool/summarize_paired_profile.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index f3c276bd..607843c0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,7 @@ analyzer: exclude: - build/** - lib/hive/hive_adapters.g.dart + - third_party/** - tool/convex_client_gauntlet/** errors: curly_braces_in_flow_control_structures: ignore diff --git a/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md index 2f2ec886..b268d02a 100644 --- a/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md +++ b/docs/cloud_sync_refactor/convex_dart_client_fair_rerun_result.md @@ -1,43 +1,74 @@ # Convex Dart client fair rerun result -Status: correctness complete; profiling blocked on 2026-08-27 +Status: complete on 2026-08-27 -Harness commit: `bf421ffef06b5d04749c77bda182f8f0a53796fe` +Harness and repair commit: `fb83488c0924f8daf57c4bdfc48d7a4a5ff0c8f5` -Candidates: Dartvex 0.2.0 and `convex_flutter` 3.0.1 +Candidates: Dartvex 0.2.0 and `convex_flutter` 3.0.1 with the Icarus auth and +reconnect repair -Decision: Dartvex wins the runtime correctness gate, but neither client has -earned an application migration. +Decision: keep `convex_flutter`; do not migrate to Dartvex. ## Verdict -Dartvex completed all 50 deterministic seeds and all 50,000 ops. Every op -resolved: 45,500 landed and 4,500 produced the planned, visible revision -rejects. The fresh Supabase token was accepted after the rejected-token fault, -the queued batch was replayed exactly once, the persisted mid-run checkpoint -resumed, every verifier hash matched, and every `.ica` export round-tripped. - -`convex_flutter` hit the handoff's immediate losing condition on seed 0. Its -first 500 ops resolved as expected (450 landed and 50 visible revision rejects), -then the injected credential was rejected. Supabase `refreshSession()` returned -a different access token, but the package did not accept authenticated work -within the bounded 20-second recovery window after the rejected auth state was -cleared, the production-style refresh handle was replaced, and its public -reconnect path was exercised. The queued batch therefore could not be proven to -land exactly once. - -The application dependency remains unchanged. A correctness survivor is not -automatically an adoption winner: the repaired Dartvex contract gate proves a -narrow strict wrapper around one explicit `folders:listForParent` result, but -the runtime adapter still uses path-and-JSON calls because the stable generated -return surface for the runtime functions is not complete. Migrating now would -claim the JSON-plumbing benefit before proving it. +The earlier “Dartvex wins” statement was not a fair final verdict. Dartvex had +passed while `convex_flutter` was blocked by a package auth defect, so the run +had identified a broken candidate rather than measured two valid candidates. + +That defect is now fixed at the package and Rust-client layers. Both candidates +completed all 50 deterministic seeds and all 50,000 ops. Each recorded 45,500 +landed ops, 4,500 planned visible revision rejects, zero unresolved ops, exact +once-only replay after auth refresh, durable checkpoint recovery, 50 matching +canonical verifier hashes, and 50 successful `.ica` round-trips. + +There is therefore no correctness winner. In the valid paired profile, +`convex_flutter` was 31.9% faster by median runner wall time and 34.0% faster on +real reconnect-to-live time. Dartvex used 5.1% less peak RSS, 36.4% less CPU in +relative terms (5.29 percentage points), and 12.9% less transfer than +`convex_flutter` (14.9% more when expressed from the Dartvex baseline). Remote +convergence differed by only 0.156 ms at the median. + +Those are trade-offs, not a blanket performance winner. Icarus should keep its +current client because Dartvex's stable generated return surface is still +incomplete for the runtime functions, so migrating would not yet remove the +path-and-JSON boundary that motivated the evaluation. The cost is explicit: +the current repair vendors both `convex_flutter` and Convex Rust 0.10.4 until +equivalent fixes are published upstream. + +## Auth repair + +The failure had four interacting causes: + +- the published Flutter adapter owned a separate token-expiry timer and fed the + Rust client static auth, outside the state machine that replays auth, + subscriptions, and in-flight mutations after reconnect; +- disposing an old refresh handle could asynchronously clear a newer auth + callback; +- the public native `reconnect()` method was an authenticated health query, not + a socket transition; +- Convex Rust applied independent client-worker and WebSocket network backoffs + to one auth rejection. A fresh token could sit behind a 15-second backoff, + while stale responses and connection metadata could start further protocol + recovery loops. + +The repair gives the token callback to the upstream Convex state machine, adds +generation ownership to auth handles, implements a real reconnect that waits +for connecting then connected, and patches Convex Rust to preserve one session +identity with monotonic connection counts. Auth rejection now uses a bounded +250 ms callback cadence, coordinates that state with the WebSocket worker so it +does not add a second network backoff, discards responses from the replaced +socket, and only exits auth recovery after a changed token receives a valid +server response. + +Ten consecutive debug calibration runs passed before the authoritative rerun. +In the final profile samples, `convex_flutter` accepted the refreshed token in +73.848–171.027 ms; all queued work landed exactly once. ## Fair workload -Both adapters used the same local Convex deployment, disposable client account, -public Supabase anon credential, base fixture, serialized op traces, IDs, -timeouts, and fault schedules. The seed-0 trace and schedule hashes match: +Both adapters used the same isolated local Convex deployment, disposable client +account, public Supabase anon credential, base fixture, serialized op traces, +IDs, timeouts, and fault schedules. The seed-0 trace and schedule hashes match: - trace: `ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06` - fault schedule: @@ -48,75 +79,107 @@ timeouts, and fault schedules. The seed-0 trace and schedule hashes match: The 1,000-op trace covers strategy, page, page content, element, and lineup changes, including add, patch, reorder, delete/recreate, duplicate delivery, -revision conflicts, subscription restart, reconnect, offline delay, auth -rejection/refresh, and durable process restart. A new Dartvex client acts as -verifier C. Canonical state excludes server-authored transport clocks, including -page-content creation/update clocks; a regression test proves those clocks -cannot create a false state mismatch. +revision conflicts, subscription restart, real reconnect, offline delay, auth +rejection/refresh, and durable process restart. A clean Dartvex client acts as +verifier C. Canonical state excludes server-authored transport clocks. + +## Correctness + +| Result | Dartvex | `convex_flutter` repaired | +| --- | ---: | ---: | +| Seeds | 50/50 | 50/50 | +| Operations | 50,000/50,000 | 50,000/50,000 | +| Landed | 45,500 | 45,500 | +| Planned visible rejects | 4,500 | 4,500 | +| Unresolved | 0 | 0 | +| Refreshed token accepted | yes | yes | +| Queued auth-fault batch landed once | yes | yes | +| Persisted checkpoint resumed | yes | yes | +| Canonical verifier and `.ica` round-trip | 50/50 | 50/50 | + +The debug correctness reports recorded 64,346.843 ms total runner wall time and +256,311,296 bytes maximum RSS for Dartvex, versus 74,446.106 ms and 266,141,696 +bytes for `convex_flutter`. These prove completion but are not used as the +performance comparison; the paired profile build below is authoritative for +that. + +## Paired macOS profile + +Ten paired profile-build trials were run per candidate, alternating which +candidate ran first and replacing deployment data before every run. CPU is +defined as process user plus system seconds divided by the runner's wall-clock +window. P95 uses nearest rank, so with ten samples it is the maximum observed +sample. + +| Metric | Dartvex median / p95 | `convex_flutter` median / p95 | +| --- | ---: | ---: | +| Remote convergence | 7.304 / 7.804 ms | 7.459 / 7.575 ms | +| Reconnect to live | 110.893 / 150.191 ms | 73.243 / 94.765 ms | +| Fresh token accepted | 9.144 / 9.662 ms | 127.134 / 171.027 ms | +| Full auth recovery | 147.883 / 166.973 ms | 205.734 / 543.221 ms | +| Runner wall time | 2,749.544 / 3,396.189 ms | 1,873.387 / 2,312.830 ms | +| Peak RSS | 134,406,144 / 134,856,704 B | 141,312,000 / 141,541,376 B | +| Average process CPU | 9.248% / 10.118% | 14.533% / 15.306% | +| Application JSON transfer | 1,439,001 / 1,439,001 B | 1,652,716 / 1,771,501 B | + +The shared universal macOS harness bundle is 78,540,800 bytes and contains both +adapters, so candidate-isolated bundle size is not available. The +`convex_flutter` native framework executable is 23,195,280 bytes; the shared App +framework executable is 8,000,528 bytes. The build contains arm64 and x86_64 +slices and ran on an arm64 Mac. Windows and Linux remain package-supported but +could not be built or measured from this macOS host; they are recorded as +unmeasured rather than silently generalized. ## Separate scorecards -| Area | Dartvex 0.2.0 | `convex_flutter` 3.0.1 | +| Area | Dartvex 0.2.0 | `convex_flutter` repaired | | --- | --- | --- | -| Explicit result rename | Old field access fails analysis | Hand-written decoding | -| Missing return | Icarus strict wrapper rejects public `dynamic` | Not applicable | -| Unsupported validator | Wrapper converts generator warning/exit 0 into failure/exit 2 | Not applicable | -| Determinism | Identical generated SHA-256 on two runs | Not generated | -| Runtime correctness | **Pass: 50/50 seeds, 50,000/50,000 ops** | **Fail: auth recovery at seed 0, op 500** | -| Rejected-token refresh | Fresh token accepted; queued batch lands once | Fresh token not accepted in 20 seconds | -| Restart recovery | Persisted checkpoint resumes | Not reached | -| Canonical and `.ica` verification | Pass on all 50 seeds | Not reached | -| Dependency health | Pure Dart client plus strict tooling package | Native bridge had to pin `flutter_rust_bridge` 2.11.1 instead of resolved 2.13.0 | -| Generated Icarus surface | Incomplete for runtime functions | None | - -The Dartvex correctness run recorded 20,224,618 application-JSON bytes sent, -48,117,804 received, 64,794.127 ms wall time, and 260,472,832 bytes maximum -RSS. These are correctness-run observations, not comparative performance -claims. There is no valid `convex_flutter` completion sample to pair with them. - -## Why Phase 4 did not run - -The handoff permits profiling only after both clients complete all correctness -seeds with canonical equality. `convex_flutter` failed before completing seed -0, so paired profile runs, CPU comparison, build-size comparison, and -cross-platform performance builds are intentionally recorded as zero/not run. -Running them anyway would let speed distract from uncertain queued work. - -## Artifacts and reproduction - -The historical first result remains unchanged at -[`convex_dart_client_gauntlet_result.md`](convex_dart_client_gauntlet_result.md). -The fair rerun adds: +| Runtime correctness | pass | pass | +| Rejected-token recovery | pass | pass after package/Rust repair | +| Real reconnect | pass | pass after replacing health-query implementation | +| Median wall time | slower | faster | +| CPU, RSS, transfer | lower | higher | +| Generated contract boundary | strict wrapper passes, runtime return surface incomplete | none; hand-written JSON boundary | +| Maintenance | pure Dart package plus local strict gate | vendored Flutter package, Rust crate, generated bridge, and pinned FRB 2.11.1 | + +## Artifacts - [`contract_gate.json`](../../tool/convex_client_gauntlet/results/contract_gate.json) - [`dartvex_correctness.json`](../../tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json) - [`convex_flutter_correctness.json`](../../tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json) +- [`paired_profile_macos.json`](../../tool/convex_client_gauntlet/runtime/results/paired_profile_macos.json) - [`fair_rerun_matrix.json`](../../tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json) - [runtime commands](../../tool/convex_client_gauntlet/runtime/README.md) - -The raw correctness artifacts contain no email, password, access token, refresh -token, Supabase key, or elevated credential. The deployment was the isolated -local instance at `127.0.0.1:3210`; no production or user library data was used. - -## Repository verification - -After writing the result artifacts, the branch passed: - -- the repaired Dartvex gate, its 2 Dart tests, and Dart analysis; -- runtime-gauntlet formatting, Dart analysis, and all 3 deterministic workload - tests (the separately invoked live-deployment smoke test is skipped without a - deployment define); -- native runner analysis and a macOS debug build; -- `npx tsc --noEmit` and all 22 Convex tests; -- all 343 Flutter tests; -- Flutter analysis with exit 0 and the same 6 pre-existing info-level lints; -- `fvm flutter build web --no-tree-shake-icons`. +- [`convex_flutter` patch notes](../../third_party/convex_flutter/ICARUS_PATCH.md) +- [Convex Rust patch notes](../../third_party/convex_rs/ICARUS_PATCH.md) + +The raw artifacts contain no email, password, access token, refresh token, +Supabase key, Convex admin key, or elevated credential. The deployment was the +isolated local instance at `127.0.0.1:3210`; no production or user library data +was used. + +## Verification + +- The contract gate passed all six checks; its two regression tests and Dart + analysis passed. +- The runtime harness passed four workload tests, with the environment-gated + transport smoke test skipped, and both the runtime and nested app analyzed + cleanly. +- The Icarus suite passed all 343 tests, including all 15 auth-provider tests. + TypeScript analysis passed and the 22 Convex boundary tests passed. +- The Icarus app analyzed with six pre-existing info lints and no warning or + error. Web and macOS release builds passed with icon tree shaking disabled; + the macOS app bundle was 97.8 MB. +- Convex Rust passed all 36 tests. The native Flutter package passed Cargo + check/test, and its Dart `lib/` analyzed with no warning or error. +- All four checked-in JSON artifacts parse, their recorded SHA-256 values + match, the final diff has no whitespace errors, and the artifact secret scan + passed. ## Next step -Keep the application unchanged for now. Dartvex is the only runtime-surviving -candidate, so any next client evaluation should focus narrowly on completing -explicit return validators and proving that its generated API materially -removes Icarus JSON plumbing without becoming a second generator or package -fork. Independently, `convex_flutter` auth recovery needs a package-level fix -or replacement before it can re-enter this comparison. +Keep `convex_flutter`, upstream the package and Convex Rust repairs, and remove +the local vendors when published releases pass this same gate. Dartvex can be +reconsidered after its stable generated return surface covers the actual Icarus +runtime boundary and proves that a migration removes JSON plumbing instead of +moving it into another wrapper. diff --git a/tool/convex_client_gauntlet/runtime/README.md b/tool/convex_client_gauntlet/runtime/README.md index db38d5df..6f3334cf 100644 --- a/tool/convex_client_gauntlet/runtime/README.md +++ b/tool/convex_client_gauntlet/runtime/README.md @@ -1,9 +1,9 @@ # Convex Dart client runtime gauntlet This package runs the same deterministic Icarus cloud workload through Dartvex -0.2.0 and `convex_flutter` 3.0.1. It targets an isolated local Convex deployment -and uses a disposable Supabase user with the public anon key. Never use or pass a -`service_role` key. +0.2.0 and the Icarus-repaired `convex_flutter` 3.0.1 package. It targets an +isolated local Convex deployment and uses a disposable Supabase user with the +public anon key. Never use or pass a `service_role` key. Each correctness candidate is configured for 50 seeds of 1,000 operations. The trace includes offline queuing, delay, duplicate delivery, subscription restart, @@ -65,3 +65,57 @@ Correctness reports are written to the app container's temporary directory and copied verbatim into `results/` after checking that they contain no credentials. Phase 4 profiling is forbidden unless both candidates pass all correctness conditions. + +## Auth and reconnect repair + +The local package at `third_party/convex_flutter` delegates refresh to the +Convex Rust state machine, protects replacement auth handles with a generation, +and exposes a real WebSocket reconnect. It uses the local Convex Rust 0.10.4 +crate at `third_party/convex_rs`, whose patch coordinates auth retry across the +client and WebSocket workers while preserving in-flight mutation replay. Read +both `ICARUS_PATCH.md` files before changing this boundary. + +Generated Flutter Rust Bridge files were produced with the pinned 2.11.1 +generator and must not be edited by hand: + +```sh +cd third_party/convex_flutter/rust +flutter_rust_bridge_codegen generate +``` + +## Run paired profile trials + +Only profile after both correctness artifacts pass. Build the native runner in +profile mode, choose an empty temporary output directory, and use the same +disposable public-client environment as correctness: + +```sh +cd tool/convex_client_gauntlet/runtime/app +fvm flutter build macos --profile +cd ../../../.. + +export PROFILE_OUTPUT_DIR=$(mktemp -d /tmp/icarus-convex-profile.XXXXXX) +export CONVEX_URL=http://127.0.0.1:3210 +export CONVEX_SELF_HOSTED_ADMIN_KEY=$(jq -r .adminKey .convex/local/default/config.json) +export TRIALS=10 +tool/convex_client_gauntlet/runtime/tool/run_paired_profile.sh +``` + +The script replaces deployment data before every candidate, runs ten pairs, +and alternates first position. Summarize the raw directory with the checked-in +tool; the three byte counts come from the built `.app` bundle and its native +framework executables: + +```sh +fvm dart run \ + tool/convex_client_gauntlet/runtime/tool/summarize_paired_profile.dart \ + "$PROFILE_OUTPUT_DIR" \ + "$SHARED_BUNDLE_BYTES" \ + "$CONVEX_FLUTTER_FRAMEWORK_BYTES" \ + "$APP_FRAMEWORK_BYTES" \ + > tool/convex_client_gauntlet/runtime/results/paired_profile_macos.json +``` + +The summary preserves every raw sample, records the alternating order, and +reports median plus nearest-rank p95. Windows and Linux measurements require +their own hosts; do not infer them from the macOS result. diff --git a/tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json b/tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json index 35fcc9ea..03d2ae0d 100644 --- a/tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json +++ b/tool/convex_client_gauntlet/runtime/results/convex_flutter_correctness.json @@ -1 +1 @@ -{"schemaVersion":1,"status":"failed","adapter":"convex_flutter","candidateVersion":"3.0.1","flutterRustBridgeVersion":"2.11.1 pinned","deployment":"local:127.0.0.1:3210","gitCommit":"bf421ffef06b5d04749c77bda182f8f0a53796fe","baseFixture":{"path":"test/fixtures/strategy_integrity/base-test-v43.ica","sha256":"8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a"},"seedCount":50,"operationsPerSeed":1000,"totalOperationsPlanned":50000,"losingCondition":"auth_refresh_recovery_failed","message":"Fresh access token was not accepted within the bounded recovery window","seed":0,"nextOperation":500,"ledgerSha256":"f924af909b90b24e268b3c48ec809f32f6efaa3bb92ed054b8e11a68a60ae151","partialSeed":{"seed":0,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06","faultScheduleSha256":"b4e244c38b89789f1191e988768aa681f4722184533077ef58eb6f95ee6a0e52","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"auth_reject_refresh","beforeBatch":10},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":false,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":true,"processRestart":false},"acknowledged":450,"rejected":50,"unresolved":0,"retryCount":0,"batchLatencyMs":[50.807,43.473,44.871,45.67,44.156,48.479,46.805,49.981,50.579,48.432],"auth":{"exercised":true,"rejectedTokenObserved":true,"refreshSessionCalled":true,"tokenChanged":true,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"2f5c83b805fd22471e866932f0ad3a6b438850368d79cbaada4e91a2306d36f8"},"bytesSent":225178,"bytesReceived":314150,"wallClockMs":31826.247,"maxRssBytes":255328256,"machine":{"operatingSystem":"macos","operatingSystemVersion":"Version 26.5.1 (Build 25F80)","processors":8,"dartVersion":"3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\""}} \ No newline at end of file +{"schemaVersion":1,"status":"passed","adapter":"convex_flutter","candidateVersion":"3.0.1","flutterRustBridgeVersion":"2.11.1 pinned","deployment":"local:127.0.0.1:3210","gitCommit":"fb83488c0924f8daf57c4bdfc48d7a4a5ff0c8f5","baseFixture":{"path":"test/fixtures/strategy_integrity/base-test-v43.ica","sha256":"8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a"},"seedCount":50,"operationsPerSeed":1000,"totalOperations":50000,"allCanonicalEqual":true,"allResolved":true,"processRestartCheckpoint":true,"bytesSent":20224647,"bytesReceived":49392642,"wallClockMs":74446.106,"maxRssBytes":266141696,"machine":{"operatingSystem":"macos","operatingSystemVersion":"Version 26.5.1 (Build 25F80)","processors":8,"dartVersion":"3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\""},"seeds":[{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":true,"exercised":true,"freshTokenAcceptedMs":101.769,"manualReconnectMs":34.005,"postReconnectAcceptedMs":156.172,"queuedBatchReplayedExactlyOnce":true,"reconnectCalled":true,"recoveryMs":156.18,"refreshSessionCalled":true,"rejectedTokenObserved":true,"tokenChanged":true},"batchLatencyMs":[44.707,41.851,42.265,45.106,42.297,45.437,44.446,48.175,46.64,47.419,29.093,49.055,50.128,51.137,64.476,52.093,66.192,67.051,45.383,44.0],"canonicalVerifierHash":"26d71e8df48fba1f7aae2c8cf4b5569e8b9360166b9bb6ed887ce7f14048f2f7","duplicateNoopHash":"2f5c83b805fd22471e866932f0ad3a6b438850368d79cbaada4e91a2306d36f8","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":10,"fault":"auth_reject_refresh"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"b4e244c38b89789f1191e988768aa681f4722184533077ef58eb6f95ee6a0e52","faults":{"authRefresh":true,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":92.329,"rejected":90,"remoteConvergenceMs":18.382,"retryCount":0,"roundTripHash":"57f03a40845f1cbcec927c4a2abc68781fcec79e10d5f8a896a7deb95bbd9d69","seed":0,"traceSha256":"ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[45.949,43.107,44.078,44.854,44.476,46.678,46.306,47.012,47.885,48.469,48.366,50.443,50.903,51.73,57.76,53.462,67.383,69.322,46.16,43.849],"canonicalVerifierHash":"521b140c0a612b8ea44a1d9b4b03cbf0573799ae00bf05498122bacab81c1bba","duplicateNoopHash":"f5cf17c272add3c1c7d3460a3f29c4ee4bf786dd0d24d00d5010fd3069450206","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":7.239,"rejected":90,"remoteConvergenceMs":11.252,"retryCount":0,"roundTripHash":"42034206a0ce276524b5f6c4b61270d036eafa3ea888f6af32fcc8898136425d","seed":1,"traceSha256":"74e1c135f93f0e9ebf7029a5222aaa38b6aa0f834e31861e9e3de8515922bca4","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[47.552,43.031,42.595,45.482,44.983,46.66,47.505,47.618,48.446,48.478,49.09,50.487,52.513,52.6,65.687,54.778,67.709,70.963,47.278,46.359],"canonicalVerifierHash":"9ca1893e49091976796548e8568b4e1791bff1f72f5f883892ef688a59483304","duplicateNoopHash":"f241cf2843f3adfa94f6566c00df83a1f234b4993b73daa1fe08ff8b34e89260","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":87.211,"rejected":90,"remoteConvergenceMs":10.05,"retryCount":0,"roundTripHash":"44c8e21bbcb1c01d01bd1446150e3c5e3daa0f256aced3da4e96ac1afeb6be93","seed":2,"traceSha256":"2b3e81b55c925a72176cd6f3d1515c7063d7706d8d4381c641b6fc0a8e121262","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[49.714,43.877,43.909,44.893,46.781,47.5,47.647,48.754,51.648,51.527,52.444,52.231,52.838,53.858,60.792,55.667,68.129,77.984,64.097,47.918],"canonicalVerifierHash":"197d1fc1c62be082b93dd2c51021a15c68e3f7551de5e0d949b11152f6ab6840","duplicateNoopHash":"1e34802380fb4c7b66dfe194d29cf6bb4a223e342c5ac58fb77bd10b9436b658","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":49.407,"rejected":90,"remoteConvergenceMs":9.995,"retryCount":0,"roundTripHash":"18bb3a755432638399f32b7fb39627dab2c7c474726dc01b433243d45e86ab8f","seed":3,"traceSha256":"f7105a396da40cd883ec4cc9e83f1a6cb53706e90e782cc3e71cb778bf252235","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[49.966,47.689,47.367,50.407,49.031,51.155,49.532,57.789,52.725,51.745,54.655,51.385,56.104,58.176,61.634,57.892,71.717,72.04,49.315,45.583],"canonicalVerifierHash":"472d3b636388ed4b3ce98242c337cdca98191dd93feeaf23b2f0bf72c2068232","duplicateNoopHash":"94cb107907f89f5fc3a85d810566e5f67ec06498ef9345e4236bce4c79443170","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":25.947,"rejected":90,"remoteConvergenceMs":10.553,"retryCount":0,"roundTripHash":"6bc869c98139d476a3b61de2ca80637ca4d956c2e99008462e6667110516d5e8","seed":4,"traceSha256":"f8cc2d86c82f00b22442e2b457b962f2e31d958be26bd26999c5035502d44d52","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.041,43.703,45.469,48.68,46.008,47.216,46.827,54.289,49.271,51.328,50.376,51.61,54.378,54.653,62.98,56.145,69.919,71.891,47.321,46.863],"canonicalVerifierHash":"60e119554dd2c84670ff85ad1f4743fe3b39bb2367a81ecfa4f978cd7e834be1","duplicateNoopHash":"69a9e28c6ba4259807f9135260188fb48763db7cd60da9f0bcbb2df689ac7fec","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":42.08,"rejected":90,"remoteConvergenceMs":9.685,"retryCount":0,"roundTripHash":"3e1eb93aba491b833fe657b72a49cb08871e51b6abd93ced58a16b4c1bb1d714","seed":5,"traceSha256":"d0e377de6c9acd5bde977020ce6b5a3b79f41ba792748eeb87617f0bbfe67073","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.553,47.819,46.304,47.833,47.204,48.559,48.989,50.424,52.12,52.562,52.797,53.893,55.107,54.868,60.989,56.504,71.466,71.321,48.614,48.095],"canonicalVerifierHash":"870bdc3fe241a36e4cafa9cd2db6550900db537148df0012129a4fc729168e44","duplicateNoopHash":"7b6d6f868d69e5c19a34b1ef7a5da5bd85c7b2c0de6b278023971d5125376186","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":29.016,"rejected":90,"remoteConvergenceMs":9.674,"retryCount":0,"roundTripHash":"a6d78c0173ff37ca01ce546bd371b25102dff424fc71e5ed158da8cc3afb9409","seed":6,"traceSha256":"4089e29162d4a1f4eeb277698a40e77dee654c5e6b024de882412828b2b868be","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[49.386,45.44,46.348,48.344,48.597,47.535,49.196,50.188,59.507,59.799,55.295,53.177,52.439,55.211,62.945,56.297,73.11,70.554,50.714,46.563],"canonicalVerifierHash":"07d665cbffdf875c025bb158a561350943ae3fbd5068762fd11819a6a4901942","duplicateNoopHash":"abc350378349c0c5ca8ac8718147350b24bba91d01f1b3584ac0ae8728392bbb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":19.424,"rejected":90,"remoteConvergenceMs":10.021,"retryCount":0,"roundTripHash":"e9e701572c478eb4ff07bdc25b9686439e87f671b6912b55edab9f6640bd9348","seed":7,"traceSha256":"2f479b6ed6743dedae4a36d95c58c95f225ba529fcd863e5640f49afc5e838b7","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[49.294,43.982,45.761,47.83,50.264,48.767,48.654,50.564,51.155,63.965,62.378,56.951,54.38,56.56,67.467,59.287,74.27,78.166,118.919,47.722],"canonicalVerifierHash":"efc2b0c123c804ed7f1847b3cfaec274bad1ea5ae14b606f04cc0a565458437f","duplicateNoopHash":"7c4a10f6a44922af7b90a06f9e3796d4915b37934f1958e659c352ef988032a7","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":85.26,"rejected":90,"remoteConvergenceMs":10.311,"retryCount":0,"roundTripHash":"2381d28c5762232a05a96531e03aabc0b196671f5f4c493cfb8a2bc1e16fed87","seed":8,"traceSha256":"18d04c6a59b74a9026ab9d6f69161a1ae3101de4fe16acc08b831a885a6a301f","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[50.295,43.717,47.616,49.941,47.425,49.697,49.079,51.88,53.159,52.308,52.199,53.113,54.791,56.474,60.777,55.946,70.53,70.857,48.277,48.61],"canonicalVerifierHash":"703442e3b93e7bc929da059e86ab44de647f270cefc120656f501966b6847100","duplicateNoopHash":"acb46046274da0c9262f4910b29e4fda0a2bfe3132374a35db02c9f2f89398f4","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":38.728,"rejected":90,"remoteConvergenceMs":10.032,"retryCount":0,"roundTripHash":"e63096e423e4fa5de04d55dd992a191a939e288f22077b5dba9f1f30d0312ac0","seed":9,"traceSha256":"10e7fe955f927f3626b5586eff07c5c452e7dd8fada065f6ea12839907ebfb8f","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.633,46.5,47.956,52.804,49.096,51.777,49.572,50.496,51.733,52.54,53.009,53.555,53.172,55.874,68.72,55.392,72.094,73.195,48.905,48.505],"canonicalVerifierHash":"0dedd28fa52610998313b57e5e621be6488408b8475b8082434152a7e4bfffde","duplicateNoopHash":"68de0d932dda40e9fa94ad87bce5062194f4b64d032743224b09543a35fadb04","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":89.362,"rejected":90,"remoteConvergenceMs":10.218,"retryCount":0,"roundTripHash":"e199bf3ee7ce8299f481c487ecb563b9bc5a22508107847ee32280f5af2799fa","seed":10,"traceSha256":"3aedbcc55f63d2e40d1053713da07189c675eeedafc45557893bfb44a4a4a3fa","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.773,45.342,45.934,48.629,47.559,49.479,51.342,50.529,51.542,52.331,53.87,55.72,53.667,56.686,63.206,56.797,71.028,72.407,49.82,49.224],"canonicalVerifierHash":"976df9bc169579795add78059b22aecb15add1a77fa132a762b81256d7a14ab9","duplicateNoopHash":"ecd01e95d0ba98e4c1033dd1c11ae805f94ea9c57caa7eeb3883adbdba18baa9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":55.064,"rejected":90,"remoteConvergenceMs":10.061,"retryCount":0,"roundTripHash":"6b2a4c8cae985614914a1122c691c7976d2f5fe6bc30dca6a2a54e7a718ebbdb","seed":11,"traceSha256":"d64e19b329a28294c7e145d5eea4d659b86b00fdc478f7472e57f1b93de378ef","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.652,46.145,48.342,49.45,48.894,49.412,50.149,50.646,50.096,51.758,52.675,55.359,57.378,56.341,63.875,58.98,71.401,73.779,50.545,47.715],"canonicalVerifierHash":"fe27618043429656abed9f1810fd3f7fe21fa7844cfe474e4ee27170aa6ba761","duplicateNoopHash":"75cb591df89ce4872e5d71150982ccb110460036da1a031d13af69b1a497bf44","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":58.156,"rejected":90,"remoteConvergenceMs":10.104,"retryCount":0,"roundTripHash":"08db87f541a6ad80969ae614cb3c192ee4668e76d650a3c3c83ae2205d7cae2f","seed":12,"traceSha256":"56270e40851c311cafe2638b7bdf9e162ca4190285215f3a788c34ab68e9fbfa","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[50.697,47.152,46.472,56.983,49.683,51.149,64.029,52.122,52.172,52.643,54.316,54.731,55.074,56.358,59.82,58.785,72.603,73.568,51.587,50.762],"canonicalVerifierHash":"1eb6c99489819977b8c62d4ac7cc2b6332253eea89c0028a0d00f543ccc80b4e","duplicateNoopHash":"a2780357577e00a64a3d6882842034f3c7a33a7fb73f76a1bb513582adc82f48","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":11.061,"rejected":90,"remoteConvergenceMs":10.052,"retryCount":0,"roundTripHash":"30a8267dc6ce65b39cf9e2f8e9d47c425c22d34ae75df7a6427dcfbf320b49ca","seed":13,"traceSha256":"04fbda33461bbb975b9e66fc1ff0fe176931fdbd9c56fe8d3b9f9abcc939ab07","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.295,46.698,48.278,49.61,48.686,49.284,51.029,51.361,51.887,342.716,56.099,59.679,58.2,59.593,70.269,60.887,78.985,74.897,53.681,48.627],"canonicalVerifierHash":"9f4f93499745ce94e2ea84804d41cdd24007491f9ad333f6994c23adaca1a97f","duplicateNoopHash":"6a79d43b71c80e3ece66ab93693e6111df9ac8633c5e8af7dfbcdc86f73f16e9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":78.161,"rejected":90,"remoteConvergenceMs":9.844,"retryCount":0,"roundTripHash":"4406199eb581980a79d880cbb483fd97b1ac2b9ecf927ac3d48a1ede8f3a8ca6","seed":14,"traceSha256":"d105bcc5f7eaa8a768bd666c3d6fb072fb97a0191a935d70e688d086e52f95c4","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.666,47.994,47.75,50.615,53.284,51.322,50.478,55.45,52.215,55.575,55.355,53.413,54.262,55.339,63.444,56.189,71.806,73.609,60.895,49.873],"canonicalVerifierHash":"e44921495d13f7e764fb5170d992c6806add23c1ee2a902d2ed586e78b6518f2","duplicateNoopHash":"89f952d57ff5c744f32966f233f39fdf76ff3729bca2ffb22da379572bc772cb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":53.214,"rejected":90,"remoteConvergenceMs":8.241,"retryCount":0,"roundTripHash":"885dcea93a305a0527b65c09ab8a51e42eeab8486b0a17a812adfc8a23dd0692","seed":15,"traceSha256":"fd2509f037ad595831e5af5de358a045a4944c5070aeccceca9a85e5d93f0dcb","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[53.93,46.824,47.308,51.829,54.307,52.601,57.036,51.022,51.847,52.357,54.543,54.229,56.829,55.343,63.123,55.565,70.102,73.997,50.979,48.806],"canonicalVerifierHash":"fcab45e94cbb1dd32f40022c4f2538d8a8921f6f9a993c44da5a1af856273521","duplicateNoopHash":"1a2c13daad7b021d5bf7aba90696fb2fba2a5479ff1fa49027c88e3b5e9bf237","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":19.791,"rejected":90,"remoteConvergenceMs":9.784,"retryCount":0,"roundTripHash":"23079740e8c81056d542e80219368de85f2c9ae8740e9db95182ac2d1433a5cf","seed":16,"traceSha256":"15974b2b6d314a57e29fe393296085940f83539b4485eb9ada849149db7a99cc","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[53.846,47.727,48.758,49.36,50.423,56.684,53.109,52.369,52.503,52.931,53.925,55.136,55.665,56.68,59.708,57.135,71.4,72.471,48.577,48.46],"canonicalVerifierHash":"0ae80e9274783e32170474a495f3c31bfa702e9acbe970cb1a04c439846eb86b","duplicateNoopHash":"834e79323bda37030a4c6af90b9a0dadade53242cd9801ba090d12a86054e645","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":7.261,"rejected":90,"remoteConvergenceMs":9.864,"retryCount":0,"roundTripHash":"5ebbea09bcb5c13323f4a6dea81b146de93f7fa134b025b2163ca48c62222abc","seed":17,"traceSha256":"53cce4ffa3236c18592a997edd684f10596e8cdceacd8402d858c284a7d9054d","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[50.608,47.559,47.477,50.158,49.069,48.533,51.221,53.199,51.877,52.813,53.072,56.713,56.292,58.505,69.121,58.484,70.911,74.338,49.988,48.599],"canonicalVerifierHash":"f3fd473e5c7029efed7f8b2b39933c0240a36cf83a5eb3286adfe377c9890da5","duplicateNoopHash":"2328c71bfb35d1fc60079b4c3572d96fac5e48f92fa4fee9baa2f9a469aa7404","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":80.278,"rejected":90,"remoteConvergenceMs":10.318,"retryCount":0,"roundTripHash":"9f8e4e09c1123b52138e32282eeb53f1d13c8317d8f3b2cda0cb11c576704704","seed":18,"traceSha256":"a542c21db854c18f4cdcb385e6ac298bf7af6282c0972913cc8fce70aec86847","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.778,47.094,48.674,50.543,48.474,50.827,50.105,51.601,71.425,52.574,53.202,53.139,55.747,56.322,60.965,58.444,70.508,72.585,50.806,47.614],"canonicalVerifierHash":"63e156ef82374f25fc17ac38cd7fb0ffa365fa4d6fcd29f42be9a52f729ab158","duplicateNoopHash":"1121a351ad1dd9adce4685ffd6e719a32e98c70594e4a09f928d6f996e9d1207","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":15.003,"rejected":90,"remoteConvergenceMs":8.472,"retryCount":0,"roundTripHash":"3bf2bcb94fab05718ea34aed383efbf9e411b10d95a6179ae911f90d6a57cb1a","seed":19,"traceSha256":"0d92405289eea8b6bef336eab7f6bd29373c9dfcab013fe1126707d0fc4cb0a6","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[50.719,47.414,46.352,50.768,48.879,49.857,51.086,51.858,70.589,65.424,60.415,60.606,60.169,56.609,69.849,59.173,71.19,75.431,70.461,49.025],"canonicalVerifierHash":"c8bdffb3ea12e788eb2a5088294f17e8705320e3c734ca0302d660b841565e56","duplicateNoopHash":"661be1cf36a3e6dfd57dfc2f78cfaaa70d9b9adf2badc7d67b4c20ac4a528c24","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":72.088,"rejected":90,"remoteConvergenceMs":9.796,"retryCount":0,"roundTripHash":"268efccad0ace19c28dc92d9c54b320c38ac129eae3a5b5f6710d15562fa7f3a","seed":20,"traceSha256":"810f5993000813418216c7c55f6a64a15516e1b6f63e77e55dc9d0beb326a59c","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.731,47.17,47.225,50.083,50.141,51.479,54.03,53.397,52.526,53.222,52.121,55.13,57.875,56.434,69.831,56.339,74.833,77.678,49.028,49.065],"canonicalVerifierHash":"c26d7da5ac7f287f6edff0b7633a49ceba13692f2421bf0738cd6bb044c5428e","duplicateNoopHash":"e1ca213dd003e35e7e8cb0e94b3cc3c65a6284240156492d42496a39c0216155","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":96.348,"rejected":90,"remoteConvergenceMs":8.193,"retryCount":0,"roundTripHash":"4d284bde91877dd19f45ec5357c0bbc3213b125ea5a2ab460aac161c834e56d1","seed":21,"traceSha256":"00514f57b0533bafeeb8cef85306163b05977fd156fb8d4f6e8971f43cd5c3d9","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.182,47.375,48.427,52.003,50.438,50.932,52.804,53.681,52.292,52.469,54.223,54.546,55.766,56.205,62.701,56.999,73.536,73.151,51.871,48.887],"canonicalVerifierHash":"7dc98e0042f910e161dc48692e61b71bee6f5f3c4e2c185947bcd346651c62c1","duplicateNoopHash":"0c84e6481a015a570d0181acae33fe459537c904b339fab12049bfcb9f98024a","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":10.958,"rejected":90,"remoteConvergenceMs":10.08,"retryCount":0,"roundTripHash":"8af6d9079e0d0da10a62772b0ec081dc064e6480c6545292713e51b5ee9afeda","seed":22,"traceSha256":"3b219f2a009454e6a7e145c270ea4540712ac42a2e64a3de49decb95d00cb6ec","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[52.834,47.695,49.879,49.493,52.11,50.887,51.601,51.303,52.565,51.975,54.821,54.489,55.559,56.603,64.082,57.817,72.343,74.53,50.789,49.613],"canonicalVerifierHash":"1f42cdfc51ff0c4a3d156d71d7af4941eda5ef046969f200459a8dfcf0df4594","duplicateNoopHash":"4a4946b0e8eb9183e6b90e464571b002e3053e95ed6510dcb24e619868058604","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":44.202,"rejected":90,"remoteConvergenceMs":9.701,"retryCount":0,"roundTripHash":"d9eb0ae47ebe116de7ffe955c9782bbc6cf6f1bacb1ff2cb44ad3528939d70d6","seed":23,"traceSha256":"a44400c0327fe034c8e1d489d65d4a1e8444a312806565991f9f90c2e7bd2427","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[52.601,48.16,47.902,50.116,47.746,50.23,51.256,52.687,51.833,52.51,54.943,56.818,59.349,56.803,68.451,59.044,72.374,73.593,51.188,49.567],"canonicalVerifierHash":"77478c60c3ae9cd45fc8dcda6a71cd0010be62120387e64242c60b1eb7c87a4e","duplicateNoopHash":"089b4218683a43b11f6a2fb52d4b660cd1db966fe14a98a652aba04a95c23a78","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":72.942,"rejected":90,"remoteConvergenceMs":8.624,"retryCount":0,"roundTripHash":"0001e26052bdd0bab5724856e8de207155cb84a9d448d7aef7c173ba4e4a96e3","seed":24,"traceSha256":"dbd479d2d15a8febd16d5f86b91f7273d4c05d57076b180f1daa3d318d89b39a","unresolved":0},{"acknowledged":910,"adapter":"convex_flutter","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[52.819,46.338,48.011,49.366,55.584,49.786,50.477,53.395,52.454,53.099,61.065,58.814,70.586,67.029,68.546,62.33,79.742,76.385,74.469,51.416],"duplicateNoopHash":"610676792608b1dd1f2e06f6dc22864d82873dd8f8cbe86ef906a8174a708d0c","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"},{"afterOperation":500,"fault":"process_restart"}],"faultScheduleSha256":"d680df6635276d5593cea5cf988ac54915d8810167aae2037195424564f25e44","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":true,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"operationCount":1000,"rejected":90,"retryCount":0,"seed":25,"traceSha256":"22a209a7f9723602fadb0c3baab2d73b8a7cef893297ade4cd87a2d61122cd5b","unresolved":0,"reconnectToLiveMs":30.004,"canonicalVerifierHash":"0b7840fc3e7157c541d7862e398acaa57ac57628f918e516746483093abf6745","roundTripHash":"44ae786ea3c9385ee3d3d5321fd63b6222cca1ea07240a307559cdbbbdbecc19","remoteConvergenceMs":39.145,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":26,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"9c530a068ba1382fbe34f6b3bd558672f8f4538963b8ad673e4be834103e93bd","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[53.745,53.494,51.657,57.322,54.544,51.922,50.729,53.488,53.581,54.145,57.771,55.506,57.69,58.869,62.922,57.807,75.719,74.213,51.179,48.136],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"119ebe1f480b6c9f5df8f95cab240265bd09233c3f4b5dcd923728ef667b7a16","reconnectToLiveMs":29.068,"canonicalVerifierHash":"b8fc7d69a834b62d4a614de02cebbde60f3a9ccb93b4bec1f27e9987beced0ce","roundTripHash":"748fe55957a8fe14588430c1b62e80c9bf9bb046656f3dcd48d1691f1b33b54b","remoteConvergenceMs":9.675,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":27,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"0135b2eafc700e90fcbcd47b19e5a1d34ab5e99f255b4b315e2161c2896ef910","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[53.684,46.976,48.29,49.928,48.676,51.475,51.646,53.141,52.541,54.143,54.734,56.506,58.438,58.31,67.949,58.425,73.844,75.157,50.36,49.724],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4ad42017a48dbc8e12d942d945792c060603e08d9b7d18872b2158673ad23d7f","reconnectToLiveMs":71.22,"canonicalVerifierHash":"16a2f4cfba0166cf6435be3b64f11efbcaa06678f991140dbacc8d4e32097661","roundTripHash":"3c1ab0d073cd7610b4cd4a7c6521c0b5e47e4534104feda430f35e148f39f367","remoteConvergenceMs":8.298,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":28,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"03fd5bbcfd5789fb329015b4ca3015f10c5322f782f41d1a33c8739dcc250103","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[54.319,47.185,48.258,50.007,48.943,50.39,52.32,52.582,54.774,53.948,54.813,57.461,59.425,58.997,74.438,57.444,70.804,72.804,52.724,60.161],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"2d62e883551de7eef0494e8e419eb251320287cfd85221fa771a2ec7467bb810","reconnectToLiveMs":81.342,"canonicalVerifierHash":"b990e106461c9e852b53e3be3e6c612b61f79bacc9e5c999f8ef1de308a108a0","roundTripHash":"8231af29d3d0c7601470ec0da754f2f0fded387c6bbda084509076505efb0e9b","remoteConvergenceMs":8.488,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":29,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"8c898c274aa218bfce9e6a4f758d9273e1401b0edeab0db3d648e233aef3c687","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[55.291,53.452,48.97,50.992,47.942,51.41,51.042,61.883,51.797,52.185,54.773,55.174,57.773,56.822,62.322,58.36,73.321,73.703,49.152,49.29],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"862b67674912aa8d134be72444f4cdb9a5b50c33276e2a90ef4ae87f302351a8","reconnectToLiveMs":7.272,"canonicalVerifierHash":"abd743722679bd1335b1944cd1a5e315a89097e014d9b43d57957012d766e41a","roundTripHash":"6954dfaff2a7993d6a4d77208f9bb004083a5722931341643974ed81212fa7e8","remoteConvergenceMs":8.062,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":30,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"222c0dc904bf299d49547394de05a001ecc68710bc02a1b311b4beafb0beb019","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[51.422,47.152,48.34,53.457,51.051,50.34,50.417,53.342,52.676,55.709,54.993,54.686,57.345,55.661,63.98,59.644,91.792,73.566,54.22,50.021],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d0cd4491b844b50e85c3cbec56f3095662a981c0b8b695d7c85637b0e269aad1","reconnectToLiveMs":23.102,"canonicalVerifierHash":"1894c1445629a6547704f4dfd8bb17797b825eb585c520264c5c08dbc6366d77","roundTripHash":"bb887ac1fe5cae5ba0f2625c2af2b79493cb3fd885fdd8e1560a3aebe032b712","remoteConvergenceMs":8.524,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":31,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"c3760666249cd5b3679cb726fd60c15bde3e2a9bd079a7800fa297f824b7d339","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[51.915,49.157,49.165,52.792,54.015,53.038,52.171,54.662,51.894,51.815,54.904,53.953,57.294,55.846,62.601,60.022,73.284,73.928,53.398,48.064],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"206eaacdceff3df5532e76f3e5adc4964146e8504a64cbfddb43fabf4fce28ef","reconnectToLiveMs":9.788,"canonicalVerifierHash":"47d58b681022fd8ecc3b63238639a194262bef74422dd577da50fc761551d216","roundTripHash":"1ef7b56e658c8f1715f96246a583228c7b022d4e53b86374934a608bda3567b1","remoteConvergenceMs":7.995,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":32,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"f8a1efc44c616e628d61be595d4b092755c5da9e4347176fbf48421b326056c1","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[53.783,47.955,48.072,50.274,50.497,51.756,51.801,53.193,53.228,52.65,53.67,54.815,59.941,55.873,66.93,61.075,75.172,73.193,51.543,49.645],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"9f583fc4b098f02824bb5fa50c8ed9a76709c8850a94de973e02fe98003010ee","reconnectToLiveMs":57.18,"canonicalVerifierHash":"ef6d53a40b328a9729ca3c0157aa0adbe1c7cb390ab38e0c6949e03aa9a45147","roundTripHash":"b5389d9f11296e131badd2a6f0a299b125227ebc6b542936d07d8d3fe2b0c7d0","remoteConvergenceMs":8.252,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":33,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"53b09101de072e7f39e1f605b4e64c528f99951f9ec10abc33901a08def7d6ad","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[55.553,49.339,49.28,52.439,51.929,50.983,51.145,52.802,51.614,54.961,54.944,55.264,56.502,63.448,73.684,57.573,73.054,74.641,52.254,49.91],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"0c9c2a625372e9dea51750e620c118b2702bab4e7484530ad1bd6872097db785","reconnectToLiveMs":102.382,"canonicalVerifierHash":"495cf6e87b63acba4ec386a2f17165d796435465d45f391c4cf212781f7eca5b","roundTripHash":"7a43f69fdf62c9f83ba733e4b8a6488902860126dc015c5037cd8ea03da60bde","remoteConvergenceMs":8.348,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":34,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"41039af22db14b082c0f8d27d0f337109516f3c0ff5395bbdbd52c3f2a73e366","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[52.417,46.785,47.867,49.984,49.108,50.718,52.081,53.852,53.585,55.537,55.205,57.244,55.598,59.088,72.131,57.788,74.259,76.373,237.73,54.525],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"1a24ce7e3c28f5807015976bbfa3e712f6def2a8d3b8f8ed87dfb6d2903f154b","reconnectToLiveMs":87.245,"canonicalVerifierHash":"cf93ed2b3ec0f97763ae4ab9f60906018563a26a37da1abcd4ea47f5a3b27149","roundTripHash":"1fbeea46ef7e1780b7fe8db03d2cc35b54a8e469a4fd1047a159498bfb643161","remoteConvergenceMs":8.364,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":35,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"b9275a1393e700c6b85ebf43295ee9fbf7e0215d3e64d41b229fd4d0d1d16252","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[48.947,49.034,49.876,57.628,56.987,53.885,50.78,52.088,53.213,55.365,54.32,54.576,57.691,59.187,64.926,61.792,70.686,74.156,49.07,49.296],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"519d837de5b7f860793ea33c7269be33f2d3286b79c6e9e9507418df34faf035","reconnectToLiveMs":19.045,"canonicalVerifierHash":"887321746985c1c8bd0822300fe6373a4ef21380ef5ecc9c335f47e8cf9a8271","roundTripHash":"b9cea9c0d0df6e43effaaff688e7e8b7dcea72d7f88d91df5d70c9dcd51b3ece","remoteConvergenceMs":8.516,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":36,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"c0c7e0207a6bcd0957b602550570e42db42800fe57c66626010f25242d5f8208","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[51.72,47.037,49.46,51.551,49.767,50.85,52.254,52.484,51.625,52.109,53.737,54.589,56.407,57.635,71.458,58.872,70.89,73.648,49.386,48.391],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"44641ee26407719b887dc44016ae6afe719ff28a61bf2ac2adff13418a002f2d","reconnectToLiveMs":83.212,"canonicalVerifierHash":"b25494e6ef17124b508fe8d47b13d055664336898cf2192f495b01b2037d7c0e","roundTripHash":"d14c83263732d3d4ce5c1ddffa521f4a8e139a0115a0b13204c75c5d5ded723f","remoteConvergenceMs":8.517,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":37,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"78c97698051da2fb983461e5c8f6d2d3bd357afb10e6d42cf9918b98c8804b5a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[52.038,50.216,49.314,51.184,49.308,50.59,52.238,52.357,59.577,56.476,53.832,55.869,56.96,56.562,61.491,57.635,72.316,74.246,51.115,47.622],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"36b5339d6a5239af0c2e64c5a19e64b084ad9ce6a27851969945465a67dbb365","reconnectToLiveMs":19.025,"canonicalVerifierHash":"c9bd2baab0265dc7a190e0cd455f13e2575f7a04ebe2a01afecfb5d78ea1cc65","roundTripHash":"6ddbcafed928297a094e02cca9d9201fd12c225dd55b7df8a267aadd5e671715","remoteConvergenceMs":8.072,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":38,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"3f2dafd0bc96122b9fd4274b15b67a6834f00aefcce0e9fd414dc56bee7ee67b","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[53.128,47.358,57.054,52.929,49.732,54.572,52.616,73.347,88.966,90.432,106.124,89.543,94.186,85.756,89.516,88.9,111.277,77.107,159.116,58.65],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4df6eb45193bcf8a0edcab57b140e5e711a42313537c1b06ad7ac40495a6ed7d","reconnectToLiveMs":40.11,"canonicalVerifierHash":"dac23c9b615ba736f667822e98cd81583d5596d3eb635400fd7923a4b821f2fd","roundTripHash":"5f2b2d6429deabacf2274aa4cd918f601a4d7979d4e6425eac0dabff353df84e","remoteConvergenceMs":8.24,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":39,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"7b901f84dd62107f244be49a92208d42d8c9d3da353225163c2eae58533cc5e7","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[75.676,179.593,67.112,54.792,53.356,50.804,54.436,53.396,51.603,53.341,54.106,56.779,56.164,56.872,70.128,60.498,71.721,73.438,50.697,49.613],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"3a0e6a8d108e4259c6dacb8b38f8b350945dc7fa4e9364e3913d2caddffe18bd","reconnectToLiveMs":79.274,"canonicalVerifierHash":"1be630ac18ad34887058f68f246be9e0f8a79ae0b431e0342f77f0aafbbf60a6","roundTripHash":"010a6713bd4cb32e2f00459f2d042fbfe23920beb3dba219f9a975fd66606098","remoteConvergenceMs":7.995,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":40,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"cd40e9a07822696a4198119192a3961886f52674873dbed1fab915a6e50927d3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[58.658,47.189,49.207,49.781,50.882,50.83,52.179,51.635,53.33,52.854,59.303,55.228,58.04,58.163,69.229,60.215,74.177,76.348,53.142,53.245],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"a91db8c869cb35510ed31cccd264a1874ddc908ddfb04db8e65f8148f5a91578","reconnectToLiveMs":70.271,"canonicalVerifierHash":"25122c18d674ea9d81866fefb9811438d5d0e6930edbb7b4f1bd1336987d3c84","roundTripHash":"f83c2ea15ce0f99cd935fd0aaeffe3bac0805ab6b6f1d45c6ba5e715257fbe02","remoteConvergenceMs":8.389,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":41,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"3c6017b5b1b61473fc4384d5c642f8c4f21fc498a2cba3981a4fcb04e8e8f815","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[53.535,50.453,52.33,52.467,50.524,51.755,53.871,53.392,52.601,55.377,56.604,58.993,58.145,61.613,72.18,60.587,72.989,75.687,52.332,49.186],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"f46fbceca36e6df65f09e8184dc171220da87ffa997e93edae4425e494a5644d","reconnectToLiveMs":86.319,"canonicalVerifierHash":"44e6edd99da52fe8e35e1513170aea2419cc5cb214cf506ff75a706440274ae3","roundTripHash":"519c1ac9267bb4369abd0b2bbc015623f2f8c3a7c58946522c36df8a6028a425","remoteConvergenceMs":8.44,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":42,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"2a3bb8f9e96bbff75da1c0bdcd21b3452ff8c8762013e2f6eca2ca641b707758","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[52.401,47.301,49.769,50.808,49.004,52.588,52.107,53.5,53.271,52.893,56.103,56.561,57.277,58.481,66.595,58.908,71.236,73.785,52.777,48.676],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"86043e9b248940531840e7cd18115beb2b0dddcf5dc9db88ab096d6ece3d17a4","reconnectToLiveMs":81.184,"canonicalVerifierHash":"94b10f4001fe9e25842dfeaa8246258d85efe73aa823a1f32a587317b43cc262","roundTripHash":"99affcac47d7e010b61d60b84890b99227a88f96e686afecc64ecc96e1a9e434","remoteConvergenceMs":8.461,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":43,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"3f8a10342efb6e53884d12c1e1425f2ec7ecdd96013bd9223c4475968eca8e3e","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[54.306,47.901,50.021,49.896,48.356,50.601,52.433,56.274,52.351,61.789,56.201,55.147,56.334,57.711,62.322,59.071,70.857,73.701,52.981,47.958],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"19c0e27e7c3cdf297feaa3b02930adcda8b97b4c388b33ea1769e9afdf522ba2","reconnectToLiveMs":31.06,"canonicalVerifierHash":"3f91d6fc35170cf6b81469cd10fdc2276151f41642c183954c905dc2fd72e913","roundTripHash":"2d5a3bcc5c59f2b66c302ee900ea418589d560867b9df6c4ad8433a1d8132683","remoteConvergenceMs":8.491,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":44,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"0eb474ca0c5f15b2c3bb2837e8ccb7540cc953610e19a90021b5d7fcc17d7e92","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[53.634,48.41,48.882,50.057,49.581,52.317,51.69,53.295,53.961,54.369,55.632,55.421,55.526,56.388,71.229,59.573,71.844,74.882,51.527,49.147],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d296acd1d505cc41e1746ba8b4f51adf18849dbc0a28408dd5e3bf8e616d4672","reconnectToLiveMs":85.215,"canonicalVerifierHash":"45675064eb12cf1f1a91352bc3ec835026de943504172dfe66afe47547538963","roundTripHash":"db68375a5b6b0d90d87a15cb7be4f53702d3686f539bd3c6e4e91fe56aecf1f5","remoteConvergenceMs":10.166,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":45,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"48fb865b80110a4ec663a65f26f9bffa285ecb926bc3654aaac732c60d7b3b0a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[58.571,54.422,54.35,57.046,56.453,57.909,55.084,57.663,54.816,60.273,55.275,58.253,61.867,57.962,70.439,61.782,72.682,75.279,67.517,50.186],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"129dec31e4e2f2823c59ae9b87fcae42d394591c61f5be778999c45da2a3e444","reconnectToLiveMs":71.228,"canonicalVerifierHash":"df97c65603a19f4c4351462abc77ddac04994d60de19108557822d5b7a571794","roundTripHash":"bd72d07f8cfa10ba473e751e8571d18cff71139e9de28b389a357005b0549039","remoteConvergenceMs":8.144,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":46,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"2547f5ef76c4962e0f352c8e03a471c880bc8ca5b6172b583e469813db94f886","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[57.297,49.602,48.728,51.819,52.78,51.647,52.095,53.299,52.682,56.697,60.222,59.316,57.049,58.813,63.12,59.483,72.977,74.498,51.085,49.826],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"50395ad41530e80d5559fc8b6af5507a8a393adec21dbaf5ef71718f035be3ef","reconnectToLiveMs":25.008,"canonicalVerifierHash":"9b4ee333fd4009d9b1edef10a8a06c285dae7b061b309176dac94fb8296f1af3","roundTripHash":"0574858639d852e4499b0d575c1ed16d92e54431275cbba318a6c50a5938a9f4","remoteConvergenceMs":8.292,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":47,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"74fe76755a9c32233697d6c69e3751a5a0429d237d306cfa1e17d9b51b4a8569","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[52.756,47.199,48.98,49.799,49.346,49.938,51.865,51.959,53.758,53.848,55.803,55.476,57.419,58.829,66.43,60.141,71.485,73.361,51.482,48.65],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e3d92a8e2ae00349f3b3869363ede60248488f17094c62af9e2b17952177dc57","reconnectToLiveMs":48.063,"canonicalVerifierHash":"3c4caaedf4de1dfe50de5ca3aedad7fd6de3694157191abd14d816ba949321c5","roundTripHash":"f055ebed68cf1f028f3ac92152eb9804730bd979cbe5771187ea7d8a93f1b604","remoteConvergenceMs":7.988,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":48,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"34d1c2996c4711c53b654ccb9172bdd508ff7023bd0729c37305aff833a9e8c3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[53.181,49.251,50.476,51.0,49.904,51.704,57.809,52.343,53.128,53.65,53.441,54.9,58.393,57.382,72.549,64.743,84.652,75.932,54.646,48.87],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e7e8f860ed711b1ba4c898d3c6a3a7443cdcb343f3b965d64050f635bebb045a","reconnectToLiveMs":91.238,"canonicalVerifierHash":"882b979772b174022efc16c448d930c4c985077f663f74e281e64d2730ca2d9e","roundTripHash":"d800ff5ea7d263d57c07e1a252a14b4b8787f97831d2de8d8608dfa3a4187f23","remoteConvergenceMs":8.934,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":49,"adapter":"convex_flutter","operationCount":1000,"traceSha256":"cd10c09eb29b4c21d63fff22d70e92b231eb178d82ab60ea4f2dd0b671e6f0c5","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[52.343,46.916,51.398,51.697,49.817,49.45,51.962,52.53,54.376,54.323,55.072,54.702,56.073,56.094,73.688,58.779,71.976,73.063,52.839,49.83],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"8d2697434c6eed85928903a55f2524696d540ea4d20637d254aaa9ff692314fc","reconnectToLiveMs":100.349,"canonicalVerifierHash":"897c1e45d48565825398a418ad07d9557552005e11b69982d8979da0e6def54e","roundTripHash":"a93b4e6243f97268681c90698b8754de9e2cf2decf12fb6104cb376aa9ace707","remoteConvergenceMs":7.988,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10}]} \ No newline at end of file diff --git a/tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json b/tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json index 353fff33..ec4b095f 100644 --- a/tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json +++ b/tool/convex_client_gauntlet/runtime/results/dartvex_correctness.json @@ -1 +1 @@ -{"schemaVersion":1,"status":"passed","adapter":"dartvex","candidateVersion":"0.2.0","flutterRustBridgeVersion":null,"deployment":"local:127.0.0.1:3210","gitCommit":"bf421ffef06b5d04749c77bda182f8f0a53796fe","baseFixture":{"path":"test/fixtures/strategy_integrity/base-test-v43.ica","sha256":"8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a"},"seedCount":50,"operationsPerSeed":1000,"totalOperations":50000,"allCanonicalEqual":true,"allResolved":true,"processRestartCheckpoint":true,"bytesSent":20224618,"bytesReceived":48117804,"wallClockMs":64794.12699999999,"maxRssBytes":260472832,"machine":{"operatingSystem":"macos","operatingSystemVersion":"Version 26.5.1 (Build 25F80)","processors":8,"dartVersion":"3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\""},"seeds":[{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":true,"exercised":true,"queuedBatchReplayedExactlyOnce":true,"refreshSessionCalled":true,"rejectedTokenObserved":true,"tokenChanged":true},"batchLatencyMs":[44.354,38.401,37.205,40.209,36.482,37.82,38.181,37.493,37.097,38.086,62.452,39.203,40.194,39.93,57.626,40.653,51.89,52.341,36.289,32.466],"canonicalVerifierHash":"26d71e8df48fba1f7aae2c8cf4b5569e8b9360166b9bb6ed887ce7f14048f2f7","duplicateNoopHash":"2f5c83b805fd22471e866932f0ad3a6b438850368d79cbaada4e91a2306d36f8","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":10,"fault":"auth_reject_refresh"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"b4e244c38b89789f1191e988768aa681f4722184533077ef58eb6f95ee6a0e52","faults":{"authRefresh":true,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":139.712,"rejected":90,"remoteConvergenceMs":9.366,"retryCount":0,"roundTripHash":"57f03a40845f1cbcec927c4a2abc68781fcec79e10d5f8a896a7deb95bbd9d69","seed":0,"traceSha256":"ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.663,38.242,36.696,37.857,35.665,37.689,38.565,42.735,39.346,39.484,38.731,39.898,41.192,40.341,46.73,43.464,50.185,51.915,35.415,32.371],"canonicalVerifierHash":"521b140c0a612b8ea44a1d9b4b03cbf0573799ae00bf05498122bacab81c1bba","duplicateNoopHash":"f5cf17c272add3c1c7d3460a3f29c4ee4bf786dd0d24d00d5010fd3069450206","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":62.2,"rejected":90,"remoteConvergenceMs":8.589,"retryCount":0,"roundTripHash":"42034206a0ce276524b5f6c4b61270d036eafa3ea888f6af32fcc8898136425d","seed":1,"traceSha256":"74e1c135f93f0e9ebf7029a5222aaa38b6aa0f834e31861e9e3de8515922bca4","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[46.78,39.744,40.588,39.327,37.441,39.078,40.443,43.848,39.236,39.79,40.009,41.432,41.909,41.245,56.622,42.175,52.564,52.323,37.34,33.256],"canonicalVerifierHash":"9ca1893e49091976796548e8568b4e1791bff1f72f5f883892ef688a59483304","duplicateNoopHash":"f241cf2843f3adfa94f6566c00df83a1f234b4993b73daa1fe08ff8b34e89260","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":131.958,"rejected":90,"remoteConvergenceMs":8.097,"retryCount":0,"roundTripHash":"44c8e21bbcb1c01d01bd1446150e3c5e3daa0f256aced3da4e96ac1afeb6be93","seed":2,"traceSha256":"2b3e81b55c925a72176cd6f3d1515c7063d7706d8d4381c641b6fc0a8e121262","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.01,36.278,38.307,42.036,39.143,38.957,38.917,46.649,41.052,40.687,39.862,41.003,40.476,41.964,59.487,42.546,50.984,52.864,35.916,33.058],"canonicalVerifierHash":"197d1fc1c62be082b93dd2c51021a15c68e3f7551de5e0d949b11152f6ab6840","duplicateNoopHash":"1e34802380fb4c7b66dfe194d29cf6bb4a223e342c5ac58fb77bd10b9436b658","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":132.808,"rejected":90,"remoteConvergenceMs":7.582,"retryCount":0,"roundTripHash":"18bb3a755432638399f32b7fb39627dab2c7c474726dc01b433243d45e86ab8f","seed":3,"traceSha256":"f7105a396da40cd883ec4cc9e83f1a6cb53706e90e782cc3e71cb778bf252235","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[46.39,38.566,38.379,38.996,37.782,39.989,39.498,41.242,39.809,40.615,40.836,42.414,40.95,42.418,55.016,41.775,51.653,50.414,35.109,32.459],"canonicalVerifierHash":"472d3b636388ed4b3ce98242c337cdca98191dd93feeaf23b2f0bf72c2068232","duplicateNoopHash":"94cb107907f89f5fc3a85d810566e5f67ec06498ef9345e4236bce4c79443170","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":103.183,"rejected":90,"remoteConvergenceMs":7.469,"retryCount":0,"roundTripHash":"6bc869c98139d476a3b61de2ca80637ca4d956c2e99008462e6667110516d5e8","seed":4,"traceSha256":"f8cc2d86c82f00b22442e2b457b962f2e31d958be26bd26999c5035502d44d52","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.467,38.629,38.282,42.15,40.325,40.74,40.806,41.27,39.767,39.994,41.586,41.433,41.491,41.985,57.23,45.778,53.56,51.936,35.118,34.226],"canonicalVerifierHash":"60e119554dd2c84670ff85ad1f4743fe3b39bb2367a81ecfa4f978cd7e834be1","duplicateNoopHash":"69a9e28c6ba4259807f9135260188fb48763db7cd60da9f0bcbb2df689ac7fec","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":144.416,"rejected":90,"remoteConvergenceMs":7.609,"retryCount":0,"roundTripHash":"3e1eb93aba491b833fe657b72a49cb08871e51b6abd93ced58a16b4c1bb1d714","seed":5,"traceSha256":"d0e377de6c9acd5bde977020ce6b5a3b79f41ba792748eeb87617f0bbfe67073","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.108,37.719,39.147,40.512,38.794,48.313,42.081,43.037,40.641,40.491,40.295,41.537,43.677,41.3,57.906,73.687,51.8,52.947,35.527,35.012],"canonicalVerifierHash":"870bdc3fe241a36e4cafa9cd2db6550900db537148df0012129a4fc729168e44","duplicateNoopHash":"7b6d6f868d69e5c19a34b1ef7a5da5bd85c7b2c0de6b278023971d5125376186","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":118.544,"rejected":90,"remoteConvergenceMs":11.598,"retryCount":0,"roundTripHash":"a6d78c0173ff37ca01ce546bd371b25102dff424fc71e5ed158da8cc3afb9409","seed":6,"traceSha256":"4089e29162d4a1f4eeb277698a40e77dee654c5e6b024de882412828b2b868be","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.201,37.516,39.713,41.717,39.319,41.041,39.905,40.309,40.31,42.247,42.587,40.457,43.919,46.201,50.613,42.495,51.66,52.034,37.017,35.448],"canonicalVerifierHash":"07d665cbffdf875c025bb158a561350943ae3fbd5068762fd11819a6a4901942","duplicateNoopHash":"abc350378349c0c5ca8ac8718147350b24bba91d01f1b3584ac0ae8728392bbb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":80.161,"rejected":90,"remoteConvergenceMs":8.071,"retryCount":0,"roundTripHash":"e9e701572c478eb4ff07bdc25b9686439e87f671b6912b55edab9f6640bd9348","seed":7,"traceSha256":"2f479b6ed6743dedae4a36d95c58c95f225ba529fcd863e5640f49afc5e838b7","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.129,38.593,39.76,40.965,40.946,40.978,39.996,41.468,39.831,41.739,41.369,42.141,43.355,42.627,55.03,43.105,51.501,52.265,37.497,34.409],"canonicalVerifierHash":"efc2b0c123c804ed7f1847b3cfaec274bad1ea5ae14b606f04cc0a565458437f","duplicateNoopHash":"7c4a10f6a44922af7b90a06f9e3796d4915b37934f1958e659c352ef988032a7","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":103.273,"rejected":90,"remoteConvergenceMs":7.705,"retryCount":0,"roundTripHash":"2381d28c5762232a05a96531e03aabc0b196671f5f4c493cfb8a2bc1e16fed87","seed":8,"traceSha256":"18d04c6a59b74a9026ab9d6f69161a1ae3101de4fe16acc08b831a885a6a301f","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.834,39.174,38.795,40.44,39.271,41.194,40.658,43.601,41.343,41.611,41.924,42.668,42.519,42.995,61.442,43.682,53.926,52.233,36.722,35.201],"canonicalVerifierHash":"703442e3b93e7bc929da059e86ab44de647f270cefc120656f501966b6847100","duplicateNoopHash":"acb46046274da0c9262f4910b29e4fda0a2bfe3132374a35db02c9f2f89398f4","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":141.644,"rejected":90,"remoteConvergenceMs":7.618,"retryCount":0,"roundTripHash":"e63096e423e4fa5de04d55dd992a191a939e288f22077b5dba9f1f30d0312ac0","seed":9,"traceSha256":"10e7fe955f927f3626b5586eff07c5c452e7dd8fada065f6ea12839907ebfb8f","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.733,38.922,39.839,40.172,39.838,41.083,42.155,41.537,41.172,42.334,41.8,41.591,43.365,41.44,54.462,43.499,52.934,51.609,35.955,35.403],"canonicalVerifierHash":"0dedd28fa52610998313b57e5e621be6488408b8475b8082434152a7e4bfffde","duplicateNoopHash":"68de0d932dda40e9fa94ad87bce5062194f4b64d032743224b09543a35fadb04","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":100.705,"rejected":90,"remoteConvergenceMs":7.342,"retryCount":0,"roundTripHash":"e199bf3ee7ce8299f481c487ecb563b9bc5a22508107847ee32280f5af2799fa","seed":10,"traceSha256":"3aedbcc55f63d2e40d1053713da07189c675eeedafc45557893bfb44a4a4a3fa","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.498,38.556,39.795,41.577,39.977,41.042,41.511,43.853,41.041,42.895,46.261,44.983,44.574,47.707,60.593,45.665,87.455,52.551,38.349,33.884],"canonicalVerifierHash":"976df9bc169579795add78059b22aecb15add1a77fa132a762b81256d7a14ab9","duplicateNoopHash":"ecd01e95d0ba98e4c1033dd1c11ae805f94ea9c57caa7eeb3883adbdba18baa9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":113.561,"rejected":90,"remoteConvergenceMs":7.656,"retryCount":0,"roundTripHash":"6b2a4c8cae985614914a1122c691c7976d2f5fe6bc30dca6a2a54e7a718ebbdb","seed":11,"traceSha256":"d64e19b329a28294c7e145d5eea4d659b86b00fdc478f7472e57f1b93de378ef","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.739,40.542,40.551,43.729,39.819,40.788,40.066,43.627,42.671,41.092,40.639,41.983,42.974,44.624,62.852,45.833,53.606,53.153,37.775,39.038],"canonicalVerifierHash":"fe27618043429656abed9f1810fd3f7fe21fa7844cfe474e4ee27170aa6ba761","duplicateNoopHash":"75cb591df89ce4872e5d71150982ccb110460036da1a031d13af69b1a497bf44","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":133.633,"rejected":90,"remoteConvergenceMs":7.404,"retryCount":0,"roundTripHash":"08db87f541a6ad80969ae614cb3c192ee4668e76d650a3c3c83ae2205d7cae2f","seed":12,"traceSha256":"56270e40851c311cafe2638b7bdf9e162ca4190285215f3a788c34ab68e9fbfa","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.918,41.058,40.192,41.048,40.576,42.727,42.287,43.909,41.666,41.569,43.005,43.27,42.773,44.089,60.701,48.77,52.802,54.466,37.907,38.372],"canonicalVerifierHash":"1eb6c99489819977b8c62d4ac7cc2b6332253eea89c0028a0d00f543ccc80b4e","duplicateNoopHash":"a2780357577e00a64a3d6882842034f3c7a33a7fb73f76a1bb513582adc82f48","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":131.567,"rejected":90,"remoteConvergenceMs":7.536,"retryCount":0,"roundTripHash":"30a8267dc6ce65b39cf9e2f8e9d47c425c22d34ae75df7a6427dcfbf320b49ca","seed":13,"traceSha256":"04fbda33461bbb975b9e66fc1ff0fe176931fdbd9c56fe8d3b9f9abcc939ab07","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.633,38.352,39.482,41.682,39.987,43.459,40.504,43.707,40.551,45.121,41.02,45.276,42.897,42.803,57.538,43.677,52.32,53.701,40.441,35.055],"canonicalVerifierHash":"9f4f93499745ce94e2ea84804d41cdd24007491f9ad333f6994c23adaca1a97f","duplicateNoopHash":"6a79d43b71c80e3ece66ab93693e6111df9ac8633c5e8af7dfbcdc86f73f16e9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":109.43,"rejected":90,"remoteConvergenceMs":7.638,"retryCount":0,"roundTripHash":"4406199eb581980a79d880cbb483fd97b1ac2b9ecf927ac3d48a1ede8f3a8ca6","seed":14,"traceSha256":"d105bcc5f7eaa8a768bd666c3d6fb072fb97a0191a935d70e688d086e52f95c4","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[45.873,39.254,39.191,40.215,48.79,43.217,40.847,40.912,46.0,42.302,44.732,41.839,49.531,47.352,50.244,43.74,54.839,53.399,36.136,35.036],"canonicalVerifierHash":"e44921495d13f7e764fb5170d992c6806add23c1ee2a902d2ed586e78b6518f2","duplicateNoopHash":"89f952d57ff5c744f32966f233f39fdf76ff3729bca2ffb22da379572bc772cb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":92.883,"rejected":90,"remoteConvergenceMs":8.378,"retryCount":0,"roundTripHash":"885dcea93a305a0527b65c09ab8a51e42eeab8486b0a17a812adfc8a23dd0692","seed":15,"traceSha256":"fd2509f037ad595831e5af5de358a045a4944c5070aeccceca9a85e5d93f0dcb","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[45.207,38.666,53.941,48.395,40.191,48.514,57.567,52.241,43.069,42.325,43.529,42.198,44.42,43.076,50.802,45.931,53.972,53.558,36.502,35.27],"canonicalVerifierHash":"fcab45e94cbb1dd32f40022c4f2538d8a8921f6f9a993c44da5a1af856273521","duplicateNoopHash":"1a2c13daad7b021d5bf7aba90696fb2fba2a5479ff1fa49027c88e3b5e9bf237","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":60.994,"rejected":90,"remoteConvergenceMs":7.829,"retryCount":0,"roundTripHash":"23079740e8c81056d542e80219368de85f2c9ae8740e9db95182ac2d1433a5cf","seed":16,"traceSha256":"15974b2b6d314a57e29fe393296085940f83539b4485eb9ada849149db7a99cc","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.355,51.715,49.469,59.482,43.866,41.0,43.691,42.488,42.377,41.069,42.066,43.259,42.925,44.613,59.639,44.394,53.044,53.739,41.307,37.36],"canonicalVerifierHash":"0ae80e9274783e32170474a495f3c31bfa702e9acbe970cb1a04c439846eb86b","duplicateNoopHash":"834e79323bda37030a4c6af90b9a0dadade53242cd9801ba090d12a86054e645","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":120.392,"rejected":90,"remoteConvergenceMs":7.489,"retryCount":0,"roundTripHash":"5ebbea09bcb5c13323f4a6dea81b146de93f7fa134b025b2163ca48c62222abc","seed":17,"traceSha256":"53cce4ffa3236c18592a997edd684f10596e8cdceacd8402d858c284a7d9054d","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[49.467,44.71,43.636,45.982,41.814,48.413,45.053,44.539,42.78,42.877,42.961,43.139,43.006,43.679,54.975,45.695,56.242,56.375,41.806,36.42],"canonicalVerifierHash":"f3fd473e5c7029efed7f8b2b39933c0240a36cf83a5eb3286adfe377c9890da5","duplicateNoopHash":"2328c71bfb35d1fc60079b4c3572d96fac5e48f92fa4fee9baa2f9a469aa7404","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":106.131,"rejected":90,"remoteConvergenceMs":7.579,"retryCount":0,"roundTripHash":"9f8e4e09c1123b52138e32282eeb53f1d13c8317d8f3b2cda0cb11c576704704","seed":18,"traceSha256":"a542c21db854c18f4cdcb385e6ac298bf7af6282c0972913cc8fce70aec86847","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[47.146,42.487,40.341,42.014,39.654,41.161,42.36,42.155,41.277,42.494,42.556,43.597,44.991,45.765,60.337,50.106,61.64,59.293,47.294,43.482],"canonicalVerifierHash":"63e156ef82374f25fc17ac38cd7fb0ffa365fa4d6fcd29f42be9a52f729ab158","duplicateNoopHash":"1121a351ad1dd9adce4685ffd6e719a32e98c70594e4a09f928d6f996e9d1207","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":147.541,"rejected":90,"remoteConvergenceMs":8.211,"retryCount":0,"roundTripHash":"3bf2bcb94fab05718ea34aed383efbf9e411b10d95a6179ae911f90d6a57cb1a","seed":19,"traceSha256":"0d92405289eea8b6bef336eab7f6bd29373c9dfcab013fe1126707d0fc4cb0a6","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[51.563,45.187,41.145,48.844,39.872,40.214,40.556,45.067,40.767,41.892,41.076,43.101,41.946,44.049,56.381,44.582,53.611,53.034,35.534,36.15],"canonicalVerifierHash":"c8bdffb3ea12e788eb2a5088294f17e8705320e3c734ca0302d660b841565e56","duplicateNoopHash":"661be1cf36a3e6dfd57dfc2f78cfaaa70d9b9adf2badc7d67b4c20ac4a528c24","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":103.337,"rejected":90,"remoteConvergenceMs":7.468,"retryCount":0,"roundTripHash":"268efccad0ace19c28dc92d9c54b320c38ac129eae3a5b5f6710d15562fa7f3a","seed":20,"traceSha256":"810f5993000813418216c7c55f6a64a15516e1b6f63e77e55dc9d0beb326a59c","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[60.501,47.69,42.794,44.114,42.319,46.833,43.034,43.849,42.2,43.672,42.471,42.543,43.816,43.431,48.614,46.127,54.325,54.029,37.313,36.197],"canonicalVerifierHash":"c26d7da5ac7f287f6edff0b7633a49ceba13692f2421bf0738cd6bb044c5428e","duplicateNoopHash":"e1ca213dd003e35e7e8cb0e94b3cc3c65a6284240156492d42496a39c0216155","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":56.808,"rejected":90,"remoteConvergenceMs":8.042,"retryCount":0,"roundTripHash":"4d284bde91877dd19f45ec5357c0bbc3213b125ea5a2ab460aac161c834e56d1","seed":21,"traceSha256":"00514f57b0533bafeeb8cef85306163b05977fd156fb8d4f6e8971f43cd5c3d9","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.656,40.338,43.689,46.133,43.726,43.983,44.87,42.928,44.019,43.894,44.233,45.026,44.555,46.736,52.36,48.964,54.776,56.366,38.433,35.854],"canonicalVerifierHash":"7dc98e0042f910e161dc48692e61b71bee6f5f3c4e2c185947bcd346651c62c1","duplicateNoopHash":"0c84e6481a015a570d0181acae33fe459537c904b339fab12049bfcb9f98024a","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":82.871,"rejected":90,"remoteConvergenceMs":7.593,"retryCount":0,"roundTripHash":"8af6d9079e0d0da10a62772b0ec081dc064e6480c6545292713e51b5ee9afeda","seed":22,"traceSha256":"3b219f2a009454e6a7e145c270ea4540712ac42a2e64a3de49decb95d00cb6ec","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.593,41.554,41.368,43.211,40.044,44.42,43.599,44.835,42.662,41.886,43.865,44.796,44.135,44.727,61.328,44.845,53.198,53.122,38.052,36.161],"canonicalVerifierHash":"1f42cdfc51ff0c4a3d156d71d7af4941eda5ef046969f200459a8dfcf0df4594","duplicateNoopHash":"4a4946b0e8eb9183e6b90e464571b002e3053e95ed6510dcb24e619868058604","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":135.587,"rejected":90,"remoteConvergenceMs":8.017,"retryCount":0,"roundTripHash":"d9eb0ae47ebe116de7ffe955c9782bbc6cf6f1bacb1ff2cb44ad3528939d70d6","seed":23,"traceSha256":"a44400c0327fe034c8e1d489d65d4a1e8444a312806565991f9f90c2e7bd2427","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[62.128,39.028,40.206,40.704,40.245,42.697,41.196,41.735,41.923,42.753,43.719,44.043,42.968,44.062,47.083,44.204,53.676,54.59,38.845,36.959],"canonicalVerifierHash":"77478c60c3ae9cd45fc8dcda6a71cd0010be62120387e64242c60b1eb7c87a4e","duplicateNoopHash":"089b4218683a43b11f6a2fb52d4b660cd1db966fe14a98a652aba04a95c23a78","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":57.791,"rejected":90,"remoteConvergenceMs":7.542,"retryCount":0,"roundTripHash":"0001e26052bdd0bab5724856e8de207155cb84a9d448d7aef7c173ba4e4a96e3","seed":24,"traceSha256":"dbd479d2d15a8febd16d5f86b91f7273d4c05d57076b180f1daa3d318d89b39a","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"queuedBatchReplayedExactlyOnce":false,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.869,39.522,40.935,41.045,39.682,43.911,41.106,41.284,42.681,42.681,49.693,44.931,43.642,44.423,58.574,45.746,53.785,55.362,37.87,35.332],"duplicateNoopHash":"610676792608b1dd1f2e06f6dc22864d82873dd8f8cbe86ef906a8174a708d0c","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"},{"afterOperation":500,"fault":"process_restart"}],"faultScheduleSha256":"d680df6635276d5593cea5cf988ac54915d8810167aae2037195424564f25e44","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":true,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"operationCount":1000,"rejected":90,"retryCount":0,"seed":25,"traceSha256":"22a209a7f9723602fadb0c3baab2d73b8a7cef893297ade4cd87a2d61122cd5b","unresolved":0,"reconnectToLiveMs":119.822,"canonicalVerifierHash":"0b7840fc3e7157c541d7862e398acaa57ac57628f918e516746483093abf6745","roundTripHash":"44ae786ea3c9385ee3d3d5321fd63b6222cca1ea07240a307559cdbbbdbecc19","remoteConvergenceMs":14.345,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":26,"adapter":"dartvex","operationCount":1000,"traceSha256":"9c530a068ba1382fbe34f6b3bd558672f8f4538963b8ad673e4be834103e93bd","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.913,38.764,39.144,41.763,40.614,41.807,40.5,43.444,43.49,43.955,44.098,43.115,45.507,43.702,54.828,46.699,54.52,53.995,37.812,34.844],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"119ebe1f480b6c9f5df8f95cab240265bd09233c3f4b5dcd923728ef667b7a16","reconnectToLiveMs":93.406,"canonicalVerifierHash":"b8fc7d69a834b62d4a614de02cebbde60f3a9ccb93b4bec1f27e9987beced0ce","roundTripHash":"748fe55957a8fe14588430c1b62e80c9bf9bb046656f3dcd48d1691f1b33b54b","remoteConvergenceMs":8.616,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":27,"adapter":"dartvex","operationCount":1000,"traceSha256":"0135b2eafc700e90fcbcd47b19e5a1d34ab5e99f255b4b315e2161c2896ef910","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[42.93,40.698,38.802,40.734,39.554,42.733,41.464,41.965,43.322,43.074,44.661,46.889,51.343,44.828,62.997,45.716,52.834,54.405,39.715,34.962],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4ad42017a48dbc8e12d942d945792c060603e08d9b7d18872b2158673ad23d7f","reconnectToLiveMs":144.469,"canonicalVerifierHash":"16a2f4cfba0166cf6435be3b64f11efbcaa06678f991140dbacc8d4e32097661","roundTripHash":"3c1ab0d073cd7610b4cd4a7c6521c0b5e47e4534104feda430f35e148f39f367","remoteConvergenceMs":8.486,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":28,"adapter":"dartvex","operationCount":1000,"traceSha256":"03fd5bbcfd5789fb329015b4ca3015f10c5322f782f41d1a33c8739dcc250103","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.976,40.385,40.735,40.577,39.67,40.706,42.246,43.046,45.319,44.969,44.034,43.094,45.092,55.426,51.524,44.286,54.288,53.6,40.446,45.774],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"2d62e883551de7eef0494e8e419eb251320287cfd85221fa771a2ec7467bb810","reconnectToLiveMs":81.163,"canonicalVerifierHash":"b990e106461c9e852b53e3be3e6c612b61f79bacc9e5c999f8ef1de308a108a0","roundTripHash":"8231af29d3d0c7601470ec0da754f2f0fded387c6bbda084509076505efb0e9b","remoteConvergenceMs":7.87,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":29,"adapter":"dartvex","operationCount":1000,"traceSha256":"8c898c274aa218bfce9e6a4f758d9273e1401b0edeab0db3d648e233aef3c687","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[47.255,42.609,46.972,56.641,44.743,47.571,48.835,43.11,44.671,41.81,42.338,41.467,44.79,57.427,65.155,52.271,54.796,58.196,39.057,40.472],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"862b67674912aa8d134be72444f4cdb9a5b50c33276e2a90ef4ae87f302351a8","reconnectToLiveMs":123.74,"canonicalVerifierHash":"abd743722679bd1335b1944cd1a5e315a89097e014d9b43d57957012d766e41a","roundTripHash":"6954dfaff2a7993d6a4d77208f9bb004083a5722931341643974ed81212fa7e8","remoteConvergenceMs":7.814,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":30,"adapter":"dartvex","operationCount":1000,"traceSha256":"222c0dc904bf299d49547394de05a001ecc68710bc02a1b311b4beafb0beb019","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.316,39.746,41.795,39.349,41.622,40.924,42.019,43.588,42.566,43.549,42.501,43.848,42.961,43.86,52.855,45.985,53.6,55.099,38.256,34.999],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d0cd4491b844b50e85c3cbec56f3095662a981c0b8b695d7c85637b0e269aad1","reconnectToLiveMs":84.011,"canonicalVerifierHash":"1894c1445629a6547704f4dfd8bb17797b825eb585c520264c5c08dbc6366d77","roundTripHash":"bb887ac1fe5cae5ba0f2625c2af2b79493cb3fd885fdd8e1560a3aebe032b712","remoteConvergenceMs":8.221,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":31,"adapter":"dartvex","operationCount":1000,"traceSha256":"c3760666249cd5b3679cb726fd60c15bde3e2a9bd079a7800fa297f824b7d339","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.067,38.435,42.229,41.586,40.15,43.142,42.557,43.916,42.692,43.42,42.976,42.971,43.563,42.587,59.843,44.257,53.113,56.858,37.968,35.932],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"206eaacdceff3df5532e76f3e5adc4964146e8504a64cbfddb43fabf4fce28ef","reconnectToLiveMs":133.044,"canonicalVerifierHash":"47d58b681022fd8ecc3b63238639a194262bef74422dd577da50fc761551d216","roundTripHash":"1ef7b56e658c8f1715f96246a583228c7b022d4e53b86374934a608bda3567b1","remoteConvergenceMs":8.363,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":32,"adapter":"dartvex","operationCount":1000,"traceSha256":"f8a1efc44c616e628d61be595d4b092755c5da9e4347176fbf48421b326056c1","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.395,38.09,38.951,39.899,41.066,45.142,43.453,45.129,43.454,42.75,45.618,45.61,43.962,45.629,50.713,45.094,52.398,53.628,37.11,34.458],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"9f583fc4b098f02824bb5fa50c8ed9a76709c8850a94de973e02fe98003010ee","reconnectToLiveMs":72.014,"canonicalVerifierHash":"ef6d53a40b328a9729ca3c0157aa0adbe1c7cb390ab38e0c6949e03aa9a45147","roundTripHash":"b5389d9f11296e131badd2a6f0a299b125227ebc6b542936d07d8d3fe2b0c7d0","remoteConvergenceMs":7.834,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":33,"adapter":"dartvex","operationCount":1000,"traceSha256":"53b09101de072e7f39e1f605b4e64c528f99951f9ec10abc33901a08def7d6ad","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[56.413,42.389,43.907,55.68,54.379,45.684,40.959,42.074,40.881,42.851,44.363,44.94,45.928,43.854,50.77,46.444,54.412,54.12,39.875,35.767],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"0c9c2a625372e9dea51750e620c118b2702bab4e7484530ad1bd6872097db785","reconnectToLiveMs":63.871,"canonicalVerifierHash":"495cf6e87b63acba4ec386a2f17165d796435465d45f391c4cf212781f7eca5b","roundTripHash":"7a43f69fdf62c9f83ba733e4b8a6488902860126dc015c5037cd8ea03da60bde","remoteConvergenceMs":7.81,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":34,"adapter":"dartvex","operationCount":1000,"traceSha256":"41039af22db14b082c0f8d27d0f337109516f3c0ff5395bbdbd52c3f2a73e366","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.068,41.501,40.387,42.872,41.613,41.298,40.906,43.915,41.885,41.227,43.83,43.104,43.912,43.071,52.982,45.107,52.957,54.903,39.642,35.613],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"1a24ce7e3c28f5807015976bbfa3e712f6def2a8d3b8f8ed87dfb6d2903f154b","reconnectToLiveMs":55.859,"canonicalVerifierHash":"cf93ed2b3ec0f97763ae4ab9f60906018563a26a37da1abcd4ea47f5a3b27149","roundTripHash":"1fbeea46ef7e1780b7fe8db03d2cc35b54a8e469a4fd1047a159498bfb643161","remoteConvergenceMs":7.965,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":35,"adapter":"dartvex","operationCount":1000,"traceSha256":"b9275a1393e700c6b85ebf43295ee9fbf7e0215d3e64d41b229fd4d0d1d16252","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.802,39.553,38.8,41.922,40.214,42.684,46.172,43.635,42.022,42.207,42.745,43.881,44.486,43.689,57.625,45.809,53.006,54.095,38.201,37.126],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"519d837de5b7f860793ea33c7269be33f2d3286b79c6e9e9507418df34faf035","reconnectToLiveMs":110.436,"canonicalVerifierHash":"887321746985c1c8bd0822300fe6373a4ef21380ef5ecc9c335f47e8cf9a8271","roundTripHash":"b9cea9c0d0df6e43effaaff688e7e8b7dcea72d7f88d91df5d70c9dcd51b3ece","remoteConvergenceMs":7.915,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":36,"adapter":"dartvex","operationCount":1000,"traceSha256":"c0c7e0207a6bcd0957b602550570e42db42800fe57c66626010f25242d5f8208","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.249,40.382,41.024,43.066,41.622,40.654,41.606,43.465,42.895,43.573,44.157,42.572,45.476,46.946,58.786,47.9,55.686,55.375,39.003,36.92],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"44641ee26407719b887dc44016ae6afe719ff28a61bf2ac2adff13418a002f2d","reconnectToLiveMs":115.353,"canonicalVerifierHash":"b25494e6ef17124b508fe8d47b13d055664336898cf2192f495b01b2037d7c0e","roundTripHash":"d14c83263732d3d4ce5c1ddffa521f4a8e139a0115a0b13204c75c5d5ded723f","remoteConvergenceMs":7.781,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":37,"adapter":"dartvex","operationCount":1000,"traceSha256":"78c97698051da2fb983461e5c8f6d2d3bd357afb10e6d42cf9918b98c8804b5a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[49.407,48.095,41.143,45.358,42.334,42.217,41.384,42.518,41.535,43.983,43.604,43.756,45.543,43.932,52.246,45.829,55.157,54.926,39.098,35.873],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"36b5339d6a5239af0c2e64c5a19e64b084ad9ce6a27851969945465a67dbb365","reconnectToLiveMs":55.789,"canonicalVerifierHash":"c9bd2baab0265dc7a190e0cd455f13e2575f7a04ebe2a01afecfb5d78ea1cc65","roundTripHash":"6ddbcafed928297a094e02cca9d9201fd12c225dd55b7df8a267aadd5e671715","remoteConvergenceMs":7.733,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":38,"adapter":"dartvex","operationCount":1000,"traceSha256":"3f2dafd0bc96122b9fd4274b15b67a6834f00aefcce0e9fd414dc56bee7ee67b","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.566,40.847,39.499,42.278,40.063,42.286,42.524,41.937,42.754,42.753,43.108,44.384,43.539,47.108,57.846,45.084,55.035,53.792,38.475,36.43],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4df6eb45193bcf8a0edcab57b140e5e711a42313537c1b06ad7ac40495a6ed7d","reconnectToLiveMs":93.025,"canonicalVerifierHash":"dac23c9b615ba736f667822e98cd81583d5596d3eb635400fd7923a4b821f2fd","roundTripHash":"5f2b2d6429deabacf2274aa4cd918f601a4d7979d4e6425eac0dabff353df84e","remoteConvergenceMs":7.919,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":39,"adapter":"dartvex","operationCount":1000,"traceSha256":"7b901f84dd62107f244be49a92208d42d8c9d3da353225163c2eae58533cc5e7","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.047,40.451,46.947,50.225,45.229,49.088,45.091,45.374,43.693,54.021,43.871,43.63,43.931,46.391,59.18,46.671,55.434,53.924,37.57,35.574],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"3a0e6a8d108e4259c6dacb8b38f8b350945dc7fa4e9364e3913d2caddffe18bd","reconnectToLiveMs":117.338,"canonicalVerifierHash":"1be630ac18ad34887058f68f246be9e0f8a79ae0b431e0342f77f0aafbbf60a6","roundTripHash":"010a6713bd4cb32e2f00459f2d042fbfe23920beb3dba219f9a975fd66606098","remoteConvergenceMs":7.653,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":40,"adapter":"dartvex","operationCount":1000,"traceSha256":"cd40e9a07822696a4198119192a3961886f52674873dbed1fab915a6e50927d3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.249,40.932,39.573,41.464,42.796,41.038,41.923,43.221,43.095,42.169,42.496,43.102,43.309,46.884,54.51,45.429,53.818,55.279,39.033,37.03],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"a91db8c869cb35510ed31cccd264a1874ddc908ddfb04db8e65f8148f5a91578","reconnectToLiveMs":94.183,"canonicalVerifierHash":"25122c18d674ea9d81866fefb9811438d5d0e6930edbb7b4f1bd1336987d3c84","roundTripHash":"f83c2ea15ce0f99cd935fd0aaeffe3bac0805ab6b6f1d45c6ba5e715257fbe02","remoteConvergenceMs":7.872,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":41,"adapter":"dartvex","operationCount":1000,"traceSha256":"3c6017b5b1b61473fc4384d5c642f8c4f21fc498a2cba3981a4fcb04e8e8f815","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.202,41.734,38.9,41.964,40.159,43.179,42.62,44.212,42.695,42.838,64.628,43.192,43.064,44.441,55.591,45.378,53.622,54.908,38.092,36.853],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"f46fbceca36e6df65f09e8184dc171220da87ffa997e93edae4425e494a5644d","reconnectToLiveMs":92.081,"canonicalVerifierHash":"44e6edd99da52fe8e35e1513170aea2419cc5cb214cf506ff75a706440274ae3","roundTripHash":"519c1ac9267bb4369abd0b2bbc015623f2f8c3a7c58946522c36df8a6028a425","remoteConvergenceMs":7.711,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":42,"adapter":"dartvex","operationCount":1000,"traceSha256":"2a3bb8f9e96bbff75da1c0bdcd21b3452ff8c8762013e2f6eca2ca641b707758","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.321,38.968,40.478,42.917,41.37,41.193,42.959,42.614,41.83,42.802,43.298,44.202,44.888,43.443,49.671,45.82,53.469,54.865,38.49,37.034],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"86043e9b248940531840e7cd18115beb2b0dddcf5dc9db88ab096d6ece3d17a4","reconnectToLiveMs":60.052,"canonicalVerifierHash":"94b10f4001fe9e25842dfeaa8246258d85efe73aa823a1f32a587317b43cc262","roundTripHash":"99affcac47d7e010b61d60b84890b99227a88f96e686afecc64ecc96e1a9e434","remoteConvergenceMs":7.848,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":43,"adapter":"dartvex","operationCount":1000,"traceSha256":"3f8a10342efb6e53884d12c1e1425f2ec7ecdd96013bd9223c4475968eca8e3e","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[47.523,40.482,41.034,42.895,41.981,43.041,42.636,42.342,42.693,42.708,43.526,41.996,43.613,44.427,61.544,45.426,52.307,53.03,36.734,36.247],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"19c0e27e7c3cdf297feaa3b02930adcda8b97b4c388b33ea1769e9afdf522ba2","reconnectToLiveMs":127.576,"canonicalVerifierHash":"3f91d6fc35170cf6b81469cd10fdc2276151f41642c183954c905dc2fd72e913","roundTripHash":"2d5a3bcc5c59f2b66c302ee900ea418589d560867b9df6c4ad8433a1d8132683","remoteConvergenceMs":7.84,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":44,"adapter":"dartvex","operationCount":1000,"traceSha256":"0eb474ca0c5f15b2c3bb2837e8ccb7540cc953610e19a90021b5d7fcc17d7e92","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.072,39.315,40.558,40.769,41.745,42.567,41.497,43.374,42.49,43.383,47.716,42.808,43.982,44.344,51.766,46.453,53.558,53.149,40.62,35.813],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d296acd1d505cc41e1746ba8b4f51adf18849dbc0a28408dd5e3bf8e616d4672","reconnectToLiveMs":78.388,"canonicalVerifierHash":"45675064eb12cf1f1a91352bc3ec835026de943504172dfe66afe47547538963","roundTripHash":"db68375a5b6b0d90d87a15cb7be4f53702d3686f539bd3c6e4e91fe56aecf1f5","remoteConvergenceMs":7.863,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":45,"adapter":"dartvex","operationCount":1000,"traceSha256":"48fb865b80110a4ec663a65f26f9bffa285ecb926bc3654aaac732c60d7b3b0a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[47.781,41.573,41.634,43.712,42.764,42.992,45.301,46.381,44.41,43.129,42.277,151.589,47.034,46.388,50.115,46.329,54.85,54.701,42.267,35.203],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"129dec31e4e2f2823c59ae9b87fcae42d394591c61f5be778999c45da2a3e444","reconnectToLiveMs":116.66,"canonicalVerifierHash":"df97c65603a19f4c4351462abc77ddac04994d60de19108557822d5b7a571794","roundTripHash":"bd72d07f8cfa10ba473e751e8571d18cff71139e9de28b389a357005b0549039","remoteConvergenceMs":8.223,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":46,"adapter":"dartvex","operationCount":1000,"traceSha256":"2547f5ef76c4962e0f352c8e03a471c880bc8ca5b6172b583e469813db94f886","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.44,39.479,41.034,42.152,43.157,40.919,41.765,42.062,45.484,43.057,44.835,44.047,45.141,44.576,63.414,45.143,53.287,55.136,56.647,42.646],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"50395ad41530e80d5559fc8b6af5507a8a393adec21dbaf5ef71718f035be3ef","reconnectToLiveMs":95.06,"canonicalVerifierHash":"9b4ee333fd4009d9b1edef10a8a06c285dae7b061b309176dac94fb8296f1af3","roundTripHash":"0574858639d852e4499b0d575c1ed16d92e54431275cbba318a6c50a5938a9f4","remoteConvergenceMs":8.205,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":47,"adapter":"dartvex","operationCount":1000,"traceSha256":"74fe76755a9c32233697d6c69e3751a5a0429d237d306cfa1e17d9b51b4a8569","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[50.378,44.705,47.392,41.014,40.584,42.299,40.686,45.283,41.684,44.283,42.74,43.751,43.668,44.643,62.479,47.431,53.854,55.571,37.789,35.194],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e3d92a8e2ae00349f3b3869363ede60248488f17094c62af9e2b17952177dc57","reconnectToLiveMs":138.369,"canonicalVerifierHash":"3c4caaedf4de1dfe50de5ca3aedad7fd6de3694157191abd14d816ba949321c5","roundTripHash":"f055ebed68cf1f028f3ac92152eb9804730bd979cbe5771187ea7d8a93f1b604","remoteConvergenceMs":8.109,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":48,"adapter":"dartvex","operationCount":1000,"traceSha256":"34d1c2996c4711c53b654ccb9172bdd508ff7023bd0729c37305aff833a9e8c3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.419,41.861,42.94,42.77,42.33,45.674,44.62,43.67,43.376,43.153,43.297,49.236,48.042,45.798,55.783,48.412,58.471,55.677,39.589,36.652],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e7e8f860ed711b1ba4c898d3c6a3a7443cdcb343f3b965d64050f635bebb045a","reconnectToLiveMs":91.068,"canonicalVerifierHash":"882b979772b174022efc16c448d930c4c985077f663f74e281e64d2730ca2d9e","roundTripHash":"d800ff5ea7d263d57c07e1a252a14b4b8787f97831d2de8d8608dfa3a4187f23","remoteConvergenceMs":8.123,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":49,"adapter":"dartvex","operationCount":1000,"traceSha256":"cd10c09eb29b4c21d63fff22d70e92b231eb178d82ab60ea4f2dd0b671e6f0c5","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[50.548,42.99,42.037,43.348,42.71,42.28,42.663,42.426,44.515,45.914,44.38,46.533,54.825,48.929,63.585,46.105,54.925,55.403,38.999,37.768],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"8d2697434c6eed85928903a55f2524696d540ea4d20637d254aaa9ff692314fc","reconnectToLiveMs":128.215,"canonicalVerifierHash":"897c1e45d48565825398a418ad07d9557552005e11b69982d8979da0e6def54e","roundTripHash":"a93b4e6243f97268681c90698b8754de9e2cf2decf12fb6104cb376aa9ace707","remoteConvergenceMs":7.968,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10}]} \ No newline at end of file +{"schemaVersion":1,"status":"passed","adapter":"dartvex","candidateVersion":"0.2.0","flutterRustBridgeVersion":null,"deployment":"local:127.0.0.1:3210","gitCommit":"fb83488c0924f8daf57c4bdfc48d7a4a5ff0c8f5","baseFixture":{"path":"test/fixtures/strategy_integrity/base-test-v43.ica","sha256":"8544873d608a0ad885b2e6042a383596a0b1dc37514034281b4e4eec6168756a"},"seedCount":50,"operationsPerSeed":1000,"totalOperations":50000,"allCanonicalEqual":true,"allResolved":true,"processRestartCheckpoint":true,"bytesSent":20224647,"bytesReceived":48149313,"wallClockMs":64346.84299999999,"maxRssBytes":256311296,"machine":{"operatingSystem":"macos","operatingSystemVersion":"Version 26.5.1 (Build 25F80)","processors":8,"dartVersion":"3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\""},"seeds":[{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":true,"exercised":true,"freshTokenAcceptedMs":10.026,"manualReconnectMs":144.884,"postReconnectAcceptedMs":155.986,"queuedBatchReplayedExactlyOnce":true,"reconnectCalled":true,"recoveryMs":156.006,"refreshSessionCalled":true,"rejectedTokenObserved":true,"tokenChanged":true},"batchLatencyMs":[42.608,35.148,34.734,35.035,34.922,36.206,35.541,37.066,37.501,36.019,65.184,38.437,38.517,38.294,55.611,38.978,48.253,48.317,32.231,30.517],"canonicalVerifierHash":"26d71e8df48fba1f7aae2c8cf4b5569e8b9360166b9bb6ed887ce7f14048f2f7","duplicateNoopHash":"2f5c83b805fd22471e866932f0ad3a6b438850368d79cbaada4e91a2306d36f8","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":10,"fault":"auth_reject_refresh"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"b4e244c38b89789f1191e988768aa681f4722184533077ef58eb6f95ee6a0e52","faults":{"authRefresh":true,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":156.184,"rejected":90,"remoteConvergenceMs":9.335,"retryCount":0,"roundTripHash":"57f03a40845f1cbcec927c4a2abc68781fcec79e10d5f8a896a7deb95bbd9d69","seed":0,"traceSha256":"ddf6d41ed9ccdbf3c60766fe6b0318218dd8954d8615fcffb2a8daefe849aa06","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[39.665,34.943,35.239,36.301,36.401,38.339,36.565,39.271,37.606,38.63,38.333,39.363,38.869,39.184,52.962,41.251,48.59,48.749,32.402,31.57],"canonicalVerifierHash":"521b140c0a612b8ea44a1d9b4b03cbf0573799ae00bf05498122bacab81c1bba","duplicateNoopHash":"f5cf17c272add3c1c7d3460a3f29c4ee4bf786dd0d24d00d5010fd3069450206","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":103.463,"rejected":90,"remoteConvergenceMs":8.175,"retryCount":0,"roundTripHash":"42034206a0ce276524b5f6c4b61270d036eafa3ea888f6af32fcc8898136425d","seed":1,"traceSha256":"74e1c135f93f0e9ebf7029a5222aaa38b6aa0f834e31861e9e3de8515922bca4","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[39.39,34.965,37.174,35.563,36.11,36.582,38.492,37.793,38.708,38.19,39.021,39.703,39.282,39.583,44.904,39.624,49.498,51.921,34.448,32.191],"canonicalVerifierHash":"9ca1893e49091976796548e8568b4e1791bff1f72f5f883892ef688a59483304","duplicateNoopHash":"f241cf2843f3adfa94f6566c00df83a1f234b4993b73daa1fe08ff8b34e89260","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":55.995,"rejected":90,"remoteConvergenceMs":8.3,"retryCount":0,"roundTripHash":"44c8e21bbcb1c01d01bd1446150e3c5e3daa0f256aced3da4e96ac1afeb6be93","seed":2,"traceSha256":"2b3e81b55c925a72176cd6f3d1515c7063d7706d8d4381c641b6fc0a8e121262","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[40.069,35.791,36.04,36.85,37.931,37.629,37.651,38.799,39.617,38.815,38.987,38.969,40.033,40.719,54.526,41.157,49.59,50.288,34.368,32.436],"canonicalVerifierHash":"197d1fc1c62be082b93dd2c51021a15c68e3f7551de5e0d949b11152f6ab6840","duplicateNoopHash":"1e34802380fb4c7b66dfe194d29cf6bb4a223e342c5ac58fb77bd10b9436b658","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":119.517,"rejected":90,"remoteConvergenceMs":7.963,"retryCount":0,"roundTripHash":"18bb3a755432638399f32b7fb39627dab2c7c474726dc01b433243d45e86ab8f","seed":3,"traceSha256":"f7105a396da40cd883ec4cc9e83f1a6cb53706e90e782cc3e71cb778bf252235","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[39.892,38.059,36.454,36.528,36.675,37.514,37.357,38.272,38.264,38.817,39.921,39.005,40.313,40.61,53.866,42.152,49.182,49.745,32.958,32.346],"canonicalVerifierHash":"472d3b636388ed4b3ce98242c337cdca98191dd93feeaf23b2f0bf72c2068232","duplicateNoopHash":"94cb107907f89f5fc3a85d810566e5f67ec06498ef9345e4236bce4c79443170","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":126.523,"rejected":90,"remoteConvergenceMs":7.589,"retryCount":0,"roundTripHash":"6bc869c98139d476a3b61de2ca80637ca4d956c2e99008462e6667110516d5e8","seed":4,"traceSha256":"f8cc2d86c82f00b22442e2b457b962f2e31d958be26bd26999c5035502d44d52","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[39.953,36.043,37.474,36.596,37.1,38.671,38.786,39.016,38.579,39.851,40.928,40.816,41.237,40.89,44.574,42.409,51.474,50.047,32.932,33.015],"canonicalVerifierHash":"60e119554dd2c84670ff85ad1f4743fe3b39bb2367a81ecfa4f978cd7e834be1","duplicateNoopHash":"69a9e28c6ba4259807f9135260188fb48763db7cd60da9f0bcbb2df689ac7fec","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":54.861,"rejected":90,"remoteConvergenceMs":7.427,"retryCount":0,"roundTripHash":"3e1eb93aba491b833fe657b72a49cb08871e51b6abd93ced58a16b4c1bb1d714","seed":5,"traceSha256":"d0e377de6c9acd5bde977020ce6b5a3b79f41ba792748eeb87617f0bbfe67073","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.123,36.347,36.803,37.735,37.933,37.726,40.935,39.849,39.641,39.264,39.902,40.177,42.264,40.966,58.928,42.26,50.693,56.274,34.561,33.006],"canonicalVerifierHash":"870bdc3fe241a36e4cafa9cd2db6550900db537148df0012129a4fc729168e44","duplicateNoopHash":"7b6d6f868d69e5c19a34b1ef7a5da5bd85c7b2c0de6b278023971d5125376186","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":138.784,"rejected":90,"remoteConvergenceMs":7.385,"retryCount":0,"roundTripHash":"a6d78c0173ff37ca01ce546bd371b25102dff424fc71e5ed158da8cc3afb9409","seed":6,"traceSha256":"4089e29162d4a1f4eeb277698a40e77dee654c5e6b024de882412828b2b868be","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.37,37.751,37.476,38.235,38.176,39.042,39.029,39.761,38.91,40.261,39.847,39.827,40.132,41.345,45.8,41.917,51.416,52.069,34.72,32.827],"canonicalVerifierHash":"07d665cbffdf875c025bb158a561350943ae3fbd5068762fd11819a6a4901942","duplicateNoopHash":"abc350378349c0c5ca8ac8718147350b24bba91d01f1b3584ac0ae8728392bbb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":58.802,"rejected":90,"remoteConvergenceMs":7.699,"retryCount":0,"roundTripHash":"e9e701572c478eb4ff07bdc25b9686439e87f671b6912b55edab9f6640bd9348","seed":7,"traceSha256":"2f479b6ed6743dedae4a36d95c58c95f225ba529fcd863e5640f49afc5e838b7","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.317,36.767,38.726,40.325,38.393,40.051,39.546,42.167,40.5,41.073,40.149,40.285,41.872,40.868,54.99,42.863,51.482,52.294,34.939,33.781],"canonicalVerifierHash":"efc2b0c123c804ed7f1847b3cfaec274bad1ea5ae14b606f04cc0a565458437f","duplicateNoopHash":"7c4a10f6a44922af7b90a06f9e3796d4915b37934f1958e659c352ef988032a7","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":141.639,"rejected":90,"remoteConvergenceMs":7.902,"retryCount":0,"roundTripHash":"2381d28c5762232a05a96531e03aabc0b196671f5f4c493cfb8a2bc1e16fed87","seed":8,"traceSha256":"18d04c6a59b74a9026ab9d6f69161a1ae3101de4fe16acc08b831a885a6a301f","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.266,37.341,38.486,40.63,38.659,39.783,39.125,41.659,40.475,42.183,41.183,41.624,43.165,43.194,48.156,43.184,52.743,52.633,36.585,32.426],"canonicalVerifierHash":"703442e3b93e7bc929da059e86ab44de647f270cefc120656f501966b6847100","duplicateNoopHash":"acb46046274da0c9262f4910b29e4fda0a2bfe3132374a35db02c9f2f89398f4","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":61.885,"rejected":90,"remoteConvergenceMs":8.022,"retryCount":0,"roundTripHash":"e63096e423e4fa5de04d55dd992a191a939e288f22077b5dba9f1f30d0312ac0","seed":9,"traceSha256":"10e7fe955f927f3626b5586eff07c5c452e7dd8fada065f6ea12839907ebfb8f","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[41.745,38.55,39.82,40.531,38.961,41.076,39.428,41.153,40.38,41.587,41.377,42.583,42.521,43.181,60.903,43.214,51.966,52.231,35.256,34.619],"canonicalVerifierHash":"0dedd28fa52610998313b57e5e621be6488408b8475b8082434152a7e4bfffde","duplicateNoopHash":"68de0d932dda40e9fa94ad87bce5062194f4b64d032743224b09543a35fadb04","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":137.639,"rejected":90,"remoteConvergenceMs":7.721,"retryCount":0,"roundTripHash":"e199bf3ee7ce8299f481c487ecb563b9bc5a22508107847ee32280f5af2799fa","seed":10,"traceSha256":"3aedbcc55f63d2e40d1053713da07189c675eeedafc45557893bfb44a4a4a3fa","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.99,38.3,39.078,40.912,38.761,40.816,39.776,42.124,40.569,41.758,40.988,41.695,43.061,43.269,62.173,44.115,51.016,53.068,34.685,34.861],"canonicalVerifierHash":"976df9bc169579795add78059b22aecb15add1a77fa132a762b81256d7a14ab9","duplicateNoopHash":"ecd01e95d0ba98e4c1033dd1c11ae805f94ea9c57caa7eeb3883adbdba18baa9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":146.874,"rejected":90,"remoteConvergenceMs":7.622,"retryCount":0,"roundTripHash":"6b2a4c8cae985614914a1122c691c7976d2f5fe6bc30dca6a2a54e7a718ebbdb","seed":11,"traceSha256":"d64e19b329a28294c7e145d5eea4d659b86b00fdc478f7472e57f1b93de378ef","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.405,37.692,37.907,38.705,38.575,41.526,39.321,41.76,40.252,42.711,40.769,41.976,42.062,43.448,62.286,44.689,52.252,52.089,35.443,35.083],"canonicalVerifierHash":"fe27618043429656abed9f1810fd3f7fe21fa7844cfe474e4ee27170aa6ba761","duplicateNoopHash":"75cb591df89ce4872e5d71150982ccb110460036da1a031d13af69b1a497bf44","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":144.977,"rejected":90,"remoteConvergenceMs":7.633,"retryCount":0,"roundTripHash":"08db87f541a6ad80969ae614cb3c192ee4668e76d650a3c3c83ae2205d7cae2f","seed":12,"traceSha256":"56270e40851c311cafe2638b7bdf9e162ca4190285215f3a788c34ab68e9fbfa","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.463,38.943,41.496,39.556,39.86,41.869,40.275,43.486,40.467,42.175,40.429,43.376,43.666,43.833,59.158,45.088,53.503,51.643,35.534,35.471],"canonicalVerifierHash":"1eb6c99489819977b8c62d4ac7cc2b6332253eea89c0028a0d00f543ccc80b4e","duplicateNoopHash":"a2780357577e00a64a3d6882842034f3c7a33a7fb73f76a1bb513582adc82f48","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":117.484,"rejected":90,"remoteConvergenceMs":7.656,"retryCount":0,"roundTripHash":"30a8267dc6ce65b39cf9e2f8e9d47c425c22d34ae75df7a6427dcfbf320b49ca","seed":13,"traceSha256":"04fbda33461bbb975b9e66fc1ff0fe176931fdbd9c56fe8d3b9f9abcc939ab07","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.967,38.415,39.008,42.222,40.17,41.074,39.967,42.243,40.277,41.792,42.81,42.716,42.0,45.339,69.81,45.81,53.544,53.172,34.924,35.053],"canonicalVerifierHash":"9f4f93499745ce94e2ea84804d41cdd24007491f9ad333f6994c23adaca1a97f","duplicateNoopHash":"6a79d43b71c80e3ece66ab93693e6111df9ac8633c5e8af7dfbcdc86f73f16e9","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":154.188,"rejected":90,"remoteConvergenceMs":7.941,"retryCount":0,"roundTripHash":"4406199eb581980a79d880cbb483fd97b1ac2b9ecf927ac3d48a1ede8f3a8ca6","seed":14,"traceSha256":"d105bcc5f7eaa8a768bd666c3d6fb072fb97a0191a935d70e688d086e52f95c4","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.663,39.379,39.698,40.552,39.281,41.158,39.782,42.725,39.955,42.478,42.702,44.346,43.69,44.168,64.574,46.865,53.618,53.677,36.153,35.789],"canonicalVerifierHash":"e44921495d13f7e764fb5170d992c6806add23c1ee2a902d2ed586e78b6518f2","duplicateNoopHash":"89f952d57ff5c744f32966f233f39fdf76ff3729bca2ffb22da379572bc772cb","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":146.749,"rejected":90,"remoteConvergenceMs":7.921,"retryCount":0,"roundTripHash":"885dcea93a305a0527b65c09ab8a51e42eeab8486b0a17a812adfc8a23dd0692","seed":15,"traceSha256":"fd2509f037ad595831e5af5de358a045a4944c5070aeccceca9a85e5d93f0dcb","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.38,38.794,39.073,40.913,40.511,40.974,40.851,42.871,41.416,41.948,42.445,42.663,44.113,43.485,51.903,43.297,52.953,53.62,36.206,33.96],"canonicalVerifierHash":"fcab45e94cbb1dd32f40022c4f2538d8a8921f6f9a993c44da5a1af856273521","duplicateNoopHash":"1a2c13daad7b021d5bf7aba90696fb2fba2a5479ff1fa49027c88e3b5e9bf237","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":79.007,"rejected":90,"remoteConvergenceMs":7.58,"retryCount":0,"roundTripHash":"23079740e8c81056d542e80219368de85f2c9ae8740e9db95182ac2d1433a5cf","seed":16,"traceSha256":"15974b2b6d314a57e29fe393296085940f83539b4485eb9ada849149db7a99cc","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[53.581,43.208,41.272,44.059,41.826,40.468,42.165,186.644,52.932,48.933,46.565,44.732,45.943,47.413,53.895,45.598,51.81,52.493,36.281,35.466],"canonicalVerifierHash":"0ae80e9274783e32170474a495f3c31bfa702e9acbe970cb1a04c439846eb86b","duplicateNoopHash":"834e79323bda37030a4c6af90b9a0dadade53242cd9801ba090d12a86054e645","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":119.19,"rejected":90,"remoteConvergenceMs":7.705,"retryCount":0,"roundTripHash":"5ebbea09bcb5c13323f4a6dea81b146de93f7fa134b025b2163ca48c62222abc","seed":17,"traceSha256":"53cce4ffa3236c18592a997edd684f10596e8cdceacd8402d858c284a7d9054d","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[44.512,39.675,40.826,41.641,41.438,40.77,42.268,41.604,41.055,43.233,42.76,43.316,43.307,42.525,58.776,45.112,53.329,53.389,37.699,34.375],"canonicalVerifierHash":"f3fd473e5c7029efed7f8b2b39933c0240a36cf83a5eb3286adfe377c9890da5","duplicateNoopHash":"2328c71bfb35d1fc60079b4c3572d96fac5e48f92fa4fee9baa2f9a469aa7404","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":134.581,"rejected":90,"remoteConvergenceMs":7.89,"retryCount":0,"roundTripHash":"9f8e4e09c1123b52138e32282eeb53f1d13c8317d8f3b2cda0cb11c576704704","seed":18,"traceSha256":"a542c21db854c18f4cdcb385e6ac298bf7af6282c0972913cc8fce70aec86847","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[42.409,38.809,39.556,40.558,39.845,40.737,41.289,43.095,42.66,42.694,42.985,43.922,43.625,43.914,61.152,44.398,52.414,52.745,35.473,34.026],"canonicalVerifierHash":"63e156ef82374f25fc17ac38cd7fb0ffa365fa4d6fcd29f42be9a52f729ab158","duplicateNoopHash":"1121a351ad1dd9adce4685ffd6e719a32e98c70594e4a09f928d6f996e9d1207","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":138.682,"rejected":90,"remoteConvergenceMs":7.692,"retryCount":0,"roundTripHash":"3bf2bcb94fab05718ea34aed383efbf9e411b10d95a6179ae911f90d6a57cb1a","seed":19,"traceSha256":"0d92405289eea8b6bef336eab7f6bd29373c9dfcab013fe1126707d0fc4cb0a6","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[45.267,39.573,38.696,40.33,39.295,40.839,38.926,42.327,40.62,41.736,43.308,43.073,43.327,45.236,49.482,43.129,52.184,51.988,36.14,34.365],"canonicalVerifierHash":"c8bdffb3ea12e788eb2a5088294f17e8705320e3c734ca0302d660b841565e56","duplicateNoopHash":"661be1cf36a3e6dfd57dfc2f78cfaaa70d9b9adf2badc7d67b4c20ac4a528c24","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":65.988,"rejected":90,"remoteConvergenceMs":7.981,"retryCount":0,"roundTripHash":"268efccad0ace19c28dc92d9c54b320c38ac129eae3a5b5f6710d15562fa7f3a","seed":20,"traceSha256":"810f5993000813418216c7c55f6a64a15516e1b6f63e77e55dc9d0beb326a59c","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[40.99,39.316,40.747,41.417,41.389,40.996,41.297,45.814,41.508,43.889,42.97,43.136,47.003,46.298,55.177,45.932,54.098,54.685,47.453,36.333],"canonicalVerifierHash":"c26d7da5ac7f287f6edff0b7633a49ceba13692f2421bf0738cd6bb044c5428e","duplicateNoopHash":"e1ca213dd003e35e7e8cb0e94b3cc3c65a6284240156492d42496a39c0216155","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":82.961,"rejected":90,"remoteConvergenceMs":7.602,"retryCount":0,"roundTripHash":"4d284bde91877dd19f45ec5357c0bbc3213b125ea5a2ab460aac161c834e56d1","seed":21,"traceSha256":"00514f57b0533bafeeb8cef85306163b05977fd156fb8d4f6e8971f43cd5c3d9","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[58.337,40.69,39.773,41.806,40.999,39.957,41.438,43.177,42.941,43.137,42.72,43.388,43.548,50.906,64.154,48.256,61.066,60.984,42.614,35.825],"canonicalVerifierHash":"7dc98e0042f910e161dc48692e61b71bee6f5f3c4e2c185947bcd346651c62c1","duplicateNoopHash":"0c84e6481a015a570d0181acae33fe459537c904b339fab12049bfcb9f98024a","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":129.992,"rejected":90,"remoteConvergenceMs":8.282,"retryCount":0,"roundTripHash":"8af6d9079e0d0da10a62772b0ec081dc064e6480c6545292713e51b5ee9afeda","seed":22,"traceSha256":"3b219f2a009454e6a7e145c270ea4540712ac42a2e64a3de49decb95d00cb6ec","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[48.029,41.083,42.59,43.682,43.026,41.77,42.796,43.43,42.992,42.906,43.135,43.328,42.783,43.114,56.152,43.677,53.926,53.75,37.53,35.939],"canonicalVerifierHash":"1f42cdfc51ff0c4a3d156d71d7af4941eda5ef046969f200459a8dfcf0df4594","duplicateNoopHash":"4a4946b0e8eb9183e6b90e464571b002e3053e95ed6510dcb24e619868058604","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":100.084,"rejected":90,"remoteConvergenceMs":7.57,"retryCount":0,"roundTripHash":"d9eb0ae47ebe116de7ffe955c9782bbc6cf6f1bacb1ff2cb44ad3528939d70d6","seed":23,"traceSha256":"a44400c0327fe034c8e1d489d65d4a1e8444a312806565991f9f90c2e7bd2427","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[43.975,40.274,40.137,42.041,42.96,41.152,40.473,41.993,41.711,42.393,42.968,43.412,43.675,43.044,58.421,43.522,52.493,55.279,35.54,35.796],"canonicalVerifierHash":"77478c60c3ae9cd45fc8dcda6a71cd0010be62120387e64242c60b1eb7c87a4e","duplicateNoopHash":"089b4218683a43b11f6a2fb52d4b660cd1db966fe14a98a652aba04a95c23a78","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"}],"faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":false,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"finalElementCount":82,"finalLineupCount":10,"finalPageCount":2,"finalStrategyRevision":15,"operationCount":1000,"reconnectToLiveMs":116.908,"rejected":90,"remoteConvergenceMs":9.145,"retryCount":0,"roundTripHash":"0001e26052bdd0bab5724856e8de207155cb84a9d448d7aef7c173ba4e4a96e3","seed":24,"traceSha256":"dbd479d2d15a8febd16d5f86b91f7273d4c05d57076b180f1daa3d318d89b39a","unresolved":0},{"acknowledged":910,"adapter":"dartvex","auth":{"acceptedAfterRefresh":false,"exercised":false,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"queuedBatchReplayedExactlyOnce":false,"reconnectCalled":false,"recoveryMs":null,"refreshSessionCalled":false,"rejectedTokenObserved":false,"tokenChanged":false},"batchLatencyMs":[46.194,39.016,41.269,40.621,40.714,41.011,40.783,42.786,41.292,41.513,56.841,50.088,52.021,49.673,53.765,45.56,55.483,57.132,44.183,35.354],"duplicateNoopHash":"610676792608b1dd1f2e06f6dc22864d82873dd8f8cbe86ef906a8174a708d0c","faultSchedule":[{"beforeBatch":0,"fault":"offline_queue"},{"beforeBatch":3,"fault":"delay","milliseconds":25},{"afterBatch":4,"fault":"duplicate"},{"beforeBatch":7,"fault":"subscription_restart"},{"beforeBatch":14,"fault":"reconnect"},{"afterOperation":500,"fault":"process_restart"}],"faultScheduleSha256":"d680df6635276d5593cea5cf988ac54915d8810167aae2037195424564f25e44","faults":{"authRefresh":false,"boundedRetries":true,"delayedDelivery":true,"deleteRecreate":true,"duplicatedDelivery":true,"offlineQueuedEdits":true,"processRestart":true,"reconnect":true,"revisionConflict":true,"subscriptionRestart":true},"operationCount":1000,"rejected":90,"retryCount":0,"seed":25,"traceSha256":"22a209a7f9723602fadb0c3baab2d73b8a7cef893297ade4cd87a2d61122cd5b","unresolved":0,"reconnectToLiveMs":101.576,"canonicalVerifierHash":"0b7840fc3e7157c541d7862e398acaa57ac57628f918e516746483093abf6745","roundTripHash":"44ae786ea3c9385ee3d3d5321fd63b6222cca1ea07240a307559cdbbbdbecc19","remoteConvergenceMs":16.915,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":26,"adapter":"dartvex","operationCount":1000,"traceSha256":"9c530a068ba1382fbe34f6b3bd558672f8f4538963b8ad673e4be834103e93bd","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.234,41.63,40.936,42.48,41.607,41.334,43.58,44.372,42.697,42.725,42.979,43.908,42.339,44.072,57.92,44.638,54.499,54.336,37.77,36.422],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"119ebe1f480b6c9f5df8f95cab240265bd09233c3f4b5dcd923728ef667b7a16","reconnectToLiveMs":129.001,"canonicalVerifierHash":"b8fc7d69a834b62d4a614de02cebbde60f3a9ccb93b4bec1f27e9987beced0ce","roundTripHash":"748fe55957a8fe14588430c1b62e80c9bf9bb046656f3dcd48d1691f1b33b54b","remoteConvergenceMs":8.488,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":27,"adapter":"dartvex","operationCount":1000,"traceSha256":"0135b2eafc700e90fcbcd47b19e5a1d34ab5e99f255b4b315e2161c2896ef910","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.256,39.124,39.475,40.925,39.882,41.26,40.733,43.702,46.276,43.669,46.007,49.697,55.305,45.26,64.296,45.285,54.843,53.42,37.169,35.085],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4ad42017a48dbc8e12d942d945792c060603e08d9b7d18872b2158673ad23d7f","reconnectToLiveMs":154.689,"canonicalVerifierHash":"16a2f4cfba0166cf6435be3b64f11efbcaa06678f991140dbacc8d4e32097661","roundTripHash":"3c1ab0d073cd7610b4cd4a7c6521c0b5e47e4534104feda430f35e148f39f367","remoteConvergenceMs":8.011,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":28,"adapter":"dartvex","operationCount":1000,"traceSha256":"03fd5bbcfd5789fb329015b4ca3015f10c5322f782f41d1a33c8739dcc250103","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.072,39.965,40.369,42.355,41.703,42.536,41.977,43.73,43.306,42.655,42.316,42.871,45.222,43.211,64.956,46.046,52.974,54.584,36.259,35.483],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"2d62e883551de7eef0494e8e419eb251320287cfd85221fa771a2ec7467bb810","reconnectToLiveMs":154.186,"canonicalVerifierHash":"b990e106461c9e852b53e3be3e6c612b61f79bacc9e5c999f8ef1de308a108a0","roundTripHash":"8231af29d3d0c7601470ec0da754f2f0fded387c6bbda084509076505efb0e9b","remoteConvergenceMs":8.059,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":29,"adapter":"dartvex","operationCount":1000,"traceSha256":"8c898c274aa218bfce9e6a4f758d9273e1401b0edeab0db3d648e233aef3c687","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.497,39.878,40.155,41.975,40.596,41.654,41.632,44.082,42.082,41.78,41.322,42.451,42.689,42.984,56.875,49.585,56.771,55.375,41.507,38.663],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"862b67674912aa8d134be72444f4cdb9a5b50c33276e2a90ef4ae87f302351a8","reconnectToLiveMs":78.069,"canonicalVerifierHash":"abd743722679bd1335b1944cd1a5e315a89097e014d9b43d57957012d766e41a","roundTripHash":"6954dfaff2a7993d6a4d77208f9bb004083a5722931341643974ed81212fa7e8","remoteConvergenceMs":8.04,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":30,"adapter":"dartvex","operationCount":1000,"traceSha256":"222c0dc904bf299d49547394de05a001ecc68710bc02a1b311b4beafb0beb019","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.681,40.799,40.974,40.953,40.552,40.607,41.857,41.8,42.629,41.38,40.858,43.434,42.728,43.527,54.171,44.274,54.212,53.196,36.127,34.308],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d0cd4491b844b50e85c3cbec56f3095662a981c0b8b695d7c85637b0e269aad1","reconnectToLiveMs":99.276,"canonicalVerifierHash":"1894c1445629a6547704f4dfd8bb17797b825eb585c520264c5c08dbc6366d77","roundTripHash":"bb887ac1fe5cae5ba0f2625c2af2b79493cb3fd885fdd8e1560a3aebe032b712","remoteConvergenceMs":8.045,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":31,"adapter":"dartvex","operationCount":1000,"traceSha256":"c3760666249cd5b3679cb726fd60c15bde3e2a9bd079a7800fa297f824b7d339","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.298,38.959,38.382,41.848,42.344,40.326,40.346,44.686,42.335,42.64,41.521,41.554,44.202,43.741,51.251,44.482,54.085,54.681,39.005,35.885],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"206eaacdceff3df5532e76f3e5adc4964146e8504a64cbfddb43fabf4fce28ef","reconnectToLiveMs":75.035,"canonicalVerifierHash":"47d58b681022fd8ecc3b63238639a194262bef74422dd577da50fc761551d216","roundTripHash":"1ef7b56e658c8f1715f96246a583228c7b022d4e53b86374934a608bda3567b1","remoteConvergenceMs":7.786,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":32,"adapter":"dartvex","operationCount":1000,"traceSha256":"f8a1efc44c616e628d61be595d4b092755c5da9e4347176fbf48421b326056c1","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.09,45.687,46.955,43.621,40.107,41.113,40.637,41.248,41.798,43.484,42.107,41.891,43.468,43.699,52.351,44.981,53.788,56.743,36.665,35.184],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"9f583fc4b098f02824bb5fa50c8ed9a76709c8850a94de973e02fe98003010ee","reconnectToLiveMs":75.109,"canonicalVerifierHash":"ef6d53a40b328a9729ca3c0157aa0adbe1c7cb390ab38e0c6949e03aa9a45147","roundTripHash":"b5389d9f11296e131badd2a6f0a299b125227ebc6b542936d07d8d3fe2b0c7d0","remoteConvergenceMs":7.737,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":33,"adapter":"dartvex","operationCount":1000,"traceSha256":"53b09101de072e7f39e1f605b4e64c528f99951f9ec10abc33901a08def7d6ad","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.435,39.798,41.407,47.951,45.777,40.732,43.537,44.461,43.111,43.657,43.306,43.956,43.872,43.327,56.297,44.881,53.069,55.16,37.253,35.813],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"0c9c2a625372e9dea51750e620c118b2702bab4e7484530ad1bd6872097db785","reconnectToLiveMs":104.226,"canonicalVerifierHash":"495cf6e87b63acba4ec386a2f17165d796435465d45f391c4cf212781f7eca5b","roundTripHash":"7a43f69fdf62c9f83ba733e4b8a6488902860126dc015c5037cd8ea03da60bde","remoteConvergenceMs":7.778,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":34,"adapter":"dartvex","operationCount":1000,"traceSha256":"41039af22db14b082c0f8d27d0f337109516f3c0ff5395bbdbd52c3f2a73e366","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[47.095,41.301,40.825,42.249,39.634,40.897,42.579,44.226,42.129,42.067,42.734,42.967,43.779,43.724,57.158,44.567,56.615,55.017,39.129,36.504],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"1a24ce7e3c28f5807015976bbfa3e712f6def2a8d3b8f8ed87dfb6d2903f154b","reconnectToLiveMs":106.293,"canonicalVerifierHash":"cf93ed2b3ec0f97763ae4ab9f60906018563a26a37da1abcd4ea47f5a3b27149","roundTripHash":"1fbeea46ef7e1780b7fe8db03d2cc35b54a8e469a4fd1047a159498bfb643161","remoteConvergenceMs":7.73,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":35,"adapter":"dartvex","operationCount":1000,"traceSha256":"b9275a1393e700c6b85ebf43295ee9fbf7e0215d3e64d41b229fd4d0d1d16252","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.49,49.443,46.883,47.815,40.734,46.451,47.456,44.131,42.486,46.913,43.587,45.317,43.465,47.334,58.984,44.185,56.206,54.582,39.856,36.529],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"519d837de5b7f860793ea33c7269be33f2d3286b79c6e9e9507418df34faf035","reconnectToLiveMs":121.464,"canonicalVerifierHash":"887321746985c1c8bd0822300fe6373a4ef21380ef5ecc9c335f47e8cf9a8271","roundTripHash":"b9cea9c0d0df6e43effaaff688e7e8b7dcea72d7f88d91df5d70c9dcd51b3ece","remoteConvergenceMs":7.922,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":36,"adapter":"dartvex","operationCount":1000,"traceSha256":"c0c7e0207a6bcd0957b602550570e42db42800fe57c66626010f25242d5f8208","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.719,42.506,41.162,39.761,39.392,42.228,40.406,41.543,40.291,42.503,43.617,42.669,42.278,45.436,58.196,45.858,56.098,53.851,39.156,36.764],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"44641ee26407719b887dc44016ae6afe719ff28a61bf2ac2adff13418a002f2d","reconnectToLiveMs":109.798,"canonicalVerifierHash":"b25494e6ef17124b508fe8d47b13d055664336898cf2192f495b01b2037d7c0e","roundTripHash":"d14c83263732d3d4ce5c1ddffa521f4a8e139a0115a0b13204c75c5d5ded723f","remoteConvergenceMs":7.631,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":37,"adapter":"dartvex","operationCount":1000,"traceSha256":"78c97698051da2fb983461e5c8f6d2d3bd357afb10e6d42cf9918b98c8804b5a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.166,41.817,41.982,42.094,39.899,42.726,40.359,43.87,42.42,41.931,42.577,43.107,44.635,45.323,54.738,45.525,56.661,55.694,36.087,35.17],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"36b5339d6a5239af0c2e64c5a19e64b084ad9ce6a27851969945465a67dbb365","reconnectToLiveMs":83.358,"canonicalVerifierHash":"c9bd2baab0265dc7a190e0cd455f13e2575f7a04ebe2a01afecfb5d78ea1cc65","roundTripHash":"6ddbcafed928297a094e02cca9d9201fd12c225dd55b7df8a267aadd5e671715","remoteConvergenceMs":7.633,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":38,"adapter":"dartvex","operationCount":1000,"traceSha256":"3f2dafd0bc96122b9fd4274b15b67a6834f00aefcce0e9fd414dc56bee7ee67b","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.932,40.228,40.432,41.801,41.63,42.562,42.729,49.707,51.857,43.805,43.119,42.909,43.017,44.312,55.097,43.811,55.0,54.319,38.159,35.281],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"4df6eb45193bcf8a0edcab57b140e5e711a42313537c1b06ad7ac40495a6ed7d","reconnectToLiveMs":91.093,"canonicalVerifierHash":"dac23c9b615ba736f667822e98cd81583d5596d3eb635400fd7923a4b821f2fd","roundTripHash":"5f2b2d6429deabacf2274aa4cd918f601a4d7979d4e6425eac0dabff353df84e","remoteConvergenceMs":7.942,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":39,"adapter":"dartvex","operationCount":1000,"traceSha256":"7b901f84dd62107f244be49a92208d42d8c9d3da353225163c2eae58533cc5e7","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.697,39.106,41.403,42.077,41.539,42.12,42.391,43.699,42.01,62.751,42.841,43.335,42.253,43.608,61.863,46.594,55.329,53.858,36.831,36.038],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"3a0e6a8d108e4259c6dacb8b38f8b350945dc7fa4e9364e3913d2caddffe18bd","reconnectToLiveMs":135.649,"canonicalVerifierHash":"1be630ac18ad34887058f68f246be9e0f8a79ae0b431e0342f77f0aafbbf60a6","roundTripHash":"010a6713bd4cb32e2f00459f2d042fbfe23920beb3dba219f9a975fd66606098","remoteConvergenceMs":7.869,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":40,"adapter":"dartvex","operationCount":1000,"traceSha256":"cd40e9a07822696a4198119192a3961886f52674873dbed1fab915a6e50927d3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.365,40.04,39.822,42.147,40.44,41.77,41.642,42.799,43.771,42.327,41.826,42.524,43.756,43.561,55.4,45.837,56.339,54.431,36.703,35.129],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"a91db8c869cb35510ed31cccd264a1874ddc908ddfb04db8e65f8148f5a91578","reconnectToLiveMs":104.052,"canonicalVerifierHash":"25122c18d674ea9d81866fefb9811438d5d0e6930edbb7b4f1bd1336987d3c84","roundTripHash":"f83c2ea15ce0f99cd935fd0aaeffe3bac0805ab6b6f1d45c6ba5e715257fbe02","remoteConvergenceMs":7.952,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":41,"adapter":"dartvex","operationCount":1000,"traceSha256":"3c6017b5b1b61473fc4384d5c642f8c4f21fc498a2cba3981a4fcb04e8e8f815","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[46.122,41.241,40.143,197.297,42.51,44.375,43.146,45.811,46.608,43.548,43.023,43.926,43.684,43.621,61.45,43.919,53.558,54.785,38.385,36.568],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"f46fbceca36e6df65f09e8184dc171220da87ffa997e93edae4425e494a5644d","reconnectToLiveMs":130.688,"canonicalVerifierHash":"44e6edd99da52fe8e35e1513170aea2419cc5cb214cf506ff75a706440274ae3","roundTripHash":"519c1ac9267bb4369abd0b2bbc015623f2f8c3a7c58946522c36df8a6028a425","remoteConvergenceMs":7.933,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":42,"adapter":"dartvex","operationCount":1000,"traceSha256":"2a3bb8f9e96bbff75da1c0bdcd21b3452ff8c8762013e2f6eca2ca641b707758","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[45.988,44.689,46.071,42.675,42.725,43.834,43.538,45.137,43.46,43.332,42.474,44.537,44.883,45.693,57.094,47.814,56.171,56.62,39.951,35.788],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"86043e9b248940531840e7cd18115beb2b0dddcf5dc9db88ab096d6ece3d17a4","reconnectToLiveMs":95.254,"canonicalVerifierHash":"94b10f4001fe9e25842dfeaa8246258d85efe73aa823a1f32a587317b43cc262","roundTripHash":"99affcac47d7e010b61d60b84890b99227a88f96e686afecc64ecc96e1a9e434","remoteConvergenceMs":7.902,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":43,"adapter":"dartvex","operationCount":1000,"traceSha256":"3f8a10342efb6e53884d12c1e1425f2ec7ecdd96013bd9223c4475968eca8e3e","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[41.953,41.659,47.41,44.927,43.958,46.428,42.175,44.625,43.337,42.318,46.276,43.325,45.741,46.238,54.809,49.586,55.04,54.784,42.1,37.152],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"19c0e27e7c3cdf297feaa3b02930adcda8b97b4c388b33ea1769e9afdf522ba2","reconnectToLiveMs":95.048,"canonicalVerifierHash":"3f91d6fc35170cf6b81469cd10fdc2276151f41642c183954c905dc2fd72e913","roundTripHash":"2d5a3bcc5c59f2b66c302ee900ea418589d560867b9df6c4ad8433a1d8132683","remoteConvergenceMs":8.229,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":44,"adapter":"dartvex","operationCount":1000,"traceSha256":"0eb474ca0c5f15b2c3bb2837e8ccb7540cc953610e19a90021b5d7fcc17d7e92","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.793,37.782,45.4,46.95,45.749,41.041,40.945,42.482,45.489,42.804,43.291,41.962,43.043,44.073,60.594,51.917,58.633,55.131,36.915,35.685],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"d296acd1d505cc41e1746ba8b4f51adf18849dbc0a28408dd5e3bf8e616d4672","reconnectToLiveMs":54.978,"canonicalVerifierHash":"45675064eb12cf1f1a91352bc3ec835026de943504172dfe66afe47547538963","roundTripHash":"db68375a5b6b0d90d87a15cb7be4f53702d3686f539bd3c6e4e91fe56aecf1f5","remoteConvergenceMs":7.928,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":45,"adapter":"dartvex","operationCount":1000,"traceSha256":"48fb865b80110a4ec663a65f26f9bffa285ecb926bc3654aaac732c60d7b3b0a","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.179,38.392,41.448,44.172,40.114,42.252,40.848,41.86,42.588,42.784,43.368,43.853,45.837,43.065,47.103,47.753,55.706,54.495,39.499,35.883],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"129dec31e4e2f2823c59ae9b87fcae42d394591c61f5be778999c45da2a3e444","reconnectToLiveMs":56.787,"canonicalVerifierHash":"df97c65603a19f4c4351462abc77ddac04994d60de19108557822d5b7a571794","roundTripHash":"bd72d07f8cfa10ba473e751e8571d18cff71139e9de28b389a357005b0549039","remoteConvergenceMs":7.867,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":46,"adapter":"dartvex","operationCount":1000,"traceSha256":"2547f5ef76c4962e0f352c8e03a471c880bc8ca5b6172b583e469813db94f886","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[44.541,39.292,41.459,42.365,40.538,41.621,42.414,42.775,42.741,42.727,42.908,43.436,45.105,43.832,63.242,45.977,54.766,56.069,38.811,35.679],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"50395ad41530e80d5559fc8b6af5507a8a393adec21dbaf5ef71718f035be3ef","reconnectToLiveMs":139.178,"canonicalVerifierHash":"9b4ee333fd4009d9b1edef10a8a06c285dae7b061b309176dac94fb8296f1af3","roundTripHash":"0574858639d852e4499b0d575c1ed16d92e54431275cbba318a6c50a5938a9f4","remoteConvergenceMs":7.791,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":47,"adapter":"dartvex","operationCount":1000,"traceSha256":"74fe76755a9c32233697d6c69e3751a5a0429d237d306cfa1e17d9b51b4a8569","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[43.563,42.675,41.15,44.006,43.016,42.782,42.187,42.73,41.725,43.513,46.287,47.532,44.004,44.117,53.092,45.778,53.654,54.411,40.029,35.629],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e3d92a8e2ae00349f3b3869363ede60248488f17094c62af9e2b17952177dc57","reconnectToLiveMs":67.892,"canonicalVerifierHash":"3c4caaedf4de1dfe50de5ca3aedad7fd6de3694157191abd14d816ba949321c5","roundTripHash":"f055ebed68cf1f028f3ac92152eb9804730bd979cbe5771187ea7d8a93f1b604","remoteConvergenceMs":8.189,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":48,"adapter":"dartvex","operationCount":1000,"traceSha256":"34d1c2996c4711c53b654ccb9172bdd508ff7023bd0729c37305aff833a9e8c3","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[49.063,41.41,39.591,46.698,41.634,44.904,47.431,47.117,43.602,43.18,41.891,43.073,46.424,46.951,58.182,44.262,56.578,54.64,42.757,41.798],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"e7e8f860ed711b1ba4c898d3c6a3a7443cdcb343f3b965d64050f635bebb045a","reconnectToLiveMs":100.174,"canonicalVerifierHash":"882b979772b174022efc16c448d930c4c985077f663f74e281e64d2730ca2d9e","roundTripHash":"d800ff5ea7d263d57c07e1a252a14b4b8787f97831d2de8d8608dfa3a4187f23","remoteConvergenceMs":7.825,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10},{"seed":49,"adapter":"dartvex","operationCount":1000,"traceSha256":"cd10c09eb29b4c21d63fff22d70e92b231eb178d82ab60ea4f2dd0b671e6f0c5","faultScheduleSha256":"2916373f0d5b6c5ff58578c3a72a0bc436584dab36f1ce931403091816eb0c7e","faultSchedule":[{"fault":"offline_queue","beforeBatch":0},{"fault":"delay","beforeBatch":3,"milliseconds":25},{"fault":"duplicate","afterBatch":4},{"fault":"subscription_restart","beforeBatch":7},{"fault":"reconnect","beforeBatch":14}],"faults":{"offlineQueuedEdits":true,"delayedDelivery":true,"duplicatedDelivery":true,"subscriptionRestart":true,"reconnect":true,"deleteRecreate":true,"revisionConflict":true,"boundedRetries":true,"authRefresh":false,"processRestart":false},"acknowledged":910,"rejected":90,"unresolved":0,"retryCount":0,"batchLatencyMs":[57.603,50.432,57.113,49.102,55.483,48.389,46.91,47.169,48.298,45.564,45.195,49.735,49.842,63.798,73.241,50.371,59.511,60.673,43.486,36.835],"auth":{"exercised":false,"rejectedTokenObserved":false,"refreshSessionCalled":false,"tokenChanged":false,"reconnectCalled":false,"recoveryMs":null,"freshTokenAcceptedMs":null,"manualReconnectMs":null,"postReconnectAcceptedMs":null,"acceptedAfterRefresh":false,"queuedBatchReplayedExactlyOnce":false},"duplicateNoopHash":"8d2697434c6eed85928903a55f2524696d540ea4d20637d254aaa9ff692314fc","reconnectToLiveMs":136.579,"canonicalVerifierHash":"897c1e45d48565825398a418ad07d9557552005e11b69982d8979da0e6def54e","roundTripHash":"a93b4e6243f97268681c90698b8754de9e2cf2decf12fb6104cb376aa9ace707","remoteConvergenceMs":7.972,"finalStrategyRevision":15,"finalPageCount":2,"finalElementCount":82,"finalLineupCount":10}]} \ No newline at end of file diff --git a/tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json b/tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json index 8ed5db5b..58fa93bb 100644 --- a/tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json +++ b/tool/convex_client_gauntlet/runtime/results/fair_rerun_matrix.json @@ -1,7 +1,7 @@ { - "schemaVersion": 1, + "schemaVersion": 2, "evaluation": "convex_dart_client_fair_rerun", - "harnessCommit": "bf421ffef06b5d04749c77bda182f8f0a53796fe", + "harnessCommit": "fb83488c0924f8daf57c4bdfc48d7a4a5ff0c8f5", "deployment": "local:127.0.0.1:3210", "baseFixture": { "path": "test/fixtures/strategy_integrity/base-test-v43.ica", @@ -18,10 +18,14 @@ "stableReturnSurfaceCompleteForRuntime": false }, "convex_flutter": { - "version": "3.0.1", + "version": "3.0.1 + Icarus patch", + "convexRustVersion": "0.10.4 + Icarus patch", "generatedContractBoundary": false, "flutterRustBridgeVersion": "2.11.1 pinned", - "resolvedFlutterRustBridgeWithoutPin": "2.13.0 incompatible with packaged 2.11.1 bindings" + "authCallbackOwnedByConvexStateMachine": true, + "realManualReconnect": true, + "vendoredPackageRequired": true, + "vendoredConvexRustRequired": true } }, "correctness": { @@ -39,30 +43,69 @@ "processRestartRecovered": true, "allCanonicalVerifierHashesPassed": true, "allIcaRoundTripsPassed": true, - "reportSha256": "284ba45543dc5ec76932d0b949569f7973a452222651dbd028fdac631e7fed8e" + "reportSha256": "df57f96fca7393df0097d70dfefcb58d6de6aeb375eba739f5ea3db69edbbd9a" }, "convex_flutter": { - "status": "failed", - "seed": 0, - "operationsCompletedBeforeFailure": 500, - "acknowledged": 450, - "visibleRevisionRejects": 50, - "unresolvedBeforeFault": 0, - "losingCondition": "auth_refresh_recovery_failed", - "freshTokenAccepted": false, - "queuedBatchReplayedExactlyOnce": false, - "reportSha256": "9e9fbcc00ef9665338e6aa3d1b8d17fd3f8a81710914eb687858013e151de520" + "status": "passed", + "seedsCompleted": 50, + "operationsCompleted": 50000, + "acknowledged": 45500, + "visibleRevisionRejects": 4500, + "unresolved": 0, + "authRefreshAccepted": true, + "queuedBatchReplayedExactlyOnce": true, + "processRestartRecovered": true, + "allCanonicalVerifierHashesPassed": true, + "allIcaRoundTripsPassed": true, + "reportSha256": "72cae3ed8f244f4122de1394008587da89057e44d102c9c2968315dd951e7b3f" } }, "profile": { - "status": "blocked_by_correctness_gate", - "pairedRuns": 0, - "reason": "convex_flutter did not recover queued work after Supabase refreshSession and reconnect" + "status": "passed", + "buildMode": "Flutter macOS profile", + "pairedTrialsPerAdapter": 10, + "totalCandidateRuns": 20, + "orderAlternated": true, + "pairedProfileReportSha256": "b1242bbdb567751648acb3c17c2e05161eae0db2dae60488d144bcfdac8b06c2", + "dartvexMedian": { + "remoteConvergenceMs": 7.3035, + "reconnectToLiveMs": 110.8925, + "authFreshTokenAcceptedMs": 9.1435, + "authRecoveryMs": 147.8825, + "maxRssBytes": 134406144, + "averageProcessCpuPercent": 9.248290046558877, + "transferredBytes": 1439001, + "runnerWallClockMs": 2749.5435 + }, + "convexFlutterMedian": { + "remoteConvergenceMs": 7.459, + "reconnectToLiveMs": 73.2425, + "authFreshTokenAcceptedMs": 127.134, + "authRecoveryMs": 205.734, + "maxRssBytes": 141312000, + "averageProcessCpuPercent": 14.533420361788625, + "transferredBytes": 1652716, + "runnerWallClockMs": 1873.387 + }, + "tradeoff": { + "convexFlutterRunnerWallClockFasterPercent": 31.865526040959157, + "convexFlutterReconnectFasterPercent": 33.95180016682823, + "convexFlutterRssHigherPercent": 5.138050831961971, + "convexFlutterCpuHigherPercent": 57.147108153212066, + "convexFlutterTransferHigherPercent": 14.851622757732619 + }, + "desktopTargetsMeasured": ["macos"], + "desktopTargetsNotMeasuredOnThisHost": ["windows", "linux"] }, "verdict": { - "runtimeGateWinner": "dartvex", - "adoptionWinner": null, - "applicationDependencyChanged": false, - "nextStep": "Do not migrate yet. Complete and prove Dartvex's stable generated return surface, and separately fix or replace convex_flutter auth recovery before another comparison." + "correctnessWinner": null, + "correctnessResult": "tie", + "performanceWinner": null, + "performanceResult": "convex_flutter is faster in wall time and reconnect; Dartvex uses less CPU, RSS, and transfer", + "adoptionDecision": "keep_convex_flutter", + "applicationClientChanged": false, + "applicationDependencyPatched": true, + "reason": "Both clients are correct. Migrating to Dartvex is not justified while its stable generated runtime return surface is incomplete; convex_flutter remains the current client but now carries a local package and Rust maintenance burden.", + "nextStep": "Upstream the convex_flutter and convex-rs fixes, and separately complete Dartvex's generated return surface before reconsidering a migration." } } diff --git a/tool/convex_client_gauntlet/runtime/results/paired_profile_macos.json b/tool/convex_client_gauntlet/runtime/results/paired_profile_macos.json new file mode 100644 index 00000000..e672672b --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/results/paired_profile_macos.json @@ -0,0 +1,838 @@ +{ + "schemaVersion": 1, + "status": "passed", + "gitCommit": "fb83488c0924f8daf57c4bdfc48d7a4a5ff0c8f5", + "deployment": "local:127.0.0.1:3210", + "trialCountPerAdapter": 10, + "pairing": "alternating first position by trial", + "profileMode": "Flutter macOS profile build", + "cpuDefinition": "(process user seconds + system seconds) / runner wall-clock seconds", + "transferDefinition": "application JSON bytes recorded by the neutral transport; excludes WebSocket framing and protocol metadata", + "percentileDefinition": "nearest-rank p95", + "machine": { + "operatingSystem": "macos", + "operatingSystemVersion": "Version 26.5.1 (Build 25F80)", + "processors": 8, + "dartVersion": "3.11.0 (stable) (Mon Feb 9 00:38:07 2026 -0800) on \"macos_arm64\"" + }, + "toolVersions": { + "flutter": "3.41.1 stable", + "dart": "3.11.0 macos_arm64", + "rustc": "1.90.0 stable", + "node": "23.11.0", + "convexCli": "1.45.0", + "dartvex": "0.2.0", + "convexFlutter": "3.0.1 + Icarus patch", + "convexRust": "0.10.4 + Icarus patch", + "flutterRustBridge": "2.11.1 pinned" + }, + "desktopTargets": { + "packageSupported": [ + "macos", + "windows", + "linux" + ], + "measured": [ + "macos (universal arm64 + x86_64 build, arm64 host run)" + ], + "notMeasured": [ + "windows (requires Windows host)", + "linux (requires Linux host)" + ] + }, + "buildSize": { + "sharedHarnessBundleBytes": 78540800, + "convexFlutterNativeFrameworkExecutableBytes": 23195280, + "sharedAppFrameworkExecutableBytes": 8000528, + "candidateIsolatedBundleBytes": null, + "note": "The same harness bundle contains both adapters; only the native convex_flutter framework is candidate-specific." + }, + "summary": { + "dartvex": { + "remoteConvergenceMs": { + "median": 7.3035, + "p95": 7.804, + "min": 7.138, + "max": 7.804, + "values": [ + 7.318, + 7.39, + 7.451, + 7.804, + 7.369, + 7.242, + 7.289, + 7.264, + 7.246, + 7.138 + ] + }, + "reconnectToLiveMs": { + "median": 110.8925, + "p95": 150.191, + "min": 60.14, + "max": 150.191, + "values": [ + 128.669, + 71.132, + 69.177, + 150.191, + 60.14, + 98.294, + 111.336, + 110.449, + 140.548, + 142.599 + ] + }, + "authFreshTokenAcceptedMs": { + "median": 9.1435, + "p95": 9.662, + "min": 2.478, + "max": 9.662, + "values": [ + 8.819, + 8.411, + 8.754, + 2.478, + 9.241, + 9.662, + 9.36, + 9.166, + 9.121, + 9.493 + ] + }, + "authRecoveryMs": { + "median": 147.8825, + "p95": 166.973, + "min": 67.838, + "max": 166.973, + "values": [ + 138.399, + 153.409, + 153.956, + 111.622, + 156.27, + 67.838, + 154.531, + 93.486, + 142.356, + 166.973 + ] + }, + "maxRssBytes": { + "median": 134406144.0, + "p95": 134856704.0, + "min": 134201344.0, + "max": 134856704.0, + "values": [ + 134856704.0, + 134250496.0, + 134774784.0, + 134758400.0, + 134332416.0, + 134381568.0, + 134250496.0, + 134594560.0, + 134430720.0, + 134201344.0 + ] + }, + "averageProcessCpuPercent": { + "median": 9.248290046558877, + "p95": 10.118065684863536, + "min": 7.361192206911925, + "max": 10.118065684863536, + "values": [ + 8.009492732126965, + 10.118065684863536, + 9.30226673914993, + 7.361192206911925, + 9.352467522657253, + 9.598999861006481, + 9.661951505505959, + 8.087238660882674, + 7.822167239740701, + 9.194313353967823 + ] + }, + "bytesSent": { + "median": 420054.0, + "p95": 420054.0, + "min": 420054.0, + "max": 420054.0, + "values": [ + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0 + ] + }, + "bytesReceived": { + "median": 1018947.0, + "p95": 1018947.0, + "min": 1018947.0, + "max": 1018947.0, + "values": [ + 1018947.0, + 1018947.0, + 1018947.0, + 1018947.0, + 1018947.0, + 1018947.0, + 1018947.0, + 1018947.0, + 1018947.0, + 1018947.0 + ] + }, + "transferredBytes": { + "median": 1439001.0, + "p95": 1439001.0, + "min": 1439001.0, + "max": 1439001.0, + "values": [ + 1439001.0, + 1439001.0, + 1439001.0, + 1439001.0, + 1439001.0, + 1439001.0, + 1439001.0, + 1439001.0, + 1439001.0, + 1439001.0 + ] + }, + "runnerWallClockMs": { + "median": 2749.5434999999998, + "p95": 3396.189, + "min": 2470.828, + "max": 3396.189, + "values": [ + 3371.0, + 2470.828, + 2687.517, + 3396.189, + 2780.015, + 2604.438, + 2587.469, + 3091.29, + 3323.887, + 2719.072 + ] + } + }, + "convex_flutter": { + "remoteConvergenceMs": { + "median": 7.459, + "p95": 7.575, + "min": 7.294, + "max": 7.575, + "values": [ + 7.471, + 7.348, + 7.457, + 7.461, + 7.405, + 7.294, + 7.473, + 7.575, + 7.509, + 7.436 + ] + }, + "reconnectToLiveMs": { + "median": 73.2425, + "p95": 94.765, + "min": 20.316, + "max": 94.765, + "values": [ + 20.316, + 87.859, + 91.76, + 77.776, + 94.765, + 68.709, + 49.212, + 26.962, + 38.532, + 93.847 + ] + }, + "authFreshTokenAcceptedMs": { + "median": 127.134, + "p95": 171.027, + "min": 73.848, + "max": 171.027, + "values": [ + 107.752, + 73.848, + 156.794, + 170.649, + 104.023, + 122.243, + 132.025, + 89.714, + 171.027, + 150.843 + ] + }, + "authRecoveryMs": { + "median": 205.73399999999998, + "p95": 543.221, + "min": 110.845, + "max": 543.221, + "values": [ + 200.155, + 114.574, + 175.913, + 211.313, + 117.57, + 543.221, + 304.983, + 110.845, + 529.07, + 359.654 + ] + }, + "maxRssBytes": { + "median": 141312000.0, + "p95": 141541376.0, + "min": 140984320.0, + "max": 141541376.0, + "values": [ + 141393920.0, + 141328384.0, + 141279232.0, + 141295616.0, + 140984320.0, + 141279232.0, + 141541376.0, + 141197312.0, + 141475840.0, + 141492224.0 + ] + }, + "averageProcessCpuPercent": { + "median": 14.533420361788625, + "p95": 15.306079064401745, + "min": 11.674009762931128, + "max": 15.306079064401745, + "values": [ + 15.266917516661389, + 11.674009762931128, + 14.381183217380435, + 15.031850821685506, + 15.306079064401745, + 13.104043767506186, + 14.638662904530303, + 15.261467084830866, + 14.266689690152097, + 14.42817781904695 + ] + }, + "bytesSent": { + "median": 420054.0, + "p95": 420054.0, + "min": 420054.0, + "max": 420054.0, + "values": [ + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0, + 420054.0 + ] + }, + "bytesReceived": { + "median": 1232662.0, + "p95": 1351447.0, + "min": 1199927.0, + "max": 1351447.0, + "values": [ + 1199927.0, + 1351447.0, + 1228252.0, + 1265397.0, + 1228252.0, + 1237072.0, + 1199927.0, + 1228252.0, + 1277157.0, + 1237072.0 + ] + }, + "transferredBytes": { + "median": 1652716.0, + "p95": 1771501.0, + "min": 1619981.0, + "max": 1771501.0, + "values": [ + 1619981.0, + 1771501.0, + 1648306.0, + 1685451.0, + 1648306.0, + 1657126.0, + 1619981.0, + 1648306.0, + 1697211.0, + 1657126.0 + ] + }, + "runnerWallClockMs": { + "median": 1873.387, + "p95": 2312.83, + "min": 1703.637, + "max": 2312.83, + "values": [ + 1834.031, + 2312.83, + 1807.918, + 1796.186, + 1764.005, + 2136.745, + 1912.743, + 1703.637, + 2032.707, + 1940.647 + ] + } + } + }, + "samples": [ + { + "trial": 1, + "position": "first", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-01-first-dartvex.json", + "timeFile": "profile-trial-01-first-dartvex.time", + "remoteConvergenceMs": 7.318, + "reconnectToLiveMs": 128.669, + "authFreshTokenAcceptedMs": 8.819, + "authRecoveryMs": 138.399, + "maxRssBytes": 134856704, + "averageProcessCpuPercent": 8.009492732126965, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 3371.0, + "processRealSeconds": 4.34, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.07 + }, + { + "trial": 1, + "position": "second", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-01-second-convex_flutter.json", + "timeFile": "profile-trial-01-second-convex_flutter.time", + "remoteConvergenceMs": 7.471, + "reconnectToLiveMs": 20.316, + "authFreshTokenAcceptedMs": 107.752, + "authRecoveryMs": 200.155, + "maxRssBytes": 141393920, + "averageProcessCpuPercent": 15.266917516661389, + "bytesSent": 420054, + "bytesReceived": 1199927, + "transferredBytes": 1619981, + "runnerWallClockMs": 1834.031, + "processRealSeconds": 1.98, + "processUserSeconds": 0.22, + "processSystemSeconds": 0.06 + }, + { + "trial": 2, + "position": "first", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-02-first-convex_flutter.json", + "timeFile": "profile-trial-02-first-convex_flutter.time", + "remoteConvergenceMs": 7.348, + "reconnectToLiveMs": 87.859, + "authFreshTokenAcceptedMs": 73.848, + "authRecoveryMs": 114.574, + "maxRssBytes": 141328384, + "averageProcessCpuPercent": 11.674009762931128, + "bytesSent": 420054, + "bytesReceived": 1351447, + "transferredBytes": 1771501, + "runnerWallClockMs": 2312.83, + "processRealSeconds": 2.46, + "processUserSeconds": 0.22, + "processSystemSeconds": 0.05 + }, + { + "trial": 2, + "position": "second", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-02-second-dartvex.json", + "timeFile": "profile-trial-02-second-dartvex.time", + "remoteConvergenceMs": 7.39, + "reconnectToLiveMs": 71.132, + "authFreshTokenAcceptedMs": 8.411, + "authRecoveryMs": 153.409, + "maxRssBytes": 134250496, + "averageProcessCpuPercent": 10.118065684863536, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 2470.828, + "processRealSeconds": 2.61, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.05 + }, + { + "trial": 3, + "position": "first", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-03-first-dartvex.json", + "timeFile": "profile-trial-03-first-dartvex.time", + "remoteConvergenceMs": 7.451, + "reconnectToLiveMs": 69.177, + "authFreshTokenAcceptedMs": 8.754, + "authRecoveryMs": 153.956, + "maxRssBytes": 134774784, + "averageProcessCpuPercent": 9.30226673914993, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 2687.517, + "processRealSeconds": 2.83, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.05 + }, + { + "trial": 3, + "position": "second", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-03-second-convex_flutter.json", + "timeFile": "profile-trial-03-second-convex_flutter.time", + "remoteConvergenceMs": 7.457, + "reconnectToLiveMs": 91.76, + "authFreshTokenAcceptedMs": 156.794, + "authRecoveryMs": 175.913, + "maxRssBytes": 141279232, + "averageProcessCpuPercent": 14.381183217380435, + "bytesSent": 420054, + "bytesReceived": 1228252, + "transferredBytes": 1648306, + "runnerWallClockMs": 1807.918, + "processRealSeconds": 1.95, + "processUserSeconds": 0.21, + "processSystemSeconds": 0.05 + }, + { + "trial": 4, + "position": "first", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-04-first-convex_flutter.json", + "timeFile": "profile-trial-04-first-convex_flutter.time", + "remoteConvergenceMs": 7.461, + "reconnectToLiveMs": 77.776, + "authFreshTokenAcceptedMs": 170.649, + "authRecoveryMs": 211.313, + "maxRssBytes": 141295616, + "averageProcessCpuPercent": 15.031850821685506, + "bytesSent": 420054, + "bytesReceived": 1265397, + "transferredBytes": 1685451, + "runnerWallClockMs": 1796.186, + "processRealSeconds": 1.94, + "processUserSeconds": 0.22, + "processSystemSeconds": 0.05 + }, + { + "trial": 4, + "position": "second", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-04-second-dartvex.json", + "timeFile": "profile-trial-04-second-dartvex.time", + "remoteConvergenceMs": 7.804, + "reconnectToLiveMs": 150.191, + "authFreshTokenAcceptedMs": 2.478, + "authRecoveryMs": 111.622, + "maxRssBytes": 134758400, + "averageProcessCpuPercent": 7.361192206911925, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 3396.189, + "processRealSeconds": 3.54, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.05 + }, + { + "trial": 5, + "position": "first", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-05-first-dartvex.json", + "timeFile": "profile-trial-05-first-dartvex.time", + "remoteConvergenceMs": 7.369, + "reconnectToLiveMs": 60.14, + "authFreshTokenAcceptedMs": 9.241, + "authRecoveryMs": 156.27, + "maxRssBytes": 134332416, + "averageProcessCpuPercent": 9.352467522657253, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 2780.015, + "processRealSeconds": 2.93, + "processUserSeconds": 0.21, + "processSystemSeconds": 0.05 + }, + { + "trial": 5, + "position": "second", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-05-second-convex_flutter.json", + "timeFile": "profile-trial-05-second-convex_flutter.time", + "remoteConvergenceMs": 7.405, + "reconnectToLiveMs": 94.765, + "authFreshTokenAcceptedMs": 104.023, + "authRecoveryMs": 117.57, + "maxRssBytes": 140984320, + "averageProcessCpuPercent": 15.306079064401745, + "bytesSent": 420054, + "bytesReceived": 1228252, + "transferredBytes": 1648306, + "runnerWallClockMs": 1764.005, + "processRealSeconds": 1.91, + "processUserSeconds": 0.22, + "processSystemSeconds": 0.05 + }, + { + "trial": 6, + "position": "first", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-06-first-convex_flutter.json", + "timeFile": "profile-trial-06-first-convex_flutter.time", + "remoteConvergenceMs": 7.294, + "reconnectToLiveMs": 68.709, + "authFreshTokenAcceptedMs": 122.243, + "authRecoveryMs": 543.221, + "maxRssBytes": 141279232, + "averageProcessCpuPercent": 13.104043767506186, + "bytesSent": 420054, + "bytesReceived": 1237072, + "transferredBytes": 1657126, + "runnerWallClockMs": 2136.745, + "processRealSeconds": 2.28, + "processUserSeconds": 0.23, + "processSystemSeconds": 0.05 + }, + { + "trial": 6, + "position": "second", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-06-second-dartvex.json", + "timeFile": "profile-trial-06-second-dartvex.time", + "remoteConvergenceMs": 7.242, + "reconnectToLiveMs": 98.294, + "authFreshTokenAcceptedMs": 9.662, + "authRecoveryMs": 67.838, + "maxRssBytes": 134381568, + "averageProcessCpuPercent": 9.598999861006481, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 2604.438, + "processRealSeconds": 2.75, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.05 + }, + { + "trial": 7, + "position": "first", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-07-first-dartvex.json", + "timeFile": "profile-trial-07-first-dartvex.time", + "remoteConvergenceMs": 7.289, + "reconnectToLiveMs": 111.336, + "authFreshTokenAcceptedMs": 9.36, + "authRecoveryMs": 154.531, + "maxRssBytes": 134250496, + "averageProcessCpuPercent": 9.661951505505959, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 2587.469, + "processRealSeconds": 2.73, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.05 + }, + { + "trial": 7, + "position": "second", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-07-second-convex_flutter.json", + "timeFile": "profile-trial-07-second-convex_flutter.time", + "remoteConvergenceMs": 7.473, + "reconnectToLiveMs": 49.212, + "authFreshTokenAcceptedMs": 132.025, + "authRecoveryMs": 304.983, + "maxRssBytes": 141541376, + "averageProcessCpuPercent": 14.638662904530303, + "bytesSent": 420054, + "bytesReceived": 1199927, + "transferredBytes": 1619981, + "runnerWallClockMs": 1912.743, + "processRealSeconds": 2.05, + "processUserSeconds": 0.23, + "processSystemSeconds": 0.05 + }, + { + "trial": 8, + "position": "first", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-08-first-convex_flutter.json", + "timeFile": "profile-trial-08-first-convex_flutter.time", + "remoteConvergenceMs": 7.575, + "reconnectToLiveMs": 26.962, + "authFreshTokenAcceptedMs": 89.714, + "authRecoveryMs": 110.845, + "maxRssBytes": 141197312, + "averageProcessCpuPercent": 15.261467084830866, + "bytesSent": 420054, + "bytesReceived": 1228252, + "transferredBytes": 1648306, + "runnerWallClockMs": 1703.637, + "processRealSeconds": 1.85, + "processUserSeconds": 0.21, + "processSystemSeconds": 0.05 + }, + { + "trial": 8, + "position": "second", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-08-second-dartvex.json", + "timeFile": "profile-trial-08-second-dartvex.time", + "remoteConvergenceMs": 7.264, + "reconnectToLiveMs": 110.449, + "authFreshTokenAcceptedMs": 9.166, + "authRecoveryMs": 93.486, + "maxRssBytes": 134594560, + "averageProcessCpuPercent": 8.087238660882674, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 3091.29, + "processRealSeconds": 3.24, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.05 + }, + { + "trial": 9, + "position": "first", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-09-first-dartvex.json", + "timeFile": "profile-trial-09-first-dartvex.time", + "remoteConvergenceMs": 7.246, + "reconnectToLiveMs": 140.548, + "authFreshTokenAcceptedMs": 9.121, + "authRecoveryMs": 142.356, + "maxRssBytes": 134430720, + "averageProcessCpuPercent": 7.822167239740701, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 3323.887, + "processRealSeconds": 3.47, + "processUserSeconds": 0.21, + "processSystemSeconds": 0.05 + }, + { + "trial": 9, + "position": "second", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-09-second-convex_flutter.json", + "timeFile": "profile-trial-09-second-convex_flutter.time", + "remoteConvergenceMs": 7.509, + "reconnectToLiveMs": 38.532, + "authFreshTokenAcceptedMs": 171.027, + "authRecoveryMs": 529.07, + "maxRssBytes": 141475840, + "averageProcessCpuPercent": 14.266689690152097, + "bytesSent": 420054, + "bytesReceived": 1277157, + "transferredBytes": 1697211, + "runnerWallClockMs": 2032.707, + "processRealSeconds": 2.17, + "processUserSeconds": 0.23, + "processSystemSeconds": 0.06 + }, + { + "trial": 10, + "position": "first", + "adapter": "convex_flutter", + "status": "passed", + "reportFile": "profile-trial-10-first-convex_flutter.json", + "timeFile": "profile-trial-10-first-convex_flutter.time", + "remoteConvergenceMs": 7.436, + "reconnectToLiveMs": 93.847, + "authFreshTokenAcceptedMs": 150.843, + "authRecoveryMs": 359.654, + "maxRssBytes": 141492224, + "averageProcessCpuPercent": 14.42817781904695, + "bytesSent": 420054, + "bytesReceived": 1237072, + "transferredBytes": 1657126, + "runnerWallClockMs": 1940.647, + "processRealSeconds": 2.08, + "processUserSeconds": 0.23, + "processSystemSeconds": 0.05 + }, + { + "trial": 10, + "position": "second", + "adapter": "dartvex", + "status": "passed", + "reportFile": "profile-trial-10-second-dartvex.json", + "timeFile": "profile-trial-10-second-dartvex.time", + "remoteConvergenceMs": 7.138, + "reconnectToLiveMs": 142.599, + "authFreshTokenAcceptedMs": 9.493, + "authRecoveryMs": 166.973, + "maxRssBytes": 134201344, + "averageProcessCpuPercent": 9.194313353967823, + "bytesSent": 420054, + "bytesReceived": 1018947, + "transferredBytes": 1439001, + "runnerWallClockMs": 2719.072, + "processRealSeconds": 2.86, + "processUserSeconds": 0.2, + "processSystemSeconds": 0.05 + } + ] +} diff --git a/tool/convex_client_gauntlet/runtime/tool/summarize_paired_profile.dart b/tool/convex_client_gauntlet/runtime/tool/summarize_paired_profile.dart new file mode 100644 index 00000000..f1732c81 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/tool/summarize_paired_profile.dart @@ -0,0 +1,210 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; + +void main(List args) { + if (args.length != 4) { + stderr.writeln( + 'Usage: dart run tool/summarize_paired_profile.dart ' + ' ' + '', + ); + exitCode = 64; + return; + } + + final rawDirectory = Directory(args[0]); + final order = File( + '${rawDirectory.path}/order.tsv', + ).readAsLinesSync().skip(1).where((line) => line.trim().isNotEmpty); + final samples = >[]; + + for (final line in order) { + final [trialText, position, adapter, reportName, timeName] = line.split( + '\t', + ); + final report = + jsonDecode(File('${rawDirectory.path}/$reportName').readAsStringSync()) + as Map; + final seed = + (report['seeds'] as List).single as Map; + final auth = seed['auth'] as Map; + final timing = _parseTime( + File('${rawDirectory.path}/$timeName').readAsStringSync(), + ); + final runnerSeconds = (report['wallClockMs'] as num).toDouble() / 1000; + final cpuPercent = + 100 * (timing.userSeconds + timing.systemSeconds) / runnerSeconds; + + samples.add({ + 'trial': int.parse(trialText), + 'position': position, + 'adapter': adapter, + 'status': report['status'], + 'reportFile': reportName, + 'timeFile': timeName, + 'remoteConvergenceMs': seed['remoteConvergenceMs'], + 'reconnectToLiveMs': seed['reconnectToLiveMs'], + 'authFreshTokenAcceptedMs': auth['freshTokenAcceptedMs'], + 'authRecoveryMs': auth['recoveryMs'], + 'maxRssBytes': timing.maxRssBytes, + 'averageProcessCpuPercent': cpuPercent, + 'bytesSent': report['bytesSent'], + 'bytesReceived': report['bytesReceived'], + 'transferredBytes': + (report['bytesSent'] as int) + (report['bytesReceived'] as int), + 'runnerWallClockMs': report['wallClockMs'], + 'processRealSeconds': timing.realSeconds, + 'processUserSeconds': timing.userSeconds, + 'processSystemSeconds': timing.systemSeconds, + }); + } + + final commits = samples + .map( + (sample) => + jsonDecode( + File( + '${rawDirectory.path}/${sample['reportFile']}', + ).readAsStringSync(), + ) + as Map, + ) + .map((report) => report['gitCommit']) + .toSet(); + final firstReport = + jsonDecode( + File( + '${rawDirectory.path}/${samples.first['reportFile']}', + ).readAsStringSync(), + ) + as Map; + + final output = { + 'schemaVersion': 1, + 'status': samples.every((sample) => sample['status'] == 'passed') + ? 'passed' + : 'failed', + 'gitCommit': commits.length == 1 ? commits.single : commits.toList(), + 'deployment': firstReport['deployment'], + 'trialCountPerAdapter': samples.length ~/ 2, + 'pairing': 'alternating first position by trial', + 'profileMode': 'Flutter macOS profile build', + 'cpuDefinition': + '(process user seconds + system seconds) / runner wall-clock seconds', + 'transferDefinition': + 'application JSON bytes recorded by the neutral transport; excludes ' + 'WebSocket framing and protocol metadata', + 'percentileDefinition': 'nearest-rank p95', + 'machine': firstReport['machine'], + 'toolVersions': { + 'flutter': '3.41.1 stable', + 'dart': '3.11.0 macos_arm64', + 'rustc': '1.90.0 stable', + 'node': '23.11.0', + 'convexCli': '1.45.0', + 'dartvex': '0.2.0', + 'convexFlutter': '3.0.1 + Icarus patch', + 'convexRust': '0.10.4 + Icarus patch', + 'flutterRustBridge': '2.11.1 pinned', + }, + 'desktopTargets': { + 'packageSupported': ['macos', 'windows', 'linux'], + 'measured': ['macos (universal arm64 + x86_64 build, arm64 host run)'], + 'notMeasured': [ + 'windows (requires Windows host)', + 'linux (requires Linux host)', + ], + }, + 'buildSize': { + 'sharedHarnessBundleBytes': int.parse(args[1]), + 'convexFlutterNativeFrameworkExecutableBytes': int.parse(args[2]), + 'sharedAppFrameworkExecutableBytes': int.parse(args[3]), + 'candidateIsolatedBundleBytes': null, + 'note': + 'The same harness bundle contains both adapters; only the native ' + 'convex_flutter framework is candidate-specific.', + }, + 'summary': { + for (final adapter in ['dartvex', 'convex_flutter']) + adapter: _summary( + samples.where((sample) => sample['adapter'] == adapter).toList(), + ), + }, + 'samples': samples, + }; + + stdout.writeln(const JsonEncoder.withIndent(' ').convert(output)); +} + +Map _summary(List> samples) { + const fields = [ + 'remoteConvergenceMs', + 'reconnectToLiveMs', + 'authFreshTokenAcceptedMs', + 'authRecoveryMs', + 'maxRssBytes', + 'averageProcessCpuPercent', + 'bytesSent', + 'bytesReceived', + 'transferredBytes', + 'runnerWallClockMs', + ]; + return { + for (final field in fields) + field: _statistics( + samples.map((sample) => (sample[field] as num).toDouble()).toList(), + ), + }; +} + +Map _statistics(List values) { + final sorted = [...values]..sort(); + final middle = sorted.length ~/ 2; + final median = sorted.length.isOdd + ? sorted[middle] + : (sorted[middle - 1] + sorted[middle]) / 2; + final p95Index = math.max(0, (0.95 * sorted.length).ceil() - 1); + return { + 'median': median, + 'p95': sorted[p95Index], + 'min': sorted.first, + 'max': sorted.last, + 'values': values, + }; +} + +_ProcessTiming _parseTime(String raw) { + double value(String label) => double.parse( + RegExp( + '^$label ([0-9.]+)\\s*\$', + multiLine: true, + ).firstMatch(raw)!.group(1)!, + ); + final rss = int.parse( + RegExp( + r'^\s*([0-9]+)\s+maximum resident set size\s*$', + multiLine: true, + ).firstMatch(raw)!.group(1)!, + ); + return _ProcessTiming( + realSeconds: value('real'), + userSeconds: value('user'), + systemSeconds: value('sys'), + maxRssBytes: rss, + ); +} + +final class _ProcessTiming { + const _ProcessTiming({ + required this.realSeconds, + required this.userSeconds, + required this.systemSeconds, + required this.maxRssBytes, + }); + + final double realSeconds; + final double userSeconds; + final double systemSeconds; + final int maxRssBytes; +} diff --git a/tsconfig.json b/tsconfig.json index be3d138c..ec8d8ff8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,6 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false - } + }, + "exclude": ["node_modules", "third_party"] }

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 Y(u>Xya-S0St~0fdBvi literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/test/widget_test.dart b/third_party/convex_flutter/example/test/widget_test.dart new file mode 100644 index 00000000..9266deaa --- /dev/null +++ b/third_party/convex_flutter/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:convex_flutter_example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/third_party/convex_flutter/example/web/favicon.png b/third_party/convex_flutter/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/web/icons/Icon-192.png b/third_party/convex_flutter/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/web/icons/Icon-512.png b/third_party/convex_flutter/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/web/icons/Icon-maskable-192.png b/third_party/convex_flutter/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/web/icons/Icon-maskable-512.png b/third_party/convex_flutter/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/web/index.html b/third_party/convex_flutter/example/web/index.html new file mode 100644 index 00000000..f01c73e0 --- /dev/null +++ b/third_party/convex_flutter/example/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + convex_flutter_example + + + + + + diff --git a/third_party/convex_flutter/example/web/manifest.json b/third_party/convex_flutter/example/web/manifest.json new file mode 100644 index 00000000..b6c15434 --- /dev/null +++ b/third_party/convex_flutter/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "convex_flutter_example", + "short_name": "convex_flutter_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/third_party/convex_flutter/example/windows/CMakeLists.txt b/third_party/convex_flutter/example/windows/CMakeLists.txt new file mode 100644 index 00000000..110b0373 --- /dev/null +++ b/third_party/convex_flutter/example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(convex_flutter_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "convex_flutter_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/third_party/convex_flutter/example/windows/flutter/CMakeLists.txt b/third_party/convex_flutter/example/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/third_party/convex_flutter/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.cc b/third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..8b6d4680 --- /dev/null +++ b/third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void RegisterPlugins(flutter::PluginRegistry* registry) { +} diff --git a/third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.h b/third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/third_party/convex_flutter/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/third_party/convex_flutter/example/windows/flutter/generated_plugins.cmake b/third_party/convex_flutter/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..06a20a90 --- /dev/null +++ b/third_party/convex_flutter/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + convex_flutter +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/third_party/convex_flutter/example/windows/runner/CMakeLists.txt b/third_party/convex_flutter/example/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/third_party/convex_flutter/example/windows/runner/Runner.rc b/third_party/convex_flutter/example/windows/runner/Runner.rc new file mode 100644 index 00000000..4bb12308 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "convex_flutter_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "convex_flutter_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "convex_flutter_example.exe" "\0" + VALUE "ProductName", "convex_flutter_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/third_party/convex_flutter/example/windows/runner/flutter_window.cpp b/third_party/convex_flutter/example/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/third_party/convex_flutter/example/windows/runner/flutter_window.h b/third_party/convex_flutter/example/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/third_party/convex_flutter/example/windows/runner/main.cpp b/third_party/convex_flutter/example/windows/runner/main.cpp new file mode 100644 index 00000000..8197cad9 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"convex_flutter_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/third_party/convex_flutter/example/windows/runner/resource.h b/third_party/convex_flutter/example/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/third_party/convex_flutter/example/windows/runner/resources/app_icon.ico b/third_party/convex_flutter/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|8^_Ub8Vp#`BLl3lbZ zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB} zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn& ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI& z($uy?c5-+cPnt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1= z#4BPMkFG1Y)x}Ui+WXr?Z!w!t_hjRq8qTaWpu}FH{MsHlU{>;08goVLm{V<&`itk~ zE_Ys=D(hjiy+5=?=$HGii=Y5)jMe9|wWoD_K07(}edAxh`~LBorOJ!Cf@f{_gNCC| z%{*04ViE!#>@hc1t5bb+NO>ncf@@Dv01K!NxH$3Eg1%)|wLyMDF8^d44lV!_Sr}iEWefOaL z8f?ud3Q%Sen39u|%00W<#!E=-RpGa+H8}{ulxVl4mwpjaU+%2pzmi{3HM)%8vb*~-M9rPUAfGCSos8GUXp02|o~0BTV2l#`>>aFV&_P$ejS;nGwSVP8 zMbOaG7<7eKD>c12VdGH;?2@q7535sa7MN*L@&!m?L`ASG%boY7(&L5imY#EQ$KrBB z4@_tfP5m50(T--qv1BJcD&aiH#b-QC>8#7Fx@3yXlonJI#aEIi=8&ChiVpc#N=5le zM*?rDIdcpawoc5kizv$GEjnveyrp3sY>+5_R5;>`>erS%JolimF=A^EIsAK zsPoVyyUHCgf0aYr&alx`<)eb6Be$m&`JYSuBu=p8j%QlNNp$-5C{b4#RubPb|CAIS zGE=9OFLP7?Hgc{?k45)84biT0k&-C6C%Q}aI~q<(7BL`C#<6HyxaR%!dFx7*o^laG z=!GBF^cwK$IA(sn9y6>60Rw{mYRYkp%$jH z*xQM~+bp)G$_RhtFPYx2HTsWk80+p(uqv9@I9)y{b$7NK53rYL$ezbmRjdXS?V}fj zWxX_feWoLFNm3MG7pMUuFPs$qrQWO9!l2B(SIuy2}S|lHNbHzoE+M2|Zxhjq9+Ws8c{*}x^VAib7SbxJ*Q3EnY5lgI9 z=U^f3IW6T=TWaVj+2N%K3<%Un;CF(wUp`TC&Y|ZjyFu6co^uqDDB#EP?DV5v_dw~E zIRK*BoY9y-G_ToU2V_XCX4nJ32~`czdjT!zwme zGgJ0nOk3U4@IE5JwtM}pwimLjk{ln^*4HMU%Fl4~n(cnsLB}Ja-jUM>xIB%aY;Nq8 z)Fp8dv1tkqKanv<68o@cN|%thj$+f;zGSO7H#b+eMAV8xH$hLggtt?O?;oYEgbq@= zV(u9bbd12^%;?nyk6&$GPI%|+<_mEpJGNfl*`!KV;VfmZWw{n{rnZ51?}FDh8we_L z8OI9nE31skDqJ5Oa_ybn7|5@ui>aC`s34p4ZEu6-s!%{uU45$Zd1=p$^^dZBh zu<*pDDPLW+c>iWO$&Z_*{VSQKg7=YEpS3PssPn1U!lSm6eZIho*{@&20e4Y_lRklKDTUCKI%o4Pc<|G^Xgu$J^Q|B87U;`c1zGwf^-zH*VQ^x+i^OUWE0yd z;{FJq)2w!%`x7yg@>uGFFf-XJl4H`YtUG%0slGKOlXV`q?RP>AEWg#x!b{0RicxGhS!3$p7 zij;{gm!_u@D4$Ox%>>bPtLJ> zwKtYz?T_DR1jN>DkkfGU^<#6sGz|~p*I{y`aZ>^Di#TC|Z!7j_O1=Wo8thuit?WxR zh9_S>kw^{V^|g}HRUF=dcq>?q(pHxw!8rx4dC6vbQVmIhmICF#zU!HkHpQ>9S%Uo( zMw{eC+`&pb=GZRou|3;Po1}m46H6NGd$t<2mQh}kaK-WFfmj_66_17BX0|j-E2fe3Jat}ijpc53 zJV$$;PC<5aW`{*^Z6e5##^`Ed#a0nwJDT#Qq~^e8^JTA=z^Kl>La|(UQ!bI@#ge{Dzz@61p-I)kc2?ZxFt^QQ}f%ldLjO*GPj(5)V9IyuUakJX=~GnTgZ4$5!3E=V#t`yOG4U z(gphZB6u2zsj=qNFLYShhg$}lNpO`P9xOSnO*$@@UdMYES*{jJVj|9z-}F^riksLK zbsU+4-{281P9e2UjY6tse^&a)WM1MFw;p#_dHhWI7p&U*9TR0zKdVuQed%6{otTsq z$f~S!;wg#Bd9kez=Br{m|66Wv z#g1xMup<0)H;c2ZO6su_ii&m8j&+jJz4iKnGZ&wxoQX|5a>v&_e#6WA!MB_4asTxLRGQCC5cI(em z%$ZfeqP>!*q5kU>a+BO&ln=4Jm>Ef(QE8o&RgLkk%2}4Tf}U%IFP&uS7}&|Q-)`5< z+e>;s#4cJ-z%&-^&!xsYx777Wt(wZY9(3(avmr|gRe4cD+a8&!LY`1^T?7x{E<=kdY9NYw>A;FtTvQ=Y&1M%lyZPl$ss1oY^Sl8we}n}Aob#6 zl4jERwnt9BlSoWb@3HxYgga(752Vu6Y)k4yk9u~Kw>cA5&LHcrvn1Y-HoIuFWg~}4 zEw4bR`mXZQIyOAzo)FYqg?$5W<;^+XX%Uz61{-L6@eP|lLH%|w?g=rFc;OvEW;^qh z&iYXGhVt(G-q<+_j}CTbPS_=K>RKN0&;dubh0NxJyDOHFF;<1k!{k#7b{|Qok9hac z;gHz}6>H6C6RnB`Tt#oaSrX0p-j-oRJ;_WvS-qS--P*8}V943RT6kou-G=A+7QPGQ z!ze^UGxtW3FC0$|(lY9^L!Lx^?Q8cny(rR`es5U;-xBhphF%_WNu|aO<+e9%6LuZq zt(0PoagJG<%hyuf;te}n+qIl_Ej;czWdc{LX^pS>77s9t*2b4s5dvP_!L^3cwlc)E!(!kGrg~FescVT zZCLeua3f4;d;Tk4iXzt}g}O@nlK3?_o91_~@UMIl?@77Qc$IAlLE95#Z=TES>2E%z zxUKpK{_HvGF;5%Q7n&vA?`{%8ohlYT_?(3A$cZSi)MvIJygXD}TS-3UwyUxGLGiJP znblO~G|*uA^|ac8E-w#}uBtg|s_~s&t>-g0X%zIZ@;o_wNMr_;{KDg^O=rg`fhDZu zFp(VKd1Edj%F zWHPl+)FGj%J1BO3bOHVfH^3d1F{)*PL&sRX`~(-Zy3&9UQX)Z;c51tvaI2E*E7!)q zcz|{vpK7bjxix(k&6=OEIBJC!9lTkUbgg?4-yE{9+pFS)$Ar@vrIf`D0Bnsed(Cf? zObt2CJ>BKOl>q8PyFO6w)+6Iz`LW%T5^R`U_NIW0r1dWv6OY=TVF?N=EfA(k(~7VBW(S;Tu5m4Lg8emDG-(mOSSs=M9Q&N8jc^Y4&9RqIsk(yO_P(mcCr}rCs%1MW1VBrn=0-oQN(Xj!k%iKV zb%ricBF3G4S1;+8lzg5PbZ|$Se$)I=PwiK=cDpHYdov2QO1_a-*dL4KUi|g&oh>(* zq$<`dQ^fat`+VW?m)?_KLn&mp^-@d=&7yGDt<=XwZZC=1scwxO2^RRI7n@g-1o8ps z)&+et_~)vr8aIF1VY1Qrq~Xe``KJrQSnAZ{CSq3yP;V*JC;mmCT6oRLSs7=GA?@6g zUooM}@tKtx(^|aKK8vbaHlUQqwE0}>j&~YlN3H#vKGm@u)xxS?n9XrOWUfCRa< z`20Fld2f&;gg7zpo{Adh+mqNntMc-D$N^yWZAZRI+u1T1zWHPxk{+?vcS1D>08>@6 zLhE@`gt1Y9mAK6Z4p|u(5I%EkfU7rKFSM=E4?VG9tI;a*@?6!ey{lzN5=Y-!$WFSe z&2dtO>^0@V4WRc#L&P%R(?@KfSblMS+N+?xUN$u3K4Ys%OmEh+tq}fnU}i>6YHM?< zlnL2gl~sF!j!Y4E;j3eIU-lfa`RsOL*Tt<%EFC0gPzoHfNWAfKFIKZN8}w~(Yi~=q z>=VNLO2|CjkxP}RkutxjV#4fWYR1KNrPYq5ha9Wl+u>ipsk*I(HS@iLnmGH9MFlTU zaFZ*KSR0px>o+pL7BbhB2EC1%PJ{67_ z#kY&#O4@P=OV#-79y_W>Gv2dxL*@G7%LksNSqgId9v;2xJ zrh8uR!F-eU$NMx@S*+sk=C~Dxr9Qn7TfWnTupuHKuQ$;gGiBcU>GF5sWx(~4IP3`f zWE;YFO*?jGwYh%C3X<>RKHC-DZ!*r;cIr}GLOno^3U4tFSSoJp%oHPiSa%nh=Zgn% z14+8v@ygy0>UgEN1bczD6wK45%M>psM)y^)IfG*>3ItX|TzV*0i%@>L(VN!zdKb8S?Qf7BhjNpziA zR}?={-eu>9JDcl*R=OP9B8N$IcCETXah9SUDhr{yrld{G;PnCWRsPD7!eOOFBTWUQ=LrA_~)mFf&!zJX!Oc-_=kT<}m|K52 z)M=G#;p;Rdb@~h5D{q^K;^fX-m5V}L%!wVC2iZ1uu401Ll}#rocTeK|7FAeBRhNdQ zCc2d^aQnQp=MpOmak60N$OgS}a;p(l9CL`o4r(e-nN}mQ?M&isv-P&d$!8|1D1I(3-z!wi zTgoo)*Mv`gC?~bm?S|@}I|m-E2yqPEvYybiD5azInexpK8?9q*$9Yy9-t%5jU8~ym zgZDx>!@ujQ=|HJnwp^wv-FdD{RtzO9SnyfB{mH_(c!jHL*$>0o-(h(eqe*ZwF6Lvu z{7rkk%PEqaA>o+f{H02tzZ@TWy&su?VNw43! z-X+rN`6llvpUms3ZiSt)JMeztB~>9{J8SPmYs&qohxdYFi!ra8KR$35Zp9oR)eFC4 zE;P31#3V)n`w$fZ|4X-|%MX`xZDM~gJyl2W;O$H25*=+1S#%|53>|LyH za@yh+;325%Gq3;J&a)?%7X%t@WXcWL*BaaR*7UEZad4I8iDt7^R_Fd`XeUo256;sAo2F!HcIQKk;h})QxEsPE5BcKc7WyerTchgKmrfRX z!x#H_%cL#B9TWAqkA4I$R^8{%do3Y*&(;WFmJ zU7Dih{t1<{($VtJRl9|&EB?|cJ)xse!;}>6mSO$o5XIx@V|AA8ZcoD88ZM?C*;{|f zZVmf94_l1OmaICt`2sTyG!$^UeTHx9YuUP!omj(r|7zpm5475|yXI=rR>>fteLI+| z)MoiGho0oEt=*J(;?VY0QzwCqw@cVm?d7Y!z0A@u#H?sCJ*ecvyhj& z-F77lO;SH^dmf?L>3i>?Z*U}Em4ZYV_CjgfvzYsRZ+1B!Uo6H6mbS<-FFL`ytqvb& zE7+)2ahv-~dz(Hs+f})z{*4|{)b=2!RZK;PWwOnO=hG7xG`JU5>bAvUbdYd_CjvtHBHgtGdlO+s^9ca^Bv3`t@VRX2_AD$Ckg36OcQRF zXD6QtGfHdw*hx~V(MV-;;ZZF#dJ-piEF+s27z4X1qi5$!o~xBnvf=uopcn7ftfsZc zy@(PuOk`4GL_n(H9(E2)VUjqRCk9kR?w)v@xO6Jm_Mx})&WGEl=GS0#)0FAq^J*o! zAClhvoTsNP*-b~rN{8Yym3g{01}Ep^^Omf=SKqvN?{Q*C4HNNAcrowIa^mf+3PRy! z*_G-|3i8a;+q;iP@~Of_$(vtFkB8yOyWt2*K)vAn9El>=D;A$CEx6b*XF@4y_6M+2 zpeW`RHoI_p(B{%(&jTHI->hmNmZjHUj<@;7w0mx3&koy!2$@cfX{sN19Y}euYJFn& z1?)+?HCkD0MRI$~uB2UWri})0bru_B;klFdwsLc!ne4YUE;t41JqfG# zZJq6%vbsdx!wYeE<~?>o4V`A3?lN%MnKQ`z=uUivQN^vzJ|C;sdQ37Qn?;lpzg})y z)_2~rUdH}zNwX;Tp0tJ78+&I=IwOQ-fl30R79O8@?Ub8IIA(6I`yHn%lARVL`%b8+ z4$8D-|MZZWxc_)vu6@VZN!HsI$*2NOV&uMxBNzIbRgy%ob_ zhwEH{J9r$!dEix9XM7n&c{S(h>nGm?el;gaX0@|QnzFD@bne`el^CO$yXC?BDJ|Qg z+y$GRoR`?ST1z^e*>;!IS@5Ovb7*RlN>BV_UC!7E_F;N#ky%1J{+iixp(dUJj93aK zzHNN>R-oN7>kykHClPnoPTIj7zc6KM(Pnlb(|s??)SMb)4!sMHU^-ntJwY5Big7xv zb1Ew`Xj;|D2kzGja*C$eS44(d&RMU~c_Y14V9_TLTz0J#uHlsx`S6{nhsA0dWZ#cG zJ?`fO50E>*X4TQLv#nl%3GOk*UkAgt=IY+u0LNXqeln3Z zv$~&Li`ZJOKkFuS)dJRA>)b_Da%Q~axwA_8zNK{BH{#}#m}zGcuckz}riDE-z_Ms> zR8-EqAMcfyGJCtvTpaUVQtajhUS%c@Yj}&6Zz;-M7MZzqv3kA7{SuW$oW#=0az2wQ zg-WG@Vb4|D`pl~Il54N7Hmsauc_ne-a!o5#j3WaBBh@Wuefb!QJIOn5;d)%A#s+5% zuD$H=VNux9bE-}1&bcYGZ+>1Fo;3Z@e&zX^n!?JK*adSbONm$XW9z;Q^L>9U!}Toj2WdafJ%oL#h|yWWwyAGxzfrAWdDTtaKl zK4`5tDpPg5>z$MNv=X0LZ0d6l%D{(D8oT@+w0?ce$DZ6pv>{1&Ok67Ix1 zH}3=IEhPJEhItCC8E=`T`N5(k?G=B4+xzZ?<4!~ ze~z6Wk9!CHTI(0rLJ4{JU?E-puc;xusR?>G?;4vt;q~iI9=kDL=z0Rr%O$vU`30X$ zDZRFyZ`(omOy@u|i6h;wtJlP;+}$|Ak|k2dea7n?U1*$T!sXqqOjq^NxLPMmk~&qI zYg0W?yK8T(6+Ea+$YyspKK?kP$+B`~t3^Pib_`!6xCs32!i@pqXfFV6PmBIR<-QW= zN8L{pt0Vap0x`Gzn#E@zh@H)0FfVfA_Iu4fjYZ+umO1LXIbVc$pY+E234u)ttcrl$ z>s92z4vT%n6cMb>=XT6;l0+9e(|CZG)$@C7t7Z7Ez@a)h)!hyuV&B5K%%)P5?Lk|C zZZSVzdXp{@OXSP0hoU-gF8s8Um(#xzjP2Vem zec#-^JqTa&Y#QJ>-FBxd7tf`XB6e^JPUgagB8iBSEps;92KG`!#mvVcPQ5yNC-GEG zTiHEDYfH+0O15}r^+ z#jxj=@x8iNHWALe!P3R67TwmhItn**0JwnzSV2O&KE8KcT+0hWH^OPD1pwiuyx=b@ zNf5Jh0{9X)8;~Es)$t@%(3!OnbY+`@?i{mGX7Yy}8T_*0a6g;kaFPq;*=px5EhO{Cp%1kI<0?*|h8v!6WnO3cCJRF2-CRrU3JiLJnj@6;L)!0kWYAc_}F{2P))3HmCrz zQ&N&gE70;`!6*eJ4^1IR{f6j4(-l&X!tjHxkbHA^Zhrnhr9g{exN|xrS`5Pq=#Xf& zG%P=#ra-TyVFfgW%cZo5OSIwFL9WtXAlFOa+ubmI5t*3=g#Y zF%;70p5;{ZeFL}&}yOY1N1*Q;*<(kTB!7vM$QokF)yr2FlIU@$Ph58$Bz z0J?xQG=MlS4L6jA22eS42g|9*9pX@$#*sUeM(z+t?hr@r5J&D1rx}2pW&m*_`VDCW zUYY@v-;bAO0HqoAgbbiGGC<=ryf96}3pouhy3XJrX+!!u*O_>Si38V{uJmQ&USptX zKp#l(?>%^7;2%h(q@YWS#9;a!JhKlkR#Vd)ERILlgu!Hr@jA@V;sk4BJ-H#p*4EqC zDGjC*tl=@3Oi6)Bn^QwFpul18fpkbpg0+peH$xyPBqb%`$OUhPKyWb32o7clB*9Z< zN=i~NLjavrLtwgJ01bufP+>p-jR2I95|TpmKpQL2!oV>g(4RvS2pK4*ou%m(h6r3A zX#s&`9LU1ZG&;{CkOK!4fLDTnBys`M!vuz>Q&9OZ0hGQl!~!jSDg|~s*w52opC{sB ze|Cf2luD(*G13LcOAGA!s2FjSK8&IE5#W%J25w!vM0^VyQM!t)inj&RTiJ!wXzFgz z3^IqzB7I0L$llljsGq})thBy9UOyjtFO_*hYM_sgcMk>44jeH0V1FDyELc{S1F-;A zS;T^k^~4biG&V*Irq}O;e}j$$+E_#G?HKIn05iP3j|87TkGK~SqG!-KBg5+mN(aLm z8ybhIM`%C19UX$H$KY6JgXbY$0AT%rEpHC;u`rQ$Y=rxUdsc5*Kvc8jaYaO$^)cI6){P6K0r)I6DY4Wr4&B zLQUBraey#0HV|&c4v7PVo3n$zHj99(TZO^3?Ly%C4nYvJTL9eLBLHsM3WKKD>5!B` zQ=BsR3aR6PD(Fa>327E2HAu5TM~Wusc!)>~(gM)+3~m;92Jd;FnSib=M5d6;;5{%R zb4V7DEJ0V!CP-F*oU?gkc>ksUtAYP&V4ND5J>J2^jt*vcFflQWCrB&fLdT%O59PVJ zhid#toR=FNgD!q3&r8#wEBr`!wzvQu5zX?Q>nlSJ4i@WC*CN*-xU66F^V5crWevQ9gsq$I@z1o(a=k7LL~ z7m_~`o;_Ozha1$8Q}{WBehvAlO4EL60y5}8GDrZ< zXh&F}71JbW2A~8KfEWj&UWV#4+Z4p`b{uAj4&WC zha`}X@3~+Iz^WRlOHU&KngK>#j}+_o@LdBC1H-`gT+krWX3-;!)6?{FBp~%20a}FL zFP9%Emqcwa#(`=G>BBZ0qZDQhmZKJg_g8<=bBFKWr!dyg(YkpE+|R*SGpDVU!+VlU zFC54^DLv}`qa%49T>nNiA9Q7Ips#!Xx90tCU2gvK`(F+GPcL=J^>No{)~we#o@&mUb6c$ zCc*<|NJBk-#+{j9xkQ&ujB zI~`#kN~7W!f*-}wkG~Ld!JqZ@tK}eeSnsS5J1fMFXm|`LJx&}5`@dK3W^7#Wnm+_P zBZkp&j1fa2Y=eIjJ0}gh85jt43kaIXXv?xmo@eHrka!Z|vQv12HN#+!I5E z`(fbuW>gFiJL|uXJ!vKt#z3e3HlVdboH7;e#i3(2<)Fg-I@BR!qY#eof3MFZ&*Y@l zI|KJf&ge@p2Dq09Vu$$Qxb7!}{m-iRk@!)%KL)txi3;~Z4Pb}u@GsW;ELiWeG9V51 znX#}B&4Y2E7-H=OpNE@q{%hFLxwIpBF2t{vPREa8_{linXT;#1vMRWjOzLOP$-hf( z>=?$0;~~PnkqY;~K{EM6Vo-T(0K{A0}VUGmu*hR z{tw3hvBN%N3G3Yw`X5Te+F{J`(3w1s3-+1EbnFQKcrgrX1Jqvs@ADGe%M0s$EbK$$ zK)=y=upBc6SjGYAACCcI=Y*6Fi8_jgwZlLxD26fnQfJmb8^gHRN5(TemhX@0e=vr> zg`W}6U>x6VhoA3DqsGGD9uL1DhB3!OXO=k}59TqD@(0Nb{)Ut_luTioK_>7wjc!5C zIr@w}b`Fez3)0wQfKl&bae7;PcTA7%?f2xucM0G)wt_KO!Ewx>F~;=BI0j=Fb4>pp zv}0R^xM4eti~+^+gE$6b81p(kwzuDti(-K9bc|?+pJEl@H+jSYuxZQV8rl8 zjp@M{#%qItIUFN~KcO9Hed*`$5A-2~pAo~K&<-Q+`9`$CK>rzqAI4w~$F%vs9s{~x zg4BP%Gy*@m?;D6=SRX?888Q6peF@_4Z->8wAH~Cn!R$|Hhq2cIzFYqT_+cDourHbY z0qroxJnrZ4Gh+Ay+F`_c%+KRT>y3qw{)89?=hJ@=KO=@ep)aBJ$c!JHfBMJpsP*3G za7|)VJJ8B;4?n{~ldJF7%jmb`-ftIvNd~ekoufG(`K(3=LNc;HBY& z(lp#q8XAD#cIf}k49zX_i`*fO+#!zKA&%T3j@%)R+#yag067CU%yUEe47>wzGU8^` z1EXFT^@I!{J!F8!X?S6ph8J=gUi5tl93*W>7}_uR<2N2~e}FaG?}KPyugQ=-OGEZs z!GBoyYY+H*ANn4?Z)X4l+7H%`17i5~zRlRIX?t)6_eu=g2Q`3WBhxSUeea+M-S?RL zX9oBGKn%a!H+*hx4d2(I!gsi+@SQK%<{X22M~2tMulJoa)0*+z9=-YO+;DFEm5eE1U9b^B(Z}2^9!Qk`!A$wUE z7$Ar5?NRg2&G!AZqnmE64eh^Anss3i!{}%6@Et+4rr!=}!SBF8eZ2*J3ujCWbl;3; z48H~goPSv(8X61fKKdpP!Z7$88NL^Z?j`!^*I?-P4X^pMxyWz~@$(UeAcTSDd(`vO z{~rc;9|GfMJcApU3k}22a!&)k4{CU!e_ny^Y3cO;tOvOMKEyWz!vG(Kp*;hB?d|R3`2X~=5a6#^o5@qn?J-bI8Ppip{-yG z!k|VcGsq!jF~}7DMr49Wap-s&>o=U^T0!Lcy}!(bhtYsPQy z4|EJe{12QL#=c(suQ89Mhw9<`bui%nx7Nep`C&*M3~vMEACmcRYYRGtANq$F%zh&V zc)cEVeHz*Z1N)L7k-(k3np#{GcDh2Q@ya0YHl*n7fl*ZPAsbU-a94MYYtA#&!c`xGIaV;yzsmrjfieTEtqB_WgZp2*NplHx=$O{M~2#i_vJ{ps-NgK zQsxKK_CBM2PP_je+Xft`(vYfXXgIUr{=PA=7a8`2EHk)Ym2QKIforz# tySWtj{oF3N9@_;i*Fv5S)9x^z=nlWP>jpp-9)52ZmLVA=i*%6g{{fxOO~wEK literal 0 HcmV?d00001 diff --git a/third_party/convex_flutter/example/windows/runner/runner.exe.manifest b/third_party/convex_flutter/example/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/third_party/convex_flutter/example/windows/runner/utils.cpp b/third_party/convex_flutter/example/windows/runner/utils.cpp new file mode 100644 index 00000000..3a0b4651 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/third_party/convex_flutter/example/windows/runner/utils.h b/third_party/convex_flutter/example/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/third_party/convex_flutter/example/windows/runner/win32_window.cpp b/third_party/convex_flutter/example/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/third_party/convex_flutter/example/windows/runner/win32_window.h b/third_party/convex_flutter/example/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/third_party/convex_flutter/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/third_party/convex_flutter/flutter_rust_bridge.yaml b/third_party/convex_flutter/flutter_rust_bridge.yaml new file mode 100644 index 00000000..9de311bb --- /dev/null +++ b/third_party/convex_flutter/flutter_rust_bridge.yaml @@ -0,0 +1,3 @@ +rust_input: crate +rust_root: rust/ +dart_output: lib/src/rust diff --git a/third_party/convex_flutter/ios/Classes/dummy_file.c b/third_party/convex_flutter/ios/Classes/dummy_file.c new file mode 100644 index 00000000..e06dab99 --- /dev/null +++ b/third_party/convex_flutter/ios/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/third_party/convex_flutter/ios/convex_flutter.podspec b/third_party/convex_flutter/ios/convex_flutter.podspec new file mode 100644 index 00000000..a40f968e --- /dev/null +++ b/third_party/convex_flutter/ios/convex_flutter.podspec @@ -0,0 +1,45 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint convex_flutter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'convex_flutter' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust convex_flutter', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/libconvex_flutter.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libconvex_flutter.a', + } +end diff --git a/third_party/convex_flutter/lib/convex_flutter.dart b/third_party/convex_flutter/lib/convex_flutter.dart new file mode 100644 index 00000000..f88b0ad1 --- /dev/null +++ b/third_party/convex_flutter/lib/convex_flutter.dart @@ -0,0 +1,9 @@ +library; + +export 'src/rust/lib.dart'; +export 'src/rust/frb_generated.dart' show RustLib; +export 'src/convex_client.dart' + show ConvexClient, AuthHandleWrapper, TokenFetcher, AuthStateCallback; +export 'src/convex_config.dart' show ConvexConfig; +export 'src/connection_status.dart' show ConnectionStatus; +export 'src/app_lifecycle_event.dart' show AppLifecycleEvent; diff --git a/third_party/convex_flutter/lib/convex_flutter_web.dart b/third_party/convex_flutter/lib/convex_flutter_web.dart new file mode 100644 index 00000000..b5546e23 --- /dev/null +++ b/third_party/convex_flutter/lib/convex_flutter_web.dart @@ -0,0 +1,18 @@ +/// Web platform implementation of convex_flutter plugin. +/// +/// This file is automatically registered by Flutter when building for web. +library convex_flutter_web; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +/// The web implementation of [ConvexFlutterPlatform]. +/// +/// This class is automatically registered when building for web. +/// The actual web functionality is provided by [WebConvexClient]. +class ConvexFlutterWeb { + /// Factory constructor for web platform plugin registration. + static void registerWith(Registrar registrar) { + // No platform channel needed for web - we use pure Dart WebSocket implementation + // The ConvexClient automatically selects WebConvexClient when kIsWeb is true + } +} diff --git a/third_party/convex_flutter/lib/src/app_lifecycle_event.dart b/third_party/convex_flutter/lib/src/app_lifecycle_event.dart new file mode 100644 index 00000000..e22b48df --- /dev/null +++ b/third_party/convex_flutter/lib/src/app_lifecycle_event.dart @@ -0,0 +1,26 @@ +/// Enum representing Flutter app lifecycle state changes. +/// +/// These events are emitted by the ConvexClient when the app +/// transitions between different lifecycle states. +/// +/// Example usage: +/// ```dart +/// ConvexClient.instance.lifecycleEvents.listen((event) { +/// if (event == AppLifecycleEvent.resumed) { +/// print('App came to foreground'); +/// } +/// }); +/// ``` +enum AppLifecycleEvent { + /// App has come to the foreground and is visible to the user + resumed, + + /// App is in the background but still running + paused, + + /// App is inactive (e.g., during a phone call or system dialog) + inactive, + + /// App is being terminated + detached, +} diff --git a/third_party/convex_flutter/lib/src/app_lifecycle_observer.dart b/third_party/convex_flutter/lib/src/app_lifecycle_observer.dart new file mode 100644 index 00000000..5dab7e64 --- /dev/null +++ b/third_party/convex_flutter/lib/src/app_lifecycle_observer.dart @@ -0,0 +1,45 @@ +import 'package:flutter/widgets.dart'; +import 'package:convex_flutter/src/app_lifecycle_event.dart'; + +/// Observes Flutter app lifecycle state changes and emits events. +/// +/// This class uses Flutter's WidgetsBindingObserver to monitor +/// when the app transitions between foreground, background, and +/// other lifecycle states. +/// +/// The observer automatically registers itself with WidgetsBinding +/// upon creation and should be disposed when no longer needed. +class AppLifecycleObserver with WidgetsBindingObserver { + /// Callback function invoked when app lifecycle state changes + final void Function(AppLifecycleEvent) onLifecycleChange; + + /// Creates a new lifecycle observer with the specified callback. + /// + /// The observer automatically registers itself with WidgetsBinding. + AppLifecycleObserver({required this.onLifecycleChange}) { + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final event = switch (state) { + AppLifecycleState.resumed => AppLifecycleEvent.resumed, + AppLifecycleState.paused => AppLifecycleEvent.paused, + AppLifecycleState.inactive => AppLifecycleEvent.inactive, + AppLifecycleState.detached => AppLifecycleEvent.detached, + _ => null, + }; + + if (event != null) { + onLifecycleChange(event); + } + } + + /// Disposes the observer and unregisters it from WidgetsBinding. + /// + /// Call this method when the observer is no longer needed to prevent + /// memory leaks. + void dispose() { + WidgetsBinding.instance.removeObserver(this); + } +} diff --git a/third_party/convex_flutter/lib/src/connection_status.dart b/third_party/convex_flutter/lib/src/connection_status.dart new file mode 100644 index 00000000..5ab60500 --- /dev/null +++ b/third_party/convex_flutter/lib/src/connection_status.dart @@ -0,0 +1,15 @@ +/// Enum representing the possible connection states when checking +/// connectivity to the Convex backend. +enum ConnectionStatus { + /// Connection check has not been performed yet + unknown, + + /// Successfully connected to the backend + connected, + + /// Connection check timed out + timeout, + + /// An error occurred during the connection check + error, +} diff --git a/third_party/convex_flutter/lib/src/convex_client.dart b/third_party/convex_flutter/lib/src/convex_client.dart new file mode 100644 index 00000000..5aae4788 --- /dev/null +++ b/third_party/convex_flutter/lib/src/convex_client.dart @@ -0,0 +1,426 @@ +import 'dart:async'; + +import 'package:convex_flutter/src/impl/convex_client_interface.dart'; +import 'package:convex_flutter/src/impl/convex_client_factory.dart'; +import 'package:convex_flutter/src/rust/lib.dart' show WebSocketConnectionState, SubscriptionHandle, AuthHandle; +import 'package:convex_flutter/src/connection_status.dart'; +import 'package:convex_flutter/src/convex_config.dart'; +import 'package:convex_flutter/src/app_lifecycle_event.dart'; + +/// Callback type for fetching authentication tokens. +/// Should return a JWT token string, or null to sign out. +typedef TokenFetcher = Future Function(); + +/// Callback type for authentication state changes. +typedef AuthStateCallback = void Function(bool isAuthenticated); + +/// A client for interacting with a Convex backend service. +/// +/// The ConvexClient provides methods for executing queries, mutations, actions and +/// managing real-time subscriptions with a Convex backend. +/// +/// This client automatically selects the appropriate implementation based on platform: +/// - **Mobile/Desktop** (Android, iOS, macOS, Windows, Linux): Uses FFI + Rust SDK +/// - **Web**: Uses pure Dart WebSocket implementation (no Rust required) +/// +/// Example usage: +/// +/// ```dart +/// // Initialize the client +/// await ConvexClient.initialize( +/// ConvexConfig( +/// deploymentUrl: "https://my-app.convex.cloud", +/// clientId: "flutter-app-1.0", +/// ), +/// ); +/// +/// // Execute a query +/// final result = await ConvexClient.instance.query( +/// "messages:list", +/// {"limit": "10"} +/// ); +/// +/// // Subscribe to real-time updates +/// final subscription = await ConvexClient.instance.subscribe( +/// name: "messages:list", +/// args: {}, +/// onUpdate: (value) { +/// print("New messages: $value"); +/// }, +/// onError: (message, value) { +/// print("Error: $message"); +/// } +/// ); +/// +/// // Execute a mutation +/// await ConvexClient.instance.mutation( +/// name: "messages:send", +/// args: { +/// "body": "Hello!", +/// "author": "User123" +/// } +/// ); +/// +/// // Cancel subscription when done +/// subscription.cancel(); +/// ``` +class ConvexClient { + /// Private static instance for singleton pattern + static ConvexClient? _instance; + + /// The platform-specific implementation (Native or Web) + final IConvexClient _impl; + + /// Private constructor + ConvexClient._(this._impl); + + /// Public getter to access singleton instance + /// Throws StateError if accessed before initialization + static ConvexClient get instance { + if (_instance == null) { + throw StateError( + 'ConvexClient not initialized. ' + 'Call ConvexClient.initialize() first.', + ); + } + return _instance!; + } + + /// Initializes the ConvexClient singleton instance with configuration. + /// + /// This method must be called once before accessing [instance]. + /// Subsequent calls will throw a StateError. + /// + /// The client automatically selects the appropriate platform implementation: + /// - **Web**: Pure Dart WebSocket (no Rust required) + /// - **Mobile/Desktop**: FFI + Rust SDK (requires Rust toolchain for building) + /// + /// Example usage: + /// ```dart + /// await ConvexClient.initialize( + /// ConvexConfig( + /// deploymentUrl: "https://your-app.convex.cloud", + /// clientId: "flutter-app", + /// operationTimeout: Duration(seconds: 30), + /// ), + /// ); + /// ``` + static Future initialize(ConvexConfig config) async { + if (_instance != null) { + throw StateError('ConvexClient already initialized'); + } + + // Create platform-specific implementation using factory + // Factory automatically selects: + // - WebConvexClient (pure Dart) on web + // - NativeConvexClient (FFI + Rust SDK) on native platforms + final IConvexClient impl = await createPlatformClient(config); + + // Create singleton with chosen implementation + _instance = ConvexClient._(impl); + } + + /// Initializes the ConvexClient singleton instance (DEPRECATED). + /// + /// This method is deprecated. Use [initialize] with [ConvexConfig] instead. + /// + /// Example migration: + /// ```dart + /// // Old way (deprecated) + /// await ConvexClient.init(deploymentUrl: "...", clientId: "..."); + /// + /// // New way + /// await ConvexClient.initialize( + /// ConvexConfig(deploymentUrl: "...", clientId: "..."), + /// ); + /// ``` + @Deprecated('Use initialize(ConvexConfig) instead') + static Future init({ + required String deploymentUrl, + required String clientId, + }) async { + if (_instance == null) { + await initialize( + ConvexConfig( + deploymentUrl: deploymentUrl, + clientId: clientId, + ), + ); + } + return _instance!; + } + + // ============================================================================ + // Public API - All methods delegate to platform-specific implementation + // ============================================================================ + + /// Configuration for this client instance + ConvexConfig get config => _impl.config; + + /// Executes a Convex query operation with timeout. + /// + /// [name] - Name of the query function to execute (e.g., "messages:list") + /// [args] - Map of arguments to pass to the query + /// + /// Returns the query result as a JSON string. + /// Throws [TimeoutException] if the operation exceeds [config.operationTimeout]. + Future query(String name, Map args) => + _impl.query(name, args); + + /// Executes a Convex mutation operation with timeout. + /// + /// [name] - Name of the mutation function to execute + /// [args] - Map of arguments to pass to the mutation + /// + /// Returns the mutation result as a JSON string. + /// Throws [TimeoutException] if the operation exceeds [config.operationTimeout]. + Future mutation({ + required String name, + required Map args, + }) => + _impl.mutation(name: name, args: args); + + /// Executes a Convex action operation with timeout. + /// + /// [name] - Name of the action function to execute + /// [args] - Map of arguments to pass to the action + /// + /// Returns the action result as a JSON string. + /// Throws [TimeoutException] if the operation exceeds [config.operationTimeout]. + Future action({ + required String name, + required Map args, + }) => + _impl.action(name: name, args: args); + + /// Creates a real-time subscription to a Convex query. + /// + /// [name] - Name of the query function to subscribe to + /// [args] - Map of arguments for the subscription + /// [onUpdate] - Callback function called when new data arrives + /// [onError] - Callback function called when an error occurs + /// + /// Returns a handle that can be used to cancel the subscription. + Future subscribe({ + required String name, + required Map args, + required void Function(String) onUpdate, + required void Function(String, String?) onError, + }) => + _impl.subscribe( + name: name, + args: args, + onUpdate: onUpdate, + onError: onError, + ); + + // ============================================================================ + // Authentication API + // ============================================================================ + + /// Sets the authentication token for the client (simple/static). + /// + /// Use this for simple auth scenarios where you manage token refresh externally. + /// For automatic token refresh, use [setAuthWithRefresh] instead. + /// + /// [token] - The authentication token to set, or null to clear auth. + /// + /// Example usage: + /// ```dart + /// // Set auth with a token + /// await client.setAuth(token: 'eyJhbGciOiJSUzI1NiIs...'); + /// + /// // Clear auth + /// await client.setAuth(token: null); + /// ``` + Future setAuth({required String? token}) => _impl.setAuth(token: token); + + /// Sets up authentication with automatic token refresh. + /// + /// This is the recommended way to handle authentication. The [fetchToken] + /// callback will be called: + /// - Immediately to get the initial token + /// - Automatically when the token is about to expire (60 seconds before) + /// + /// Example usage: + /// ```dart + /// final authHandle = await client.setAuthWithRefresh( + /// fetchToken: () async { + /// // Get token from your auth provider (Clerk, Auth0, Firebase, etc.) + /// return await FirebaseAuth.instance.currentUser?.getIdToken(); + /// }, + /// onAuthChange: (isAuthenticated) { + /// print('Auth state changed: $isAuthenticated'); + /// }, + /// ); + /// + /// // Later, when signing out: + /// authHandle.dispose(); + /// ``` + /// + /// [fetchToken] - Async function that returns a JWT token, or null to sign out. + /// [onAuthChange] - Optional callback invoked when auth state changes. + /// + /// Returns an [AuthHandleWrapper] that can be used to dispose the auth session. + Future setAuthWithRefresh({ + required TokenFetcher fetchToken, + AuthStateCallback? onAuthChange, + }) async { + final handle = await _impl.setAuthWithRefresh( + tokenFetcher: fetchToken, + onAuthChange: onAuthChange, + ); + return AuthHandleWrapper._(handle); + } + + /// Clears authentication and disposes any active auth refresh loop. + /// + /// This will: + /// - Stop any running token refresh loop + /// - Clear the auth token from the Convex client + /// - Emit `false` on the [authState] stream + Future clearAuth() => _impl.clearAuth(); + + /// Stream of authentication state changes. + /// Emits `true` when authenticated, `false` when not. + /// + /// Example usage: + /// ```dart + /// ConvexClient.instance.authState.listen((isAuthenticated) { + /// setState(() => _isLoggedIn = isAuthenticated); + /// }); + /// ``` + Stream get authState => _impl.authState; + + /// Current authentication state (synchronous). + /// Returns `true` if authenticated via [setAuthWithRefresh], `false` otherwise. + bool get isAuthenticated => _impl.isAuthenticated; + + // ============================================================================ + // Connection Management API + // ============================================================================ + + /// Stream of WebSocket connection state changes. + /// + /// Emits state whenever the underlying WebSocket connection changes + /// between Connected and Connecting states. This provides real-time + /// connection monitoring without manual polling. + /// + /// Example usage: + /// ```dart + /// ConvexClient.instance.connectionState.listen((state) { + /// if (state == WebSocketConnectionState.connected) { + /// print('Connected to Convex!'); + /// } + /// }); + /// ``` + Stream get connectionState => _impl.connectionState; + + /// Current WebSocket connection state (synchronous). + /// Returns the most recent state from the WebSocket connection. + WebSocketConnectionState get currentConnectionState => + _impl.currentConnectionState; + + /// Convenience getter - returns true if WebSocket is currently connected. + bool get isConnected => _impl.isConnected; + + /// Manually checks the connection status to the Convex backend. + /// + /// **DEPRECATED:** Use the [connectionState] stream for real-time state tracking. + /// This method is slower and less accurate than the WebSocket state stream. + /// + /// This method uses the [ConvexConfig.healthCheckQuery] to verify connectivity. + /// If no health check query is configured, throws a [StateError]. + /// + /// Returns [ConnectionStatus.connected] if the connection is working, + /// [ConnectionStatus.timeout] if the check times out, or + /// [ConnectionStatus.error] if an error occurs. + /// + /// Example usage (deprecated): + /// ```dart + /// final status = await ConvexClient.instance.checkConnection(); + /// if (status == ConnectionStatus.connected) { + /// print('Connected!'); + /// } + /// ``` + /// + /// Recommended alternative - use the real-time connection state stream: + /// ```dart + /// ConvexClient.instance.connectionState.listen((state) { + /// if (state == WebSocketConnectionState.connected) { + /// print('Connected!'); + /// } + /// }); + /// ``` + @Deprecated('Use connectionState stream for real-time connection monitoring') + Future checkConnection() => _impl.checkConnection(); + + /// Attempts to reconnect to the Convex backend. + /// + /// This method calls [checkConnection] and returns true if the + /// connection check succeeds, false otherwise. + /// + /// Typically called after the app resumes from background or + /// after detecting a network interruption. + /// + /// Example usage: + /// ```dart + /// ConvexClient.instance.lifecycleEvents.listen((event) { + /// if (event == AppLifecycleEvent.resumed) { + /// final connected = await ConvexClient.instance.reconnect(); + /// if (connected) { + /// print('Reconnected successfully'); + /// } + /// } + /// }); + /// ``` + Future reconnect() => _impl.reconnect(); + + // ============================================================================ + // Lifecycle Management API + // ============================================================================ + + /// Stream of app lifecycle events (foreground/background transitions). + /// + /// Emits events when the app transitions between foreground/background states. + /// Useful for handling reconnection or other lifecycle-based logic. + /// + /// Example usage: + /// ```dart + /// ConvexClient.instance.lifecycleEvents.listen((event) { + /// if (event == AppLifecycleEvent.resumed) { + /// // App came to foreground + /// ConvexClient.instance.reconnect(); + /// } + /// }); + /// ``` + Stream get lifecycleEvents => _impl.lifecycleEvents; + + // ============================================================================ + // Resource Management + // ============================================================================ + + /// Dispose the client and clean up resources. + /// + /// Call this when you're done using the client to free up resources. + /// Note: This is typically not needed as the client is a singleton, + /// but can be useful in testing scenarios. + void dispose() => _impl.dispose(); +} + +/// Wrapper for auth handle providing Dart-friendly API. +/// +/// Returned by [ConvexClient.setAuthWithRefresh] to control the auth session. +class AuthHandleWrapper { + final AuthHandle _handle; + + AuthHandleWrapper._(this._handle); + + /// Whether the user is currently authenticated. + bool get isAuthenticated => _handle.isAuthenticated(); + + /// Dispose the auth session, stopping token refresh and clearing auth. + /// + /// Call this when signing out or when you no longer need automatic token refresh. + void dispose() => _handle.dispose(); +} diff --git a/third_party/convex_flutter/lib/src/convex_config.dart b/third_party/convex_flutter/lib/src/convex_config.dart new file mode 100644 index 00000000..d48f3751 --- /dev/null +++ b/third_party/convex_flutter/lib/src/convex_config.dart @@ -0,0 +1,53 @@ +/// Configuration for ConvexClient initialization. +/// +/// This class holds all configuration options for initializing +/// the Convex client singleton. +/// +/// Example usage: +/// ```dart +/// await ConvexClient.initialize( +/// ConvexConfig( +/// deploymentUrl: "https://your-app.convex.cloud", +/// clientId: "flutter-app", +/// operationTimeout: Duration(seconds: 30), +/// healthCheckQuery: "system:ping", +/// ), +/// ); +/// ``` +class ConvexConfig { + /// The URL of your Convex deployment. + /// + /// Example: "https://my-app.convex.cloud" + final String deploymentUrl; + + /// Optional unique identifier for this client instance. + /// + /// If not provided, defaults to 'flutter-client'. + final String? clientId; + + /// Timeout duration for all query, mutation, and action operations. + /// + /// Operations that take longer than this duration will throw + /// a TimeoutException. Defaults to 30 seconds. + final Duration operationTimeout; + + /// Optional query name to use for manual connection health checks. + /// + /// This should be the name of a lightweight query in your Convex backend + /// that can be used to verify the connection is working. + /// + /// Example: "system:ping" or any query that returns quickly. + /// + /// If null, calling `ConvexClient.instance.checkConnection()` will throw + /// a StateError. You can still check connection by attempting regular + /// queries and catching TimeoutException. + final String? healthCheckQuery; + + /// Creates a new ConvexConfig with the specified options. + const ConvexConfig({ + required this.deploymentUrl, + this.clientId, + this.operationTimeout = const Duration(seconds: 30), + this.healthCheckQuery, + }); +} diff --git a/third_party/convex_flutter/lib/src/impl/convex_client_factory.dart b/third_party/convex_flutter/lib/src/impl/convex_client_factory.dart new file mode 100644 index 00000000..04e7ea12 --- /dev/null +++ b/third_party/convex_flutter/lib/src/impl/convex_client_factory.dart @@ -0,0 +1,23 @@ +/// Factory for creating platform-specific ConvexClient implementations. +/// +/// Uses conditional imports to avoid compiling web-only code on native platforms. +library convex_client_factory; + +import 'package:convex_flutter/src/convex_config.dart'; +import 'package:convex_flutter/src/impl/convex_client_interface.dart'; + +// Import appropriate implementation based on platform +import 'convex_client_factory_io.dart' + if (dart.library.js_interop) 'convex_client_factory_web.dart'; + +/// Creates the appropriate platform-specific ConvexClient implementation. +/// +/// This factory method uses conditional imports to: +/// - Return NativeConvexClient on native platforms (iOS, Android, macOS, Windows, Linux) +/// - Return WebConvexClient on web platform +/// +/// This prevents web-only libraries (dart:js_interop, package:web) from being +/// compiled into native builds, which would cause compilation errors. +Future createPlatformClient(ConvexConfig config) async { + return await createClientImpl(config); +} diff --git a/third_party/convex_flutter/lib/src/impl/convex_client_factory_io.dart b/third_party/convex_flutter/lib/src/impl/convex_client_factory_io.dart new file mode 100644 index 00000000..57252d56 --- /dev/null +++ b/third_party/convex_flutter/lib/src/impl/convex_client_factory_io.dart @@ -0,0 +1,12 @@ +/// Native platform (IO) implementation factory. +/// +/// This file is imported on iOS, Android, macOS, Windows, and Linux platforms. + +import 'package:convex_flutter/src/convex_config.dart'; +import 'package:convex_flutter/src/impl/convex_client_interface.dart'; +import 'package:convex_flutter/src/impl/convex_client_native.dart'; + +/// Creates a NativeConvexClient for native platforms. +Future createClientImpl(ConvexConfig config) async { + return await NativeConvexClient.create(config); +} diff --git a/third_party/convex_flutter/lib/src/impl/convex_client_factory_web.dart b/third_party/convex_flutter/lib/src/impl/convex_client_factory_web.dart new file mode 100644 index 00000000..58d51411 --- /dev/null +++ b/third_party/convex_flutter/lib/src/impl/convex_client_factory_web.dart @@ -0,0 +1,12 @@ +/// Web platform implementation factory. +/// +/// This file is imported only on web platform (when dart:js_interop is available). + +import 'package:convex_flutter/src/convex_config.dart'; +import 'package:convex_flutter/src/impl/convex_client_interface.dart'; +import 'package:convex_flutter/src/impl/convex_client_web.dart'; + +/// Creates a WebConvexClient for web platform. +Future createClientImpl(ConvexConfig config) async { + return await WebConvexClient.create(config); +} diff --git a/third_party/convex_flutter/lib/src/impl/convex_client_interface.dart b/third_party/convex_flutter/lib/src/impl/convex_client_interface.dart new file mode 100644 index 00000000..1b1bed41 --- /dev/null +++ b/third_party/convex_flutter/lib/src/impl/convex_client_interface.dart @@ -0,0 +1,162 @@ +import 'dart:async'; + +import 'package:convex_flutter/src/rust/lib.dart' show WebSocketConnectionState, SubscriptionHandle, AuthHandle; +import 'package:convex_flutter/src/connection_status.dart'; +import 'package:convex_flutter/src/convex_config.dart'; +import 'package:convex_flutter/src/app_lifecycle_event.dart'; + +/// Abstract interface for platform-specific Convex client implementations. +/// +/// This interface defines the contract that both native (FFI) and web (pure Dart) +/// implementations must follow, ensuring API consistency across all platforms. +/// +/// Implementations: +/// - [NativeConvexClient]: Uses Flutter Rust Bridge (FFI) to call Convex Rust SDK +/// - [WebConvexClient]: Uses pure Dart WebSocket for web platform +abstract class IConvexClient { + /// Configuration for this client instance + ConvexConfig get config; + + // ============================================================================ + // Core Operations + // ============================================================================ + + /// Executes a Convex query operation. + /// + /// [name] - Name of the query function to execute (e.g., "messages:list") + /// [args] - Map of arguments to pass to the query + /// + /// Returns the query result as a JSON string. + /// + /// Throws: + /// - [TimeoutException] if operation exceeds configured timeout + /// - [ClientError] for Convex-specific errors + Future query(String name, Map args); + + /// Executes a Convex mutation operation. + /// + /// [name] - Name of the mutation function to execute + /// [args] - Map of arguments to pass to the mutation + /// + /// Returns the mutation result as a JSON string. + /// + /// Throws: + /// - [TimeoutException] if operation exceeds configured timeout + /// - [ClientError] for Convex-specific errors + Future mutation({ + required String name, + required Map args, + }); + + /// Executes a Convex action operation. + /// + /// [name] - Name of the action function to execute + /// [args] - Map of arguments to pass to the action + /// + /// Returns the action result as a JSON string. + /// + /// Throws: + /// - [TimeoutException] if operation exceeds configured timeout + /// - [ClientError] for Convex-specific errors + Future action({ + required String name, + required Map args, + }); + + /// Creates a real-time subscription to a Convex query. + /// + /// [name] - Name of the query function to subscribe to + /// [args] - Map of arguments for the subscription + /// [onUpdate] - Callback function called when new data arrives + /// [onError] - Callback function called when an error occurs + /// + /// Returns a handle that can be used to cancel the subscription. + Future subscribe({ + required String name, + required Map args, + required void Function(String) onUpdate, + required void Function(String, String?) onError, + }); + + // ============================================================================ + // Authentication + // ============================================================================ + + /// Sets the authentication token for the client. + /// + /// [token] - The JWT authentication token to set, or null to clear + /// + /// Used to authenticate requests to the Convex backend. + Future setAuth({required String? token}); + + /// Sets authentication with automatic token refresh. + /// + /// [tokenFetcher] - Function that returns a fresh JWT token when called + /// [onAuthChange] - Optional callback for auth state changes + /// + /// Returns an [AuthHandle] that manages the auth session and token refresh. + Future setAuthWithRefresh({ + required Future Function() tokenFetcher, + void Function(bool isAuthenticated)? onAuthChange, + }); + + /// Clears the authentication token and stops any active token refresh. + Future clearAuth(); + + /// Stream of authentication state changes. + /// + /// Emits `true` when authenticated, `false` when not authenticated. + Stream get authState; + + /// Returns whether the user is currently authenticated. + bool get isAuthenticated; + + // ============================================================================ + // Connection Management + // ============================================================================ + + /// Stream of WebSocket connection state changes. + /// + /// Emits [WebSocketConnectionState.connected] when connection is established, + /// [WebSocketConnectionState.connecting] when connecting or reconnecting. + /// + /// This is the recommended way to monitor connection status. + Stream get connectionState; + + /// Returns the current WebSocket connection state (synchronous). + WebSocketConnectionState get currentConnectionState; + + /// Returns whether the WebSocket is currently connected. + bool get isConnected; + + /// Manually checks connection status using a health check query. + /// + /// **Deprecated**: Use [connectionState] stream instead for real-time monitoring. + /// + /// Returns [ConnectionStatus] indicating connection state. + @Deprecated('Use connectionState stream instead') + Future checkConnection(); + + /// Manually triggers a reconnection attempt. + /// + /// Returns `true` if reconnection was successful, `false` otherwise. + Future reconnect(); + + // ============================================================================ + // Lifecycle Management + // ============================================================================ + + /// Stream of app lifecycle events (foreground/background transitions). + /// + /// Useful for managing connections when app state changes. + Stream get lifecycleEvents; + + // ============================================================================ + // Resource Management + // ============================================================================ + + /// Disposes of client resources and closes connections. + /// + /// Should be called when the client is no longer needed. + void dispose(); +} diff --git a/third_party/convex_flutter/lib/src/impl/convex_client_native.dart b/third_party/convex_flutter/lib/src/impl/convex_client_native.dart new file mode 100644 index 00000000..bf11e6ee --- /dev/null +++ b/third_party/convex_flutter/lib/src/impl/convex_client_native.dart @@ -0,0 +1,274 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:convex_flutter/src/impl/convex_client_interface.dart'; +import 'package:convex_flutter/src/rust/lib.dart'; +import 'package:convex_flutter/src/rust/frb_generated.dart'; +import 'package:convex_flutter/src/utils.dart'; +import 'package:convex_flutter/src/connection_status.dart'; +import 'package:convex_flutter/src/convex_config.dart'; +import 'package:convex_flutter/src/app_lifecycle_event.dart'; +import 'package:convex_flutter/src/app_lifecycle_observer.dart'; + +/// Native (FFI-based) implementation of Convex client. +/// +/// This implementation uses Flutter Rust Bridge to call into the official +/// Convex Rust SDK for mobile and desktop platforms (Android, iOS, macOS, +/// Windows, Linux). +/// +/// For web platform, use [WebConvexClient] instead. +class NativeConvexClient implements IConvexClient { + /// The underlying Rust FFI client + final MobileConvexClient _rustClient; + + /// Configuration for this client + @override + final ConvexConfig config; + + /// Stream controller for auth state changes + final StreamController _authStateController = + StreamController.broadcast(); + + /// Stream controller for lifecycle events + final StreamController _lifecycleController = + StreamController.broadcast(); + + /// Stream controller for WebSocket connection state changes + final StreamController _connectionStateController = + StreamController.broadcast(); + + /// Current connection state (cached for sync access) + WebSocketConnectionState _currentConnectionState = + WebSocketConnectionState.connecting; + + /// Current auth handle (if using refresh-based auth) + AuthHandle? _currentAuthHandle; + + /// Lifecycle observer for app state changes + late final AppLifecycleObserver _lifecycleObserver; + + /// Private constructor + NativeConvexClient._(this._rustClient, this.config); + + /// Factory method to create and initialize a native client. + /// + /// This handles: + /// - Rust FFI library initialization + /// - WebSocket state listener setup + /// - Lifecycle observer setup + static Future create(ConvexConfig config) async { + // Initialize Rust FFI library + await RustLib.init(); + + // Create Rust client instance + final rustClient = MobileConvexClient( + deploymentUrl: config.deploymentUrl, + clientId: config.clientId ?? 'flutter-client', + ); + + // Create native client wrapper + final client = NativeConvexClient._(rustClient, config); + + // Setup connection state listener BEFORE any operations + // This prevents race conditions where state changes are missed + await client._setupConnectionStateListener(); + + // Setup lifecycle observer + client._lifecycleObserver = AppLifecycleObserver( + onLifecycleChange: (event) { + client._lifecycleController.add(event); + }, + ); + + return client; + } + + /// Sets up the WebSocket connection state listener. + /// + /// This must be called before any queries/mutations to capture all state changes. + Future _setupConnectionStateListener() async { + debugPrint('=== [NativeConvexClient] Setting up WebSocket state listener ==='); + debugPrint('=== [NativeConvexClient] Current state: ${_currentConnectionState.name} ==='); + + try { + await _rustClient.onWebsocketStateChange( + onStateChange: (state) async { + debugPrint('=== [NativeConvexClient] State changed: ${state.name} ==='); + _currentConnectionState = state; + _connectionStateController.add(state); + debugPrint('=== [NativeConvexClient] Stream emission complete ==='); + }, + ); + debugPrint('=== [NativeConvexClient] Listener registered successfully ==='); + } catch (e) { + debugPrint('ERROR: [NativeConvexClient] Listener setup failed: $e'); + rethrow; + } + } + + // ============================================================================ + // IConvexClient Implementation - Core Operations + // ============================================================================ + + @override + Future query(String name, Map args) async { + final formattedArgs = buildArgs(args); + return await _rustClient + .query(name: name, args: formattedArgs) + .timeout(config.operationTimeout); + } + + @override + Future mutation({ + required String name, + required Map args, + }) async { + final formattedArgs = buildArgs(args); + return await _rustClient + .mutation(name: name, args: formattedArgs) + .timeout(config.operationTimeout); + } + + @override + Future action({ + required String name, + required Map args, + }) async { + final formattedArgs = buildArgs(args); + return await _rustClient + .action(name: name, args: formattedArgs) + .timeout(config.operationTimeout); + } + + @override + Future subscribe({ + required String name, + required Map args, + required void Function(String) onUpdate, + required void Function(String, String?) onError, + }) async { + final formattedArgs = buildArgs(args); + return await _rustClient.subscribe( + name: name, + args: formattedArgs, + onUpdate: (value) => onUpdate(value), + onError: (message, value) => onError(message, value), + ); + } + + // ============================================================================ + // IConvexClient Implementation - Authentication + // ============================================================================ + + @override + Future setAuth({required String? token}) async { + // Clear any existing refresh-based auth + _currentAuthHandle?.dispose(); + _currentAuthHandle = null; + + await _rustClient.setAuth(token: token); + _authStateController.add(token != null); + } + + @override + Future setAuthWithRefresh({ + required Future Function() tokenFetcher, + void Function(bool isAuthenticated)? onAuthChange, + }) async { + // Dispose any existing auth handle + _currentAuthHandle?.dispose(); + + final handle = await _rustClient.setAuthWithRefresh( + fetchToken: () async => await tokenFetcher(), + onAuthChange: (bool isAuth) async { + onAuthChange?.call(isAuth); + _authStateController.add(isAuth); + }, + ); + + _currentAuthHandle = handle; + return handle; + } + + @override + Future clearAuth() async { + _currentAuthHandle?.dispose(); + _currentAuthHandle = null; + await _rustClient.setAuth(token: null); + _authStateController.add(false); + } + + @override + Stream get authState => _authStateController.stream; + + @override + bool get isAuthenticated => _currentAuthHandle?.isAuthenticated() ?? false; + + // ============================================================================ + // IConvexClient Implementation - Connection Management + // ============================================================================ + + @override + Stream get connectionState => + _connectionStateController.stream; + + @override + WebSocketConnectionState get currentConnectionState => _currentConnectionState; + + @override + bool get isConnected => + _currentConnectionState == WebSocketConnectionState.connected; + + @override + @Deprecated('Use connectionState stream for real-time monitoring') + Future checkConnection() async { + if (config.healthCheckQuery == null) { + throw StateError( + 'No health check query configured. ' + 'Set healthCheckQuery in ConvexConfig or use a real query.', + ); + } + + try { + await _rustClient + .query(name: config.healthCheckQuery!, args: {}) + .timeout(config.operationTimeout); + return ConnectionStatus.connected; + } on TimeoutException { + return ConnectionStatus.timeout; + } catch (e) { + return ConnectionStatus.error; + } + } + + @override + Future reconnect() async { + try { + final status = await checkConnection(); + return status == ConnectionStatus.connected; + } catch (e) { + // If healthCheckQuery not configured, just return false + return false; + } + } + + // ============================================================================ + // IConvexClient Implementation - Lifecycle Management + // ============================================================================ + + @override + Stream get lifecycleEvents => _lifecycleController.stream; + + // ============================================================================ + // IConvexClient Implementation - Resource Management + // ============================================================================ + + @override + void dispose() { + _currentAuthHandle?.dispose(); + _lifecycleObserver.dispose(); + _authStateController.close(); + _lifecycleController.close(); + _connectionStateController.close(); + } +} diff --git a/third_party/convex_flutter/lib/src/impl/convex_client_web.dart b/third_party/convex_flutter/lib/src/impl/convex_client_web.dart new file mode 100644 index 00000000..870eee2d --- /dev/null +++ b/third_party/convex_flutter/lib/src/impl/convex_client_web.dart @@ -0,0 +1,875 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop'; +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:web/web.dart' as web; +import 'package:convex_flutter/src/impl/convex_client_interface.dart'; +import 'package:convex_flutter/src/rust/lib.dart' show WebSocketConnectionState, SubscriptionHandle, AuthHandle; +import 'package:convex_flutter/src/connection_status.dart'; +import 'package:convex_flutter/src/convex_config.dart'; +import 'package:convex_flutter/src/app_lifecycle_event.dart'; +import 'package:convex_flutter/src/app_lifecycle_observer.dart'; + +/// Web (pure Dart) implementation of Convex client. +/// +/// This implementation uses the browser's native WebSocket API for web platform, +/// avoiding the need for Rust toolchain or FFI. It implements the same +/// [IConvexClient] interface as [NativeConvexClient], ensuring API compatibility +/// across all platforms. +/// +/// For mobile/desktop platforms, use [NativeConvexClient] instead. +class WebConvexClient implements IConvexClient { + /// Configuration for this client + @override + final ConvexConfig config; + + /// WebSocket connection to Convex backend + web.WebSocket? _ws; + + /// Stream controller for auth state changes + final StreamController _authStateController = + StreamController.broadcast(); + + /// Stream controller for lifecycle events + final StreamController _lifecycleController = + StreamController.broadcast(); + + /// Stream controller for WebSocket connection state changes + final StreamController _connectionStateController = + StreamController.broadcast(); + + /// Current connection state (cached for sync access) + WebSocketConnectionState _currentConnectionState = + WebSocketConnectionState.connecting; + + /// Current auth token + String? _currentAuthToken; + + /// Lifecycle observer for app state changes + late final AppLifecycleObserver _lifecycleObserver; + + /// Message ID counter for generating unique request IDs + int _messageIdCounter = 0; + + /// Session ID for Convex sync protocol + String? _sessionId; + + /// Query ID counter for subscriptions + int _queryIdCounter = 0; + + /// Query set version counter for ModifyQuerySet messages + int _querySetVersion = 0; + + /// Pending requests waiting for responses (query, mutation, action) + final Map> _pendingRequests = {}; + + /// Active subscriptions + final Map _subscriptions = {}; + + /// Reconnection attempt counter + int _reconnectAttempts = 0; + + /// Maximum reconnection attempts + static const int _maxReconnectAttempts = 10; + + /// Base reconnection delay + static const Duration _baseReconnectDelay = Duration(seconds: 1); + + /// Timer for reconnection + Timer? _reconnectTimer; + + /// Whether client is disposed + bool _isDisposed = false; + + /// Private constructor + WebConvexClient._(this.config); + + /// Factory method to create and initialize a web client. + /// + /// This handles: + /// - WebSocket connection setup + /// - Event listener registration + /// - Lifecycle observer setup + static Future create(ConvexConfig config) async { + debugPrint('=== [WebConvexClient] Creating web client ==='); + + final client = WebConvexClient._(config); + + // Setup lifecycle observer + // Note: On web, we don't reconnect on lifecycle events because: + // 1. Page navigation triggers lifecycle events but doesn't disconnect WebSocket + // 2. WebSocket onclose handler already manages reconnection + // 3. Browser tab visibility changes are the only real "background" events + client._lifecycleObserver = AppLifecycleObserver( + onLifecycleChange: (event) { + client._lifecycleController.add(event); + // Do NOT trigger reconnection on web - let WebSocket manage itself + debugPrint('=== [WebConvexClient] Lifecycle event: ${event.name} (no action on web) ==='); + }, + ); + + // Establish WebSocket connection + await client._connect(); + + debugPrint('=== [WebConvexClient] Client created successfully ==='); + return client; + } + + /// Establishes WebSocket connection to Convex backend. + Future _connect() async { + if (_isDisposed) return; + + debugPrint('=== [WebConvexClient] Connecting to Convex ==='); + + try { + // Convert HTTPS to WSS URL with correct Convex sync endpoint + // Format: wss://deployment.convex.cloud/api/{version}/sync + final wsUrl = config.deploymentUrl.replaceFirst('https', 'wss'); + final fullUrl = '$wsUrl/api/sync'; + + debugPrint('=== [WebConvexClient] WebSocket URL: $fullUrl ==='); + + // Update state to connecting + _updateConnectionState(WebSocketConnectionState.connecting); + + // Create WebSocket connection + _ws = web.WebSocket(fullUrl); + + // Setup event listeners + _setupWebSocketListeners(); + + debugPrint('=== [WebConvexClient] WebSocket connection initiated ==='); + } catch (e) { + debugPrint('ERROR: [WebConvexClient] Connection failed: $e'); + _scheduleReconnect(); + } + } + + /// Sets up WebSocket event listeners. + void _setupWebSocketListeners() { + final ws = _ws; + if (ws == null) return; + + // Connection opened + ws.onopen = (web.Event event) { + debugPrint('=== [WebConvexClient] WebSocket opened ==='); + _reconnectAttempts = 0; // Reset reconnection counter + _querySetVersion = 0; // Reset query set version for new connection + _updateConnectionState(WebSocketConnectionState.connected); + + // Send Connect handshake (required by Convex protocol) + _sendConnectMessage(); + + // Send auth token if available + if (_currentAuthToken != null) { + _sendAuthMessage(_currentAuthToken!); + } + }.toJS; + + // Connection closed + ws.onclose = (web.CloseEvent event) { + final code = event.code; + final reason = event.reason; + final wasClean = event.wasClean; + debugPrint('=== [WebConvexClient] WebSocket closed ==='); + debugPrint('=== [WebConvexClient] Close code: $code, reason: "$reason", wasClean: $wasClean ==='); + _updateConnectionState(WebSocketConnectionState.connecting); + + // Attempt reconnection if not disposed + if (!_isDisposed) { + _scheduleReconnect(); + } + }.toJS; + + // Connection error + ws.onerror = (web.Event event) { + debugPrint('ERROR: [WebConvexClient] WebSocket error occurred'); + debugPrint('ERROR: [WebConvexClient] Event type: ${event.type}'); + _updateConnectionState(WebSocketConnectionState.connecting); + }.toJS; + + // Message received + ws.onmessage = (web.MessageEvent event) { + final data = event.data; + + // Convert JSAny? to String + final dataString = (data as JSString?)?.toDart; + if (dataString != null) { + _handleMessage(dataString); + } else { + debugPrint('WARNING: [WebConvexClient] Received non-string message'); + } + }.toJS; + } + + /// Handles incoming WebSocket messages. + void _handleMessage(String data) { + try { + debugPrint('=== [WebConvexClient] RAW MESSAGE: $data ==='); + + final message = jsonDecode(data) as Map; + final type = message['type'] as String?; + final id = message['id'] as String?; + + debugPrint('=== [WebConvexClient] Received message type: $type, id: $id ==='); + + switch (type) { + case 'Transition': + // Query subscription updates + _handleTransition(message); + break; + + case 'MutationResponse': + _handleMutationResponse(message); + break; + + case 'ActionResponse': + _handleActionResponse(message); + break; + + case 'Ping': + // Respond to server ping + _sendPong(); + break; + + case 'FatalError': + _handleFatalError(message); + break; + + case 'AuthError': + _handleAuthError(message); + break; + + default: + debugPrint('WARNING: [WebConvexClient] Unknown message type: $type'); + } + } catch (e) { + debugPrint('ERROR: [WebConvexClient] Failed to parse message: $e'); + } + } + + /// Handles Transition messages (query subscription updates). + void _handleTransition(Map message) { + final modifications = message['modifications'] as List?; + if (modifications == null) return; + + for (final mod in modifications) { + final queryId = mod['queryId']?.toString(); + if (queryId == null) continue; + + final subscription = _subscriptions[queryId]; + if (subscription == null) continue; + + final value = mod['value']; + if (value != null) { + final valueJson = jsonEncode(value); + subscription.onUpdate(valueJson); + } + } + } + + /// Handles MutationResponse messages. + void _handleMutationResponse(Map message) { + final requestId = message['requestId'] as int?; + if (requestId == null) return; + + final completer = _pendingRequests.remove(requestId); + if (completer == null) return; + + final result = message['result']; + if (result != null) { + final resultJson = jsonEncode(result); + completer.complete(resultJson); + } else { + completer.completeError(Exception('No result in mutation response')); + } + } + + /// Handles ActionResponse messages. + void _handleActionResponse(Map message) { + final requestId = message['requestId'] as int?; + if (requestId == null) return; + + final completer = _pendingRequests.remove(requestId); + if (completer == null) return; + + final result = message['result']; + if (result != null) { + final resultJson = jsonEncode(result); + completer.complete(resultJson); + } else { + completer.completeError(Exception('No result in action response')); + } + } + + /// Handles FatalError messages. + void _handleFatalError(Map message) { + final error = message['error'] as String? ?? 'Unknown fatal error'; + debugPrint('FATAL ERROR: [WebConvexClient] $error'); + + // Close connection on fatal error + _ws?.close(); + } + + /// Handles AuthError messages. + void _handleAuthError(Map message) { + final error = message['error'] as String? ?? 'Authentication error'; + debugPrint('AUTH ERROR: [WebConvexClient] $error'); + + // Clear auth and notify + _authStateController.add(false); + } + + /// Sends Pong response to server Ping. + void _sendPong() { + try { + _sendMessage({ + 'type': 'Event', + 'eventType': 'Pong', // Required field + 'event': null, // Required field (can be null) + }); + debugPrint('=== [WebConvexClient] Sent Pong ==='); + } catch (e) { + debugPrint('ERROR: [WebConvexClient] Failed to send Pong: $e'); + } + } + + /// Sends Connect handshake message. + void _sendConnectMessage() { + try { + // Generate or reuse session ID (must be valid UUID format) + _sessionId ??= _generateUuid(); + + _sendMessage({ + 'type': 'Connect', + 'sessionId': _sessionId, + 'maxObservedTimestamp': null, + 'connectionCount': _reconnectAttempts + 1, + 'lastCloseReason': null, // Required field + 'clientTs': DateTime.now().millisecondsSinceEpoch, // Required field + }); + debugPrint('=== [WebConvexClient] Sent Connect handshake ==='); + } catch (e) { + debugPrint('ERROR: [WebConvexClient] Failed to send Connect: $e'); + } + } + + /// Generates a RFC 4122 compliant UUID v4 string. + String _generateUuid() { + // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + // Where 4 = version 4, y = variant bits (8, 9, A, or B) + final random = math.Random(); + + // Generate random values for each segment + final segment1 = random.nextInt(0x100000000); // 32 bits = 8 hex chars + final segment2 = random.nextInt(0x10000); // 16 bits = 4 hex chars + final segment3 = random.nextInt(0x10000); // 16 bits = 4 hex chars (we'll set version) + final segment4 = random.nextInt(0x10000); // 16 bits = 4 hex chars (we'll set variant) + final segment5a = random.nextInt(0x100000000); // 32 bits = 8 hex chars + final segment5b = random.nextInt(0x10000); // 16 bits = 4 hex chars + + // Set version 4 (bits 12-15 of segment3 = 0100) + final version4 = (segment3 & 0x0FFF) | 0x4000; + + // Set variant bits (bits 14-15 of segment4 = 10) + final variant = (segment4 & 0x3FFF) | 0x8000; + + // Combine segment5 parts into 12 hex digits + final segment5 = '${segment5a.toRadixString(16).padLeft(8, '0')}${segment5b.toRadixString(16).padLeft(4, '0')}'; + + return '${segment1.toRadixString(16).padLeft(8, '0')}-' + '${segment2.toRadixString(16).padLeft(4, '0')}-' + '${version4.toRadixString(16).padLeft(4, '0')}-' + '${variant.toRadixString(16).padLeft(4, '0')}-' + '$segment5'; + } + + /// Updates connection state and emits to stream. + void _updateConnectionState(WebSocketConnectionState newState) { + if (_currentConnectionState != newState) { + debugPrint('=== [WebConvexClient] State transition: ${_currentConnectionState.name} → ${newState.name} ==='); + _currentConnectionState = newState; + _connectionStateController.add(newState); + } + } + + /// Schedules a reconnection attempt with exponential backoff. + void _scheduleReconnect() { + if (_isDisposed) return; + + _reconnectTimer?.cancel(); + + if (_reconnectAttempts >= _maxReconnectAttempts) { + debugPrint('ERROR: [WebConvexClient] Max reconnection attempts reached'); + return; + } + + // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s (max) + final delay = _baseReconnectDelay * (1 << _reconnectAttempts.clamp(0, 5)); + _reconnectAttempts++; + + debugPrint('=== [WebConvexClient] Scheduling reconnect attempt $_reconnectAttempts in ${delay.inSeconds}s ==='); + + _reconnectTimer = Timer(delay, () { + debugPrint('=== [WebConvexClient] Executing reconnect attempt $_reconnectAttempts ==='); + _connect(); + }); + } + + /// Generates a unique message ID. + int _generateMessageId() { + return _messageIdCounter++; + } + + /// Sends a message over WebSocket. + void _sendMessage(Map message) { + final ws = _ws; + if (ws == null || ws.readyState != web.WebSocket.OPEN) { + throw StateError('WebSocket not connected'); + } + + final messageJson = jsonEncode(message); + debugPrint('=== [WebConvexClient] SENDING: $messageJson ==='); + ws.send(messageJson.toJS); + + debugPrint('=== [WebConvexClient] Sent message: ${message['type']} (id: ${message['id']}) ==='); + } + + /// Sends authentication message. + void _sendAuthMessage(String token) { + try { + // Send Authenticate message (Convex protocol) + _sendMessage({ + 'type': 'Authenticate', + 'token': token, + }); + debugPrint('=== [WebConvexClient] Auth token sent ==='); + } catch (e) { + debugPrint('ERROR: [WebConvexClient] Failed to send auth: $e'); + } + } + + // ============================================================================ + // IConvexClient Implementation - Core Operations + // ============================================================================ + + @override + Future query(String name, Map args) async { + // Queries in Convex protocol use ModifyQuerySet (like subscriptions) + // We subscribe, wait for first result, then unsubscribe + final queryId = _queryIdCounter++; + final queryIdStr = queryId.toString(); + final completer = Completer(); + + // Create temporary subscription for one-shot query + final subscription = _WebSubscription( + id: queryIdStr, + onUpdate: (value) { + if (!completer.isCompleted) { + completer.complete(value); + // Auto-unsubscribe after getting result + _unsubscribe(queryIdStr); + } + }, + onError: (message, value) { + if (!completer.isCompleted) { + completer.completeError(Exception(message)); + _subscriptions.remove(queryIdStr); + } + }, + ); + _subscriptions[queryIdStr] = subscription; + + try { + // Send ModifyQuerySet with Add (Convex protocol for queries) + final baseVersion = _querySetVersion; + final newVersion = ++_querySetVersion; + + _sendMessage({ + 'type': 'ModifyQuerySet', + 'baseVersion': baseVersion, + 'newVersion': newVersion, + 'modifications': [ + { + 'type': 'Add', + 'queryId': queryId, + 'udfPath': name, + 'args': [args], // Args must be array + } + ], + }); + + return await completer.future.timeout( + config.operationTimeout, + onTimeout: () { + _subscriptions.remove(queryIdStr); + throw TimeoutException('Query timeout: $name'); + }, + ); + } catch (e) { + _subscriptions.remove(queryIdStr); + rethrow; + } + } + + @override + Future mutation({ + required String name, + required Map args, + }) async { + final requestId = _generateMessageId(); + final completer = Completer(); + _pendingRequests[requestId] = completer; + + try { + // Send Mutation message (Convex protocol) + _sendMessage({ + 'type': 'Mutation', + 'requestId': requestId, + 'udfPath': name, // Use udfPath instead of name + 'args': [args], // Args must be array, not object + }); + + return await completer.future.timeout( + config.operationTimeout, + onTimeout: () { + _pendingRequests.remove(requestId); + throw TimeoutException('Mutation timeout: $name'); + }, + ); + } catch (e) { + _pendingRequests.remove(requestId); + rethrow; + } + } + + @override + Future action({ + required String name, + required Map args, + }) async { + final requestId = _generateMessageId(); + final completer = Completer(); + _pendingRequests[requestId] = completer; + + try { + // Send Action message (Convex protocol) + _sendMessage({ + 'type': 'Action', + 'requestId': requestId, + 'udfPath': name, // Use udfPath instead of name + 'args': [args], // Args must be array, not object + }); + + return await completer.future.timeout( + config.operationTimeout, + onTimeout: () { + _pendingRequests.remove(requestId); + throw TimeoutException('Action timeout: $name'); + }, + ); + } catch (e) { + _pendingRequests.remove(requestId); + rethrow; + } + } + + @override + Future subscribe({ + required String name, + required Map args, + required void Function(String) onUpdate, + required void Function(String, String?) onError, + }) async { + // Use incrementing query ID (Convex protocol requirement) + final queryId = _queryIdCounter++; + final queryIdStr = queryId.toString(); + + // Create subscription record + final subscription = _WebSubscription( + id: queryIdStr, + onUpdate: onUpdate, + onError: onError, + ); + _subscriptions[queryIdStr] = subscription; + + try { + // Send ModifyQuerySet with Add modification (Convex protocol) + final baseVersion = _querySetVersion; + final newVersion = ++_querySetVersion; + + _sendMessage({ + 'type': 'ModifyQuerySet', + 'baseVersion': baseVersion, + 'newVersion': newVersion, + 'modifications': [ + { + 'type': 'Add', + 'queryId': queryId, + 'udfPath': name, // Use udfPath instead of name + 'args': [args], // Args must be array, not object + } + ], + }); + + debugPrint('=== [WebConvexClient] Subscription created: queryId=$queryId ==='); + + // Return handle for cancellation + return _WebSubscriptionHandle( + onCancel: () { + _unsubscribe(queryIdStr); + }, + ); + } catch (e) { + _subscriptions.remove(queryIdStr); + rethrow; + } + } + + /// Unsubscribes from a subscription. + void _unsubscribe(String queryIdStr) { + final subscription = _subscriptions.remove(queryIdStr); + if (subscription == null) return; + + debugPrint('=== [WebConvexClient] Unsubscribing: queryId=$queryIdStr ==='); + + try { + final queryId = int.tryParse(queryIdStr); + if (queryId == null) return; + + // Send ModifyQuerySet with Remove modification (Convex protocol) + final baseVersion = _querySetVersion; + final newVersion = ++_querySetVersion; + + _sendMessage({ + 'type': 'ModifyQuerySet', + 'baseVersion': baseVersion, + 'newVersion': newVersion, + 'modifications': [ + { + 'type': 'Remove', + 'queryId': queryId, + } + ], + }); + } catch (e) { + debugPrint('ERROR: [WebConvexClient] Failed to send unsubscribe: $e'); + } + } + + // ============================================================================ + // IConvexClient Implementation - Authentication + // ============================================================================ + + @override + Future setAuth({required String? token}) async { + _currentAuthToken = token; + + if (token != null) { + _sendAuthMessage(token); + _authStateController.add(true); + } else { + _sendAuthMessage(''); // Clear auth + _authStateController.add(false); + } + } + + @override + Future setAuthWithRefresh({ + required Future Function() tokenFetcher, + void Function(bool isAuthenticated)? onAuthChange, + }) async { + // TODO: Implement token refresh for web + // For now, just fetch token once and set it + final token = await tokenFetcher(); + await setAuth(token: token); + + if (onAuthChange != null) { + onAuthChange(token != null); + } + + // Return a simple auth handle (no auto-refresh yet) + return _WebAuthHandle( + isAuth: token != null, + onDispose: () async { + await setAuth(token: null); + }, + ); + } + + @override + Future clearAuth() async { + await setAuth(token: null); + } + + @override + Stream get authState => _authStateController.stream; + + @override + bool get isAuthenticated => _currentAuthToken != null; + + // ============================================================================ + // IConvexClient Implementation - Connection Management + // ============================================================================ + + @override + Stream get connectionState => + _connectionStateController.stream; + + @override + WebSocketConnectionState get currentConnectionState => _currentConnectionState; + + @override + bool get isConnected => + _currentConnectionState == WebSocketConnectionState.connected; + + @override + @Deprecated('Use connectionState stream for real-time monitoring') + Future checkConnection() async { + if (config.healthCheckQuery == null) { + throw StateError( + 'No health check query configured. ' + 'Set healthCheckQuery in ConvexConfig or use a real query.', + ); + } + + try { + await query(config.healthCheckQuery!, {}); + return ConnectionStatus.connected; + } on TimeoutException { + return ConnectionStatus.timeout; + } catch (e) { + return ConnectionStatus.error; + } + } + + @override + Future reconnect() async { + debugPrint('=== [WebConvexClient] Manual reconnect requested ==='); + + // Close existing connection if any + _ws?.close(); + _ws = null; + + // Reset reconnection counter for manual reconnect + _reconnectAttempts = 0; + + // Attempt connection + try { + await _connect(); + + // Wait a bit for connection to establish + await Future.delayed(const Duration(seconds: 2)); + + return isConnected; + } catch (e) { + debugPrint('ERROR: [WebConvexClient] Manual reconnect failed: $e'); + return false; + } + } + + // ============================================================================ + // IConvexClient Implementation - Lifecycle Management + // ============================================================================ + + @override + Stream get lifecycleEvents => _lifecycleController.stream; + + // ============================================================================ + // IConvexClient Implementation - Resource Management + // ============================================================================ + + @override + void dispose() { + if (_isDisposed) return; + + debugPrint('=== [WebConvexClient] Disposing client ==='); + _isDisposed = true; + + // Cancel reconnection timer + _reconnectTimer?.cancel(); + + // Close WebSocket + _ws?.close(); + _ws = null; + + // Dispose lifecycle observer + _lifecycleObserver.dispose(); + + // Close streams + _authStateController.close(); + _lifecycleController.close(); + _connectionStateController.close(); + + // Clear pending requests and subscriptions + _pendingRequests.clear(); + _subscriptions.clear(); + + debugPrint('=== [WebConvexClient] Client disposed ==='); + } +} + +/// Internal subscription record for web client. +class _WebSubscription { + final String id; + final void Function(String) onUpdate; + final void Function(String, String?) onError; + + _WebSubscription({ + required this.id, + required this.onUpdate, + required this.onError, + }); +} + +/// Web implementation of SubscriptionHandle. +class _WebSubscriptionHandle implements SubscriptionHandle { + final void Function() onCancel; + bool _isCancelled = false; + + _WebSubscriptionHandle({required this.onCancel}); + + @override + void cancel() { + if (!_isCancelled) { + _isCancelled = true; + onCancel(); + } + } + + @override + void dispose() { + cancel(); + } + + @override + bool get isDisposed => _isCancelled; +} + +/// Web implementation of AuthHandle. +class _WebAuthHandle implements AuthHandle { + final bool isAuth; + final Future Function() onDispose; + bool _isDisposed = false; + + _WebAuthHandle({ + required this.isAuth, + required this.onDispose, + }); + + @override + bool isAuthenticated() => isAuth && !_isDisposed; + + @override + void dispose() { + if (!_isDisposed) { + _isDisposed = true; + onDispose(); + } + } + + @override + bool get isDisposed => _isDisposed; +} diff --git a/third_party/convex_flutter/lib/src/rust/frb_generated.dart b/third_party/convex_flutter/lib/src/rust/frb_generated.dart new file mode 100644 index 00000000..80f18bed --- /dev/null +++ b/third_party/convex_flutter/lib/src/rust/frb_generated.dart @@ -0,0 +1,2247 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'frb_generated.io.dart' + if (dart.library.js_interop) 'frb_generated.web.dart'; +import 'lib.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +/// Main entrypoint of the Rust API +class RustLib extends BaseEntrypoint { + @internal + static final instance = RustLib._(); + + RustLib._(); + + /// Initialize flutter_rust_bridge + static Future init({ + RustLibApi? api, + BaseHandler? handler, + ExternalLibrary? externalLibrary, + bool forceSameCodegenVersion = true, + }) async { + await instance.initImpl( + api: api, + handler: handler, + externalLibrary: externalLibrary, + forceSameCodegenVersion: forceSameCodegenVersion, + ); + } + + /// Initialize flutter_rust_bridge in mock mode. + /// No libraries for FFI are loaded. + static void initMock({required RustLibApi api}) { + instance.initMockImpl(api: api); + } + + /// Dispose flutter_rust_bridge + /// + /// The call to this function is optional, since flutter_rust_bridge (and everything else) + /// is automatically disposed when the app stops. + static void dispose() => instance.disposeImpl(); + + @override + ApiImplConstructor get apiImplConstructor => + RustLibApiImpl.new; + + @override + WireConstructor get wireConstructor => + RustLibWire.fromExternalLibrary; + + @override + Future executeRustInitializers() async {} + + @override + ExternalLibraryLoaderConfig get defaultExternalLibraryLoaderConfig => + kDefaultExternalLibraryLoaderConfig; + + @override + String get codegenVersion => '2.11.1'; + + @override + int get rustContentHash => 1095084362; + + static const kDefaultExternalLibraryLoaderConfig = + ExternalLibraryLoaderConfig( + stem: 'convex_flutter', + ioDirectory: 'rust/target/release/', + webPrefix: 'pkg/', + ); +} + +abstract class RustLibApi extends BaseApi { + void crateAuthHandleDispose({required AuthHandle that}); + + bool crateAuthHandleIsAuthenticated({required AuthHandle that}); + + Future crateCallbackSubscriberDartFnOnError({ + required CallbackSubscriberDartFn that, + required String message, + String? value, + }); + + Future crateCallbackSubscriberDartFnOnUpdate({ + required CallbackSubscriberDartFn that, + required String value, + }); + + Future crateCallbackSubscriberOnError({ + required CallbackSubscriber that, + required String message, + String? value, + }); + + Future crateCallbackSubscriberOnUpdate({ + required CallbackSubscriber that, + required String value, + }); + + Future crateMobileConvexClientAction({ + required MobileConvexClient that, + required String name, + required Map args, + }); + + Future crateMobileConvexClientMutation({ + required MobileConvexClient that, + required String name, + required Map args, + }); + + MobileConvexClient crateMobileConvexClientNew({ + required String deploymentUrl, + required String clientId, + }); + + Future crateMobileConvexClientOnWebsocketStateChange({ + required MobileConvexClient that, + required FutureOr Function(WebSocketConnectionState) onStateChange, + }); + + Future crateMobileConvexClientQuery({ + required MobileConvexClient that, + required String name, + required Map args, + }); + + Future crateMobileConvexClientSetAuth({ + required MobileConvexClient that, + String? token, + }); + + Future crateMobileConvexClientSetAuthWithRefresh({ + required MobileConvexClient that, + required FutureOr Function() fetchToken, + required FutureOr Function(bool) onAuthChange, + }); + + Future crateMobileConvexClientSubscribe({ + required MobileConvexClient that, + required String name, + required Map args, + required FutureOr Function(String) onUpdate, + required FutureOr Function(String, String?) onError, + }); + + void crateSubscriptionHandleCancel({required SubscriptionHandle that}); + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_AuthHandle; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_AuthHandle; + + CrossPlatformFinalizerArg get rust_arc_decrement_strong_count_AuthHandlePtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CallbackSubscriber; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CallbackSubscriber; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CallbackSubscriberPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CallbackSubscriberDartFn; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CallbackSubscriberDartFn; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CallbackSubscriberDartFnPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_MobileConvexClient; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_MobileConvexClient; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_MobileConvexClientPtr; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_SubscriptionHandle; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_SubscriptionHandle; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_SubscriptionHandlePtr; +} + +class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { + RustLibApiImpl({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + @override + void crateAuthHandleDispose({required AuthHandle that}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 1)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateAuthHandleDisposeConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateAuthHandleDisposeConstMeta => + const TaskConstMeta(debugName: "AuthHandle_dispose", argNames: ["that"]); + + @override + bool crateAuthHandleIsAuthenticated({required AuthHandle that}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 2)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_bool, + decodeErrorData: null, + ), + constMeta: kCrateAuthHandleIsAuthenticatedConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateAuthHandleIsAuthenticatedConstMeta => + const TaskConstMeta( + debugName: "AuthHandle_is_authenticated", + argNames: ["that"], + ); + + @override + Future crateCallbackSubscriberDartFnOnError({ + required CallbackSubscriberDartFn that, + required String message, + String? value, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + that, + serializer, + ); + sse_encode_String(message, serializer); + sse_encode_opt_String(value, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 3, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateCallbackSubscriberDartFnOnErrorConstMeta, + argValues: [that, message, value], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateCallbackSubscriberDartFnOnErrorConstMeta => + const TaskConstMeta( + debugName: "CallbackSubscriberDartFn_on_error", + argNames: ["that", "message", "value"], + ); + + @override + Future crateCallbackSubscriberDartFnOnUpdate({ + required CallbackSubscriberDartFn that, + required String value, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + that, + serializer, + ); + sse_encode_String(value, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 4, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateCallbackSubscriberDartFnOnUpdateConstMeta, + argValues: [that, value], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateCallbackSubscriberDartFnOnUpdateConstMeta => + const TaskConstMeta( + debugName: "CallbackSubscriberDartFn_on_update", + argNames: ["that", "value"], + ); + + @override + Future crateCallbackSubscriberOnError({ + required CallbackSubscriber that, + required String message, + String? value, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + that, + serializer, + ); + sse_encode_String(message, serializer); + sse_encode_opt_String(value, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 5, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateCallbackSubscriberOnErrorConstMeta, + argValues: [that, message, value], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateCallbackSubscriberOnErrorConstMeta => + const TaskConstMeta( + debugName: "CallbackSubscriber_on_error", + argNames: ["that", "message", "value"], + ); + + @override + Future crateCallbackSubscriberOnUpdate({ + required CallbackSubscriber that, + required String value, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + that, + serializer, + ); + sse_encode_String(value, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 6, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateCallbackSubscriberOnUpdateConstMeta, + argValues: [that, value], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateCallbackSubscriberOnUpdateConstMeta => + const TaskConstMeta( + debugName: "CallbackSubscriber_on_update", + argNames: ["that", "value"], + ); + + @override + Future crateMobileConvexClientAction({ + required MobileConvexClient that, + required String name, + required Map args, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_String(name, serializer); + sse_encode_Map_String_String_None(args, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 7, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientActionConstMeta, + argValues: [that, name, args], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientActionConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_action", + argNames: ["that", "name", "args"], + ); + + @override + Future crateMobileConvexClientMutation({ + required MobileConvexClient that, + required String name, + required Map args, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_String(name, serializer); + sse_encode_Map_String_String_None(args, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 8, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientMutationConstMeta, + argValues: [that, name, args], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientMutationConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_mutation", + argNames: ["that", "name", "args"], + ); + + @override + MobileConvexClient crateMobileConvexClientNew({ + required String deploymentUrl, + required String clientId, + }) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(deploymentUrl, serializer); + sse_encode_String(clientId, serializer); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 9)!; + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient, + decodeErrorData: null, + ), + constMeta: kCrateMobileConvexClientNewConstMeta, + argValues: [deploymentUrl, clientId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientNewConstMeta => const TaskConstMeta( + debugName: "MobileConvexClient_new", + argNames: ["deploymentUrl", "clientId"], + ); + + @override + Future crateMobileConvexClientOnWebsocketStateChange({ + required MobileConvexClient that, + required FutureOr Function(WebSocketConnectionState) onStateChange, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + onStateChange, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 10, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientOnWebsocketStateChangeConstMeta, + argValues: [that, onStateChange], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientOnWebsocketStateChangeConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_on_websocket_state_change", + argNames: ["that", "onStateChange"], + ); + + @override + Future crateMobileConvexClientQuery({ + required MobileConvexClient that, + required String name, + required Map args, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_String(name, serializer); + sse_encode_Map_String_String_None(args, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 11, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientQueryConstMeta, + argValues: [that, name, args], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientQueryConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_query", + argNames: ["that", "name", "args"], + ); + + @override + Future crateMobileConvexClientSetAuth({ + required MobileConvexClient that, + String? token, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_opt_String(token, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 12, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientSetAuthConstMeta, + argValues: [that, token], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientSetAuthConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_set_auth", + argNames: ["that", "token"], + ); + + @override + Future crateMobileConvexClientSetAuthWithRefresh({ + required MobileConvexClient that, + required FutureOr Function() fetchToken, + required FutureOr Function(bool) onAuthChange, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_DartFn_Inputs__Output_opt_String_AnyhowException( + fetchToken, + serializer, + ); + sse_encode_DartFn_Inputs_bool_Output_unit_AnyhowException( + onAuthChange, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 13, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientSetAuthWithRefreshConstMeta, + argValues: [that, fetchToken, onAuthChange], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientSetAuthWithRefreshConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_set_auth_with_refresh", + argNames: ["that", "fetchToken", "onAuthChange"], + ); + + @override + Future crateMobileConvexClientSubscribe({ + required MobileConvexClient that, + required String name, + required Map args, + required FutureOr Function(String) onUpdate, + required FutureOr Function(String, String?) onError, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_String(name, serializer); + sse_encode_Map_String_String_None(args, serializer); + sse_encode_DartFn_Inputs_String_Output_unit_AnyhowException( + onUpdate, + serializer, + ); + sse_encode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + onError, + serializer, + ); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 14, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientSubscribeConstMeta, + argValues: [that, name, args, onUpdate, onError], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientSubscribeConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_subscribe", + argNames: ["that", "name", "args", "onUpdate", "onError"], + ); + + @override + void crateSubscriptionHandleCancel({required SubscriptionHandle that}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + that, + serializer, + ); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: null, + ), + constMeta: kCrateSubscriptionHandleCancelConstMeta, + argValues: [that], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateSubscriptionHandleCancelConstMeta => + const TaskConstMeta( + debugName: "SubscriptionHandle_cancel", + argNames: ["that"], + ); + + Future Function(int, dynamic) + encode_DartFn_Inputs_String_Output_unit_AnyhowException( + FutureOr Function(String) raw, + ) { + return (callId, rawArg0) async { + final arg0 = dco_decode_String(rawArg0); + + Box? rawOutput; + Box? rawError; + try { + rawOutput = Box(await raw(arg0)); + } catch (e, s) { + rawError = Box(AnyhowException("$e\n\n$s")); + } + + final serializer = SseSerializer(generalizedFrbRustBinding); + assert((rawOutput != null) ^ (rawError != null)); + if (rawOutput != null) { + serializer.buffer.putUint8(0); + sse_encode_unit(rawOutput.value, serializer); + } else { + serializer.buffer.putUint8(1); + sse_encode_AnyhowException(rawError!.value, serializer); + } + final output = serializer.intoRaw(); + + generalizedFrbRustBinding.dartFnDeliverOutput( + callId: callId, + ptr: output.ptr, + rustVecLen: output.rustVecLen, + dataLen: output.dataLen, + ); + }; + } + + Future Function(int, dynamic, dynamic) + encode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + FutureOr Function(String, String?) raw, + ) { + return (callId, rawArg0, rawArg1) async { + final arg0 = dco_decode_String(rawArg0); + final arg1 = dco_decode_opt_String(rawArg1); + + Box? rawOutput; + Box? rawError; + try { + rawOutput = Box(await raw(arg0, arg1)); + } catch (e, s) { + rawError = Box(AnyhowException("$e\n\n$s")); + } + + final serializer = SseSerializer(generalizedFrbRustBinding); + assert((rawOutput != null) ^ (rawError != null)); + if (rawOutput != null) { + serializer.buffer.putUint8(0); + sse_encode_unit(rawOutput.value, serializer); + } else { + serializer.buffer.putUint8(1); + sse_encode_AnyhowException(rawError!.value, serializer); + } + final output = serializer.intoRaw(); + + generalizedFrbRustBinding.dartFnDeliverOutput( + callId: callId, + ptr: output.ptr, + rustVecLen: output.rustVecLen, + dataLen: output.dataLen, + ); + }; + } + + Future Function(int) + encode_DartFn_Inputs__Output_opt_String_AnyhowException( + FutureOr Function() raw, + ) { + return (callId) async { + Box? rawOutput; + Box? rawError; + try { + rawOutput = Box(await raw()); + } catch (e, s) { + rawError = Box(AnyhowException("$e\n\n$s")); + } + + final serializer = SseSerializer(generalizedFrbRustBinding); + assert((rawOutput != null) ^ (rawError != null)); + if (rawOutput != null) { + serializer.buffer.putUint8(0); + sse_encode_opt_String(rawOutput.value, serializer); + } else { + serializer.buffer.putUint8(1); + sse_encode_AnyhowException(rawError!.value, serializer); + } + final output = serializer.intoRaw(); + + generalizedFrbRustBinding.dartFnDeliverOutput( + callId: callId, + ptr: output.ptr, + rustVecLen: output.rustVecLen, + dataLen: output.dataLen, + ); + }; + } + + Future Function(int, dynamic) + encode_DartFn_Inputs_bool_Output_unit_AnyhowException( + FutureOr Function(bool) raw, + ) { + return (callId, rawArg0) async { + final arg0 = dco_decode_bool(rawArg0); + + Box? rawOutput; + Box? rawError; + try { + rawOutput = Box(await raw(arg0)); + } catch (e, s) { + rawError = Box(AnyhowException("$e\n\n$s")); + } + + final serializer = SseSerializer(generalizedFrbRustBinding); + assert((rawOutput != null) ^ (rawError != null)); + if (rawOutput != null) { + serializer.buffer.putUint8(0); + sse_encode_unit(rawOutput.value, serializer); + } else { + serializer.buffer.putUint8(1); + sse_encode_AnyhowException(rawError!.value, serializer); + } + final output = serializer.intoRaw(); + + generalizedFrbRustBinding.dartFnDeliverOutput( + callId: callId, + ptr: output.ptr, + rustVecLen: output.rustVecLen, + dataLen: output.dataLen, + ); + }; + } + + Future Function(int, dynamic) + encode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + FutureOr Function(WebSocketConnectionState) raw, + ) { + return (callId, rawArg0) async { + final arg0 = dco_decode_web_socket_connection_state(rawArg0); + + Box? rawOutput; + Box? rawError; + try { + rawOutput = Box(await raw(arg0)); + } catch (e, s) { + rawError = Box(AnyhowException("$e\n\n$s")); + } + + final serializer = SseSerializer(generalizedFrbRustBinding); + assert((rawOutput != null) ^ (rawError != null)); + if (rawOutput != null) { + serializer.buffer.putUint8(0); + sse_encode_unit(rawOutput.value, serializer); + } else { + serializer.buffer.putUint8(1); + sse_encode_AnyhowException(rawError!.value, serializer); + } + final output = serializer.intoRaw(); + + generalizedFrbRustBinding.dartFnDeliverOutput( + callId: callId, + ptr: output.ptr, + rustVecLen: output.rustVecLen, + dataLen: output.dataLen, + ); + }; + } + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_AuthHandle => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_AuthHandle => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CallbackSubscriber => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CallbackSubscriber => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_CallbackSubscriberDartFn => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_CallbackSubscriberDartFn => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_MobileConvexClient => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_MobileConvexClient => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient; + + RustArcIncrementStrongCountFnType + get rust_arc_increment_strong_count_SubscriptionHandle => wire + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle; + + RustArcDecrementStrongCountFnType + get rust_arc_decrement_strong_count_SubscriptionHandle => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle; + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AnyhowException(raw as String); + } + + @protected + AuthHandle + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AuthHandleImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CallbackSubscriber + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CallbackSubscriberImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CallbackSubscriberDartFn + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CallbackSubscriberDartFnImpl.frbInternalDcoDecode( + raw as List, + ); + } + + @protected + MobileConvexClient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MobileConvexClientImpl.frbInternalDcoDecode(raw as List); + } + + @protected + SubscriptionHandle + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return SubscriptionHandleImpl.frbInternalDcoDecode(raw as List); + } + + @protected + AuthHandle + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AuthHandleImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CallbackSubscriber + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CallbackSubscriberImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CallbackSubscriberDartFn + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CallbackSubscriberDartFnImpl.frbInternalDcoDecode( + raw as List, + ); + } + + @protected + MobileConvexClient + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MobileConvexClientImpl.frbInternalDcoDecode(raw as List); + } + + @protected + SubscriptionHandle + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return SubscriptionHandleImpl.frbInternalDcoDecode(raw as List); + } + + @protected + FutureOr Function(String) + dco_decode_DartFn_Inputs_String_Output_unit_AnyhowException(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(''); + } + + @protected + FutureOr Function(String, String?) + dco_decode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(''); + } + + @protected + FutureOr Function() + dco_decode_DartFn_Inputs__Output_opt_String_AnyhowException(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(''); + } + + @protected + FutureOr Function(bool) + dco_decode_DartFn_Inputs_bool_Output_unit_AnyhowException(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(''); + } + + @protected + FutureOr Function(WebSocketConnectionState) + dco_decode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(''); + } + + @protected + Object dco_decode_DartOpaque(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return decodeDartOpaque(raw, generalizedFrbRustBinding); + } + + @protected + Map dco_decode_Map_String_String_None(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return Map.fromEntries( + dco_decode_list_record_string_string( + raw, + ).map((e) => MapEntry(e.$1, e.$2)), + ); + } + + @protected + AuthHandle + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return AuthHandleImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CallbackSubscriber + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CallbackSubscriberImpl.frbInternalDcoDecode(raw as List); + } + + @protected + CallbackSubscriberDartFn + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return CallbackSubscriberDartFnImpl.frbInternalDcoDecode( + raw as List, + ); + } + + @protected + MobileConvexClient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return MobileConvexClientImpl.frbInternalDcoDecode(raw as List); + } + + @protected + SubscriptionHandle + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return SubscriptionHandleImpl.frbInternalDcoDecode(raw as List); + } + + @protected + String dco_decode_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as String; + } + + @protected + QuerySubscriber dco_decode_TraitDef_QuerySubscriber(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(); + } + + @protected + bool dco_decode_bool(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as bool; + } + + @protected + ClientError dco_decode_client_error(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return ClientError_InternalError(msg: dco_decode_String(raw[1])); + case 1: + return ClientError_ConvexError(data: dco_decode_String(raw[1])); + case 2: + return ClientError_ServerError(msg: dco_decode_String(raw[1])); + default: + throw Exception("unreachable"); + } + } + + @protected + int dco_decode_i_32(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + PlatformInt64 dco_decode_isize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeI64(raw); + } + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as Uint8List; + } + + @protected + List<(String, String)> dco_decode_list_record_string_string(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_record_string_string).toList(); + } + + @protected + String? dco_decode_opt_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_String(raw); + } + + @protected + (String, String) dco_decode_record_string_string(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) { + throw Exception('Expected 2 elements, got ${arr.length}'); + } + return (dco_decode_String(arr[0]), dco_decode_String(arr[1])); + } + + @protected + int dco_decode_u_8(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as int; + } + + @protected + void dco_decode_unit(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return; + } + + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + + @protected + WebSocketConnectionState dco_decode_web_socket_connection_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return WebSocketConnectionState.values[raw as int]; + } + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_String(deserializer); + return AnyhowException(inner); + } + + @protected + AuthHandle + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return AuthHandleImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CallbackSubscriber + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CallbackSubscriberImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CallbackSubscriberDartFn + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CallbackSubscriberDartFnImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + MobileConvexClient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MobileConvexClientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + SubscriptionHandle + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return SubscriptionHandleImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + AuthHandle + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return AuthHandleImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CallbackSubscriber + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CallbackSubscriberImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CallbackSubscriberDartFn + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CallbackSubscriberDartFnImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + MobileConvexClient + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MobileConvexClientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + SubscriptionHandle + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return SubscriptionHandleImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + Object sse_decode_DartOpaque(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_isize(deserializer); + return decodeDartOpaque(inner, generalizedFrbRustBinding); + } + + @protected + Map sse_decode_Map_String_String_None( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_record_string_string(deserializer); + return Map.fromEntries(inner.map((e) => MapEntry(e.$1, e.$2))); + } + + @protected + AuthHandle + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return AuthHandleImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CallbackSubscriber + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CallbackSubscriberImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + CallbackSubscriberDartFn + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return CallbackSubscriberDartFnImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + MobileConvexClient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return MobileConvexClientImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + SubscriptionHandle + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return SubscriptionHandleImpl.frbInternalSseDecode( + sse_decode_usize(deserializer), + sse_decode_i_32(deserializer), + ); + } + + @protected + String sse_decode_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return utf8.decoder.convert(inner); + } + + @protected + bool sse_decode_bool(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8() != 0; + } + + @protected + ClientError sse_decode_client_error(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_msg = sse_decode_String(deserializer); + return ClientError_InternalError(msg: var_msg); + case 1: + var var_data = sse_decode_String(deserializer); + return ClientError_ConvexError(data: var_data); + case 2: + var var_msg = sse_decode_String(deserializer); + return ClientError_ServerError(msg: var_msg); + default: + throw UnimplementedError(''); + } + } + + @protected + int sse_decode_i_32(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getInt32(); + } + + @protected + PlatformInt64 sse_decode_isize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getPlatformInt64(); + } + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var len_ = sse_decode_i_32(deserializer); + return deserializer.buffer.getUint8List(len_); + } + + @protected + List<(String, String)> sse_decode_list_record_string_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = <(String, String)>[]; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_record_string_string(deserializer)); + } + return ans_; + } + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_String(deserializer)); + } else { + return null; + } + } + + @protected + (String, String) sse_decode_record_string_string( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_String(deserializer); + var var_field1 = sse_decode_String(deserializer); + return (var_field0, var_field1); + } + + @protected + int sse_decode_u_8(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getUint8(); + } + + @protected + void sse_decode_unit(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + + @protected + WebSocketConnectionState sse_decode_web_socket_connection_state( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return WebSocketConnectionState.values[inner]; + } + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.message, serializer); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as AuthHandleImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CallbackSubscriberImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CallbackSubscriberDartFnImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MobileConvexClientImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as SubscriptionHandleImpl).frbInternalSseEncode(move: true), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as AuthHandleImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CallbackSubscriberImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CallbackSubscriberDartFnImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MobileConvexClientImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as SubscriptionHandleImpl).frbInternalSseEncode(move: false), + serializer, + ); + } + + @protected + void sse_encode_DartFn_Inputs_String_Output_unit_AnyhowException( + FutureOr Function(String) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_DartOpaque( + encode_DartFn_Inputs_String_Output_unit_AnyhowException(self), + serializer, + ); + } + + @protected + void sse_encode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + FutureOr Function(String, String?) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_DartOpaque( + encode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException(self), + serializer, + ); + } + + @protected + void sse_encode_DartFn_Inputs__Output_opt_String_AnyhowException( + FutureOr Function() self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_DartOpaque( + encode_DartFn_Inputs__Output_opt_String_AnyhowException(self), + serializer, + ); + } + + @protected + void sse_encode_DartFn_Inputs_bool_Output_unit_AnyhowException( + FutureOr Function(bool) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_DartOpaque( + encode_DartFn_Inputs_bool_Output_unit_AnyhowException(self), + serializer, + ); + } + + @protected + void + sse_encode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + FutureOr Function(WebSocketConnectionState) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_DartOpaque( + encode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + self, + ), + serializer, + ); + } + + @protected + void sse_encode_DartOpaque(Object self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_isize( + PlatformPointerUtil.ptrToPlatformInt64( + encodeDartOpaque( + self, + portManager.dartHandlerPort, + generalizedFrbRustBinding, + ), + ), + serializer, + ); + } + + @protected + void sse_encode_Map_String_String_None( + Map self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_record_string_string( + self.entries.map((e) => (e.key, e.value)).toList(), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as AuthHandleImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CallbackSubscriberImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as CallbackSubscriberDartFnImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as MobileConvexClientImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize( + (self as SubscriptionHandleImpl).frbInternalSseEncode(move: null), + serializer, + ); + } + + @protected + void sse_encode_String(String self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); + } + + @protected + void sse_encode_bool(bool self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self ? 1 : 0); + } + + @protected + void sse_encode_client_error(ClientError self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case ClientError_InternalError(msg: final msg): + sse_encode_i_32(0, serializer); + sse_encode_String(msg, serializer); + case ClientError_ConvexError(data: final data): + sse_encode_i_32(1, serializer); + sse_encode_String(data, serializer); + case ClientError_ServerError(msg: final msg): + sse_encode_i_32(2, serializer); + sse_encode_String(msg, serializer); + } + } + + @protected + void sse_encode_i_32(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putInt32(self); + } + + @protected + void sse_encode_isize(PlatformInt64 self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putPlatformInt64(self); + } + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + serializer.buffer.putUint8List(self); + } + + @protected + void sse_encode_list_record_string_string( + List<(String, String)> self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_record_string_string(item, serializer); + } + } + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_String(self, serializer); + } + } + + @protected + void sse_encode_record_string_string( + (String, String) self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.$1, serializer); + sse_encode_String(self.$2, serializer); + } + + @protected + void sse_encode_u_8(int self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putUint8(self); + } + + @protected + void sse_encode_unit(void self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + } + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } + + @protected + void sse_encode_web_socket_connection_state( + WebSocketConnectionState self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } +} + +@sealed +class AuthHandleImpl extends RustOpaque implements AuthHandle { + // Not to be used by end users + AuthHandleImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + AuthHandleImpl.frbInternalSseDecode(BigInt ptr, int externalSizeOnNative) + : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_AuthHandle, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_AuthHandle, + rustArcDecrementStrongCountPtr: + RustLib.instance.api.rust_arc_decrement_strong_count_AuthHandlePtr, + ); + + /// Disposes the auth session, stopping the token refresh loop and clearing authentication. + void dispose() => RustLib.instance.api.crateAuthHandleDispose(that: this); + + /// Returns whether the user is currently authenticated. + bool isAuthenticated() => + RustLib.instance.api.crateAuthHandleIsAuthenticated(that: this); +} + +@sealed +class CallbackSubscriberDartFnImpl extends RustOpaque + implements CallbackSubscriberDartFn { + // Not to be used by end users + CallbackSubscriberDartFnImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + CallbackSubscriberDartFnImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: RustLib + .instance + .api + .rust_arc_increment_strong_count_CallbackSubscriberDartFn, + rustArcDecrementStrongCount: RustLib + .instance + .api + .rust_arc_decrement_strong_count_CallbackSubscriberDartFn, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_CallbackSubscriberDartFnPtr, + ); + + Future onError({required String message, String? value}) => + RustLib.instance.api.crateCallbackSubscriberDartFnOnError( + that: this, + message: message, + value: value, + ); + + Future onUpdate({required String value}) => RustLib.instance.api + .crateCallbackSubscriberDartFnOnUpdate(that: this, value: value); +} + +@sealed +class CallbackSubscriberImpl extends RustOpaque implements CallbackSubscriber { + // Not to be used by end users + CallbackSubscriberImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + CallbackSubscriberImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_CallbackSubscriber, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_CallbackSubscriber, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_CallbackSubscriberPtr, + ); + + Future onError({required String message, String? value}) => + RustLib.instance.api.crateCallbackSubscriberOnError( + that: this, + message: message, + value: value, + ); + + Future onUpdate({required String value}) => RustLib.instance.api + .crateCallbackSubscriberOnUpdate(that: this, value: value); +} + +@sealed +class MobileConvexClientImpl extends RustOpaque implements MobileConvexClient { + // Not to be used by end users + MobileConvexClientImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + MobileConvexClientImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_MobileConvexClient, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_MobileConvexClient, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_MobileConvexClientPtr, + ); + + /// Executes an action on the Convex backend. + Future action({ + required String name, + required Map args, + }) => RustLib.instance.api.crateMobileConvexClientAction( + that: this, + name: name, + args: args, + ); + + /// Executes a mutation on the Convex backend. + Future mutation({ + required String name, + required Map args, + }) => RustLib.instance.api.crateMobileConvexClientMutation( + that: this, + name: name, + args: args, + ); + + /// Sets up WebSocket connection state change listener. + /// + /// Must be called BEFORE any queries/mutations to capture all state changes. + /// The callback will be invoked whenever the WebSocket transitions between + /// Connected and Connecting states. + /// + /// # Arguments + /// + /// * `on_state_change` - Async callback invoked when connection state changes + /// + /// # Example + /// + /// ```dart + /// await client.onWebsocketStateChange( + /// onStateChange: (state) async { + /// print('Connection state: ${state.name}'); + /// }, + /// ); + /// ``` + Future onWebsocketStateChange({ + required FutureOr Function(WebSocketConnectionState) onStateChange, + }) => RustLib.instance.api.crateMobileConvexClientOnWebsocketStateChange( + that: this, + onStateChange: onStateChange, + ); + + /// Executes a query on the Convex backend. + Future query({ + required String name, + required Map args, + }) => RustLib.instance.api.crateMobileConvexClientQuery( + that: this, + name: name, + args: args, + ); + + /// Sets authentication token for the client. + Future setAuth({String? token}) => RustLib.instance.api + .crateMobileConvexClientSetAuth(that: this, token: token); + + /// Sets authentication with automatic token refresh. + /// + /// The `fetch_token` callback is called: + /// - Immediately to get the initial token + /// - Automatically when the token is about to expire (60 seconds before expiry) + /// + /// The `on_auth_change` callback is called whenever auth state changes. + /// + /// Returns an AuthHandle that can be used to dispose the auth session. + Future setAuthWithRefresh({ + required FutureOr Function() fetchToken, + required FutureOr Function(bool) onAuthChange, + }) => RustLib.instance.api.crateMobileConvexClientSetAuthWithRefresh( + that: this, + fetchToken: fetchToken, + onAuthChange: onAuthChange, + ); + + /// Subscribes to real-time updates from a Convex query. + Future subscribe({ + required String name, + required Map args, + required FutureOr Function(String) onUpdate, + required FutureOr Function(String, String?) onError, + }) => RustLib.instance.api.crateMobileConvexClientSubscribe( + that: this, + name: name, + args: args, + onUpdate: onUpdate, + onError: onError, + ); +} + +@sealed +class SubscriptionHandleImpl extends RustOpaque implements SubscriptionHandle { + // Not to be used by end users + SubscriptionHandleImpl.frbInternalDcoDecode(List wire) + : super.frbInternalDcoDecode(wire, _kStaticData); + + // Not to be used by end users + SubscriptionHandleImpl.frbInternalSseDecode( + BigInt ptr, + int externalSizeOnNative, + ) : super.frbInternalSseDecode(ptr, externalSizeOnNative, _kStaticData); + + static final _kStaticData = RustArcStaticData( + rustArcIncrementStrongCount: + RustLib.instance.api.rust_arc_increment_strong_count_SubscriptionHandle, + rustArcDecrementStrongCount: + RustLib.instance.api.rust_arc_decrement_strong_count_SubscriptionHandle, + rustArcDecrementStrongCountPtr: RustLib + .instance + .api + .rust_arc_decrement_strong_count_SubscriptionHandlePtr, + ); + + /// Cancels the subscription by sending a cancellation signal. + void cancel() => + RustLib.instance.api.crateSubscriptionHandleCancel(that: this); +} diff --git a/third_party/convex_flutter/lib/src/rust/frb_generated.io.dart b/third_party/convex_flutter/lib/src/rust/frb_generated.io.dart new file mode 100644 index 00000000..7a8d9851 --- /dev/null +++ b/third_party/convex_flutter/lib/src/rust/frb_generated.io.dart @@ -0,0 +1,738 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi' as ffi; +import 'frb_generated.dart'; +import 'lib.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_AuthHandlePtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandlePtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CallbackSubscriberPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CallbackSubscriberDartFnPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFnPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_MobileConvexClientPtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClientPtr; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_SubscriptionHandlePtr => wire + ._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandlePtr; + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw); + + @protected + AuthHandle + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ); + + @protected + CallbackSubscriber + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ); + + @protected + CallbackSubscriberDartFn + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ); + + @protected + MobileConvexClient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ); + + @protected + SubscriptionHandle + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ); + + @protected + AuthHandle + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ); + + @protected + CallbackSubscriber + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ); + + @protected + CallbackSubscriberDartFn + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ); + + @protected + MobileConvexClient + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ); + + @protected + SubscriptionHandle + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ); + + @protected + FutureOr Function(String) + dco_decode_DartFn_Inputs_String_Output_unit_AnyhowException(dynamic raw); + + @protected + FutureOr Function(String, String?) + dco_decode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + dynamic raw, + ); + + @protected + FutureOr Function() + dco_decode_DartFn_Inputs__Output_opt_String_AnyhowException(dynamic raw); + + @protected + FutureOr Function(bool) + dco_decode_DartFn_Inputs_bool_Output_unit_AnyhowException(dynamic raw); + + @protected + FutureOr Function(WebSocketConnectionState) + dco_decode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + dynamic raw, + ); + + @protected + Object dco_decode_DartOpaque(dynamic raw); + + @protected + Map dco_decode_Map_String_String_None(dynamic raw); + + @protected + AuthHandle + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ); + + @protected + CallbackSubscriber + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ); + + @protected + CallbackSubscriberDartFn + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ); + + @protected + MobileConvexClient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ); + + @protected + SubscriptionHandle + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + QuerySubscriber dco_decode_TraitDef_QuerySubscriber(dynamic raw); + + @protected + bool dco_decode_bool(dynamic raw); + + @protected + ClientError dco_decode_client_error(dynamic raw); + + @protected + int dco_decode_i_32(dynamic raw); + + @protected + PlatformInt64 dco_decode_isize(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + List<(String, String)> dco_decode_list_record_string_string(dynamic raw); + + @protected + String? dco_decode_opt_String(dynamic raw); + + @protected + (String, String) dco_decode_record_string_string(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + WebSocketConnectionState dco_decode_web_socket_connection_state(dynamic raw); + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + + @protected + AuthHandle + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriber + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriberDartFn + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ); + + @protected + MobileConvexClient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ); + + @protected + SubscriptionHandle + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ); + + @protected + AuthHandle + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriber + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriberDartFn + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ); + + @protected + MobileConvexClient + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ); + + @protected + SubscriptionHandle + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ); + + @protected + Object sse_decode_DartOpaque(SseDeserializer deserializer); + + @protected + Map sse_decode_Map_String_String_None( + SseDeserializer deserializer, + ); + + @protected + AuthHandle + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriber + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriberDartFn + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ); + + @protected + MobileConvexClient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ); + + @protected + SubscriptionHandle + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + ClientError sse_decode_client_error(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + PlatformInt64 sse_decode_isize(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + List<(String, String)> sse_decode_list_record_string_string( + SseDeserializer deserializer, + ); + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + + @protected + (String, String) sse_decode_record_string_string( + SseDeserializer deserializer, + ); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + WebSocketConnectionState sse_decode_web_socket_connection_state( + SseDeserializer deserializer, + ); + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs_String_Output_unit_AnyhowException( + FutureOr Function(String) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + FutureOr Function(String, String?) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs__Output_opt_String_AnyhowException( + FutureOr Function() self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs_bool_Output_unit_AnyhowException( + FutureOr Function(bool) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + FutureOr Function(WebSocketConnectionState) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartOpaque(Object self, SseSerializer serializer); + + @protected + void sse_encode_Map_String_String_None( + Map self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_client_error(ClientError self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_record_string_string( + List<(String, String)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + + @protected + void sse_encode_record_string_string( + (String, String) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_web_socket_connection_state( + WebSocketConnectionState self, + SseSerializer serializer, + ); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + factory RustLibWire.fromExternalLibrary(ExternalLibrary lib) => + RustLibWire(lib.ffiDynamicLibrary); + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustLibWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandlePtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandlePtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandlePtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandlePtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberPtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberPtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFnPtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFnPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFnPtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFnPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClientPtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClientPtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClientPtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClientPtr + .asFunction)>(); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ffi.Pointer ptr, + ) { + return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr, + ); + } + + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandlePtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle', + ); + late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle = + _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandlePtr + .asFunction)>(); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ffi.Pointer ptr, + ) { + return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr, + ); + } + + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandlePtr = + _lookup)>>( + 'frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle', + ); + late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle = + _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandlePtr + .asFunction)>(); +} diff --git a/third_party/convex_flutter/lib/src/rust/frb_generated.web.dart b/third_party/convex_flutter/lib/src/rust/frb_generated.web.dart new file mode 100644 index 00000000..0af0c816 --- /dev/null +++ b/third_party/convex_flutter/lib/src/rust/frb_generated.web.dart @@ -0,0 +1,698 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field + +// Static analysis wrongly picks the IO variant, thus ignore this +// ignore_for_file: argument_type_not_assignable + +import 'dart:async'; +import 'dart:convert'; +import 'frb_generated.dart'; +import 'lib.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart'; + +abstract class RustLibApiImplPlatform extends BaseApiImpl { + RustLibApiImplPlatform({ + required super.handler, + required super.wire, + required super.generalizedFrbRustBinding, + required super.portManager, + }); + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_AuthHandlePtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CallbackSubscriberPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_CallbackSubscriberDartFnPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_MobileConvexClientPtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient; + + CrossPlatformFinalizerArg + get rust_arc_decrement_strong_count_SubscriptionHandlePtr => wire + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle; + + @protected + AnyhowException dco_decode_AnyhowException(dynamic raw); + + @protected + AuthHandle + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ); + + @protected + CallbackSubscriber + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ); + + @protected + CallbackSubscriberDartFn + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ); + + @protected + MobileConvexClient + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ); + + @protected + SubscriptionHandle + dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ); + + @protected + AuthHandle + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ); + + @protected + CallbackSubscriber + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ); + + @protected + CallbackSubscriberDartFn + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ); + + @protected + MobileConvexClient + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ); + + @protected + SubscriptionHandle + dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ); + + @protected + FutureOr Function(String) + dco_decode_DartFn_Inputs_String_Output_unit_AnyhowException(dynamic raw); + + @protected + FutureOr Function(String, String?) + dco_decode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + dynamic raw, + ); + + @protected + FutureOr Function() + dco_decode_DartFn_Inputs__Output_opt_String_AnyhowException(dynamic raw); + + @protected + FutureOr Function(bool) + dco_decode_DartFn_Inputs_bool_Output_unit_AnyhowException(dynamic raw); + + @protected + FutureOr Function(WebSocketConnectionState) + dco_decode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + dynamic raw, + ); + + @protected + Object dco_decode_DartOpaque(dynamic raw); + + @protected + Map dco_decode_Map_String_String_None(dynamic raw); + + @protected + AuthHandle + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + dynamic raw, + ); + + @protected + CallbackSubscriber + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + dynamic raw, + ); + + @protected + CallbackSubscriberDartFn + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + dynamic raw, + ); + + @protected + MobileConvexClient + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + dynamic raw, + ); + + @protected + SubscriptionHandle + dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + dynamic raw, + ); + + @protected + String dco_decode_String(dynamic raw); + + @protected + QuerySubscriber dco_decode_TraitDef_QuerySubscriber(dynamic raw); + + @protected + bool dco_decode_bool(dynamic raw); + + @protected + ClientError dco_decode_client_error(dynamic raw); + + @protected + int dco_decode_i_32(dynamic raw); + + @protected + PlatformInt64 dco_decode_isize(dynamic raw); + + @protected + Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); + + @protected + List<(String, String)> dco_decode_list_record_string_string(dynamic raw); + + @protected + String? dco_decode_opt_String(dynamic raw); + + @protected + (String, String) dco_decode_record_string_string(dynamic raw); + + @protected + int dco_decode_u_8(dynamic raw); + + @protected + void dco_decode_unit(dynamic raw); + + @protected + BigInt dco_decode_usize(dynamic raw); + + @protected + WebSocketConnectionState dco_decode_web_socket_connection_state(dynamic raw); + + @protected + AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); + + @protected + AuthHandle + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriber + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriberDartFn + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ); + + @protected + MobileConvexClient + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ); + + @protected + SubscriptionHandle + sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ); + + @protected + AuthHandle + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriber + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriberDartFn + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ); + + @protected + MobileConvexClient + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ); + + @protected + SubscriptionHandle + sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ); + + @protected + Object sse_decode_DartOpaque(SseDeserializer deserializer); + + @protected + Map sse_decode_Map_String_String_None( + SseDeserializer deserializer, + ); + + @protected + AuthHandle + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriber + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + SseDeserializer deserializer, + ); + + @protected + CallbackSubscriberDartFn + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + SseDeserializer deserializer, + ); + + @protected + MobileConvexClient + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + SseDeserializer deserializer, + ); + + @protected + SubscriptionHandle + sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SseDeserializer deserializer, + ); + + @protected + String sse_decode_String(SseDeserializer deserializer); + + @protected + bool sse_decode_bool(SseDeserializer deserializer); + + @protected + ClientError sse_decode_client_error(SseDeserializer deserializer); + + @protected + int sse_decode_i_32(SseDeserializer deserializer); + + @protected + PlatformInt64 sse_decode_isize(SseDeserializer deserializer); + + @protected + Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); + + @protected + List<(String, String)> sse_decode_list_record_string_string( + SseDeserializer deserializer, + ); + + @protected + String? sse_decode_opt_String(SseDeserializer deserializer); + + @protected + (String, String) sse_decode_record_string_string( + SseDeserializer deserializer, + ); + + @protected + int sse_decode_u_8(SseDeserializer deserializer); + + @protected + void sse_decode_unit(SseDeserializer deserializer); + + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + + @protected + WebSocketConnectionState sse_decode_web_socket_connection_state( + SseDeserializer deserializer, + ); + + @protected + void sse_encode_AnyhowException( + AnyhowException self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs_String_Output_unit_AnyhowException( + FutureOr Function(String) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + FutureOr Function(String, String?) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs__Output_opt_String_AnyhowException( + FutureOr Function() self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartFn_Inputs_bool_Output_unit_AnyhowException( + FutureOr Function(bool) self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + FutureOr Function(WebSocketConnectionState) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_DartOpaque(Object self, SseSerializer serializer); + + @protected + void sse_encode_Map_String_String_None( + Map self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + AuthHandle self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + CallbackSubscriber self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + CallbackSubscriberDartFn self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + MobileConvexClient self, + SseSerializer serializer, + ); + + @protected + void + sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + SubscriptionHandle self, + SseSerializer serializer, + ); + + @protected + void sse_encode_String(String self, SseSerializer serializer); + + @protected + void sse_encode_bool(bool self, SseSerializer serializer); + + @protected + void sse_encode_client_error(ClientError self, SseSerializer serializer); + + @protected + void sse_encode_i_32(int self, SseSerializer serializer); + + @protected + void sse_encode_isize(PlatformInt64 self, SseSerializer serializer); + + @protected + void sse_encode_list_prim_u_8_strict( + Uint8List self, + SseSerializer serializer, + ); + + @protected + void sse_encode_list_record_string_string( + List<(String, String)> self, + SseSerializer serializer, + ); + + @protected + void sse_encode_opt_String(String? self, SseSerializer serializer); + + @protected + void sse_encode_record_string_string( + (String, String) self, + SseSerializer serializer, + ); + + @protected + void sse_encode_u_8(int self, SseSerializer serializer); + + @protected + void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_web_socket_connection_state( + WebSocketConnectionState self, + SseSerializer serializer, + ); +} + +// Section: wire_class + +class RustLibWire implements BaseWire { + RustLibWire.fromExternalLibrary(ExternalLibrary lib); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr, + ); + + void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + int ptr, + ) => wasmModule + .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr, + ); + + void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + int ptr, + ) => wasmModule + .rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr, + ); +} + +@JS('wasm_bindgen') +external RustLibWasmModule get wasmModule; + +@JS() +@anonymous +extension type RustLibWasmModule._(JSObject _) implements JSObject { + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + int ptr, + ); + + external void + rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + int ptr, + ); + + external void + rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + int ptr, + ); +} diff --git a/third_party/convex_flutter/lib/src/rust/lib.dart b/third_party/convex_flutter/lib/src/rust/lib.dart new file mode 100644 index 00000000..30fd23a6 --- /dev/null +++ b/third_party/convex_flutter/lib/src/rust/lib.dart @@ -0,0 +1,162 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import 'frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:freezed_annotation/freezed_annotation.dart' hide protected; +part 'lib.freezed.dart'; + +// These functions are ignored because they are not marked as `pub`: `connected_client`, `decode_jwt_expiry`, `handle_direct_function_result`, `internal_action`, `internal_mutation`, `internal_set_auth`, `internal_subscribe`, `new`, `new`, `parse_json_args` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `JwtClaims` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`, `fmt`, `fmt`, `from`, `from` + +// Rust type: RustOpaqueMoi> +abstract class AuthHandle implements RustOpaqueInterface { + /// Disposes the auth session, stopping the token refresh loop and clearing authentication. + @override + void dispose(); + + /// Returns whether the user is currently authenticated. + bool isAuthenticated(); +} + +// Rust type: RustOpaqueMoi> +abstract class CallbackSubscriber + implements RustOpaqueInterface, QuerySubscriber { + @override + Future onError({required String message, String? value}); + + @override + Future onUpdate({required String value}); +} + +// Rust type: RustOpaqueMoi> +abstract class CallbackSubscriberDartFn + implements RustOpaqueInterface, QuerySubscriber { + @override + Future onError({required String message, String? value}); + + @override + Future onUpdate({required String value}); +} + +// Rust type: RustOpaqueMoi> +abstract class MobileConvexClient implements RustOpaqueInterface { + /// Executes an action on the Convex backend. + Future action({ + required String name, + required Map args, + }); + + /// Executes a mutation on the Convex backend. + Future mutation({ + required String name, + required Map args, + }); + + /// Creates a new MobileConvexClient instance with the given deployment URL and client ID. + factory MobileConvexClient({ + required String deploymentUrl, + required String clientId, + }) => RustLib.instance.api.crateMobileConvexClientNew( + deploymentUrl: deploymentUrl, + clientId: clientId, + ); + + /// Sets up WebSocket connection state change listener. + /// + /// Must be called BEFORE any queries/mutations to capture all state changes. + /// The callback will be invoked whenever the WebSocket transitions between + /// Connected and Connecting states. + /// + /// # Arguments + /// + /// * `on_state_change` - Async callback invoked when connection state changes + /// + /// # Example + /// + /// ```dart + /// await client.onWebsocketStateChange( + /// onStateChange: (state) async { + /// print('Connection state: ${state.name}'); + /// }, + /// ); + /// ``` + Future onWebsocketStateChange({ + required FutureOr Function(WebSocketConnectionState) onStateChange, + }); + + /// Executes a query on the Convex backend. + Future query({ + required String name, + required Map args, + }); + + /// Sets authentication token for the client. + Future setAuth({String? token}); + + /// Sets authentication with automatic token refresh. + /// + /// The `fetch_token` callback is called: + /// - Immediately to get the initial token + /// - Automatically when the token is about to expire (60 seconds before expiry) + /// + /// The `on_auth_change` callback is called whenever auth state changes. + /// + /// Returns an AuthHandle that can be used to dispose the auth session. + Future setAuthWithRefresh({ + required FutureOr Function() fetchToken, + required FutureOr Function(bool) onAuthChange, + }); + + /// Subscribes to real-time updates from a Convex query. + Future subscribe({ + required String name, + required Map args, + required FutureOr Function(String) onUpdate, + required FutureOr Function(String, String?) onError, + }); +} + +// Rust type: RustOpaqueMoi> +abstract class SubscriptionHandle implements RustOpaqueInterface { + /// Cancels the subscription by sending a cancellation signal. + void cancel(); +} + +abstract class QuerySubscriber { + Future onError({required String message, String? value}); + + Future onUpdate({required String value}); +} + +@freezed +sealed class ClientError with _$ClientError implements FrbException { + const ClientError._(); + + /// An internal error within the mobile Convex client. + const factory ClientError.internalError({required String msg}) = + ClientError_InternalError; + + /// An application-specific error from a remote Convex backend function. + const factory ClientError.convexError({required String data}) = + ClientError_ConvexError; + + /// An unexpected server-side error from a remote Convex function. + const factory ClientError.serverError({required String msg}) = + ClientError_ServerError; +} + +/// WebSocket connection state exposed to Flutter/Dart. +/// +/// This enum represents the current state of the WebSocket connection +/// to the Convex backend, allowing real-time connection monitoring. +enum WebSocketConnectionState { + /// The WebSocket is open and connected to the Convex backend. + connected, + + /// The WebSocket is closed and is connecting or reconnecting. + connecting, +} diff --git a/third_party/convex_flutter/lib/src/rust/lib.freezed.dart b/third_party/convex_flutter/lib/src/rust/lib.freezed.dart new file mode 100644 index 00000000..277a15fc --- /dev/null +++ b/third_party/convex_flutter/lib/src/rust/lib.freezed.dart @@ -0,0 +1,378 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'lib.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ClientError { + + + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ClientError); +} + + +@override +int get hashCode => runtimeType.hashCode; + +@override +String toString() { + return 'ClientError()'; +} + + +} + +/// @nodoc +class $ClientErrorCopyWith<$Res> { +$ClientErrorCopyWith(ClientError _, $Res Function(ClientError) __); +} + + +/// Adds pattern-matching-related methods to [ClientError]. +extension ClientErrorPatterns on ClientError { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap({TResult Function( ClientError_InternalError value)? internalError,TResult Function( ClientError_ConvexError value)? convexError,TResult Function( ClientError_ServerError value)? serverError,required TResult orElse(),}){ +final _that = this; +switch (_that) { +case ClientError_InternalError() when internalError != null: +return internalError(_that);case ClientError_ConvexError() when convexError != null: +return convexError(_that);case ClientError_ServerError() when serverError != null: +return serverError(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map({required TResult Function( ClientError_InternalError value) internalError,required TResult Function( ClientError_ConvexError value) convexError,required TResult Function( ClientError_ServerError value) serverError,}){ +final _that = this; +switch (_that) { +case ClientError_InternalError(): +return internalError(_that);case ClientError_ConvexError(): +return convexError(_that);case ClientError_ServerError(): +return serverError(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull({TResult? Function( ClientError_InternalError value)? internalError,TResult? Function( ClientError_ConvexError value)? convexError,TResult? Function( ClientError_ServerError value)? serverError,}){ +final _that = this; +switch (_that) { +case ClientError_InternalError() when internalError != null: +return internalError(_that);case ClientError_ConvexError() when convexError != null: +return convexError(_that);case ClientError_ServerError() when serverError != null: +return serverError(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen({TResult Function( String msg)? internalError,TResult Function( String data)? convexError,TResult Function( String msg)? serverError,required TResult orElse(),}) {final _that = this; +switch (_that) { +case ClientError_InternalError() when internalError != null: +return internalError(_that.msg);case ClientError_ConvexError() when convexError != null: +return convexError(_that.data);case ClientError_ServerError() when serverError != null: +return serverError(_that.msg);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when({required TResult Function( String msg) internalError,required TResult Function( String data) convexError,required TResult Function( String msg) serverError,}) {final _that = this; +switch (_that) { +case ClientError_InternalError(): +return internalError(_that.msg);case ClientError_ConvexError(): +return convexError(_that.data);case ClientError_ServerError(): +return serverError(_that.msg);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String msg)? internalError,TResult? Function( String data)? convexError,TResult? Function( String msg)? serverError,}) {final _that = this; +switch (_that) { +case ClientError_InternalError() when internalError != null: +return internalError(_that.msg);case ClientError_ConvexError() when convexError != null: +return convexError(_that.data);case ClientError_ServerError() when serverError != null: +return serverError(_that.msg);case _: + return null; + +} +} + +} + +/// @nodoc + + +class ClientError_InternalError extends ClientError { + const ClientError_InternalError({required this.msg}): super._(); + + + final String msg; + +/// Create a copy of ClientError +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ClientError_InternalErrorCopyWith get copyWith => _$ClientError_InternalErrorCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ClientError_InternalError&&(identical(other.msg, msg) || other.msg == msg)); +} + + +@override +int get hashCode => Object.hash(runtimeType,msg); + +@override +String toString() { + return 'ClientError.internalError(msg: $msg)'; +} + + +} + +/// @nodoc +abstract mixin class $ClientError_InternalErrorCopyWith<$Res> implements $ClientErrorCopyWith<$Res> { + factory $ClientError_InternalErrorCopyWith(ClientError_InternalError value, $Res Function(ClientError_InternalError) _then) = _$ClientError_InternalErrorCopyWithImpl; +@useResult +$Res call({ + String msg +}); + + + + +} +/// @nodoc +class _$ClientError_InternalErrorCopyWithImpl<$Res> + implements $ClientError_InternalErrorCopyWith<$Res> { + _$ClientError_InternalErrorCopyWithImpl(this._self, this._then); + + final ClientError_InternalError _self; + final $Res Function(ClientError_InternalError) _then; + +/// Create a copy of ClientError +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? msg = null,}) { + return _then(ClientError_InternalError( +msg: null == msg ? _self.msg : msg // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class ClientError_ConvexError extends ClientError { + const ClientError_ConvexError({required this.data}): super._(); + + + final String data; + +/// Create a copy of ClientError +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ClientError_ConvexErrorCopyWith get copyWith => _$ClientError_ConvexErrorCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ClientError_ConvexError&&(identical(other.data, data) || other.data == data)); +} + + +@override +int get hashCode => Object.hash(runtimeType,data); + +@override +String toString() { + return 'ClientError.convexError(data: $data)'; +} + + +} + +/// @nodoc +abstract mixin class $ClientError_ConvexErrorCopyWith<$Res> implements $ClientErrorCopyWith<$Res> { + factory $ClientError_ConvexErrorCopyWith(ClientError_ConvexError value, $Res Function(ClientError_ConvexError) _then) = _$ClientError_ConvexErrorCopyWithImpl; +@useResult +$Res call({ + String data +}); + + + + +} +/// @nodoc +class _$ClientError_ConvexErrorCopyWithImpl<$Res> + implements $ClientError_ConvexErrorCopyWith<$Res> { + _$ClientError_ConvexErrorCopyWithImpl(this._self, this._then); + + final ClientError_ConvexError _self; + final $Res Function(ClientError_ConvexError) _then; + +/// Create a copy of ClientError +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? data = null,}) { + return _then(ClientError_ConvexError( +data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +/// @nodoc + + +class ClientError_ServerError extends ClientError { + const ClientError_ServerError({required this.msg}): super._(); + + + final String msg; + +/// Create a copy of ClientError +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ClientError_ServerErrorCopyWith get copyWith => _$ClientError_ServerErrorCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ClientError_ServerError&&(identical(other.msg, msg) || other.msg == msg)); +} + + +@override +int get hashCode => Object.hash(runtimeType,msg); + +@override +String toString() { + return 'ClientError.serverError(msg: $msg)'; +} + + +} + +/// @nodoc +abstract mixin class $ClientError_ServerErrorCopyWith<$Res> implements $ClientErrorCopyWith<$Res> { + factory $ClientError_ServerErrorCopyWith(ClientError_ServerError value, $Res Function(ClientError_ServerError) _then) = _$ClientError_ServerErrorCopyWithImpl; +@useResult +$Res call({ + String msg +}); + + + + +} +/// @nodoc +class _$ClientError_ServerErrorCopyWithImpl<$Res> + implements $ClientError_ServerErrorCopyWith<$Res> { + _$ClientError_ServerErrorCopyWithImpl(this._self, this._then); + + final ClientError_ServerError _self; + final $Res Function(ClientError_ServerError) _then; + +/// Create a copy of ClientError +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? msg = null,}) { + return _then(ClientError_ServerError( +msg: null == msg ? _self.msg : msg // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/third_party/convex_flutter/lib/src/utils.dart b/third_party/convex_flutter/lib/src/utils.dart new file mode 100644 index 00000000..f1dd0ae0 --- /dev/null +++ b/third_party/convex_flutter/lib/src/utils.dart @@ -0,0 +1,5 @@ +import 'dart:convert'; + +Map buildArgs(Map record) { + return {for (var entry in record.entries) entry.key: jsonEncode(entry.value)}; +} diff --git a/third_party/convex_flutter/linux/CMakeLists.txt b/third_party/convex_flutter/linux/CMakeLists.txt new file mode 100644 index 00000000..11cd02c1 --- /dev/null +++ b/third_party/convex_flutter/linux/CMakeLists.txt @@ -0,0 +1,19 @@ +# The Flutter tooling requires that developers have CMake 3.10 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.10) + +# Project-level configuration. +set(PROJECT_NAME "convex_flutter") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../rust convex_flutter "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(convex_flutter_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/third_party/convex_flutter/macos/Classes/dummy_file.c b/third_party/convex_flutter/macos/Classes/dummy_file.c new file mode 100644 index 00000000..e06dab99 --- /dev/null +++ b/third_party/convex_flutter/macos/Classes/dummy_file.c @@ -0,0 +1 @@ +// This is an empty file to force CocoaPods to create a framework. diff --git a/third_party/convex_flutter/macos/convex_flutter.podspec b/third_party/convex_flutter/macos/convex_flutter.podspec new file mode 100644 index 00000000..15b4329c --- /dev/null +++ b/third_party/convex_flutter/macos/convex_flutter.podspec @@ -0,0 +1,44 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint convex_flutter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'convex_flutter' + s.version = '0.0.1' + s.summary = 'A new Flutter FFI plugin project.' + s.description = <<-DESC +A new Flutter FFI plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + # This will ensure the source files in Classes/ are included in the native + # builds of apps using this FFI plugin. Podspec does not support relative + # paths, so Classes contains a forwarder C file that relatively imports + # `../src/*` so that the C sources can be shared among all target platforms. + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.script_phase = { + :name => 'Build Rust library', + # First argument is relative path to the `rust` folder, second is name of rust library + :script => 'sh "$PODS_TARGET_SRCROOT/../cargokit/build_pod.sh" ../rust convex_flutter', + :execution_position => :before_compile, + :input_files => ['${BUILT_PRODUCTS_DIR}/cargokit_phony'], + # Let XCode know that the static library referenced in -force_load below is + # created by this build step. + :output_files => ["${BUILT_PRODUCTS_DIR}/libconvex_flutter.a"], + } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + # Flutter.framework does not contain a i386 slice. + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + 'OTHER_LDFLAGS' => '-force_load ${BUILT_PRODUCTS_DIR}/libconvex_flutter.a', + } +end diff --git a/third_party/convex_flutter/pubspec.yaml b/third_party/convex_flutter/pubspec.yaml new file mode 100644 index 00000000..fffbd600 --- /dev/null +++ b/third_party/convex_flutter/pubspec.yaml @@ -0,0 +1,95 @@ +name: convex_flutter +description: Multi-platform Convex backend integration for Flutter. Real-time WebSocket, subscriptions, auth, lifecycle management. Supports web (pure Dart) and native. +version: 3.0.1 +repository: https://github.com/jkuldev/convex_flutter +homepage: https://jkuldev.com + +environment: + sdk: ^3.8.1 + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + flutter_rust_bridge: ^2.11.1 + flutter_web_plugins: + sdk: flutter + freezed_annotation: ^3.1.0 + plugin_platform_interface: ^2.0.2 + http: ^1.2.0 # HTTP client for web REST fallback + web: ^1.0.0 # WebSocket API for web platform + +dev_dependencies: + ffi: ^2.1.3 + ffigen: ^13.0.0 + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + integration_test: + sdk: flutter + freezed: ^3.1.0 + build_runner: ^2.5.4 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + # + # Please refer to README.md for a detailed explanation. + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + windows: + ffiPlugin: true + web: + pluginClass: ConvexFlutterWeb + fileName: convex_flutter_web.dart + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/to/font-from-package diff --git a/third_party/convex_flutter/rust/Cargo.lock b/third_party/convex_flutter/rust/Cargo.lock new file mode 100644 index 00000000..77804251 --- /dev/null +++ b/third_party/convex_flutter/rust/Cargo.lock @@ -0,0 +1,2189 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449e356a4864c017286dbbec0e12767ea07efba29e3b7d984194c2a7ff3c4550" +dependencies = [ + "anyhow", + "atomic", + "backtrace", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "archery" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" + +[[package]] +name = "async-once-cell" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convex" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca2fbc7cfc35747ade0731b173cf72c70c7f72c4578440f9a77c871adaa1e2f" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.13.1", + "bytes", + "convex_sync_types", + "futures", + "imbl", + "rand", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "convex_flutter" +version = "0.1.0" +dependencies = [ + "android_logger 0.14.1", + "anyhow", + "async-once-cell", + "convex", + "flutter_rust_bridge", + "futures", + "log", + "maplit", + "once_cell", + "parking_lot", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", +] + +[[package]] +name = "convex_sync_types" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba8235188b091cc50205a436bf6505cb386be208263fecdb4b62bd2b3c90a0a" +dependencies = [ + "anyhow", + "base64 0.13.1", + "bytes", + "derive_more", + "headers", + "rand", + "serde", + "serde_json", + "strum", + "uuid", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dart-sys" +version = "4.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57967e4b200d767d091b961d6ab42cc7d0cc14fe9e052e75d0d3cf9eb732d895" +dependencies = [ + "cc", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "delegate-attr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + +[[package]] +name = "flutter_rust_bridge" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde126295b2acc5f0a712e265e91b6fdc0ed38767496483e592ae7134db83725" +dependencies = [ + "allo-isolate", + "android_logger 0.15.1", + "anyhow", + "build-target", + "bytemuck", + "byteorder", + "console_error_panic_hook", + "dart-sys", + "delegate-attr", + "flutter_rust_bridge_macros", + "futures", + "js-sys", + "lazy_static", + "log", + "oslog", + "portable-atomic", + "threadpool", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "flutter_rust_bridge_macros" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f0420326b13675321b194928bb7830043b68cf8b810e1c651285c747abb080" +dependencies = [ + "hex", + "md-5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imbl" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ea8d4c37ee560727e824d62804183624d371e632019b0e9e3532bce64a33e5" +dependencies = [ + "archery", + "bitmaps", + "equivalent", + "imbl-sized-chunks", + "rand_core", + "rand_xoshiro", + "version_check", + "wide", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.17", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/third_party/convex_flutter/rust/Cargo.toml b/third_party/convex_flutter/rust/Cargo.toml new file mode 100644 index 00000000..d4fd88ca --- /dev/null +++ b/third_party/convex_flutter/rust/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "convex_flutter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +flutter_rust_bridge = "=2.11.1" +tokio = { version = "1", features = ["full"] } +android_logger = { version = "0.14.1" } +log = { version = "0.4.21" } +convex = { version = "0.10.4", features = ["rustls-tls-webpki-roots"] } +anyhow = { version = "1.0.86" } +thiserror = { version = "1.0.61" } +tokio-stream = { features = [ "io-util", "sync" ], version = "0.1" } +once_cell = { version = "1.19.0" } +futures = { version = "0.3" } +parking_lot = { version = "0.12.3" } +async-once-cell = { version = "0.5.3" } +serde_json = { version = "1.0.120" } +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } + +[dev-dependencies] +maplit = { version = "1" } diff --git a/third_party/convex_flutter/rust/example/lib/main.dart b/third_party/convex_flutter/rust/example/lib/main.dart new file mode 100644 index 00000000..58dd592f --- /dev/null +++ b/third_party/convex_flutter/rust/example/lib/main.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:convex_flutter/convex_flutter.dart'; +import 'widgets/connection_status_indicator.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await ConvexClient.initialize( + ConvexConfig( + deploymentUrl: "https://your-deployment.convex.cloud", + clientId: "flutter-example-app", + operationTimeout: const Duration(seconds: 30), + ), + ); + + runApp(const ConvexExampleApp()); +} + +class ConvexExampleApp extends StatelessWidget { + const ConvexExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Convex Flutter - WebSocket State Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const ConnectionDemoScreen(), + ); + } +} + +class ConnectionDemoScreen extends StatefulWidget { + const ConnectionDemoScreen({super.key}); + + @override + State createState() => _ConnectionDemoScreenState(); +} + +class _ConnectionDemoScreenState extends State { + final List _stateHistory = []; + + @override + void initState() { + super.initState(); + _listenToConnectionChanges(); + } + + void _listenToConnectionChanges() { + ConvexClient.instance.connectionState.listen((state) { + setState(() { + _stateHistory.insert(0, ConnectionEvent( + state: state, + timestamp: DateTime.now(), + )); + if (_stateHistory.length > 20) { + _stateHistory.removeLast(); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('WebSocket State Demo'), + actions: const [ConnectionStatusIndicator()], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCurrentStateCard(), + const SizedBox(height: 16), + _buildFeatureCard(), + const SizedBox(height: 16), + _buildHistoryCard(), + ], + ), + ), + ); + } + + Widget _buildCurrentStateCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: StreamBuilder( + stream: ConvexClient.instance.connectionState, + initialData: ConvexClient.instance.currentConnectionState, + builder: (context, snapshot) { + final state = snapshot.data!; + final isConnected = state == WebSocketConnectionState.connected; + return Column( + children: [ + Icon( + isConnected ? Icons.cloud_done : Icons.cloud_sync, + color: isConnected ? Colors.green : Colors.orange, + size: 64, + ), + const SizedBox(height: 12), + Text( + state.name.toUpperCase(), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: isConnected ? Colors.green : Colors.orange, + ), + ), + Text('isConnected: ${ConvexClient.instance.isConnected}'), + ], + ); + }, + ), + ), + ); + } + + Widget _buildFeatureCard() { + return Card( + color: Colors.blue.shade50, + child: const Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('✨ Real-time Connection State', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + SizedBox(height: 8), + Text('• Automatic state updates\n' + '• Two states: Connected/Connecting\n' + '• Access via connectionState stream'), + ], + ), + ), + ); + } + + Widget _buildHistoryCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('History', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + TextButton( + onPressed: () => setState(() => _stateHistory.clear()), + child: const Text('Clear'), + ), + ], + ), + if (_stateHistory.isEmpty) + const Padding( + padding: EdgeInsets.all(16), + child: Text('No state changes yet'), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _stateHistory.length, + itemBuilder: (context, index) { + final event = _stateHistory[index]; + final isConnected = + event.state == WebSocketConnectionState.connected; + return ListTile( + leading: Icon( + isConnected ? Icons.cloud_done : Icons.cloud_sync, + color: isConnected ? Colors.green : Colors.orange, + ), + title: Text(event.state.name.toUpperCase()), + subtitle: Text(event.timestamp.toString()), + ); + }, + ), + ], + ), + ), + ); + } +} + +class ConnectionEvent { + final WebSocketConnectionState state; + final DateTime timestamp; + ConnectionEvent({required this.state, required this.timestamp}); +} diff --git a/third_party/convex_flutter/rust/src/frb_generated.rs b/third_party/convex_flutter/rust/src/frb_generated.rs new file mode 100644 index 00000000..5a4998f1 --- /dev/null +++ b/third_party/convex_flutter/rust/src/frb_generated.rs @@ -0,0 +1,1954 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +#![allow( + non_camel_case_types, + unused, + non_snake_case, + clippy::needless_return, + clippy::redundant_closure_call, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::unused_unit, + clippy::double_parens, + clippy::let_and_return, + clippy::too_many_arguments, + clippy::match_single_binding, + clippy::clone_on_copy, + clippy::let_unit_value, + clippy::deref_addrof, + clippy::explicit_auto_deref, + clippy::borrow_deref_ref, + clippy::needless_borrow +)] + +// Section: imports + +use crate::QuerySubscriber; +use crate::*; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; +use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::{Handler, IntoIntoDart}; + +// Section: boilerplate + +flutter_rust_bridge::frb_generated_boilerplate!( + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, +); +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1095084362; + +// Section: executor + +flutter_rust_bridge::frb_generated_default_handler!(); + +// Section: wire_funcs + +fn wire__crate__AuthHandle_dispose_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "AuthHandle_dispose", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::AuthHandle::dispose(&*api_that_guard); + })?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__AuthHandle_is_authenticated_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "AuthHandle_is_authenticated", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + Result::<_, ()>::Ok(crate::AuthHandle::is_authenticated(&*api_that_guard))?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__CallbackSubscriberDartFn_on_error_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CallbackSubscriberDartFn_on_error", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_message = ::sse_decode(&mut deserializer); + let api_value = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::CallbackSubscriberDartFn::on_error( + &*api_that_guard, + api_message, + api_value, + ); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__CallbackSubscriberDartFn_on_update_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CallbackSubscriberDartFn_on_update", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_value = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::CallbackSubscriberDartFn::on_update(&*api_that_guard, api_value); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__CallbackSubscriber_on_error_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CallbackSubscriber_on_error", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_message = ::sse_decode(&mut deserializer); + let api_value = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::CallbackSubscriber::on_error( + &*api_that_guard, + api_message, + api_value, + ); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__CallbackSubscriber_on_update_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "CallbackSubscriber_on_update", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_value = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::CallbackSubscriber::on_update(&*api_that_guard, api_value); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__MobileConvexClient_action_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_action", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + let api_args = + >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + crate::MobileConvexClient::action(&*api_that_guard, api_name, api_args) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__MobileConvexClient_mutation_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_mutation", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + let api_args = + >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::MobileConvexClient::mutation( + &*api_that_guard, + api_name, + api_args, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__MobileConvexClient_new_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_new", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_deployment_url = ::sse_decode(&mut deserializer); + let api_client_id = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::MobileConvexClient::new( + api_deployment_url, + api_client_id, + ))?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__MobileConvexClient_on_websocket_state_change_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_on_websocket_state_change", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_on_state_change = + decode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + ::sse_decode(&mut deserializer), + ); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::MobileConvexClient::on_websocket_state_change( + &*api_that_guard, + api_on_state_change, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__MobileConvexClient_query_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_query", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + let api_args = + >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + crate::MobileConvexClient::query(&*api_that_guard, api_name, api_args) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__MobileConvexClient_set_auth_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_set_auth", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_token = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + crate::MobileConvexClient::set_auth(&*api_that_guard, api_token) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__MobileConvexClient_set_auth_with_refresh_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_set_auth_with_refresh", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_fetch_token = decode_DartFn_Inputs__Output_opt_String_AnyhowException( + ::sse_decode(&mut deserializer), + ); + let api_on_auth_change = decode_DartFn_Inputs_bool_Output_unit_AnyhowException( + ::sse_decode(&mut deserializer), + ); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::MobileConvexClient::set_auth_with_refresh( + &*api_that_guard, + api_fetch_token, + api_on_auth_change, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__MobileConvexClient_subscribe_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_subscribe", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_name = ::sse_decode(&mut deserializer); + let api_args = + >::sse_decode(&mut deserializer); + let api_on_update = decode_DartFn_Inputs_String_Output_unit_AnyhowException( + ::sse_decode(&mut deserializer), + ); + let api_on_error = decode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + ::sse_decode(&mut deserializer), + ); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::MobileConvexClient::subscribe( + &*api_that_guard, + api_name, + api_args, + api_on_update, + api_on_error, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__SubscriptionHandle_cancel_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "SubscriptionHandle_cancel", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::SubscriptionHandle::cancel(&*api_that_guard); + })?; + Ok(output_ok) + })()) + }, + ) +} + +// Section: related_funcs + +fn decode_DartFn_Inputs_String_Output_unit_AnyhowException( + dart_opaque: flutter_rust_bridge::DartOpaque, +) -> impl Fn(String) -> flutter_rust_bridge::DartFnFuture<()> { + use flutter_rust_bridge::IntoDart; + + async fn body(dart_opaque: flutter_rust_bridge::DartOpaque, arg0: String) -> () { + let args = vec![arg0.into_into_dart().into_dart()]; + let message = FLUTTER_RUST_BRIDGE_HANDLER + .dart_fn_invoke(dart_opaque, args) + .await; + + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let action = deserializer.cursor.read_u8().unwrap(); + let ans = match action { + 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), + 1 => std::result::Result::Err( + ::sse_decode(&mut deserializer), + ), + _ => unreachable!(), + }; + deserializer.end(); + let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); + ans + } + + move |arg0: String| { + flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( + dart_opaque.clone(), + arg0, + )) + } +} +fn decode_DartFn_Inputs_String_opt_String_Output_unit_AnyhowException( + dart_opaque: flutter_rust_bridge::DartOpaque, +) -> impl Fn(String, Option) -> flutter_rust_bridge::DartFnFuture<()> { + use flutter_rust_bridge::IntoDart; + + async fn body( + dart_opaque: flutter_rust_bridge::DartOpaque, + arg0: String, + arg1: Option, + ) -> () { + let args = vec![ + arg0.into_into_dart().into_dart(), + arg1.into_into_dart().into_dart(), + ]; + let message = FLUTTER_RUST_BRIDGE_HANDLER + .dart_fn_invoke(dart_opaque, args) + .await; + + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let action = deserializer.cursor.read_u8().unwrap(); + let ans = match action { + 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), + 1 => std::result::Result::Err( + ::sse_decode(&mut deserializer), + ), + _ => unreachable!(), + }; + deserializer.end(); + let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); + ans + } + + move |arg0: String, arg1: Option| { + flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( + dart_opaque.clone(), + arg0, + arg1, + )) + } +} +fn decode_DartFn_Inputs__Output_opt_String_AnyhowException( + dart_opaque: flutter_rust_bridge::DartOpaque, +) -> impl Fn() -> flutter_rust_bridge::DartFnFuture> { + use flutter_rust_bridge::IntoDart; + + async fn body(dart_opaque: flutter_rust_bridge::DartOpaque) -> Option { + let args = vec![]; + let message = FLUTTER_RUST_BRIDGE_HANDLER + .dart_fn_invoke(dart_opaque, args) + .await; + + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let action = deserializer.cursor.read_u8().unwrap(); + let ans = match action { + 0 => std::result::Result::Ok(>::sse_decode(&mut deserializer)), + 1 => std::result::Result::Err( + ::sse_decode(&mut deserializer), + ), + _ => unreachable!(), + }; + deserializer.end(); + let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); + ans + } + + move || { + flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body(dart_opaque.clone())) + } +} +fn decode_DartFn_Inputs_bool_Output_unit_AnyhowException( + dart_opaque: flutter_rust_bridge::DartOpaque, +) -> impl Fn(bool) -> flutter_rust_bridge::DartFnFuture<()> { + use flutter_rust_bridge::IntoDart; + + async fn body(dart_opaque: flutter_rust_bridge::DartOpaque, arg0: bool) -> () { + let args = vec![arg0.into_into_dart().into_dart()]; + let message = FLUTTER_RUST_BRIDGE_HANDLER + .dart_fn_invoke(dart_opaque, args) + .await; + + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let action = deserializer.cursor.read_u8().unwrap(); + let ans = match action { + 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), + 1 => std::result::Result::Err( + ::sse_decode(&mut deserializer), + ), + _ => unreachable!(), + }; + deserializer.end(); + let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); + ans + } + + move |arg0: bool| { + flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( + dart_opaque.clone(), + arg0, + )) + } +} +fn decode_DartFn_Inputs_web_socket_connection_state_Output_unit_AnyhowException( + dart_opaque: flutter_rust_bridge::DartOpaque, +) -> impl Fn(crate::WebSocketConnectionState) -> flutter_rust_bridge::DartFnFuture<()> { + use flutter_rust_bridge::IntoDart; + + async fn body( + dart_opaque: flutter_rust_bridge::DartOpaque, + arg0: crate::WebSocketConnectionState, + ) -> () { + let args = vec![arg0.into_into_dart().into_dart()]; + let message = FLUTTER_RUST_BRIDGE_HANDLER + .dart_fn_invoke(dart_opaque, args) + .await; + + let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let action = deserializer.cursor.read_u8().unwrap(); + let ans = match action { + 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), + 1 => std::result::Result::Err( + ::sse_decode(&mut deserializer), + ), + _ => unreachable!(), + }; + deserializer.end(); + let ans = ans.expect("Dart throws exception but Rust side assume it is not failable"); + ans + } + + move |arg0: crate::WebSocketConnectionState| { + flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( + dart_opaque.clone(), + arg0, + )) + } +} +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + +// Section: dart2rust + +impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); + } +} + +impl SseDecode for AuthHandle { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for CallbackSubscriber { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for CallbackSubscriberDartFn { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for MobileConvexClient { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for SubscriptionHandle { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for flutter_rust_bridge::DartOpaque { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return unsafe { flutter_rust_bridge::for_generated::sse_decode_dart_opaque(inner) }; + } +} + +impl SseDecode for std::collections::HashMap { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return inner.into_iter().collect(); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi< + flutter_rust_bridge::for_generated::RustAutoOpaqueInner, + > +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +impl SseDecode for crate::ClientError { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_msg = ::sse_decode(deserializer); + return crate::ClientError::InternalError { msg: var_msg }; + } + 1 => { + let mut var_data = ::sse_decode(deserializer); + return crate::ClientError::ConvexError { data: var_data }; + } + 2 => { + let mut var_msg = ::sse_decode(deserializer); + return crate::ClientError::ServerError { msg: var_msg }; + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +impl SseDecode for isize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i64::().unwrap() as _ + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec<(String, String)> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(<(String, String)>::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + +impl SseDecode for (String, String) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = ::sse_decode(deserializer); + let mut var_field1 = ::sse_decode(deserializer); + return (var_field0, var_field1); + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + +impl SseDecode for crate::WebSocketConnectionState { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::WebSocketConnectionState::Connected, + 1 => crate::WebSocketConnectionState::Connecting, + _ => unreachable!("Invalid variant for WebSocketConnectionState: {}", inner), + }; + } +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 3 => wire__crate__CallbackSubscriberDartFn_on_error_impl(port, ptr, rust_vec_len, data_len), + 4 => { + wire__crate__CallbackSubscriberDartFn_on_update_impl(port, ptr, rust_vec_len, data_len) + } + 5 => wire__crate__CallbackSubscriber_on_error_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__CallbackSubscriber_on_update_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__MobileConvexClient_action_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__MobileConvexClient_mutation_impl(port, ptr, rust_vec_len, data_len), + 10 => wire__crate__MobileConvexClient_on_websocket_state_change_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 11 => wire__crate__MobileConvexClient_query_impl(port, ptr, rust_vec_len, data_len), + 12 => wire__crate__MobileConvexClient_set_auth_impl(port, ptr, rust_vec_len, data_len), + 13 => wire__crate__MobileConvexClient_set_auth_with_refresh_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 14 => wire__crate__MobileConvexClient_subscribe_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__AuthHandle_dispose_impl(ptr, rust_vec_len, data_len), + 2 => wire__crate__AuthHandle_is_authenticated_impl(ptr, rust_vec_len, data_len), + 9 => wire__crate__MobileConvexClient_new_impl(ptr, rust_vec_len, data_len), + 15 => wire__crate__SubscriptionHandle_cancel_impl(ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} + +// Section: rust2dart + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for AuthHandle { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} + +impl flutter_rust_bridge::IntoIntoDart> for CallbackSubscriber { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} + +impl flutter_rust_bridge::IntoIntoDart> + for CallbackSubscriberDartFn +{ + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} + +impl flutter_rust_bridge::IntoIntoDart> for MobileConvexClient { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for FrbWrapper +{ +} + +impl flutter_rust_bridge::IntoIntoDart> for SubscriptionHandle { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::ClientError { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::ClientError::InternalError { msg } => { + [0.into_dart(), msg.into_into_dart().into_dart()].into_dart() + } + crate::ClientError::ConvexError { data } => { + [1.into_dart(), data.into_into_dart().into_dart()].into_dart() + } + crate::ClientError::ServerError { msg } => { + [2.into_dart(), msg.into_into_dart().into_dart()].into_dart() + } + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::ClientError {} +impl flutter_rust_bridge::IntoIntoDart for crate::ClientError { + fn into_into_dart(self) -> crate::ClientError { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::WebSocketConnectionState { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Connected => 0.into_dart(), + Self::Connecting => 1.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::WebSocketConnectionState +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::WebSocketConnectionState +{ + fn into_into_dart(self) -> crate::WebSocketConnectionState { + self + } +} + +impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(format!("{:?}", self), serializer); + } +} + +impl SseEncode for AuthHandle { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for CallbackSubscriber { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for CallbackSubscriberDartFn { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + , + >>::sse_encode( + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), + serializer, + ); + } +} + +impl SseEncode for MobileConvexClient { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for SubscriptionHandle { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for flutter_rust_bridge::DartOpaque { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.encode(), serializer); + } +} + +impl SseEncode for std::collections::HashMap { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_iter().collect(), serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi< + flutter_rust_bridge::for_generated::RustAutoOpaqueInner, + > +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_bytes(), serializer); + } +} + +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +impl SseEncode for crate::ClientError { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::ClientError::InternalError { msg } => { + ::sse_encode(0, serializer); + ::sse_encode(msg, serializer); + } + crate::ClientError::ConvexError { data } => { + ::sse_encode(1, serializer); + ::sse_encode(data, serializer); + } + crate::ClientError::ServerError { msg } => { + ::sse_encode(2, serializer); + ::sse_encode(msg, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + +impl SseEncode for isize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_i64::(self as _) + .unwrap(); + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec<(String, String)> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + <(String, String)>::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + +impl SseEncode for (String, String) { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.0, serializer); + ::sse_encode(self.1, serializer); + } +} + +impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self).unwrap(); + } +} + +impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + +impl SseEncode for crate::WebSocketConnectionState { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::WebSocketConnectionState::Connected => 0, + crate::WebSocketConnectionState::Connecting => 1, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +#[cfg(not(target_family = "wasm"))] +mod io { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.11.1. + + // Section: imports + + use super::*; + use crate::QuerySubscriber; + use crate::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_io!(); + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_convex_flutter_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(not(target_family = "wasm"))] +pub use io::*; + +/// cbindgen:ignore +#[cfg(target_family = "wasm")] +mod web { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.11.1. + + // Section: imports + + use super::*; + use crate::QuerySubscriber; + use crate::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_web!(); + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerAuthHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriber( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCallbackSubscriberDartFn( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSubscriptionHandle( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(target_family = "wasm")] +pub use web::*; diff --git a/third_party/convex_flutter/rust/src/lib.rs b/third_party/convex_flutter/rust/src/lib.rs new file mode 100644 index 00000000..c6b521ec --- /dev/null +++ b/third_party/convex_flutter/rust/src/lib.rs @@ -0,0 +1,535 @@ +mod frb_generated; +use std::{ + collections::{BTreeMap, HashMap}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; + +#[cfg(debug_assertions)] +use android_logger::Config; +use async_once_cell::OnceCell; +use convex::{ + AuthTokenFetcher, + AuthenticationToken, + ConvexClient, + ConvexClientBuilder, + FunctionResult, + Value, // Convex client and result types + WebSocketState as ConvexWebSocketState, +}; +use flutter_rust_bridge::{frb, DartFnFuture}; +use futures::{ + channel::oneshot::{self, Sender}, + pin_mut, select_biased, FutureExt, StreamExt, +}; +use log::debug; // Logging for debugging purposes +#[cfg(debug_assertions)] +use log::LevelFilter; +use parking_lot::Mutex; +// Custom error type for Convex client operations, exposed to Dart. +#[derive(Debug, thiserror::Error)] +#[frb] +pub enum ClientError { + /// An internal error within the mobile Convex client. + #[error("InternalError: {msg}")] + InternalError { msg: String }, + /// An application-specific error from a remote Convex backend function. + #[error("ConvexError: {data}")] + ConvexError { data: String }, + /// An unexpected server-side error from a remote Convex function. + #[error("ServerError: {msg}")] + ServerError { msg: String }, +} + +impl From for ClientError { + fn from(value: anyhow::Error) -> Self { + Self::InternalError { + msg: value.to_string(), + } + } +} + +/// WebSocket connection state exposed to Flutter/Dart. +/// +/// This enum represents the current state of the WebSocket connection +/// to the Convex backend, allowing real-time connection monitoring. +#[derive(Debug, Clone)] +#[frb] +pub enum WebSocketConnectionState { + /// The WebSocket is open and connected to the Convex backend. + Connected, + /// The WebSocket is closed and is connecting or reconnecting. + Connecting, +} + +impl From for WebSocketConnectionState { + fn from(state: ConvexWebSocketState) -> Self { + match state { + ConvexWebSocketState::Connected => WebSocketConnectionState::Connected, + ConvexWebSocketState::Connecting => WebSocketConnectionState::Connecting, + } + } +} + +/// Trait defining the interface for handling subscription updates. +// Not directly exposed to Dart, used internally by subscribers. +pub trait QuerySubscriber: Send + Sync { + fn on_update(&self, value: String); // Called when a new update is received + fn on_error(&self, message: String, value: Option); // Called on error with optional value +} + +/// Adapter struct to implement QuerySubscriber using Dart callbacks. +pub struct CallbackSubscriber { + on_update: Box, // Callback for updates + on_error: Box) + Send + Sync>, // Callback for errors +} + +impl QuerySubscriber for CallbackSubscriber { + fn on_update(&self, value: String) { + (self.on_update)(value); + } + + fn on_error(&self, message: String, value: Option) { + (self.on_error)(message, value); + } +} + +/// Opaque type for Dart, representing a subscription handle with cancellation. +#[frb(opaque)] +pub struct SubscriptionHandle { + cancel_sender: Arc>>>, // Sender to cancel the subscription +} + +impl SubscriptionHandle { + fn new(cancel_sender: Sender<()>) -> Self { + SubscriptionHandle { + cancel_sender: Arc::new(Mutex::new(Some(cancel_sender))), + } + } + + /// Cancels the subscription by sending a cancellation signal. + #[frb(sync)] + pub fn cancel(&self) { + if let Some(sender) = self.cancel_sender.lock().take() { + sender.send(()).unwrap(); + } + } +} + +/// Opaque type for Dart, representing an auth session handle with lifecycle management. +/// Used to control the token refresh loop and check authentication state. +#[frb(opaque)] +pub struct AuthHandle { + cancel_sender: Arc>>>, + is_authenticated: Arc, +} + +impl AuthHandle { + fn new(cancel_sender: Sender<()>, is_authenticated: Arc) -> Self { + AuthHandle { + cancel_sender: Arc::new(Mutex::new(Some(cancel_sender))), + is_authenticated, + } + } + + /// Disposes the auth session, stopping the token refresh loop and clearing authentication. + #[frb(sync)] + pub fn dispose(&self) { + if let Some(sender) = self.cancel_sender.lock().take() { + let _ = sender.send(()); + } + } + + /// Returns whether the user is currently authenticated. + #[frb(sync)] + pub fn is_authenticated(&self) -> bool { + self.is_authenticated.load(Ordering::SeqCst) + } +} + +/// Adapter for Dart functions as subscribers, handling async callbacks. +pub struct CallbackSubscriberDartFn { + on_update: Box DartFnFuture<()> + Send + Sync>, // Async update callback + on_error: Box) -> DartFnFuture<()> + Send + Sync>, // Async error callback +} + +impl QuerySubscriber for CallbackSubscriberDartFn { + fn on_update(&self, value: String) { + let future = (self.on_update)(value); + tokio::spawn(async move { + let _ = future.await; // Await the future, ignoring the result + }); + } + + fn on_error(&self, message: String, value: Option) { + let future = (self.on_error)(message, value); + tokio::spawn(async move { + let _ = future.await; + }); + } +} + +/// Main Convex client struct, opaque to Dart, managing connections and operations. +#[frb(opaque)] +pub struct MobileConvexClient { + deployment_url: String, // URL of the Convex deployment + client_id: String, // Client ID for authentication + client: OnceCell, // Lazy-initialized Convex client + rt: tokio::runtime::Runtime, // Tokio runtime for async operations + // Channel sender for WebSocket state change notifications + state_change_sender: Arc>>>, +} + +impl MobileConvexClient { + /// Creates a new MobileConvexClient instance with the given deployment URL and client ID. + #[frb(sync)] + pub fn new(deployment_url: String, client_id: String) -> MobileConvexClient { + #[cfg(debug_assertions)] + android_logger::init_once(Config::default().with_max_level(LevelFilter::Error)); + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + MobileConvexClient { + deployment_url, + client_id, + client: OnceCell::new(), + rt, + state_change_sender: Arc::new(Mutex::new(None)), + } + } + + /// Sets up WebSocket connection state change listener. + /// + /// Must be called BEFORE any queries/mutations to capture all state changes. + /// The callback will be invoked whenever the WebSocket transitions between + /// Connected and Connecting states. + /// + /// # Arguments + /// + /// * `on_state_change` - Async callback invoked when connection state changes + /// + /// # Example + /// + /// ```dart + /// await client.onWebsocketStateChange( + /// onStateChange: (state) async { + /// print('Connection state: ${state.name}'); + /// }, + /// ); + /// ``` + #[frb] + pub async fn on_websocket_state_change( + &self, + on_state_change: impl Fn(WebSocketConnectionState) -> DartFnFuture<()> + Send + Sync + 'static, + ) -> Result<(), ClientError> { + println!("RUST: on_websocket_state_change() called"); + + // Create tokio mpsc channel for receiving state changes from convex client + let (state_tx, mut state_rx) = tokio::sync::mpsc::channel::(10); + println!("RUST: Created mpsc channel for state changes"); + + // Store sender for use when initializing the client + { + let mut sender = self.state_change_sender.lock(); + *sender = Some(state_tx); + println!("RUST: Stored state_tx in state_change_sender"); + } + + // Spawn task to listen for state changes and call Dart callback + let on_state_change = Arc::new(on_state_change); + println!("RUST: Spawning listener task for state changes"); + self.rt.spawn(async move { + println!("RUST: Listener task started, waiting for state changes"); + while let Some(state) = state_rx.recv().await { + println!("RUST: Received state change from channel: {:?}", state); + let dart_state = WebSocketConnectionState::from(state); + println!("RUST: Converted to Dart state: {:?}", dart_state); + let callback = on_state_change.clone(); + let future = (callback)(dart_state); + println!("RUST: Calling Dart callback"); + let _ = future.await; + println!("RUST: Dart callback completed"); + } + println!("RUST: Listener task exiting (channel closed)"); + }); + + println!("RUST: on_websocket_state_change() returning"); + Ok(()) + } + + /// Retrieves or initializes a connected Convex client. + async fn connected_client(&self) -> anyhow::Result { + let url = self.deployment_url.clone(); + let state_sender = self.state_change_sender.lock().clone(); + + println!( + "RUST: connected_client() called with sender: {:?}", + state_sender.is_some() + ); + + self.client + .get_or_try_init(async { + let client_id = self.client_id.to_owned(); + + // Build client directly without spawning a task + // This ensures callback is registered BEFORE connection starts + println!("RUST: Building ConvexClient directly (no task spawn)"); + let mut builder = ConvexClientBuilder::new(url.as_str()).with_client_id(&client_id); + + // Register state change callback BEFORE building + if let Some(sender) = state_sender { + println!("RUST: Registering state change callback with builder"); + builder = builder.with_on_state_change(sender); + } else { + println!( + "RUST WARNING: No sender available - state changes will not be emitted" + ); + } + + println!("RUST: Calling builder.build() - connection will start now"); + let result = builder.build().await; + match &result { + Ok(_) => println!("RUST: ConvexClient built successfully"), + Err(e) => println!("RUST ERROR: Failed to build ConvexClient: {:?}", e), + } + result + }) + .await + .map(|client_ref| client_ref.clone()) + } + + /// Executes a query on the Convex backend. + #[frb] + pub async fn query( + &self, + name: String, + args: HashMap, + ) -> Result { + let mut client = self.connected_client().await?; + debug!("got the client"); + let result = client.query(name.as_str(), parse_json_args(args)).await?; + debug!("got the result"); + handle_direct_function_result(result) + } + + /// Subscribes to real-time updates from a Convex query. + #[frb] + pub async fn subscribe( + &self, + name: String, + args: HashMap, + on_update: impl Fn(String) -> DartFnFuture<()> + Send + Sync + 'static, + on_error: impl Fn(String, Option) -> DartFnFuture<()> + Send + Sync + 'static, + ) -> Result { + let subscriber = Arc::new(CallbackSubscriberDartFn { + on_update: Box::new(on_update), + on_error: Box::new(on_error), + }); + self.internal_subscribe(name, args, subscriber) + .await + .map_err(Into::into) + } + + /// Internal method for subscription logic. + async fn internal_subscribe( + &self, + name: String, + args: HashMap, + subscriber: Arc, + ) -> anyhow::Result { + let mut client = self.connected_client().await?; + debug!("New subscription"); + let mut subscription = client + .subscribe(name.as_str(), parse_json_args(args)) + .await?; + let (cancel_sender, cancel_receiver) = oneshot::channel::<()>(); + self.rt.spawn(async move { + let cancel_fut = cancel_receiver.fuse(); + pin_mut!(cancel_fut); + loop { + select_biased! { + new_val = subscription.next().fuse() => { + let new_val = match new_val { + Some(val) => val, + None => { + log::warn!("Subscription stream ended for {}", &name); + break; + } + }; + match new_val { + FunctionResult::Value(value) => { + debug!("Updating with {value:?}"); + subscriber.on_update(serde_json::to_string( + &serde_json::Value::from(value), + ).unwrap()); + } + FunctionResult::ErrorMessage(message) => { + subscriber.on_error(message, None); + } + FunctionResult::ConvexError(error) => subscriber.on_error( + error.message, + Some(serde_json::ser::to_string( + &serde_json::Value::from(error.data), + ).unwrap()), + ), + } + } + _ = cancel_fut => { + break; + } + } + } + debug!("Subscription canceled"); + }); + Ok(SubscriptionHandle::new(cancel_sender)) + } + + /// Executes a mutation on the Convex backend. + #[frb] + pub async fn mutation( + &self, + name: String, + args: HashMap, + ) -> Result { + let result = self.internal_mutation(name, args).await?; + handle_direct_function_result(result) + } + + /// Internal method for mutation logic. + async fn internal_mutation( + &self, + name: String, + args: HashMap, + ) -> anyhow::Result { + let mut client = self.connected_client().await?; + self.rt + .spawn(async move { client.mutation(&name, parse_json_args(args)).await }) + .await? + } + + /// Executes an action on the Convex backend. + #[frb] + pub async fn action( + &self, + name: String, + args: HashMap, + ) -> Result { + debug!("Running action: {}", name); + let result = self.internal_action(name, args).await?; + debug!("Got action result: {:?}", result); + handle_direct_function_result(result) + } + + /// Internal method for action logic. + async fn internal_action( + &self, + name: String, + args: HashMap, + ) -> anyhow::Result { + let mut client = self.connected_client().await?; + debug!("Running action: {}", name); + self.rt + .spawn(async move { client.action(&name, parse_json_args(args)).await }) + .await? + } + + /// Sets authentication token for the client. + #[frb] + pub async fn set_auth(&self, token: Option) -> Result<(), ClientError> { + Ok(self.internal_set_auth(token).await?) + } + + /// Internal method for setting authentication. + async fn internal_set_auth(&self, token: Option) -> anyhow::Result<()> { + let mut client = self.connected_client().await?; + self.rt + .spawn(async move { client.set_auth(token).await }) + .await + .map_err(|e| e.into()) + } + + /// Sets authentication with token refresh on every WebSocket reconnect. + /// + /// The callback is owned by the upstream Convex client so authentication + /// and query state are replayed together after a disconnect. + /// + /// Returns an AuthHandle that can be used to dispose the auth session. + #[frb] + pub async fn set_auth_with_refresh( + &self, + fetch_token: impl Fn() -> DartFnFuture> + Send + Sync + 'static, + on_auth_change: impl Fn(bool) -> DartFnFuture<()> + Send + Sync + 'static, + ) -> Result { + let is_authenticated = Arc::new(AtomicBool::new(false)); + let (cancel_sender, cancel_receiver) = oneshot::channel::<()>(); + + let mut client = self.connected_client().await?; + let fetch_token = Arc::new(fetch_token); + let on_auth_change = Arc::new(on_auth_change); + let cancel_on_auth_change = on_auth_change.clone(); + let callback_is_authenticated = is_authenticated.clone(); + let callback: AuthTokenFetcher = Box::new(move |_force_refresh| { + let fetch_token = fetch_token.clone(); + let on_auth_change = on_auth_change.clone(); + let is_authenticated = callback_is_authenticated.clone(); + Box::pin(async move { + let token = (fetch_token)().await; + let next_is_authenticated = token.is_some(); + let changed = is_authenticated.swap(next_is_authenticated, Ordering::SeqCst) + != next_is_authenticated; + if changed { + let _ = (on_auth_change)(next_is_authenticated).await; + } + Ok(match token { + Some(token) => AuthenticationToken::User(token), + None => AuthenticationToken::None, + }) + }) + }); + client.set_auth_callback(Some(callback)).await; + + let cancel_is_authenticated = is_authenticated.clone(); + self.rt.spawn(async move { + let _ = cancel_receiver.await; + let mut client = client.clone(); + client.set_auth_callback(None).await; + if cancel_is_authenticated.swap(false, Ordering::SeqCst) { + let _ = (cancel_on_auth_change)(false).await; + } + }); + + Ok(AuthHandle::new(cancel_sender, is_authenticated)) + } +} + +/// Utility function to parse HashMap arguments into Convex Value format. +fn parse_json_args(raw_args: HashMap) -> BTreeMap { + raw_args + .into_iter() + .map(|(k, v)| { + ( + k, + Value::try_from( + serde_json::from_str::(&v) + .expect("Invalid JSON data from FFI"), + ) + .expect("Invalid Convex data from FFI"), + ) + }) + .collect() +} + +/// Utility function to handle and serialize FunctionResult into a string or error. +fn handle_direct_function_result(result: FunctionResult) -> Result { + match result { + FunctionResult::Value(v) => serde_json::to_string(&serde_json::Value::from(v)) + .map_err(|e| ClientError::InternalError { msg: e.to_string() }), + FunctionResult::ConvexError(e) => Err(ClientError::ConvexError { + data: serde_json::ser::to_string(&serde_json::Value::from(e.data)).unwrap(), + }), + FunctionResult::ErrorMessage(msg) => Err(ClientError::ServerError { msg }), + } +} diff --git a/third_party/convex_flutter/test_driver/integration_test.dart b/third_party/convex_flutter/test_driver/integration_test.dart new file mode 100644 index 00000000..b38629cc --- /dev/null +++ b/third_party/convex_flutter/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/third_party/convex_flutter/windows/CMakeLists.txt b/third_party/convex_flutter/windows/CMakeLists.txt new file mode 100644 index 00000000..4c825e7a --- /dev/null +++ b/third_party/convex_flutter/windows/CMakeLists.txt @@ -0,0 +1,20 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "convex_flutter") +project(${PROJECT_NAME} LANGUAGES CXX) + +include("../cargokit/cmake/cargokit.cmake") +apply_cargokit(${PROJECT_NAME} ../rust convex_flutter "") + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(convex_flutter_bundled_libraries + "${${PROJECT_NAME}_cargokit_lib}" + PARENT_SCOPE +) diff --git a/tool/convex_client_gauntlet/runtime/app/pubspec.lock b/tool/convex_client_gauntlet/runtime/app/pubspec.lock index f59c8545..fa57fa78 100644 --- a/tool/convex_client_gauntlet/runtime/app/pubspec.lock +++ b/tool/convex_client_gauntlet/runtime/app/pubspec.lock @@ -68,10 +68,9 @@ packages: convex_flutter: dependency: transitive description: - name: convex_flutter - sha256: db3bca4e3e6792eadadba9a662225ccb743c287f4550879f67885cd40a2198f2 - url: "https://pub.dev" - source: hosted + path: "../../../../third_party/convex_flutter" + relative: true + source: path version: "3.0.1" crypto: dependency: transitive diff --git a/tool/convex_client_gauntlet/runtime/lib/runner.dart b/tool/convex_client_gauntlet/runtime/lib/runner.dart index 35010823..279eb821 100644 --- a/tool/convex_client_gauntlet/runtime/lib/runner.dart +++ b/tool/convex_client_gauntlet/runtime/lib/runner.dart @@ -9,6 +9,11 @@ import 'workload.dart'; typedef TransportFactory = Future Function(); +const rejectedExpiredAccessToken = + 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.' + 'eyJleHAiOjB9.' + 'rejected'; + final class GauntletFailure implements Exception { const GauntletFailure(this.code, this.message); @@ -383,6 +388,8 @@ final class GauntletRunner { 'rejectedTokenObserved': false, 'refreshSessionCalled': false, 'tokenChanged': false, + 'reconnectCalled': false, + 'recoveryMs': null, 'acceptedAfterRefresh': false, 'queuedBatchReplayedExactlyOnce': false, }, @@ -412,7 +419,7 @@ final class GauntletRunner { auth['exercised'] = true; var rejected = false; try { - await candidate.authenticate('invalid.invalid.invalid'); + await candidate.injectRejectedAuth(rejectedExpiredAccessToken); await candidate .mutation('ops:applyBatch', { 'strategyPublicId': strategyId(seed), @@ -437,10 +444,10 @@ final class GauntletRunner { } auth['refreshSessionCalled'] = true; auth['tokenChanged'] = nextSession.accessToken != oldAccessToken; - // Make the rejected-to-fresh transition explicit. convex_flutter can retain - // its rejected auth state when one static token is replaced directly. - await candidate.authenticate(null); - await candidate.authenticate(nextSession.accessToken); + final recovery = Stopwatch()..start(); + await candidate.recoverAuth(nextSession.accessToken); + await candidate.reconnect(); + auth['reconnectCalled'] = true; Object? me; for (var attempt = 0; attempt < 20 && me == null; attempt += 1) { await Future.delayed(const Duration(milliseconds: 250)); @@ -458,6 +465,8 @@ final class GauntletRunner { 'Fresh access token was not accepted within the bounded recovery window', ); } + recovery.stop(); + auth['recoveryMs'] = recovery.elapsedMicroseconds / 1000; auth['acceptedAfterRefresh'] = true; return nextSession; } diff --git a/tool/convex_client_gauntlet/runtime/lib/transport.dart b/tool/convex_client_gauntlet/runtime/lib/transport.dart index 5d9f23e1..063b9f5e 100644 --- a/tool/convex_client_gauntlet/runtime/lib/transport.dart +++ b/tool/convex_client_gauntlet/runtime/lib/transport.dart @@ -13,6 +13,10 @@ abstract interface class IcarusConvexTransport { Future authenticate(String? token); + Future injectRejectedAuth(String token); + + Future recoverAuth(String token); + Future mutation(String path, Map arguments); Future query(String path, Map arguments); @@ -59,6 +63,12 @@ final class DartvexTransport implements IcarusConvexTransport { @override Future authenticate(String? token) => _client.setAuth(token); + @override + Future injectRejectedAuth(String token) => _client.setAuth(token); + + @override + Future recoverAuth(String token) => _client.setAuth(token); + @override Future mutation(String path, Map arguments) async { _recordSend(path, arguments); @@ -128,6 +138,7 @@ final class ConvexFlutterTransport implements IcarusConvexTransport { final convex_flutter.ConvexClient _client; convex_flutter.AuthHandleWrapper? _authHandle; + String? _nextAuthToken; int _bytesSent = 0; int _bytesReceived = 0; @@ -153,17 +164,33 @@ final class ConvexFlutterTransport implements IcarusConvexTransport { int get bytesReceived => _bytesReceived; @override - Future authenticate(String? token) async { + Future authenticate(String? token) => _replaceRefreshHandle(token); + + @override + Future injectRejectedAuth(String token) => _replaceRefreshHandle(token); + + @override + Future recoverAuth(String token) async { + if (_authHandle == null) { + throw StateError('convex_flutter has no refresh handle to recover'); + } + // Keep the rejected handle alive. The native client asks this callback for + // a fresh token when it reconnects after the server rejects the old one. + _nextAuthToken = token; + } + + Future _replaceRefreshHandle(String? token) async { _authHandle?.dispose(); _authHandle = null; + _nextAuthToken = null; await _client.clearAuth(); - // The package's native refresh handle cancels asynchronously and clears - // auth as it exits. Let that cancellation settle before installing the - // replacement handle so it cannot erase the fresh token afterward. - await Future.delayed(const Duration(milliseconds: 25)); + // The native handle clears auth asynchronously when its cancellation wakes. + // Let that task settle before creating the replacement handle. + await Future.delayed(const Duration(milliseconds: 50)); if (token == null) return; + _nextAuthToken = token; _authHandle = await _client.setAuthWithRefresh( - fetchToken: () async => token, + fetchToken: () async => _nextAuthToken, ); } @@ -242,6 +269,7 @@ final class ConvexFlutterTransport implements IcarusConvexTransport { Future close() async { _authHandle?.dispose(); _authHandle = null; + _nextAuthToken = null; _client.dispose(); } diff --git a/tool/convex_client_gauntlet/runtime/pubspec.lock b/tool/convex_client_gauntlet/runtime/pubspec.lock index e29e4a9f..ebe381fb 100644 --- a/tool/convex_client_gauntlet/runtime/pubspec.lock +++ b/tool/convex_client_gauntlet/runtime/pubspec.lock @@ -68,10 +68,9 @@ packages: convex_flutter: dependency: "direct main" description: - name: convex_flutter - sha256: db3bca4e3e6792eadadba9a662225ccb743c287f4550879f67885cd40a2198f2 - url: "https://pub.dev" - source: hosted + path: "../../../third_party/convex_flutter" + relative: true + source: path version: "3.0.1" crypto: dependency: "direct main" diff --git a/tool/convex_client_gauntlet/runtime/pubspec.yaml b/tool/convex_client_gauntlet/runtime/pubspec.yaml index 016caf9a..e31de1b0 100644 --- a/tool/convex_client_gauntlet/runtime/pubspec.yaml +++ b/tool/convex_client_gauntlet/runtime/pubspec.yaml @@ -9,7 +9,8 @@ environment: dependencies: flutter: sdk: flutter - convex_flutter: 3.0.1 + convex_flutter: + path: ../../../third_party/convex_flutter crypto: 3.0.7 dartvex: 0.2.0 # convex_flutter's generated Rust bridge is pinned to this runtime version. diff --git a/tool/convex_client_gauntlet/runtime/test/workload_test.dart b/tool/convex_client_gauntlet/runtime/test/workload_test.dart index 2f86e8f3..678d9036 100644 --- a/tool/convex_client_gauntlet/runtime/test/workload_test.dart +++ b/tool/convex_client_gauntlet/runtime/test/workload_test.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:flutter_test/flutter_test.dart'; +import 'package:icarus_convex_runtime_gauntlet/runner.dart'; import 'package:icarus_convex_runtime_gauntlet/workload.dart'; void main() { @@ -57,4 +60,13 @@ void main() { expect(canonicalHash(state(1, 2)), canonicalHash(state(10, 20))); }); + + test('rejected auth fixture is an expired, decodable JWT', () { + final parts = rejectedExpiredAccessToken.split('.'); + expect(parts, hasLength(3)); + final claims = + jsonDecode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1])))) + as Map; + expect(claims['exp'], 0); + }); } From ada27560062f045f5b8b3bd5854772836459d76f Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 27 Aug 2026 03:00:09 -0400 Subject: [PATCH 09/11] fix: serialize Convex auth handle replacement --- third_party/convex_flutter/ICARUS_PATCH.md | 4 + third_party/convex_flutter/rust/src/lib.rs | 20 ++++- .../runtime/lib/transport.dart | 5 +- .../runtime/tool/run_paired_profile.sh | 85 +++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100755 tool/convex_client_gauntlet/runtime/tool/run_paired_profile.sh diff --git a/third_party/convex_flutter/ICARUS_PATCH.md b/third_party/convex_flutter/ICARUS_PATCH.md index 7ac771ca..2fe244d3 100644 --- a/third_party/convex_flutter/ICARUS_PATCH.md +++ b/third_party/convex_flutter/ICARUS_PATCH.md @@ -14,6 +14,10 @@ reconnect. The old adapter owned a separate expiry timer and called static `set_auth`, which could leave the client disconnected after the server rejected an expired token. +Auth handles also carry an internal generation. Disposal only clears auth when +the handle still owns the current generation, so a delayed cancellation from a +replaced handle cannot erase the fresh callback. + No generated Dart or Rust bridge file is edited: the public bridge signature is unchanged. The hand-written changes are limited to `rust/src/lib.rs` and the minimum `convex` crate version in `rust/Cargo.toml`; `rust/Cargo.lock` is diff --git a/third_party/convex_flutter/rust/src/lib.rs b/third_party/convex_flutter/rust/src/lib.rs index c6b521ec..d9184280 100644 --- a/third_party/convex_flutter/rust/src/lib.rs +++ b/third_party/convex_flutter/rust/src/lib.rs @@ -2,7 +2,7 @@ mod frb_generated; use std::{ collections::{BTreeMap, HashMap}, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicU64, Ordering}, Arc, }, }; @@ -178,6 +178,7 @@ pub struct MobileConvexClient { client_id: String, // Client ID for authentication client: OnceCell, // Lazy-initialized Convex client rt: tokio::runtime::Runtime, // Tokio runtime for async operations + auth_generation: Arc, // Channel sender for WebSocket state change notifications state_change_sender: Arc>>>, } @@ -197,6 +198,7 @@ impl MobileConvexClient { client_id, client: OnceCell::new(), rt, + auth_generation: Arc::new(AtomicU64::new(0)), state_change_sender: Arc::new(Mutex::new(None)), } } @@ -444,6 +446,9 @@ impl MobileConvexClient { /// Internal method for setting authentication. async fn internal_set_auth(&self, token: Option) -> anyhow::Result<()> { + // Invalidate any older refresh handle before replacing its callback. + // A delayed disposal from that handle must not clear this auth state. + self.auth_generation.fetch_add(1, Ordering::SeqCst); let mut client = self.connected_client().await?; self.rt .spawn(async move { client.set_auth(token).await }) @@ -465,6 +470,7 @@ impl MobileConvexClient { ) -> Result { let is_authenticated = Arc::new(AtomicBool::new(false)); let (cancel_sender, cancel_receiver) = oneshot::channel::<()>(); + let generation = self.auth_generation.fetch_add(1, Ordering::SeqCst) + 1; let mut client = self.connected_client().await?; let fetch_token = Arc::new(fetch_token); @@ -492,8 +498,20 @@ impl MobileConvexClient { client.set_auth_callback(Some(callback)).await; let cancel_is_authenticated = is_authenticated.clone(); + let cancel_auth_generation = self.auth_generation.clone(); self.rt.spawn(async move { let _ = cancel_receiver.await; + if cancel_auth_generation + .compare_exchange( + generation, + generation + 1, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .is_err() + { + return; + } let mut client = client.clone(); client.set_auth_callback(None).await; if cancel_is_authenticated.swap(false, Ordering::SeqCst) { diff --git a/tool/convex_client_gauntlet/runtime/lib/transport.dart b/tool/convex_client_gauntlet/runtime/lib/transport.dart index 063b9f5e..74be617e 100644 --- a/tool/convex_client_gauntlet/runtime/lib/transport.dart +++ b/tool/convex_client_gauntlet/runtime/lib/transport.dart @@ -174,8 +174,9 @@ final class ConvexFlutterTransport implements IcarusConvexTransport { if (_authHandle == null) { throw StateError('convex_flutter has no refresh handle to recover'); } - // Keep the rejected handle alive. The native client asks this callback for - // a fresh token when it reconnects after the server rejects the old one. + // The stored upstream callback reads this value during the reconnect that + // follows the auth rejection. Replacing the callback here could leave the + // new request queued behind the reconnect backoff. _nextAuthToken = token; } diff --git a/tool/convex_client_gauntlet/runtime/tool/run_paired_profile.sh b/tool/convex_client_gauntlet/runtime/tool/run_paired_profile.sh new file mode 100755 index 00000000..50a27d03 --- /dev/null +++ b/tool/convex_client_gauntlet/runtime/tool/run_paired_profile.sh @@ -0,0 +1,85 @@ +#!/bin/sh + +set -eu + +: "${PROFILE_OUTPUT_DIR:?Set PROFILE_OUTPUT_DIR to an empty output directory}" +: "${SUPABASE_URL:?Set SUPABASE_URL}" +: "${SUPABASE_KEY:?Set SUPABASE_KEY to the public anon key}" +: "${TEST_EMAIL:?Set TEST_EMAIL to the disposable account}" +: "${TEST_PASSWORD:?Set TEST_PASSWORD to the disposable account}" +: "${CONVEX_SELF_HOSTED_ADMIN_KEY:?Set CONVEX_SELF_HOSTED_ADMIN_KEY for the isolated deployment}" + +profile_tool_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +runtime_dir=$(CDPATH= cd -- "$profile_tool_dir/.." && pwd) +repo_dir=$(CDPATH= cd -- "$runtime_dir/../../.." && pwd) +app_dir="$runtime_dir/app" +profile_binary="$app_dir/build/macos/Build/Products/Profile/icarus_convex_runtime_runner.app/Contents/MacOS/icarus_convex_runtime_runner" +trial_count=${TRIALS:-10} +convex_url=${CONVEX_URL:-http://127.0.0.1:3210} + +if [ ! -x "$profile_binary" ]; then + printf 'Missing profile runner: %s\n' "$profile_binary" >&2 + exit 2 +fi + +mkdir -p "$PROFILE_OUTPUT_DIR" +printf 'trial\tposition\tadapter\treport\ttime\n' > "$PROFILE_OUTPUT_DIR/order.tsv" + +run_candidate() { + trial=$1 + position=$2 + adapter=$3 + report_name="profile-trial-$(printf '%02d' "$trial")-$position-$adapter" + log_file="$PROFILE_OUTPUT_DIR/$report_name.log" + time_file="$PROFILE_OUTPUT_DIR/$report_name.time" + + ( + cd "$repo_dir" + CONVEX_DEPLOYMENT= \ + CONVEX_SELF_HOSTED_URL="$convex_url" \ + npx convex import --replace-all --table users \ + "$runtime_dir/fixtures/empty.json" -y >/dev/null 2>&1 + ) + + if ! ( + cd "$app_dir" + export CONVEX_URL="$convex_url" + export ADAPTER="$adapter" + export SEED_COUNT=1 + export ALLOW_CHECKPOINT=0 + export RESET_PROGRESS=1 + export REPORT_NAME="$report_name" + export GIT_COMMIT + GIT_COMMIT=$(git -C "$repo_dir" rev-parse HEAD) + /usr/bin/time -lp "$profile_binary" > "$log_file" + ) 2> "$time_file"; then + tail -40 "$log_file" >&2 + tail -40 "$time_file" >&2 + exit 1 + fi + + report_path=$(sed -n 's/^GAUNTLET_RESULT://p' "$log_file" | tail -1 | jq -r .reportPath) + if [ ! -f "$report_path" ]; then + printf 'Missing report for trial %s, adapter %s\n' "$trial" "$adapter" >&2 + exit 1 + fi + cp "$report_path" "$PROFILE_OUTPUT_DIR/$report_name.json" + printf '%s\t%s\t%s\t%s\t%s\n' \ + "$trial" "$position" "$adapter" "$report_name.json" "$report_name.time" \ + >> "$PROFILE_OUTPUT_DIR/order.tsv" + printf 'trial=%s position=%s adapter=%s passed\n' "$trial" "$position" "$adapter" +} + +trial=1 +while [ "$trial" -le "$trial_count" ]; do + if [ $((trial % 2)) -eq 1 ]; then + first=dartvex + second=convex_flutter + else + first=convex_flutter + second=dartvex + fi + run_candidate "$trial" first "$first" + run_candidate "$trial" second "$second" + trial=$((trial + 1)) +done From fb83488c0924f8daf57c4bdfc48d7a4a5ff0c8f5 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 27 Aug 2026 04:07:02 -0400 Subject: [PATCH 10/11] fix: make Convex auth recovery deterministic --- third_party/convex_flutter/ICARUS_PATCH.md | 14 +- .../convex_flutter/lib/src/convex_client.dart | 31 +- .../lib/src/impl/convex_client_native.dart | 61 +- .../lib/src/rust/frb_generated.dart | 66 +- .../convex_flutter/lib/src/rust/lib.dart | 15 +- third_party/convex_flutter/pubspec.yaml | 2 +- third_party/convex_flutter/rust/Cargo.lock | 2 - third_party/convex_flutter/rust/Cargo.toml | 2 +- .../convex_flutter/rust/src/frb_generated.rs | 69 +- third_party/convex_flutter/rust/src/lib.rs | 11 + third_party/convex_rs/.gitignore | 6 + third_party/convex_rs/.prettierrc | 2 + third_party/convex_rs/CHANGELOG.md | 103 + third_party/convex_rs/CONTRIBUTING.md | 38 + third_party/convex_rs/Cargo.lock | 2145 +++++++++++++++++ third_party/convex_rs/Cargo.toml | 180 ++ third_party/convex_rs/Cargo.toml.orig | 71 + third_party/convex_rs/ICARUS_PATCH.md | 40 + third_party/convex_rs/LICENSE | 202 ++ third_party/convex_rs/README.md | 63 + .../convex_rs/examples/convex_chat_client.rs | 189 ++ .../examples/quickstart/convex/tasks.ts | 7 + .../convex_rs/examples/quickstart/main.rs | 18 + .../examples/quickstart/package-lock.json | 540 +++++ .../examples/quickstart/package.json | 7 + .../examples/quickstart/sampleData.jsonl | 3 + third_party/convex_rs/rust-toolchain | 2 + third_party/convex_rs/rustfmt.toml | 16 + third_party/convex_rs/src/base_client/mod.rs | 1062 ++++++++ .../convex_rs/src/base_client/query_result.rs | 159 ++ .../src/base_client/request_manager.rs | 170 ++ third_party/convex_rs/src/client/mod.rs | 1153 +++++++++ .../convex_rs/src/client/subscription.rs | 149 ++ third_party/convex_rs/src/client/worker.rs | 367 +++ third_party/convex_rs/src/lib.rs | 75 + third_party/convex_rs/src/sync/mod.rs | 56 + third_party/convex_rs/src/sync/testing.rs | 106 + .../convex_rs/src/sync/web_socket_manager.rs | 375 +++ third_party/convex_rs/src/value/export/mod.rs | 191 ++ .../convex_rs/src/value/export/roundtrip.rs | 169 ++ third_party/convex_rs/src/value/json/bytes.rs | 14 + third_party/convex_rs/src/value/json/float.rs | 19 + .../convex_rs/src/value/json/integer.rs | 19 + third_party/convex_rs/src/value/json/mod.rs | 157 ++ third_party/convex_rs/src/value/mod.rs | 136 ++ third_party/convex_rs/src/value/sorting.rs | 75 + .../runtime/lib/runner.dart | 52 +- .../runtime/lib/transport.dart | 19 +- 48 files changed, 8342 insertions(+), 86 deletions(-) create mode 100644 third_party/convex_rs/.gitignore create mode 100644 third_party/convex_rs/.prettierrc create mode 100644 third_party/convex_rs/CHANGELOG.md create mode 100644 third_party/convex_rs/CONTRIBUTING.md create mode 100644 third_party/convex_rs/Cargo.lock create mode 100644 third_party/convex_rs/Cargo.toml create mode 100644 third_party/convex_rs/Cargo.toml.orig create mode 100644 third_party/convex_rs/ICARUS_PATCH.md create mode 100644 third_party/convex_rs/LICENSE create mode 100644 third_party/convex_rs/README.md create mode 100644 third_party/convex_rs/examples/convex_chat_client.rs create mode 100644 third_party/convex_rs/examples/quickstart/convex/tasks.ts create mode 100644 third_party/convex_rs/examples/quickstart/main.rs create mode 100644 third_party/convex_rs/examples/quickstart/package-lock.json create mode 100644 third_party/convex_rs/examples/quickstart/package.json create mode 100644 third_party/convex_rs/examples/quickstart/sampleData.jsonl create mode 100644 third_party/convex_rs/rust-toolchain create mode 100644 third_party/convex_rs/rustfmt.toml create mode 100644 third_party/convex_rs/src/base_client/mod.rs create mode 100644 third_party/convex_rs/src/base_client/query_result.rs create mode 100644 third_party/convex_rs/src/base_client/request_manager.rs create mode 100644 third_party/convex_rs/src/client/mod.rs create mode 100644 third_party/convex_rs/src/client/subscription.rs create mode 100644 third_party/convex_rs/src/client/worker.rs create mode 100644 third_party/convex_rs/src/lib.rs create mode 100644 third_party/convex_rs/src/sync/mod.rs create mode 100644 third_party/convex_rs/src/sync/testing.rs create mode 100644 third_party/convex_rs/src/sync/web_socket_manager.rs create mode 100644 third_party/convex_rs/src/value/export/mod.rs create mode 100644 third_party/convex_rs/src/value/export/roundtrip.rs create mode 100644 third_party/convex_rs/src/value/json/bytes.rs create mode 100644 third_party/convex_rs/src/value/json/float.rs create mode 100644 third_party/convex_rs/src/value/json/integer.rs create mode 100644 third_party/convex_rs/src/value/json/mod.rs create mode 100644 third_party/convex_rs/src/value/mod.rs create mode 100644 third_party/convex_rs/src/value/sorting.rs diff --git a/third_party/convex_flutter/ICARUS_PATCH.md b/third_party/convex_flutter/ICARUS_PATCH.md index 2fe244d3..dc5fc8c8 100644 --- a/third_party/convex_flutter/ICARUS_PATCH.md +++ b/third_party/convex_flutter/ICARUS_PATCH.md @@ -18,14 +18,18 @@ Auth handles also carry an internal generation. Disposal only clears auth when the handle still owns the current generation, so a delayed cancellation from a replaced handle cannot erase the fresh callback. -No generated Dart or Rust bridge file is edited: the public bridge signature is -unchanged. The hand-written changes are limited to `rust/src/lib.rs` and the -minimum `convex` crate version in `rust/Cargo.toml`; `rust/Cargo.lock` is -regenerated with: +The native manual reconnect API calls the Icarus-patched `convex` 0.10.4 crate +in `../convex_rs`. It waits for a real connecting-to-connected transition; the +published package method only ran its configured health query. + +Generated Dart and Rust bridge files are never hand-edited. They are regenerated +from `rust/src/lib.rs` with `flutter_rust_bridge_codegen` 2.11.1. Hand-written +changes are limited to the Dart client implementation, `rust/src/lib.rs`, and +the local `convex` dependency in `rust/Cargo.toml`. ```sh cd third_party/convex_flutter/rust -cargo update -p convex --precise 0.10.4 +flutter_rust_bridge_codegen generate ``` This directory can be removed once a published `convex_flutter` release uses a diff --git a/third_party/convex_flutter/lib/src/convex_client.dart b/third_party/convex_flutter/lib/src/convex_client.dart index 5aae4788..b98ff3af 100644 --- a/third_party/convex_flutter/lib/src/convex_client.dart +++ b/third_party/convex_flutter/lib/src/convex_client.dart @@ -2,7 +2,8 @@ import 'dart:async'; import 'package:convex_flutter/src/impl/convex_client_interface.dart'; import 'package:convex_flutter/src/impl/convex_client_factory.dart'; -import 'package:convex_flutter/src/rust/lib.dart' show WebSocketConnectionState, SubscriptionHandle, AuthHandle; +import 'package:convex_flutter/src/rust/lib.dart' + show WebSocketConnectionState, SubscriptionHandle, AuthHandle; import 'package:convex_flutter/src/connection_status.dart'; import 'package:convex_flutter/src/convex_config.dart'; import 'package:convex_flutter/src/app_lifecycle_event.dart'; @@ -141,10 +142,7 @@ class ConvexClient { }) async { if (_instance == null) { await initialize( - ConvexConfig( - deploymentUrl: deploymentUrl, - clientId: clientId, - ), + ConvexConfig(deploymentUrl: deploymentUrl, clientId: clientId), ); } return _instance!; @@ -177,8 +175,7 @@ class ConvexClient { Future mutation({ required String name, required Map args, - }) => - _impl.mutation(name: name, args: args); + }) => _impl.mutation(name: name, args: args); /// Executes a Convex action operation with timeout. /// @@ -190,8 +187,7 @@ class ConvexClient { Future action({ required String name, required Map args, - }) => - _impl.action(name: name, args: args); + }) => _impl.action(name: name, args: args); /// Creates a real-time subscription to a Convex query. /// @@ -206,13 +202,12 @@ class ConvexClient { required Map args, required void Function(String) onUpdate, required void Function(String, String?) onError, - }) => - _impl.subscribe( - name: name, - args: args, - onUpdate: onUpdate, - onError: onError, - ); + }) => _impl.subscribe( + name: name, + args: args, + onUpdate: onUpdate, + onError: onError, + ); // ============================================================================ // Authentication API @@ -357,8 +352,8 @@ class ConvexClient { /// Attempts to reconnect to the Convex backend. /// - /// This method calls [checkConnection] and returns true if the - /// connection check succeeds, false otherwise. + /// This method restarts the native WebSocket and returns true after the + /// connection-state stream observes the complete reconnect transition. /// /// Typically called after the app resumes from background or /// after detecting a network interruption. diff --git a/third_party/convex_flutter/lib/src/impl/convex_client_native.dart b/third_party/convex_flutter/lib/src/impl/convex_client_native.dart index bf11e6ee..5ac70dbc 100644 --- a/third_party/convex_flutter/lib/src/impl/convex_client_native.dart +++ b/third_party/convex_flutter/lib/src/impl/convex_client_native.dart @@ -76,7 +76,9 @@ class NativeConvexClient implements IConvexClient { // Setup lifecycle observer client._lifecycleObserver = AppLifecycleObserver( onLifecycleChange: (event) { - client._lifecycleController.add(event); + if (!client._lifecycleController.isClosed) { + client._lifecycleController.add(event); + } }, ); @@ -87,19 +89,29 @@ class NativeConvexClient implements IConvexClient { /// /// This must be called before any queries/mutations to capture all state changes. Future _setupConnectionStateListener() async { - debugPrint('=== [NativeConvexClient] Setting up WebSocket state listener ==='); - debugPrint('=== [NativeConvexClient] Current state: ${_currentConnectionState.name} ==='); + debugPrint( + '=== [NativeConvexClient] Setting up WebSocket state listener ===', + ); + debugPrint( + '=== [NativeConvexClient] Current state: ${_currentConnectionState.name} ===', + ); try { await _rustClient.onWebsocketStateChange( onStateChange: (state) async { - debugPrint('=== [NativeConvexClient] State changed: ${state.name} ==='); + debugPrint( + '=== [NativeConvexClient] State changed: ${state.name} ===', + ); _currentConnectionState = state; - _connectionStateController.add(state); + if (!_connectionStateController.isClosed) { + _connectionStateController.add(state); + } debugPrint('=== [NativeConvexClient] Stream emission complete ==='); }, ); - debugPrint('=== [NativeConvexClient] Listener registered successfully ==='); + debugPrint( + '=== [NativeConvexClient] Listener registered successfully ===', + ); } catch (e) { debugPrint('ERROR: [NativeConvexClient] Listener setup failed: $e'); rethrow; @@ -167,7 +179,9 @@ class NativeConvexClient implements IConvexClient { _currentAuthHandle = null; await _rustClient.setAuth(token: token); - _authStateController.add(token != null); + if (!_authStateController.isClosed) { + _authStateController.add(token != null); + } } @override @@ -182,7 +196,9 @@ class NativeConvexClient implements IConvexClient { fetchToken: () async => await tokenFetcher(), onAuthChange: (bool isAuth) async { onAuthChange?.call(isAuth); - _authStateController.add(isAuth); + if (!_authStateController.isClosed) { + _authStateController.add(isAuth); + } }, ); @@ -195,7 +211,9 @@ class NativeConvexClient implements IConvexClient { _currentAuthHandle?.dispose(); _currentAuthHandle = null; await _rustClient.setAuth(token: null); - _authStateController.add(false); + if (!_authStateController.isClosed) { + _authStateController.add(false); + } } @override @@ -213,7 +231,8 @@ class NativeConvexClient implements IConvexClient { _connectionStateController.stream; @override - WebSocketConnectionState get currentConnectionState => _currentConnectionState; + WebSocketConnectionState get currentConnectionState => + _currentConnectionState; @override bool get isConnected => @@ -243,12 +262,26 @@ class NativeConvexClient implements IConvexClient { @override Future reconnect() async { + final states = StreamIterator(connectionState); + var sawConnecting = false; + final deadline = DateTime.now().add(config.operationTimeout); try { - final status = await checkConnection(); - return status == ConnectionStatus.connected; - } catch (e) { - // If healthCheckQuery not configured, just return false + await _rustClient.reconnectNow(reason: 'convex_flutter:manual'); + while (DateTime.now().isBefore(deadline)) { + final remaining = deadline.difference(DateTime.now()); + if (!await states.moveNext().timeout(remaining)) return false; + if (states.current == WebSocketConnectionState.connecting) { + sawConnecting = true; + } else if (sawConnecting && + states.current == WebSocketConnectionState.connected) { + return true; + } + } + return false; + } on TimeoutException { return false; + } finally { + await states.cancel(); } } diff --git a/third_party/convex_flutter/lib/src/rust/frb_generated.dart b/third_party/convex_flutter/lib/src/rust/frb_generated.dart index 80f18bed..47d8eb5e 100644 --- a/third_party/convex_flutter/lib/src/rust/frb_generated.dart +++ b/third_party/convex_flutter/lib/src/rust/frb_generated.dart @@ -64,7 +64,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 1095084362; + int get rustContentHash => -829523767; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -129,6 +129,11 @@ abstract class RustLibApi extends BaseApi { required Map args, }); + Future crateMobileConvexClientReconnectNow({ + required MobileConvexClient that, + required String reason, + }); + Future crateMobileConvexClientSetAuth({ required MobileConvexClient that, String? token, @@ -605,6 +610,44 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that", "name", "args"], ); + @override + Future crateMobileConvexClientReconnectNow({ + required MobileConvexClient that, + required String reason, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMobileConvexClient( + that, + serializer, + ); + sse_encode_String(reason, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 12, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_client_error, + ), + constMeta: kCrateMobileConvexClientReconnectNowConstMeta, + argValues: [that, reason], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateMobileConvexClientReconnectNowConstMeta => + const TaskConstMeta( + debugName: "MobileConvexClient_reconnect_now", + argNames: ["that", "reason"], + ); + @override Future crateMobileConvexClientSetAuth({ required MobileConvexClient that, @@ -622,7 +665,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 12, + funcId: 13, port: port_, ); }, @@ -668,7 +711,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 13, + funcId: 14, port: port_, ); }, @@ -719,7 +762,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 14, + funcId: 15, port: port_, ); }, @@ -751,7 +794,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer, ); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 16)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2181,17 +2224,18 @@ class MobileConvexClientImpl extends RustOpaque implements MobileConvexClient { args: args, ); + /// Forces a WebSocket reconnect while retaining current client state. + Future reconnectNow({required String reason}) => RustLib.instance.api + .crateMobileConvexClientReconnectNow(that: this, reason: reason); + /// Sets authentication token for the client. Future setAuth({String? token}) => RustLib.instance.api .crateMobileConvexClientSetAuth(that: this, token: token); - /// Sets authentication with automatic token refresh. - /// - /// The `fetch_token` callback is called: - /// - Immediately to get the initial token - /// - Automatically when the token is about to expire (60 seconds before expiry) + /// Sets authentication with token refresh on every WebSocket reconnect. /// - /// The `on_auth_change` callback is called whenever auth state changes. + /// The callback is owned by the upstream Convex client so authentication + /// and query state are replayed together after a disconnect. /// /// Returns an AuthHandle that can be used to dispose the auth session. Future setAuthWithRefresh({ diff --git a/third_party/convex_flutter/lib/src/rust/lib.dart b/third_party/convex_flutter/lib/src/rust/lib.dart index 30fd23a6..a6e06b11 100644 --- a/third_party/convex_flutter/lib/src/rust/lib.dart +++ b/third_party/convex_flutter/lib/src/rust/lib.dart @@ -8,8 +8,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:freezed_annotation/freezed_annotation.dart' hide protected; part 'lib.freezed.dart'; -// These functions are ignored because they are not marked as `pub`: `connected_client`, `decode_jwt_expiry`, `handle_direct_function_result`, `internal_action`, `internal_mutation`, `internal_set_auth`, `internal_subscribe`, `new`, `new`, `parse_json_args` -// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `JwtClaims` +// These functions are ignored because they are not marked as `pub`: `connected_client`, `handle_direct_function_result`, `internal_action`, `internal_mutation`, `internal_set_auth`, `internal_subscribe`, `new`, `new`, `parse_json_args` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`, `fmt`, `fmt`, `from`, `from` // Rust type: RustOpaqueMoi> @@ -94,16 +93,16 @@ abstract class MobileConvexClient implements RustOpaqueInterface { required Map args, }); + /// Forces a WebSocket reconnect while retaining current client state. + Future reconnectNow({required String reason}); + /// Sets authentication token for the client. Future setAuth({String? token}); - /// Sets authentication with automatic token refresh. - /// - /// The `fetch_token` callback is called: - /// - Immediately to get the initial token - /// - Automatically when the token is about to expire (60 seconds before expiry) + /// Sets authentication with token refresh on every WebSocket reconnect. /// - /// The `on_auth_change` callback is called whenever auth state changes. + /// The callback is owned by the upstream Convex client so authentication + /// and query state are replayed together after a disconnect. /// /// Returns an AuthHandle that can be used to dispose the auth session. Future setAuthWithRefresh({ diff --git a/third_party/convex_flutter/pubspec.yaml b/third_party/convex_flutter/pubspec.yaml index fffbd600..5505d5bd 100644 --- a/third_party/convex_flutter/pubspec.yaml +++ b/third_party/convex_flutter/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - flutter_rust_bridge: ^2.11.1 + flutter_rust_bridge: 2.11.1 flutter_web_plugins: sdk: flutter freezed_annotation: ^3.1.0 diff --git a/third_party/convex_flutter/rust/Cargo.lock b/third_party/convex_flutter/rust/Cargo.lock index 77804251..a8ba7533 100644 --- a/third_party/convex_flutter/rust/Cargo.lock +++ b/third_party/convex_flutter/rust/Cargo.lock @@ -216,8 +216,6 @@ dependencies = [ [[package]] name = "convex" version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca2fbc7cfc35747ade0731b173cf72c70c7f72c4578440f9a77c871adaa1e2f" dependencies = [ "anyhow", "async-trait", diff --git a/third_party/convex_flutter/rust/Cargo.toml b/third_party/convex_flutter/rust/Cargo.toml index d4fd88ca..ce117698 100644 --- a/third_party/convex_flutter/rust/Cargo.toml +++ b/third_party/convex_flutter/rust/Cargo.toml @@ -11,7 +11,7 @@ flutter_rust_bridge = "=2.11.1" tokio = { version = "1", features = ["full"] } android_logger = { version = "0.14.1" } log = { version = "0.4.21" } -convex = { version = "0.10.4", features = ["rustls-tls-webpki-roots"] } +convex = { path = "../../convex_rs", features = ["rustls-tls-webpki-roots"] } anyhow = { version = "1.0.86" } thiserror = { version = "1.0.61" } tokio-stream = { features = [ "io-util", "sync" ], version = "0.1" } diff --git a/third_party/convex_flutter/rust/src/frb_generated.rs b/third_party/convex_flutter/rust/src/frb_generated.rs index 5a4998f1..69cf500f 100644 --- a/third_party/convex_flutter/rust/src/frb_generated.rs +++ b/third_party/convex_flutter/rust/src/frb_generated.rs @@ -39,7 +39,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1095084362; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -829523767; // Section: executor @@ -640,6 +640,64 @@ fn wire__crate__MobileConvexClient_query_impl( }, ) } +fn wire__crate__MobileConvexClient_reconnect_now_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "MobileConvexClient_reconnect_now", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_reason = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, crate::ClientError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + crate::MobileConvexClient::reconnect_now(&*api_that_guard, api_reason) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__MobileConvexClient_set_auth_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1350,14 +1408,15 @@ fn pde_ffi_dispatcher_primary_impl( data_len, ), 11 => wire__crate__MobileConvexClient_query_impl(port, ptr, rust_vec_len, data_len), - 12 => wire__crate__MobileConvexClient_set_auth_impl(port, ptr, rust_vec_len, data_len), - 13 => wire__crate__MobileConvexClient_set_auth_with_refresh_impl( + 12 => wire__crate__MobileConvexClient_reconnect_now_impl(port, ptr, rust_vec_len, data_len), + 13 => wire__crate__MobileConvexClient_set_auth_impl(port, ptr, rust_vec_len, data_len), + 14 => wire__crate__MobileConvexClient_set_auth_with_refresh_impl( port, ptr, rust_vec_len, data_len, ), - 14 => wire__crate__MobileConvexClient_subscribe_impl(port, ptr, rust_vec_len, data_len), + 15 => wire__crate__MobileConvexClient_subscribe_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -1373,7 +1432,7 @@ fn pde_ffi_dispatcher_sync_impl( 1 => wire__crate__AuthHandle_dispose_impl(ptr, rust_vec_len, data_len), 2 => wire__crate__AuthHandle_is_authenticated_impl(ptr, rust_vec_len, data_len), 9 => wire__crate__MobileConvexClient_new_impl(ptr, rust_vec_len, data_len), - 15 => wire__crate__SubscriptionHandle_cancel_impl(ptr, rust_vec_len, data_len), + 16 => wire__crate__SubscriptionHandle_cancel_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } diff --git a/third_party/convex_flutter/rust/src/lib.rs b/third_party/convex_flutter/rust/src/lib.rs index d9184280..289ab6d0 100644 --- a/third_party/convex_flutter/rust/src/lib.rs +++ b/third_party/convex_flutter/rust/src/lib.rs @@ -456,6 +456,17 @@ impl MobileConvexClient { .map_err(|e| e.into()) } + /// Forces a WebSocket reconnect while retaining current client state. + #[frb] + pub async fn reconnect_now(&self, reason: String) -> Result<(), ClientError> { + let mut client = self.connected_client().await?; + self.rt + .spawn(async move { client.reconnect_now(&reason).await }) + .await + .map_err(|e| ClientError::InternalError { msg: e.to_string() })?; + Ok(()) + } + /// Sets authentication with token refresh on every WebSocket reconnect. /// /// The callback is owned by the upstream Convex client so authentication diff --git a/third_party/convex_rs/.gitignore b/third_party/convex_rs/.gitignore new file mode 100644 index 00000000..dec8ba25 --- /dev/null +++ b/third_party/convex_rs/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +target/ +convex_local_backend*.sqlite3 +convex_local_storage/ +.DS_Store +# testing 123 diff --git a/third_party/convex_rs/.prettierrc b/third_party/convex_rs/.prettierrc new file mode 100644 index 00000000..473aecc5 --- /dev/null +++ b/third_party/convex_rs/.prettierrc @@ -0,0 +1,2 @@ +proseWrap: "always" +arrowParens: "avoid" diff --git a/third_party/convex_rs/CHANGELOG.md b/third_party/convex_rs/CHANGELOG.md new file mode 100644 index 00000000..da573d9d --- /dev/null +++ b/third_party/convex_rs/CHANGELOG.md @@ -0,0 +1,103 @@ +# 0.10.4 + +- Optimizations to `check_valid_field_name` in `sync_types` +- Fix for memory leak in query subscriptions + (https://github.com/get-convex/convex-rs/issues/15) +- Bump rust-version minimum from 1.80.1 to 1.85 + +# 0.10.3 + +- Fix for incorrect client state on WebSocket reconnect +- New `set_auth_callback` method on `ConvexClient` to allow token refresh on + WebSocket reconnect + +# 0.10.2 + +- Fix for deadlock between client and websocket worker tasks +- Update `tokio` dependency + +# 0.10.1 + +- Bump sync_types version and depend on it + +# 0.10.0 + +- Fix for panic in query subscriptions +- Bump rust-version minimum from 1.71.1 to 1.80.1 + +# 0.9.0 + +- Add `ConvexClientBuilder` pattern for constructing `ConvexClient` +- Add support for `on_state_change` for handling reconnects. +- Bump rust-version minimum from 1.65.0 to 1.71.1 +- Update `url` dependency. + +# 0.8.1 + +Remove native-tls-vendored dependency for tokio-tungstenite. Rely on requested +features instead. + +# 0.8.0 + +- Support for passing through a client_id to ConvexClient +- Dependency upgrades + +# 0.7.0 + +- Several dependency upgrades + +# 0.6.0 + +- Remove support for Set and Map Convex types. These types are deprecated. +- Add comprehensive support for ConvexError with `data` payload as part of the + `FunctionResult` enum. +- Better support for emitting loglines + +# 0.5.0 + +- Prelim support for ConvexError, encoded into an anyhow::Error. Eventual plan + is to expose a separate catchable type, but just getting something out + quickly. PRs accepted! + +# 0.4.0 + +- Expose an alternate cleaner JSON export format on Value. The clean format is + lossy in some cases (eg both integers and strings are encoded as JSON + strings). +- Expose native-tls-vendored feature + +# 0.3.1 + +- Fix compilation with `--features=testing` +- Minor syntactic changes to quickstart + +# 0.3.0 + +- Remove `Value::Id` since document IDs are `Value::String`s for Convex + functions starting from NPM version 0.17 +- Minor improvements to convex_chat_client example +- Minor improvements in convex_sync_types + +# 0.2.0 + +- BUGFIX: Client occasionally used to get stuck in a hot loop after network + disconnect. +- Tweak backoff params for better performance across network disconnect. +- Minor improvements to convex_chat_client example +- Minor fix to running tests +- Bump tokio-tungstenite to 0.18 +- Minor improvements in convex_sync_types + +# 0.1.2 + +Yanked and re-released as 0.2.0 + +# 0.1.1 + +- Fix race between mutation result and dropping a subscription. +- Minor logging/error message improvements. + +# 0.1.0 + +- Initial release. +- Support for queries, subscriptions, mutations, actions diff --git a/third_party/convex_rs/CONTRIBUTING.md b/third_party/convex_rs/CONTRIBUTING.md new file mode 100644 index 00000000..0d5fd9f7 --- /dev/null +++ b/third_party/convex_rs/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing + +Contributions are welcome! + +Please share any general questions, feature requests, or product feedback in our +[Convex Discord Community](https://convex.dev/community). We're particularly +excited to see what you build on Convex! + +Please ensure that rust code is formatted with +[cargo fmt](https://github.com/rust-lang/rustfmt) and markdown files are +formatted with [prettier](https://prettier.io/). + +Run tests with + +``` +cargo test -p {crate} +``` + +Convex is a fast moving project developed by a dedicated team. We're excited to +contribute to the community by releasing this code, but we want to manage +expectations as well. + +- We are a small company with a lot of product surface area. +- We value a cohesive developer experience for folks building applications + across all of our languages and platforms. +- We value transparency in how we operate. + +We're excited for community PRs. Be aware we may not get to it for a while. +Smaller PRs that only affect documentation/comments are easier to review and +integrate. For any larger or more fundamental changes, get in touch with us on +Discord before you put in too much work to see if it's consistent with our short +term plan. We think carefully about how our APIs contribute to a cohesive +product, so chatting up front goes a long way. + +# Docs contributions + +Docs are located in the `npm-packages/docs` directory. See the README there for +more info. diff --git a/third_party/convex_rs/Cargo.lock b/third_party/convex_rs/Cargo.lock new file mode 100644 index 00000000..13d25da1 --- /dev/null +++ b/third_party/convex_rs/Cargo.lock @@ -0,0 +1,2145 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "archery" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitmaps" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convex" +version = "0.10.4" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.13.1", + "bytes", + "colored", + "convex_sync_types", + "dotenvy", + "futures", + "imbl", + "maplit", + "parking_lot", + "pretty_assertions", + "proptest", + "proptest-derive", + "rand 0.9.0", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "convex_sync_types" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba8235188b091cc50205a436bf6505cb386be208263fecdb4b62bd2b3c90a0a" +dependencies = [ + "anyhow", + "base64 0.13.1", + "bytes", + "derive_more", + "headers", + "pretty_assertions", + "proptest", + "proptest-derive", + "rand 0.9.0", + "serde", + "serde_json", + "strum", + "uuid", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "data-encoding" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.108", + "unicode-xid", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imbl" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e525189e5f603908d0c6e0d402cb5de9c4b2c8866151fabc4ebd771ed2630a2e" +dependencies = [ + "archery", + "bitmaps", + "imbl-sized-chunks", + "rand_core 0.9.1", + "rand_xoshiro", + "version_check", + "wide", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.8.2", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.3+3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "output_vt100" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" +dependencies = [ + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty_assertions" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +dependencies = [ + "ctor", + "diff", + "output_vt100", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.10.0", + "lazy_static", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.1", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3" +dependencies = [ + "getrandom 0.3.1", + "zerocopy", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "0.38.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.2.0", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.3", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.108", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.2.15", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.0", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] diff --git a/third_party/convex_rs/Cargo.toml b/third_party/convex_rs/Cargo.toml new file mode 100644 index 00000000..41e09222 --- /dev/null +++ b/third_party/convex_rs/Cargo.toml @@ -0,0 +1,180 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.85" +name = "convex" +version = "0.10.4" +authors = ["Convex, Inc. "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Client library for Convex (convex.dev)" +homepage = "https://www.convex.dev/" +readme = "README.md" +license = "Apache-2.0" +repository = "https://github.com/get-convex/convex-rs" +resolver = "2" + +[features] +default = ["native-tls-vendored"] +native-tls = ["tokio-tungstenite/native-tls"] +native-tls-vendored = ["tokio-tungstenite/native-tls-vendored"] +rustls-tls-native-roots = ["tokio-tungstenite/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["tokio-tungstenite/rustls-tls-webpki-roots"] +testing = [ + "convex_sync_types/testing", + "proptest", + "proptest-derive", + "parking_lot", +] + +[lib] +name = "convex" +path = "src/lib.rs" + +[[example]] +name = "convex_chat_client" +path = "examples/convex_chat_client.rs" + +[[example]] +name = "quickstart" +path = "examples/quickstart/main.rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.async-trait] +version = "0.1" + +[dependencies.base64] +version = "0.13" + +[dependencies.bytes] +version = "1.6.0" + +[dependencies.convex_sync_types] +version = "=0.10.4" + +[dependencies.futures] +version = "0.3" + +[dependencies.imbl] +version = "7.0.0" + +[dependencies.parking_lot] +version = "0.12" +features = ["hardware-lock-elision"] +optional = true + +[dependencies.proptest] +version = "1" +optional = true + +[dependencies.proptest-derive] +version = "0.5.0" +optional = true + +[dependencies.rand] +version = "0.9" + +[dependencies.serde_json] +version = "1" +features = [ + "float_roundtrip", + "preserve_order", + "raw_value", +] + +[dependencies.thiserror] +version = "2" + +[dependencies.tokio] +version = "1.47.1" +features = ["full"] + +[dependencies.tokio-stream] +version = "0.1" +features = [ + "io-util", + "sync", +] + +[dependencies.tokio-tungstenite] +version = "0.28.0" +features = ["url"] + +[dependencies.tracing] +version = "0.1" + +[dependencies.url] +version = "2.5.4" + +[dependencies.uuid] +version = "1.6" +features = [ + "serde", + "v4", +] + +[dev-dependencies.colored] +version = "3" + +[dev-dependencies.convex_sync_types] +version = "=0.10.4" +features = ["testing"] + +[dev-dependencies.dotenvy] +version = "0.15.7" + +[dev-dependencies.maplit] +version = "1" + +[dev-dependencies.parking_lot] +version = "0.12" +features = ["hardware-lock-elision"] + +[dev-dependencies.pretty_assertions] +version = "1" + +[dev-dependencies.proptest] +version = "1" + +[dev-dependencies.proptest-derive] +version = "0.5.0" + +[dev-dependencies.tracing-subscriber] +version = "0.3.17" +features = ["env-filter"] + +[lints.clippy] +await_holding_lock = "warn" +await_holding_refcell_ref = "warn" +large_enum_variant = "allow" +manual_is_multiple_of = "allow" +manual_map = "allow" +new_without_default = "allow" +op_ref = "allow" +ptr_arg = "allow" +result_large_err = "allow" +single_match = "allow" +too_many_arguments = "allow" +type_complexity = "allow" +upper_case_acronyms = "allow" +useless_format = "allow" +useless_vec = "allow" + +[lints.rust] +unused_extern_crates = "warn" diff --git a/third_party/convex_rs/Cargo.toml.orig b/third_party/convex_rs/Cargo.toml.orig new file mode 100644 index 00000000..2cfc9c31 --- /dev/null +++ b/third_party/convex_rs/Cargo.toml.orig @@ -0,0 +1,71 @@ +[package] +name = "convex" +description = "Client library for Convex (convex.dev)" +authors = [ "Convex, Inc. " ] +version = "0.10.4" +edition = "2021" +rust-version = "1.85" +resolver = "2" +license = "Apache-2.0" +repository = "https://github.com/get-convex/convex-rs" +homepage = "https://www.convex.dev/" + +[features] +default = [ "native-tls-vendored" ] +native-tls = [ "tokio-tungstenite/native-tls" ] +native-tls-vendored = [ "tokio-tungstenite/native-tls-vendored" ] +rustls-tls-native-roots = [ "tokio-tungstenite/rustls-tls-native-roots" ] +rustls-tls-webpki-roots = [ "tokio-tungstenite/rustls-tls-webpki-roots" ] +testing = [ "convex_sync_types/testing", "proptest", "proptest-derive", "parking_lot" ] + +[dependencies] +anyhow = { version = "1" } +async-trait = { version = "0.1" } +base64 = { version = "0.13" } +bytes = { version = "1.6.0" } +convex_sync_types = { path = "./sync_types", version = "=0.10.4" } +futures = { version = "0.3" } +imbl = { version = "7.0.0" } +parking_lot = { optional = true, version = "0.12", features = [ "hardware-lock-elision" ] } +proptest = { optional = true, version = "1" } +proptest-derive = { optional = true, version = "0.5.0" } +rand = { version = "0.9" } +serde_json = { features = [ "float_roundtrip", "preserve_order", "raw_value" ], version = "1" } +thiserror = { version = "2" } +tokio = { features = [ "full" ], version = "1.47.1" } +tokio-stream = { features = [ "io-util", "sync" ], version = "0.1" } +tokio-tungstenite = { features = [ "url" ], version = "0.28.0" } +tracing = { version = "0.1" } +url = { version = "2.5.4" } +uuid = { features = [ "serde", "v4" ], version = "1.6" } + +[dev-dependencies] +colored = { version = "3" } +convex_sync_types = { path = "./sync_types", version = "=0.10.4", features = [ "testing" ] } +dotenvy = { version = "0.15.7" } +maplit = { version = "1" } +parking_lot = { version = "0.12", features = [ "hardware-lock-elision" ] } +pretty_assertions = { version = "1" } +proptest = { version = "1" } +proptest-derive = { version = "0.5.0" } +tracing-subscriber = { features = [ "env-filter" ], version = "0.3.17" } + +[lints.rust] +unused_extern_crates = "warn" + +[lints.clippy] +await_holding_lock = "warn" +await_holding_refcell_ref = "warn" +large_enum_variant = "allow" +manual_is_multiple_of = "allow" +manual_map = "allow" +new_without_default = "allow" +op_ref = "allow" +ptr_arg = "allow" +result_large_err = "allow" +single_match = "allow" +too_many_arguments = "allow" +type_complexity = "allow" +upper_case_acronyms = "allow" +useless_format = "allow" +useless_vec = "allow" diff --git a/third_party/convex_rs/ICARUS_PATCH.md b/third_party/convex_rs/ICARUS_PATCH.md new file mode 100644 index 00000000..78ad4956 --- /dev/null +++ b/third_party/convex_rs/ICARUS_PATCH.md @@ -0,0 +1,40 @@ +# Icarus convex-rs patch + +Source: the published `convex` 0.10.4 crate. + +Icarus adds a public `reconnect_now` request that tells the existing worker to +restart its WebSocket and replay current auth, subscriptions, and in-flight +mutations. The upstream client already owns that recovery path, but 0.10.4 does +not expose a way for a host application to trigger it. + +The local `convex_flutter` package uses this method for its documented manual +reconnect API. This keeps the runtime fault symmetric with Dartvex instead of +mistaking an authenticated health query for a socket reconnect. + +Icarus also separates server auth rejection from generic network failure in the +client worker. Auth rejection and the protocol-close errors caused by its old +socket retry the stored refresh callback every 250 ms. Buffered transitions from +the rejected socket cannot end recovery: the client first observes that the +callback produced a different token, then requires a server transition or +function response on that attempt. Other failures retain the upstream +randomized exponential backoff. This prevents a fresh token from sitting behind +a network backoff of up to 15 seconds without creating a hot reconnect loop +while the auth provider refreshes. + +The reconnect request carries that auth-recovery state to the WebSocket worker. +The client worker already supplies the 250 ms pacing, so the WebSocket worker +resets and skips its independent network backoff for those attempts. Without +that coordination, the two workers can each back off the same rejected socket +and still strand a fresh token for up to 15 seconds. + +Before a reconnect, the worker drains responses buffered by the connection it +is replacing. Auth rejection can otherwise be followed by a stale socket-close +failure during the retry delay; replaying that old failure on the new connection +starts a second backoff and can also apply old query-set transitions after the +client has rebuilt its versions. + +WebSocket reconnects now retain one session ID for the lifetime of the client +and increment `connection_count` after every successful socket open, including +client-requested reconnects. The published Rust client created a new session ID +for every socket and did not advance the count on clean reconnects, unlike the +Convex sync protocol's client-lifetime session and monotonic connection model. diff --git a/third_party/convex_rs/LICENSE b/third_party/convex_rs/LICENSE new file mode 100644 index 00000000..b615b0fc --- /dev/null +++ b/third_party/convex_rs/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Convex, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/convex_rs/README.md b/third_party/convex_rs/README.md new file mode 100644 index 00000000..cfbbce12 --- /dev/null +++ b/third_party/convex_rs/README.md @@ -0,0 +1,63 @@ +# Convex + +The official Rust client for [Convex](https://convex.dev/). + +![GitHub](https://img.shields.io/github/license/get-convex/convex-rs) + +Convex is the backend application platform with everything you need to build +your product. + +This Rust client can write and read data from a Convex backend with queries, +mutations, and actions. Get up and running at +[docs.convex.dev](https://docs.convex.dev/introduction/). + +[Join us on Discord](https://www.convex.dev/community) to share what you're +working on or get your questions answered. + +# Installation + +Add the following to your `Cargo.toml` file + +```toml +[dependencies] +convex = "*" +``` + +# Example + +```rust +let mut client = ConvexClient::new(DEPLOYMENT_URL).await?; +let mut subscription = client.subscribe("getCounter", vec![]).await?; +while let Some(new_val) = subscription.next().await { + println!("Counter updated to {new_val:?}"); +} +``` + +# Documentation + +Check out the full convex documentation at +[docs.convex.dev](https://docs.convex.dev/introduction/) The rust API docs are +available on [docs.rs](https://docs.rs/convex/latest/convex/) + +# MSRV + +The Convex rust client works on stable rust 1.71.1 and higher. It also works on +nightly. + +# Debug Logging + +The Convex Rust Client uses the +[tracing](https://docs.rs/tracing/latest/tracing/) crate for logging. One common +way of initializing is via `tracing_subscriber`. Then, you can see debug logging +by running your program with `RUST_LOG=convex=debug`. + +```rust +tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); +``` + +By default, this will emit all logs, including internal logs from the client. +Logs from your Convex backend will show up under the `convex_logs` target at +Level=DEBUG. If you want to isolate just those logs, please refer to the +[tracing_subscriber documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#filtering-with-layers). diff --git a/third_party/convex_rs/examples/convex_chat_client.rs b/third_party/convex_rs/examples/convex_chat_client.rs new file mode 100644 index 00000000..ec697a8d --- /dev/null +++ b/third_party/convex_rs/examples/convex_chat_client.rs @@ -0,0 +1,189 @@ +//! A client for the Convex tutorial chat app. +//! +//! Please run this Convex Chat client from an initialized Convex project. +//! Check out the https://docs.convex.dev/get-started - to get started. +//! +//! Once you've initialized a Convex project with the tutorial, run this +//! demo from inside the project's working directory. +//! +//! For example: +//! cd /path/to/convex-rs +//! cargo build --example convex_chat_client +//! cd /path/to/convex-demos/tutorial +//! /path/to/convex-rs/target/debug/examples/convex_chat_client + +use std::env; + +use colored::Colorize; +use convex::{ + ConvexClient, + FunctionResult, + Value, +}; +use futures::{ + pin_mut, + select_biased, + FutureExt, + StreamExt, +}; +use maplit::btreemap; +use tokio::sync::oneshot; + +const SETUP_MSG: &str = r" +Please run this Convex Chat client from an initialized Convex project. +Check out the https://docs.convex.dev/get-started - to get started. + +Once you've initialized a Convex project with the tutorial, run this +demo from inside the project's working directory. + +For example: +cd /path/to/convex-rs +cargo build --example convex_chat_clientt +cd /path/to/convex-demos/tutorial +/path/to/convex-rs/target/debug/examples/convex_chat_client + +"; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + // Load the tutorial's VITE_CONVEX_URL from the env file + dotenvy::from_filename(".env.local").ok(); + dotenvy::dotenv().ok(); + let Ok(deployment_url) = env::var("VITE_CONVEX_URL") else { + panic!("{SETUP_MSG}"); + }; + println!("Connecting to {deployment_url}"); + + // Client code used in thread #1 + let mut client = ConvexClient::new(&deployment_url).await?; + + // Client code used in thread #2 + let mut client_ = client.clone(); + + println!("{}", format!("Hi! What's your name?").red().bold()); + let mut sender = readline()?; + if sender.is_empty() { + sender = String::from("Anonymous Person"); + } + + let sender_clone = sender.clone(); + + // Thread listening for new messages (use_query demo) + let (cancel_sender, cancel_receiver) = oneshot::channel::<()>(); + let handle = tokio::spawn(async move { + let mut subscription = client + .subscribe("messages:list", btreemap! {}) + .await + .unwrap(); + + let cancel_fut = cancel_receiver.fuse(); + pin_mut!(cancel_fut); + loop { + select_biased! { + new_val = subscription.next().fuse() => { + let new_val = new_val.expect("Client dropped prematurely"); + println!( + "{}", + format!("---------------- Message History ----------------").yellow() + ); + if let FunctionResult::Value(Value::Array(array)) = new_val { + for item in array { + if let Value::Object(obj) = item { + if let Some(Value::String(str)) = obj.get("body") { + let author = match obj.get("author") { + Some(Value::String(name)) => name, + _ => "Anonymous Author", + }; + let author_string = if author == &sender_clone { + format!("{author}").yellow().bold() + } else { + format!("{author}").red().bold() + }; + println!("{author_string}: {str:?}"); + } + } + } + } + println!( + "{}", + format!("-------------- End Message History --------------").yellow() + ); + }, + _ = cancel_fut => { + break + }, + } + } + println!("Message listener closed"); + }); + + // Loop for sending messages + loop { + let line = readline()?; + let line = line.trim(); + if line.is_empty() { + continue; + } + + if line == "quit" || line == "exit" { + println!( + "{}", + format!("------------- Exiting Convex Demo -------------").blue() + ); + break; + } + + println!("{}", format!("Sending a message").yellow().bold()); + let result = client_ + .mutation( + "messages:send", + btreemap! { + "body".to_string() => line.into(), + "author".to_string() => sender.clone().into(), + }, + ) + .await?; + match result { + FunctionResult::Value(Value::Null) => { + println!("{}.", format!("Message sent").green().bold()); + }, + FunctionResult::Value(v) => { + println!( + "{}", + format!("Unexpected non-null result from messages:send {v:?}") + .red() + .bold() + ); + }, + FunctionResult::ErrorMessage(err) => { + println!("{}.", err.red().bold()); + }, + FunctionResult::ConvexError(err) => { + println!("{err:?}"); + }, + }; + } + + cancel_sender + .send(()) + .expect("Failed to send termination signal"); + handle.await?; + + Ok(()) +} + +fn readline() -> anyhow::Result { + let mut buffer = String::new(); + std::io::stdin().read_line(&mut buffer)?; + if buffer.ends_with('\n') { + buffer.pop(); + if buffer.ends_with('\r') { + buffer.pop(); + } + } + Ok(buffer) +} diff --git a/third_party/convex_rs/examples/quickstart/convex/tasks.ts b/third_party/convex_rs/examples/quickstart/convex/tasks.ts new file mode 100644 index 00000000..31159f01 --- /dev/null +++ b/third_party/convex_rs/examples/quickstart/convex/tasks.ts @@ -0,0 +1,7 @@ +import { query } from "./_generated/server"; + +export const get = query({ + handler: async ({ db }) => { + return await db.query("tasks").collect(); + }, +}); diff --git a/third_party/convex_rs/examples/quickstart/main.rs b/third_party/convex_rs/examples/quickstart/main.rs new file mode 100644 index 00000000..9e5949a0 --- /dev/null +++ b/third_party/convex_rs/examples/quickstart/main.rs @@ -0,0 +1,18 @@ +use std::{ + collections::BTreeMap, + env, +}; + +use convex::ConvexClient; + +#[tokio::main] +async fn main() { + dotenvy::from_filename(".env.local").ok(); + dotenvy::dotenv().ok(); + + let deployment_url = env::var("CONVEX_URL").unwrap(); + + let mut client = ConvexClient::new(&deployment_url).await.unwrap(); + let result = client.query("tasks:get", BTreeMap::new()).await.unwrap(); + println!("{result:#?}"); +} diff --git a/third_party/convex_rs/examples/quickstart/package-lock.json b/third_party/convex_rs/examples/quickstart/package-lock.json new file mode 100644 index 00000000..be5de5f4 --- /dev/null +++ b/third_party/convex_rs/examples/quickstart/package-lock.json @@ -0,0 +1,540 @@ +{ + "name": "convex-rust-quickstart", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "convex-rust-quickstart", + "dependencies": { + "convex": "^1.34.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/convex": { + "version": "1.34.1", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.34.1.tgz", + "integrity": "sha512-ooyFnZVVq0u6b5zt0Ptq8QB2ixhf/2vXe+PIcUtdtrs0lq/TwpkmmruHdqkFmWgMd6N+Tmfy8AGkz6QnZUYZBA==", + "license": "Apache-2.0", + "dependencies": { + "esbuild": "0.27.0", + "prettier": "^3.0.0", + "ws": "8.18.0" + }, + "bin": { + "convex": "bin/main.js" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@auth0/auth0-react": "^2.0.1", + "@clerk/clerk-react": "^4.12.8 || ^5.0.0", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@auth0/auth0-react": { + "optional": true + }, + "@clerk/clerk-react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/third_party/convex_rs/examples/quickstart/package.json b/third_party/convex_rs/examples/quickstart/package.json new file mode 100644 index 00000000..4015a3c7 --- /dev/null +++ b/third_party/convex_rs/examples/quickstart/package.json @@ -0,0 +1,7 @@ +{ + "name": "convex-rust-quickstart", + "private": true, + "dependencies": { + "convex": "^1.34.1" + } +} diff --git a/third_party/convex_rs/examples/quickstart/sampleData.jsonl b/third_party/convex_rs/examples/quickstart/sampleData.jsonl new file mode 100644 index 00000000..737408ee --- /dev/null +++ b/third_party/convex_rs/examples/quickstart/sampleData.jsonl @@ -0,0 +1,3 @@ +{"text": "Buy groceries", "isCompleted": true} +{"text": "Go for a swim", "isCompleted": true} +{"text": "Integrate Convex", "isCompleted": false} \ No newline at end of file diff --git a/third_party/convex_rs/rust-toolchain b/third_party/convex_rs/rust-toolchain new file mode 100644 index 00000000..2a8b1190 --- /dev/null +++ b/third_party/convex_rs/rust-toolchain @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2026-02-18" diff --git a/third_party/convex_rs/rustfmt.toml b/third_party/convex_rs/rustfmt.toml new file mode 100644 index 00000000..753cfef0 --- /dev/null +++ b/third_party/convex_rs/rustfmt.toml @@ -0,0 +1,16 @@ +use_field_init_shorthand = true +use_try_shorthand = true +match_block_trailing_comma = true + +# Nightly only options: +unstable_features = true +condense_wildcard_suffixes = true +format_strings = true +imports_granularity = "Crate" +reorder_impl_items = true +imports_layout = "Vertical" +group_imports = "StdExternalCrate" +wrap_comments = true +normalize_comments = false +error_on_line_overflow = true +style_edition = "2021" # in 2024 sort order is now case-insensitive, deferring the mass reformat diff --git a/third_party/convex_rs/src/base_client/mod.rs b/third_party/convex_rs/src/base_client/mod.rs new file mode 100644 index 00000000..5226fe3b --- /dev/null +++ b/third_party/convex_rs/src/base_client/mod.rs @@ -0,0 +1,1062 @@ +//! The synchronous state machine for Convex. It's +//! recommended to use the higher level [`ConvexClient`] unless you are building +//! a framework. +//! +//! See docs for [`BaseConvexClient`]. +use std::{ + cmp, + collections::{ + BTreeMap, + BTreeSet, + VecDeque, + }, + future::Future, + pin::Pin, +}; + +use convex_sync_types::{ + types::SerializedArgs, + AuthenticationToken, + CanonicalizedUdfPath, + ClientMessage, + IdentityVersion, + QueryId, + QuerySetModification, + QuerySetVersion, + SessionRequestSeqNumber, + StateModification, + StateVersion, + Timestamp, + UdfPath, +}; +use serde_json::json; +use tokio::sync::oneshot; + +#[cfg(doc)] +use crate::ConvexClient; +use crate::{ + convex_logs, + sync::{ + ReconnectProtocolReason, + ServerMessage, + }, + value::Value, + ConvexError, +}; + +mod request_manager; +use request_manager::{ + RequestId, + RequestManager, +}; +mod query_result; +pub use query_result::{ + FunctionResult, + QueryResults, +}; + +use self::request_manager::RequestType; + +/// A callback that fetches an auth token. The `bool` parameter indicates +/// whether a forced refresh is requested (e.g. on websocket reconnect). +pub type AuthTokenFetcher = Box< + dyn Fn(bool) -> Pin> + Send>> + + Send + + Sync, +>; + +#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] +struct QueryToken(String); + +#[derive(Clone, Debug)] +struct LocalQuery { + id: QueryId, + canonicalized_udf_path: CanonicalizedUdfPath, + args: BTreeMap, + num_subscribers: usize, // TODO: remove + /// A unique index value for each subscription to this query. + /// + /// Must be incremented each time a new subscription is added, and never + /// decremented. + subscription_index: usize, +} + +#[derive(Clone, Debug)] +struct Query { + result: FunctionResult, + _udf_path: CanonicalizedUdfPath, + _args: BTreeMap, +} + +/// An identifier for a single subscriber to a query. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash)] +#[cfg_attr(test, derive(proptest_derive::Arbitrary))] +pub struct SubscriberId(QueryId, usize); + +impl SubscriberId { + #[cfg(test)] + pub fn query_id(&self) -> QueryId { + self.0 + } +} + +fn serialize_path_and_args(udf_path: UdfPath, args: BTreeMap) -> QueryToken { + let json_path: String = udf_path.canonicalize().into(); + let json_args: serde_json::Value = Value::Array(vec![Value::Object(args)]).into(); + let json = json!({ + "udfPath": json_path, + "args": json_args, + }); + QueryToken(json.to_string()) +} + +#[derive(Default)] +struct LocalSyncState { + next_query_id: QueryId, + query_set_version: QuerySetVersion, + query_set: BTreeMap, + query_id_to_token: BTreeMap, + latest_results: QueryResults, + identity_version: IdentityVersion, + auth_fetcher: Option, + last_auth_token: AuthenticationToken, +} + +impl LocalSyncState { + fn subscribe( + &mut self, + udf_path: UdfPath, + args: BTreeMap, + ) -> (Option, SubscriberId) { + let canonicalized_udf_path = udf_path.clone().canonicalize(); + let query_token = serialize_path_and_args(udf_path.clone(), args.clone()); + + if let Some(existing_entry) = self.query_set.get_mut(&query_token) { + // This is a new subscription to an existing query. + existing_entry.num_subscribers += 1; + existing_entry.subscription_index += 1; + let query_id = existing_entry.id; + let subscription = SubscriberId(query_id, existing_entry.subscription_index); + let prev = self.latest_results.subscribers.insert(subscription); + assert!(prev.is_none(), "INTERNAL BUG: Subscriber ID already taken."); + return (None, subscription); + } + + let query_id = self.next_query_id; + self.next_query_id = QueryId::new(self.next_query_id.get_id() + 1); + let base_version = self.query_set_version; + self.query_set_version += 1; + let new_version = self.query_set_version; + + let add = QuerySetModification::Add(convex_sync_types::Query { + query_id, + udf_path, + args: SerializedArgs::from_args(vec![Value::Object(args.clone()).into()]) + .expect("Could not serialize query arguments"), + journal: None, + component_path: None, + }); + let message = ClientMessage::ModifyQuerySet { + base_version, + new_version, + modifications: vec![add], + }; + + let query = LocalQuery { + id: query_id, + canonicalized_udf_path, + args, + num_subscribers: 1, + subscription_index: 0, + }; + + self.query_set.insert(query_token.clone(), query); + self.query_id_to_token.insert(query_id, query_token.clone()); + let subscription = SubscriberId(query_id, 0); + let prev = self.latest_results.subscribers.insert(subscription); + assert!(prev.is_none(), "INTERNAL BUG: Subscriber ID already taken."); + (Some(message), subscription) + } + + fn remove_subscriber(&mut self, subscriber_id: SubscriberId) -> Option { + let query_id = self + .latest_results + .subscribers + .remove(&subscriber_id) + .expect("INTERNAL BUG: Dropped unknown Subscriber ID") + .0; + let query_token = match self.query_token(query_id) { + None => panic!("INTERNAL BUG: Unknown query id {query_id}"), + Some(t) => t, + }; + let local_query = match self.query_set.get_mut(&query_token) { + None => panic!("INTERNAL BUG: No query found for query token {query_token:?}",), + Some(q) => q, + }; + + // Update local state + if local_query.num_subscribers > 1 { + local_query.num_subscribers -= 1; + return None; + } + self.query_set.remove(&query_token); + self.query_id_to_token.remove(&query_id); + self.latest_results.results.remove(&query_id); + + let base_version = self.query_set_version; + self.query_set_version += 1; + let new_version = self.query_set_version; + + let remove = QuerySetModification::Remove { query_id }; + Some(ClientMessage::ModifyQuerySet { + base_version, + new_version, + modifications: vec![remove], + }) + } + + fn query_token(&self, query_id: QueryId) -> Option { + self.query_id_to_token.get(&query_id).cloned() + } + + fn query_args(&self, query_id: QueryId) -> Option> { + Some( + self.query_set + .get(&self.query_token(query_id)?)? + .args + .clone(), + ) + } + + fn query_path(&self, query_id: QueryId) -> Option { + Some( + self.query_set + .get(&self.query_token(query_id)?)? + .canonicalized_udf_path + .clone(), + ) + } + + fn authenticate(&mut self, token: AuthenticationToken) -> (ClientMessage, bool) { + let base_version = self.identity_version; + self.identity_version += 1; + let token_changed = token != self.last_auth_token; + self.last_auth_token = token.clone(); + ( + ClientMessage::Authenticate { + base_version, + token, + }, + token_changed, + ) + } + + async fn restart(&mut self) -> (Vec, bool) { + self.identity_version = 0; + let mut messages = Vec::new(); + let mut auth_token_changed = false; + + // If we have a fetcher, get a fresh token for the new connection. + if let Some(ref fetcher) = self.auth_fetcher { + match fetcher(true).await { + Ok(token) if token != AuthenticationToken::None => { + auth_token_changed = token != self.last_auth_token; + self.last_auth_token = token.clone(); + messages.push(ClientMessage::Authenticate { + base_version: 0, + token, + }); + self.identity_version += 1; + }, + Ok(_) => {}, + Err(e) => { + tracing::error!( + "Auth fetcher failed during reconnect: {e:?}. Skipping auth for this \ + reconnect attempt." + ); + }, + } + } + + let mut modifications = Vec::new(); + for local_query in self.query_set.values() { + let add = QuerySetModification::Add(convex_sync_types::Query { + query_id: local_query.id, + udf_path: local_query.canonicalized_udf_path.clone().into(), + args: SerializedArgs::from_args(vec![ + Value::Object(local_query.args.clone()).into() + ]) + .expect("Could not serialize query arguments"), + journal: None, + component_path: None, + }); + modifications.push(add) + } + self.query_set_version = 1; + + messages.push(ClientMessage::ModifyQuerySet { + base_version: 0, + new_version: 1, + modifications, + }); + + (messages, auth_token_changed) + } +} + +#[derive(Debug)] +struct RemoteQuerySet { + version: StateVersion, + remote_query_set: BTreeMap, +} + +impl RemoteQuerySet { + fn new() -> Self { + Self { + version: StateVersion::initial(), + remote_query_set: Default::default(), + } + } + + fn transition(&mut self, transition: ServerMessage) -> Result<(), ReconnectProtocolReason> { + let ServerMessage::Transition { + start_version, + end_version, + modifications, + client_clock_skew: _, + server_ts: _, + } = transition + else { + panic!("not transition"); + }; + if start_version != self.version { + tracing::error!( + "INTERNAL BUG: Protocol Error start_version {:?} is different from self.version \ + {:?}", + start_version, + self.version + ); + return Err("StartVersionMismatch".into()); + } + for modification in modifications { + match modification { + StateModification::QueryUpdated { + query_id, + value, + log_lines, + journal: _, + } => { + for log_line in log_lines.0 { + convex_logs!("{}", log_line); + } + self.remote_query_set + .insert(query_id, FunctionResult::Value(value)); + }, + StateModification::QueryFailed { + query_id, + error_message, + log_lines, + journal: _, + error_data, + } => { + for log_line in log_lines.0 { + convex_logs!("{}", log_line); + } + let function_result = match error_data { + Some(v) => FunctionResult::ConvexError(ConvexError { + message: error_message, + data: v, + }), + None => FunctionResult::ErrorMessage(error_message), + }; + self.remote_query_set.insert(query_id, function_result); + }, + StateModification::QueryRemoved { query_id } => { + self.remote_query_set.remove(&query_id); + }, + } + } + self.version = end_version; + Ok(()) + } +} + +#[derive(Default, Debug)] +struct OptimisticQueryResults { + query_results: BTreeMap, +} + +impl OptimisticQueryResults { + fn ingest_query_results_from_server( + &mut self, + server_query_results: BTreeMap, + _optimistic_updates_to_drop: BTreeSet, + ) -> BTreeMap { + // TODO: use optimistic_updates_to_drop + let old_query_results = self.query_results.clone(); + self.query_results = server_query_results; + let mut changed_queries = BTreeMap::new(); + for (query_id, query) in self.query_results.iter() { + let old_query = old_query_results.get(query_id); + if match old_query { + Some(old_query) => old_query.result != query.result, + None => true, + } { + let result = query.result.clone(); + changed_queries.insert(*query_id, result); + } + } + changed_queries + } + + fn query_result(&self, query_id: QueryId) -> Option { + self.query_results.get(&query_id).map(|q| q.result.clone()) + } +} + +/// The synchronous state machine for the `ConvexClient`. It's recommended to +/// use the higher level `ConvexClient` unless you are building a framework. +/// +/// This struct should be used instead of the `ConvexClient` when you want the +/// ability to build consistent client views. For example, in order to use your +/// own websocket manager or make a client compatible with another language +/// (e.g. Swift or Python). +/// +/// For the latter use case, we strongly recommend you to take a look at the +/// implementation of the `ConvexClient`. The recommended pattern to use an +/// [`BaseConvexClient`] is to create a background thread to manage actions on +/// queries/mutations and incoming websocket connections, and use that to +/// advance the BaseConvexClient's state. +/// +/// ## Managing Convex State +/// The main methods, [`subscribe`](Self::subscribe()), +/// [`unsubscribe`](Self::unsubscribe()), and +/// [`mutation`](Self::mutation()) directly correspond to its +/// equivalent for the external [ConvexClient]. +/// +/// The only different method is [`get_query`](Self::get_query()), which +/// returns the current value for a query given its query id. This method can be +/// used to synchronously request the current value, as opposed to a stream of +/// values in [`subscribe`](crate::ConvexClient::subscribe()). +/// +/// **Note: these methods have the side effect of +/// adding messages to be sent to the server, so you would need to flush all +/// outgoing messages by looping on +/// [`pop_next_message`](Self::pop_next_message()) after each call of the above +/// functions.** +/// +/// ## Watching for consistent updates to queries +/// To watch for consistent changes in query values, you can add the following +/// code to the background thread: +/// ```no_run +/// use convex::base_client::BaseConvexClient; +/// use convex::Value; +/// use convex_sync_types::ServerMessage; +/// +/// fn on_receive_server_message(mut base_client: BaseConvexClient, msg: ServerMessage) { +/// let res = base_client.receive_message(msg).expect("Base client error"); +/// if let Some(latest_result_map) = res { +/// for (subscriber_id, function_result) in latest_result_map.iter() { +/// // Notify components of the updated_value +/// } +/// } +/// } +/// ``` +/// +/// ## Managing Web Socket States +/// To manage websocket messages, use +/// [`receive_message`](Self::receive_message()) (for incoming messages from the +/// server) and [`pop_next_message`](Self::pop_next_message()) (for outgoing +/// messages to send to the server). **The [`BaseConvexClient`] does not +/// send these messages, so you will have to regularly monitor if there are +/// messages to be sent by calling +/// [`pop_next_message`](Self::pop_next_message()).** +/// +/// Additionally, when the websocket reconnects, you should call +/// [`resend_ongoing_queries_mutations`](Self::resend_ongoing_queries_mutations()) and loop on +/// [`pop_next_message`](Self::pop_next_message()) to resend requests to the +/// Server to resubscribe to queries and perform ongoing mutations. +/// +/// #### [`pop_next_message`](Self::pop_next_message()) should be called after the following methods: +/// - [`resend_ongoing_queries_mutations`](Self::resend_ongoing_queries_mutations()) +/// - [`subscribe`](Self::unsubscribe()) +/// - [`unsubscribe`](Self::unsubscribe()) +/// - [`mutation`](Self::unsubscribe()) +pub struct BaseConvexClient { + state: LocalSyncState, + remote_query_set: RemoteQuerySet, + optimistic_query_results: OptimisticQueryResults, + request_manager: RequestManager, + next_request_id: SessionRequestSeqNumber, + outgoing_message_queue: VecDeque, + max_observed_timestamp: Option, +} + +impl BaseConvexClient { + /// Construct a new [`BaseConvexClient`]. + pub fn new() -> Self { + let request_manager = RequestManager::new(); + let state = LocalSyncState::default(); + let remote_query_set = RemoteQuerySet::new(); + let optimistic_query_results: OptimisticQueryResults = Default::default(); + let next_request_id: SessionRequestSeqNumber = 0; + + BaseConvexClient { + request_manager, + state, + remote_query_set, + optimistic_query_results, + next_request_id, + outgoing_message_queue: VecDeque::new(), + max_observed_timestamp: None, + } + } + + /// Update state to be subscribed to a query and add subscription request to + /// the outgoing message queue. + /// + /// After calling this, it is highly recommended to loop on + /// [`pop_next_message`](Self::pop_next_message()) to flush websocket + /// messages to the server. + pub fn subscribe(&mut self, udf_path: UdfPath, args: BTreeMap) -> SubscriberId { + let (modification, subscription) = self.state.subscribe(udf_path, args); + if let Some(modification) = modification { + self.outgoing_message_queue.push_back(modification); + } + subscription + } + + /// Update state to be unsubscribed to a query and add unsubscription + /// request to the outgoing message queue. + /// + /// After calling this, it is highly recommended to loop on + /// [`pop_next_message`](Self::pop_next_message()) to flush websocket + /// messages to the server. + pub fn unsubscribe(&mut self, subscriber_id: SubscriberId) { + let unsubscribe_message = self.state.remove_subscriber(subscriber_id); + + if let Some(message) = unsubscribe_message { + self.outgoing_message_queue.push_back(message); + } + } + + /// Return the local value of a query. + pub fn get_query(&self, query_id: QueryId) -> Option { + self.local_query_result(query_id) + } + + /// Track mutation and add mutation request to the outgoing message queue. + /// + /// After calling this, it is highly recommended to loop on + /// [`pop_next_message`](Self::pop_next_message()) to flush websocket + /// messages to the server. + pub fn mutation( + &mut self, + udf_path: UdfPath, + args: BTreeMap, + ) -> oneshot::Receiver { + let request_id = self.next_request_id; + self.next_request_id = request_id + 1; + tracing::info!("Starting mutation {udf_path} with id {request_id}"); + let message = ClientMessage::Mutation { + request_id, + udf_path, + args: SerializedArgs::from_args(vec![Value::Object(args).into()]) + .expect("Failed to serialize arguments"), + component_path: None, + }; + + let result_receiver = self.request_manager.track_request( + &message, + RequestId::new(request_id), + RequestType::Mutation, + ); + self.outgoing_message_queue.push_back(message); + result_receiver + } + + /// Track action and add action request to the outgoing message queue. + /// + /// After calling this, it is highly recommended to loop on + /// [`pop_next_message`](Self::pop_next_message()) to flush websocket + /// messages to the server. + pub fn action( + &mut self, + udf_path: UdfPath, + args: BTreeMap, + ) -> oneshot::Receiver { + let request_id = self.next_request_id; + self.next_request_id = request_id + 1; + tracing::info!("Starting action {udf_path:?} with id {request_id:?}"); + let message = ClientMessage::Action { + request_id, + udf_path, + args: SerializedArgs::from_args(vec![Value::Object(args).into()]).unwrap(), + component_path: None, + }; + + let result_receiver = self.request_manager.track_request( + &message, + RequestId::new(request_id), + RequestType::Action, + ); + self.outgoing_message_queue.push_back(message); + result_receiver + } + + /// Store (or clear) an auth token fetcher callback and update auth state. + /// + /// When a fetcher is provided it is invoked immediately (with + /// `force_refresh=false`) and stored for future reconnects — on each + /// websocket reconnect the fetcher is called again with + /// `force_refresh=true`. + /// + /// When `None` is passed the stored fetcher is cleared and auth is unset. + pub async fn set_auth_fetcher(&mut self, fetcher: Option) -> bool { + let mut auth_token_changed = false; + match fetcher { + Some(fetcher) => { + match fetcher(false).await { + Ok(token) => { + let (message, changed) = self.state.authenticate(token); + auth_token_changed = changed; + self.outgoing_message_queue.push_back(message); + }, + Err(e) => { + tracing::error!("Auth token fetcher failed: {e:?}"); + }, + } + self.state.auth_fetcher = Some(fetcher); + }, + None => { + self.state.auth_fetcher = None; + let (message, changed) = self.state.authenticate(AuthenticationToken::None); + auth_token_changed = changed; + self.outgoing_message_queue.push_back(message); + }, + } + auth_token_changed + } + + /// Pop the next message from the outgoing message queue. + /// + /// Note that this does not *send* the message because the Internal client + /// has no awareness of websockets. After popping the next message, it is + /// the caller's responsibility to actually send it. + pub fn pop_next_message(&mut self) -> Option { + self.outgoing_message_queue.pop_front() + } + + fn observe_timestamp(&mut self, ts: Timestamp) { + if let Some(max_observed_timestamp) = self.max_observed_timestamp { + self.max_observed_timestamp = Some(cmp::max(ts, max_observed_timestamp)); + } else { + self.max_observed_timestamp = Some(ts); + } + } + + /// Returns the maximum timestamp observed by the client. + pub fn max_observed_timestamp(&self) -> Option { + self.max_observed_timestamp + } + + /// Given a message from a Server, update the base state accordingly. + pub fn receive_message( + &mut self, + message: ServerMessage, + ) -> Result, ReconnectProtocolReason> { + match message { + ServerMessage::Transition { end_version, .. } => { + self.observe_timestamp(end_version.ts); + self.remote_query_set.transition(message)?; + let completed_requests = self + .request_manager + .remove_and_notify_completed(end_version.ts); + let changed_query_ids = self.on_query_result_changes(completed_requests)?; + for (id, result) in changed_query_ids { + self.state.latest_results.results.insert(id, result); + } + return Ok(Some(self.state.latest_results.clone())); + }, + ServerMessage::MutationResponse { + request_id, + result, + ts, + log_lines, + } => { + for log_line in log_lines.0 { + convex_logs!("{}", log_line); + } + + if let Some(ts) = ts { + self.observe_timestamp(ts); + } + let request_id = RequestId::new(request_id); + self.request_manager.update_request( + &request_id, + RequestType::Mutation, + result.into(), + ts, + )?; + }, + ServerMessage::AuthError { + error_message, + base_version, + .. + } => { + tracing::error!( + "AuthError: {error_message} for identity version {base_version:?}. Restarting \ + protocol." + ); + return Err(format!( + "AuthError: {error_message} for identity version {base_version:?}" + )); + }, + ServerMessage::FatalError { error_message } => { + tracing::error!("FatalError: {error_message}. Restarting protocol."); + return Err(format!("FatalError: {error_message}")); + }, + ServerMessage::ActionResponse { + request_id, + result, + log_lines, + } => { + for log_line in log_lines.0 { + convex_logs!("{}", log_line); + } + let request_id = RequestId::new(request_id); + self.request_manager.update_request( + &request_id, + RequestType::Action, + result.into(), + None, + )?; + }, + ServerMessage::Ping => { + // Do nothing + }, + ServerMessage::TransitionChunk { .. } => { + // The Rust client should never receive TransitionChunk messages + // as this feature is only enabled for npm clients + return Err("Unexpected TransitionChunk message received".to_string()); + }, + } + Ok(None) + } + + /// Grab a snapshot of the latest query results to all subscribed queries. + pub fn latest_results(&self) -> &QueryResults { + &self.state.latest_results + } + + /// Resend all subscribed queries and ongoing mutations. Should be used once + /// the websocket closes and reconnects. + pub async fn resend_ongoing_queries_mutations(&mut self) -> bool { + // Clear any stale messages from the queue. During reconnection + // retries, messages can accumulate from previous failed attempts + // or from subscription changes made while disconnected. Since + // restart() rebuilds the full query set and resets version + // numbers, any pre-existing messages would have stale versions + // that conflict with the fresh restart messages. + self.outgoing_message_queue.clear(); + + let (state_restart_messages, auth_token_changed) = self.state.restart().await; + let mut ongoing_mutation_messages = self.request_manager.restart(); + + self.remote_query_set = RemoteQuerySet::new(); + for state_restart_message in state_restart_messages { + self.outgoing_message_queue.push_back(state_restart_message); + } + self.outgoing_message_queue + .append(&mut ongoing_mutation_messages); + auth_token_changed + } + + fn on_query_result_changes( + &mut self, + completed_requests: BTreeSet, + ) -> Result, ReconnectProtocolReason> { + let remote_query_results = &self.remote_query_set.remote_query_set; + let mut query_id_to_value = BTreeMap::new(); + for (query_id, result) in remote_query_results.iter() { + let Some(_udf_path) = self.state.query_path(*query_id) else { + // It's possible that we've already unsubscribed to this query but + // the server hasn't learned about that yet. If so, ignore this one. + continue; + }; + let _args = self + .state + .query_args(*query_id) + .expect("INTERNAL BUG: Query args exist, but not query path."); + query_id_to_value.insert( + *query_id, + Query { + result: result.clone(), + _udf_path, + _args, + }, + ); + } + Ok(self + .optimistic_query_results + .ingest_query_results_from_server(query_id_to_value, completed_requests)) + } + + fn local_query_result(&self, query_id: QueryId) -> Option { + self.optimistic_query_results.query_result(query_id) + } +} + +/// Macro used for piping UDF logs to a custom formatter that exposes +/// just the log content, without any additional Rust metadata. +#[macro_export] +macro_rules! convex_logs { + (target: $target:expr, $($arg:tt)+) => { + tracing::event!(target: "convex_logs", tracing::Level::DEBUG, $($arg)+); + // Additional custom behavior can be added here + }; + ($($arg:tt)+) => { + tracing::event!(target: "convex_logs", tracing::Level::DEBUG, $($arg)+); + // Additional custom behavior can be added here + }; +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use convex_sync_types::{ + AuthenticationToken, + ClientMessage, + LogLinesMessage, + QuerySetVersion, + UdfPath, + }; + use maplit::btreemap; + + use super::*; + + /// Simulates the server-side version tracking from + /// `sync::state::SyncState::modify_query_set`. Returns Err with the + /// same message the server produces when versions don't match. + fn simulate_server_version_check(messages: &[ClientMessage]) -> Result<(), String> { + let mut query_set_version: QuerySetVersion = 0; + for msg in messages { + if let ClientMessage::ModifyQuerySet { + base_version, + new_version, + .. + } = msg + { + if *base_version != query_set_version { + return Err(format!( + "Base version {base_version} passed up doesn't match the current version \ + {query_set_version}" + )); + } + query_set_version = *new_version; + } + } + Ok(()) + } + + /// Reproduces the bug where repeated reconnection attempts accumulate + /// stale messages in the outgoing queue, causing the server to reject + /// messages with "Base version 0 passed up doesn't match the current + /// version 1". + /// + /// In the real client, this happens when `communicate()` is interrupted + /// by a `ProtocolResponse::Failure` mid-drain (e.g. the WebSocket + /// connection attempt fails). The first message is popped and sent to + /// the WebSocket worker channel, but remaining messages stay in the + /// queue. When `resend_ongoing_queries_mutations()` appends fresh + /// restart messages, the stale leftovers cause version conflicts. + #[tokio::test] + async fn test_reconnect_does_not_send_duplicate_version_messages() { + let mut client = BaseConvexClient::new(); + + // Authenticated client with one active subscription. + client + .set_auth_fetcher(Some(Box::new(|_force_refetch| { + Box::pin(async { Ok(AuthenticationToken::User("test-token".into())) }) + }))) + .await; + let udf = UdfPath::from_str("some:query").unwrap(); + client.subscribe(udf, btreemap! {}); + + // Drain initial messages (successfully sent while connected). + while client.pop_next_message().is_some() {} + + // --- Connection drops, first reconnect attempt --- + assert!(!client.resend_ongoing_queries_mutations().await); + + // Simulate partial drain: the first message (Authenticate) was + // popped and handed to the WebSocket layer, but the connection + // failed before the second message (ModifyQuerySet) could be sent. + let _ = client.pop_next_message(); + + // --- Connection still down, second reconnect attempt --- + assert!(!client.resend_ongoing_queries_mutations().await); + + // Connection finally succeeds — all queued messages are flushed. + let mut messages = vec![]; + while let Some(msg) = client.pop_next_message() { + messages.push(msg); + } + + // The server tracks query set versions sequentially and rejects + // any message whose base_version doesn't match its current state + // (sync::state::SyncState::modify_query_set). Without the fix, + // the stale ModifyQuerySet{base_version:0} from the first attempt + // is still in the queue, followed by the second attempt's + // ModifyQuerySet{base_version:0}. + simulate_server_version_check(&messages) + .expect("Server would reject these messages with a FatalError"); + } + + #[tokio::test] + async fn test_reconnect_path_requests_refreshed_token() { + let mut client = BaseConvexClient::new(); + + // Authenticated client with one active subscription. + client + .set_auth_fetcher(Some(Box::new(|force_refetch| { + Box::pin(async move { + if force_refetch { + // A fake refreshed token. + Ok(AuthenticationToken::User("refetched-token".into())) + } else { + Ok(AuthenticationToken::User("original-token".into())) + } + }) + }))) + .await; + let udf = UdfPath::from_str("some:query").unwrap(); + client.subscribe(udf, btreemap! {}); + + // Drain initial messages (successfully sent while connected). + while client.pop_next_message().is_some() {} + + // --- Connection drops, reconnect attempt --- + assert!(client.resend_ongoing_queries_mutations().await); + + // A fresh authentication attempt should have been initiated, with a new token. + assert_eq!( + client + .pop_next_message() + .expect("Expected an authentication message."), + ClientMessage::Authenticate { + base_version: 0, + token: AuthenticationToken::User("refetched-token".into()), + } + ); + } + + fn drain_add_message(client: &mut BaseConvexClient) -> QueryId { + match client.pop_next_message() { + Some(ClientMessage::ModifyQuerySet { modifications, .. }) => { + let [QuerySetModification::Add(query)] = modifications.as_slice() else { + panic!("expected a single add modification, got {modifications:?}"); + }; + query.query_id + }, + other => panic!("expected add query message, got {other:?}"), + } + } + + fn drain_remove_message(client: &mut BaseConvexClient) -> QueryId { + match client.pop_next_message() { + Some(ClientMessage::ModifyQuerySet { modifications, .. }) => { + let [QuerySetModification::Remove { query_id }] = modifications.as_slice() else { + panic!("expected a single remove modification, got {modifications:?}"); + }; + *query_id + }, + other => panic!("expected remove query message, got {other:?}"), + } + } + + fn apply_query_update( + client: &mut BaseConvexClient, + version: &mut StateVersion, + query_id: QueryId, + value: Value, + ) { + let end_version = StateVersion { + ts: version.ts.succ().expect("timestamp overflow in test"), + ..*version + }; + let transition = ServerMessage::Transition { + start_version: *version, + end_version, + modifications: vec![StateModification::QueryUpdated { + query_id, + value, + log_lines: LogLinesMessage(vec![]), + journal: None, + }], + client_clock_skew: None, + server_ts: None, + }; + + let latest_results = client + .receive_message(transition) + .expect("transition should be accepted"); + assert!( + latest_results.is_some(), + "query update should publish results" + ); + *version = end_version; + } + + #[test] + fn test_final_unsubscribe_removes_cached_query_result() { + let mut client = BaseConvexClient::new(); + let mut version = StateVersion::initial(); + // Add a subscriber. + let subscriber_id = client.subscribe("getValue1".parse().unwrap(), BTreeMap::new()); + let query_id = drain_add_message(&mut client); + assert!(client.pop_next_message().is_none()); + + apply_query_update(&mut client, &mut version, query_id, 10.into()); + assert!(client.state.latest_results.results.contains_key(&query_id)); + assert_eq!( + client.latest_results().get(&subscriber_id), + Some(&FunctionResult::Value(10.into())) + ); + + client.unsubscribe(subscriber_id); + + assert_eq!(drain_remove_message(&mut client), query_id); + assert!(client.pop_next_message().is_none()); + + // The latest_results are gone since there are no more subscribers. + assert!(client.state.latest_results.subscribers.is_empty()); + assert!(!client.state.latest_results.results.contains_key(&query_id)); + } + + #[test] + fn test_cached_query_result_persists_while_subscribers_exist() { + let mut client = BaseConvexClient::new(); + let mut version = StateVersion::initial(); + // Add two subscribers. + let subscriber_a = client.subscribe("getValue1".parse().unwrap(), BTreeMap::new()); + let query_id = drain_add_message(&mut client); + let subscriber_b = client.subscribe("getValue1".parse().unwrap(), BTreeMap::new()); + assert!(client.pop_next_message().is_none()); + + apply_query_update(&mut client, &mut version, query_id, 10.into()); + + // The first subscriber drops. + client.unsubscribe(subscriber_a); + + assert!(client.pop_next_message().is_none()); + + // The latest_results persist since a subscriber still exists. + assert!(client.state.latest_results.results.contains_key(&query_id)); + assert_eq!( + client.latest_results().get(&subscriber_b), + Some(&FunctionResult::Value(10.into())) + ); + } +} diff --git a/third_party/convex_rs/src/base_client/query_result.rs b/third_party/convex_rs/src/base_client/query_result.rs new file mode 100644 index 00000000..4f870150 --- /dev/null +++ b/third_party/convex_rs/src/base_client/query_result.rs @@ -0,0 +1,159 @@ +use convex_sync_types::{ + types::ErrorPayload, + QueryId, +}; +use imbl::{ + OrdMap, + OrdSet, +}; + +use super::SubscriberId; +use crate::{ + ConvexError, + Value, +}; + +/// Result of a Convex function (query/mutation/action). +/// +/// The function returns a Convex value or an error message string. +#[derive(Clone, Eq, PartialEq)] +pub enum FunctionResult { + /// The Convex value returned on a successful run of a Convex function + Value(Value), + /// The error message of a Convex function run that does not complete + /// successfully. + ErrorMessage(String), + /// The error payload of a Convex function run that doesn't complete + /// successfully, with an application-level error. + ConvexError(ConvexError), +} + +impl From>> for FunctionResult { + fn from(result: Result>) -> Self { + match result { + Ok(value) => FunctionResult::Value(value), + Err(ErrorPayload::ErrorData { message, data }) => { + FunctionResult::ConvexError(ConvexError { message, data }) + }, + Err(ErrorPayload::Message(message)) => FunctionResult::ErrorMessage(message), + } + } +} + +impl From for Result> { + fn from(result: FunctionResult) -> Self { + match result { + FunctionResult::Value(value) => Ok(value), + FunctionResult::ErrorMessage(error) => Err(ErrorPayload::Message(error)), + FunctionResult::ConvexError(error) => Err(ErrorPayload::ErrorData { + message: error.message, + data: error.data, + }), + } + } +} + +impl std::fmt::Debug for FunctionResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FunctionResult::Value(value) => f.debug_tuple("Value").field(value).finish(), + FunctionResult::ErrorMessage(error) => write!(f, "{error}"), + FunctionResult::ConvexError(error) => { + f.debug_tuple("ConvexError").field(error).finish() + }, + } + } +} + +/// A mapping from [`SubscriberId`] to its current result [`FunctionResult`] +/// for each actively subscribed query. +#[derive(Clone, Default, Debug)] +pub struct QueryResults { + pub(super) results: OrdMap, + pub(super) subscribers: OrdSet, +} + +impl QueryResults { + /// Get the [`FunctionResult`] for the given [`SubscriberId`] + pub fn get(&self, subscriber_id: &SubscriberId) -> Option<&FunctionResult> { + if !self.subscribers.contains(subscriber_id) { + return None; + }; + self.results.get(&subscriber_id.0) + } + + /// Get the size of the map. + pub fn len(&self) -> usize { + self.subscribers.len() + } + + /// Test whether the map is empty. + pub fn is_empty(&self) -> bool { + self.subscribers.is_empty() + } + + /// Get an iterator over the subscriber_id/query_result pairs of the map. + pub fn iter(&self) -> impl Iterator)> { + self.subscribers.iter().map(|s| (s, self.results.get(&s.0))) + } +} + +#[cfg(test)] +mod tests { + use convex_sync_types::QueryId; + use imbl::{ + ordmap, + ordset, + }; + + use crate::{ + base_client::SubscriberId, + FunctionResult, + QueryResults, + Value, + }; + + #[test] + fn test_query_results() { + let q = QueryId::new; + let s = SubscriberId; + + let qr = QueryResults { + results: ordmap! { + q(0) => FunctionResult::Value(Value::Null), + q(1) => FunctionResult::Value(Value::Int64(5)) + }, + subscribers: ordset! { + s(q(0), 0), + s(q(0), 1), + s(q(1), 0), + s(q(2), 0) + }, + }; + assert_eq!( + qr.get(&s(q(0), 0)), + Some(&FunctionResult::Value(Value::Null)) + ); + assert_eq!( + qr.get(&s(q(0), 1)), + Some(&FunctionResult::Value(Value::Null)) + ); + assert_eq!( + qr.get(&s(q(1), 0)), + Some(&FunctionResult::Value(Value::Int64(5))) + ); + assert_eq!(qr.get(&s(q(2), 0)), None,); + assert_eq!(qr.len(), 4); + assert!(!qr.is_empty()); + let v: Vec<_> = qr.iter().collect(); + assert_eq!( + v, + vec![ + (&s(q(0), 0), Some(&FunctionResult::Value(Value::Null))), + (&s(q(0), 1), Some(&FunctionResult::Value(Value::Null))), + (&s(q(1), 0), Some(&FunctionResult::Value(Value::Int64(5)))), + (&s(q(2), 0), None), + ], + ); + } +} diff --git a/third_party/convex_rs/src/base_client/request_manager.rs b/third_party/convex_rs/src/base_client/request_manager.rs new file mode 100644 index 00000000..8c461354 --- /dev/null +++ b/third_party/convex_rs/src/base_client/request_manager.rs @@ -0,0 +1,170 @@ +use std::{ + cmp::Reverse, + collections::{ + BTreeMap, + BTreeSet, + VecDeque, + }, +}; + +use convex_sync_types::{ + ClientMessage, + Timestamp, +}; +use tokio::sync::oneshot; + +use crate::{ + sync::ReconnectProtocolReason, + FunctionResult, +}; + +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug)] +pub struct RequestId(u32); +impl RequestId { + pub fn new(id: u32) -> Self { + RequestId(id) + } +} + +#[derive(Copy, Clone, PartialEq, PartialOrd, Ord, Eq)] +pub enum RequestType { + Mutation, + Action, +} + +#[derive(Clone, PartialEq, PartialOrd, Ord, Eq)] +pub enum RequestStatus { + Requested, + Completed, +} + +#[derive(Clone, PartialEq, Eq)] +pub struct Request { + pub id: RequestId, + pub typ: RequestType, + pub status: RequestStatus, + pub ts: Option, + pub value: Option, + pub message: ClientMessage, +} + +impl Request { + pub fn new(id: RequestId, typ: RequestType, message: ClientMessage) -> Self { + Request { + id, + typ, + status: RequestStatus::Requested, + ts: None, + value: None, + message, + } + } + + pub fn update_value(&mut self, value: FunctionResult) { + self.value = Some(value); + } + + pub fn update_timestamp(&mut self, ts: Option) { + self.ts = ts; + } +} + +pub struct RequestManager { + ongoing_requests: BTreeMap)>, +} + +impl RequestManager { + pub fn new() -> Self { + RequestManager { + ongoing_requests: BTreeMap::new(), + } + } + + pub fn update_request( + &mut self, + request_id: &RequestId, + request_type: RequestType, + value: FunctionResult, + ts: Option, + ) -> Result<(), ReconnectProtocolReason> { + let Some((request, _)) = self.ongoing_requests.get_mut(request_id) else { + return Err("Invalid request id from server".to_string()); + }; + if request.typ != request_type { + return Err("Mismatched request type from server".to_string()); + }; + let errored = matches!(value, FunctionResult::ErrorMessage(_)); + request.update_value(value); + request.update_timestamp(ts); + request.status = RequestStatus::Completed; + + // Actions and errored mutations are ok to complete immediately + if request_type == RequestType::Action || errored { + self._remove_and_notify_completed(request_id); + } + Ok(()) + } + + pub fn remove_and_notify_completed(&mut self, ts: Timestamp) -> BTreeSet { + let mut completed_requests = BTreeSet::new(); + for (id, (request, _)) in self.ongoing_requests.iter() { + let mut is_completed = false; + if request.status == RequestStatus::Completed { + is_completed = true; + } + if let Some(request_ts) = request.ts { + if request_ts <= ts { + is_completed = true; + } + } + if is_completed { + completed_requests.insert(*id); + } + } + for id in completed_requests.iter() { + self._remove_and_notify_completed(id); + } + completed_requests + } + + fn _remove_and_notify_completed(&mut self, request_id: &RequestId) { + let (request, sender) = self + .ongoing_requests + .remove(request_id) + .expect("INTERNAL BUG: request_id must be present"); + if let Err(value) = sender.send( + request + .value + .expect("INTERNAL BUG: Value missing on completed request"), + ) { + tracing::info!( + "Request {request_id:?} completed with result {value:?}, but result receiver was \ + dropped" + ); + } + } + + pub fn track_request( + &mut self, + message: &ClientMessage, + request_id: RequestId, + request_type: RequestType, + ) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + let request = Request::new(request_id, request_type, message.clone()); + self.ongoing_requests.insert(request_id, (request, tx)); + rx + } + + pub fn restart(&self) -> VecDeque { + // Sort ongoing requests by timestamp + let mut ordered_requests = Vec::from_iter(self.ongoing_requests.values()); + ordered_requests.sort_by_key(|(req, _)| Reverse(req.ts)); + + let mut messages = VecDeque::new(); + for (request, _) in ordered_requests { + messages.push_back(request.message.clone()); + } + messages + } +} diff --git a/third_party/convex_rs/src/client/mod.rs b/third_party/convex_rs/src/client/mod.rs new file mode 100644 index 00000000..012554e1 --- /dev/null +++ b/third_party/convex_rs/src/client/mod.rs @@ -0,0 +1,1153 @@ +use std::{ + collections::BTreeMap, + convert::Infallible, + future::Future, + pin::Pin, + sync::Arc, +}; + +use convex_sync_types::{ + AuthenticationToken, + UdfPath, + UserIdentityAttributes, +}; +#[cfg(doc)] +use futures::Stream; +use futures::StreamExt; +use tokio::{ + sync::{ + broadcast, + mpsc, + oneshot, + }, + task::JoinHandle, +}; +use tokio_stream::wrappers::BroadcastStream; +use url::Url; + +pub use crate::base_client::AuthTokenFetcher; +#[cfg(doc)] +use crate::SubscriberId; +use crate::{ + base_client::{ + BaseConvexClient, + QueryResults, + }, + client::{ + subscription::{ + QuerySetSubscription, + QuerySubscription, + }, + worker::{ + worker, + ActionRequest, + ClientRequest, + MutationRequest, + SubscribeRequest, + }, + }, + sync::{ + web_socket_manager::WebSocketManager, + SyncProtocol, + WebSocketState, + }, + value::Value, + FunctionResult, +}; + +pub mod subscription; +mod worker; + +const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); + +/// An asynchronous client to interact with a specific project to perform +/// mutations and manage query subscriptions using [`tokio`]. +/// +/// The Convex client requires a deployment url, +/// which can be found in the [dashboard](https://dashboard.convex.dev/) settings tab. +/// +/// ```no_run +/// use convex::ConvexClient; +/// use futures::StreamExt; +/// +/// #[tokio::main] +/// async fn main() -> anyhow::Result<()> { +/// let mut client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; +/// let mut sub = client.subscribe("listMessages", maplit::btreemap!{}).await?; +/// while let Some(result) = sub.next().await { +/// println!("{result:?}"); +/// } +/// Ok(()) +/// } +/// ``` +/// +/// The [`ConvexClient`] internally holds a connection and a [`tokio`] +/// background task to manage it. It is advised that you create one and +/// **reuse** it. You can safely clone with [`ConvexClient::clone()`] to share +/// the connection and outstanding subscriptions. +/// +/// ## Examples +/// For example code, please refer to the examples directory. +pub struct ConvexClient { + listen_handle: Option>>, + request_sender: mpsc::UnboundedSender, + watch_receiver: broadcast::Receiver, +} + +/// Clone the [`ConvexClient`], sharing the connection and outstanding +/// subscriptions. +impl Clone for ConvexClient { + fn clone(&self) -> Self { + Self { + listen_handle: self.listen_handle.clone(), + request_sender: self.request_sender.clone(), + watch_receiver: self.watch_receiver.resubscribe(), + } + } +} + +/// Drop the [`ConvexClient`]. When the final reference to the [`ConvexClient`] +/// is dropped, the connection is cleaned up. +impl Drop for ConvexClient { + fn drop(&mut self) { + if let Ok(j_handle) = Arc::try_unwrap( + self.listen_handle + .take() + .expect("INTERNAL BUG: listen handle should never be none"), + ) { + j_handle.abort() + } + } +} + +impl ConvexClient { + /// Constructs a new client for communicating with `deployment_url`. + /// + /// ```no_run + /// # use convex::ConvexClient; + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// let client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn new(deployment_url: &str) -> anyhow::Result { + ConvexClient::new_from_builder(ConvexClientBuilder::new(deployment_url)).await + } + + #[doc(hidden)] + pub async fn new_from_builder(builder: ConvexClientBuilder) -> anyhow::Result { + let client_id = builder + .client_id + .unwrap_or_else(|| format!("rust-{}", VERSION.unwrap_or("unknown"))); + let ws_url = deployment_to_ws_url(builder.deployment_url.as_str().try_into()?)?; + + // Channels for the `listen` background thread + let (response_sender, response_receiver) = mpsc::channel(1); + let (request_sender, request_receiver) = mpsc::unbounded_channel(); + + // Listener for when each transaction completes + let (watch_sender, watch_receiver) = broadcast::channel(1); + + let base_client = BaseConvexClient::new(); + + let protocol = WebSocketManager::open( + ws_url, + response_sender, + builder.on_state_change, + client_id.as_str(), + ) + .await?; + + let listen_handle = tokio::spawn(worker( + response_receiver, + request_receiver, + watch_sender, + base_client, + protocol, + )); + let client = ConvexClient { + listen_handle: Some(Arc::new(listen_handle)), + request_sender, + watch_receiver, + }; + Ok(client) + } + + /// Subscribe to the results of query `name` called with `args`. + /// + /// Returns a [`QuerySubscription`] which implements [`Stream`]< + /// [`FunctionResult`]>. A new value appears on the stream each + /// time the query function produces a new result. + /// + /// The subscription is automatically unsubscribed when it is dropped. + /// + /// ```no_run + /// # use convex::ConvexClient; + /// # use futures::StreamExt; + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// let mut client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; + /// let mut sub = client.subscribe("listMessages", maplit::btreemap!{}).await?; + /// while let Some(result) = sub.next().await { + /// println!("{result:?}"); + /// } + /// # Ok(()) + /// # } + pub async fn subscribe( + &mut self, + name: &str, + args: BTreeMap, + ) -> anyhow::Result { + let (tx, rx) = oneshot::channel(); + + let udf_path = name.parse()?; + let request = SubscribeRequest { udf_path, args }; + + self.request_sender.send(ClientRequest::Subscribe( + request, + tx, + self.request_sender.clone(), + ))?; + + let res = rx.await?; + Ok(res) + } + + /// Make a oneshot request to a query `name` with `args`. + /// + /// Returns a [`FunctionResult`] representing the result of the query. + /// + /// This method is syntactic sugar for waiting for a single result on + /// a subscription. + /// It is equivalent to `client.subscribe(name, + /// args).await?.next().unwrap()` + /// + /// ```no_run + /// # use convex::ConvexClient; + /// # use futures::StreamExt; + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// let mut client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; + /// let result = client.query("listMessages", maplit::btreemap!{}).await?; + /// println!("{result:?}"); + /// # Ok(()) + /// # } + pub async fn query( + &mut self, + name: &str, + args: BTreeMap, + ) -> anyhow::Result { + Ok(self + .subscribe(name, args) + .await? + .next() + .await + .expect("INTERNAL BUG: Convex Client dropped prematurely.")) + } + + /// Perform a mutation `name` with `args` and return a future + /// containing the return value of the mutation once it completes. + /// + /// ```no_run + /// # use convex::ConvexClient; + /// # use futures::StreamExt; + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// let mut client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; + /// let result = client.mutation("sendMessage", maplit::btreemap!{ + /// "body".into() => "Let it be.".into(), + /// "author".into() => "The Beatles".into(), + /// }).await?; + /// println!("{result:?}"); + /// # Ok(()) + /// # } + pub async fn mutation( + &mut self, + name: &str, + args: BTreeMap, + ) -> anyhow::Result { + let (tx, rx) = oneshot::channel(); + + let udf_path: UdfPath = name.parse()?; + let request = MutationRequest { udf_path, args }; + + self.request_sender + .send(ClientRequest::Mutation(request, tx))?; + + let res = rx.await?; + Ok(res.await?) + } + + /// Perform an action `name` with `args` and return a future + /// containing the return value of the action once it completes. + /// + /// ```no_run + /// # use convex::ConvexClient; + /// # use futures::StreamExt; + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// let mut client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; + /// let result = client.action("sendGif", maplit::btreemap!{ + /// "body".into() => "Tatooine Sunrise.".into(), + /// "author".into() => "Luke Skywalker".into(), + /// }).await?; + /// println!("{result:?}"); + /// # Ok(()) + /// # } + pub async fn action( + &mut self, + name: &str, + args: BTreeMap, + ) -> anyhow::Result { + let (tx, rx) = oneshot::channel(); + + let udf_path: UdfPath = name.parse()?; + let request = ActionRequest { udf_path, args }; + + self.request_sender + .send(ClientRequest::Action(request, tx))?; + + let res = rx.await?; + Ok(res.await?) + } + + /// Get a consistent view of the results of multiple queries (query set). + /// + /// Returns a [`QuerySetSubscription`] which + /// implements [`Stream`]<[`QueryResults`]>. + /// Each item in the stream contains a consistent view + /// of the results of all the queries in the query set. + /// + /// Queries can be added to the query set via [`ConvexClient::subscribe`]. + /// Queries can be removed from the query set via dropping the + /// [`QuerySubscription`] token returned by [`ConvexClient::subscribe`]. + /// + /// + /// [`QueryResults`] is a copy-on-write mapping from [`SubscriberId`] to + /// its latest result [`Value`]. + /// + /// ```no_run + /// # use convex::ConvexClient; + /// # use futures::StreamExt; + /// # #[tokio::main] + /// # async fn main() -> anyhow::Result<()> { + /// let mut client = ConvexClient::new("https://cool-music-123.convex.cloud").await?; + /// let mut watch = client.watch_all(); + /// let sub1 = client.subscribe("listMessages", maplit::btreemap!{ + /// "channel".into() => 1.into(), + /// }).await?; + /// let sub2 = client.subscribe("listMessages", maplit::btreemap!{ + /// "channel".into() => 1.into(), + /// }).await?; + /// # Ok(()) + /// # } + pub fn watch_all(&self) -> QuerySetSubscription { + QuerySetSubscription::new(BroadcastStream::new(self.watch_receiver.resubscribe())) + } + + /// Set auth for use when calling Convex functions. + /// + /// Set it with a token that you get from your auth provider via their login + /// flow. If `None` is passed as the token, then auth is unset (logging + /// out). + /// + /// Internally this wraps the static token in a trivial callback and the + /// same token is re-sent on websocket reconnect. + /// + ///

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