UNPKG

19.4 kBJavaScriptView Raw
1const Url = require('url');
2const errors = require('./Errors'); // Standard Dweb Errors
3
4function delay(ms, val) { return new Promise(resolve => {setTimeout(() => { resolve(val); },ms)})}
5
6
7class 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}
416Transport.STATUS_CONNECTED = 0; // Connected - all other numbers are some version of not ok to use
417Transport.STATUS_FAILED = 1; // Failed to connect
418Transport.STATUS_STARTING = 2; // In the process of connecting
419Transport.STATUS_LOADED = 3; // Code loaded, but haven't tried to connect. (this is typically hard coded in subclasses constructor)
420Transport.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
421Transport.STATUSTEXT = ["Connected", "Failed", "Starting", "Loaded", "Paused"];
422exports = module.exports = Transport;