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