eliasdb-graphql.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /**
  2. * EliasDB - JavaScript GraphQL client library
  3. *
  4. * Copyright 2019 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the Mozilla Public
  7. * License, v. 2.0. If a copy of the MPL was not distributed with this
  8. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  9. *
  10. */
  11. export enum RequestMetod {
  12. Post = 'post',
  13. Get = 'get',
  14. }
  15. export class EliasDBGraphQLClient {
  16. /**
  17. * Host this client is connected to.
  18. */
  19. protected host: string;
  20. /**
  21. * Partition this client is working on.
  22. */
  23. protected partition: string;
  24. /**
  25. * Websocket over which we can handle subscriptions.
  26. */
  27. private ws?: WebSocket;
  28. /**
  29. * EliasDB GraphQL endpoints.
  30. */
  31. private graphQLEndpoint: string;
  32. private graphQLReadOnlyEndpoint: string;
  33. /**
  34. * List of operations to execute once the websocket connection is established.
  35. */
  36. private delayedOperations: {(): void}[] = [];
  37. /**
  38. * Queue of subscriptions which await an id;
  39. */
  40. private subscriptionQueue: {(data: any): void}[] = [];
  41. /**
  42. * Map of active subscriptions.
  43. */
  44. private subscriptionCallbacks: {[id: string]: {(data: any): void}} = {};
  45. /**
  46. * Createa a new EliasDB GraphQL Client.
  47. *
  48. * @param host Host to connect to.
  49. * @param partition Partition to query.
  50. */
  51. public constructor(
  52. host: string = window.location.host,
  53. partition: string = 'main',
  54. ) {
  55. this.host = host;
  56. this.partition = partition;
  57. this.graphQLEndpoint = `https://${host}/db/v1/graphql/${partition}`;
  58. this.graphQLReadOnlyEndpoint = `https://${host}/db/v1/graphql-query/${partition}`;
  59. }
  60. /**
  61. * Initialize a websocket to support subscriptions.
  62. */
  63. private initWebsocket() {
  64. const url = `wss://${this.host}/db/v1/graphql-subscriptions/${this.partition}`;
  65. this.ws = new WebSocket(url);
  66. this.ws.onmessage = this.message.bind(this);
  67. this.ws.onopen = () => {
  68. if (this.ws) {
  69. this.ws.send(
  70. JSON.stringify({
  71. type: 'init',
  72. payload: {},
  73. }),
  74. );
  75. }
  76. };
  77. }
  78. /**
  79. * Run a GraphQL query or mutation and return the response.
  80. *
  81. * @param query Query to run.
  82. * @param variables List of variable values. The query must define these
  83. * variables.
  84. * @param operationName Name of the named operation to run. The query must
  85. * specify this named operation.
  86. * @param method Request method to use. Get requests cannot run mutations.
  87. */
  88. public req(
  89. query: string,
  90. variables: {[key: string]: any} = {},
  91. operationName: string = '',
  92. method: RequestMetod = RequestMetod.Post,
  93. ): Promise<any> {
  94. const http = new XMLHttpRequest();
  95. const toSend: {[key: string]: any} = {
  96. operationName,
  97. variables,
  98. query,
  99. };
  100. // Send an async ajax call
  101. if (method === RequestMetod.Post) {
  102. http.open(method, this.graphQLEndpoint, true);
  103. } else {
  104. const params = Object.keys(toSend)
  105. .map(key => {
  106. const val =
  107. key !== 'variables'
  108. ? toSend[key]
  109. : JSON.stringify(toSend[key]);
  110. return `${key}=${encodeURIComponent(val)}`;
  111. })
  112. .join('&');
  113. const url = `${this.graphQLReadOnlyEndpoint}?${params}`;
  114. http.open(method, url, true);
  115. }
  116. http.setRequestHeader('content-type', 'application/json');
  117. return new Promise(function(resolve, reject) {
  118. http.onload = function() {
  119. try {
  120. if (http.status === 200) {
  121. resolve(http.response);
  122. } else {
  123. let err: string;
  124. try {
  125. err = JSON.parse(http.responseText)['errors'];
  126. } catch {
  127. err = http.responseText.trim();
  128. }
  129. reject(err);
  130. }
  131. } catch (e) {
  132. reject(e);
  133. }
  134. };
  135. if (method === RequestMetod.Post) {
  136. http.send(JSON.stringify(toSend));
  137. } else {
  138. http.send();
  139. }
  140. });
  141. }
  142. /**
  143. * Run a GraphQL subscription and receive updates if the data changes.
  144. *
  145. * @param query Query to run.
  146. * @param update Update callback.
  147. */
  148. public subscribe(
  149. query: string,
  150. update: (data: any) => void,
  151. variables: any = null,
  152. ) {
  153. if (!this.ws) {
  154. this.initWebsocket();
  155. }
  156. if (this.ws) {
  157. const that = this;
  158. const subscribeCall = function() {
  159. if (that.ws) {
  160. that.ws.send(
  161. JSON.stringify({
  162. id: that.subscriptionQueue.length,
  163. query,
  164. type: 'subscription_start',
  165. variables: null,
  166. }),
  167. );
  168. that.subscriptionQueue.push(update);
  169. }
  170. };
  171. if (this.ws.readyState !== WebSocket.OPEN) {
  172. this.delayedOperations.push(subscribeCall);
  173. } else {
  174. subscribeCall();
  175. }
  176. }
  177. }
  178. /**
  179. * Process a new websocket message.
  180. *
  181. * @param msg New message.
  182. */
  183. protected message(msg: MessageEvent) {
  184. const pmsg = JSON.parse(msg.data);
  185. if (pmsg.type == 'init_success') {
  186. // Execute the delayed operations
  187. this.delayedOperations.forEach(c => c());
  188. this.delayedOperations = [];
  189. } else if (pmsg.type == 'subscription_success') {
  190. const callback = this.subscriptionQueue.shift();
  191. if (callback) {
  192. const id = pmsg.id;
  193. this.subscriptionCallbacks[id] = callback;
  194. }
  195. } else if (pmsg.type == 'subscription_data') {
  196. const callback = this.subscriptionCallbacks[pmsg.id];
  197. if (callback) {
  198. callback(pmsg.payload);
  199. }
  200. } else if (pmsg.type == 'subscription_fail') {
  201. console.error(
  202. 'Subscription failed: ',
  203. pmsg.payload.errors.join('; '),
  204. );
  205. }
  206. }
  207. }