@naanlang/naan 1.0.16 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE.md +2 -2
  2. package/README.md +13 -11
  3. package/bin/index.js +4 -4
  4. package/dist/naan.min.js +14 -14
  5. package/frameworks/browser/https_request.nlg +26 -10
  6. package/frameworks/browser/sworker.js +26 -25
  7. package/frameworks/browser/terminals.nlg +118 -55
  8. package/frameworks/browser/ws_client.nlg +124 -0
  9. package/frameworks/client/apiclient.nlg +82 -116
  10. package/frameworks/client/psm_client.nlg +37 -16
  11. package/frameworks/client/relaycon.nlg +308 -0
  12. package/frameworks/common/common.nlg +1 -1
  13. package/frameworks/common/utils.nlg +40 -2
  14. package/frameworks/node/apiserver.nlg +342 -103
  15. package/frameworks/node/https_request.nlg +32 -6
  16. package/frameworks/node/node.nlg +60 -2
  17. package/frameworks/node/psm_server.nlg +11 -7
  18. package/frameworks/node/worker.nlg +22 -9
  19. package/frameworks/node/ws_client.nlg +127 -0
  20. package/frameworks/project/build.nlg +3 -2
  21. package/frameworks/project/projects.nlg +8 -2
  22. package/frameworks/running/debugnub.nlg +2 -2
  23. package/frameworks/running/debugutil.nlg +1 -1
  24. package/frameworks/running/executors.nlg +69 -17
  25. package/frameworks/running/sourcecode.nlg +2 -2
  26. package/frameworks/running/taskexec.nlg +25 -25
  27. package/frameworks/storage/dbt_pouch.nlg +8 -6
  28. package/frameworks/storage/file_manager.nlg +6 -3
  29. package/frameworks/storage/psm.nlg +5 -3
  30. package/frameworks/storage/psm_dbtables.nlg +75 -53
  31. package/frameworks/storage/resources.nlg +4 -2
  32. package/lib/browser/env_web.js +6 -6
  33. package/lib/browser/env_webworker.js +1 -1
  34. package/lib/core/naanlib.js +14 -14
  35. package/lib/env_node.js +97 -8
  36. package/lib/env_nodeworker.js +22 -4
  37. package/package.json +1 -1
  38. package/plugins/serviceAws/aws/lambda_rest_index.js +5 -1
  39. package/plugins/serviceAws/aws_dynamo.nlg +91 -32
  40. package/plugins/serviceAws/dbt_aws.nlg +5 -5
  41. package/plugins/serviceAws/psm_aws.nlg +2 -2
  42. package/test/harness.nlg +1 -1
  43. package/test/test_01_core.nlg +30 -4
  44. package/test/test_02_context.nlg +2 -2
  45. package/test/test_07_lingo.nlg +3 -0
@@ -6,49 +6,107 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2017-2022 by Richard C. Zulch
9
+ * Copyright (c) 2017-2024 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
13
13
 
14
14
  /*
15
- * MakeAPI
15
+ * APIServer
16
16
  *
17
17
  * Make an API server object.
18
18
  *
19
19
  */
20
20
 
