Compare commits

..

2 commits

Author SHA1 Message Date
tk
47cfb949ac improve quote list performance 2025-08-17 02:28:19 +02:00
tk
bf28ff429a introduce bottom modal for quote entry 2025-08-17 01:44:23 +02:00
13 changed files with 345 additions and 130 deletions

View file

@ -47,12 +47,6 @@ class EndpointQuote extends _i1.EndpointRef {
@override @override
String get name => 'quote'; String get name => 'quote';
_i2.Future<String> dbPing() => caller.callServerEndpoint<String>(
'quote',
'dbPing',
{},
);
_i2.Future<_i4.Quote> createQuote(_i5.CreateQuoteRequest req) => _i2.Future<_i4.Quote> createQuote(_i5.CreateQuoteRequest req) =>
caller.callServerEndpoint<_i4.Quote>( caller.callServerEndpoint<_i4.Quote>(
'quote', 'quote',
@ -73,6 +67,14 @@ class EndpointQuote extends _i1.EndpointRef {
'getAllQuotes', 'getAllQuotes',
{}, {},
); );
_i2.Stream<_i4.Quote> streamAllQuotes({required int limit}) =>
caller.callStreamingServerEndpoint<_i2.Stream<_i4.Quote>, _i4.Quote>(
'quote',
'streamAllQuotes',
{'limit': limit},
{},
);
} }
/// {@category Endpoint} /// {@category Endpoint}

View file

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View file

@ -29,7 +29,7 @@ class FunmapMgr {
serverUrl = serverUrl =
serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv; serverUrlFromEnv.isEmpty ? 'http://$localhost:8080/' : serverUrlFromEnv;
client = Client(serverUrl, connectionTimeout: const Duration(seconds: 5)) client = Client(serverUrl, connectionTimeout: const Duration(seconds: 2))
..connectivityMonitor = FlutterConnectivityMonitor(); ..connectivityMonitor = FlutterConnectivityMonitor();
client.openStreamingConnection(); client.openStreamingConnection();

View file

@ -0,0 +1,17 @@
// Clanker code, I caved
String timeAgo(DateTime? dt) {
final d =
(dt ?? DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)).toLocal();
final now = DateTime.now();
final diff = now.isBefore(d) ? Duration.zero : now.difference(d);
return switch (diff.inSeconds) {
< 60 => 'just now',
< 3600 => '${diff.inMinutes}m ago',
< 86400 => '${diff.inHours}h ago',
< 604800 => '${diff.inDays}d ago',
_ => '${d.year.toString().padLeft(4, '0')}-'
'${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}',
};
}

View file

@ -20,7 +20,7 @@ class HomeScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
IntroTextWidget(), IntroTextWidget(),
SizedBox(height: 200, child: ShowLatestNewsWidget()), SizedBox(height: 200, child: LatestQuotesScreen()),
SizedBox( SizedBox(
height: 30, height: 30,
), ),

View file

@ -17,7 +17,7 @@ class NewsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
HeadingText(text: "What's being said"), HeadingText(text: "What's being said"),
ShowLatestNewsWidget(), LatestQuotesScreen(),
SizedBox( SizedBox(
height: 30, height: 30,
), ),

View file

