@covenant-rpc/react 0.1.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # @covenant-rpc/react
2
+
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 8061179: Initial publish
8
+ - Updated dependencies [8061179]
9
+ - @covenant-rpc/client@0.1.3
10
+ - @covenant-rpc/core@0.1.3
11
+
12
+ ## 0.1.2
13
+
14
+ ### Patch Changes
15
+
16
+ - Fix all packages
package/CLAUDE.md ADDED
@@ -0,0 +1,107 @@
1
+ ---
2
+
3
+ Default to using Bun instead of Node.js.
4
+
5
+ - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
6
+ - Use `bun test` instead of `jest` or `vitest`
7
+ - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
8
+ - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
9
+ - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
10
+ - Bun automatically loads .env, so don't use dotenv.
11
+
12
+ ## APIs
13
+
14
+ - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
15
+ - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
16
+ - `Bun.redis` for Redis. Don't use `ioredis`.
17
+ - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
18
+ - `WebSocket` is built-in. Don't use `ws`.
19
+ - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
20
+ - Bun.$`ls` instead of execa.
21
+
22
+ ## Testing
23
+
24
+ Use `bun test` to run tests.
25
+
26
+ ```ts#index.test.ts
27
+ import { test, expect } from "bun:test";
28
+
29
+ test("hello world", () => {
30
+ expect(1).toBe(1);
31
+ });
32
+ ```
33
+
34
+ ## Frontend
35
+
36
+ Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
37
+
38
+ Server:
39
+
40
+ ```ts#index.ts
41
+ import index from "./index.html"
42
+
43
+ Bun.serve({
44
+ routes: {
45
+ "/": index,
46
+ "/api/users/:id": {
47
+ GET: (req) => {
48
+ return new Response(JSON.stringify({ id: req.params.id }));
49
+ },
50
+ },
51
+ },
52
+ // optional websocket support
53
+ websocket: {
54
+ open: (ws) => {
55
+ ws.send("Hello, world!");
56
+ },
57
+ message: (ws, message) => {
58
+ ws.send(message);
59
+ },
60
+ close: (ws) => {
61
+ // handle close
62
+ }
63
+ },
64
+ development: {
65
+ hmr: true,
66
+ console: true,
67
+ }
68
+ })
69
+ ```
70
+
71
+ HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
72
+
73
+ ```html#index.html
74
+ <html>
75
+ <body>
76
+ <h1>Hello, world!</h1>
77
+ <script type="module" src="./frontend.tsx"></script>
78
+ </body>
79
+ </html>
80
+ ```
81
+
82
+ With the following `frontend.tsx`:
83
+
84
+ ```tsx#frontend.tsx
85
+ import React from "react";
86
+
87
+ // import .css files directly and it works
88
+ import './index.css';
89
+
90
+ import { createRoot } from "react-dom/client";
91
+
92
+ const root = createRoot(document.body);
93
+
94
+ export default function Frontend() {
95
+ return <h1>Hello, world!</h1>;
96
+ }
97
+
98
+ root.render(<Frontend />);
99
+ ```
100
+
101
+ Then, run index.ts
102
+
103
+ ```sh
104
+ bun --hot ./index.ts
105
+ ```
106
+
107
+ For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @covenant/react
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.2.19. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/helpers.ts ADDED
@@ -0,0 +1,105 @@
1
+ import type { AsyncHook } from ".";
2
+ import { useEffect, useState } from "react";
3
+ // definition for AsyncHook:
4
+ // export type AsyncHook<T> = {
5
+ // loading: true,
6
+ // data: null,
7
+ // error: null,
8
+ // } | {
9
+ // loading: false,
10
+ // data: T,
11
+ // error: null,
12
+ // } | {
13
+ // loading: false,
14
+ // data: null,
15
+ // error: ReactProcedureError,
16
+ // }
17
+
18
+
19
+ export type CallbackFunction<T> = () => T;
20
+
21
+
22
+ // useDataStream is best described by an example. Let's say we have a chatFunction which has two methods:
23
+ //
24
+ // getCurrentMessages: () => Promise<Message[]>
25
+ // connectToUpdates: (onUpdate: () => Message): Promise<void>
26
+ // disconnect: () => void
27
+ //
28
+ // We want to fetch all messages as well as continue to listen to them. useDataStream should handle this
29
+ // as well as various race conditions
30
+
31
+ export function useDataStream<T>({ initialFetch, connect, disconnect }: {
32
+ initialFetch: () => Promise<T[]>,
33
+ connect: (callback: CallbackFunction<T>) => Promise<void>,
34
+ disconnect: () => void
35
+ }): AsyncHook<T[]> {
36
+ const [state, setState] = useState<AsyncHook<T[]>>({
37
+ loading: true,
38
+ data: null,
39
+ error: null
40
+ });
41
+
42
+ useEffect(() => {
43
+ let isMounted = true;
44
+ let items: T[] = [];
45
+ let isConnecting = false;
46
+
47
+ const handleNewItem = (newItem: T) => {
48
+ if (!isMounted) return;
49
+
50
+ // Add new item to the list
51
+ items = [...items, newItem];
52
+ setState({
53
+ loading: false,
54
+ data: items,
55
+ error: null
56
+ });
57
+ };
58
+
59
+ const initialize = async () => {
60
+ try {
61
+ // Fetch initial data first
62
+ const initialData = await initialFetch();
63
+
64
+ if (!isMounted) return;
65
+
66
+ items = initialData;
67
+ setState({
68
+ loading: false,
69
+ data: items,
70
+ error: null
71
+ });
72
+
73
+ // Connect to updates
74
+ // Note: The type signature indicates CallbackFunction<T> = () => T,
75
+ // but the practical use case requires (newItem: T) => void
76
+ // Using type assertion to work with the expected runtime behavior
77
+ isConnecting = true;
78
+ await connect(handleNewItem as any);
79
+
80
+ } catch (error) {
81
+ if (!isMounted) return;
82
+
83
+ setState({
84
+ loading: false,
85
+ data: null,
86
+ error: {
87
+ code: 500,
88
+ message: error instanceof Error ? error.message : 'Unknown error'
89
+ }
90
+ });
91
+ }
92
+ };
93
+
94
+ initialize();
95
+
96
+ return () => {
97
+ isMounted = false;
98
+ if (isConnecting) {
99
+ disconnect();
100
+ }
101
+ };
102
+ }, [initialFetch, connect, disconnect]);
103
+
104
+ return state;
105
+ }
package/index.ts ADDED
@@ -0,0 +1,210 @@
1
+ import type { ProcedureMap, ChannelMap } from "@covenant-rpc/core";
2
+ import { InferChannelServerMessage, type InferChannelConnectionRequest, type InferChannelParams } from "@covenant-rpc/core/channel";
3
+ import { CovenantClient, type MutationKey, type QueryKey } from "@covenant-rpc/client";
4
+ import type { InferProcedureInputs, InferProcedureOutputs } from "@covenant-rpc/core/procedure";
5
+ import { useEffect, useState } from "react";
6
+
7
+
8
+ export interface ReactProcedureError {
9
+ code: number;
10
+ message: string;
11
+ }
12
+
13
+ export type AsyncHook<T> = {
14
+ loading: true,
15
+ data: null,
16
+ error: null,
17
+ } | {
18
+ loading: false,
19
+ data: T,
20
+ error: null,
21
+ } | {
22
+ loading: false,
23
+ data: null,
24
+ error: ReactProcedureError,
25
+ }
26
+
27
+ type Listener = (k: AsyncHook<any>) => void;
28
+
29
+
30
+ export class CovenantReactClient<P extends ProcedureMap, C extends ChannelMap> extends CovenantClient<P, C> {
31
+ // this is any becuse we have no other choice. You just gotta trust me on this one.
32
+ private cache: Map<string, AsyncHook<any>> = new Map();
33
+ private listenes: Map<string, Listener[]> = new Map();
34
+
35
+ useQuery<Q extends QueryKey<P>>(procedureName: Q, inputs: InferProcedureInputs<P[Q]>): AsyncHook<InferProcedureOutputs<P[Q]>> {
36
+ const [state, setState] = useState<AsyncHook<InferProcedureOutputs<P[Q]>>>({
37
+ loading: true,
38
+ data: null,
39
+ error: null,
40
+ });
41
+
42
+ useEffect(() => {
43
+ const fn = async () => {
44
+ setState({
45
+ loading: true,
46
+ data: null,
47
+ error: null,
48
+ });
49
+
50
+ const response = await this.query(procedureName, inputs);
51
+
52
+ if (response.success) {
53
+ setState({
54
+ loading: false,
55
+ data: response.data,
56
+ error: null,
57
+ });
58
+ } else {
59
+ setState({
60
+ loading: false,
61
+ data: null,
62
+ error: response.error,
63
+ });
64
+ }
65
+ }
66
+ fn();
67
+ }, [inputs]);
68
+
69
+ return state;
70
+ }
71
+
72
+ useMutation<Q extends MutationKey<P>>(procedureName: Q, inputs: InferProcedureInputs<P[Q]>): AsyncHook<InferProcedureOutputs<P[Q]>> {
73
+ const [state, setState] = useState<AsyncHook<InferProcedureOutputs<P[Q]>>>({
74
+ loading: true,
75
+ data: null,
76
+ error: null,
77
+ });
78
+
79
+ useEffect(() => {
80
+ const fn = async () => {
81
+ setState({
82
+ loading: true,
83
+ data: null,
84
+ error: null,
85
+ });
86
+
87
+ const response = await this.mutate(procedureName, inputs);
88
+
89
+ if (response.success) {
90
+ setState({
91
+ loading: false,
92
+ data: response.data,
93
+ error: null,
94
+ });
95
+ } else {
96
+ setState({
97
+ loading: false,
98
+ data: null,
99
+ error: response.error,
100
+ });
101
+ }
102
+ }
103
+ fn();
104
+ }, [inputs]);
105
+
106
+ return state;
107
+ }
108
+
109
+ useListenedQuery<Q extends QueryKey<P>>(procedureName: Q, inputs: InferProcedureInputs<P[Q]>): AsyncHook<InferProcedureOutputs<P[Q]>> {
110
+ const [state, setState] = useState<AsyncHook<InferProcedureOutputs<P[Q]>>>({
111
+ loading: true,
112
+ data: null,
113
+ error: null,
114
+ });
115
+
116
+
117
+ useEffect(() => {
118
+ return this.listen(procedureName, inputs, ({ data, error }) => {
119
+
120
+ if (error !== null) {
121
+ setState({
122
+ loading: false,
123
+ error: error,
124
+ data: null,
125
+ });
126
+ } else {
127
+ setState({
128
+ loading: false,
129
+ error: null,
130
+ data: data,
131
+ });
132
+ }
133
+ });
134
+ }, [inputs])
135
+
136
+
137
+ return state;
138
+ }
139
+
140
+ private createCachedQuery<Q extends QueryKey<P>>(procedureName: Q, inputs: InferProcedureInputs<P[Q]>) {
141
+ this.listen(procedureName, inputs, ({ data, error }) => {
142
+ const state: AsyncHook<InferProcedureOutputs<P[Q]>> = error === null ? {
143
+ loading: false,
144
+ error: null,
145
+ data: data
146
+ } : {
147
+ loading: false,
148
+ error: error,
149
+ data: null,
150
+ }
151
+ const key = this.getCacheKey(String(procedureName), inputs);
152
+
153
+ const listeners = this.listenes.get(key) ?? [];
154
+ this.cache.set(key, state);
155
+
156
+ for (const l of listeners) {
157
+ l(state);
158
+ }
159
+ })
160
+ }
161
+
162
+ private getCacheKey(procedureName: string, inputs: any) {
163
+ return `${procedureName}-${JSON.stringify(inputs)}`;
164
+ }
165
+
166
+ private addCacheListener(key: string, l: Listener) {
167
+ const current = this.listenes.get(key) ?? [];
168
+ current.push(l);
169
+ this.listenes.set(key, current);
170
+ }
171
+ private removeCacheListener(listener: Listener) {
172
+ for (const [k, v] of this.listenes) {
173
+ if (v.find(l => l === listener) !== undefined) {
174
+ const n = v.filter(l => l !== listener);
175
+ this.listenes.set(k, n);
176
+ }
177
+ }
178
+ }
179
+
180
+
181
+ useCachedQuery<Q extends QueryKey<P>>(procedureName: Q, inputs: InferProcedureInputs<P[Q]>): AsyncHook<InferProcedureOutputs<P[Q]>> {
182
+ const k = this.getCacheKey(String(procedureName), inputs);
183
+ const [state, setState] = useState<AsyncHook<InferProcedureOutputs<P[Q]>>>(this.cache.has(k) ? this.cache.get(k)! : {
184
+ loading: true,
185
+ data: null,
186
+ error: null,
187
+ });
188
+
189
+ useEffect(() => {
190
+ // we have to refetch the key in case inputs changed
191
+ const newKey = this.getCacheKey(String(procedureName), inputs);
192
+
193
+ if (!this.cache.has(k)) {
194
+ this.createCachedQuery(procedureName, inputs);
195
+ }
196
+ const l: Listener = (s) => setState(s);
197
+
198
+ this.addCacheListener(newKey, l);
199
+
200
+ return () => this.removeCacheListener(l);
201
+
202
+ }, [setState, inputs]);
203
+
204
+
205
+ return state;
206
+ }
207
+ }
208
+
209
+
210
+
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@covenant-rpc/react",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "version": "0.1.3",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "devDependencies": {
10
+ "@types/bun": "latest"
11
+ },
12
+ "peerDependencies": {
13
+ "typescript": "^5"
14
+ },
15
+ "dependencies": {
16
+ "react": "^19.1.1",
17
+ "@covenant-rpc/core": "workspace:*",
18
+ "@covenant-rpc/client": "workspace:*"
19
+ }
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }