@streamlayer/sdk-web-api 0.22.0 → 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' });
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
+ };
@@ -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
  };
@@ -2,9 +2,15 @@ import { MapStore, createMapStore } from '@streamlayer/sdk-web-interfaces';
2
2
  import { nanoid } from 'nanoid';
3
3
  import { createRouterTransport, createPromiseClient, } from '@connectrpc/connect';
4
4
  import { createGrpcWebTransport } from '@connectrpc/connect-web';
5
+ import { createContextValues, createContextKey } from '@connectrpc/connect';
5
6
  import { nanoquery } from '@nanostores/query';
6
7
  import { __GRPC_DEVTOOLS_EXTENSION__ } from '../utils/devtools';
7
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
+ };
8
14
  // generate random device id and store it in local storage
9
15
  const getDeviceId = () => {
10
16
  const deviceId = localStorage.getItem('sl-device-id');
@@ -48,7 +54,7 @@ export class Transport {
48
54
  this.nanoquery = { createFetcherStore, createMutatorStore, utils };
49
55
  this.transport = createGrpcWebTransport({
50
56
  baseUrl: host,
51
- defaultTimeoutMs: 30000,
57
+ defaultTimeoutMs: 10000,
52
58
  interceptors: this.interceptors,
53
59
  useBinaryFormat: true,
54
60
  });
@@ -127,7 +133,15 @@ export class Transport {
127
133
  ...(Array.isArray(params) ? params : [params]),
128
134
  ];
129
135
  const queryKeyWithoutParams = [service.typeName, methodName.charAt(0).toLowerCase() + methodName.slice(1)];
130
- 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('') };
131
145
  };
132
146
  // create promise client, used for server stream subscriptions
133
147
  createStreamClient = (service) => {
@@ -159,6 +173,7 @@ export class Transport {
159
173
  if (process.env.NODE_ENV !== 'test') {
160
174
  this.interceptors.push(__GRPC_DEVTOOLS_EXTENSION__());
161
175
  }
176
+ this.interceptors.push(retry);
162
177
  // if (window.__GRPC_DEVTOOLS_EXTENSION__) {
163
178
  // this.interceptors.push(window.__GRPC_DEVTOOLS_EXTENSION__())
164
179
  // } else {
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.23.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.js",
6
6
  "typings": "./lib/index.d.ts",
@@ -21,14 +21,15 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@streamlayer/sdk-web-interfaces": "^0.20.5"
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",
33
34
  "nanoid": "3.3.7",
34
35
  "nanostores": "^0.9.5",