@streamlayer/sdk-web-api 0.21.2 → 0.23.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' });
17
- // inviterKey to do add to payload
16
+ const { client, createRequestOptions } = transport.createPromiseClient(Users, { method: 'bypassAuth' });
17
+ const contextValues = createRequestOptions({ retryAttempts: 0 });
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
+ };
@@ -25,6 +25,12 @@ type NanoqueryObjectType = {
25
25
  createMutatorStore: NanoqueryReturnType[1];
26
26
  utils: NanoqueryReturnType[2];
27
27
  };
28
+ export declare const RequestOptionsKeys: {
29
+ retryAttempts: import("@connectrpc/connect").ContextKey<number>;
30
+ retryDelay: import("@connectrpc/connect").ContextKey<number>;
31
+ };
32
+ type RequestOptionsKey = keyof typeof RequestOptionsKeys;
33
+ type RequestOptions = Partial<Record<RequestOptionsKey, (typeof RequestOptionsKeys)[RequestOptionsKey]['defaultValue']>>;
28
34
  /**
29
35
  * transport wrapper, initialize grpc transport, store headers and connect interceptors
30
36
  */
@@ -56,6 +62,7 @@ export declare class Transport {
56
62
  method: keyof T["methods"];
57
63
  }) => {
58
64
  client: PromiseClient<T>;
65
+ createRequestOptions: (options: RequestOptions) => import("@connectrpc/connect").ContextValues;
59
66
  queryKey: ((string | number | true) | import("nanostores").ReadableAtom<(string | number | true) | (false | void | null | undefined)> | import("@nanostores/query").FetcherStore<any, any>)[];
60
67
  queryKeyStr: string;
61
68
  };
@@ -1,9 +1,26 @@
1
1
  import { MapStore, createMapStore } from '@streamlayer/sdk-web-interfaces';
2
+ import { nanoid } from 'nanoid';
2
3
  import { createRouterTransport, createPromiseClient, } from '@connectrpc/connect';
3
4
  import { createGrpcWebTransport } from '@connectrpc/connect-web';
5
+ import { createContextValues, createContextKey } from '@connectrpc/connect';
4
6
  import { nanoquery } from '@nanostores/query';
5
7
  import { __GRPC_DEVTOOLS_EXTENSION__ } from '../utils/devtools';
6
8
  import { ServerStreamSubscription } from './subscription';
9
+ import { retry } from './retry';
10
+ export const RequestOptionsKeys = {
11
+ retryAttempts: createContextKey(5, { description: 'Number of attempts to retry' }),
12
+ retryDelay: createContextKey(30000, { description: 'Max delay between retries in milliseconds' }),
13
+ };
14
+ // generate random device id and store it in local storage
15
+ const getDeviceId = () => {
16
+ const deviceId = localStorage.getItem('sl-device-id');
17
+ if (deviceId) {
18
+ return deviceId;
19
+ }
20
+ const newDeviceId = nanoid();
21
+ localStorage.setItem('sl-device-id', newDeviceId);
22
+ return newDeviceId;
23
+ };
7
24
  /**
8
25
  * transport wrapper, initialize grpc transport, store headers and connect interceptors
9
26
  */
@@ -27,7 +44,7 @@ export class Transport {
27
44
  constructor(host) {
28
45
  this.host = host;
29
46
  this.$headers = new MapStore(createMapStore({
30
- ['sl-device-id']: process?.env?.NX_DEVICE_ID || 'sdk-web-dev',
47
+ ['sl-device-id']: getDeviceId(),
31
48
  }), 'transport:headers');
32
49
  this.initInterceptors();
33
50
  this.clients = new Map();
@@ -37,7 +54,7 @@ export class Transport {
37
54
  this.nanoquery = { createFetcherStore, createMutatorStore, utils };
38
55
  this.transport = createGrpcWebTransport({
39
56
  baseUrl: host,
40
- defaultTimeoutMs: 30000,
57
+ defaultTimeoutMs: 10000,
41
58
  interceptors: this.interceptors,
42
59
  useBinaryFormat: true,
43
60
  });
@@ -116,7 +133,15 @@ export class Transport {
116
133
  ...(Array.isArray(params) ? params : [params]),
117
134
  ];
118
135
  const queryKeyWithoutParams = [service.typeName, methodName.charAt(0).toLowerCase() + methodName.slice(1)];
119
- return { client, queryKey, queryKeyStr: queryKeyWithoutParams.join('') };
136
+ const createRequestOptions = (options) => {
137
+ const contextValues = createContextValues();
138
+ for (const option in options) {
139
+ const contextKey = RequestOptionsKeys[option];
140
+ contextValues.set(contextKey, options[option]);
141
+ }
142
+ return contextValues;
143
+ };
144
+ return { client, createRequestOptions, queryKey, queryKeyStr: queryKeyWithoutParams.join('') };
120
145
  };
121
146
  // create promise client, used for server stream subscriptions
122
147
  createStreamClient = (service) => {
@@ -148,6 +173,7 @@ export class Transport {
148
173
  if (process.env.NODE_ENV !== 'test') {
149
174
  this.interceptors.push(__GRPC_DEVTOOLS_EXTENSION__());
150
175
  }
176
+ this.interceptors.push(retry);
151
177
  // if (window.__GRPC_DEVTOOLS_EXTENSION__) {
152
178
  // this.interceptors.push(window.__GRPC_DEVTOOLS_EXTENSION__())
153
179
  // } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamlayer/sdk-web-api",
3
- "version": "0.21.2",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.js",
6
6
  "typings": "./lib/index.d.ts",
@@ -21,15 +21,17 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@streamlayer/sdk-web-interfaces": "^0.20.4"
24
+ "@streamlayer/sdk-web-interfaces": "^0.20.6",
25
+ "@streamlayer/sdk-web-logger": "^0.5.17"
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
31
  "@nanostores/query": "^0.2.9",
31
- "@streamlayer/sl-eslib": "^5.67.0",
32
+ "@streamlayer/sl-eslib": "^5.79.3",
32
33
  "@swc/helpers": "^0.5.3",
34
+ "nanoid": "3.3.7",
33
35
  "nanostores": "^0.9.5",
34
36
  "tslib": "^2.6.2"
35
37
  }