OLD | NEW |
| (Empty) |
1 // Copyright (c) 2015, the Dartino project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 import 'dart:dartino'; | |
6 import 'dart:convert' show JSON, UTF8; | |
7 | |
8 import 'package:http/http.dart'; | |
9 import 'package:socket/socket.dart'; | |
10 | |
11 abstract class Connection { | |
12 final String host; | |
13 final int port; | |
14 Socket connect(); | |
15 void close(); | |
16 } | |
17 | |
18 class _ConnectionImpl implements Connection { | |
19 final String host; | |
20 final int port; | |
21 _ConnectionImpl(this.host, this.port); | |
22 Socket connect() => new Socket.connect(host, port); | |
23 void close() {} | |
24 } | |
25 | |
26 class _ConnectionInvertedImpl implements Connection { | |
27 final String host = '127.0.0.1'; | |
28 final int port; | |
29 ServerSocket _socket; | |
30 _ConnectionInvertedImpl(this.port) { | |
31 _socket = new ServerSocket(host, port); | |
32 print('Waiting for Github mock server on $host:$port'); | |
33 connect(); | |
34 } | |
35 Socket connect() => _socket.accept(); | |
36 void close() { _socket.close(); } | |
37 } | |
38 | |
39 getJson(Connection service, String resource) { | |
40 HttpConnection connection = new HttpConnection(service.connect()); | |
41 HttpRequest request = new HttpRequest('${service.host}/$resource'); | |
42 request.headers["Host"] = service.host; | |
43 request.headers["User-Agent"] = 'dartino'; | |
44 HttpResponse response = connection.send(request); | |
45 if (response.statusCode != 200) { | |
46 throw 'Failed request: $resource on port ${service.port}'; | |
47 } | |
48 return JSON.decode(UTF8.decode(response.body)); | |
49 } | |
50 | |
51 class Server { | |
52 static const githubApiUrl = 'https://api.github.com'; | |
53 String get host => _connection.host; | |
54 int get port => _connection.port; | |
55 | |
56 Connection _connection; | |
57 | |
58 List<Channel> outstanding = []; | |
59 | |
60 Server(String host, int port) { | |
61 _connection = new _ConnectionImpl(host, port); | |
62 } | |
63 | |
64 Server.invertedForTesting(int port) { | |
65 _connection = new _ConnectionInvertedImpl(port); | |
66 } | |
67 | |
68 void close() { | |
69 var local = outstanding; | |
70 outstanding = []; | |
71 for (Channel channel in local) channel.receive(); | |
72 _connection.close(); | |
73 } | |
74 | |
75 dynamic get(String resource) => getJson(_connection, resource); | |
76 | |
77 User getUser(String name) => new User(name, this); | |
78 } | |
79 | |
80 abstract class Service { | |
81 final String api; | |
82 final Server server; | |
83 | |
84 var _data = null; | |
85 | |
86 Service(this.api, this.server); | |
87 | |
88 dynamic operator[](String key) { | |
89 _ensureData(); | |
90 return _data[key]; | |
91 } | |
92 | |
93 void _ensureData() { | |
94 if (_data != null) return; | |
95 _data = server.get(api); | |
96 } | |
97 } | |
98 | |
99 class User extends Service { | |
100 final String name; | |
101 | |
102 User(String name, Server parent) | |
103 : super('users/$name', parent), | |
104 this.name = name; | |
105 | |
106 Repository getRepository(String repo) => new Repository(repo, this); | |
107 } | |
108 | |
109 class Repository extends Service { | |
110 final String name; | |
111 final Pagination _commitPages; | |
112 | |
113 Repository(String name, User parent) | |
114 : super('repos/${parent.name}/$name', parent.server), | |
115 this.name = name, | |
116 _commitPages = new Pagination( | |
117 'repos/${parent.name}/$name/commits', parent.server); | |
118 | |
119 dynamic getCommitAt(int index) => _commitPages.itemAt(index); | |
120 | |
121 void prefetchCommitsInRange(int start, int end) => | |
122 _commitPages.prefetchItemsInRange(start, end); | |
123 } | |
124 | |
125 class Pagination { | |
126 static const count = 30; | |
127 | |
128 final Server server; | |
129 final String api; | |
130 List _pages = []; | |
131 | |
132 Pagination(this.api, this.server); | |
133 | |
134 void prefetch(int page) { | |
135 _fetch(page, true); | |
136 } | |
137 | |
138 List fetch(int page) { | |
139 return _fetch(page, false); | |
140 } | |
141 | |
142 void prefetchItemsInRange(int start, int end) { | |
143 int firstPage = start ~/ count; | |
144 int lastPage = (end ~/ count) + 1; | |
145 // Scheduling prefetching of surrounding pages in descending order. | |
146 // (Currently the dartino scheduler will process these in reverse order). | |
147 if (firstPage > 0) prefetch(firstPage - 1); | |
148 prefetch(lastPage + 1); | |
149 for (int i = lastPage; i >= firstPage; --i) { | |
150 prefetch(i); | |
151 } | |
152 } | |
153 | |
154 dynamic itemAt(int index) { | |
155 int page = index ~/ count; | |
156 int entry = index % count; | |
157 List entries = fetch(page); | |
158 return (entries != null && entry < entries.length) ? entries[entry] : null; | |
159 } | |
160 | |
161 _fetch(int page, bool prefetch) { | |
162 var entries = null; | |
163 if (_pages.length <= page) { | |
164 _pages.length = page + 1; | |
165 } else { | |
166 entries = _pages[page]; | |
167 } | |
168 if (entries is Channel) { | |
169 if (prefetch) return; | |
170 return entries.receive(); | |
171 } | |
172 if (entries == null) { | |
173 Channel channel = new Channel(); | |
174 _pages[page] = channel; | |
175 if (prefetch) { | |
176 server.outstanding.add(channel); | |
177 Fiber.fork(() { | |
178 _doFetch(channel, page); | |
179 server.outstanding.remove(channel); | |
180 }); | |
181 } else { | |
182 return _doFetch(channel, page); | |
183 } | |
184 } | |
185 return entries; | |
186 } | |
187 | |
188 List _doFetch(Channel channel, int page) { | |
189 List entries = null; | |
190 try { | |
191 entries = server.get('$api?page=${page + 1}'); | |
192 } catch (_) { | |
193 // Throws once when we hit past the last page. | |
194 } | |
195 _pages[page] = entries; | |
196 channel.send(entries); | |
197 return entries; | |
198 } | |
199 } | |
OLD | NEW |