@lightsparkdev/core 1.3.1 → 1.4.1

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.
@@ -62,7 +62,7 @@ __export(utils_exports, {
62
62
  isErrorWithMessage: () => isErrorWithMessage,
63
63
  isNode: () => isNode,
64
64
  isNumber: () => isNumber,
65
- isObject: () => isObject2,
65
+ isObject: () => isObject,
66
66
  isRecord: () => isRecord,
67
67
  isSDKCurrencyAmount: () => isSDKCurrencyAmount,
68
68
  isTest: () => isTest,
@@ -974,6 +974,18 @@ function localeToCurrencySymbol(locale) {
974
974
  return symbol;
975
975
  }
976
976
 
977
+ // src/utils/typeGuards.ts
978
+ function isUint8Array(value) {
979
+ return value instanceof Uint8Array;
980
+ }
981
+ function isObject(value) {
982
+ const type = typeof value;
983
+ return value != null && (type == "object" || type == "function");
984
+ }
985
+ function isRecord(value) {
986
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]";
987
+ }
988
+
977
989
  // src/utils/errors.ts
978
990
  var isError = (e) => {
979
991
  return Boolean(
@@ -994,31 +1006,71 @@ var isErrorMsg = (e, msg) => {
994
1006
  }
995
1007
  return false;
996
1008
  };
997
- function errorToJSON(err) {
1009
+ function normalizeObject(obj) {
1010
+ const normalized = {};
1011
+ if (isObject(obj)) {
1012
+ const props = Object.getOwnPropertyNames(obj);
1013
+ for (const prop of props) {
1014
+ const objRecord = obj;
1015
+ normalized[prop] = objRecord[prop];
1016
+ }
1017
+ }
1018
+ return normalized;
1019
+ }
1020
+ function errorToJSON(err, stringifyObjects = false) {
998
1021
  if (!err) {
999
1022
  return null;
1000
1023
  }
1001
- if (typeof err === "object" && "toJSON" in err && typeof err.toJSON === "function") {
1024
+ if (isObject(err) && "toJSON" in err && typeof err.toJSON === "function") {
1025
+ if (stringifyObjects === true) {
1026
+ return objectToJSON(err.toJSON());
1027
+ }
1002
1028
  return err.toJSON();
1003
1029
  }
1004
1030
  if (typeof err === "object" && /* This happens for certain errors like DOMException: */
1005
1031
  Object.getOwnPropertyNames(err).length === 0 && "message" in err && typeof err.message === "string") {
1006
1032
  return { message: err.message };
1007
1033
  }
1034
+ return objectToJSON(err);
1035
+ }
1036
+ function objectToJSON(obj) {
1037
+ const normalizedObj = normalizeObject(obj);
1008
1038
  return JSON.parse(
1009
- JSON.stringify(err, Object.getOwnPropertyNames(err))
1039
+ JSON.stringify(normalizedObj, (key, value) => {
1040
+ if (key === "") {
1041
+ return value;
1042
+ }
1043
+ const objProps = Object.getOwnPropertyNames(normalizedObj);
1044
+ if (!objProps.includes(key)) {
1045
+ return void 0;
1046
+ }
1047
+ if (isObject(value)) {
1048
+ return JSON.stringify(value);
1049
+ }
1050
+ return value;
1051
+ })
1010
1052
  );
1011
1053
  }
1012
1054
 
1013
1055
  // src/utils/localStorage.ts
1014
1056
  function getLocalStorageConfigItem(key) {
1015
- return getLocalStorageBoolean(key);
1057
+ const localStorageBoolean = getLocalStorageBoolean(key);
1058
+ if (localStorageBoolean == null) {
1059
+ return false;
1060
+ }
1061
+ return localStorageBoolean;
1016
1062
  }
1017
1063
  function getLocalStorageBoolean(key) {
1018
1064
  try {
1019
- return localStorage.getItem(key) === "1";
1065
+ if (localStorage.getItem(key) === "1") {
1066
+ return true;
1067
+ } else if (localStorage.getItem(key) == null) {
1068
+ return null;
1069
+ } else {
1070
+ return false;
1071
+ }
1020
1072
  } catch (e) {
1021
- return false;
1073
+ return null;
1022
1074
  }
1023
1075
  }
1024
1076
  function setLocalStorageBoolean(key, value) {
@@ -1092,11 +1144,11 @@ function baseGetTag(value) {
1092
1144
  var baseGetTag_default = baseGetTag;
1093
1145
 
1094
1146
  // ../../node_modules/lodash-es/isObject.js
1095
- function isObject(value) {
1147
+ function isObject2(value) {
1096
1148
  var type = typeof value;
1097
1149
  return value != null && (type == "object" || type == "function");
1098
1150
  }
1099
- var isObject_default = isObject;
1151
+ var isObject_default = isObject2;
1100
1152
 
1101
1153
  // ../../node_modules/lodash-es/isFunction.js
1102
1154
  var asyncTag = "[object AsyncFunction]";
@@ -1160,15 +1212,6 @@ function lsidToUUID(lsid) {
1160
1212
  return lsid.replace(/^[^:]+:(.*)$/, "$1");
1161
1213
  }
1162
1214
 
1163
- // src/utils/typeGuards.ts
1164
- function isUint8Array(value) {
1165
- return value instanceof Uint8Array;
1166
- }
1167
- function isObject2(value) {
1168
- const type = typeof value;
1169
- return value != null && (type == "object" || type == "function");
1170
- }
1171
-
1172
1215
  // src/utils/types.ts
1173
1216
  var isType = (typename) => (node) => {
1174
1217
  return node?.__typename === typename;
@@ -1176,9 +1219,6 @@ var isType = (typename) => (node) => {
1176
1219
  function notNullUndefined(value) {
1177
1220
  return value !== null && value !== void 0;
1178
1221
  }
1179
- function isRecord(value) {
1180
- return typeof value === "object" && value !== null && !Array.isArray(value);
1181
- }
1182
1222
  // Annotate the CommonJS export names for ESM import in node:
1183
1223
  0 && (module.exports = {
1184
1224
  CurrencyUnit,
@@ -1 +1 @@
1
- export { A as AppendUnitsOptions, aa as ById, al as Complete, o as CurrencyAmountArg, k as CurrencyAmountInputObj, m as CurrencyAmountObj, n as CurrencyAmountPreferenceObj, V as CurrencyCodes, T as CurrencyLocales, j as CurrencyMap, f as CurrencyUnit, g as CurrencyUnitType, ae as DeepPartial, D as DeprecatedCurrencyAmountObj, a9 as ExpandRecursively, ad as ExtractByTypename, af as JSONLiteral, ah as JSONObject, ag as JSONType, a8 as Maybe, ai as NN, ab as OmitTypename, ak as PartialBy, am as RequiredKeys, S as SDKCurrencyAmountType, U as UmaCurrency, l as UmaCurrencyAmount, z as abbrCurrencyUnit, b as b64decode, a as b64encode, O as bytesToHex, $ as clamp, i as convertCurrencyAmount, h as convertCurrencyAmountValue, R as countryCodesToCurrencyCodes, c as createSha256Hash, d as defaultCurrencyCode, _ as deleteLocalStorageItem, e as ensureArray, N as errorToJSON, B as formatCurrencyStr, w as getCurrencyAmount, Q as getCurrentLocale, L as getErrorMsg, Y as getLocalStorageBoolean, X as getLocalStorageConfigItem, P as hexToBytes, G as isBrowser, p as isCurrencyAmountInputObj, s as isCurrencyAmountObj, t as isCurrencyAmountPreferenceObj, y as isCurrencyMap, r as isDeprecatedCurrencyAmountObj, J as isError, M as isErrorMsg, K as isErrorWithMessage, H as isNode, a2 as isNumber, a7 as isObject, an as isRecord, v as isSDKCurrencyAmount, I as isTest, ac as isType, a6 as isUint8Array, q as isUmaCurrencyAmount, a0 as linearInterpolate, W as localeToCurrencyCode, F as localeToCurrencySymbol, a5 as lsidToUUID, x as mapCurrencyAmount, aj as notNullUndefined, a3 as pollUntil, a1 as round, E as separateCurrencyStrParts, Z as setLocalStorageBoolean, a4 as sleep, u as urlsafe_b64decode } from '../index-C7dqDM91.cjs';
1
+ export { A as AppendUnitsOptions, ab as ById, am as Complete, o as CurrencyAmountArg, k as CurrencyAmountInputObj, m as CurrencyAmountObj, n as CurrencyAmountPreferenceObj, V as CurrencyCodes, T as CurrencyLocales, j as CurrencyMap, f as CurrencyUnit, g as CurrencyUnitType, af as DeepPartial, D as DeprecatedCurrencyAmountObj, aa as ExpandRecursively, ae as ExtractByTypename, ag as JSONLiteral, ai as JSONObject, ah as JSONType, a9 as Maybe, aj as NN, ac as OmitTypename, al as PartialBy, an as RequiredKeys, S as SDKCurrencyAmountType, U as UmaCurrency, l as UmaCurrencyAmount, z as abbrCurrencyUnit, b as b64decode, a as b64encode, O as bytesToHex, $ as clamp, i as convertCurrencyAmount, h as convertCurrencyAmountValue, R as countryCodesToCurrencyCodes, c as createSha256Hash, d as defaultCurrencyCode, _ as deleteLocalStorageItem, e as ensureArray, N as errorToJSON, B as formatCurrencyStr, w as getCurrencyAmount, Q as getCurrentLocale, L as getErrorMsg, Y as getLocalStorageBoolean, X as getLocalStorageConfigItem, P as hexToBytes, G as isBrowser, p as isCurrencyAmountInputObj, s as isCurrencyAmountObj, t as isCurrencyAmountPreferenceObj, y as isCurrencyMap, r as isDeprecatedCurrencyAmountObj, J as isError, M as isErrorMsg, K as isErrorWithMessage, H as isNode, a2 as isNumber, a7 as isObject, a8 as isRecord, v as isSDKCurrencyAmount, I as isTest, ad as isType, a6 as isUint8Array, q as isUmaCurrencyAmount, a0 as linearInterpolate, W as localeToCurrencyCode, F as localeToCurrencySymbol, a5 as lsidToUUID, x as mapCurrencyAmount, ak as notNullUndefined, a3 as pollUntil, a1 as round, E as separateCurrencyStrParts, Z as setLocalStorageBoolean, a4 as sleep, u as urlsafe_b64decode } from '../index-DZbfPQqd.cjs';
@@ -1 +1 @@
1
- export { A as AppendUnitsOptions, aa as ById, al as Complete, o as CurrencyAmountArg, k as CurrencyAmountInputObj, m as CurrencyAmountObj, n as CurrencyAmountPreferenceObj, V as CurrencyCodes, T as CurrencyLocales, j as CurrencyMap, f as CurrencyUnit, g as CurrencyUnitType, ae as DeepPartial, D as DeprecatedCurrencyAmountObj, a9 as ExpandRecursively, ad as ExtractByTypename, af as JSONLiteral, ah as JSONObject, ag as JSONType, a8 as Maybe, ai as NN, ab as OmitTypename, ak as PartialBy, am as RequiredKeys, S as SDKCurrencyAmountType, U as UmaCurrency, l as UmaCurrencyAmount, z as abbrCurrencyUnit, b as b64decode, a as b64encode, O as bytesToHex, $ as clamp, i as convertCurrencyAmount, h as convertCurrencyAmountValue, R as countryCodesToCurrencyCodes, c as createSha256Hash, d as defaultCurrencyCode, _ as deleteLocalStorageItem, e as ensureArray, N as errorToJSON, B as formatCurrencyStr, w as getCurrencyAmount, Q as getCurrentLocale, L as getErrorMsg, Y as getLocalStorageBoolean, X as getLocalStorageConfigItem, P as hexToBytes, G as isBrowser, p as isCurrencyAmountInputObj, s as isCurrencyAmountObj, t as isCurrencyAmountPreferenceObj, y as isCurrencyMap, r as isDeprecatedCurrencyAmountObj, J as isError, M as isErrorMsg, K as isErrorWithMessage, H as isNode, a2 as isNumber, a7 as isObject, an as isRecord, v as isSDKCurrencyAmount, I as isTest, ac as isType, a6 as isUint8Array, q as isUmaCurrencyAmount, a0 as linearInterpolate, W as localeToCurrencyCode, F as localeToCurrencySymbol, a5 as lsidToUUID, x as mapCurrencyAmount, aj as notNullUndefined, a3 as pollUntil, a1 as round, E as separateCurrencyStrParts, Z as setLocalStorageBoolean, a4 as sleep, u as urlsafe_b64decode } from '../index-C7dqDM91.js';
1
+ export { A as AppendUnitsOptions, ab as ById, am as Complete, o as CurrencyAmountArg, k as CurrencyAmountInputObj, m as CurrencyAmountObj, n as CurrencyAmountPreferenceObj, V as CurrencyCodes, T as CurrencyLocales, j as CurrencyMap, f as CurrencyUnit, g as CurrencyUnitType, af as DeepPartial, D as DeprecatedCurrencyAmountObj, aa as ExpandRecursively, ae as ExtractByTypename, ag as JSONLiteral, ai as JSONObject, ah as JSONType, a9 as Maybe, aj as NN, ac as OmitTypename, al as PartialBy, an as RequiredKeys, S as SDKCurrencyAmountType, U as UmaCurrency, l as UmaCurrencyAmount, z as abbrCurrencyUnit, b as b64decode, a as b64encode, O as bytesToHex, $ as clamp, i as convertCurrencyAmount, h as convertCurrencyAmountValue, R as countryCodesToCurrencyCodes, c as createSha256Hash, d as defaultCurrencyCode, _ as deleteLocalStorageItem, e as ensureArray, N as errorToJSON, B as formatCurrencyStr, w as getCurrencyAmount, Q as getCurrentLocale, L as getErrorMsg, Y as getLocalStorageBoolean, X as getLocalStorageConfigItem, P as hexToBytes, G as isBrowser, p as isCurrencyAmountInputObj, s as isCurrencyAmountObj, t as isCurrencyAmountPreferenceObj, y as isCurrencyMap, r as isDeprecatedCurrencyAmountObj, J as isError, M as isErrorMsg, K as isErrorWithMessage, H as isNode, a2 as isNumber, a7 as isObject, a8 as isRecord, v as isSDKCurrencyAmount, I as isTest, ad as isType, a6 as isUint8Array, q as isUmaCurrencyAmount, a0 as linearInterpolate, W as localeToCurrencyCode, F as localeToCurrencySymbol, a5 as lsidToUUID, x as mapCurrencyAmount, ak as notNullUndefined, a3 as pollUntil, a1 as round, E as separateCurrencyStrParts, Z as setLocalStorageBoolean, a4 as sleep, u as urlsafe_b64decode } from '../index-DZbfPQqd.js';
@@ -50,7 +50,7 @@ import {
50
50
  setLocalStorageBoolean,
51
51
  sleep,
52
52
  urlsafe_b64decode
53
- } from "../chunk-23SS5EX2.js";
53
+ } from "../chunk-NV53XYAS.js";
54
54
  export {
55
55
  CurrencyUnit,
56
56
  abbrCurrencyUnit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightsparkdev/core",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "Lightspark JS SDK",
5
5
  "author": "Lightspark Inc.",
6
6
  "keywords": [
@@ -53,13 +53,13 @@
53
53
  "lint": "eslint .",
54
54
  "package:checks": "yarn publint && yarn attw --pack .",
55
55
  "postversion": "yarn build",
56
- "test": "node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --bail src/**/tests/**/*.test.ts",
56
+ "test-cmd": "node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --bail",
57
+ "test": "yarn test-cmd -- src/**/tests/**/*.test.ts",
57
58
  "types:watch": "tsc-absolute --watch",
58
59
  "types": "tsc"
59
60
  },
60
61
  "license": "Apache-2.0",
61
62
  "dependencies": {
62
- "crypto-browserify": "^3.12.0",
63
63
  "dayjs": "^1.11.7",
64
64
  "graphql": "^16.6.0",
65
65
  "graphql-ws": "^5.11.3",
@@ -5,7 +5,6 @@ import dayjs from "dayjs";
5
5
  import utc from "dayjs/plugin/utc.js";
6
6
  import type { Client as WsClient } from "graphql-ws";
7
7
  import { createClient } from "graphql-ws";
8
- import NodeWebSocket from "ws";
9
8
  import { Observable } from "zen-observable-ts";
10
9
 
11
10
  import type Query from "./Query.js";
@@ -33,7 +32,8 @@ type BodyData = {
33
32
  };
34
33
 
35
34
  class Requester {
36
- private readonly wsClient: WsClient;
35
+ private wsClient: Promise<WsClient>;
36
+ private resolveWsClient: ((value: WsClient) => void) | null = null;
37
37
  constructor(
38
38
  private readonly nodeKeyCache: NodeKeyCache,
39
39
  private readonly schemaEndpoint: string,
@@ -44,22 +44,43 @@ class Requester {
44
44
  private readonly signingKey?: SigningKey,
45
45
  private readonly fetchImpl: typeof fetch = fetch,
46
46
  ) {
47
+ this.wsClient = new Promise<WsClient>((resolve) => {
48
+ this.resolveWsClient = resolve;
49
+ });
50
+ void this.initWsClient(baseUrl, authProvider);
51
+ autoBind(this);
52
+ }
53
+
54
+ private async initWsClient(baseUrl: string, authProvider: AuthProvider) {
55
+ if (!this.resolveWsClient) {
56
+ /* If resolveWsClient is null assume already initialized: */
57
+ return this.wsClient;
58
+ }
59
+
47
60
  let websocketImpl;
48
- if (typeof WebSocket === "undefined" && typeof window === "undefined") {
49
- websocketImpl = NodeWebSocket;
61
+ if (isNode && typeof WebSocket === "undefined") {
62
+ const wsModule = await import("ws");
63
+ websocketImpl = wsModule.default;
50
64
  }
51
65
  let websocketProtocol = "wss";
52
66
  if (baseUrl.startsWith("http://")) {
53
67
  websocketProtocol = "ws";
54
68
  }
55
- this.wsClient = createClient({
69
+
70
+ const wsClient = createClient({
56
71
  url: `${websocketProtocol}://${this.stripProtocol(this.baseUrl)}/${
57
72
  this.schemaEndpoint
58
73
  }`,
59
74
  connectionParams: () => authProvider.addWsConnectionParams({}),
60
75
  webSocketImpl: websocketImpl,
61
76
  });
62
- autoBind(this);
77
+
78
+ if (this.resolveWsClient) {
79
+ this.resolveWsClient(wsClient);
80
+ this.resolveWsClient = null;
81
+ }
82
+
83
+ return wsClient;
63
84
  }
64
85
 
65
86
  public async executeQuery<T>(query: Query<T>): Promise<T | null> {
@@ -106,11 +127,31 @@ class Requester {
106
127
 
107
128
  return new Observable<{ data: T }>((observer) => {
108
129
  logger.trace(`Requester.subscribe observer`, observer);
109
- return this.wsClient.subscribe(bodyData, {
110
- next: (data) => observer.next(data as { data: T }),
111
- error: (err) => observer.error(err),
112
- complete: () => observer.complete(),
113
- });
130
+
131
+ let cleanup: (() => void) | null = null;
132
+ let canceled = false;
133
+
134
+ void (async () => {
135
+ try {
136
+ const wsClient = await this.wsClient;
137
+ if (!canceled) {
138
+ cleanup = wsClient.subscribe(bodyData, {
139
+ next: (data) => observer.next(data as { data: T }),
140
+ error: (err) => observer.error(err),
141
+ complete: () => observer.complete(),
142
+ });
143
+ }
144
+ } catch (err) {
145
+ observer.error(err);
146
+ }
147
+ })();
148
+
149
+ return () => {
150
+ canceled = true;
151
+ if (cleanup) {
152
+ cleanup();
153
+ }
154
+ };
114
155
  });
115
156
  }
116
157
 
@@ -0,0 +1,347 @@
1
+ import { beforeEach, jest } from "@jest/globals";
2
+
3
+ import type { Client as WsClient } from "graphql-ws";
4
+ import type AuthProvider from "../../auth/AuthProvider.js";
5
+ import type { CryptoInterface } from "../../crypto/crypto.js";
6
+ import type NodeKeyCache from "../../crypto/NodeKeyCache.js";
7
+ import type { SigningKey } from "../../crypto/SigningKey.js";
8
+ import { SigningKeyType } from "../../crypto/types.js";
9
+ import LightsparkException from "../../LightsparkException.js";
10
+ import type Query from "../Query.js";
11
+
12
+ /* Mocking ESM modules (when running node with --experimental-vm-modules)
13
+ requires unstable_mockModule, see https://bit.ly/433nRV1 */
14
+ await jest.unstable_mockModule("graphql-ws", () => ({
15
+ __esModule: true,
16
+ createClient: jest.fn(),
17
+ }));
18
+ /* Since Requester uses graphql-ws we need a dynamic import after the above mock */
19
+ const { Requester } = await import("../index.js");
20
+
21
+ describe("Requester", () => {
22
+ const schemaEndpoint = "graphql";
23
+ const sdkUserAgent = "test-agent";
24
+ const baseUrl = "https://api.example.com";
25
+
26
+ let nodeKeyCache: NodeKeyCache;
27
+ let authProvider: AuthProvider;
28
+ let signingKey: SigningKey;
29
+ let cryptoImpl: CryptoInterface;
30
+ let fetchImpl: typeof fetch;
31
+
32
+ beforeEach(() => {
33
+ nodeKeyCache = {
34
+ getKey: jest.fn(),
35
+ hasKey: jest.fn(),
36
+ } as unknown as NodeKeyCache;
37
+
38
+ authProvider = {
39
+ addAuthHeaders: jest.fn(async (headers: Record<string, string>) => ({
40
+ ...headers,
41
+ "X-Test": "1",
42
+ })),
43
+ isAuthorized: jest.fn(async () => true),
44
+ addWsConnectionParams: jest.fn(
45
+ async (params: Record<string, unknown>) => ({
46
+ ...params,
47
+ ws: true,
48
+ }),
49
+ ),
50
+ } satisfies AuthProvider;
51
+
52
+ signingKey = {
53
+ type: SigningKeyType.RSASigningKey,
54
+ sign: jest.fn(async (data: Uint8Array) => new Uint8Array([1, 2, 3])),
55
+ } satisfies SigningKey;
56
+
57
+ cryptoImpl = {
58
+ decryptSecretWithNodePassword: jest.fn(async () => new ArrayBuffer(0)),
59
+ generateSigningKeyPair: jest.fn(async () => ({
60
+ publicKey: "",
61
+ privateKey: "",
62
+ })),
63
+ serializeSigningKey: jest.fn(async () => new ArrayBuffer(0)),
64
+ getNonce: jest.fn(async () => 123),
65
+ sign: jest.fn(async () => new ArrayBuffer(0)),
66
+ importPrivateSigningKey: jest.fn(async () => ""),
67
+ } satisfies CryptoInterface;
68
+
69
+ fetchImpl = jest.fn(
70
+ async () =>
71
+ ({
72
+ ok: true,
73
+ json: async () => ({ data: { foo: "bar" }, errors: undefined }),
74
+ statusText: "OK",
75
+ }) as Response,
76
+ );
77
+ });
78
+
79
+ it("constructs without error", () => {
80
+ expect(
81
+ () =>
82
+ new Requester(
83
+ nodeKeyCache,
84
+ schemaEndpoint,
85
+ sdkUserAgent,
86
+ authProvider,
87
+ baseUrl,
88
+ cryptoImpl,
89
+ signingKey,
90
+ fetchImpl,
91
+ ),
92
+ ).not.toThrow();
93
+ });
94
+
95
+ describe("executeQuery", () => {
96
+ it("calls makeRawRequest and returns constructed object", async () => {
97
+ const requester = new Requester(
98
+ nodeKeyCache,
99
+ schemaEndpoint,
100
+ sdkUserAgent,
101
+ authProvider,
102
+ baseUrl,
103
+ cryptoImpl,
104
+ signingKey,
105
+ fetchImpl,
106
+ );
107
+ const query: Query<{ foo: string }> = {
108
+ queryPayload: "query TestQuery { foo }",
109
+ variables: { a: 1 },
110
+ constructObject: (rawData) => ({
111
+ foo: (rawData as { foo: string }).foo,
112
+ }),
113
+ };
114
+ jest.spyOn(requester, "makeRawRequest").mockResolvedValue({ foo: "bar" });
115
+ const result = await requester.executeQuery(query);
116
+ expect(result).toEqual({ foo: "bar" });
117
+ });
118
+ });
119
+
120
+ describe("makeRawRequest", () => {
121
+ it("makes a successful request and returns data", async () => {
122
+ const requester = new Requester(
123
+ nodeKeyCache,
124
+ schemaEndpoint,
125
+ sdkUserAgent,
126
+ authProvider,
127
+ baseUrl,
128
+ cryptoImpl,
129
+ signingKey,
130
+ fetchImpl,
131
+ );
132
+ const result = await requester.makeRawRequest("query TestQuery { foo }", {
133
+ a: 1,
134
+ });
135
+ expect(result).toEqual({ foo: "bar" });
136
+ expect(fetchImpl).toHaveBeenCalled();
137
+ });
138
+
139
+ it("throws on invalid query", async () => {
140
+ const requester = new Requester(
141
+ nodeKeyCache,
142
+ schemaEndpoint,
143
+ sdkUserAgent,
144
+ authProvider,
145
+ baseUrl,
146
+ cryptoImpl,
147
+ signingKey,
148
+ fetchImpl,
149
+ );
150
+ await expect(requester.makeRawRequest("invalid", {})).rejects.toThrow(
151
+ LightsparkException,
152
+ );
153
+ });
154
+
155
+ it("throws on subscription query", async () => {
156
+ const requester = new Requester(
157
+ nodeKeyCache,
158
+ schemaEndpoint,
159
+ sdkUserAgent,
160
+ authProvider,
161
+ baseUrl,
162
+ cryptoImpl,
163
+ signingKey,
164
+ fetchImpl,
165
+ );
166
+ await expect(
167
+ requester.makeRawRequest("subscription TestSub { foo }", {}),
168
+ ).rejects.toThrow(LightsparkException);
169
+ });
170
+
171
+ it("throws on failed response", async () => {
172
+ fetchImpl = jest.fn(
173
+ async () =>
174
+ ({
175
+ ok: false,
176
+ statusText: "Bad Request",
177
+ json: async () => ({
178
+ errors: [
179
+ { message: "fail", extensions: { error_name: "TestError" } },
180
+ ],
181
+ }),
182
+ }) as Response,
183
+ );
184
+ const requester = new Requester(
185
+ nodeKeyCache,
186
+ schemaEndpoint,
187
+ sdkUserAgent,
188
+ authProvider,
189
+ baseUrl,
190
+ cryptoImpl,
191
+ signingKey,
192
+ fetchImpl,
193
+ );
194
+ await expect(
195
+ requester.makeRawRequest("query TestQuery { foo }", {}),
196
+ ).rejects.toThrow(LightsparkException);
197
+ });
198
+
199
+ it("throws if response has no data and errors", async () => {
200
+ fetchImpl = jest.fn(
201
+ async () =>
202
+ ({
203
+ ok: true,
204
+ json: async () => ({
205
+ data: undefined,
206
+ errors: [
207
+ { message: "fail", extensions: { error_name: "TestError" } },
208
+ ],
209
+ }),
210
+ statusText: "OK",
211
+ }) as Response,
212
+ );
213
+ const requester = new Requester(
214
+ nodeKeyCache,
215
+ schemaEndpoint,
216
+ sdkUserAgent,
217
+ authProvider,
218
+ baseUrl,
219
+ cryptoImpl,
220
+ signingKey,
221
+ fetchImpl,
222
+ );
223
+ await expect(
224
+ requester.makeRawRequest("query TestQuery { foo }", {}),
225
+ ).rejects.toThrow(LightsparkException);
226
+ });
227
+ });
228
+
229
+ describe("subscribe", () => {
230
+ it("throws on mutation query", () => {
231
+ const requester = new Requester(
232
+ nodeKeyCache,
233
+ schemaEndpoint,
234
+ sdkUserAgent,
235
+ authProvider,
236
+ baseUrl,
237
+ cryptoImpl,
238
+ signingKey,
239
+ fetchImpl,
240
+ );
241
+ expect(() =>
242
+ requester.subscribe("mutation TestMutation { foo }"),
243
+ ).toThrow(LightsparkException);
244
+ });
245
+
246
+ it("throws on invalid query", () => {
247
+ const requester = new Requester(
248
+ nodeKeyCache,
249
+ schemaEndpoint,
250
+ sdkUserAgent,
251
+ authProvider,
252
+ baseUrl,
253
+ cryptoImpl,
254
+ signingKey,
255
+ fetchImpl,
256
+ );
257
+ expect(() => requester.subscribe("invalid")).toThrow(LightsparkException);
258
+ });
259
+
260
+ it("returns an Observable for a valid subscription", async () => {
261
+ // Mock wsClient and its subscribe method
262
+ const wsClient = {
263
+ subscribe: jest.fn(
264
+ (
265
+ _body,
266
+ handlers: { next?: (data: unknown) => void; complete?: () => void },
267
+ ) => {
268
+ setTimeout(() => {
269
+ handlers.next?.({ data: { foo: "bar" } });
270
+ handlers.complete?.();
271
+ }, 10);
272
+ return jest.fn();
273
+ },
274
+ ),
275
+ } as unknown as WsClient;
276
+
277
+ const { createClient } = await import("graphql-ws");
278
+ (createClient as jest.Mock).mockReturnValue(wsClient);
279
+
280
+ const requester = new Requester(
281
+ nodeKeyCache,
282
+ schemaEndpoint,
283
+ sdkUserAgent,
284
+ authProvider,
285
+ baseUrl,
286
+ cryptoImpl,
287
+ signingKey,
288
+ fetchImpl,
289
+ );
290
+
291
+ const observable = requester.subscribe<{ foo: string }>(
292
+ "subscription TestSub { foo }",
293
+ );
294
+
295
+ const results: { foo: string }[] = [];
296
+ await new Promise<void>((resolve) => {
297
+ observable.subscribe({
298
+ next: (data) => {
299
+ results.push(data.data);
300
+ },
301
+ complete: () => {
302
+ expect(results).toEqual([{ foo: "bar" }]);
303
+ resolve();
304
+ },
305
+ });
306
+ });
307
+
308
+ expect(wsClient.subscribe).toHaveBeenCalled();
309
+ });
310
+ });
311
+
312
+ describe("signing logic", () => {
313
+ it("adds signing headers if signingNodeId is provided", async () => {
314
+ (nodeKeyCache.getKey as jest.Mock).mockReturnValue(signingKey);
315
+ const requester = new Requester(
316
+ nodeKeyCache,
317
+ schemaEndpoint,
318
+ sdkUserAgent,
319
+ authProvider,
320
+ baseUrl,
321
+ cryptoImpl,
322
+ undefined,
323
+ fetchImpl,
324
+ );
325
+ const spy = jest.spyOn(signingKey, "sign");
326
+ await requester.makeRawRequest("query TestQuery { foo }", {}, "node123");
327
+ expect(spy).toHaveBeenCalled();
328
+ });
329
+
330
+ it("throws if signingKey is missing", async () => {
331
+ (nodeKeyCache.getKey as jest.Mock).mockReturnValue(undefined);
332
+ const requester = new Requester(
333
+ nodeKeyCache,
334
+ schemaEndpoint,
335
+ sdkUserAgent,
336
+ authProvider,
337
+ baseUrl,
338
+ cryptoImpl,
339
+ undefined,
340
+ fetchImpl,
341
+ );
342
+ await expect(
343
+ requester.makeRawRequest("query TestQuery { foo }", {}, "node123"),
344
+ ).rejects.toThrow();
345
+ });
346
+ });
347
+ });