Skip to content

Commit ae121ea

Browse files
fix(realtime): add presence flag (#1233)
* fix: add presence flag * fix test * fix format * style: dart format * add resubscribe * fix test * style: dart format --------- Co-authored-by: Guilherme Souza <[email protected]>
1 parent cd9757b commit ae121ea

File tree

4 files changed

+271
-7
lines changed

4 files changed

+271
-7
lines changed

packages/realtime_client/lib/src/realtime_channel.dart

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,27 @@ class RealtimeChannel {
104104
}
105105
}
106106

107+
bool _shouldEnablePresence() {
108+
return (_bindings['presence']?.isNotEmpty == true) ||
109+
(params['config']['presence']['enabled'] == true);
110+
}
111+
112+
void _handlePresenceUpdate() {
113+
if (joinedOnce && isJoined) {
114+
final currentPresenceEnabled = params['config']['presence']['enabled'];
115+
final shouldEnablePresence = _shouldEnablePresence();
116+
117+
if (!currentPresenceEnabled && shouldEnablePresence) {
118+
final config = Map<String, dynamic>.from(params['config']);
119+
config['presence'] = Map<String, dynamic>.from(config['presence']);
120+
config['presence']['enabled'] = true;
121+
params['config'] = config;
122+
updateJoinPayload({'config': config});
123+
rejoin();
124+
}
125+
}
126+
}
127+
107128
/// Subscribes to receive real-time changes
108129
///
109130
/// Pass a [callback] to react to different status changes.
@@ -130,10 +151,12 @@ class RealtimeChannel {
130151
if (callback != null) callback(RealtimeSubscribeStatus.closed, null);
131152
});
132153