@ -3,97 +3,91 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:wien_talks_client/wien_talks_client.dart'; import 'package:wien_talks_client/wien_talks_client.dart';
import 'package:wien_talks_flutter/helper/funmap_mgr.dart'; import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
import 'package:wien_talks_flutter/helper/time_util.dart';
import 'package:wien_talks_flutter/widgets/quote_card.dart'; import 'package:wien_talks_flutter/widgets/quote_card.dart';
class ShowLatestNewsWidget extends StatefulWidget { class LatestQuotesScreen extends StatefulWidget {
const ShowLatestNewsWidget({super.key}); const LatestQuotesScreen({super.key});
@override @override
State<ShowLatestNewsWidget> createState() => _ShowLatestNewsWidgetState(); State<LatestQuotesScreen> createState() => _LatestQuotesScreenState();
} }
class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> { class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
List<Quote>? _quotes; final List<Quote> _quotes = [];
StreamSubscription<Quote>? _sub;
Object? _error; Object? _error;
bool _loading = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_fetch(); _connectStream();
} }
Future<void> _fetch() async { @override
setState(() { void dispose() {
_loading = true; _sub?.cancel();
_error = null; super.dispose();
});
try {
final list = await FunmapMgr().client.quote.getAllQuotes();
final quotes = list.whereType<Quote>().toList(growable: false);
quotes.sort((a, b) => (b.createdAt).compareTo(a.createdAt));
setState(() {
_quotes = quotes;
});
} catch (e) {
setState(() => _error = e);
} finally {
if (mounted) setState(() => _loading = false);
}
} }
Future<void> _refresh() => _fetch(); void _connectStream() {
_sub?.cancel();
_sub = FunmapMgr().client.quote.streamAllQuotes(limit: 50).listen(
(q) => setState(() => _upsert(q)),
onError: (e) => setState(() => _error = e),
onDone: () => Future.delayed(const Duration(seconds: 2), () {
if (mounted) _connectStream();
}),
cancelOnError: false,
);
}
void _upsert(Quote q) {
final i = _quotes.indexWhere((x) => x.id == q.id);
if (i >= 0)
_quotes[i] = q;
else
_quotes.add(q);
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
void _sortDesc() {
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
Future<void> _vote(Quote quote, bool up) async { Future<void> _vote(Quote quote, bool up) async {
if (_quotes == null) return; final idx = _quotes.indexWhere((q) => q.id == quote.id);
final idx = _quotes!.indexWhere((q) => q.id == quote.id);
if (idx < 0) return; if (idx < 0) return;
final original = _quotes![idx]; final original = _quotes[idx];
final updated = original.copyWith( final updated = original.copyWith(
upvotes: up ? original.upvotes + 1 : original.upvotes, upvotes: up ? original.upvotes + 1 : original.upvotes,
downvotes: up ? original.downvotes : original.downvotes + 1, downvotes: up ? original.downvotes : original.downvotes + 1,
); );
setState(() { setState(() {
final copy = List<Quote>.from(_quotes!); _quotes[idx] = updated;
copy[idx] = updated; _sortDesc();
_quotes = copy;
}); });
try { try {
await FunmapMgr().client.quote.updateQuote(updated); await FunmapMgr().client.quote.updateQuote(updated);
} catch (e) { } catch (e) {
setState(() {
final copy = List<Quote>.from(_quotes!);
copy[idx] = original;
_quotes = copy;
});
if (!mounted) return; if (!mounted) return;
setState(() => _quotes[idx] = original);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Vote failed: $e')), SnackBar(content: Text('Vote failed: $e')),
); );
} }
} }
String _timeAgo(DateTime? dt) {
final d = (dt ?? DateTime.fromMillisecondsSinceEpoch(0)).toLocal();
final diff = DateTime.now().difference(d);
if (diff.inSeconds < 60) return 'just now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}h ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
final m = d.month.toString().padLeft(2, '0');
final day = d.day.toString().padLeft(2, '0');
return '${d.year}-$m-$day';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_loading) { if (_quotes.isEmpty && _error == null) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (_error != null) { if (_error != null && _quotes.isEmpty) {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -101,9 +95,8 @@ class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
), ),
); );
} }
final quotes = _quotes ?? const <Quote>[]; if (_quotes.isEmpty) {
if (quotes.isEmpty) { return const Center(child: Text('Nix da. Sag halt was'));
return const Center(child: Text('No quotes yet.'));
} }
return LayoutBuilder( return LayoutBuilder(
@ -116,14 +109,14 @@ class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
physics: unboundedHeight physics: unboundedHeight
? const NeverScrollableScrollPhysics() ? const NeverScrollableScrollPhysics()
: const AlwaysScrollableScrollPhysics(), : const AlwaysScrollableScrollPhysics(),
itemCount: quotes.length, itemCount: _quotes.length,
separatorBuilder: (_, __) => const SizedBox(height: 6), separatorBuilder: (_, __) => const SizedBox(height: 6),
itemBuilder: (context, i) { itemBuilder: (context, i) {
final q = quotes[i]; final q = _quotes[i];
final author = (q.authorName ?? '').trim(); final author = (q.authorName ?? '').trim();
final meta = [ final meta = [
if (author.isNotEmpty) author, if (author.isNotEmpty) author,
_timeAgo(q.createdAt), timeAgo(q.createdAt),
].join(' · '); ].join(' · ');
return QuoteCard( return QuoteCard(
@ -135,8 +128,9 @@ class _ShowLatestNewsWidgetState extends State<ShowLatestNewsWidget> {
}, },
); );
if (unboundedHeight) return list; return unboundedHeight
return RefreshIndicator(onRefresh: _refresh, child: list); ? list
: RefreshIndicator(onRefresh: () async {}, child: list);
}, },
); );
} }

