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   1169 use 5.014;
  40         388  
4 40     2   116 use strict;
  20         165  
  2         45  
5 2     2   10 use warnings;
  2         9  
  2         49  
6 2     2   12 use Moo;
  2         3  
  2         23  
7 2     2   833 use GraphQL::Debug qw(_debug);
  2         5  
  2         157  
8 2     2   14 use Types::Standard -all;
  2         42  
  2         49  
9 2     2   96906 use Types::TypeTiny -all;
  2         5  
  2         15  
10 2     2   1435 use GraphQL::Type::Library -all;
  2         6  
  2         22  
11 2     2   28651 use GraphQL::PubSub;
  2         18  
  2         29  
12 2     2   15 use GraphQL::MaybeTypeCheck;
  2         6  
  2         7  
13 2     2   12 use curry;
  2         6  
  2         82  
14              
15 2     2   15 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  2         4  
  2         469  
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 3794 method publish(@values) {
  22         41  
  22         47  
  22         34  
61 22         60 $self->_emit('resolve', \@values);
62             }
63              
64 38 50   38   92 method _promisify((Enum[qw(resolve reject)]) $method, $data) {
  38 50       77  
  38 50       297  
  38         76  
  38         96  
  20         298  
65 20 50       55 return $data if is_Promise($data);
66 20         35 $self->promise_code->{$method}->(@$data);
67             }
68              
69 20 50   20   48 method _thenify(Maybe[CodeLike] $then, Maybe[CodeLike] $catch, (Enum[qw(resolve reject)]) $method, $data) {
  20 50       65  
  20 50       273  
  20 50       202  
  20 50       255  
  20         73  
  20         54  
  25         63  
70 25 50 33     54 return $data unless $then or $catch;
71 25         37 $self->_promisify($method, $data)->then($then, $catch);
72             }
73              
74 25 50   25   47 method _emit((Enum[qw(resolve reject)]) $method, $data) {
  25 100       79  
  25 50       384  
  25         492  
  1         13  
  24         514  
75 6 100       74 if ($self->_exhausted) {
76 6         188 die "Tried to emit to closed-off AsyncIterator\n";
77             }
78 18 50       114 if (my $next_promise = $self->_next_promise) {
79 18 50       97 $next_promise->$method(ref $data eq 'ARRAY' ? @$data : $data);
80 3         95 $self->_next_promise(undef);
81             } else {
82 3         6 push @{$self->_values_queue}, { data => $data, method => $method };
  3         10  
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 16 method error(@values) {
  3         12  
  11         59  
  11         31  
93 11         19 $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 7092 method next_p() :ReturnType(Maybe[Promise]) {
  27 50       72  
  27         50  
  27         42  
108 27 100 100     556 return undef if $self->_exhausted and !@{$self->_values_queue};
  5         65  
109 24         192 my $np;
110 24 100       40 if (my $value = shift @{$self->_values_queue}) {
  24         91  
111 18         59 $np = $self->_promisify(@$value{qw(method data)});
112             } else {
113 6         32 $np = $self->_next_promise($self->promise_code->{new}->());
114             }
115 24         1704 $np = $self->_thenify(@$_, 'resolve', $np) for @{$self->_handler_frames};
  24         114  
116 24         923 $np;
117 2     2   5263 }
  2         6  
  2         22  
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 80 method close_tap() :ReturnType(Maybe[Promise]) {
  3 50       11  
  3         7  
  3         4  
131 3 50       61 return if $self->_exhausted; # already done - no need to redo
132 3         67 $self->_exhausted(1);
133 2     2   7700 }
  2         5  
  2         13  
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 29 method map_then(Maybe[CodeLike] $then, Maybe[CodeLike] $catch = undef) {
  11 50       36  
  11         217  
  11         101  
  11         18  
  11         46  
  11         91  
147             push @{$self->_handler_frames}, [ $then, $catch ];
148             $self;
149             }
150              
151             __PACKAGE__->meta->make_immutable();
152              
153             1;