@lightsparkdev/core 1.4.3 → 1.4.4

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.
@@ -0,0 +1,46 @@
1
+ import type AuthProvider from "../auth/AuthProvider.js";
2
+ import { isBare, isNode } from "../utils/environment.js";
3
+ import Requester from "./Requester.js";
4
+
5
+ export class DefaultRequester extends Requester {
6
+ protected async initWsClient(baseUrl: string, authProvider: AuthProvider) {
7
+ if (!this.resolveWsClient) {
8
+ /* If resolveWsClient is null assume already initialized: */
9
+ return this.wsClient;
10
+ }
11
+
12
+ if (isBare) {
13
+ /* graphql-ws library is currently not supported in Bare environment, see LIG-7942 */
14
+ return null;
15
+ }
16
+
17
+ let websocketImpl;
18
+ if (isNode && typeof WebSocket === "undefined") {
19
+ const wsModule = await import("ws");
20
+ websocketImpl = wsModule.default;
21
+ }
22
+ let websocketProtocol = "wss";
23
+ if (baseUrl.startsWith("http://")) {
24
+ websocketProtocol = "ws";
25
+ }
26
+
27
+ const graphqlWsModule = await import("graphql-ws");
28
+ const { createClient } = graphqlWsModule;
29
+
30
+ const wsClient = createClient({
31
+ url: `${websocketProtocol}://${this.stripProtocol(this.baseUrl)}/${
32
+ this.schemaEndpoint
33
+ }`,
34
+ connectionParams: () => authProvider.addWsConnectionParams({}),
35
+ webSocketImpl: websocketImpl,
36
+ });
37
+
38
+ if (this.resolveWsClient) {
39
+ this.resolveWsClient(wsClient);
40
+ this.resolveWsClient = null;
41
+ }
42
+
43
+ return wsClient;
44
+ }
45
+ }
46
+ export default DefaultRequester;
@@ -17,7 +17,7 @@ import type { SigningKey } from "../crypto/SigningKey.js";
17
17
  import LightsparkException from "../LightsparkException.js";
18
18
  import { logger } from "../Logger.js";
19
19
  import { b64encode } from "../utils/base64.js";
20
- import { isBare, isNode } from "../utils/environment.js";
20
+ import { isNode } from "../utils/environment.js";
21
21
 
22
22
  const DEFAULT_BASE_URL = "api.lightspark.com";
23
23
  dayjs.extend(utc);
@@ -31,14 +31,14 @@ type BodyData = {
31
31
  };
32
32
 