View file

@ -1,17 +1,199 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:location/location.dart';
import 'package:wien_talks_client/wien_talks_client.dart'; import 'package:wien_talks_client/wien_talks_client.dart';
import 'package:wien_talks_flutter/helper/funmap_mgr.dart'; import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
class QuoteDraft {
final String text;
final String? author;
const QuoteDraft({required this.text, this.author});
}
typedef QuoteSubmit = FutureOr<void> Function(QuoteDraft draft);
/// If Simon reads this I'm sorry
class AddQuoteFab extends StatelessWidget { class AddQuoteFab extends StatelessWidget {
const AddQuoteFab({ const AddQuoteFab({
super.key, super.key,
this.tooltip = 'Add quote',
this.icon = Icons.format_quote_rounded,
this.mini = false,
}); });
final String tooltip;
final IconData icon;
final bool mini;
Future<void> onSubmit(QuoteDraft draft) async {
final fix = await _getHackyLocation();
if (fix == null || fix.latitude == null) {
return;
}
await FunmapMgr().client.quote.createQuote(
CreateQuoteRequest(
text: draft.text,
authorName: draft.author,
lat: fix.latitude!,
lng: fix.longitude!,
),
);
}
Future<LocationData?> _getHackyLocation() async {
final loc = Location();
if (!await loc.serviceEnabled()) {
if (!await loc.requestService()) return null;
}
var perm = await loc.hasPermission();
if (perm == PermissionStatus.denied) {
perm = await loc.requestPermission();
if (perm != PermissionStatus.granted) return null;
}
return await loc.getLocation();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FloatingActionButton(onPressed: () { return FloatingActionButton(
FunmapMgr().client.quote.createQuote( mini: mini,
CreateQuoteRequest(text: 'Quote Text', lat: 22, lng: 140)); tooltip: tooltip,
}); onPressed: () async {
final draft = await showQuoteEditor(context);
if (draft != null) {
await onSubmit(draft);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Quote saved')),
);
}
}
},
child: Icon(icon),
);
} }
} }
Future<QuoteDraft?> showQuoteEditor(
BuildContext context, {
String? initialText,
String? initialAuthor,
}) async {
final textCtrl = TextEditingController(text: initialText ?? '');
final authorCtrl = TextEditingController(text: initialAuthor ?? '');
const maxChars = 500;
return showModalBottomSheet<QuoteDraft>(
context: context,
isScrollControlled: true,
useSafeArea: true,
showDragHandle: true,
builder: (ctx) {
bool canSave() {
final t = textCtrl.text.trim();
return t.isNotEmpty && t.length <= maxChars;
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(ctx).viewInsets.bottom,
),
child: StatefulBuilder(
builder: (ctx, setSheetState) {
void onChanged(_) => setSheetState(() {});
final remaining = maxChars - textCtrl.text.characters.length;
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Text('New quote', style: Theme.of(ctx).textTheme.titleMedium),
const SizedBox(height: 8),
TextField(
controller: textCtrl,
onChanged: onChanged,
autofocus: true,
maxLines: null,
minLines: 3,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
labelText: 'Vienna`s finest',
hintText: 'How is Vienna surprising you today',
helperText: 'Max $maxChars characters',
counterText:
'${textCtrl.text.characters.length}/$maxChars',
border: const OutlineInputBorder(),
),
maxLength: maxChars,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
),
const SizedBox(height: 12),
TextField(
controller: authorCtrl,
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
labelText: 'Author (optional)',
hintText: 'e.g., Schmausi Wamperl',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(ctx).pop(null),
child: const Text('Cancel'),
),
),
const SizedBox(width: 12),
Expanded(
child: FilledButton.icon(
onPressed: canSave()
? () => Navigator.of(ctx).pop(
QuoteDraft(
text: textCtrl.text.trim(),
author: authorCtrl.text.trim().isEmpty
? null
: authorCtrl.text.trim(),
),
)
: null,
icon: const Icon(Icons.check),
label: const Text('Save'),
),
),
],
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: Text(
remaining >= 0
? '$remaining characters left'
: '${-remaining} over limit',
style: Theme.of(ctx).textTheme.labelSmall?.copyWith(
color: Theme.of(ctx)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
),
),
],
),
);
},
),
);
},
);
}

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:loader_overlay/loader_overlay.dart'; import 'package:loader_overlay/loader_overlay.dart';
import 'package:wien_talks_flutter/widgets/add_quote_fab.dart';
class ScreenWidget extends StatelessWidget { class ScreenWidget extends StatelessWidget {
final Widget child; final Widget child;
@ -15,7 +16,7 @@ class ScreenWidget extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: const Text('FunMap'), title: const Text('FunMap'),
), ),
// floatingActionButton: AddQuoteFab(), floatingActionButton: AddQuoteFab(),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -28,7 +29,8 @@ class ScreenWidget extends StatelessWidget {
); );
case 1: case 1:
return Center( return Center(
child: SpinKitCubeGrid(size: 50, color: Theme.of(context).primaryColor), child: SpinKitCubeGrid(
size: 50, color: Theme.of(context).primaryColor),
); );
case 2: case 2:
return Center( return Center(
@ -36,15 +38,18 @@ class ScreenWidget extends StatelessWidget {
); );
case 3: case 3:
return Center( return Center(
child: SpinKitHourGlass(color: Theme.of(context).primaryColor), child:
SpinKitHourGlass(color: Theme.of(context).primaryColor),
); );
case 4: case 4:
return Center( return Center(
child: SpinKitFadingCircle(color: Theme.of(context).primaryColor), child: SpinKitFadingCircle(
color: Theme.of(context).primaryColor),
); );
default: default:
return Center( return Center(
child: SpinKitPulsingGrid(color: Theme.of(context).primaryColor), child: SpinKitPulsingGrid(
color: Theme.of(context).primaryColor),
); );
} }
}, },

