@streamlayer/sdk-web-api 0.22.0 → 0.24.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.
@@ -17,5 +17,7 @@ export declare const $organizationSettings: ($enabled: ReadableAtom<'on' | undef
17
17
  brandDefaults?: import("@streamlayer/sl-eslib/sdkSettings/sdkSettings.common_pb").BrandDefaults | undefined;
18
18
  pub?: import("@streamlayer/sl-eslib/sdkSettings/sdkSettings.common_pb").JWK | undefined;
19
19
  getstream?: import("@streamlayer/sl-eslib/sdkSettings/sdkSettings.common_pb").GetStreamSettingsClient | undefined;
20
+ publicName?: import("@streamlayer/sl-eslib/sdkSettings/sdkSettings.common_pb").PublicName | undefined;
21
+ analyticsVersion?: import("@streamlayer/sl-eslib/sdkSettings/sdkSettings.common_pb").AnalyticsVersion | undefined;
20
22
  } | undefined, any>;
21
23
  export declare const $organizationAdvertising: ($enabled: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/sdkSettings/sdkSettings.common_pb").Advertising | undefined, any>;
@@ -13,10 +13,11 @@ export const $user = ($userToken, transport) => {
13
13
  });
14
14
  };
15
15
  export const bypassLogin = (transport) => {
16
- const { client } = transport.createPromiseClient(Users, { method: 'bypassAuth' });
16
+ const { client, createRequestOptions } = transport.createPromiseClient(Users, { method: 'bypassAuth' });
17
+ const contextValues = createRequestOptions({ retryAttempts: 0 });
17
18
  // inviterKey to do add to payload, ONLY after implement it on backend!
18
19
  return async ({ userKey, schema, init, inviterKey }) => {
19
- const res = await client.bypassAuth({ userKey, schema, init });
20
+ const res = await client.bypassAuth({ userKey, schema, init }, { contextValues });
20
21
  if (res.meta) {
21
22
  res.meta.inviterKey = inviterKey;
22
23
  }
@@ -24,8 +25,9 @@ export const bypassLogin = (transport) => {
24
25
  };
25
26
  };
26
27
  export const bypassAuth = (transport, params) => {
27
- const { client } = transport.createPromiseClient(Users, { method: 'bypassAuth' });
28
- return client.bypassAuth(params);
28
+ const { client, createRequestOptions } = transport.createPromiseClient(Users, { method: 'bypassAuth' });
29
+ const contextValues = createRequestOptions({ retryAttempts: 0 });
30
+ return client.bypassAuth(params, { contextValues });
29
31
  };
30
32
  export const $userSettings = ($userToken, // sl user token
31
33
  transport) => {
@@ -38,6 +40,7 @@ transport) => {
38
40
  });
39
41
  };
40
42
  export const register = (transport, phone) => {
41
- const { client } = transport.createPromiseClient(Users, { method: 'register' });
42
- return client.register({ id: phone });
43
+ const { client, createRequestOptions } = transport.createPromiseClient(Users, { method: 'register' });
44
+ const contextValues = createRequestOptions({ retryAttempts: 0 });
45
+ return client.register({ id: phone }, { contextValues });
43
46
  };
