@kibinrpc/client 0.0.3 → 0.1.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.
- package/README.md +171 -0
- package/dist/index.d.ts +1 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -61
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
|
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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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;;;iBC9BA,iBAAA,QAAA,CAA0B,MAAA,EAAQ,iBAAA,GAAoB,WAAA,CAAY,MAAA"}
|
package/dist/index.js
CHANGED
|
@@ -16,12 +16,40 @@ const RETRY_DEFAULTS = {
|
|
|
16
16
|
attempts: 3,
|
|
17
17
|
delay: 300
|
|
18
18
|
};
|
|
19
|
+
function rpcError(error) {
|
|
20
|
+
return new KibinError(error.code ?? "RPC_ERROR", error.message ?? "RPC Error");
|
|
21
|
+
}
|
|
22
|
+
function sleep(attempt, baseDelay) {
|
|
23
|
+
return new Promise((r) => setTimeout(r, baseDelay * 2 ** (attempt - 1)));
|
|
24
|
+
}
|
|
19
25
|
function createKibinClient(config) {
|
|
20
26
|
const maxAttempts = config.retry?.attempts ?? RETRY_DEFAULTS.attempts;
|
|
21
27
|
const baseDelay = config.retry?.delay ?? RETRY_DEFAULTS.delay;
|
|
22
|
-
const batchUrl = config.batchUrl ?? `${config.baseUrl}/batch`;
|
|
23
28
|
const { interceptors } = config;
|
|
24
|
-
let
|
|
29
|
+
let pendingBatch = [];
|
|
30
|
+
let flushScheduled = false;
|
|
31
|
+
async function settleError(item, err) {
|
|
32
|
+
if (interceptors?.error) try {
|
|
33
|
+
item.resolve(await interceptors.error({
|
|
34
|
+
...item.ctx,
|
|
35
|
+
error: err
|
|
36
|
+
}));
|
|
37
|
+
} catch (interceptorError) {
|
|
38
|
+
item.reject(interceptorError);
|
|
39
|
+
}
|
|
40
|
+
else item.reject(err);
|
|
41
|
+
}
|
|
42
|
+
async function settleSuccess(item, data) {
|
|
43
|
+
if (interceptors?.response) try {
|
|
44
|
+
item.resolve(await interceptors.response({
|
|
45
|
+
...item.ctx,
|
|
46
|
+
data
|
|
47
|
+
}));
|
|
48
|
+
} catch (interceptorError) {
|
|
49
|
+
item.reject(interceptorError);
|
|
50
|
+
}
|
|
51
|
+
else item.resolve(data);
|
|
52
|
+
}
|
|
25
53
|
async function rpcCall(namespace, method, args) {
|
|
26
54
|
let ctx = {
|
|
27
55
|
namespace,
|
|
@@ -29,19 +57,30 @@ function createKibinClient(config) {
|
|
|
29
57
|
args
|
|
30
58
|
};
|
|
31
59
|
if (interceptors?.request) ctx = await interceptors.request(ctx);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
resolve,
|
|
38
|
-
reject
|
|
39
|
-
});
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
pendingBatch.push({
|
|
62
|
+
ctx,
|
|
63
|
+
resolve,
|
|
64
|
+
reject
|
|
40
65
|
});
|
|
41
|
-
|
|
66
|
+
if (!flushScheduled) {
|
|
67
|
+
flushScheduled = true;
|
|
68
|
+
queueMicrotask(flush);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function flush() {
|
|
73
|
+
const batch = pendingBatch;
|
|
74
|
+
pendingBatch = [];
|
|
75
|
+
flushScheduled = false;
|
|
76
|
+
if (batch.length === 0) return;
|
|
77
|
+
if (batch.length === 1) flushSingle(batch[0]);
|
|
78
|
+
else flushBatch(batch);
|
|
79
|
+
}
|
|
80
|
+
async function flushSingle(item) {
|
|
42
81
|
let lastError;
|
|
43
82
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
44
|
-
if (attempt > 0) await
|
|
83
|
+
if (attempt > 0) await sleep(attempt, baseDelay);
|
|
45
84
|
try {
|
|
46
85
|
const response = await fetch(config.baseUrl, {
|
|
47
86
|
method: "POST",
|
|
@@ -49,73 +88,71 @@ function createKibinClient(config) {
|
|
|
49
88
|
"Content-Type": "application/json",
|
|
50
89
|
...config.headers
|
|
51
90
|
},
|
|
52
|
-
body: JSON.stringify(ctx)
|
|
91
|
+
body: JSON.stringify(item.ctx)
|
|
53
92
|
});
|
|
54
93
|
const result = await response.json();
|
|
55
94
|
if (result.error) {
|
|
56
|
-
const err =
|
|
95
|
+
const err = rpcError(result.error);
|
|
57
96
|
if (response.status >= 500) {
|
|
58
97
|
lastError = err;
|
|
59
98
|
continue;
|
|
60
99
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
error: err
|
|
64
|
-
});
|
|
65
|
-
throw err;
|
|
100
|
+
await settleError(item, err);
|
|
101
|
+
return;
|
|
66
102
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data: result.data
|
|
70
|
-
});
|
|
71
|
-
return result.data;
|
|
103
|
+
await settleSuccess(item, result.data);
|
|
104
|
+
return;
|
|
72
105
|
} catch (err) {
|
|
73
|
-
if (err instanceof KibinError) throw err;
|
|
74
106
|
lastError = err;
|
|
75
107
|
}
|
|
76
108
|
}
|
|
77
|
-
if (lastError instanceof KibinError
|
|
78
|
-
|
|
79
|
-
error: lastError
|
|
80
|
-
});
|
|
81
|
-
throw lastError;
|
|
109
|
+
if (lastError instanceof KibinError) await settleError(item, lastError);
|
|
110
|
+
else item.reject(lastError);
|
|
82
111
|
}
|
|
83
|
-
async function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
+
async function flushBatch(batch) {
|
|
113
|
+
let pending = [...batch];
|
|
114
|
+
const lastErrors = /* @__PURE__ */ new Map();
|
|
115
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
116
|
+
if (attempt > 0) await sleep(attempt, baseDelay);
|
|
117
|
+
let results;
|
|
118
|
+
try {
|
|
119
|
+
results = await (await fetch(config.baseUrl, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
...config.headers
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify(pending.map((item) => item.ctx))
|
|
126
|
+
})).json();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
for (const item of pending) lastErrors.set(item, err);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const retryItems = [];
|
|
132
|
+
for (let i = 0; i < pending.length; i++) {
|
|
133
|
+
const result = results[i];
|
|
134
|
+
const item = pending[i];
|
|
135
|
+
if (result?.error) {
|
|
136
|
+
const err = rpcError(result.error);
|
|
137
|
+
if (result.status >= 500) {
|
|
138
|
+
retryItems.push(item);
|
|
139
|
+
lastErrors.set(item, err);
|
|
140
|
+
} else await settleError(item, err);
|
|
141
|
+
} else await settleSuccess(item, result?.data);
|
|
112
142
|
}
|
|
143
|
+
pending = retryItems;
|
|
144
|
+
if (pending.length === 0) break;
|
|
145
|
+
}
|
|
146
|
+
for (const item of pending) {
|
|
147
|
+
const err = lastErrors.get(item);
|
|
148
|
+
if (err instanceof KibinError) await settleError(item, err);
|
|
149
|
+
else item.reject(err);
|
|
113
150
|
}
|
|
114
|
-
return Promise.all(promises);
|
|
115
151
|
}
|
|
116
152
|
return new Proxy({}, { get(_, key) {
|
|
117
|
-
if (key
|
|
153
|
+
if (typeof key !== "string") return void 0;
|
|
118
154
|
return new Proxy({}, { get(_, method) {
|
|
155
|
+
if (typeof method !== "string") return void 0;
|
|
119
156
|
return (...args) => rpcCall(key, method, args);
|
|
120
157
|
} });
|
|
121
158
|
} });
|
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
|
|
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\nfunction rpcError(error: { code?: string; message?: string }): KibinError {\n\treturn new KibinError(error.code ?? 'RPC_ERROR', error.message ?? 'RPC Error');\n}\n\nfunction sleep(attempt: number, baseDelay: number): Promise<void> {\n\treturn new Promise<void>((r) => setTimeout(r, baseDelay * 2 ** (attempt - 1)));\n}\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 settleError(item: QueueItem, err: KibinError): Promise<void> {\n\t\tif (interceptors?.error) {\n\t\t\ttry {\n\t\t\t\titem.resolve(await interceptors.error({ ...item.ctx, error: err }));\n\t\t\t} catch (interceptorError) {\n\t\t\t\titem.reject(interceptorError);\n\t\t\t}\n\t\t} else {\n\t\t\titem.reject(err);\n\t\t}\n\t}\n\n\tasync function settleSuccess(item: QueueItem, data: unknown): Promise<void> {\n\t\tif (interceptors?.response) {\n\t\t\ttry {\n\t\t\t\titem.resolve(await interceptors.response({ ...item.ctx, data }));\n\t\t\t} catch (interceptorError) {\n\t\t\t\titem.reject(interceptorError);\n\t\t\t}\n\t\t} else {\n\t\t\titem.resolve(data);\n\t\t}\n\t}\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\tlet lastError: unknown;\n\n\t\tfor (let attempt = 0; attempt < maxAttempts; attempt++) {\n\t\t\tif (attempt > 0) await sleep(attempt, baseDelay);\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(item.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 = rpcError(result.error);\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\tawait settleError(item, err);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tawait settleSuccess(item, result.data);\n\t\t\t\treturn;\n\t\t\t} catch (err) {\n\t\t\t\tlastError = err;\n\t\t\t}\n\t\t}\n\n\t\tif (lastError instanceof KibinError) {\n\t\t\tawait settleError(item, 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) await sleep(attempt, baseDelay);\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 = rpcError(result.error);\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\tawait settleError(item, err);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tawait settleSuccess(item, result?.data);\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) {\n\t\t\t\tawait settleError(item, 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) {\n\t\t\t\tif (typeof key !== 'string') return undefined;\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) {\n\t\t\t\t\t\t\tif (typeof method !== 'string') return undefined;\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
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Type-safe and developer-friendly RPC client with end-to-end type inference",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ixexel661",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsdown",
|
|
36
|
-
"dev": "tsdown --watch"
|
|
36
|
+
"dev": "tsdown --watch",
|
|
37
|
+
"test": "vitest run"
|
|
37
38
|
}
|
|
38
39
|
}
|