File Coverage

blib/lib/GraphQL/AsyncIterator.pm
Criterion Covered Total %
statement 109 109 100.0
branch 31 54 57.4
condition 4 6 66.6
subroutine 22 22 100.0
pod 5 5 100.0
total 171 196 87.2


line stmt bran cond sub pod time code
1              
2             use 5.014;
3 2     2   1120 use strict;
  40         361  
4 40     2   322 use warnings;
  20         257  
  2         46  
5 2     2   10 use Moo;
  2         4  
  2         70  
6 2     2   11 use GraphQL::Debug qw(_debug);
  2         5  
  2         16  
7 2     2   745 use Types::Standard -all;
  2         38  
  2         134  
8 2     2   13 use Types::TypeTiny -all;
  2         4  
  2         40  
9 2     2   82108 use GraphQL::Type::Library -all;
  2         6  
  2         14  
10 2     2   1283 use GraphQL::PubSub;
  2         4  
  2         24  
11 2     2   24929 use GraphQL::MaybeTypeCheck;
  2         5  
  2         35  
12 2     2   11 use curry;
  2         5  
  2         9  
13 2     2   10  
  2         5  
  2         73  
14             use constant DEBUG => $ENV{GRAPHQL_DEBUG};
15 2     2   10  
  2         4  
  2         415  
16             =head1 NAME
17              
18             GraphQL::AsyncIterator - iterator objects that return promise to next result
19              
20             =head1 SYNOPSIS
21              
22             use GraphQL::AsyncIterator;
23             my $i = GraphQL::AsyncIterator->new(
24             promise_code => $pc,
25             );
26             # also works when publish happens before next_p called
27             my $promised_value = $i->next_p;
28             $i->publish('hi'); # now $promised_value will be fulfilled
29              
30             $i->close_tap; # now next_p will return undef
31              
32             =head1 DESCRIPTION
33              
34             Encapsulates the asynchronous event-handling needed for the
35             publish/subscribe behaviour needed by L<GraphQL::Subscription>.
36              
37             =head1 ATTRIBUTES
38              
39             =head2 promise_code
40              
41             A hash-ref matching L<GraphQL::Type::Library/PromiseCode>, which must
42             provide the C<new> key.
43              
44             =cut
45              
46             has promise_code => (is => 'ro', isa => PromiseCode);
47              
48             =head1 METHODS
49              
50             =head2 publish(@values)
51              
52             Resolves the relevant promise with C<@values>.
53              
54             =cut
55              
56             has _values_queue => (is => 'ro', isa => ArrayRef, default => sub { [] });
57             has _next_promise => (is => 'rw', isa => Maybe[Promise]);
58              
59             method publish(@values) {
60 22 50   22 1 4828 $self->_emit('resolve', \@values);
  22         44  
  22         52  
  22         37  
61 22         96 }
62              
63             method _promisify((Enum[qw(resolve reject)]) $method, $data) {
64 38 50   38   107 return $data if is_Promise($data);
  38 50       399  
  38 50       82  
  38         73  
  38         141  
  20         368  
65 20 50       76 $self->promise_code->{$method}->(@$data);
66 20         41 }
67              
68             method _thenify(Maybe[CodeLike] $then, Maybe[CodeLike] $catch, (Enum[qw(resolve reject)]) $method, $data) {
69 20 50   20   60 return $data unless $then or $catch;
  20 50       79  
  20 50       284  
  20 50       195  
  20 50       290  
  20         70  
  20         62  
  25         75  
70 25 50 33     70 $self->_promisify($method, $data)->then($then, $catch);
71 25         46 }
72              
73             method _emit((Enum[qw(resolve reject)]) $method, $data) {
74 25 50   25   53 if ($self->_exhausted) {
  25 100       126  
  25 50       468  
  25         482  
  1         15  
  24         501  
75 6 100       85 die "Tried to emit to closed-off AsyncIterator\n";
76 6         278 }
77             if (my $next_promise = $self->_next_promise) {
78 18 50       120 $next_promise->$method(ref $data eq 'ARRAY' ? @$data : $data);
79 18 50       116 $self->_next_promise(undef);
80 3         164 } else {
81             push @{$self->_values_queue}, { data => $data, method => $method };
82 3         9 }
  3         12  
83             }
84              
85             =head2 error(@values)
86              
87             Rejects the relevant promise with C<@values>.
88              
89             =cut
90              
91             method error(@values) {
92 3 50   3 1 4 $self->_emit('reject', \@values);
  3         17  
  11         61  
  11         35  
93 11         24 }
94              
95             =head2 next_p
96              
97             Returns either a L<GraphQL::Type::Library/Promise> of the next value,
98             or C<undef> when closed off. Do not call this if a previous promised next
99             value has not been settled, as a queue is not maintained.
100              
101             The promise will have each of the sets of handlers added by L</map_then>
102             appended.
103              
104             =cut
105              
106             method next_p() :ReturnType(Maybe[Promise]) {
107 27 50   27 1 6664 return undef if $self->_exhausted and !@{$self->_values_queue};
  27 50       103  
  27         65  
  27         49  
108 27 100 100     530 my $np;
  5         73  
109 24         194 if (my $value = shift @{$self->_values_queue}) {
110 24 100       45 $np = $self->_promisify(@$value{qw(method data)});
  24         128  
111 18         84 } else {
112             $np = $self->_next_promise($self->promise_code->{new}->());
113 6         43 }
114             $np = $self->_thenify(@$_, 'resolve', $np) for @{$self->_handler_frames};
115 24         1920 $np;
  24         165  
116 24         938 }
117 2     2   4756  
  2         4  
  2         16  
118             =head2 close_tap
119              
120             Switch to being closed off. L</next_p> will return C<undef> as soon as
121             it runs out of L</publish>ed values. L</publish> will throw an exception.
122             B<NB> This will not cause the settling of any outstanding promise returned
123             by L</next_p>.
124              
125             =cut
126              
127             has _exhausted => (is => 'rw', isa => Bool, default => sub { 0 });
128              
129             method close_tap() :ReturnType(Maybe[Promise]) {
130 3 50   3 1 91 return if $self->_exhausted; # already done - no need to redo
  3 50       15  
  3         7  
  3         7  
131 3 50       77 $self->_exhausted(1);
132 3         74 }
133 2     2   6831  
  2         6  
  2         11  
134             =head2 map_then($then, $catch)
135              
136             Adds the handlers to this object's list of handlers, which will be
137             attached to promises returned by L</next_p>. Returns self.
138              
139             =cut
140              
141             has _handler_frames => (
142             is => 'ro', isa => ArrayRef[ArrayRef[CodeLike]], default => sub {[]},
143             );
144              
145             method map_then(Maybe[CodeLike] $then, Maybe[CodeLike] $catch = undef) {
146 11 50   11 1 30 push @{$self->_handler_frames}, [ $then, $catch ];
  11 50       54  
  11         224  
  11         87  
  11         18  
  11         56  
  11         102  
147             $self;
148             }
149              
150             __PACKAGE__->meta->make_immutable();
151              
152             1;