1 | const Url = require('url');
|
2 | const errors = require('./Errors'); // Standard Dweb Errors
|
3 |
|
4 | function delay(ms, val) { return new Promise(resolve => {setTimeout(() => { resolve(val); },ms)})}
|
5 |
|
6 |
|
7 | class Transport {
|
8 |
|
9 | constructor(options) {
|
10 | /*
|
11 | Doesnt do anything, its all done by SuperClasses,
|
12 | Superclass should merge with default options, call super
|
13 |
|
14 | Fields:
|
15 | statuselement: If set is an HTML Element that should be adjusted to indicate status (this is managed by Transports, just stored on Transport)
|
16 | statuscb: Callback when status changes
|
17 | name: Short name of element e.g. HTTP IPFS WEBTORRENT GUN
|
18 | */
|
19 | }
|
20 |
|
21 | /**
|
22 | * Load the code for the transport,
|
23 | * By default uses TransportXXX.requires
|
24 | * requires can be any of
|
25 | * STRING: require it and return result
|
26 | * {KEY: STRING} require string and assign to global Key
|
27 | * [STRING]: Require each of them (e.g. Gun)
|
28 | * Can also be superclassed e.g. Wolk
|
29 | */
|
30 | static loadIntoNode() {
|
31 | const requires = this.requires;
|
32 | if (Array.isArray(requires)) {
|
33 | requires.map(r => {
|
34 | debug("Requiring %s %s", t, s);
|
35 | require(r);
|
36 | });
|
37 | } else if (typeof requires === "object") {
|
38 | Object.entries(requires).map(kv => {
|
39 | debug("Requiring %s %s", t, s);
|
40 | global[kv[0]] = require(kv[1]);
|
41 | })
|
42 | } else if (typeof requires === "string") {
|
43 | return require(requires);
|
44 | }
|
45 | }
|
46 |
|
47 | static setup0(options) {
|
48 | /*
|
49 | First part of setup, create obj, add to Transports but dont attempt to connect, typically called instead of p_setup if want to parallelize connections.
|
50 | */
|
51 | throw new errors.IntentionallyUnimplementedError("Intentionally undefined function Transport.setup0 should have been subclassed");
|
52 | }
|
53 |
|
54 | p_setup1(cb) {
|
55 | /*
|
56 | Setup the resource and open any P2P connections etc required to be done just once. Asynchronous and should leave status=STATUS_STARTING until it resolves, or STATUS_FAILED if fails.
|
57 |
|
58 | cb (t)=>void If set, will be called back as status changes (so could be multiple times)
|
59 | Resolves to the Transport instance
|
60 | */
|
61 | return this;
|
62 | }
|
63 | p_setup2(cb) {
|
64 | /*
|
65 | Works like p_setup1 but runs after p_setup1 has completed for all transports. This allows for example YJS to wait for IPFS to be connected in TransportIPFS.setup1() and then connect itself using the IPFS object.
|
66 |
|
67 | cb (t)=>void If set, will be called back as status changes (so could be multiple times)
|
68 | Resolves to the Transport instance
|
69 | */
|
70 | return this;
|
71 | }
|
72 | static async p_setup(options, cb) {
|
73 | /*
|
74 | A deprecated utility to simply setup0 then p_setup1 then p_setup2 to allow a transport to be started in one step, normally Transports.p_setup should be called instead.
|
75 | */
|
76 | let t = await this.setup0(options) // Sync version that doesnt connect
|
77 | .p_setup1(cb); // And connect
|
78 |
|
79 | return t.p_setup2(cb); // And connect
|
80 | }
|
81 | /* Disconnect from the transport service - there is no guarrantee that a restart will be successfull so this is usually only for when exiting */
|
82 | stop(refreshstatus, cb) {
|
83 | // refreshstatus(Transport instance) => optional callback to the UI to update the status on the display
|
84 | this.status = Transport.STATUS_FAILED;
|
85 | if (refreshstatus) refreshstatus(this);
|
86 | cb(null, this);
|
87 | }
|
88 | togglePaused(cb) {
|
89 | /*
|
90 | Switch the state of the transport between STATUS_CONNECTED and STATUS_PAUSED,
|
91 | in the paused state it will not be used for transport but, in some cases, will still do background tasks like serving files.
|
92 |
|
93 | cb(transport)=>void a callback called after this is run, may be used for example to change the UI
|
94 | */
|
95 | switch (this.status) {
|
96 | case Transport.STATUS_CONNECTED:
|
97 | this.status = Transport.STATUS_PAUSED;
|
98 | break;
|
99 | case Transport.STATUS_PAUSED:
|
100 | this.status = Transport.STATUS_CONNECTED; // Superclass might change to STATUS_STARTING if needs to stop/restart
|
101 | break;
|
102 | case Transport.STATUS_LOADED:
|
103 | this.p_setup1(cb).then((t)=>t.p_setup2(cb)); // Allows for updating status progressively as attempts to connect
|
104 | }
|
105 | if (cb) cb(this);
|
106 | }
|
107 |
|
108 | async p_status() {
|
109 | /*
|
110 | Check the status of the underlying transport. This may update the "status" field from the underlying transport.
|
111 | returns: a numeric code for the status of a transport.
|
112 | */
|
113 | return this.status;
|
114 | }
|
115 |
|
116 | connected() {
|
117 | // True if connected (status==STATUS_CONNECTED==0) should not need subclassing
|
118 | return ! this.status;
|
119 | }
|
120 | supports(url, func, {noCache=undefined}={}) { //TODO-API
|
121 | /*
|
122 | Determine if this transport supports a certain set of URLs and a func
|
123 |
|
124 | :param url: String or parsed URL
|
125 | :param opts: { noCache } check against supportFeatures
|
126 | :return: true if this protocol supports these URLs and this func
|
127 | :throw: TransportError if invalid URL
|
128 | */
|
129 | if (typeof url === "string") {
|
130 | url = Url.parse(url); // For efficiency, only parse once.
|
131 | }
|
132 | if (url && !url.protocol) {
|
133 | throw new Error("URL failed to specific a scheme (before :) " + url.href)
|
134 | } //Should be TransportError but out of scope here
|
135 | // noinspection Annotator supportURLs is defined in subclasses
|
136 | return ( (!url || this.supportURLs.includes(url.protocol.slice(0, -1)))
|
137 | && (!func || this.supportFunctions.includes(func))
|
138 | && (!noCache || this.supportFeatures.includes("noCache"))
|
139 | )
|
140 | }
|
141 |
|
142 | validFor(url, func, opts) {
|
143 | // By default a transport can handle a url and a func if its connected and supports that url/func
|
144 | // This shouldnt need subclassing, an exception is HTTP which only applies "connected" against urls heading for the gateway
|
145 | return this.connected() && this.supports(url, func, opts);
|
146 | }
|
147 |
|
148 |
|
149 | p_rawstore(data, opts) {
|
150 | /*
|
151 | Store a blob of data onto the decentralised transport.
|
152 | Returns a promise that resolves to the url of the data
|
153 |
|
154 | :param string|Buffer data: Data to store - no assumptions made to size or content
|
155 | :resolve string: url of data stored
|
156 | */
|
157 | throw new errors.ToBeImplementedError("Intentionally undefined function Transport.p_rawstore should have been subclassed");
|
158 | }
|
159 |
|
160 | async p_rawstoreCaught(data) {
|
161 | try {
|
162 | return await this.p_rawstore(data);
|
163 | } catch (err) {
|
164 |
|
165 | }
|
166 | }
|
167 | p_store() {
|
168 | throw new errors.ToBeImplementedError("Undefined function Transport.p_store - may define higher level semantics here (see Python)");
|
169 | }
|
170 |
|
171 | //noinspection JSUnusedLocalSymbols
|
172 |
|
173 | p_rawfetch(url, {timeoutMS=undefined, start=undefined, end=undefined, relay=false}={}) {
|
174 | /*
|
175 | Fetch some bytes based on a url, no assumption is made about the data in terms of size or structure.
|
176 | Where required by the underlying transport it should retrieve a number if its "blocks" and concatenate them.
|
177 | Returns a new Promise that resolves currently to a string.
|
178 | There may also be need for a streaming version of this call, at this point undefined.
|
179 |
|
180 | :param string url: URL of object being retrieved
|
181 | :param timeoutMS Max time to wait on transports that support it (IPFS for fetch)
|
182 | :param start,end Inclusive byte range wanted (must be supported, uses a "slice" on output if transport ignores it.
|
183 | :param relay If first transport fails, try and retrieve on 2nd, then store on 1st, and so on.
|
184 |
|
185 | :resolve string: Return the object being fetched, (note currently returned as a string, may refactor to return Buffer)
|
186 | :throws: TransportError if url invalid - note this happens immediately, not as a catch in the promise
|
187 | */
|
188 | console.assert(false, "Intentionally undefined function Transport.p_rawfetch should have been subclassed");
|
189 | return "UNIMPLEMENTED";
|
190 | }
|
191 |
|
192 | p_fetch() {
|
193 | throw new errors.ToBeImplementedError("Undefined function Transport.p_fetch - may define higher level semantics here (see Python)");
|
194 | }
|
195 |
|
196 | p_rawadd(url, sig) {
|
197 | /*
|
198 | Store a new list item, ideally it should be stored so that it can be retrieved either by "signedby" (using p_rawlist) or
|
199 | by "url" (with p_rawreverse). The underlying transport does not need to guarantee the signature,
|
200 | an invalid item on a list should be rejected on higher layers.
|
201 |
|
202 | :param string url: String identifying an object being added to the list.
|
203 | :param Signature sig: A signature data structure.
|
204 | :resolve undefined:
|
205 | */
|
206 | throw new errors.ToBeImplementedError("Undefined function Transport.p_rawadd");
|
207 | }
|
208 |
|
209 | p_rawlist(url) {
|
210 | /*
|
211 | Fetch all the objects in a list, these are identified by the url of the public key used for signing.
|
212 | (Note this is the 'signedby' parameter of the p_rawadd call, not the 'url' parameter
|
213 | Returns a promise that resolves to the list.
|
214 | Each item of the list is a dict: {"url": url, "date": date, "signature": signature, "signedby": signedby}
|
215 | List items may have other data (e.g. reference ids of underlying transport)
|
216 |
|
217 | :param string url: String with the url that identifies the list.
|
218 | :resolve array: An array of objects as stored on the list.
|
219 | */
|
220 | throw new errors.ToBeImplementedError("Undefined function Transport.p_rawlist");
|
221 | }
|
222 |
|
223 | p_list() {
|
224 | throw new Error("Undefined function Transport.p_list");
|
225 | }
|
226 | p_newlisturls(cl) {
|
227 | /*
|
228 | Must be implemented by any list, return a pair of URLS that may be the same, private and public links to the list.
|
229 | returns: ( privateurl, publicurl) e.g. yjs:xyz/abc or orbitdb:a123
|
230 | */
|
231 | throw new Error("undefined function Transport.p_newlisturls");
|
232 | }
|
233 |
|
234 | //noinspection JSUnusedGlobalSymbols
|
235 | p_rawreverse(url) {
|
236 | /*
|
237 | Similar to p_rawlist, but return the list item of all the places where the object url has been listed.
|
238 | The url here corresponds to the "url" parameter of p_rawadd
|
239 | Returns a promise that resolves to the list.
|
240 |
|
241 | :param string url: String with the url that identifies the object put on a list.
|
242 | :resolve array: An array of objects as stored on the list.
|
243 | */
|
244 | throw new errors.ToBeImplementedError("Undefined function Transport.p_rawreverse");
|
245 | }
|
246 |
|
247 | listmonitor(url, callback, {current=false}={}) {
|
248 | /*
|
249 | Setup a callback called whenever an item is added to a list, typically it would be called immediately after a p_rawlist to get any more items not returned by p_rawlist.
|
250 |
|
251 | :param url: string Identifier of list (as used by p_rawlist and "signedby" parameter of p_rawadd
|
252 | :param callback: function(obj) Callback for each new item added to the list
|
253 | obj is same format as p_rawlist or p_rawreverse
|
254 | */
|
255 | console.log("Undefined function Transport.listmonitor"); // Note intentionally a log, as legitamte to not implement it
|
256 | }
|
257 |
|
258 |
|
259 | // ==== TO SUPPORT KEY VALUE INTERFACES IMPLEMENT THESE =====
|
260 | // Support for Key-Value pairs as per
|
261 | // https://docs.google.com/document/d/1yfmLRqKPxKwB939wIy9sSaa7GKOzM5PrCZ4W1jRGW6M/edit#
|
262 |
|
263 | async p_newdatabase(pubkey) {
|
264 | /*
|
265 | Create a new database based on some existing object
|
266 | pubkey: Something that is, or has a pubkey, by default support Dweb.PublicPrivate, KeyPair or an array of strings as in the output of keypair.publicexport()
|
267 | returns: {publicurl, privateurl} which may be the same if there is no write authentication
|
268 | */
|
269 | throw new errors.ToBeImplementedError("Undefined function Transport.p_newdatabase");
|
270 | }
|
271 | //TODO maybe change the listmonitor / monitor code for to use "on" and the structure of PP.events
|
272 | //TODO but note https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy about Proxy which might be suitable, prob not as doesnt map well to lists
|
273 | async p_newtable(pubkey, table) {
|
274 | /*
|
275 | Create a new table,
|
276 | pubkey: Is or has a pubkey (see p_newdatabase)
|
277 | table: String representing the table - unique to the database
|
278 | returns: {privateurl, publicurl} which may be the same if there is no write authentication
|
279 | */
|
280 | throw new errors.ToBeImplementedError("Undefined function Transport.p_newtable");
|
281 | }
|
282 |
|
283 | async p_set(url, keyvalues, value) { // url = yjs:/yjs/database/table/key
|
284 | /*
|
285 | Set one or more keys in a table.
|
286 | url: URL of the table
|
287 | keyvalues: String representing a single key OR dictionary of keys
|
288 | value: String or other object to be stored (its not defined yet what objects should be supported, e.g. any object ?
|
289 | */
|
290 | throw new errors.ToBeImplementedError("Undefined function Transport.p_set");
|
291 | }
|
292 | async p_get(url, keys) {
|
293 | /* Get one or more keys from a table
|
294 | url: URL of the table
|
295 | keys: Array of keys
|
296 | returns: Dictionary of values found (undefined if not found)
|
297 | */
|
298 | throw new errors.ToBeImplementedError("Undefined function Transport.p_get");
|
299 | }
|
300 |
|
301 | async p_delete(url, keys) {
|
302 | /* Delete one or more keys from a table
|
303 | url: URL of the table
|
304 | keys: Array of keys
|
305 | */
|
306 | throw new errors.ToBeImplementedError("Undefined function Transport.p_delete");
|
307 | }
|
308 |
|
309 | async p_keys(url) {
|
310 | /* Return a list of keys in a table (suitable for iterating through)
|
311 | url: URL of the table
|
312 | returns: Array of strings
|
313 | */
|
314 | throw new errors.ToBeImplementedError("Undefined function Transport.p_keys");
|
315 | }
|
316 | async p_getall(url) {
|
317 | /* Return a dictionary representing the table
|
318 | url: URL of the table
|
319 | returns: Dictionary of Key:Value pairs, note take care if this could be large.
|
320 | */
|
321 | throw new errors.ToBeImplementedError("Undefined function Transport.p_keys");
|
322 | }
|
323 | static async p_f_createReadStream(url, {wanturl=false}) {
|
324 | /*
|
325 | Provide a function of the form needed by tag and renderMedia library etc
|
326 |
|
327 | url Urls of stream
|
328 | wanturl True if want the URL of the stream (for service workers)
|
329 | returns f(opts) => stream returning bytes from opts.start || start of file to opts.end-1 || end of file
|
330 | */
|
331 | }
|
332 | // ------ UTILITY FUNCTIONS, NOT REQD TO BE SUBCLASSED ----
|
333 |
|
334 | static mergeoptions(a) {
|
335 | /*
|
336 | Deep merge options dictionaries, careful since searchparameters from URL passed in as null
|
337 | */
|
338 | let c = {};
|
339 | for (let i = 0; i < arguments.length; i++) {
|
340 | let b = arguments[i];
|
341 | for (let key in b) {
|
342 | let val = b[key];
|
343 | if (val !== null) {
|
344 | if ((typeof val === "object") && !Array.isArray(val) && c[key]) {
|
345 | c[key] = Transport.mergeoptions(a[key], b[key]);
|
346 | } else {
|
347 | c[key] = b[key];
|
348 | }
|
349 | }
|
350 | }
|
351 | }
|
352 | return c;
|
353 | }
|
354 |
|
355 | async p_test_list({urlexpectedsubstring=undefined}={}) {
|
356 | //TODO - this test doesn't work since we dont have Signature nor want to create dependency on it - when works, add to GUN & YJS
|
357 | {console.log(this.name,"p_test_kvt")}
|
358 | try {
|
359 | let table = await this.p_newlisturls("NACL VERIFY:1234567LIST");
|
360 | let mapurl = table.publicurl;
|
361 | console.log("newlisturls=",mapurl);
|
362 | console.assert((!urlexpectedsubstring) || mapurl.includes(urlexpectedsubstring));
|
363 | await this.p_rawadd(mapurl, "testvalue");
|
364 | let res = await this.p_rawlist(mapurl);
|
365 | console.assert(res.length===1 && res[0] === "testvalue");
|
366 | await this.p_rawadd(mapurl, {foo: "bar"}); // Try adding an object
|
367 | res = await this.p_rawlist(mapurl);
|
368 | console.assert(res.length === 2 && res[1].foo === "bar");
|
369 | await this.p_rawadd(mapurl, [1,2,3]); // Try setting to an array
|
370 | res = await this.p_rawlist(mapurl);
|
371 | console.assert(res.length === 2 && res[2].length === 3 && res[2][1] === 2);
|
372 | await delay(200);
|
373 | console.log(this.name, "p_test_list complete")
|
374 | } catch(err) {
|
375 | console.log("Exception thrown in ", this.name, "p_test_list:", err.message);
|
376 | throw err;
|
377 | }
|
378 |
|
379 | }
|
380 | async p_test_kvt(urlexpectedsubstring) {
|
381 | /*
|
382 | Test the KeyValue functionality of any transport that supports it.
|
383 | urlexpectedsubstring: Some string expected in the publicurl of the table.
|
384 | */
|
385 | {console.log(this.name,"p_test_kvt")}
|
386 | try {
|
387 | let table = await this.p_newtable("NACL VERIFY:1234567KVT","mytable");
|
388 | let mapurl = table.publicurl;
|
389 | console.log("newtable=",mapurl);
|
390 | console.assert(mapurl.includes(urlexpectedsubstring));
|
391 | await this.p_set(mapurl, "testkey", "testvalue");
|
392 | let res = await this.p_get(mapurl, "testkey");
|
393 | console.assert(res === "testvalue");
|
394 | await this.p_set(mapurl, "testkey2", {foo: "bar"}); // Try setting to an object
|
395 | res = await this.p_get(mapurl, "testkey2");
|
396 | console.assert(res.foo === "bar");
|
397 | await this.p_set(mapurl, "testkey3", [1,2,3]); // Try setting to an array
|
398 | res = await this.p_get(mapurl, "testkey3");
|
399 | console.assert(res[1] === 2);
|
400 | res = await this.p_keys(mapurl);
|
401 | console.assert(res.includes("testkey") && res.includes("testkey3") && res.length === 3);
|
402 | await this.p_delete(mapurl, ["testkey"]);
|
403 | res = await this.p_getall(mapurl);
|
404 | console.log("getall=>",res);
|
405 | console.assert(res.testkey2.foo === "bar" && res.testkey3["1"] === 2 && !res.testkey);
|
406 | await delay(200);
|
407 | console.log(this.name, "p_test_kvt complete")
|
408 | } catch(err) {
|
409 | console.log("Exception thrown in ", this.name, "p_test_kvt:", err.message);
|
410 | throw err;
|
411 | }
|
412 | }
|
413 |
|
414 |
|
415 | }
|
416 | Transport.STATUS_CONNECTED = 0; // Connected - all other numbers are some version of not ok to use
|
417 | Transport.STATUS_FAILED = 1; // Failed to connect
|
418 | Transport.STATUS_STARTING = 2; // In the process of connecting
|
419 | Transport.STATUS_LOADED = 3; // Code loaded, but haven't tried to connect. (this is typically hard coded in subclasses constructor)
|
420 | Transport.STATUS_PAUSED = 4; // It was launched, probably connected, but now paused so will be ignored by validFor // Note this is copied to dweb-archive/Nav.js so check if change
|
421 | Transport.STATUSTEXT = ["Connected", "Failed", "Starting", "Loaded", "Paused"];
|
422 | exports = module.exports = Transport;
|