33
33
  class Requester {
34
- private wsClient: Promise<WsClient | null>;
35
- private resolveWsClient: ((value: WsClient | null) => void) | null = null;
34
+ protected wsClient: Promise<WsClient | null>;
35
+ protected resolveWsClient: ((value: WsClient | null) => void) | null = null;
36
36
  constructor(
37
37
  private readonly nodeKeyCache: NodeKeyCache,
38
- private readonly schemaEndpoint: string,
38
+ protected readonly schemaEndpoint: string,
39
39
  private readonly sdkUserAgent: string,
40
40
  private readonly authProvider: AuthProvider = new StubAuthProvider(),
41
- private readonly baseUrl: string = DEFAULT_BASE_URL,
41
+ protected readonly baseUrl: string = DEFAULT_BASE_URL,
42
42
  private readonly cryptoImpl: CryptoInterface = DefaultCrypto,
43
43
  private readonly signingKey?: SigningKey,
44
44
  private readonly fetchImpl: typeof fetch = fetch,
@@ -50,44 +50,11 @@ class Requester {
50
50
  autoBind(this);
51
51
  }
52
52
 
53
- private async initWsClient(baseUrl: string, authProvider: AuthProvider) {
54
- if (!this.resolveWsClient) {
55
- /* If resolveWsClient is null assume already initialized: */
56
- return this.wsClient;
57
- }
58
-
59
- if (isBare) {
60
- /* graphql-ws library is currently not supported in Bare environment, see LIG-7942 */
61
- return null;
62
- }
63
-
64
- let websocketImpl;
65
- if (isNode && typeof WebSocket === "undefined") {
66
- const wsModule = await import("ws");
67
- websocketImpl = wsModule.default;
68
- }
69
- let websocketProtocol = "wss";
70
- if (baseUrl.startsWith("http://")) {
71
- websocketProtocol = "ws";
72
- }
73
-
74
- const graphqlWsModule = await import("graphql-ws");
75
- const { createClient } = graphqlWsModule;
76
-
77
- const wsClient = createClient({
78
- url: `${websocketProtocol}://${this.stripProtocol(this.baseUrl)}/${
79
- this.schemaEndpoint
80
- }`,
81
- connectionParams: () => authProvider.addWsConnectionParams({}),
82
- webSocketImpl: websocketImpl,
83
- });
84
-
85
- if (this.resolveWsClient) {
86
- this.resolveWsClient(wsClient);
87
- this.resolveWsClient = null;
88
- }
89
-
90
- return wsClient;
53
+ protected initWsClient(
54
+ baseUrl: string,
55
+ authProvider: AuthProvider,
56
+ ): Promise<WsClient | null> {
57
+ return Promise.resolve(null);
91
58
  }
92
59
 
93
60
  public async executeQuery<T>(query: Query<T>): Promise<T | null> {
@@ -293,7 +260,7 @@ class Requester {
293
260
  return `${this.sdkUserAgent} ${platform}/${platformVersion}`;
294
261
  }
295
262
 
296
- private stripProtocol(url: string): string {
263
+ protected stripProtocol(url: string): string {
297
264
  return url.replace(/.*?:\/\//g, "");
298
265
  }
299
266
 
@@ -0,0 +1,129 @@
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
+
10
+ /* Mocking ESM modules (when running node with --experimental-vm-modules)
11
+ requires unstable_mockModule, see https://bit.ly/433nRV1 */
12
+ await jest.unstable_mockModule("graphql-ws", () => ({
13
+ __esModule: true,
14
+ createClient: jest.fn(),
15
+ }));
16
+ /* Since Requester uses graphql-ws we need a dynamic import after the above mock */
17
+ const { DefaultRequester } = await import("../DefaultRequester.js");
18
+
19
+ describe("DefaultRequester", () => {
20
+ const schemaEndpoint = "graphql";
21
+ const sdkUserAgent = "test-agent";
22
+ const baseUrl = "https://api.example.com";
23
+
24
+ let nodeKeyCache: NodeKeyCache;
25
+ let authProvider: AuthProvider;
26
+ let signingKey: SigningKey;
27
+ let cryptoImpl: CryptoInterface;
28
+ let fetchImpl: typeof fetch;
29
+
30
+ beforeEach(() => {
31
+ nodeKeyCache = {
32
+ getKey: jest.fn(),
33
+ hasKey: jest.fn(),
34
+ } as unknown as NodeKeyCache;
35
+
36
+ authProvider = {
37
+ addAuthHeaders: jest.fn(async (headers: Record<string, string>) => ({
38
+ ...headers,
39
+ "X-Test": "1",
40
+ })),
41
+ isAuthorized: jest.fn(async () => true),
42
+ addWsConnectionParams: jest.fn(
43
+ async (params: Record<string, unknown>) => ({
44
+ ...params,
45
+ ws: true,
46
+ }),
47
+ ),
48
+ } satisfies AuthProvider;
49
+
50
+ signingKey = {
51
+ type: SigningKeyType.RSASigningKey,
52
+ sign: jest.fn(async (data: Uint8Array) => new Uint8Array([1, 2, 3])),
53
+ } satisfies SigningKey;
54
+
55
+ cryptoImpl = {
56
+ decryptSecretWithNodePassword: jest.fn(async () => new ArrayBuffer(0)),
57
+ generateSigningKeyPair: jest.fn(async () => ({
58
+ publicKey: "",
59
+ privateKey: "",
60
+ })),
61
+ serializeSigningKey: jest.fn(async () => new ArrayBuffer(0)),
62
+ getNonce: jest.fn(async () => 123),
63
+ sign: jest.fn(async () => new ArrayBuffer(0)),
64
+ importPrivateSigningKey: jest.fn(async () => ""),
65
+ } satisfies CryptoInterface;
66
+
67
+ fetchImpl = jest.fn(
68
+ async () =>
69
+ ({
70
+ ok: true,
71
+ json: async () => ({ data: { foo: "bar" }, errors: undefined }),
72
+ statusText: "OK",
73
+ }) as Response,
74
+ );
75
+ });
76
+
77
+ describe("subscribe", () => {
78
+ it("returns an Observable for a valid subscription", async () => {
79
+ // Mock wsClient and its subscribe method
80
+ const wsClient = {
81
+ subscribe: jest.fn(
82
+ (
83
+ _body,
84
+ handlers: { next?: (data: unknown) => void; complete?: () => void },
85
+ ) => {
86
+ setTimeout(() => {
87
+ handlers.next?.({ data: { foo: "bar" } });
88
+ handlers.complete?.();
89
+ }, 10);
90
+ return jest.fn();
91
+ },
92
+ ),
93
+ } as unknown as WsClient;
94
+
95
+ const { createClient } = await import("graphql-ws");
96
+ (createClient as jest.Mock).mockReturnValue(wsClient);
97
+
98
+ const requester = new DefaultRequester(
99
+ nodeKeyCache,
100
+ schemaEndpoint,
101
+ sdkUserAgent,
102
+ authProvider,
103
+ baseUrl,
104
+ cryptoImpl,
105
+ signingKey,
106
+ fetchImpl,
107
+ );
108
+
109
+ const observable = requester.subscribe<{ foo: string }>(
110
+ "subscription TestSub { foo }",
111
+ );
112
+
113
+ const results: { foo: string }[] = [];
114
+ await new Promise<void>((resolve) => {
115
+ observable.subscribe({
116
+ next: (data: { data: { foo: string } }) => {
117
+ results.push(data.data);
118
+ },
119
+ complete: () => {
120
+ expect(results).toEqual([{ foo: "bar" }]);
121
+ resolve();
122
+ },
123
+ });
124
+ });
125
+
126
+ expect(wsClient.subscribe).toHaveBeenCalled();
127
+ });
128
+ });
129
+ });
@@ -1,6 +1,5 @@
1
1
  import { beforeEach, jest } from "@jest/globals";
2
2
 
3
- import type { Client as WsClient } from "graphql-ws";
4
3
  import type AuthProvider from "../../auth/AuthProvider.js";
5
4
  import type { CryptoInterface } from "../../crypto/crypto.js";
6
5
  import type NodeKeyCache from "../../crypto/NodeKeyCache.js";
@@ -16,7 +15,7 @@ await jest.unstable_mockModule("graphql-ws", () => ({
16
15
  createClient: jest.fn(),
17
16
  }));
18
17
  /* Since Requester uses graphql-ws we need a dynamic import after the above mock */
19
- const { Requester } = await import("../index.js");
18
+ const { default: Requester } = await import("../Requester.js");
20
19
 
21
20
  describe("Requester", () => {
22
21
  const schemaEndpoint = "graphql";
@@ -257,26 +256,7 @@ describe("Requester", () => {
257
256
  expect(() => requester.subscribe("invalid")).toThrow(LightsparkException);
258
257
  });
259
258
 
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
-
259
+ it("emits error when wsClient is not initialized", async () => {
280
260
  const requester = new Requester(
281
261
  nodeKeyCache,
282
262
  schemaEndpoint,
@@ -287,25 +267,36 @@ describe("Requester", () => {
287
267
  signingKey,
288
268
  fetchImpl,
289
269
  );
270
+ // Resolve internal wsClient promise to null so the observable emits an error.
271
+ (
272
+ requester as unknown as {
273
+ resolveWsClient: ((v: unknown) => void) | null;
274
+ }
275
+ ).resolveWsClient?.(null);
290
276
 
291
- const observable = requester.subscribe<{ foo: string }>(
292
- "subscription TestSub { foo }",
293
- );
277
+ const observable = requester.subscribe("subscription TestSub { foo }");
294
278
 
295
- const results: { foo: string }[] = [];
296
279
  await new Promise<void>((resolve) => {
297
280
  observable.subscribe({
298
- next: (data) => {
299
- results.push(data.data);
281
+ next: () => {
282
+ throw new Error(
283
+ "Should not emit next when wsClient is uninitialized",
284
+ );
300
285
  },
301
- complete: () => {
302
- expect(results).toEqual([{ foo: "bar" }]);
286
+ error: (err) => {
287
+ expect(err).toBeInstanceOf(LightsparkException);
288
+ expect(String((err as Error).message)).toMatch(
289
+ /WebSocket client is not initialized/,
290
+ );
303
291
  resolve();
304
292
  },
293
+ complete: () => {
294
+ throw new Error(
295
+ "Should not complete when wsClient is uninitialized",
296
+ );
297
+ },
305
298
  });
306
299
  });
307
-
308
- expect(wsClient.subscribe).toHaveBeenCalled();
309
300
  });
310
301
  });
311
302
 
package/src/shared.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./auth/index.js";
2
+ export * from "./constants/index.js";
3
+ export * from "./crypto/index.js";
4
+ export { default as LightsparkException } from "./LightsparkException.js";
5
+ export { Logger, LoggingLevel, logger } from "./Logger.js";
6
+ export {
7
+ default as ServerEnvironment,
8
+ apiDomainForEnvironment,
9
+ } from "./ServerEnvironment.js";
10
+ export * from "./utils/index.js";
@@ -14,3 +14,6 @@ export const isTest = isNode && process.env.NODE_ENV === "test";
14
14
 
15
15
  /* https://github.com/holepunchto/which-runtime/blob/main/index.js */
16
16
  export const isBare = typeof Bare !== "undefined";
17
+
18
+ export const isReactNative =
19
+ typeof navigator !== "undefined" && navigator.product === "ReactNative";