154+
final presenceEnabled = _shouldEnablePresence();
155+
133156
final accessTokenPayload = <String, String>{};
134157
final config = <String, dynamic>{
135158
'broadcast': broadcast,
136-
'presence': presence,
159+
'presence': {...presence, 'enabled': presenceEnabled},
137160
'postgres_changes':
138161
_bindings['postgres_changes']?.map((r) => r.filter).toList() ?? [],
139162
'private': isPrivate == true,
@@ -348,7 +371,7 @@ class RealtimeChannel {
348371
RealtimeChannel onPresenceSync(
349372
void Function(RealtimePresenceSyncPayload payload) callback,
350373
) {
351-
return onEvents(
374+
final result = onEvents(
352375
'presence',
353376
ChannelFilter(
354377
event: PresenceEvent.sync.name,
@@ -358,6 +381,8 @@ class RealtimeChannel {
358381
Map<String, dynamic>.from(payload)));
359382
},
360383
);
384+
_handlePresenceUpdate();
385+
return result;
361386
}
362387

363388
/// Sets up a listener for realtime presence join event.
@@ -374,7 +399,7 @@ class RealtimeChannel {
374399
RealtimeChannel onPresenceJoin(
375400
void Function(RealtimePresenceJoinPayload payload) callback,
376401
) {
377-
return onEvents(
402+
final result = onEvents(
378403
'presence',
379404
ChannelFilter(
380405
event: PresenceEvent.join.name,
@@ -384,6 +409,8 @@ class RealtimeChannel {
384409
Map<String, dynamic>.from(payload)));
385410
},
386411
);
412+
_handlePresenceUpdate();
413+
return result;
387414
}
388415

389416
/// Sets up a listener for realtime presence leave event.
@@ -400,7 +427,7 @@ class RealtimeChannel {
400427
RealtimeChannel onPresenceLeave(
401428
void Function(RealtimePresenceLeavePayload payload) callback,
402429
) {
403-
return onEvents(
430+
final result = onEvents(
404431
'presence',
405432
ChannelFilter(
406433
event: PresenceEvent.leave.name,
@@ -410,6 +437,8 @@ class RealtimeChannel {
410437
Map<String, dynamic>.from(payload)));
411438
},
412439
);
440+
_handlePresenceUpdate();
441+
return result;
413442
}
414443

415444
/// Sets up a listener for realtime system events for debugging purposes.

packages/realtime_client/lib/src/types.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,17 @@ class RealtimeChannelConfig {
148148
/// [key] option is used to track presence payload across clients
149149
final String key;
150150

151+
/// Enables presence even without presence bindings
152+
final bool enabled;
153+
151154
/// Defines if the channel is private or not and if RLS policies will be used to check data
152155
final bool private;
153156

154157
const RealtimeChannelConfig({
155158
this.ack = false,
156159
this.self = false,
157160
this.key = '',
161+
this.enabled = false,
158162
this.private = false,
159163
});
160164

@@ -167,6 +171,7 @@ class RealtimeChannelConfig {
167171
},
168172
'presence': {
169173
'key': key,
174+
'enabled': enabled,
170175
},
171176
'private': private,
172177
}

packages/realtime_client/test/channel_test.dart

Lines changed: 232 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ void main() {
3434
expect(channel.params, {
3535
'config': {
3636
'broadcast': {'ack': false, 'self': false},
37-
'presence': {'key': ''},
37+
'presence': {'key': '', 'enabled': false},
3838
'private': false,
3939
}
4040
});
@@ -54,7 +54,7 @@ void main() {
5454
expect(joinPush.payload, {
5555
'config': {
5656
'broadcast': {'ack': false, 'self': false},
57-
'presence': {'key': ''},
57+
'presence': {'key': '', 'enabled': false},
5858
'private': true,
5959
},
6060
});
@@ -386,4 +386,234 @@ void main() {
386386
expect(leaveCalled, isTrue);
387387
});
388388
});
389+
390+
group('presence enabled', () {
391+
setUp(() {
392+
socket = RealtimeClient('', timeout: const Duration(milliseconds: 1234));
393+
});
394+
395+
test(
396+
'should enable presence when config.presence.enabled is true even without bindings',
397+
() {
398+
channel = RealtimeChannel(
399+
'topic',
400+
socket,
401+
params: const RealtimeChannelConfig(enabled: true),
402+
);
403+
404+
channel.subscribe();
405+
406+
final joinPayload = channel.joinPush.payload;
407+
expect(joinPayload['config']['presence']['enabled'], isTrue);
408+
});
409+
410+
test('should enable presence when presence listeners exist', () {
411+
channel = RealtimeChannel(
412+
'topic',
413+
socket,
414+
params: const RealtimeChannelConfig(),
415+
);
416+
417+
channel.onPresenceSync((payload) {});
418+
channel.subscribe();
419+
420+
final joinPayload = channel.joinPush.payload;
421+
expect(joinPayload['config']['presence']['enabled'], isTrue);
422+
});
423+
424+
test(
425+
'should enable presence when both bindings exist and config.presence.enabled is true',
426+
() {
427+
channel = RealtimeChannel(
428+
'topic',
429+
socket,
430+
params: const RealtimeChannelConfig(enabled: true),
431+
);
432+
433+
channel.onPresenceSync((payload) {});
434+
channel.subscribe();
435+
436+
final joinPayload = channel.joinPush.payload;
437+
expect(joinPayload['config']['presence']['enabled'], isTrue);
438+
});
439+
440+
test(
441+
'should not enable presence when neither bindings exist nor config.presence.enabled is true',
442+
() {
443+
channel = RealtimeChannel(
444+
'topic',
445+
socket,
446+
params: const RealtimeChannelConfig(),
447+
);
448+
449+
channel.subscribe();
450+
451+
final joinPayload = channel.joinPush.payload;
452+
expect(joinPayload['config']['presence']['enabled'], isFalse);
453+
});
454+
455+
test('should enable presence when join listener exists', () {
456+
channel = RealtimeChannel(
457+
'topic',
458+
socket,
459+
params: const RealtimeChannelConfig(),
460+
);
461+
462+
channel.onPresenceJoin((payload) {});
463+
channel.subscribe();
464+
465+
final joinPayload = channel.joinPush.payload;
466+
expect(joinPayload['config']['presence']['enabled'], isTrue);
467+
});
468+
469+
test('should enable presence when leave listener exists', () {
470+
channel = RealtimeChannel(
471+
'topic',
472+
socket,
473+
params: const RealtimeChannelConfig(),
474+
);
475+
476+
channel.onPresenceLeave((payload) {});
477+
channel.subscribe();
478+
479+
final joinPayload = channel.joinPush.payload;
480+
expect(joinPayload['config']['presence']['enabled'], isTrue);
481+
});
482+
});
483+
484+
group('presence resubscription', () {
485+
setUp(() {
486+
socket = RealtimeClient('', timeout: const Duration(milliseconds: 1234));
487+
});
488+
489+
test(
490+
'should resubscribe when presence callback added to subscribed channel without initial presence',
491+
() {
492+
channel = RealtimeChannel(
493+
'topic',
494+
socket,
495+
params: const RealtimeChannelConfig(),
496+
);
497+
498+
channel.subscribe();
499+
channel.joinPush.trigger('ok', {});
500+
expect(channel.params['config']['presence']['enabled'], isFalse);
501+
502+
channel.onPresenceSync((payload) {});
503+
504+
expect(channel.params['config']['presence']['enabled'], isTrue);
505+
});
506+
507+
test(
508+
'should not resubscribe when presence callback added to channel with existing presence',
509+
() {
510+
channel = RealtimeChannel(
511+
'topic',
512+
socket,
513+
params: const RealtimeChannelConfig(enabled: true),
514+
);
515+
516+
channel.subscribe();
517+
channel.joinPush.trigger('ok', {});
518+
final initialPayload = Map.from(channel.params);
519+
520+
channel.onPresenceSync((payload) {});
521+
522+
expect(channel.params['config']['presence']['enabled'], isTrue);
523+
expect(channel.params, equals(initialPayload));
524+
});
525+
526+
test('should only resubscribe once when multiple presence callbacks added',
527+
() {
528+
channel = RealtimeChannel(
529+
'topic',
530+
socket,
531+
params: const RealtimeChannelConfig(),
532+
);
533+
534+
channel.subscribe();
535+
channel.joinPush.trigger('ok', {});
536+
expect(channel.params['config']['presence']['enabled'], isFalse);
537+
538+
channel.onPresenceSync((payload) {});
539+
expect(channel.params['config']['presence']['enabled'], isTrue);
540+
541+
final payloadAfterFirst = Map.from(channel.params);
542+
543+
channel.onPresenceJoin((payload) {});
544+
channel.onPresenceLeave((payload) {});
545+
546+
expect(channel.params, equals(payloadAfterFirst));
547+
});
548+
549+
test(
550+
'should not resubscribe when presence callback added to unsubscribed channel',
551+
() {
552+
channel = RealtimeChannel(
553+
'topic',
554+
socket,
555+
params: const RealtimeChannelConfig(),
556+
);
557+
558+
expect(channel.joinedOnce, isFalse);
559+
560+
channel.onPresenceSync((payload) {});
561+
562+
expect(channel.params['config']['presence']['enabled'], isFalse);
563+
});
564+
565+
test(
566+
'should receive presence events after resubscription triggered by adding callback',
567+
() {
568+
channel = RealtimeChannel(
569+
'topic',
570+
socket,
571+
params: const RealtimeChannelConfig(),
572+
);
573+
574+
channel.subscribe();
575+
channel.joinPush.trigger('ok', {});
576+
577+
bool syncCalled = false;
578+
channel.onPresenceSync((payload) {
579+
syncCalled = true;
580+
});
581+
582+
channel.trigger('presence', {'event': 'sync'}, '1');
583+
584+
expect(syncCalled, isTrue);
585+
});
586+
587+
test('should handle presence join callback resubscription', () {
588+
channel = RealtimeChannel(
589+
'topic',
590+
socket,
591+
params: const RealtimeChannelConfig(),
592+
);
593+
594+
channel.subscribe();
595+
channel.joinPush.trigger('ok', {});
596+
expect(channel.params['config']['presence']['enabled'], isFalse);
597+
598+
channel.onPresenceJoin((payload) {});
599+
600+
expect(channel.params['config']['presence']['enabled'], isTrue);
601+
});
602+
603+
test('should handle presence leave callback resubscription', () {
604+
channel = RealtimeChannel(
605+
'topic',
606+
socket,
607+
params: const RealtimeChannelConfig(),
608+
);
609+
610+
channel.subscribe();
611+
channel.joinPush.trigger('ok', {});
612+
expect(channel.params['config']['presence']['enabled'], isFalse);
613+
614+
channel.onPresenceLeave((payload) {});
615+
616+
expect(channel.params['config']['presence']['enabled'], isTrue);
617+
});
618+
});
389619
}

packages/realtime_client/test/socket_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ void main() {
354354
expect(channel.params, {
355355
'config': {
356356
'broadcast': {'ack': false, 'self': false},
357-
'presence': {'key': ''},
357+
'presence': {'key': '', 'enabled': false},
358358
'private': false,
359359
}
360360
});

0 commit comments

Comments
 (0)