21
- closure MakeAPI(port, options, callback, local chroot, server) {
22
- if !options
23
- options = { }
24
- else
25
- options = new(options)
26
- if !options.relpath
27
- options.relpath = ""
28
-
29
- chroot = JSpath.join(js.d, options.relpath)
30
- if not string(chroot) || chroot.length < 2 {
31
- debuglog("APIserver: invalid path", chroot)
32
- return (false)
33
- }
34
-
21
+ closure APIServer(options, local server, chroot) {
22
+ global(JSpath, js, bodyParser, express, psmServerMod, workerMod)
23
+ apisLoadLibs()
35
24
  server = new(object, this)
36
- server.psm = require("./psm_server.nlg").MakeServerPSM(server)
37
- server.root = chroot
38
25
  server.app = express()
39
26
  server.guid = options.guid
40
27
  server.notifyq = []
41
28
  server.responseq = []
42
- server.projects = { }
29
+ options = merge({
30
+ relpath: ""
31
+ name: "NaanServer"
32
+ }, options)
33
+ chroot = JSpath.join(js.d, options.relpath)
34
+ server.root = chroot
35
+
36
+ // updatePort
37
+ // Update our state based on the specified port number.
38
+ function updatePort(port) {
39
+ server.host = "localhost:${tostring(port)}"
40
+ server.oururl = "http://${server.host}/nide.html"
41
+ server.port = port
42
+ }
43
+
44
+ // open
45
+ //
46
+ // Open the server, returning a result tuple. A fixed port may be specified.
47
+ //
48
+ server.open = closure open(port, local error, pending, result, hostfs) {
49
+ if server.http
50
+ return (list(false, { open: true })) // already open
51
+ if !string(chroot) || chroot.length < 2 {
52
+ error = Error("APIserver: invalid path", chroot) // invalid option
53
+ return (list(error))
54
+ }
55
+ if port
56
+ options.fixedPort = port // override fixed port from argument
57
+ else if options.fixedPort
58
+ port = options.fixedPort // use fixed port option
59
+ else
60
+ port = options.basePort // search for port from base
61
+ if options.fixedPort
62
+ updatePort(port) // need this to access in-use server
63
+ pending = new(nonce)
64
+ while !result {
65
+ server.http = server.app.listen(port, function() {
66
+ pending.signal(list(false, port)) // success
67
+ }).on("error", function (err) {
68
+ if err.code == "EADDRINUSE" && !options.fixedPort {
69
+ ++port // try next port
70
+ pending.signal(false)
71
+ }
72
+ else
73
+ pending.signal(list(Error("server listen error", err, { code: err.code })))
74
+ })
75
+ result = pending.wait()
76
+ pending.reset()
77
+ }
78
+ if result.0 { // open failed
79
+ server.http = false
80
+ return (result)
81
+ }
82
+ updatePort(port)
83
+ if server.guid
84
+ server.oururl = server.oururl.concat("?guid=", server.guid)
85
+ App.api = server // used remotely by client/relaycon.nlg
86
+ server.ws = WebSocketServer(server)
87
+ WesRelay(server) // registers as subsystem
88
+ server.psm = psmServerMod.MakeServerPSM(server, options.name)
89
+ `(error, hostfs) = server.psm.conns.NodeFS.connect("Host", "NideFS", list(""))
90
+ if error {
91
+ error = Error("APIServer: can't access HostFS", error)
92
+ ErrorDebuglog(error)
93
+ return (list(error))
94
+ }
95
+ server.hostfs = hostfs
96
+ workerMod.WorkerController(server)
97
+ list(false, port)
98
+ }
43
99
 
44
100
  // close
45
101
  //
46
102
  // Close the server.
47
-
103
+ //
48
104
  server.close = function close() {
49
105
  if server.ws
50
106
  server.ws.close()
51
107
  server.http.close(function(){debuglog("API Server closed")})
108
+ server.ws = false
109
+ server.http = false
52
110
  }
53
111
 
54
112
  // Register
@@ -57,10 +115,18 @@ closure MakeAPI(port, options, callback, local chroot, server) {
57
115
  // have the wsReceive method invoked:
58
116
  // wsReceive(message, conn)
59
117
  // And can send a reply on the specified connection.
60
-
118
+ //
61
119
  server.Register = function Register(serverID, subsystem) {
62
120
  server.ws.subsystems[serverID] = subsystem
63
121
  }
122
+
123
+ // Relays
124
+ //
125
+ // Report our relay destIDs.
126
+ //
127
+ server.Relays = function Relays() {
128
+ server.ws.subsystems.Relay.destList()
129
+ }
64
130
 
65
131
  //
66
132
  // status and message notification
@@ -182,7 +248,7 @@ closure MakeAPI(port, options, callback, local chroot, server) {
182
248
  if responder.active {
183
249
  queued = server.notifyq.filter(function(item,index,array) { item.stamp > after })
184
250
  if queued.length > 0
185
- res.send(js.g.JSON.stringify(queued))
251
+ res.send(JSONstringify(queued))
186
252
  else if timeout
187
253
  res.status(200).end()
188
254
  else
@@ -209,47 +275,8 @@ closure MakeAPI(port, options, callback, local chroot, server) {
209
275
  server.hostfs.api(req, res)
210
276
  })
211
277
 
212
- //
213
- // initialize the server
214
- //
215
-
216
- let(local pending, result) {
217
- pending = new(nonce)
218
-
219
- // find our port
220
-
221
- loop {
222
- server.http = server.app.listen(port, function(){
223
- pending.signal({}) // success
224
- }).on("error", function (err) {
225
- if err.code == "EADDRINUSE"
226
- ++port // try next port
227
- else {
228
- pending.result = Error("server listen error", err) // permanent failure
229
- callback(err, false)
230
- }
231
- pending.signal(pending.result)
232
- })
233
- if pending.wait()
234
- break
235
- pending.reset()
236
- }
237
- if pending.result
238
- return (false) // failed to open
239
-
240
- // start the services
241
-
242
- server.ws = WebSocketServer(server)
243
- server.port = port
244
- callback(false, port, server) // notify of success
245
- result = server.psm.conns.NodeFS.connect("Host", "NideFS", list("")) // ### this should go through resource tracker
246
- if result.0
247
- ErrorDebuglog(Error("can't access HostFS", result.0))
248
- else
249
- server.hostfs = result.1
250
- }()
278
+ // finis
251
279
 
252
- // return new API server object
253
280
  server
254
281
  };
255
282
 
@@ -262,6 +289,7 @@ closure MakeAPI(port, options, callback, local chroot, server) {
262
289
  */
263
290
 
264
291
  closure WebSocketServer(server, local websocks) {
292
+ global(ws)
265
293
  websocks = new(object, this)
266
294
  websocks.conns = [] // our connections
267
295
  websocks.subsystems = { } // registered subsystems
@@ -277,26 +305,31 @@ closure WebSocketServer(server, local websocks) {
277
305
 
278
306
  // Event handlers
279
307
 
280
- websocks.wss.on("connection", function(socket, request) {
281
- websocks.conns.push(WesCon(server, websocks, socket, request))
308
+ websocks.wss.on("connection", function(socket, request, local conn, serverID) {
309
+ conn = WesCon(server, websocks, socket, request)
310
+ websocks.conns.push(conn)
311
+ for serverID in websocks.subsystems
312
+ websocks.subsystems[serverID].wsOpen(conn) // notify of our openings
282
313
  })
283
-
314
+
284
315
  server.wss.on("error", function(error) {
285
- ErrorDebuglog(Error("ws server error:", error))
316
+ ErrorDebuglog(Error("WebSocketServer: connection error", error))
286
317
  })
287
318
 
288
319
  server.wss.on("close", function() {
289
320
  debuglog("WebSocketServer has closed")
290
321
  websocks.closed = true // note that we are closed
291
322
  })
292
-
323
+
293
324
  // closeConn
294
325
  // respond to connection closing
295
-
296
- websocks.closeConn = function closeConn(conn) {
326
+
327
+ websocks.closeConn = function closeConn(conn, local serverID) {
297
328
  websocks.conns = websocks.conns.filter(function(item) {
298
- !(item eq conn)
329
+ item !== conn
299
330
  })
331
+ for serverID in websocks.subsystems
332
+ websocks.subsystems[serverID].wsClose(conn) // notify of our closures
300
333
  }
301
334
 
302
335
  // finis
@@ -318,23 +351,34 @@ closure WebSocketServer(server, local websocks) {
318
351
  */
319
352
 
320
353
  closure WesCon(server, websocks, socket, request, local wescon) {
354
+ global()
321
355
  wescon = new(object, this)
322
356
  wescon.socket = socket
323
- wescon.serverIDs = []
357
+ wescon.info = {
358
+ destID: UUID()
359
+ address: new(socket._socket.address())
360
+ }
324
361
 
325
362
  // on message
326
363
  //
327
364
  // Incoming data.
328
-
329
- socket.on("message", function onmessage(text, local message, error) {
365
+ //
366
+ socket.on("message", closure onmessage(text, local message, error) {
330
367
  try {
331
368
  message = new(js.g.JSON.parse(text))
332
369
  if server.guid && message.guid != server.guid
333
370
  error = Error("unauthorized")
334
- else if message.clientID && websocks.subsystems[message.serverID] {
335
- wescon.serverIDs.push(message.serverID) // ### track subsystems for closing
336
- websocks.subsystems[message.serverID].wsReceive(message, wescon)
371
+ else if message.init {
372
+ if message.init.destID {
373
+ wescon.relay = true
374
+ websocks.subsystems.Relay.wsChangeDestID(wescon, wescon.info.destID, message.init.destID)
375
+ }
376
+ wescon.info = merge(wescon.info, message.init)
337
377
  }
378
+ else if message.respOp && websocks.subsystems.Relay
379
+ websocks.subsystems.Relay.wsReceive(message, wescon) // with respOp, must be a relay
380
+ else if message.clientID && websocks.subsystems[message.serverID]
381
+ websocks.subsystems[message.serverID].wsReceive(message, wescon)
338
382
  else
339
383
  error = Error("can't route message")
340
384
  } catch {
@@ -342,43 +386,41 @@ closure WesCon(server, websocks, socket, request, local wescon) {
342
386
  error = Error("can't decode message JSON", exception)
343
387
  }
344
388
  if error
345
- socket.send(js.g.JSON.stringify([error]))
389
+ socket.send(JSONstringify([error]))
346
390
  })
347
391
 
348
392
  // on error
349
393
  //
350
394
  // Error has occurred on this connection.
351
-
352
- socket.on("error", function onerror(error) {
353
- debuglog("connection error", error)
395
+ //
396
+ socket.on("error", closure onerror(error) {
397
+ debuglog("Wescon: connection error", error)
354
398
  })
355
399
 
356
400
  // on close
357
401
  //
358
402
  // Connection has closed.
359
-
360
- socket.on("close", function onclose(code, reason, local serverID) {
361
- if code != 1000 && code != 1001 && code != 1005
362
- debuglog("connection has closed:", code, reason)
363
- websocks.closeConn(wescon)
364
- wecson.closed = {
403
+ //
404
+ socket.on("close", closure onclose(code, reason) {
405
+ if code != 1000 && code != 1001 && code != 1005 && code != 1006
406
+ debuglog("Wescon: connection has closed:", code, reason, JSONstringify(wescon.info))
407
+ wescon.closed = {
365
408
  code: code
366
409
  reason: reason
367
410
  }
368
- for serverID in wescon.serverIDs
369
- websocks.subsystems[message.serverID].wsClose(wescon) // notify of our closures
411
+ websocks.closeConn(wescon)
370
412
  })
371
413
 
372
414
  // send
373
415
  //
374
416
  // Send a dictionary, which must have a clientID defined.
375
-
417
+ //
376
418
  wescon.send = function send(message) {
377
419
  if !message.clientID || !message.serverID
378
420
  throw("Wescon.send: client and server IDs required")
379
421
  if wescon.closed
380
422
  list(Error("Socket connection closed", wescon.closed))
381
- socket.send(js.g.JSON.stringify(message)),
423
+ socket.send(JSONstringify(message)),
382
424
  [false, {ok: true}]
383
425
  }
384
426
 
@@ -387,6 +429,201 @@ closure WesCon(server, websocks, socket, request, local wescon) {
387
429
  };
388
430
 
389
431
 
432
+ /*
433
+ * WesRelay
434
+ *
435
+ * Web socket relay controller. This allows apiserver clients to discover each other and connect
436
+ * for remote evaluation, etc. A source connection makes a request of a destination connection, and
437
+ * the response is routed back to the original connection.
438
+ *
439
+ * This uses the following message fields for the requesting source:
440
+ * guid: security token
441
+ * clientID: defined by the source, this string defines where replies should be sent
442
+ * serverID: "Relay"
443
+ * destID: the destination connection
444
+ * endServerID: the serverID to use at the destination
445
+ * The message is modified with a virtual clientID to send to the destination, and then changed back
446
+ * for return to the requester.
447
+ *
448
+ * To discover the available destination connections a source should use:
449
+ * clientID: requester
450
+ * serverID: "Relay"
451
+ * relayOp: "getdests"
452
+ * which returns:
453
+ * relayOp: "destlist"
454
+ * destinations: [
455
+ * destID: guid for the connection
456
+ * address: socket port / family / ip address
457
+ * name: <string>
458
+ * workerID: id of destination worker
459
+ * ]
460
+ * New and closed destinations are sent asynchronously to each source:
461
+ * relayOp: "opened" / "closed"
462
+ * info: {
463
+ * destID: guid for the connection
464
+ * address: socket port / family / ip address
465
+ * name: <string>
466
+ * workerID: id of destination worker
467
+ * }
468
+ *
469
+ */
470
+
471
+ closure WesRelay(server, local welay) {
472
+ global()
473
+ welay = new(object, this)
474
+ welay.serverID = "Relay" // our serverID as a subsystem
475
+ welay.destIDs = { } // destination conns index
476
+ welay.virtualIDs = { } // virtual clientID index
477
+ welay.links = new(weakmap) // source and dest connections
478
+ server.Register(welay.serverID, welay)
479
+
480
+ // getVirtualID
481
+ // Return the virtual clientID for a requesting source/clientID and destination.
482
+ function getVirtualID(sconn, clientID, dconn, local sdict, ddict, vid) {
483
+ sdict = welay.links[sconn]
484
+ if !sdict
485
+ welay.links[sconn] = sdict = {
486
+ virtualIDs: { }
487
+ }
488
+ ddict = welay.links[dconn]
489
+ if !ddict
490
+ welay.links[dconn] = ddict = {
491
+ virtualIDs: { }
492
+ }
493
+ vid = ddict.virtualIDs[clientID]
494
+ if !vid {
495
+ sdict.virtualIDs[clientID] = vid = UUID()
496
+ welay.virtualIDs[vid] = {
497
+ sconn: sconn
498
+ dconn: dconn
499
+ clientID: clientID
500
+ }
501
+ }
502
+ vid
503
+ }
504
+
505
+ // addConnection
506
+ // Record a new connection as a possible destination.
507
+ function addConnection(conn) {
508
+ welay.destIDs[conn.info.destID] = conn
509
+ }
510
+
511
+ // removeConnection
512
+ // Cleanup for a closed connection, be it source or destination or both.
513
+ function removeConnection(conn, local vid, cdict, did, dconn) {
514
+ welay.links[conn] = undefined
515
+ for `(vid, cdict) in welay.virtualIDs
516
+ if cdict.sconn === conn || cdict.dconn === conn
517
+ welay.virtualIDs[vid] = undefined
518
+ for `(did, dconn) in welay.destIDs
519
+ if dconn === conn
520
+ welay.destIDs[did] = undefined
521
+ }
522
+
523
+ // destList
524
+ // Make an array of destination connections, excluding the specified one.
525
+ //
526
+ welay.destList = function destList(conn, local output, did, dconn) {
527
+ output = []
528
+ for `(did, dconn) in welay.destIDs
529
+ if dconn !== conn && dconn.relay
530
+ output.push(dconn.info)
531
+ output
532
+ }
533
+
534
+ // wsReceive
535
+ //
536
+ // Process a message from the websocket API.
537
+ //
538
+ welay.wsReceive = function wsReceive(message, sconn, local dconn, error, cdict) {
539
+ if message.relayOp == "getdests" { // get destination list
540
+ message.relayOp = "destlist"
541
+ message.destinations = destList(sconn)
542
+ sconn.send(message)
543
+ return
544
+ }
545
+ else if message.destID && message.endServerID { // request from a source
546
+ dconn = welay.destIDs[message.destID]
547
+ if !dconn
548
+ error = Error("destID or endServerID invalid", message.relayOp)
549
+ else {
550
+ message.serverID = message.endServerID
551
+ message.clientID = getVirtualID(sconn, message.clientID, dconn)
552
+ message.endServerID = undefined
553
+ message.destID = undefined
554
+ dconn.send(message)
555
+ return
556
+ }
557
+ }
558
+ else { // response from a destination
559
+ cdict = welay.virtualIDs[message.clientID]
560
+ dconn = cdict.sconn // this is original source
561
+ if !cdict || !dconn
562
+ error = Error("cannot route reply", message.relayOp)
563
+ else {
564
+ message.endServerID = message.serverID
565
+ message.serverID = welay.serverID
566
+ message.clientID = cdict.clientID
567
+ message.destID = dconn.info.destID
568
+ dconn.send(message)
569
+ return
570
+ }
571
+ }
572
+ sconn.send({
573
+ serverID: message.serverID
574
+ clientID: message.clientID
575
+ respOp: "error"
576
+ payload: [error]
577
+ })
578
+ }
579
+
580
+ // wsOpen
581
+ //
582
+ // Note that a new connection was opened.
583
+ //
584
+ welay.wsOpen = function wsOpen(conn) {
585
+ addConnection(conn)
586
+ }
587
+
588
+ // wsClose
589
+ //
590
+ // Note that an existing connection was closed.
591
+ //
592
+ welay.wsClose = function wsClose(conn, local destID, sconn) {
593
+ removeConnection(conn)
594
+ if conn.relay
595
+ for `(destID, sconn) in welay.destIDs
596
+ sconn.send({
597
+ serverID: welay.serverID
598
+ clientID: "*"
599
+ relayOp: "closed"
600
+ info: conn.info
601
+ })
602
+ }
603
+
604
+ // wsChangeDestID
605
+ //
606
+ // The destID has changed (via the client init message.)
607
+ //
608
+ welay.wsChangeDestID = function wsChangeDestID(conn, prevID, newID, local destID, sconn) {
609
+ welay.destIDs[prevID] = undefined
610
+ welay.destIDs[newID] = conn
611
+ conn.info.destID = newID
612
+ for `(destID, sconn) in welay.destIDs
613
+ if destID != newID
614
+ sconn.send({
615
+ serverID: welay.serverID
616
+ clientID: "*"
617
+ relayOp: "opened"
618
+ info: conn.info
619
+ })
620
+ }
621
+
622
+ // finis
623
+ welay
624
+ };
625
+
626
+
390
627
  /*
391
628
  * ApisInit
392
629
  *
@@ -394,22 +631,24 @@ closure WesCon(server, websocks, socket, request, local wescon) {
394
631
  *
395
632
  */
396
633
 
397
- function ApisInit(local manifest) {
634
+ function apisLoadLibs() {
635
+ if typeof(express) === xobject
636
+ return
637
+ express = js.r("express")
638
+ bodyParser = js.r("body-parser")
639
+ textParser = bodyParser.text()
640
+ ws = js.r("ws")
641
+ };
398
642
 
399
- manifest = `(MakeAPI, WebSocketServer, WesCon, ApisInit)
643
+ function ApisInit(local manifest) {
644
+ manifest = `(APIServer, WebSocketServer, WesCon, WesRelay, apisLoadLibs, ApisInit)
400
645
 
401
- Naan.module.build(module.id, "API", function(modobj, compobj) {
646
+ Naan.module.build(module.id, "apiserver", function(modobj, compobj) {
402
647
  require("./node.nlg")
403
648
  compobj.manifest = manifest
404
- modobj.exports.MakeAPI = MakeAPI
405
-
406
- express = js.r("express")
407
- bodyParser = js.r("body-parser")
408
- textParser = bodyParser.text()
409
- ws = js.r("ws")
410
-
411
- require("./filesystem.nlg")
412
- require("./psm_server.nlg")
413
- require("./worker.nlg")
649
+ modobj.exports.APIServer = APIServer
414
650
  })
651
+ require("./filesystem.nlg")
652
+ psmServerMod = require("./psm_server.nlg")
653
+ workerMod = require("./worker.nlg")
415
654
  }();
@@ -5,15 +5,15 @@
5
5
  *
6
6
  * column positioning: // // !
7
7
  *
8
- * Copyright (c) 2021-2023 by Richard C. Zulch
8
+ * Copyright (c) 2021-2024 by Richard C. Zulch
9
9
  *
10
10
  */
11
11
 
12
12
 
13
13
  /*
14
- * HttpsApiRequest
14
+ * httpsApiReqOnce
15
15
  *
16
- * Perform an API request from in NodeJS. This operates the same as HttpsApiRequest for browsers.
16
+ * Perform an API request from NodeJS. This operates the same as HttpsApiRequest for browsers.
17
17
  *
18
18
  * Options:
19
19
  * {
@@ -45,7 +45,7 @@
45
45
  *
46
46
  */
47
47
 
48
- closure HttpsApiRequest(url, options,
48
+ closure httpsApiReqOnce(url, options,
49
49
  local urlq, reqOptions, putdata, item, xurl, pending, chunks, request) {
50
50
  if options.query
51
51
  urlq = url.concat(EncodeQuery("?", options.query))
@@ -105,7 +105,7 @@ closure HttpsApiRequest(url, options,
105
105
  })
106
106
  response.on("end", function (local content, error, headers) {
107
107
  content = js.g.Buffer.concat(chunks)
108
- if response.statusCode < 200 || response.statusCode >= 300 {
108
+ if response.statusCode < 200 || response.statusCode >= 299 { // 299 is "cancelled"
109
109
  if options.debug
110
110
  debuglog("HttpsApiRequest status:", url, response.statusCode, "message:", content)
111
111
  pending.signal(list(Error("HttpsApiRequest statusCode:", url, response.statusCode, {
@@ -132,6 +132,7 @@ closure HttpsApiRequest(url, options,
132
132
  pending.signal(list(error, content, {
133
133
  headers: response.headers
134
134
  status: response.statusCode
135
+ contentType: response.headers["content-type"]
135
136
  }))
136
137
  })
137
138
  })
@@ -148,6 +149,31 @@ closure HttpsApiRequest(url, options,
148
149
  };
149
150
 
150
151
 
152
+ /*
153
+ * HttpsApiRequest
154
+ *
155
+ * Make the request with optional retry logic.
156
+ *
157
+ * Options:
158
+ * {
159
+ * retrier: function(error) { error.status == 502 } // retry 502 errors
160
+ * }
161
+ *
162
+ */
163
+
164
+ closure HttpsApiRequest(url, options, local result) {
165
+ loop {
166
+ result = httpsApiReqOnce(url, options)
167
+ if result.0 && result.0.status { // check for retries
168
+ if !options.retrier(result.0)
169
+ break
170
+ } else
171
+ break
172
+ }
173
+ result
174
+ };
175
+
176
+
151
177
  /*
152
178
  * https_nodeInit
153
179
  *
@@ -156,7 +182,7 @@ closure HttpsApiRequest(url, options,
156
182
  */
157
183
 
158
184
  function https_nodeInit(local manifest) {
159
- manifest = `(HttpsApiRequest, https_nodeInit)
185
+ manifest = `(httpsApiReqOnce, HttpsApiRequest, https_nodeInit)
160
186
 
161
187
  Naan.module.build(module.id, "https_request", function(modobj, compobj) {
162
188
  require("./node.nlg")