@kibinrpc/client 0.0.3 → 0.0.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.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # @kibinrpc/client
2
+
3
+ Type-safe fetch client for [kibinrpc](../../README.md) — fully inferred from your server router, with automatic batching, retry, and interceptors.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @kibinrpc/client
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { createKibinClient } from '@kibinrpc/client'
15
+ import type { AppRouter } from './server/router'
16
+
17
+ const client = createKibinClient<AppRouter>({
18
+ baseUrl: '/api/rpc',
19
+ })
20
+
21
+ // Return types are inferred from the server — no manual annotations
22
+ const user = await client.user.getUser('1')
23
+ const posts = await client.post.listPosts()
24
+ ```
25
+
26
+ ## Automatic batching
27
+
28
+ Concurrent calls are automatically coalesced into a single HTTP request:
29
+
30
+ ```ts
31
+ // Both calls happen in the same tick → sent as one batched request
32
+ const [users, posts] = await Promise.all([
33
+ client.user.listUsers(),
34
+ client.post.listPosts(),
35
+ ])
36
+
37
+ // Sequential calls → two separate requests
38
+ const user = await client.user.getUser('1')
39
+ const posts = await client.post.listPosts()
40
+ ```
41
+
42
+ No configuration required. The server receives either a single object or an array — it handles both automatically.
43
+
44
+ ## Configuration
45
+
46
+ ```ts
47
+ const client = createKibinClient<AppRouter>({
48
+ baseUrl: '/api/rpc',
49
+
50
+ // Static headers sent with every request
51
+ headers: {
52
+ 'X-App-Version': '1.0.0',
53
+ },
54
+
55
+ // Retry on network errors and 5xx responses
56
+ retry: {
57
+ attempts: 3, // total attempts (default: 3)
58
+ delay: 300, // base delay in ms, doubles each retry (default: 300)
59
+ },
60
+
61
+ interceptors: {
62
+ request: (ctx) => ctx,
63
+ response: (ctx) => ctx.data,
64
+ error: (ctx) => { throw ctx.error },
65
+ },
66
+ })
67
+ ```
68
+
69
+ ## Interceptors
70
+
71
+ ### `request`
72
+
73
+ Runs before every call. Use it to attach auth tokens or modify arguments:
74
+
75
+ ```ts
76
+ interceptors: {
77
+ request(ctx) {
78
+ return { ...ctx, args: [{ ...ctx.args[0], token: getToken() }] }
79
+ },
80
+ }
81
+ ```
82
+
83
+ ### `response`
84
+
85
+ Runs after every successful call. Use it to transform or log responses:
86
+
87
+ ```ts
88
+ interceptors: {
89
+ response({ namespace, method, data }) {
90
+ console.log(`← ${namespace}.${method}`, data)
91
+ return data
92
+ },
93
+ }
94
+ ```
95
+
96
+ ### `error`
97
+
98
+ Runs on every failed call (after retries are exhausted). Return a fallback value or rethrow:
99
+
100
+ ```ts
101
+ interceptors: {
102
+ error({ error }) {
103
+ if (error.code === 'UNAUTHORIZED') {
104
+ window.location.href = '/login'
105
+ }
106
+ throw error
107
+ },
108
+ }
109
+ ```
110
+
111
+ ## Error handling
112
+
113
+ ```ts
114
+ import { isKibinError } from '@kibinrpc/client'
115
+
116
+ try {
117
+ await client.user.getUser('999')
118
+ } catch (err) {
119
+ if (isKibinError(err)) {
120
+ console.log(err.code) // e.g. 'NOT_FOUND'
121
+ console.log(err.message) // e.g. 'User not found'
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## Retry behaviour
127
+
128
+ | Scenario | Single call | Batched call |
129
+ |---|---|---|
130
+ | Network error | retry (all attempts) | retry whole batch |
131
+ | HTTP 5xx | retry (all attempts) | retry only failed items |
132
+ | HTTP 4xx | no retry, throw immediately | no retry, reject that item |
133
+
134
+ ## API
135
+
136
+ ### `createKibinClient<Router>(config)`
137
+
138
+ Returns a typed proxy. Every namespace from your router becomes a property, every registered action becomes an async method.
139
+
140
+ ```ts
141
+ const client = createKibinClient<AppRouter>({ baseUrl: '/api/rpc' })
142
+
143
+ client.user.getUser('1') // Promise<User>
144
+ client.post.listPosts() // Promise<Post[]>
145
+ ```
146
+
147
+ ### `isKibinError(err)`
148
+
149
+ Type guard for `KibinError`:
150
+
151
+ ```ts
152
+ import { isKibinError, KibinError } from '@kibinrpc/client'
153
+
154
+ isKibinError(err) // err is KibinError
155
+ err.code // string
156
+ err.message // string
157
+ ```
158
+
159
+ ### Exported types
160
+
161
+ ```ts
162
+ import type {
163
+ KibinClient,
164
+ KibinClientConfig,
165
+ ClientInterceptors,
166
+ RequestCtx,
167
+ ResponseCtx,
168
+ ErrorCtx,
169
+ RetryConfig,
170
+ } from '@kibinrpc/client'
171
+ ```
package/dist/index.d.ts CHANGED
@@ -11,10 +11,7 @@ type ServiceClient<T> = { [K in keyof T as T[K] extends ((...args: never[]) => u
11
11
  type ExtractServices<T> = T extends {
12
12
  services: infer S;
13
13
  } ? S : never;
14
- type BatchFn = <T extends Array<() => Promise<unknown>>>(thunks: [...T]) => Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }>;
15
- type KibinClient<Router> = { [K in keyof ExtractServices<Router>]: ServiceClient<ExtractServices<Router>[K]> } & {
16
- $batch: BatchFn;
17
- };
14
+ type KibinClient<Router> = { [K in keyof ExtractServices<Router>]: ServiceClient<ExtractServices<Router>[K]> };
18
15
  interface RequestCtx {
19
16
  namespace: string;
20
17
  method: string;
@@ -39,8 +36,6 @@ interface RetryConfig {
39
36
  }
40
37
  interface KibinClientConfig {
41
38
  baseUrl: string;
42
- /** Batch endpoint URL. Default: `${baseUrl}/batch` */
43
- batchUrl?: string;
44
39
  headers?: Record<string, string>;
45
40
  retry?: RetryConfig;
46
41
  interceptors?: ClientInterceptors;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/client.ts"],"mappings":";cAAa,UAAA,SAAmB,KAAK;EAAA,SAC3B,IAAA;cAEG,IAAA,UAAc,OAAA;AAAA;AAAA,iBAOX,YAAA,CAAa,KAAA,YAAiB,KAAA,IAAS,UAAU;;;KCR5D,cAAA,MAAoB,CAAA,cAAc,IAAA,qCAChC,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,OAAA,CAAQ,MAAA;AAAA,KAGjC,aAAA,oBACQ,CAAA,IAAK,CAAA,CAAE,CAAA,eAAe,IAAA,yBAA4B,CAAA,WAAY,cAAA,CAAe,CAAA,CAAE,CAAA;AAAA,KAGvF,eAAA,MAAqB,CAAC;EAAW,QAAA;AAAA,IAAsB,CAAA;AAAA,KAEhD,OAAA,cAAqB,KAAA,OAAY,OAAA,YAC5C,MAAA,MAAY,CAAA,MACR,OAAA,eAAsB,CAAA,GAAI,OAAA,CAAQ,UAAA,CAAW,CAAA,CAAE,CAAA;AAAA,KAExC,WAAA,yBACC,eAAA,CAAgB,MAAA,IAAU,aAAA,CAAc,eAAA,CAAgB,MAAA,EAAQ,CAAA;EACvE,MAAA,EAAQ,OAAA;AAAA;AAAA,UAEG,UAAA;EAChB,SAAA;EACA,MAAA;EACA,IAAA;AAAA;AAAA,UAGgB,WAAA,SAAoB,UAAU;EAC9C,IAAI;AAAA;AAAA,UAGY,QAAA,SAAiB,UAAU;EAC3C,KAAA,EAAO,UAAA;AAAA;AAAA,UAGS,kBAAA;EAChB,OAAA,IAAW,GAAA,EAAK,UAAA,KAAe,UAAA,GAAa,OAAA,CAAQ,UAAA;EACpD,QAAA,IAAY,GAAA,EAAK,WAAA,eAA0B,OAAA;EAC3C,KAAA,IAAS,GAAA,EAAK,QAAA,eAAuB,OAAA;AAAA;AAAA,UAGrB,WAAA;EArCY;EAuC5B,QAAA;EAxCwB;EA0CxB,KAAK;AAAA;AAAA,UAGW,iBAAA;EAChB,OAAA;EA7CY;EA+CZ,QAAA;EACA,OAAA,GAAU,MAAA;EACV,KAAA,GAAQ,WAAA;EACR,YAAA,GAAe,kBAAA;AAAA;;;iBC7CA,iBAAA,QAAA,CAA0B,MAAA,EAAQ,iBAAA,GAAoB,WAAA,CAAY,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/types.ts","../src/client.ts"],"mappings":";cAAa,UAAA,SAAmB,KAAK;EAAA,SAC3B,IAAA;cAEG,IAAA,UAAc,OAAA;AAAA;AAAA,iBAOX,YAAA,CAAa,KAAA,YAAiB,KAAA,IAAS,UAAU;;;KCR5D,cAAA,MAAoB,CAAA,cAAc,IAAA,qCAChC,IAAA,EAAM,IAAA,KAAS,OAAA,CAAQ,OAAA,CAAQ,MAAA;AAAA,KAGjC,aAAA,oBACQ,CAAA,IAAK,CAAA,CAAE,CAAA,eAAe,IAAA,yBAA4B,CAAA,WAAY,cAAA,CAAe,CAAA,CAAE,CAAA;AAAA,KAGvF,eAAA,MAAqB,CAAC;EAAW,QAAA;AAAA,IAAsB,CAAA;AAAA,KAEhD,WAAA,yBACC,eAAA,CAAgB,MAAA,IAAU,aAAA,CAAc,eAAA,CAAgB,MAAA,EAAQ,CAAA;AAAA,UAG5D,UAAA;EAChB,SAAA;EACA,MAAA;EACA,IAAA;AAAA;AAAA,UAGgB,WAAA,SAAoB,UAAU;EAC9C,IAAI;AAAA;AAAA,UAGY,QAAA,SAAiB,UAAU;EAC3C,KAAA,EAAO,UAAA;AAAA;AAAA,UAGS,kBAAA;EAChB,OAAA,IAAW,GAAA,EAAK,UAAA,KAAe,UAAA,GAAa,OAAA,CAAQ,UAAA;EACpD,QAAA,IAAY,GAAA,EAAK,WAAA,eAA0B,OAAA;EAC3C,KAAA,IAAS,GAAA,EAAK,QAAA,eAAuB,OAAA;AAAA;AAAA,UAGrB,WAAA;EAjCJ;EAmCZ,QAAA;EAnC6B;EAqC7B,KAAK;AAAA;AAAA,UAGW,iBAAA;EAChB,OAAA;EACA,OAAA,GAAU,MAAA;EACV,KAAA,GAAQ,WAAA;EACR,YAAA,GAAe,kBAAA;AAAA;;;iBCtCA,iBAAA,QAAA,CAA0B,MAAA,EAAQ,iBAAA,GAAoB,WAAA,CAAY,MAAA"}
package/dist/index.js CHANGED
@@ -19,9 +19,9 @@ const RETRY_DEFAULTS = {
19
19
  function createKibinClient(config) {
20
20
  const maxAttempts = config.retry?.attempts ?? RETRY_DEFAULTS.attempts;
21
21
  const baseDelay = config.retry?.delay ?? RETRY_DEFAULTS.delay;
22
- const batchUrl = config.batchUrl ?? `${config.baseUrl}/batch`;
23
22
  const { interceptors } = config;
24
- let currentBatch = null;
23
+ let pendingBatch = [];
24
+ let flushScheduled = false;
25
25
  async function rpcCall(namespace, method, args) {
26
26
  let ctx = {
27
27
  namespace,
@@ -29,16 +29,28 @@ function createKibinClient(config) {
29
29
  args
30
30
  };
31
31
  if (interceptors?.request) ctx = await interceptors.request(ctx);
32
- if (currentBatch !== null) {
33
- const batch = currentBatch;
34
- return new Promise((resolve, reject) => {
35
- batch.push({
36
- ctx,
37
- resolve,
38
- reject
39
- });
32
+ return new Promise((resolve, reject) => {
33
+ pendingBatch.push({
34
+ ctx,
35
+ resolve,
36
+ reject
40
37
  });
41
- }
38
+ if (!flushScheduled) {
39
+ flushScheduled = true;
40
+ queueMicrotask(flush);
41
+ }
42
+ });
43
+ }
44
+ function flush() {
45
+ const batch = pendingBatch;
46
+ pendingBatch = [];
47
+ flushScheduled = false;
48
+ if (batch.length === 0) return;
49
+ if (batch.length === 1) flushSingle(batch[0]);
50
+ else flushBatch(batch);
51
+ }
52
+ async function flushSingle(item) {
53
+ const { ctx } = item;
42
54
  let lastError;
43
55
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
44
56
  if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, baseDelay * 2 ** (attempt - 1)));
@@ -58,63 +70,85 @@ function createKibinClient(config) {
58
70
  lastError = err;
59
71
  continue;
60
72
  }
61
- if (interceptors?.error) return await interceptors.error({
73
+ if (interceptors?.error) item.resolve(await interceptors.error({
62
74
  ...ctx,
63
75
  error: err
64
- });
65
- throw err;
76
+ }));
77
+ else item.reject(err);
78
+ return;
66
79
  }
67
- if (interceptors?.response) return await interceptors.response({
80
+ if (interceptors?.response) item.resolve(await interceptors.response({
68
81
  ...ctx,
69
82
  data: result.data
70
- });
71
- return result.data;
83
+ }));
84
+ else item.resolve(result.data);
85
+ return;
72
86
  } catch (err) {
73
- if (err instanceof KibinError) throw err;
87
+ if (err instanceof KibinError) {
88
+ item.reject(err);
89
+ return;
90
+ }
74
91
  lastError = err;
75
92
  }
76
93
  }
77
- if (lastError instanceof KibinError && interceptors?.error) return await interceptors.error({
94
+ if (lastError instanceof KibinError && interceptors?.error) item.resolve(await interceptors.error({
78
95
  ...ctx,
79
96
  error: lastError
80
- });
81
- throw lastError;
97
+ }));
98
+ else item.reject(lastError);
82
99
  }
83
- async function $batch(thunks) {
84
- const queue = [];
85
- currentBatch = queue;
86
- const promises = thunks.map((thunk) => thunk());
87
- currentBatch = null;
88
- const results = await (await fetch(batchUrl, {
89
- method: "POST",
90
- headers: {
91
- "Content-Type": "application/json",
92
- ...config.headers
93
- },
94
- body: JSON.stringify(queue.map((item) => item.ctx))
95
- })).json();
96
- for (let i = 0; i < queue.length; i++) {
97
- const result = results[i];
98
- if (result?.error) {
99
- const err = new KibinError(result.error.code ?? "RPC_ERROR", result.error.message ?? "RPC Error");
100
- if (interceptors?.error) queue[i].resolve(await interceptors.error({
101
- ...queue[i].ctx,
102
- error: err
103
- }));
104
- else queue[i].reject(err);
105
- } else {
106
- const data = result?.data;
107
- if (interceptors?.response) queue[i].resolve(await interceptors.response({
108
- ...queue[i].ctx,
109
- data
100
+ async function flushBatch(batch) {
101
+ let pending = [...batch];
102
+ const lastErrors = /* @__PURE__ */ new Map();
103
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
104
+ if (attempt > 0) await new Promise((r) => setTimeout(r, baseDelay * 2 ** (attempt - 1)));
105
+ let results;
106
+ try {
107
+ results = await (await fetch(config.baseUrl, {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json",
111
+ ...config.headers
112
+ },
113
+ body: JSON.stringify(pending.map((item) => item.ctx))
114
+ })).json();
115
+ } catch (err) {
116
+ for (const item of pending) lastErrors.set(item, err);
117
+ continue;
118
+ }
119
+ const retryItems = [];
120
+ for (let i = 0; i < pending.length; i++) {
121
+ const result = results[i];
122
+ const item = pending[i];
123
+ if (result?.error) {
124
+ const err = new KibinError(result.error.code ?? "RPC_ERROR", result.error.message ?? "RPC Error");
125
+ if (result.status >= 500) {
126
+ retryItems.push(item);
127
+ lastErrors.set(item, err);
128
+ } else if (interceptors?.error) item.resolve(await interceptors.error({
129
+ ...item.ctx,
130
+ error: err
131
+ }));
132
+ else item.reject(err);
133
+ } else if (interceptors?.response) item.resolve(await interceptors.response({
134
+ ...item.ctx,
135
+ data: result?.data
110
136
  }));
111
- else queue[i].resolve(data);
137
+ else item.resolve(result?.data);
112
138
  }
139
+ pending = retryItems;
140
+ if (pending.length === 0) break;
141
+ }
142
+ for (const item of pending) {
143
+ const err = lastErrors.get(item);
144
+ if (err instanceof KibinError && interceptors?.error) item.resolve(await interceptors.error({
145
+ ...item.ctx,
146
+ error: err
147
+ }));
148
+ else item.reject(err);
113
149
  }
114
- return Promise.all(promises);
115
150
  }
116
151
  return new Proxy({}, { get(_, key) {
117
- if (key === "$batch") return $batch;
118
152
  return new Proxy({}, { get(_, method) {
119
153
  return (...args) => rpcCall(key, method, args);
120
154
  } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":["export class KibinError extends Error {\n\treadonly code: string;\n\n\tconstructor(code: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'KibinError';\n\t\tthis.code = code;\n\t}\n}\n\nexport function isKibinError(error: unknown): error is KibinError {\n\treturn error instanceof KibinError;\n}\n","import { KibinError } from './errors.js';\nimport type { KibinClient, KibinClientConfig, RequestCtx } from './types.js';\n\nconst RETRY_DEFAULTS = { attempts: 3, delay: 300 };\n\ntype RpcResult = { data?: unknown; error?: { code?: string; message?: string } };\ntype QueueItem = { ctx: RequestCtx; resolve: (v: unknown) => void; reject: (e: unknown) => void };\n\nexport function createKibinClient<Router>(config: KibinClientConfig): KibinClient<Router> {\n\tconst maxAttempts = config.retry?.attempts ?? RETRY_DEFAULTS.attempts;\n\tconst baseDelay = config.retry?.delay ?? RETRY_DEFAULTS.delay;\n\tconst batchUrl = config.batchUrl ?? `${config.baseUrl}/batch`;\n\tconst { interceptors } = config;\n\n\tlet currentBatch: QueueItem[] | null = null;\n\n\tasync function rpcCall(namespace: string, method: string, args: unknown[]): Promise<unknown> {\n\t\tlet ctx: RequestCtx = { namespace, method, args };\n\n\t\tif (interceptors?.request) {\n\t\t\tctx = await interceptors.request(ctx);\n\t\t}\n\n\t\tif (currentBatch !== null) {\n\t\t\tconst batch = currentBatch;\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tbatch.push({ ctx, resolve, reject });\n\t\t\t});\n\t\t}\n\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 0; attempt < maxAttempts; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, baseDelay * 2 ** (attempt - 1)));\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(config.baseUrl, {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { 'Content-Type': 'application/json', ...config.headers },\n\t\t\t\t\tbody: JSON.stringify(ctx),\n\t\t\t\t});\n\n\t\t\t\tconst result = (await response.json()) as RpcResult;\n\n\t\t\t\tif (result.error) {\n\t\t\t\t\tconst err = new KibinError(\n\t\t\t\t\t\tresult.error.code ?? 'RPC_ERROR',\n\t\t\t\t\t\tresult.error.message ?? 'RPC Error',\n\t\t\t\t\t);\n\t\t\t\t\tif (response.status >= 500) {\n\t\t\t\t\t\tlastError = err;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (interceptors?.error) {\n\t\t\t\t\t\treturn await interceptors.error({ ...ctx, error: err });\n\t\t\t\t\t}\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\n\t\t\t\tif (interceptors?.response) {\n\t\t\t\t\treturn await interceptors.response({ ...ctx, data: result.data });\n\t\t\t\t}\n\n\t\t\t\treturn result.data;\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof KibinError) throw err;\n\t\t\t\tlastError = err;\n\t\t\t}\n\t\t}\n\n\t\tif (lastError instanceof KibinError && interceptors?.error) {\n\t\t\treturn await interceptors.error({ ...ctx, error: lastError });\n\t\t}\n\n\t\tthrow lastError;\n\t}\n\n\tasync function $batch<T extends Array<() => Promise<unknown>>>(\n\t\tthunks: [...T],\n\t): Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }> {\n\t\tconst queue: QueueItem[] = [];\n\t\tcurrentBatch = queue;\n\t\tconst promises = thunks.map((thunk) => thunk());\n\t\tcurrentBatch = null;\n\n\t\tconst response = await fetch(batchUrl, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json', ...config.headers },\n\t\t\tbody: JSON.stringify(queue.map((item) => item.ctx)),\n\t\t});\n\n\t\tconst results = (await response.json()) as RpcResult[];\n\n\t\tfor (let i = 0; i < queue.length; i++) {\n\t\t\tconst result = results[i];\n\t\t\tif (result?.error) {\n\t\t\t\tconst err = new KibinError(\n\t\t\t\t\tresult.error.code ?? 'RPC_ERROR',\n\t\t\t\t\tresult.error.message ?? 'RPC Error',\n\t\t\t\t);\n\t\t\t\tif (interceptors?.error) {\n\t\t\t\t\tqueue[i].resolve(await interceptors.error({ ...queue[i].ctx, error: err }));\n\t\t\t\t} else {\n\t\t\t\t\tqueue[i].reject(err);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst data = result?.data;\n\t\t\t\tif (interceptors?.response) {\n\t\t\t\t\tqueue[i].resolve(await interceptors.response({ ...queue[i].ctx, data }));\n\t\t\t\t} else {\n\t\t\t\t\tqueue[i].resolve(data);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn Promise.all(promises) as Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>> }>;\n\t}\n\n\treturn new Proxy(\n\t\t{},\n\t\t{\n\t\t\tget(_, key: string) {\n\t\t\t\tif (key === '$batch') return $batch;\n\t\t\t\treturn new Proxy(\n\t\t\t\t\t{},\n\t\t\t\t\t{\n\t\t\t\t\t\tget(_, method: string) {\n\t\t\t\t\t\t\treturn (...args: unknown[]) => rpcCall(key, method, args);\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t) as unknown as KibinClient<Router>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":["export class KibinError extends Error {\n\treadonly code: string;\n\n\tconstructor(code: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = 'KibinError';\n\t\tthis.code = code;\n\t}\n}\n\nexport function isKibinError(error: unknown): error is KibinError {\n\treturn error instanceof KibinError;\n}\n","import { KibinError } from './errors.js';\nimport type { KibinClient, KibinClientConfig, RequestCtx } from './types.js';\n\nconst RETRY_DEFAULTS = { attempts: 3, delay: 300 };\n\ntype RpcResult = { data?: unknown; error?: { code?: string; message?: string } };\ntype BatchRpcResult = RpcResult & { status: number };\ntype QueueItem = { ctx: RequestCtx; resolve: (v: unknown) => void; reject: (e: unknown) => void };\n\nexport function createKibinClient<Router>(config: KibinClientConfig): KibinClient<Router> {\n\tconst maxAttempts = config.retry?.attempts ?? RETRY_DEFAULTS.attempts;\n\tconst baseDelay = config.retry?.delay ?? RETRY_DEFAULTS.delay;\n\tconst { interceptors } = config;\n\n\tlet pendingBatch: QueueItem[] = [];\n\tlet flushScheduled = false;\n\n\tasync function rpcCall(namespace: string, method: string, args: unknown[]): Promise<unknown> {\n\t\tlet ctx: RequestCtx = { namespace, method, args };\n\n\t\tif (interceptors?.request) {\n\t\t\tctx = await interceptors.request(ctx);\n\t\t}\n\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tpendingBatch.push({ ctx, resolve, reject });\n\t\t\tif (!flushScheduled) {\n\t\t\t\tflushScheduled = true;\n\t\t\t\tqueueMicrotask(flush);\n\t\t\t}\n\t\t});\n\t}\n\n\tfunction flush() {\n\t\tconst batch = pendingBatch;\n\t\tpendingBatch = [];\n\t\tflushScheduled = false;\n\n\t\tif (batch.length === 0) return;\n\t\tif (batch.length === 1) {\n\t\t\tflushSingle(batch[0]);\n\t\t} else {\n\t\t\tflushBatch(batch);\n\t\t}\n\t}\n\n\tasync function flushSingle(item: QueueItem) {\n\t\tconst { ctx } = item;\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 0; attempt < maxAttempts; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => setTimeout(resolve, baseDelay * 2 ** (attempt - 1)));\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(config.baseUrl, {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { 'Content-Type': 'application/json', ...config.headers },\n\t\t\t\t\tbody: JSON.stringify(ctx),\n\t\t\t\t});\n\n\t\t\t\tconst result = (await response.json()) as RpcResult;\n\n\t\t\t\tif (result.error) {\n\t\t\t\t\tconst err = new KibinError(\n\t\t\t\t\t\tresult.error.code ?? 'RPC_ERROR',\n\t\t\t\t\t\tresult.error.message ?? 'RPC Error',\n\t\t\t\t\t);\n\t\t\t\t\tif (response.status >= 500) {\n\t\t\t\t\t\tlastError = err;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (interceptors?.error) {\n\t\t\t\t\t\titem.resolve(await interceptors.error({ ...ctx, error: err }));\n\t\t\t\t\t} else {\n\t\t\t\t\t\titem.reject(err);\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (interceptors?.response) {\n\t\t\t\t\titem.resolve(await interceptors.response({ ...ctx, data: result.data }));\n\t\t\t\t} else {\n\t\t\t\t\titem.resolve(result.data);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof KibinError) {\n\t\t\t\t\titem.reject(err);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlastError = err;\n\t\t\t}\n\t\t}\n\n\t\tif (lastError instanceof KibinError && interceptors?.error) {\n\t\t\titem.resolve(await interceptors.error({ ...ctx, error: lastError }));\n\t\t} else {\n\t\t\titem.reject(lastError);\n\t\t}\n\t}\n\n\tasync function flushBatch(batch: QueueItem[]) {\n\t\tlet pending = [...batch];\n\t\tconst lastErrors = new Map<QueueItem, unknown>();\n\n\t\tfor (let attempt = 0; attempt < maxAttempts; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\tawait new Promise<void>((r) => setTimeout(r, baseDelay * 2 ** (attempt - 1)));\n\t\t\t}\n\n\t\t\tlet results: BatchRpcResult[];\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(config.baseUrl, {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { 'Content-Type': 'application/json', ...config.headers },\n\t\t\t\t\tbody: JSON.stringify(pending.map((item) => item.ctx)),\n\t\t\t\t});\n\t\t\t\tresults = (await response.json()) as BatchRpcResult[];\n\t\t\t} catch (err) {\n\t\t\t\tfor (const item of pending) lastErrors.set(item, err);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst retryItems: QueueItem[] = [];\n\n\t\t\tfor (let i = 0; i < pending.length; i++) {\n\t\t\t\tconst result = results[i];\n\t\t\t\tconst item = pending[i];\n\n\t\t\t\tif (result?.error) {\n\t\t\t\t\tconst err = new KibinError(\n\t\t\t\t\t\tresult.error.code ?? 'RPC_ERROR',\n\t\t\t\t\t\tresult.error.message ?? 'RPC Error',\n\t\t\t\t\t);\n\t\t\t\t\tif (result.status >= 500) {\n\t\t\t\t\t\tretryItems.push(item);\n\t\t\t\t\t\tlastErrors.set(item, err);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (interceptors?.error) {\n\t\t\t\t\t\t\titem.resolve(await interceptors.error({ ...item.ctx, error: err }));\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\titem.reject(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (interceptors?.response) {\n\t\t\t\t\t\titem.resolve(await interceptors.response({ ...item.ctx, data: result?.data }));\n\t\t\t\t\t} else {\n\t\t\t\t\t\titem.resolve(result?.data);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpending = retryItems;\n\t\t\tif (pending.length === 0) break;\n\t\t}\n\n\t\tfor (const item of pending) {\n\t\t\tconst err = lastErrors.get(item);\n\t\t\tif (err instanceof KibinError && interceptors?.error) {\n\t\t\t\titem.resolve(await interceptors.error({ ...item.ctx, error: err }));\n\t\t\t} else {\n\t\t\t\titem.reject(err);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn new Proxy(\n\t\t{},\n\t\t{\n\t\t\tget(_, key: string) {\n\t\t\t\treturn new Proxy(\n\t\t\t\t\t{},\n\t\t\t\t\t{\n\t\t\t\t\t\tget(_, method: string) {\n\t\t\t\t\t\t\treturn (...args: unknown[]) => rpcCall(key, method, args);\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t},\n\t\t},\n\t) as unknown as KibinClient<Router>;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kibinrpc/client",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Type-safe and developer-friendly RPC client with end-to-end type inference",
5
5
  "license": "MIT",
6
6
  "author": "ixexel661",