@@ -0,0 +1,49 @@
1
+ import { type Interceptor } from '@connectrpc/connect';
2
+ /**
3
+ * Retry interceptor
4
+ *
5
+ * This interceptor is used to retry requests in case of network errors.
6
+ * Retries are performed according to the exponential backoff algorithm.
7
+ * Allowing retries for the following error codes:
8
+ * [
9
+ * Code.Unknown,
10
+ * Code.Internal,
11
+ * Code.DeadlineExceeded,
12
+ * Code.ResourceExhausted,
13
+ * Code.FailedPrecondition,
14
+ * Code.Unavailable,
15
+ * Code.DataLoss,
16
+ * ]
17
+ *
18
+ * Retry params:
19
+ * - retryAttempts: number of attempts to retry the request, 0 means no retries
20
+ * - retryDelay: max delay between retries in milliseconds
21
+ *
22
+ * Example:
23
+ * ```ts
24
+ const { client, createRequestOptions, queryKey } = transport.createPromiseClient(Leaderboard, {
25
+ method: 'summary',
26
+ params: [$eventId, $userId],
27
+ })
28
+
29
+ return transport.nanoquery.createFetcherStore(queryKey, {
30
+ fetcher: async (_, __, eventId, userId) => {
31
+ const contextValues = createRequestOptions({
32
+ retryAttempts: 5,
33
+ retryDelay: 10000,
34
+ })
35
+
36
+ const res = await client.summary(
37
+ {
38
+ eventId: eventId as unknown as bigint,
39
+ userId: userId as string,
40
+ },
41
+ { contextValues }
42
+ )
43
+
44
+ return res.data?.attributes
45
+ },
46
+ })
47
+ * ```
48
+ */
49
+ export declare const retry: Interceptor;
@@ -0,0 +1,87 @@
1
+ import { createLogger } from '@streamlayer/sdk-web-logger';
2
+ import { Code, ConnectError } from '@connectrpc/connect';
3
+ import { RequestOptionsKeys } from './transport';
4
+ const allowedRetryFor = new Set([
5
+ Code.Unknown,
6
+ Code.Internal,
7
+ Code.DeadlineExceeded,
8
+ Code.ResourceExhausted,
9
+ Code.FailedPrecondition,
10
+ Code.Unavailable,
11
+ Code.DataLoss,
12
+ ]);
13
+ const log = createLogger('grpc:retry');
14
+ /**
15
+ * Retry interceptor
16
+ *
17
+ * This interceptor is used to retry requests in case of network errors.
18
+ * Retries are performed according to the exponential backoff algorithm.
19
+ * Allowing retries for the following error codes:
20
+ * [
21
+ * Code.Unknown,
22
+ * Code.Internal,
23
+ * Code.DeadlineExceeded,
24
+ * Code.ResourceExhausted,
25
+ * Code.FailedPrecondition,
26
+ * Code.Unavailable,
27
+ * Code.DataLoss,
28
+ * ]
29
+ *
30
+ * Retry params:
31
+ * - retryAttempts: number of attempts to retry the request, 0 means no retries
32
+ * - retryDelay: max delay between retries in milliseconds
33
+ *
34
+ * Example:
35
+ * ```ts
36
+ const { client, createRequestOptions, queryKey } = transport.createPromiseClient(Leaderboard, {
37
+ method: 'summary',
38
+ params: [$eventId, $userId],
39
+ })
40
+
41
+ return transport.nanoquery.createFetcherStore(queryKey, {
42
+ fetcher: async (_, __, eventId, userId) => {
43
+ const contextValues = createRequestOptions({
44
+ retryAttempts: 5,
45
+ retryDelay: 10000,
46
+ })
47
+
48
+ const res = await client.summary(
49
+ {
50
+ eventId: eventId as unknown as bigint,
51
+ userId: userId as string,
52
+ },
53
+ { contextValues }
54
+ )
55
+
56
+ return res.data?.attributes
57
+ },
58
+ })
59
+ * ```
60
+ */
61
+ export const retry = (next) => {
62
+ return async (req) => {
63
+ const attempts = req.contextValues.get(RequestOptionsKeys.retryAttempts);
64
+ const minDelay = 300;
65
+ const maxDelay = req.contextValues.get(RequestOptionsKeys.retryDelay);
66
+ if (req.stream || attempts === 0) {
67
+ return next(req);
68
+ }
69
+ log.trace({ url: req.url, attempts, maxDelay }, 'retry options');
70
+ for (let attempt = 0;; attempt++) {
71
+ try {
72
+ return await next(req);
73
+ }
74
+ catch (error) {
75
+ log.trace({ attempt, error }, 'retry attempt');
76
+ const cErr = ConnectError.from(error);
77
+ if (attempt >= attempts || !(cErr instanceof ConnectError) || !allowedRetryFor.has(cErr.code)) {
78
+ throw error;
79
+ }
80
+ // https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/
81
+ const backoff = Math.min(maxDelay, Math.pow(2, attempt) * minDelay);
82
+ const delayMs = Math.round((backoff * (1 + Math.random())) / 2);
83
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
84
+ }
85
+ }
86
+ };
87
+ };
@@ -1,14 +1,15 @@
1
+ import { MapStore } from '@streamlayer/sdk-web-interfaces';
1
2
  import { createRouterTransport, ConnectRouter, Interceptor, PromiseClient, UnaryRequest, StreamRequest } from '@connectrpc/connect';
