@signal-spec/react-client 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,30 @@
1
- # Temporary Holding Version
1
+ # SignalSpec React Client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Generate typed React hooks and DTOs from a SignalSpec JSON document:
4
+
5
+ ```sh
6
+ signal-spec generate --input=./signal-spec.json --output=./src/generated --backend-uri=https://localhost:49363
7
+ ```
8
+
9
+ During development, use `vp run generate` followed by the same flags.
10
+ `--input` accepts a local JSON file, a tsconfig path alias such as `@/spec.json`,
11
+ or an HTTP(S) URL. `--output` accepts a local folder or tsconfig alias.
12
+ Paths resolve from the current working directory. Quote paths containing spaces.
13
+ All three options are required; both `--flag=value` and `--flag value` work.
14
+
15
+ Generated hooks automatically import `config.ts`, which initializes the shared
16
+ Zustand connection store with the supplied backend URI. No provider is required.
17
+ After importing generated hooks, applications can override the default:
18
+
19
+ ```ts
20
+ import { useSignalRConnectionsStore } from "@signal-spec/react-client";
21
+
22
+ useSignalRConnectionsStore.setState({ backendUri: "https://api.example.com" });
23
+ ```
24
+
25
+ Overrides affect new connections. Existing connections keep their original URL.
26
+ The URI is shared across generated clients in the same application.
27
+ Generation overwrites matching files, retains stale files, and may leave partial
28
+ output if TypeSynth reports an error. Generation failures return a nonzero exit code.
29
+
30
+ Development: `vp install`, `vp check`, `vp test`, and `vp run build`.
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { spawn } from "node:child_process";
8
+
9
+ const help = `Usage: signal-spec generate --input <json-or-url> --output <folder> --backend-uri <uri>
10
+
11
+ Input and output paths resolve from the current directory, including tsconfig aliases.
12
+ The backend URI becomes the generated default and can be overridden at runtime.
13
+ Options accept --flag=value or --flag value. Use --help to show this message.`;
14
+
15
+ try {
16
+ const args = process.argv.slice(2);
17
+ if (!args.length || args.includes("--help")) {
18
+ console.log(help);
19
+ } else {
20
+ if (args.shift() !== "generate") throw new Error('Expected command "generate".');
21
+ const flags = {};
22
+ while (args.length) {
23
+ const arg = args.shift();
24
+ const match = /^--(input|output|backend-uri)(?:=(.*))?$/.exec(arg);
25
+ if (!match) throw new Error(`Unknown argument: ${arg}`);
26
+ const [, key, inline] = match;
27
+ if (Object.hasOwn(flags, key)) throw new Error(`Duplicate option --${key}.`);
28
+ const value = inline ?? args.shift();
29
+ if (!value || value.startsWith("--")) throw new Error(`--${key} requires a value.`);
30
+ flags[key] = value;
31
+ }
32
+ for (const key of ["input", "output", "backend-uri"]) {
33
+ if (!flags[key]) throw new Error(`Missing required option --${key}.`);
34
+ }
35
+ const require = createRequire(import.meta.url);
36
+ const typesynth = resolve(dirname(require.resolve("typesynth/package.json")), "dist/bin/cli.js");
37
+ const sources = fileURLToPath(new URL("../src/generation/", import.meta.url));
38
+ let temporary;
39
+ try {
40
+ if (/^https?:\/\//i.test(flags.input)) {
41
+ const response = await fetch(flags.input);
42
+ if (!response.ok) throw new Error(`Failed to fetch spec: HTTP ${response.status}`);
43
+ const text = await response.text();
44
+ JSON.parse(text);
45
+ temporary = await mkdtemp(resolve(tmpdir(), "signal-spec-"));
46
+ flags.input = resolve(temporary, "spec.json");
47
+ await writeFile(flags.input, text);
48
+ }
49
+ const child = spawn(process.execPath, [typesynth, "generate", `--sources=${sources}`,
50
+ `--input=${flags.input}`, `--output=${flags.output}`], {
51
+ stdio: "inherit",
52
+ env: { ...process.env, SIGNAL_SPEC_BACKEND_URI: flags["backend-uri"] },
53
+ });
54
+ process.exitCode = await new Promise((done, reject) => {
55
+ child.on("error", reject);
56
+ child.on("exit", code => done(code ?? 1));
57
+ });
58
+ } finally {
59
+ if (temporary) await rm(temporary, { recursive: true, force: true });
60
+ }
61
+ }
62
+ } catch (error) {
63
+ console.error(error instanceof Error ? error.message : String(error));
64
+ process.exitCode = 1;
65
+ }
66
+
67
+
@@ -0,0 +1,39 @@
1
+ import * as SignalR from "@microsoft/signalr";
2
+ import { HubConnection } from "@microsoft/signalr";
3
+ //#region src/helpers/useSignalRConnectionsStore.d.ts
4
+ export interface SignalRConnectionData {
5
+ connection: SignalR.HubConnection;
6
+ componentsSubscribed: number;
7
+ }
8
+ export declare function isConnectionAlive(connectionState: SignalR.HubConnectionState | null | undefined): boolean | null | undefined;
9
+ export interface SignalRConnectionsStore {
10
+ backendUri: string;
11
+ EnableSignalRDebug: boolean;
12
+ connectionsStore: Map<string, SignalRConnectionData>;
13
+ createConnection: (hubName: string) => Promise<SignalRConnectionData>;
14
+ setupEventHandlers: <T>(hubName: string, handlers: Partial<T>, onConnected?: (connection: SignalR.HubConnection) => void) => Promise<void>;
15
+ deleteConnection: <T>(hubName: string, handlers: Partial<T>) => void;
16
+ }
17
+ export declare const useSignalRConnectionsStore: import("zustand").UseBoundStore<import("zustand").StoreApi<SignalRConnectionsStore>>;
18
+ export declare const useSignalRConnections: import("zustand").UseBoundStore<import("zustand").StoreApi<SignalRConnectionsStore>>;
19
+ //#endregion
20
+ //#region src/helpers/useStableHandlers.d.ts
21
+ /**
22
+ * This is basically MUI's {@link useEventCallback} hook, but designed for objects
23
+ * containing handlers instead of a singular handler.
24
+ * @param handlers
25
+ */
26
+ export declare function useStableHandlers<T extends Record<string, (...args: any[]) => any>>(handlers: T | undefined): T;
27
+ //#endregion
28
+ //#region src/generation/safeInvokeHubMethod.d.ts
29
+ type Warning = {
30
+ warning: string;
31
+ };
32
+ /**
33
+ * Safely invokes the SignalR hub method, i.e. it will only throw in case of an actual error,
34
+ * and will return `Warning` object in case the connection hasn't yet been established or
35
+ * has been already closed.
36
+ */
37
+ export declare function safeInvokeHubMethod(connection: HubConnection | null, methodName: string, ...args: any[]): Promise<Warning | any>;
38
+ //#endregion
39
+ export {};
package/dist/index.mjs ADDED
@@ -0,0 +1,100 @@
1
+ import { create } from "zustand";
2
+ import * as SignalR from "@microsoft/signalr";
3
+ import { HubConnectionState } from "@microsoft/signalr";
4
+ import { useLayoutEffect, useMemo, useRef } from "react";
5
+ //#region src/helpers/useSignalRConnectionsStore.ts
6
+ function isConnectionAlive(connectionState) {
7
+ return connectionState && connectionState !== SignalR.HubConnectionState.Disconnected && connectionState !== SignalR.HubConnectionState.Disconnecting;
8
+ }
9
+ const useSignalRConnectionsStore = create((set, get) => ({
10
+ backendUri: "",
11
+ EnableSignalRDebug: false,
12
+ connectionsStore: /* @__PURE__ */ new Map(),
13
+ async createConnection(hubName) {
14
+ let data = get().connectionsStore.get(hubName);
15
+ if (!data) {
16
+ const { backendUri, EnableSignalRDebug } = get();
17
+ data = {
18
+ connection: new SignalR.HubConnectionBuilder().withUrl(backendUri + "/hubs/" + hubName, { withCredentials: true }).configureLogging(EnableSignalRDebug ? SignalR.LogLevel.Information : SignalR.LogLevel.None).withAutomaticReconnect().build(),
19
+ componentsSubscribed: 0
20
+ };
21
+ const connectionsStore = new Map(get().connectionsStore);
22
+ connectionsStore.set(hubName, data);
23
+ set({ connectionsStore });
24
+ }
25
+ if (!isConnectionAlive(data.connection.state)) await data.connection.start().catch((error) => {
26
+ if (get().EnableSignalRDebug) console.error(error);
27
+ });
28
+ return data;
29
+ },
30
+ async setupEventHandlers(hubName, handlers, onConnected) {
31
+ let data = get().connectionsStore.get(hubName);
32
+ if (!data) {
33
+ data = await get().createConnection(hubName);
34
+ onConnected?.(data.connection);
35
+ }
36
+ for (const key of Object.keys(handlers)) {
37
+ const handler = handlers[key];
38
+ if (handler !== void 0) data.connection.on(key, handler);
39
+ }
40
+ data.componentsSubscribed++;
41
+ set({ connectionsStore: new Map(get().connectionsStore) });
42
+ },
43
+ deleteConnection(hubName, handlers) {
44
+ const data = get().connectionsStore.get(hubName);
45
+ if (!data) return;
46
+ for (const key of Object.keys(handlers)) {
47
+ const handler = handlers[key];
48
+ if (handler !== void 0) data.connection.off(key, handler);
49
+ }
50
+ data.componentsSubscribed--;
51
+ const connectionsStore = new Map(get().connectionsStore);
52
+ if (data.componentsSubscribed === 0) {
53
+ data.connection.stop().catch((error) => {
54
+ if (get().EnableSignalRDebug) console.error(error);
55
+ });
56
+ connectionsStore.delete(hubName);
57
+ }
58
+ set({ connectionsStore });
59
+ }
60
+ }));
61
+ const useSignalRConnections = useSignalRConnectionsStore;
62
+ //#endregion
63
+ //#region src/helpers/useStableHandlers.ts
64
+ /**
65
+ * This is basically MUI's {@link useEventCallback} hook, but designed for objects
66
+ * containing handlers instead of a singular handler.
67
+ * @param handlers
68
+ */
69
+ function useStableHandlers(handlers) {
70
+ const ref = useRef(handlers);
71
+ useLayoutEffect(() => {
72
+ ref.current = handlers;
73
+ });
74
+ return useMemo(() => {
75
+ const result = {};
76
+ if (!handlers) return result;
77
+ for (const key of Object.keys(handlers)) result[key] = (...args) => {
78
+ return ref.current[key](...args);
79
+ };
80
+ return result;
81
+ }, []);
82
+ }
83
+ //#endregion
84
+ //#region src/generation/safeInvokeHubMethod.ts
85
+ /**
86
+ * Safely invokes the SignalR hub method, i.e. it will only throw in case of an actual error,
87
+ * and will return `Warning` object in case the connection hasn't yet been established or
88
+ * has been already closed.
89
+ */
90
+ async function safeInvokeHubMethod(connection, methodName, ...args) {
91
+ if (connection?.state !== HubConnectionState.Connected) return { warning: "Skipped invocation: not connected" };
92
+ try {
93
+ return await connection.invoke(methodName, ...args);
94
+ } catch (err) {
95
+ if (err instanceof Error && err.message.includes("underlying connection being closed")) return { warning: "Skipped invocation: connection is being closed" };
96
+ throw err;
97
+ }
98
+ }
99
+ //#endregion
100
+ export { isConnectionAlive, safeInvokeHubMethod, useSignalRConnections, useSignalRConnectionsStore, useStableHandlers };
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@signal-spec/react-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "A SignalR client generator for React based on the SignalSpec.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/IvanKobtsev/SignalSpec.ReactClient.git"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin",
12
+ "src/generation"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": "./dist/index.mjs",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "typesynth": "0.1.1",
24
+ "zustand": "^5.0.16"
25
+ },
26
+ "peerDependencies": {
27
+ "@microsoft/signalr": "^10.0.11",
28
+ "react": "^18.0.0 || ^19.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^26.6.5",
32
+ "@types/react": "^19.3.0",
33
+ "typescript": "^7.0.2",
34
+ "vite-plus": "1.3.0"
35
+ },
36
+ "devEngines": {
37
+ "packageManager": {
38
+ "name": "pnpm",
39
+ "version": "12.16.1",
40
+ "onFail": "download"
41
+ }
42
+ },
43
+ "bin": {
44
+ "signal-spec": "bin/signal-spec.mjs"
45
+ },
46
+ "scripts": {
47
+ "build": "vp pack",
48
+ "dev": "vp pack --watch",
49
+ "test": "vp test",
50
+ "check": "vp check",
51
+ "generate": "node ./bin/signal-spec.mjs generate"
52
+ }
6
53
  }