View file

@ -77,15 +77,6 @@ class Endpoints extends _i1.EndpointDispatch {
name: 'quote', name: 'quote',
endpoint: endpoints['quote']!, endpoint: endpoints['quote']!,
methodConnectors: { methodConnectors: {
'dbPing': _i1.MethodConnector(
name: 'dbPing',
params: {},
call: (
_i1.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['quote'] as _i3.QuoteEndpoint).dbPing(session),
),
'createQuote': _i1.MethodConnector( 'createQuote': _i1.MethodConnector(
name: 'createQuote', name: 'createQuote',
params: { params: {
@ -131,6 +122,27 @@ class Endpoints extends _i1.EndpointDispatch {
) async => ) async =>
(endpoints['quote'] as _i3.QuoteEndpoint).getAllQuotes(session), (endpoints['quote'] as _i3.QuoteEndpoint).getAllQuotes(session),
), ),
'streamAllQuotes': _i1.MethodStreamConnector(
name: 'streamAllQuotes',
params: {
'limit': _i1.ParameterDescription(
name: 'limit',
type: _i1.getType<int>(),
nullable: false,
)
},
streamParams: {},
returnType: _i1.MethodStreamReturnType.streamType,
call: (
_i1.Session session,
Map<String, dynamic> params,
Map<String, Stream> streamParams,
) =>
(endpoints['quote'] as _i3.QuoteEndpoint).streamAllQuotes(
session,
limit: params['limit'],
),
),
}, },
); );
connectors['votes'] = _i1.EndpointConnector( connectors['votes'] = _i1.EndpointConnector(

View file

@ -2,10 +2,10 @@ health:
- ping: - ping:
- all: - all:
quote: quote:
- dbPing:
- createQuote: - createQuote:
- updateQuote: - updateQuote:
- getAllQuotes: - getAllQuotes:
- streamAllQuotes:
votes: votes:
- getAllVotes: - getAllVotes:
- createVote: - createVote:

View file

@ -8,13 +8,6 @@ import 'package:wien_talks_server/src/quotes/quote_util.dart';
class QuoteEndpoint extends Endpoint { class QuoteEndpoint extends Endpoint {
static const _channelQuoteUpdates = 'quote-updates'; static const _channelQuoteUpdates = 'quote-updates';
Future<String> dbPing(Session session) async {
await session.db.unsafeQuery('SELECT 1;'); // connectivity
await session.db
.unsafeQuery('SELECT 1 FROM public.quote LIMIT 1;'); // table visible
return 'ok';
}
Future<Quote> createQuote(Session session, CreateQuoteRequest req) async { Future<Quote> createQuote(Session session, CreateQuoteRequest req) async {
final authInfo = await session.authenticated; final authInfo = await session.authenticated;
final userId = authInfo?.userId; final userId = authInfo?.userId;
@ -50,7 +43,6 @@ class QuoteEndpoint extends Endpoint {
final quoteList = await Quote.db.find( final quoteList = await Quote.db.find(
session, session,
// where: (t) => t.visibility.equals(0),
orderBy: (t) => t.createdAt, orderBy: (t) => t.createdAt,
orderDescending: true, orderDescending: true,
); );
@ -62,13 +54,12 @@ class QuoteEndpoint extends Endpoint {
return quoteList; return quoteList;
} }
Future<Stream<Quote>> streamAllQuotes(StreamingSession session, Stream<Quote> streamAllQuotes(Session session, {int limit = 50}) {
{int limit = 200}) async {
if (limit <= 0 || limit > 500) limit = 200;
final controller = StreamController<Quote>(); final controller = StreamController<Quote>();
final live = session.messages.createStream<Quote>(_channelQuoteUpdates); final live = session.messages.createStream<Quote>(_channelQuoteUpdates);
final liveSub = live.listen(
StreamSubscription<Quote>? sub;
sub = live.listen(
(q) { (q) {
if (q.visibility == 0) controller.add(q); if (q.visibility == 0) controller.add(q);
}, },
@ -76,10 +67,9 @@ class QuoteEndpoint extends Endpoint {
onDone: () { onDone: () {
if (!controller.isClosed) controller.close(); if (!controller.isClosed) controller.close();
}, },
cancelOnError: false,
); );
() async* { () async {
try { try {
final snapshot = await Quote.db.find( final snapshot = await Quote.db.find(
session, session,
@ -89,18 +79,22 @@ class QuoteEndpoint extends Endpoint {
limit: limit, limit: limit,
); );
var i = 0;
for (final q in snapshot.reversed) { for (final q in snapshot.reversed) {
controller.add(q); controller.add(q);
if ((++i % 25) == 0) {
await Future<void>.delayed(Duration.zero);
}
} }
} catch (e, st) { } catch (e, st) {
controller.addError(e, st); controller.addError(e, st);
} }
}(); }();
await session.close().then((_) async { controller.onCancel = () async {
await liveSub.cancel(); await sub?.cancel();
await controller.close(); await controller.close();
}); };
return controller.stream; return controller.stream;
} }

View file

@ -210,32 +210,6 @@ class _QuoteEndpoint {
final _i2.SerializationManager _serializationManager; final _i2.SerializationManager _serializationManager;
_i3.Future<String> dbPing(_i1.TestSessionBuilder sessionBuilder) async {
return _i1.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild(
endpoint: 'quote',
method: 'dbPing',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'quote',
methodName: 'dbPing',
parameters: _i1.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue = await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
) as _i3.Future<String>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_i3.Future<_i5.Quote> createQuote( _i3.Future<_i5.Quote> createQuote(
_i1.TestSessionBuilder sessionBuilder, _i1.TestSessionBuilder sessionBuilder,
_i6.CreateQuoteRequest req, _i6.CreateQuoteRequest req,
@ -320,6 +294,38 @@ class _QuoteEndpoint {
} }
}); });
} }
_i3.Stream<_i5.Quote> streamAllQuotes(
_i1.TestSessionBuilder sessionBuilder, {
required int limit,
}) {
var _localTestStreamManager = _i1.TestStreamManager<_i5.Quote>();
_i1.callStreamFunctionAndHandleExceptions(
() async {
var _localUniqueSession =
(sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild(
endpoint: 'quote',
method: 'streamAllQuotes',
);
var _localCallContext =
await _endpointDispatch.getMethodStreamCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'quote',
methodName: 'streamAllQuotes',
arguments: {'limit': limit},
requestedInputStreams: [],
serializationManager: _serializationManager,
);
await _localTestStreamManager.callStreamMethod(
_localCallContext,
_localUniqueSession,
{},
);
},
_localTestStreamManager.outputStreamController,
);
return _localTestStreamManager.outputStreamController.stream;
}
} }
class _VotesEndpoint { class _VotesEndpoint {