@toyz/loom-rpc 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 +245 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/mutate.d.ts +31 -0
- package/dist/mutate.d.ts.map +1 -0
- package/dist/mutate.js +107 -0
- package/dist/mutate.js.map +1 -0
- package/dist/rpc.d.ts +36 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +205 -0
- package/dist/rpc.js.map +1 -0
- package/dist/testing.d.ts +43 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +87 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport.d.ts +47 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +78 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# @toyz/loom-rpc
|
|
2
|
+
|
|
3
|
+
Type-safe, decorator-driven RPC for [Loom](https://github.com/Toyz/loom). Server-agnostic, transport-swappable.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install @toyz/loom-rpc
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**One dependency:** `@toyz/loom`. That's it.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Define a Contract
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// contracts/user.ts — shared between client and server
|
|
19
|
+
export class UserRouter {
|
|
20
|
+
getUser(id: string): User {
|
|
21
|
+
return null!;
|
|
22
|
+
}
|
|
23
|
+
listUsers(page: number, limit: number): User[] {
|
|
24
|
+
return null!;
|
|
25
|
+
}
|
|
26
|
+
updateProfile(data: ProfileUpdate): User {
|
|
27
|
+
return null!;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The class is the type contract. Methods have dummy bodies — they exist for TypeScript to extract parameter and return types. Nothing runs. Nothing ships to the client.
|
|
33
|
+
|
|
34
|
+
### 2. Register a Transport
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// main.tsx
|
|
38
|
+
import { app } from "@toyz/loom";
|
|
39
|
+
import { RpcTransport, HttpTransport } from "@toyz/loom-rpc";
|
|
40
|
+
|
|
41
|
+
app.use(RpcTransport, new HttpTransport());
|
|
42
|
+
// or: new HttpTransport("https://api.example.com/rpc")
|
|
43
|
+
|
|
44
|
+
app.start();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. Query with `@rpc`
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { rpc } from "@toyz/loom-rpc";
|
|
51
|
+
import { UserRouter } from "../contracts/user";
|
|
52
|
+
|
|
53
|
+
@component("user-profile")
|
|
54
|
+
class UserProfile extends LoomElement {
|
|
55
|
+
@prop({ param: "id" }) accessor userId!: string;
|
|
56
|
+
|
|
57
|
+
@rpc(UserRouter, "getUser", {
|
|
58
|
+
fn: el => [el.userId], // args from element state — re-fetches on change
|
|
59
|
+
staleTime: 60_000, // SWR: cache for 1 minute
|
|
60
|
+
})
|
|
61
|
+
accessor user!: ApiState<User>;
|
|
62
|
+
|
|
63
|
+
update() {
|
|
64
|
+
return this.user.match({
|
|
65
|
+
loading: () => <div>Loading...</div>,
|
|
66
|
+
ok: (user) => <h1>{user.name}</h1>,
|
|
67
|
+
err: (e) => <div>Error: {e.message}</div>,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 4. Mutate with `@mutate`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { mutate } from "@toyz/loom-rpc";
|
|
77
|
+
|
|
78
|
+
@component("edit-profile")
|
|
79
|
+
class EditProfile extends LoomElement {
|
|
80
|
+
@mutate(UserRouter, "updateProfile")
|
|
81
|
+
accessor save!: RpcMutator<[ProfileUpdate], User>;
|
|
82
|
+
|
|
83
|
+
async handleSubmit(data: ProfileUpdate) {
|
|
84
|
+
try {
|
|
85
|
+
const user = await this.save.call(data);
|
|
86
|
+
console.log("Saved:", user.name);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error("Failed:", e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
update() {
|
|
93
|
+
return (
|
|
94
|
+
<form onSubmit={() => this.handleSubmit({ name: "New Name" })}>
|
|
95
|
+
<button disabled={this.save.loading}>
|
|
96
|
+
{this.save.loading ? "Saving..." : "Save"}
|
|
97
|
+
</button>
|
|
98
|
+
</form>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
### `@rpc(Router, method, opts?)`
|
|
109
|
+
|
|
110
|
+
Auto-accessor decorator for queries. Returns `ApiState<T>` with `.match()`, `.unwrap()`, `.refetch()`, `.invalidate()`.
|
|
111
|
+
|
|
112
|
+
| Option | Type | Default | Description |
|
|
113
|
+
| ----------- | -------------- | ------- | ----------------------------------------- |
|
|
114
|
+
| `fn` | `(el) => Args` | `[]` | Extract procedure args from element state |
|
|
115
|
+
| `staleTime` | `number` | `0` | SWR cache duration (ms) |
|
|
116
|
+
| `eager` | `boolean` | `true` | Fetch on connect |
|
|
117
|
+
| `retry` | `number` | `0` | Retry count with exponential backoff |
|
|
118
|
+
|
|
119
|
+
### `@mutate(Router, method)`
|
|
120
|
+
|
|
121
|
+
Auto-accessor decorator for mutations. Returns `RpcMutator<Args, Return>`.
|
|
122
|
+
|
|
123
|
+
| Property | Type | Description |
|
|
124
|
+
| ---------------- | ---------------- | ---------------------- |
|
|
125
|
+
| `.call(...args)` | `Promise<T>` | Execute the mutation |
|
|
126
|
+
| `.loading` | `boolean` | In-flight state |
|
|
127
|
+
| `.error` | `Error \| null` | Last error |
|
|
128
|
+
| `.data` | `T \| undefined` | Last successful result |
|
|
129
|
+
| `.reset()` | `void` | Clear state |
|
|
130
|
+
|
|
131
|
+
### `RpcTransport`
|
|
132
|
+
|
|
133
|
+
Abstract class — implement to control how RPC calls reach the server.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
abstract class RpcTransport {
|
|
137
|
+
abstract call<T>(router: string, method: string, args: any[]): Promise<T>;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `HttpTransport`
|
|
142
|
+
|
|
143
|
+
Default transport — `POST /rpc/{Router}/{Method}` with JSON body.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
new HttpTransport(baseUrl?: string, headers?: Record<string, string>)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `RpcError`
|
|
150
|
+
|
|
151
|
+
Structured error with `.status`, `.router`, `.method`, `.code`.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Wire Protocol
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
POST /rpc/{RouterName}/{MethodName}
|
|
159
|
+
Content-Type: application/json
|
|
160
|
+
|
|
161
|
+
Request: { "args": [arg1, arg2, ...] }
|
|
162
|
+
Response: { "data": <return value> }
|
|
163
|
+
Error: { "error": { "message": "...", "code": "..." } }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Any server that follows this convention works.** Go, Rust, Python, Express, Hono, Cloudflare Workers — anything that accepts HTTP POST and returns JSON.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Custom Transports
|
|
171
|
+
|
|
172
|
+
Swap HTTP for WebSocket, gRPC-Web, or anything else:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
class WsTransport extends RpcTransport {
|
|
176
|
+
constructor(private ws: WebSocket) {
|
|
177
|
+
super();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async call<T>(router: string, method: string, args: any[]): Promise<T> {
|
|
181
|
+
// Your WebSocket RPC logic here
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
app.use(RpcTransport, new WsTransport(ws));
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
One DI swap. Every `@rpc` and `@mutate` in the app uses the new transport. No component changes.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Testing
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { MockTransport } from "@toyz/loom-rpc/testing";
|
|
196
|
+
|
|
197
|
+
const transport = new MockTransport();
|
|
198
|
+
|
|
199
|
+
// Static mocks
|
|
200
|
+
transport.mock("UserRouter", "getUser", { id: "1", name: "Alice" });
|
|
201
|
+
|
|
202
|
+
// Dynamic mocks
|
|
203
|
+
transport.mock("UserRouter", "getUser", (id: string) => ({
|
|
204
|
+
id,
|
|
205
|
+
name: `User ${id}`,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
// Error mocks
|
|
209
|
+
transport.mockError("UserRouter", "deleteUser", new Error("Forbidden"));
|
|
210
|
+
|
|
211
|
+
// Delay simulation
|
|
212
|
+
transport.delay("UserRouter", "getUser", 500);
|
|
213
|
+
|
|
214
|
+
// Register and go
|
|
215
|
+
app.use(RpcTransport, transport);
|
|
216
|
+
|
|
217
|
+
// Assertions
|
|
218
|
+
transport.assertCalled("UserRouter", "getUser", ["1"]);
|
|
219
|
+
transport.assertNotCalled("UserRouter", "deleteUser");
|
|
220
|
+
console.log(transport.history); // [{ router, method, args }]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Type Safety
|
|
226
|
+
|
|
227
|
+
Everything is inferred from the contract class:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
@rpc(UserRouter, "getUser", { fn: el => [el.userId] })
|
|
231
|
+
// ↑ autocompleted ↑ must be [string]
|
|
232
|
+
accessor user!: ApiState<User>;
|
|
233
|
+
// ↑ inferred from UserRouter.getUser return type
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- Method names are autocompleted and type-checked
|
|
237
|
+
- Argument types are inferred from the contract method parameters
|
|
238
|
+
- Return types flow into `ApiState<T>` automatically
|
|
239
|
+
- Pass the wrong types? Compile error.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — Barrel exports
|
|
3
|
+
*
|
|
4
|
+
* Type-safe, decorator-driven RPC for Loom.
|
|
5
|
+
* Server-agnostic, transport-swappable.
|
|
6
|
+
*/
|
|
7
|
+
export { rpc } from "./rpc";
|
|
8
|
+
export { mutate } from "./mutate";
|
|
9
|
+
export { RpcTransport, HttpTransport, RpcError } from "./transport";
|
|
10
|
+
export type { RpcMethods, InferArgs, InferReturn, RpcQueryOptions, RpcMutator, RpcRequest, RpcResponse, } from "./types";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGlC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGpE,YAAY,EACV,UAAU,EACV,SAAS,EACT,WAAW,EACX,eAAe,EACf,UAAU,EACV,UAAU,EACV,WAAW,GACZ,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — Barrel exports
|
|
3
|
+
*
|
|
4
|
+
* Type-safe, decorator-driven RPC for Loom.
|
|
5
|
+
* Server-agnostic, transport-swappable.
|
|
6
|
+
*/
|
|
7
|
+
// Decorators
|
|
8
|
+
export { rpc } from "./rpc";
|
|
9
|
+
export { mutate } from "./mutate";
|
|
10
|
+
// Transport
|
|
11
|
+
export { RpcTransport, HttpTransport, RpcError } from "./transport";
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,aAAa;AACb,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC,YAAY;AACZ,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/mutate.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — @mutate mutation decorator
|
|
3
|
+
*
|
|
4
|
+
* Auto-accessor decorator for type-safe RPC mutations.
|
|
5
|
+
* Unlike @rpc (queries), mutations are not auto-fetched —
|
|
6
|
+
* you call them manually via .call().
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* @mutate(UserRouter, "updateProfile")
|
|
10
|
+
* accessor save!: RpcMutator<[ProfileUpdate], User>;
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import type { RpcMethods, InferArgs, InferReturn, RpcMutator } from "./types";
|
|
14
|
+
/**
|
|
15
|
+
* @mutate(Router, method) — Mutation decorator
|
|
16
|
+
*
|
|
17
|
+
* Returns an RpcMutator with .call(), .loading, .error, .data.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* @mutate(UserRouter, "updateProfile")
|
|
21
|
+
* accessor save!: RpcMutator<[ProfileUpdate], User>;
|
|
22
|
+
*
|
|
23
|
+
* // In a handler:
|
|
24
|
+
* const result = await this.save.call({ name: "New Name" });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param router - The contract class (used for router name + type inference)
|
|
28
|
+
* @param method - The method name on the contract to call
|
|
29
|
+
*/
|
|
30
|
+
export declare function mutate<TRouter extends object, TMethod extends RpcMethods<TRouter>>(router: new (...args: any[]) => TRouter, method: TMethod): <This extends object>(_target: ClassAccessorDecoratorTarget<This, RpcMutator<InferArgs<TRouter, TMethod> extends any[] ? InferArgs<TRouter, TMethod> : [InferArgs<TRouter, TMethod>], InferReturn<TRouter, TMethod>>>, context: ClassAccessorDecoratorContext<This, RpcMutator<InferArgs<TRouter, TMethod> extends any[] ? InferArgs<TRouter, TMethod> : [InferArgs<TRouter, TMethod>], InferReturn<TRouter, TMethod>>>) => ClassAccessorDecoratorResult<This, RpcMutator<InferArgs<TRouter, TMethod> extends any[] ? InferArgs<TRouter, TMethod> : [InferArgs<TRouter, TMethod>], InferReturn<TRouter, TMethod>>>;
|
|
31
|
+
//# sourceMappingURL=mutate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutate.d.ts","sourceRoot":"","sources":["../src/mutate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAG9E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,MAAM,CACpB,OAAO,SAAS,MAAM,EACtB,OAAO,SAAS,UAAU,CAAC,OAAO,CAAC,EAEnC,MAAM,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EACvC,MAAM,EAAE,OAAO,IAMP,IAAI,SAAS,MAAM,EACzB,SAAS,4BAA4B,CAAC,IAAI,EAAE,UAAU,CAAC,oCAAc,GAAG,EAAE,iCAAW,6BAAO,gCAAU,CAAC,EACvG,SAAS,6BAA6B,CAAC,IAAI,EAAE,UAAU,CAAC,oCAAc,GAAG,EAAE,iCAAW,6BAAO,gCAAU,CAAC,KACvG,4BAA4B,CAAC,IAAI,EAAE,UAAU,CAAC,oCAAc,GAAG,EAAE,iCAAW,6BAAO,gCAAU,CAAC,CA2BlG"}
|
package/dist/mutate.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — @mutate mutation decorator
|
|
3
|
+
*
|
|
4
|
+
* Auto-accessor decorator for type-safe RPC mutations.
|
|
5
|
+
* Unlike @rpc (queries), mutations are not auto-fetched —
|
|
6
|
+
* you call them manually via .call().
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* @mutate(UserRouter, "updateProfile")
|
|
10
|
+
* accessor save!: RpcMutator<[ProfileUpdate], User>;
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { app } from "@toyz/loom";
|
|
14
|
+
import { Reactive } from "@toyz/loom/store";
|
|
15
|
+
import { RpcTransport } from "./transport";
|
|
16
|
+
/**
|
|
17
|
+
* @mutate(Router, method) — Mutation decorator
|
|
18
|
+
*
|
|
19
|
+
* Returns an RpcMutator with .call(), .loading, .error, .data.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* @mutate(UserRouter, "updateProfile")
|
|
23
|
+
* accessor save!: RpcMutator<[ProfileUpdate], User>;
|
|
24
|
+
*
|
|
25
|
+
* // In a handler:
|
|
26
|
+
* const result = await this.save.call({ name: "New Name" });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @param router - The contract class (used for router name + type inference)
|
|
30
|
+
* @param method - The method name on the contract to call
|
|
31
|
+
*/
|
|
32
|
+
export function mutate(router, method) {
|
|
33
|
+
const routerName = router.name;
|
|
34
|
+
return (_target, context) => {
|
|
35
|
+
const stateKey = Symbol(`mutate:${String(context.name)}`);
|
|
36
|
+
const traceKey = Symbol(`mutate:trace:${String(context.name)}`);
|
|
37
|
+
return {
|
|
38
|
+
get() {
|
|
39
|
+
if (!this[stateKey]) {
|
|
40
|
+
const sentinel = new Reactive(0);
|
|
41
|
+
this[traceKey] = sentinel;
|
|
42
|
+
sentinel.subscribe(() => this.scheduleUpdate?.());
|
|
43
|
+
const notify = () => { sentinel.set((v) => v + 1); };
|
|
44
|
+
this[stateKey] = createMutator(routerName, method, notify);
|
|
45
|
+
}
|
|
46
|
+
// Read sentinel so traced update() sees the dependency
|
|
47
|
+
this[traceKey].value;
|
|
48
|
+
return this[stateKey];
|
|
49
|
+
},
|
|
50
|
+
set(_val) {
|
|
51
|
+
// State is managed internally
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Internal mutator factory.
|
|
58
|
+
*/
|
|
59
|
+
function createMutator(routerName, method, scheduleUpdate) {
|
|
60
|
+
let data;
|
|
61
|
+
let error = null;
|
|
62
|
+
let loading = false;
|
|
63
|
+
return {
|
|
64
|
+
async call(...args) {
|
|
65
|
+
let transport;
|
|
66
|
+
try {
|
|
67
|
+
transport = app.get(RpcTransport);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error("[LoomRPC] No RpcTransport registered. " +
|
|
71
|
+
"Call app.use(RpcTransport, new HttpTransport()) before app.start().");
|
|
72
|
+
}
|
|
73
|
+
loading = true;
|
|
74
|
+
error = null;
|
|
75
|
+
scheduleUpdate();
|
|
76
|
+
try {
|
|
77
|
+
data = await transport.call(routerName, method, args);
|
|
78
|
+
loading = false;
|
|
79
|
+
error = null;
|
|
80
|
+
scheduleUpdate();
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
loading = false;
|
|
85
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
86
|
+
scheduleUpdate();
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
get loading() {
|
|
91
|
+
return loading;
|
|
92
|
+
},
|
|
93
|
+
get error() {
|
|
94
|
+
return error;
|
|
95
|
+
},
|
|
96
|
+
get data() {
|
|
97
|
+
return data;
|
|
98
|
+
},
|
|
99
|
+
reset() {
|
|
100
|
+
data = undefined;
|
|
101
|
+
error = null;
|
|
102
|
+
loading = false;
|
|
103
|
+
scheduleUpdate();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=mutate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutate.js","sourceRoot":"","sources":["../src/mutate.ts"],"names":[],"mappings":"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"}
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — @rpc query decorator
|
|
3
|
+
*
|
|
4
|
+
* Auto-accessor decorator for type-safe RPC queries.
|
|
5
|
+
* Reuses Loom's ApiState pattern under the hood.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* @rpc(UserRouter, "getUser", {
|
|
9
|
+
* fn: el => [el.userId],
|
|
10
|
+
* staleTime: 60_000,
|
|
11
|
+
* })
|
|
12
|
+
* accessor user!: ApiState<User>;
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The contract class is passed as a runtime value (not just a type)
|
|
16
|
+
* so the router name survives TypeScript erasure.
|
|
17
|
+
*/
|
|
18
|
+
import type { ApiState } from "@toyz/loom";
|
|
19
|
+
import type { RpcMethods, InferReturn, RpcQueryOptions } from "./types";
|
|
20
|
+
/**
|
|
21
|
+
* @rpc(Router, method, opts?) — Query decorator
|
|
22
|
+
*
|
|
23
|
+
* Fetches data from the server via the registered RpcTransport.
|
|
24
|
+
* Returns an ApiState<T> with .match(), .unwrap(), .refetch(), etc.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* @rpc(UserRouter, "getUser", { fn: el => [el.userId], staleTime: 60_000 })
|
|
28
|
+
* accessor user!: ApiState<User>;
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @param router - The contract class (used for router name + type inference)
|
|
32
|
+
* @param method - The method name on the contract to call
|
|
33
|
+
* @param opts - Optional configuration (fn, staleTime, retry, eager)
|
|
34
|
+
*/
|
|
35
|
+
export declare function rpc<TRouter extends object, TMethod extends RpcMethods<TRouter>>(router: new (...args: any[]) => TRouter, method: TMethod, opts?: RpcQueryOptions<TRouter, TMethod>): <This extends object>(_target: ClassAccessorDecoratorTarget<This, ApiState<InferReturn<TRouter, TMethod>>>, context: ClassAccessorDecoratorContext<This, ApiState<InferReturn<TRouter, TMethod>>>) => ClassAccessorDecoratorResult<This, ApiState<InferReturn<TRouter, TMethod>>>;
|
|
36
|
+
//# sourceMappingURL=rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAGxE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,GAAG,CACjB,OAAO,SAAS,MAAM,EACtB,OAAO,SAAS,UAAU,CAAC,OAAO,CAAC,EAEnC,MAAM,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,EACvC,MAAM,EAAE,OAAO,EACf,IAAI,CAAC,EAAE,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,IAKhC,IAAI,SAAS,MAAM,EACzB,SAAS,4BAA4B,CAAC,IAAI,EAAE,QAAQ,+BAAS,CAAC,EAC9D,SAAS,6BAA6B,CAAC,IAAI,EAAE,QAAQ,+BAAS,CAAC,KAC9D,4BAA4B,CAAC,IAAI,EAAE,QAAQ,+BAAS,CAAC,CA6BzD"}
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — @rpc query decorator
|
|
3
|
+
*
|
|
4
|
+
* Auto-accessor decorator for type-safe RPC queries.
|
|
5
|
+
* Reuses Loom's ApiState pattern under the hood.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* @rpc(UserRouter, "getUser", {
|
|
9
|
+
* fn: el => [el.userId],
|
|
10
|
+
* staleTime: 60_000,
|
|
11
|
+
* })
|
|
12
|
+
* accessor user!: ApiState<User>;
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The contract class is passed as a runtime value (not just a type)
|
|
16
|
+
* so the router name survives TypeScript erasure.
|
|
17
|
+
*/
|
|
18
|
+
import { app } from "@toyz/loom";
|
|
19
|
+
import { Reactive } from "@toyz/loom/store";
|
|
20
|
+
import { LoomResult } from "@toyz/loom";
|
|
21
|
+
import { RpcTransport } from "./transport";
|
|
22
|
+
/**
|
|
23
|
+
* @rpc(Router, method, opts?) — Query decorator
|
|
24
|
+
*
|
|
25
|
+
* Fetches data from the server via the registered RpcTransport.
|
|
26
|
+
* Returns an ApiState<T> with .match(), .unwrap(), .refetch(), etc.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* @rpc(UserRouter, "getUser", { fn: el => [el.userId], staleTime: 60_000 })
|
|
30
|
+
* accessor user!: ApiState<User>;
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @param router - The contract class (used for router name + type inference)
|
|
34
|
+
* @param method - The method name on the contract to call
|
|
35
|
+
* @param opts - Optional configuration (fn, staleTime, retry, eager)
|
|
36
|
+
*/
|
|
37
|
+
export function rpc(router, method, opts) {
|
|
38
|
+
const routerName = router.name;
|
|
39
|
+
return (_target, context) => {
|
|
40
|
+
const stateKey = Symbol(`rpc:${String(context.name)}`);
|
|
41
|
+
const traceKey = Symbol(`rpc:trace:${String(context.name)}`);
|
|
42
|
+
return {
|
|
43
|
+
get() {
|
|
44
|
+
if (!this[stateKey]) {
|
|
45
|
+
const sentinel = new Reactive(0);
|
|
46
|
+
this[traceKey] = sentinel;
|
|
47
|
+
sentinel.subscribe(() => this.scheduleUpdate?.());
|
|
48
|
+
const notify = () => { sentinel.set((v) => v + 1); };
|
|
49
|
+
this[stateKey] = createRpcState(routerName, method, opts, notify, this);
|
|
50
|
+
}
|
|
51
|
+
// Read sentinel so traced update() sees the dependency
|
|
52
|
+
this[traceKey].value;
|
|
53
|
+
return this[stateKey];
|
|
54
|
+
},
|
|
55
|
+
set(_val) {
|
|
56
|
+
// State is managed internally
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Internal state factory — mirrors createApiState but uses RpcTransport.
|
|
63
|
+
*/
|
|
64
|
+
function createRpcState(routerName, method, opts, scheduleUpdate, host) {
|
|
65
|
+
let data;
|
|
66
|
+
let error;
|
|
67
|
+
let loading = true;
|
|
68
|
+
let stale = false;
|
|
69
|
+
let lastFetchTime = 0;
|
|
70
|
+
let lastArgs;
|
|
71
|
+
let fetching = false;
|
|
72
|
+
const staleTime = opts?.staleTime ?? 0;
|
|
73
|
+
const maxRetries = opts?.retry ?? 0;
|
|
74
|
+
const eager = opts?.eager ?? true;
|
|
75
|
+
async function runFetch() {
|
|
76
|
+
let transport;
|
|
77
|
+
try {
|
|
78
|
+
transport = app.get(RpcTransport);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
error = new Error("[LoomRPC] No RpcTransport registered. " +
|
|
82
|
+
"Call app.use(RpcTransport, new HttpTransport()) before app.start().");
|
|
83
|
+
loading = false;
|
|
84
|
+
scheduleUpdate();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Resolve args from element state
|
|
88
|
+
const args = opts?.fn ? opts.fn(host) : [];
|
|
89
|
+
// Check if args changed (SWR key)
|
|
90
|
+
const argsKey = JSON.stringify(args);
|
|
91
|
+
if (argsKey === lastArgs && !stale && data !== undefined && !fetching) {
|
|
92
|
+
return; // Same args, data is fresh — skip
|
|
93
|
+
}
|
|
94
|
+
lastArgs = argsKey;
|
|
95
|
+
loading = data === undefined; // SWR: only show loading if no cached data
|
|
96
|
+
error = undefined;
|
|
97
|
+
stale = false;
|
|
98
|
+
fetching = true;
|
|
99
|
+
scheduleUpdate();
|
|
100
|
+
let attempt = 0;
|
|
101
|
+
while (true) {
|
|
102
|
+
try {
|
|
103
|
+
data = await transport.call(routerName, method, args);
|
|
104
|
+
error = undefined;
|
|
105
|
+
loading = false;
|
|
106
|
+
fetching = false;
|
|
107
|
+
lastFetchTime = Date.now();
|
|
108
|
+
scheduleUpdate();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
attempt++;
|
|
113
|
+
if (attempt > maxRetries) {
|
|
114
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
115
|
+
loading = false;
|
|
116
|
+
fetching = false;
|
|
117
|
+
scheduleUpdate();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Exponential backoff: 200ms, 400ms, 800ms...
|
|
121
|
+
await new Promise((r) => setTimeout(r, 200 * Math.pow(2, attempt - 1)));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function checkStale() {
|
|
126
|
+
if (staleTime > 0 && lastFetchTime > 0 && !stale) {
|
|
127
|
+
if (Date.now() - lastFetchTime > staleTime) {
|
|
128
|
+
stale = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const state = {
|
|
133
|
+
get ok() {
|
|
134
|
+
return data !== undefined && error === undefined;
|
|
135
|
+
},
|
|
136
|
+
get data() {
|
|
137
|
+
checkStale();
|
|
138
|
+
// Re-derive args and refetch if they changed
|
|
139
|
+
if (opts?.fn) {
|
|
140
|
+
const newArgs = JSON.stringify(opts.fn(host));
|
|
141
|
+
if (newArgs !== lastArgs) {
|
|
142
|
+
runFetch();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return data;
|
|
146
|
+
},
|
|
147
|
+
get error() {
|
|
148
|
+
return error;
|
|
149
|
+
},
|
|
150
|
+
get loading() {
|
|
151
|
+
return loading;
|
|
152
|
+
},
|
|
153
|
+
get stale() {
|
|
154
|
+
checkStale();
|
|
155
|
+
return stale;
|
|
156
|
+
},
|
|
157
|
+
async refetch() {
|
|
158
|
+
lastArgs = undefined; // Force refetch
|
|
159
|
+
await runFetch();
|
|
160
|
+
},
|
|
161
|
+
invalidate() {
|
|
162
|
+
stale = true;
|
|
163
|
+
lastArgs = undefined;
|
|
164
|
+
runFetch();
|
|
165
|
+
},
|
|
166
|
+
// ── LoomResult combinators ──
|
|
167
|
+
unwrap() {
|
|
168
|
+
if (data !== undefined && error === undefined)
|
|
169
|
+
return data;
|
|
170
|
+
throw error ?? new Error("unwrap() called on loading RPC state");
|
|
171
|
+
},
|
|
172
|
+
unwrap_or(fallback) {
|
|
173
|
+
return (data !== undefined && error === undefined) ? data : fallback;
|
|
174
|
+
},
|
|
175
|
+
map(fn) {
|
|
176
|
+
if (data !== undefined && error === undefined)
|
|
177
|
+
return LoomResult.ok(fn(data));
|
|
178
|
+
return LoomResult.err(error ?? new Error("No data"));
|
|
179
|
+
},
|
|
180
|
+
map_err(fn) {
|
|
181
|
+
if (data !== undefined && error === undefined)
|
|
182
|
+
return LoomResult.ok(data);
|
|
183
|
+
return LoomResult.err(fn(error ?? new Error("No data")));
|
|
184
|
+
},
|
|
185
|
+
and_then(fn) {
|
|
186
|
+
if (data !== undefined && error === undefined)
|
|
187
|
+
return fn(data);
|
|
188
|
+
return LoomResult.err(error ?? new Error("No data"));
|
|
189
|
+
},
|
|
190
|
+
match(cases) {
|
|
191
|
+
if (loading && data === undefined && error === undefined && cases.loading) {
|
|
192
|
+
return cases.loading();
|
|
193
|
+
}
|
|
194
|
+
return (data !== undefined && error === undefined)
|
|
195
|
+
? cases.ok(data)
|
|
196
|
+
: cases.err(error ?? new Error("No data"));
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
// Fire initial fetch if eager
|
|
200
|
+
if (eager) {
|
|
201
|
+
runFetch();
|
|
202
|
+
}
|
|
203
|
+
return state;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=rpc.js.map
|
package/dist/rpc.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — MockTransport for testing
|
|
3
|
+
*
|
|
4
|
+
* Drop-in RpcTransport replacement that returns pre-configured responses.
|
|
5
|
+
* No network. No server. Just mocks.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { MockTransport } from "@toyz/loom-rpc/testing";
|
|
9
|
+
*
|
|
10
|
+
* const transport = new MockTransport();
|
|
11
|
+
* transport.mock("UserRouter", "getUser", { id: "1", name: "Test" });
|
|
12
|
+
* transport.mockError("UserRouter", "deleteUser", new Error("Forbidden"));
|
|
13
|
+
*
|
|
14
|
+
* app.provide(RpcTransport, transport);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { RpcTransport } from "./transport";
|
|
18
|
+
export declare class MockTransport extends RpcTransport {
|
|
19
|
+
private mocks;
|
|
20
|
+
private errors;
|
|
21
|
+
private calls;
|
|
22
|
+
private delays;
|
|
23
|
+
/** Register a mock response for a specific router.method */
|
|
24
|
+
mock<T>(router: string, method: string, response: T): this;
|
|
25
|
+
/** Register a mock error for a specific router.method */
|
|
26
|
+
mockError(router: string, method: string, error: Error): this;
|
|
27
|
+
/** Add an artificial delay (ms) for a specific router.method */
|
|
28
|
+
delay(router: string, method: string, ms: number): this;
|
|
29
|
+
/** All calls that have been made through this transport */
|
|
30
|
+
get history(): {
|
|
31
|
+
router: string;
|
|
32
|
+
method: string;
|
|
33
|
+
args: any[];
|
|
34
|
+
}[];
|
|
35
|
+
/** Clear all mocks and call history */
|
|
36
|
+
reset(): void;
|
|
37
|
+
/** Assert a specific call was made */
|
|
38
|
+
assertCalled(router: string, method: string, args?: any[]): void;
|
|
39
|
+
/** Assert a specific call was NOT made */
|
|
40
|
+
assertNotCalled(router: string, method: string): void;
|
|
41
|
+
call<T>(router: string, method: string, args: any[]): Promise<T>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,KAAK,CAA8D;IAC3E,OAAO,CAAC,MAAM,CAA6B;IAE3C,4DAA4D;IAC5D,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI;IAK1D,yDAAyD;IACzD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAK7D,gEAAgE;IAChE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAKvD,2DAA2D;IAC3D,IAAI,OAAO;gBAtBoB,MAAM;gBAAU,MAAM;cAAQ,GAAG,EAAE;QAwBjE;IAED,uCAAuC;IACvC,KAAK,IAAI,IAAI;IAOb,sCAAsC;IACtC,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI;IAahE,0CAA0C;IAC1C,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAW/C,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;CA0BvE"}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — MockTransport for testing
|
|
3
|
+
*
|
|
4
|
+
* Drop-in RpcTransport replacement that returns pre-configured responses.
|
|
5
|
+
* No network. No server. Just mocks.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { MockTransport } from "@toyz/loom-rpc/testing";
|
|
9
|
+
*
|
|
10
|
+
* const transport = new MockTransport();
|
|
11
|
+
* transport.mock("UserRouter", "getUser", { id: "1", name: "Test" });
|
|
12
|
+
* transport.mockError("UserRouter", "deleteUser", new Error("Forbidden"));
|
|
13
|
+
*
|
|
14
|
+
* app.provide(RpcTransport, transport);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { RpcTransport } from "./transport";
|
|
18
|
+
export class MockTransport extends RpcTransport {
|
|
19
|
+
mocks = new Map();
|
|
20
|
+
errors = new Map();
|
|
21
|
+
calls = [];
|
|
22
|
+
delays = new Map();
|
|
23
|
+
/** Register a mock response for a specific router.method */
|
|
24
|
+
mock(router, method, response) {
|
|
25
|
+
this.mocks.set(`${router}.${method}`, response);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
/** Register a mock error for a specific router.method */
|
|
29
|
+
mockError(router, method, error) {
|
|
30
|
+
this.errors.set(`${router}.${method}`, error);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
/** Add an artificial delay (ms) for a specific router.method */
|
|
34
|
+
delay(router, method, ms) {
|
|
35
|
+
this.delays.set(`${router}.${method}`, ms);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
/** All calls that have been made through this transport */
|
|
39
|
+
get history() {
|
|
40
|
+
return this.calls;
|
|
41
|
+
}
|
|
42
|
+
/** Clear all mocks and call history */
|
|
43
|
+
reset() {
|
|
44
|
+
this.mocks.clear();
|
|
45
|
+
this.errors.clear();
|
|
46
|
+
this.calls.length = 0;
|
|
47
|
+
this.delays.clear();
|
|
48
|
+
}
|
|
49
|
+
/** Assert a specific call was made */
|
|
50
|
+
assertCalled(router, method, args) {
|
|
51
|
+
const match = this.calls.find((c) => c.router === router && c.method === method &&
|
|
52
|
+
(args === undefined || JSON.stringify(c.args) === JSON.stringify(args)));
|
|
53
|
+
if (!match) {
|
|
54
|
+
throw new Error(`Expected ${router}.${method}(${args ? JSON.stringify(args) : "..."}) to have been called. ` +
|
|
55
|
+
`Calls: ${JSON.stringify(this.calls)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Assert a specific call was NOT made */
|
|
59
|
+
assertNotCalled(router, method) {
|
|
60
|
+
const match = this.calls.find((c) => c.router === router && c.method === method);
|
|
61
|
+
if (match) {
|
|
62
|
+
throw new Error(`Expected ${router}.${method} to NOT have been called, but it was.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async call(router, method, args) {
|
|
66
|
+
this.calls.push({ router, method, args });
|
|
67
|
+
const key = `${router}.${method}`;
|
|
68
|
+
// Simulate network delay if configured
|
|
69
|
+
const delayMs = this.delays.get(key);
|
|
70
|
+
if (delayMs) {
|
|
71
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
72
|
+
}
|
|
73
|
+
// Check for mock error first
|
|
74
|
+
const err = this.errors.get(key);
|
|
75
|
+
if (err)
|
|
76
|
+
throw err;
|
|
77
|
+
// Check for mock response
|
|
78
|
+
if (this.mocks.has(key)) {
|
|
79
|
+
const value = this.mocks.get(key);
|
|
80
|
+
// If it's a function, call it with args (dynamic mocks)
|
|
81
|
+
return typeof value === "function" ? value(...args) : value;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`[MockTransport] No mock registered for ${key}. ` +
|
|
84
|
+
`Register one with: transport.mock("${router}", "${method}", responseData)`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — Transport layer
|
|
3
|
+
*
|
|
4
|
+
* RpcTransport is the interface for how @rpc talks to the server.
|
|
5
|
+
* HttpTransport is the default implementation — plain fetch to
|
|
6
|
+
* POST /rpc/{router}/{method}.
|
|
7
|
+
*
|
|
8
|
+
* Swap transports via Loom's DI container:
|
|
9
|
+
* app.provide(RpcTransport, new WsTransport(...));
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Abstract transport — implement this to control how RPC calls reach the server.
|
|
13
|
+
*
|
|
14
|
+
* Registered as a DI service via `app.provide(RpcTransport, impl)`.
|
|
15
|
+
*/
|
|
16
|
+
export declare abstract class RpcTransport {
|
|
17
|
+
abstract call<T>(router: string, method: string, args: any[]): Promise<T>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Default HTTP transport — POST JSON to `/rpc/{router}/{method}`.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { app } from "@toyz/loom";
|
|
24
|
+
* import { RpcTransport, HttpTransport } from "@toyz/loom-rpc";
|
|
25
|
+
*
|
|
26
|
+
* app.provide(RpcTransport, new HttpTransport());
|
|
27
|
+
* // or with a custom base URL:
|
|
28
|
+
* app.provide(RpcTransport, new HttpTransport("https://api.example.com/rpc"));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class HttpTransport extends RpcTransport {
|
|
32
|
+
private readonly baseUrl;
|
|
33
|
+
private readonly headers;
|
|
34
|
+
constructor(baseUrl?: string, headers?: Record<string, string>);
|
|
35
|
+
call<T>(router: string, method: string, args: any[]): Promise<T>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Structured error from an RPC call.
|
|
39
|
+
*/
|
|
40
|
+
export declare class RpcError extends Error {
|
|
41
|
+
readonly status?: number | undefined;
|
|
42
|
+
readonly router?: string | undefined;
|
|
43
|
+
readonly method?: string | undefined;
|
|
44
|
+
readonly code?: string | undefined;
|
|
45
|
+
constructor(message: string, status?: number | undefined, router?: string | undefined, method?: string | undefined, code?: string | undefined);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;GAIG;AACH,8BAAsB,YAAY;IAChC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;CAC1E;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAErC,OAAO,SAAS,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM;IAO5D,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;CAqCvE;AAED;;GAEG;AACH,qBAAa,QAAS,SAAQ,KAAK;aAGf,MAAM,CAAC,EAAE,MAAM;aACf,MAAM,CAAC,EAAE,MAAM;aACf,MAAM,CAAC,EAAE,MAAM;aACf,IAAI,CAAC,EAAE,MAAM;gBAJ7B,OAAO,EAAE,MAAM,EACC,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,IAAI,CAAC,EAAE,MAAM,YAAA;CAKhC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — Transport layer
|
|
3
|
+
*
|
|
4
|
+
* RpcTransport is the interface for how @rpc talks to the server.
|
|
5
|
+
* HttpTransport is the default implementation — plain fetch to
|
|
6
|
+
* POST /rpc/{router}/{method}.
|
|
7
|
+
*
|
|
8
|
+
* Swap transports via Loom's DI container:
|
|
9
|
+
* app.provide(RpcTransport, new WsTransport(...));
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Abstract transport — implement this to control how RPC calls reach the server.
|
|
13
|
+
*
|
|
14
|
+
* Registered as a DI service via `app.provide(RpcTransport, impl)`.
|
|
15
|
+
*/
|
|
16
|
+
export class RpcTransport {
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Default HTTP transport — POST JSON to `/rpc/{router}/{method}`.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { app } from "@toyz/loom";
|
|
23
|
+
* import { RpcTransport, HttpTransport } from "@toyz/loom-rpc";
|
|
24
|
+
*
|
|
25
|
+
* app.provide(RpcTransport, new HttpTransport());
|
|
26
|
+
* // or with a custom base URL:
|
|
27
|
+
* app.provide(RpcTransport, new HttpTransport("https://api.example.com/rpc"));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class HttpTransport extends RpcTransport {
|
|
31
|
+
baseUrl;
|
|
32
|
+
headers;
|
|
33
|
+
constructor(baseUrl = "/rpc", headers = {}) {
|
|
34
|
+
super();
|
|
35
|
+
// Strip trailing slash
|
|
36
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
37
|
+
this.headers = headers;
|
|
38
|
+
}
|
|
39
|
+
async call(router, method, args) {
|
|
40
|
+
const url = `${this.baseUrl}/${router}/${method}`;
|
|
41
|
+
const body = { args };
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
...this.headers,
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const text = await res.text().catch(() => res.statusText);
|
|
52
|
+
throw new RpcError(`RPC ${router}.${method} failed: ${res.status} ${text}`, res.status, router, method);
|
|
53
|
+
}
|
|
54
|
+
const envelope = await res.json();
|
|
55
|
+
if (envelope.error) {
|
|
56
|
+
throw new RpcError(envelope.error.message, undefined, router, method, envelope.error.code);
|
|
57
|
+
}
|
|
58
|
+
return envelope.data;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Structured error from an RPC call.
|
|
63
|
+
*/
|
|
64
|
+
export class RpcError extends Error {
|
|
65
|
+
status;
|
|
66
|
+
router;
|
|
67
|
+
method;
|
|
68
|
+
code;
|
|
69
|
+
constructor(message, status, router, method, code) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.status = status;
|
|
72
|
+
this.router = router;
|
|
73
|
+
this.method = method;
|
|
74
|
+
this.code = code;
|
|
75
|
+
this.name = "RpcError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH;;;;GAIG;AACH,MAAM,OAAgB,YAAY;CAEjC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,aAAc,SAAQ,YAAY;IAC5B,OAAO,CAAS;IAChB,OAAO,CAAyB;IAEjD,YAAY,OAAO,GAAG,MAAM,EAAE,UAAkC,EAAE;QAChE,KAAK,EAAE,CAAC;QACR,uBAAuB;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,IAAI,CAAI,MAAc,EAAE,MAAc,EAAE,IAAW;QACvD,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;QAClD,MAAM,IAAI,GAAe,EAAE,IAAI,EAAE,CAAC;QAElC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,GAAG,IAAI,CAAC,OAAO;aAChB;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC1D,MAAM,IAAI,QAAQ,CAChB,OAAO,MAAM,IAAI,MAAM,YAAY,GAAG,CAAC,MAAM,IAAI,IAAI,EAAE,EACvD,GAAG,CAAC,MAAM,EACV,MAAM,EACN,MAAM,CACP,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAmB,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAElD,IAAI,QAAQ,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,IAAI,QAAQ,CAChB,QAAQ,CAAC,KAAK,CAAC,OAAO,EACtB,SAAS,EACT,MAAM,EACN,MAAM,EACN,QAAQ,CAAC,KAAK,CAAC,IAAI,CACpB,CAAC;QACJ,CAAC;QAED,OAAO,QAAQ,CAAC,IAAS,CAAC;IAC5B,CAAC;CACF;AAED;;GAEG;AACH,MAAM,OAAO,QAAS,SAAQ,KAAK;IAGf;IACA;IACA;IACA;IALlB,YACE,OAAe,EACC,MAAe,EACf,MAAe,EACf,MAAe,EACf,IAAa;QAE7B,KAAK,CAAC,OAAO,CAAC,CAAC;QALC,WAAM,GAAN,MAAM,CAAS;QACf,WAAM,GAAN,MAAM,CAAS;QACf,WAAM,GAAN,MAAM,CAAS;QACf,SAAI,GAAJ,IAAI,CAAS;QAG7B,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoomRPC — Type utilities
|
|
3
|
+
*
|
|
4
|
+
* Extract method names, parameter types, and return types from contract classes.
|
|
5
|
+
* Powers the type-safe @rpc and @mutate decorators.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extract callable method names from a contract class.
|
|
9
|
+
* Filters out non-function properties so only procedure names are valid.
|
|
10
|
+
*/
|
|
11
|
+
export type RpcMethods<T> = {
|
|
12
|
+
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
|
|
13
|
+
}[keyof T] & string;
|
|
14
|
+
/**
|
|
15
|
+
* Extract the parameter types for a specific method on a contract class.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* type Args = InferArgs<UserRouter, "getUser">; // [id: string]
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export type InferArgs<T, M extends keyof T> = T[M] extends (...args: infer A) => any ? A : never;
|
|
22
|
+
/**
|
|
23
|
+
* Extract the return type for a specific method on a contract class.
|
|
24
|
+
* Unwraps Promise<T> to T automatically.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* type Result = InferReturn<UserRouter, "getUser">; // User
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export type InferReturn<T, M extends keyof T> = T[M] extends (...args: any[]) => Promise<infer R> ? R : T[M] extends (...args: any[]) => infer R ? R : never;
|
|
31
|
+
/**
|
|
32
|
+
* Configuration for @rpc query decorator.
|
|
33
|
+
*/
|
|
34
|
+
export interface RpcQueryOptions<TRouter, TMethod extends RpcMethods<TRouter>> {
|
|
35
|
+
/** Extract procedure args from element state. Re-evaluates on reactive changes. */
|
|
36
|
+
fn?: (el: any) => InferArgs<TRouter, TMethod>;
|
|
37
|
+
/** SWR cache duration in ms (default: 0 = always refetch) */
|
|
38
|
+
staleTime?: number;
|
|
39
|
+
/** Whether to fetch on connect (default: true) */
|
|
40
|
+
eager?: boolean;
|
|
41
|
+
/** Number of retries on failure with exponential backoff (default: 0) */
|
|
42
|
+
retry?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* State container for a mutation — manual `.call()`, tracks loading/error.
|
|
46
|
+
*/
|
|
47
|
+
export interface RpcMutator<TArgs extends any[], TReturn> {
|
|
48
|
+
/** Execute the mutation with the given arguments */
|
|
49
|
+
call(...args: TArgs): Promise<TReturn>;
|
|
50
|
+
/** True while the mutation is in flight */
|
|
51
|
+
readonly loading: boolean;
|
|
52
|
+
/** Error from the last mutation attempt, or null */
|
|
53
|
+
readonly error: Error | null;
|
|
54
|
+
/** Data from the last successful mutation, or undefined */
|
|
55
|
+
readonly data: TReturn | undefined;
|
|
56
|
+
/** Reset the mutator state (clear data, error, loading) */
|
|
57
|
+
reset(): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Wire protocol envelope for RPC requests.
|
|
61
|
+
*/
|
|
62
|
+
export interface RpcRequest {
|
|
63
|
+
args: any[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Wire protocol envelope for RPC responses.
|
|
67
|
+
*/
|
|
68
|
+
export interface RpcResponse<T = any> {
|
|
69
|
+
data?: T;
|
|
70
|
+
error?: {
|
|
71
|
+
message: string;
|
|
72
|
+
code?: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;KACzB,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK;CACjE,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IACxC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAC1C,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GACrD,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,OAAO,EAAE,OAAO,SAAS,UAAU,CAAC,OAAO,CAAC;IAC3E,mFAAmF;IACnF,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC9C,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU,CAAC,KAAK,SAAS,GAAG,EAAE,EAAE,OAAO;IACtD,oDAAoD;IACpD,IAAI,CAAC,GAAG,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,2DAA2D;IAC3D,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,GAAG,EAAE,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,GAAG;IAClC,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toyz/loom-rpc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Type-safe, decorator-driven RPC for Loom — server-agnostic, transport-swappable",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "toyz",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Toyz/loom.git",
|
|
11
|
+
"directory": "loom-rpc"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./testing": {
|
|
26
|
+
"types": "./dist/testing.d.ts",
|
|
27
|
+
"import": "./dist/testing.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"dev": "tsc --watch",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"clean": "rm -rf dist",
|
|
35
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@toyz/loom": "^0.12.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"happy-dom": "^17.4.4",
|
|
42
|
+
"typescript": "^5.7.0",
|
|
43
|
+
"vitest": "^3.0.0"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"loom",
|
|
47
|
+
"rpc",
|
|
48
|
+
"decorators",
|
|
49
|
+
"type-safe",
|
|
50
|
+
"web-components"
|
|
51
|
+
]
|
|
52
|
+
}
|