2
3
  import type { ServiceType, Message, PlainMessage } from '@bufbuild/protobuf';
3
4
  import { createGrpcWebTransport } from '@connectrpc/connect-web';
4
- import type { KeyInput } from '@nanostores/query';
5
- import { nanoquery } from '@nanostores/query';
5
+ import { type KeyInput, nanoquery } from '@nanostores/query';
6
6
  import { Atom } from 'nanostores';
7
7
  import { ServerStreamSubscription, type ServerStreamSubscriptionOptions } from './subscription';
8
8
  type KnownHeaders = {
9
9
  authorization?: string;
10
10
  sdk?: string;
11
11
  'sl-device-id': string;
12
+ 'sl-device-os': string;
12
13
  'sl-user-id'?: string;
13
14
  } & Record<string, string>;
14
15
  export type GrpcTransport = Transport['transport'];
@@ -25,6 +26,12 @@ type NanoqueryObjectType = {
25
26
  createMutatorStore: NanoqueryReturnType[1];
26
27
  utils: NanoqueryReturnType[2];
27
28
  };
29
+ export declare const RequestOptionsKeys: {
30
+ retryAttempts: import("@connectrpc/connect").ContextKey<number>;
31
+ retryDelay: import("@connectrpc/connect").ContextKey<number>;
32
+ };
33
+ type RequestOptionsKey = keyof typeof RequestOptionsKeys;
34
+ type RequestOptions = Partial<Record<RequestOptionsKey, (typeof RequestOptionsKeys)[RequestOptionsKey]['defaultValue']>>;
28
35
  /**
29
36
  * transport wrapper, initialize grpc transport, store headers and connect interceptors
30
37
  */
@@ -38,10 +45,10 @@ export declare class Transport {
38
45
  readonly streamTransport: ReturnType<typeof createGrpcWebTransport>;
39
46
  readonly nanoquery: NanoqueryObjectType;
40
47
  readonly host: string;
48
+ readonly $headers: MapStore<KnownHeaders>;
41
49
  protected interceptors: Interceptor[];
42
- private readonly $headers;
43
50
  private clients;
44
- private callbackClients;
51
+ private streamClients;
45
52
  private subscriptions;
46
53
  constructor(host: string);
47
54
  addSubscription: <T extends ServiceType, Req extends Message<Req>, Res extends Message<Res>>(method: { [P in keyof PromiseClient<T> as T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? P : never]: T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? PromiseClient<T>[P] : never; }[keyof { [P in keyof PromiseClient<T> as T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? P : never]: T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? PromiseClient<T>[P] : never; }], params: PlainMessage<Req> | Atom<PlainMessage<Req>>, options: ServerStreamSubscriptionOptions) => ServerStreamSubscription<ServiceType, Message<import("@bufbuild/protobuf").AnyMessage>, Message<import("@bufbuild/protobuf").AnyMessage>, never, never> | ServerStreamSubscription<T, Req, Res, keyof { [P in keyof PromiseClient<T> as T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? P : never]: T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? PromiseClient<T>[P] : never; }, { [P in keyof PromiseClient<T> as T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? P : never]: T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? PromiseClient<T>[P] : never; }[keyof { [P in keyof PromiseClient<T> as T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? P : never]: T["methods"][P] extends import("@bufbuild/protobuf").MethodInfoServerStreaming<any, any> ? PromiseClient<T>[P] : never; }]>;
@@ -56,6 +63,7 @@ export declare class Transport {
56
63
  method: keyof T["methods"];
57
64
  }) => {
58
65
  client: PromiseClient<T>;
66
+ createRequestOptions: (options: RequestOptions) => import("@connectrpc/connect").ContextValues;
59
67
  queryKey: ((string | number | true) | import("nanostores").ReadableAtom<(string | number | true) | (false | void | null | undefined)> | import("@nanostores/query").FetcherStore<any, any>)[];
60
68
  queryKeyStr: string;
61
69
  };