@@ -0,0 +1,108 @@
1
+ export enum SignalRMethodType {
2
+ Default = "default",
3
+ FrontendMethod = "frontendMethod",
4
+ BackendMethod = "backendMethod",
5
+ }
6
+
7
+ export type MethodParameter = {
8
+ name: string;
9
+ type: string;
10
+ };
11
+
12
+ export type SignalRClientMethod = {
13
+ name: string;
14
+ methodType: SignalRMethodType;
15
+ parameters: MethodParameter[];
16
+ returns: string;
17
+ };
18
+
19
+ /*
20
+ * Converts unknown types into records or void's
21
+ */
22
+ function safeType(p: { type: string } | string | undefined) {
23
+ if (!p) return "void";
24
+ if (typeof p === "string") return p;
25
+ if (p.type === "object") return "Record<string, unknown>";
26
+ else return p.type;
27
+ }
28
+
29
+ /*
30
+ * For differentiating between interfaces and types
31
+ */
32
+ function isInterface(name: unknown) {
33
+ return typeof name === "string" && name.startsWith("I");
34
+ }
35
+
36
+ /*
37
+ * For skipping frontend methods when generating invoker
38
+ */
39
+ function isBackendMethod(method?: SignalRClientMethod) {
40
+ return (
41
+ method?.methodType === SignalRMethodType.BackendMethod ||
42
+ method?.methodType === SignalRMethodType.Default
43
+ );
44
+ }
45
+
46
+ /*
47
+ * For skipping backend when generating handling interface
48
+ */
49
+
50
+ function isFrontendMethod(method?: SignalRClientMethod) {
51
+ return (
52
+ method?.methodType === SignalRMethodType.FrontendMethod ||
53
+ method?.methodType === SignalRMethodType.Default
54
+ );
55
+ }
56
+
57
+ /*
58
+ * For skipping entire event block if there's no frontend methods at all
59
+ */
60
+ function hasAnyFrontendMethods(methods?: SignalRClientMethod[]) {
61
+ return (
62
+ methods &&
63
+ methods.some(
64
+ (method) =>
65
+ method.methodType === SignalRMethodType.FrontendMethod ||
66
+ method.methodType === SignalRMethodType.Default,
67
+ )
68
+ );
69
+ }
70
+
71
+ /*
72
+ * For skipping entire invoker block if there's no backend methods at all
73
+ */
74
+ function hasAnyBackendMethods(methods?: SignalRClientMethod[]) {
75
+ return (
76
+ methods &&
77
+ methods.some(
78
+ (method) =>
79
+ method.methodType === SignalRMethodType.BackendMethod ||
80
+ method.methodType === SignalRMethodType.Default,
81
+ )
82
+ );
83
+ }
84
+
85
+ /*
86
+ * For comparing values in templates, e.g. for checking if return type is void or not
87
+ */
88
+ function eq(a: unknown, b: unknown) {
89
+ return a === b;
90
+ }
91
+
92
+ /*
93
+ * Concatenates two strings
94
+ */
95
+ function concat(a?: string, b?: string) {
96
+ return (a ?? "") + (b ?? "");
97
+ }
98
+
99
+ export default {
100
+ eq,
101
+ concat,
102
+ safeType,
103
+ isInterface,
104
+ isBackendMethod,
105
+ isFrontendMethod,
106
+ hasAnyFrontendMethods,
107
+ hasAnyBackendMethods,
108
+ };
@@ -0,0 +1,20 @@
1
+ import { type GeneratorScriptProps } from "typesynth";
2
+ import { dtoImports, validateSpec } from "./spec.ts";
3
+
4
+ export default async function main({ data, generate }: GeneratorScriptProps) {
5
+ validateSpec(data);
6
+ await generate(
7
+ "config",
8
+ { backendUriLiteral: JSON.stringify(process.env.SIGNAL_SPEC_BACKEND_URI ?? "") },
9
+ "./config.ts",
10
+ );
11
+ await generate("dtos", data, "./DTOs.ts");
12
+
13
+ for (const [name, hubData] of Object.entries(data["hubs"])) {
14
+ await generate(
15
+ "hubClient",
16
+ { ...hubData, clientName: name, dtoImports: dtoImports(hubData, data.dtos) },
17
+ `./hooks/use${hubData.hubName}.ts`,
18
+ );
19
+ }
20
+ }
@@ -0,0 +1,27 @@
1
+ import { HubConnectionState, type HubConnection } from "@microsoft/signalr";
2
+
3
+ type Warning = { warning: string };
4
+
5
+ /**
6
+ * Safely invokes the SignalR hub method, i.e. it will only throw in case of an actual error,
7
+ * and will return `Warning` object in case the connection hasn't yet been established or
8
+ * has been already closed.
9
+ */
10
+ export async function safeInvokeHubMethod(
11
+ connection: HubConnection | null,
12
+ methodName: string,
13
+ ...args: any[]
14
+ ): Promise<Warning | any> {
15
+ if (connection?.state !== HubConnectionState.Connected) {
16
+ return { warning: "Skipped invocation: not connected" };
17
+ }
18
+
19
+ try {
20
+ return await connection.invoke(methodName, ...args);
21
+ } catch (err) {
22
+ if (err instanceof Error && err.message.includes("underlying connection being closed")) {
23
+ return { warning: "Skipped invocation: connection is being closed" };
24
+ }
25
+ throw err;
26
+ }
27
+ }
@@ -0,0 +1,76 @@
1
+ import { SignalRMethodType, type SignalRClientMethod } from "./hbs-helpers.ts";
2
+
3
+ type Hub = { hubName: string; hubContract: { methods: SignalRClientMethod[] } };
4
+ export type Spec = { hubs: Record<string, Hub>; dtos: Record<string, unknown> };
5
+
6
+ function invalid(path: string, expected: string): never {
7
+ throw new Error(
8
+ `Invalid SignalSpec at ${path}: expected ${expected}. The JSON may have been generated by an incompatible SignalSpec version, or may not be a SignalSpec document.`,
9
+ );
10
+ }
11
+
12
+ function record(value: unknown, path: string): Record<string, unknown> {
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(path, "an object");
14
+ return value as Record<string, unknown>;
15
+ }
16
+
17
+ function text(value: unknown, path: string) {
18
+ if (typeof value !== "string" || !value.trim()) invalid(path, "a non-empty string");
19
+ }
20
+
21
+ function array(value: unknown, path: string): unknown[] {
22
+ if (!Array.isArray(value)) invalid(path, "an array");
23
+ return value;
24
+ }
25
+
26
+ function properties(value: unknown, path: string) {
27
+ for (const [index, entry] of array(value, path).entries()) {
28
+ const itemPath = `${path}[${index}]`;
29
+ const item = record(entry, itemPath);
30
+ text(item.name, `${itemPath}.name`);
31
+ text(item.type, `${itemPath}.type`);
32
+ }
33
+ }
34
+
35
+ export function validateSpec(value: unknown): asserts value is Spec {
36
+ const spec = record(value, "$");
37
+ const hubs = record(spec.hubs, "$.hubs");
38
+ const dtos = record(spec.dtos, "$.dtos");
39
+ for (const [name, value] of Object.entries(dtos)) {
40
+ const path = `$.dtos[${JSON.stringify(name)}]`;
41
+ properties(record(value, path).properties, `${path}.properties`);
42
+ }
43
+ for (const [name, value] of Object.entries(hubs)) {
44
+ const path = `$.hubs[${JSON.stringify(name)}]`;
45
+ const hub = record(value, path);
46
+ text(hub.hubName, `${path}.hubName`);
47
+ const contract = record(hub.hubContract, `${path}.hubContract`);
48
+ for (const [index, value] of array(contract.methods, `${path}.hubContract.methods`).entries()) {
49
+ const methodPath = `${path}.hubContract.methods[${index}]`;
50
+ const method = record(value, methodPath);
51
+ text(method.name, `${methodPath}.name`);
52
+ if (!Object.values(SignalRMethodType).includes(method.methodType as SignalRMethodType))
53
+ invalid(`${methodPath}.methodType`, "default, frontendMethod, or backendMethod");
54
+ if (method.parameters !== undefined)
55
+ properties(method.parameters, `${methodPath}.parameters`);
56
+ if (method.returns !== undefined && method.returns !== null)
57
+ text(method.returns, `${methodPath}.returns`);
58
+ }
59
+ }
60
+ }
61
+
62
+ export function dtoImports(hub: Hub, dtos: Spec["dtos"]): string[] {
63
+ const names = new Set(Object.keys(dtos));
64
+ const imports = new Set<string>();
65
+ for (const method of hub.hubContract.methods) {
66
+ for (const type of [
67
+ ...(method.parameters ?? []).map((parameter) => parameter.type),
68
+ method.returns ?? "",
69
+ ]) {
70
+ for (const identifier of type.match(/[A-Za-z_$][\w$]*/g) ?? []) {
71
+ if (names.has(identifier)) imports.add(identifier);
72
+ }
73
+ }
74
+ }
75
+ return [...imports].sort();
76
+ }
@@ -0,0 +1,56 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as SignalR from '@microsoft/signalr';
3
+ import { useSignalRConnections } from '@/services/signalR/SignalRConnectionsProvider.tsx';
4
+ import { EnableSignalRDebugInformation } from '@/application/constants/env-variables.ts';
5
+ import { useStableHandlers } from '@/helpers/useStableHandlers.ts';
6
+
7
+
8
+
9
+
10
+
11
+ //--------------------------------------------------------------------------------------------------------------
12
+ // Hubs
13
+ //--------------------------------------------------------------------------------------------------------------
14
+
15
+
16
+
17
+ //--------------------------------------------------------------------------------------------------------------
18
+ // DTOs
19
+ //--------------------------------------------------------------------------------------------------------------
20
+
21
+
22
+
23
+
24
+
25
+ {{#each x-signal-r-client.hubs}}
26
+
27
+
28
+ {{/each}}
29
+
30
+ //--------------------------------------------------------------------------------------------------------------
31
+ // Change tracking
32
+ //--------------------------------------------------------------------------------------------------------------
33
+
34
+ export enum DomainType {
35
+ {{#each x-domain-types.types}}
36
+ {{this}} = '{{this}}',
37
+ {{/each}}
38
+ }
39
+
40
+ {{#each x-domain-types.payloads}}
41
+ export interface {{@key}} {
42
+ {{#each properties}}
43
+ {{name}}: {{safeType this}},
44
+ {{/each}}
45
+ }
46
+ {{/each}}
47
+
48
+ export type DomainTypePayloadMap = {
49
+ {{#each x-domain-types.types}}
50
+ {{#if (lookup ../x-domain-types.payloads (concat this "Payload"))}}
51
+ [DomainType.{{this}}]: {{this}}Payload;
52
+ {{/if}}
53
+ {{/each}}
54
+ };
55
+
56
+ export const SIGNAL_R_CLIENT_CHECKSUM = "{{magic__checksum}}";
@@ -0,0 +1,5 @@
1
+ import { useSignalRConnectionsStore } from "@signal-spec/react-client";
2
+
3
+ export const backendUri = {{{backendUriLiteral}}};
4
+ export const EnableSignalRDebug = false;
5
+ useSignalRConnectionsStore.setState({ backendUri, EnableSignalRDebug });
@@ -0,0 +1,25 @@
1
+ //--------------------------------------------------------------------------------------------------------------
2
+ // <auto-generated>
3
+ // This file is auto-generated by "SignalSpec.ReactClient".
4
+ // Do not modify this file manually.
5
+ // If you want to adjust it, copy the template and supply your edited version of it.
6
+ // </auto-generated>
7
+ //--------------------------------------------------------------------------------------------------------------
8
+
9
+ {{#each dtos}}
10
+ {{#if (isInterface @key)}}
11
+ export interface {{@key}} {
12
+ {{#each properties}}
13
+ {{name}}: {{safeType this}},
14
+ {{/each}}
15
+ }
16
+
17
+ {{else}}
18
+ export type {{@key}} = {
19
+ {{#each properties}}
20
+ {{name}}: {{safeType this}},
21
+ {{/each}}
22
+ }
23
+
24
+ {{/if}}
25
+ {{/each}}
@@ -0,0 +1,113 @@
1
+ import "../config.js";
2
+ import type { HubConnection } from "@microsoft/signalr";
3
+ import { useState, useEffect } from "react";
4
+ {{#if dtoImports}}
5
+ import type { {{#each dtoImports}}{{this}}{{#unless @last}}, {{/unless}}{{/each}} } from "../DTOs.js";
6
+ {{/if}}
7
+ import { useStableHandlers, useSignalRConnections{{#if (hasAnyBackendMethods hubContract.methods)}}, safeInvokeHubMethod{{/if}} } from "@signal-spec/react-client";
8
+
9
+
10
+ //--------------------------------------------------------------------------------------------------------------
11
+ // <auto-generated>
12
+ // This file is auto-generated by "SignalSpec.ReactClient" (@signal-spec/react-client).
13
+ // Do not modify this file manually.
14
+ // If you want to adjust it, copy the template and supply your edited version of it.
15
+ // </auto-generated>
16
+ //--------------------------------------------------------------------------------------------------------------
17
+
18
+ {{#if (hasAnyFrontendMethods hubContract.methods)}}
19
+ interface {{clientName}}Frontend {
20
+ {{#each hubContract.methods}}
21
+ {{#if (isFrontendMethod this)}}
22
+ {{name}}: (
23
+ {{#if parameters}}
24
+ {{#each parameters}}
25
+ {{name}}: {{safeType this}}{{#unless @last}}, {{/unless}}
26
+ {{/each}}
27
+ {{/if}}
28
+ ) => Promise<{{#if returns}}{{safeType returns}}{{else}}void{{/if}}>;
29
+ {{/if}}
30
+ {{/each}}
31
+ }
32
+ {{/if}}
33
+
34
+ {{#if (hasAnyBackendMethods hubContract.methods)}}
35
+ interface {{clientName}}Backend {
36
+ {{#each hubContract.methods}}
37
+ {{#if (isBackendMethod this)}}
38
+ {{name}}: (
39
+ {{#if parameters}}
40
+ {{#each parameters}}
41
+ {{name}}: {{safeType this}}{{#unless @last}}, {{/unless}}
42
+ {{/each}}
43
+ {{/if}}
44
+ ) => Promise<{{#if returns}}{{safeType returns}}{{else}}void{{/if}}>;
45
+ {{/if}}
46
+ {{/each}}
47
+ }
48
+ {{/if}}
49
+
50
+
51
+ export function use{{hubName}}(
52
+ {{#if (hasAnyFrontendMethods hubContract.methods)}}handlers: Partial<{{clientName}}Frontend> = {},{{/if}}
53
+ enabled: boolean = true,
54
+ ): { {{#if (hasAnyBackendMethods hubContract.methods)}}invoker: {{clientName}}Backend;{{/if}} connection: HubConnection | null; } {
55
+
56
+ const { setupEventHandlers, deleteConnection } = useSignalRConnections();
57
+ const [connection, setConnection] = useState<HubConnection | null>(
58
+ null,
59
+ );
60
+
61
+ const stableHandlers = useStableHandlers({{#if (hasAnyFrontendMethods hubContract.methods)}}handlers{{else}}{}{{/if}});
62
+ const onConnected = (connection: HubConnection) =>
63
+ setConnection(connection);
64
+
65
+ useEffect(() => {
66
+ if (!enabled) return;
67
+
68
+ const data = useSignalRConnections.getState().createConnection('{{hubName}}');
69
+ onConnected(data.connection);
70
+ setupEventHandlers('{{hubName}}', stableHandlers);
71
+
72
+ return () => {
73
+ deleteConnection('{{hubName}}', stableHandlers);
74
+ };
75
+ }, [enabled]);
76
+
77
+ {{#if (hasAnyBackendMethods hubContract.methods)}}
78
+ const invoker = {
79
+ {{#each hubContract.methods}}
80
+ {{#if (isBackendMethod this)}}
81
+ {{name}}: async (
82
+ {{#if parameters}}
83
+ {{#each parameters}}
84
+ {{name}}: {{safeType this}}{{#unless @last}}, {{/unless}}
85
+ {{/each}}
86
+ {{/if}}
87
+ ) => {
88
+ {{#if returns}}
89
+ return await safeInvokeHubMethod(connection,
90
+ "{{name}}"
91
+ {{#if parameters}}, {{/if}}
92
+ {{#each parameters}}
93
+ {{name}}{{#unless @last}}, {{/unless}}
94
+ {{/each}}
95
+ ) as {{safeType returns}};
96
+ {{else}}
97
+ await safeInvokeHubMethod(connection,
98
+ "{{name}}"
99
+ {{#if parameters}}, {{/if}}
100
+ {{#each parameters}}
101
+ {{name}}{{#unless @last}}, {{/unless}}
102
+ {{/each}}
103
+ );
104
+ {{/if}}
105
+ }{{#unless @last}},{{/unless}}
106
+ {{/if}}
107
+ {{/each}}
108
+ };
109
+ {{/if}}
110
+
111
+ return { {{#if (hasAnyBackendMethods hubContract.methods)}}invoker,{{/if}} connection };
112
+ }
113
+