@@ -1,10 +1,15 @@
1
1
  import { MapStore, createMapStore } from '@streamlayer/sdk-web-interfaces';
2
2
  import { nanoid } from 'nanoid';
3
- import { createRouterTransport, createPromiseClient, } from '@connectrpc/connect';
3
+ import { createRouterTransport, createPromiseClient, createContextValues, createContextKey, } from '@connectrpc/connect';
4
4
  import { createGrpcWebTransport } from '@connectrpc/connect-web';
5
5
  import { nanoquery } from '@nanostores/query';
6
6
  import { __GRPC_DEVTOOLS_EXTENSION__ } from '../utils/devtools';
7
7
  import { ServerStreamSubscription } from './subscription';
8
+ import { retry } from './retry';
9
+ export const RequestOptionsKeys = {
10
+ retryAttempts: createContextKey(5, { description: 'Number of attempts to retry' }),
11
+ retryDelay: createContextKey(30000, { description: 'Max delay between retries in milliseconds' }),
12
+ };
8
13
  // generate random device id and store it in local storage
9
14
  const getDeviceId = () => {
10
15
  const deviceId = localStorage.getItem('sl-device-id');
@@ -15,6 +20,11 @@ const getDeviceId = () => {
15
20
  localStorage.setItem('sl-device-id', newDeviceId);
16
21
  return newDeviceId;
17
22
  };
23
+ const getDeviceOs = () => {
24
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
25
+ // @ts-ignore
26
+ return navigator.userAgentData?.platform || navigator.platform;
27
+ };
18
28
  /**
19
29
  * transport wrapper, initialize grpc transport, store headers and connect interceptors
20
30
  */
@@ -30,25 +40,29 @@ export class Transport {
30
40
  streamTransport;
31
41
  nanoquery;
32
42
  host;
33
- interceptors = [];
34
43
  $headers;
44
+ interceptors = [];
35
45
  clients;
36
- callbackClients;
46
+ streamClients;
37
47
  subscriptions;
38
48
  constructor(host) {
39
49
  this.host = host;
40
50
  this.$headers = new MapStore(createMapStore({
41
51
  ['sl-device-id']: getDeviceId(),
52
+ ['sl-device-os']: getDeviceOs(),
42
53
  }), 'transport:headers');
43
54
  this.initInterceptors();
44
55
  this.clients = new Map();
45
- this.callbackClients = new Map();
56
+ this.streamClients = new Map();
46
57
  this.subscriptions = new Map();
47
- const [createFetcherStore, createMutatorStore, utils] = nanoquery();
58
+ const [createFetcherStore, createMutatorStore, utils] = nanoquery({
59
+ dedupeTime: 1000 * 60 * 5,
60
+ refetchInterval: 1000 * 60 * 5,
61
+ });
48
62
  this.nanoquery = { createFetcherStore, createMutatorStore, utils };
49
63
  this.transport = createGrpcWebTransport({
50
64
  baseUrl: host,
51
- defaultTimeoutMs: 30000,
65
+ defaultTimeoutMs: 10000,
52
66
  interceptors: this.interceptors,
53
67
  useBinaryFormat: true,
54
68
  });
@@ -84,7 +98,7 @@ export class Transport {
84
98
  this.subscriptions.delete(params);
85
99
  }
86
100
  this.clients.clear();
87
- this.callbackClients.clear();
101
+ this.streamClients.clear();
88
102
  this.subscriptions.clear();
89
103
  // ToDo: reset nanoquery instance, basically we should add connect
90
104
  // method where setup nanoquery, transports and interceptors, not in constructor
@@ -127,7 +141,15 @@ export class Transport {
127
141
  ...(Array.isArray(params) ? params : [params]),
128
142
  ];
129
143
  const queryKeyWithoutParams = [service.typeName, methodName.charAt(0).toLowerCase() + methodName.slice(1)];
130
- return { client, queryKey, queryKeyStr: queryKeyWithoutParams.join('') };
144
+ const createRequestOptions = (options) => {
145
+ const contextValues = createContextValues();
146
+ for (const option in options) {
147
+ const contextKey = RequestOptionsKeys[option];
148
+ contextValues.set(contextKey, options[option]);
149
+ }
150
+ return contextValues;
151
+ };
152
+ return { client, createRequestOptions, queryKey, queryKeyStr: queryKeyWithoutParams.join('') };
131
153
  };
132
154
  // create promise client, used for server stream subscriptions
133
155
  createStreamClient = (service) => {
@@ -159,6 +181,7 @@ export class Transport {
159
181
  if (process.env.NODE_ENV !== 'test') {
160
182
  this.interceptors.push(__GRPC_DEVTOOLS_EXTENSION__());
161
183
  }
184
+ this.interceptors.push(retry);
162
185
  // if (window.__GRPC_DEVTOOLS_EXTENSION__) {
163
186
  // this.interceptors.push(window.__GRPC_DEVTOOLS_EXTENSION__())
164
187
  // } else {
package/lib/index.d.ts CHANGED
@@ -12,10 +12,12 @@ declare module '@streamlayer/sdk-web-interfaces' {
12
12
  }
13
13
  interface StreamLayerContext {
14
14
  transport: Transport;
15
+ analyticsTransport: Transport;
15
16
  }
16
17
  }
17
18
  export declare const transport: (instance: StreamLayerContext, opts: {
18
19
  sdkKey: string;
19
20
  host: string;
21
+ analyticsHost: string;
20
22
  version?: string;
21
23
  }, done: () => void) => void;
package/lib/index.js CHANGED
@@ -3,8 +3,18 @@ import { Transport } from './grpc/transport';
3
3
  export * as queries from './grpc/queries';
4
4
  export const transport = (instance, opts, done) => {
5
5
  instance.transport = new Transport(opts.host);
6
+ instance.analyticsTransport = new Transport(opts.analyticsHost);
6
7
  instance.sdk.host = instance.transport.host;
7
8
  instance.transport.setSdkKey(opts.sdkKey);
8
9
  instance.transport.setHeader('sl-sdk-version', opts.version || '-');
10
+ instance.transport.$headers.subscribe((headers) => {
11
+ instance.analyticsTransport.$headers.getStore().set(headers);
12
+ });
13
+ instance.sdk.onMount(() => {
14
+ return () => {
15
+ instance.transport.disconnect();
16
+ instance.analyticsTransport.disconnect();
17
+ };
18
+ });
9
19
  done();
10
20
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamlayer/sdk-web-api",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.js",
6
6
  "typings": "./lib/index.d.ts",
@@ -21,17 +21,18 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@streamlayer/sdk-web-interfaces": "^0.20.5"
24
+ "@streamlayer/sdk-web-interfaces": "^0.21.0",
25
+ "@streamlayer/sdk-web-logger": "^0.5.18"
25
26
  },
26
27
  "devDependencies": {
27
- "@bufbuild/protobuf": "^1.6.0",
28
+ "@bufbuild/protobuf": "^1.7.2",
28
29
  "@connectrpc/connect": "^1.3.0",
29
30
  "@connectrpc/connect-web": "^1.3.0",
30
- "@nanostores/query": "^0.2.9",
31
- "@streamlayer/sl-eslib": "^5.67.0",
32
- "@swc/helpers": "^0.5.3",
31
+ "@nanostores/query": "^0.2.10",
32
+ "@streamlayer/sl-eslib": "^5.83.1",
33
+ "@swc/helpers": "~0.5.3",
33
34
  "nanoid": "3.3.7",
34
- "nanostores": "^0.9.5",
35
+ "nanostores": "^0.10.0",
35
36
  "tslib": "^2.6.2"
36
37
  }
37
38
  }