@intrig/plugin-react 0.0.1
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/.swcrc +29 -0
- package/README.md +7 -0
- package/eslint.config.mjs +19 -0
- package/package.json +25 -0
- package/project.json +29 -0
- package/rollup.config.cjs +54 -0
- package/rollup.config.mjs +33 -0
- package/src/index.ts +2 -0
- package/src/lib/code-generator.ts +79 -0
- package/src/lib/get-endpoint-documentation.ts +35 -0
- package/src/lib/get-schema-documentation.ts +11 -0
- package/src/lib/internal-types.ts +15 -0
- package/src/lib/plugin-react.ts +22 -0
- package/src/lib/templates/context.template.ts +74 -0
- package/src/lib/templates/docs/__snapshots__/async-hook.spec.ts.snap +889 -0
- package/src/lib/templates/docs/__snapshots__/download-hook.spec.ts.snap +1445 -0
- package/src/lib/templates/docs/__snapshots__/react-hook.spec.ts.snap +1371 -0
- package/src/lib/templates/docs/__snapshots__/sse-hook.spec.ts.snap +2008 -0
- package/src/lib/templates/docs/async-hook.spec.ts +92 -0
- package/src/lib/templates/docs/async-hook.ts +226 -0
- package/src/lib/templates/docs/download-hook.spec.ts +182 -0
- package/src/lib/templates/docs/download-hook.ts +170 -0
- package/src/lib/templates/docs/react-hook.spec.ts +97 -0
- package/src/lib/templates/docs/react-hook.ts +323 -0
- package/src/lib/templates/docs/schema.ts +105 -0
- package/src/lib/templates/docs/sse-hook.spec.ts +207 -0
- package/src/lib/templates/docs/sse-hook.ts +221 -0
- package/src/lib/templates/extra.template.ts +198 -0
- package/src/lib/templates/index.template.ts +14 -0
- package/src/lib/templates/intrigMiddleware.template.ts +21 -0
- package/src/lib/templates/logger.template.ts +67 -0
- package/src/lib/templates/media-type-utils.template.ts +191 -0
- package/src/lib/templates/network-state.template.ts +702 -0
- package/src/lib/templates/packageJson.template.ts +63 -0
- package/src/lib/templates/provider/__tests__/provider-templates.spec.ts +209 -0
- package/src/lib/templates/provider/axios-config.template.ts +49 -0
- package/src/lib/templates/provider/hooks.template.ts +240 -0
- package/src/lib/templates/provider/interfaces.template.ts +72 -0
- package/src/lib/templates/provider/intrig-provider-stub.template.ts +73 -0
- package/src/lib/templates/provider/intrig-provider.template.ts +185 -0
- package/src/lib/templates/provider/main.template.ts +48 -0
- package/src/lib/templates/provider/reducer.template.ts +50 -0
- package/src/lib/templates/provider/status-trap.template.ts +80 -0
- package/src/lib/templates/provider.template.ts +698 -0
- package/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts +196 -0
- package/src/lib/templates/source/controller/method/clientIndex.template.ts +38 -0
- package/src/lib/templates/source/controller/method/download.template.ts +256 -0
- package/src/lib/templates/source/controller/method/params.template.ts +31 -0
- package/src/lib/templates/source/controller/method/requestHook.template.ts +220 -0
- package/src/lib/templates/source/type/typeTemplate.ts +257 -0
- package/src/lib/templates/swcrc.template.ts +25 -0
- package/src/lib/templates/tsconfig.template.ts +37 -0
- package/src/lib/templates/type-utils.template.ts +28 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +20 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { reactSseHookDocs } from './sse-hook';
|
|
2
|
+
|
|
3
|
+
// We use a plain object cast to any to avoid direct dependency on the types from "@intrig/plugin-sdk" in test.
|
|
4
|
+
|
|
5
|
+
describe('reactSseHookDocs', () => {
|
|
6
|
+
it('snapshot — simple REST descriptor (no body, no path params)', async () => {
|
|
7
|
+
const descriptor = {
|
|
8
|
+
id: '1',
|
|
9
|
+
name: 'streamLogs',
|
|
10
|
+
type: 'rest' as const,
|
|
11
|
+
source: 'demo_api',
|
|
12
|
+
path: 'logs',
|
|
13
|
+
data: {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
paths: ['/api/logs/stream'],
|
|
16
|
+
operationId: 'streamLogs',
|
|
17
|
+
// no requestBody
|
|
18
|
+
// no variables
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
23
|
+
|
|
24
|
+
expect(result.path).toBe('sse-hook.md');
|
|
25
|
+
expect(result.content).toMatchSnapshot();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('snapshot — request body only (no path params)', async () => {
|
|
29
|
+
const descriptor = {
|
|
30
|
+
id: '2',
|
|
31
|
+
name: 'streamCustomEvents',
|
|
32
|
+
type: 'rest' as const,
|
|
33
|
+
source: 'demo_api',
|
|
34
|
+
path: 'events',
|
|
35
|
+
data: {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
paths: ['/api/events/stream'],
|
|
38
|
+
operationId: 'streamCustomEvents',
|
|
39
|
+
requestBody: 'StreamCustomEventsRequest',
|
|
40
|
+
// no variables
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
45
|
+
expect(result.path).toBe('sse-hook.md');
|
|
46
|
+
expect(result.content).toMatchSnapshot();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('snapshot — path params only (no request body)', async () => {
|
|
50
|
+
const descriptor = {
|
|
51
|
+
id: '3',
|
|
52
|
+
name: 'streamUserActivity',
|
|
53
|
+
type: 'rest' as const,
|
|
54
|
+
source: 'demo_api',
|
|
55
|
+
path: 'users/{userId}/activity',
|
|
56
|
+
data: {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
paths: ['/api/users/{userId}/activity/stream'],
|
|
59
|
+
operationId: 'streamUserActivity',
|
|
60
|
+
variables: [
|
|
61
|
+
{ name: 'userId', in: 'path', ref: '#/components/schemas/UserId' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
67
|
+
expect(result.path).toBe('sse-hook.md');
|
|
68
|
+
expect(result.content).toMatchSnapshot();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('snapshot — request body and path params', async () => {
|
|
72
|
+
const descriptor = {
|
|
73
|
+
id: '4',
|
|
74
|
+
name: 'streamTaskProgress',
|
|
75
|
+
type: 'rest' as const,
|
|
76
|
+
source: 'demo_api',
|
|
77
|
+
path: 'tasks/{taskId}/progress',
|
|
78
|
+
data: {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
paths: ['/api/tasks/{taskId}/progress/stream'],
|
|
81
|
+
operationId: 'streamTaskProgress',
|
|
82
|
+
requestBody: 'StreamTaskProgressRequest',
|
|
83
|
+
variables: [
|
|
84
|
+
{ name: 'taskId', in: 'PATH', ref: '#/components/schemas/TaskId' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
90
|
+
expect(result.path).toBe('sse-hook.md');
|
|
91
|
+
expect(result.content).toMatchSnapshot();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('handles multiple path params', async () => {
|
|
95
|
+
const descriptor = {
|
|
96
|
+
id: '5',
|
|
97
|
+
name: 'streamProjectNotifications',
|
|
98
|
+
type: 'rest' as const,
|
|
99
|
+
source: 'demo_api',
|
|
100
|
+
path: 'projects/{projectId}/notifications/{notificationId}',
|
|
101
|
+
data: {
|
|
102
|
+
method: 'GET',
|
|
103
|
+
paths: ['/api/projects/{projectId}/notifications/{notificationId}/stream'],
|
|
104
|
+
operationId: 'streamProjectNotifications',
|
|
105
|
+
variables: [
|
|
106
|
+
{ name: 'projectId', in: 'path', ref: '#/components/schemas/ProjectId' },
|
|
107
|
+
{ name: 'notificationId', in: 'PATH', ref: '#/components/schemas/NotificationId' },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
113
|
+
expect(result.path).toBe('sse-hook.md');
|
|
114
|
+
expect(result.content).toMatchSnapshot();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('handles query params mixed with path params', async () => {
|
|
118
|
+
const descriptor = {
|
|
119
|
+
id: '6',
|
|
120
|
+
name: 'streamSystemMetrics',
|
|
121
|
+
type: 'rest' as const,
|
|
122
|
+
source: 'demo_api',
|
|
123
|
+
path: 'systems/{systemId}/metrics',
|
|
124
|
+
data: {
|
|
125
|
+
method: 'GET',
|
|
126
|
+
paths: ['/api/systems/{systemId}/metrics/stream'],
|
|
127
|
+
operationId: 'streamSystemMetrics',
|
|
128
|
+
variables: [
|
|
129
|
+
{ name: 'systemId', in: 'path', ref: '#/components/schemas/SystemId' },
|
|
130
|
+
{ name: 'interval', in: 'query', ref: '#/components/schemas/Interval' },
|
|
131
|
+
{ name: 'includeDetails', in: 'QUERY', ref: '#/components/schemas/Boolean' },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
137
|
+
expect(result.path).toBe('sse-hook.md');
|
|
138
|
+
expect(result.content).toMatchSnapshot();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('handles empty variables array', async () => {
|
|
142
|
+
const descriptor = {
|
|
143
|
+
id: '7',
|
|
144
|
+
name: 'streamGlobalEvents',
|
|
145
|
+
type: 'rest' as const,
|
|
146
|
+
source: 'demo_api',
|
|
147
|
+
path: 'events',
|
|
148
|
+
data: {
|
|
149
|
+
method: 'GET',
|
|
150
|
+
paths: ['/api/events/global/stream'],
|
|
151
|
+
operationId: 'streamGlobalEvents',
|
|
152
|
+
variables: [], // empty array instead of undefined
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
157
|
+
expect(result.path).toBe('sse-hook.md');
|
|
158
|
+
expect(result.content).toMatchSnapshot();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('handles case-insensitive path parameter detection', async () => {
|
|
162
|
+
const descriptor = {
|
|
163
|
+
id: '8',
|
|
164
|
+
name: 'streamOrderUpdates',
|
|
165
|
+
type: 'rest' as const,
|
|
166
|
+
source: 'demo_api',
|
|
167
|
+
path: 'orders/{orderId}',
|
|
168
|
+
data: {
|
|
169
|
+
method: 'GET',
|
|
170
|
+
paths: ['/api/orders/{orderId}/updates/stream'],
|
|
171
|
+
operationId: 'streamOrderUpdates',
|
|
172
|
+
variables: [
|
|
173
|
+
{ name: 'orderId', in: 'Path', ref: '#/components/schemas/OrderId' }, // Mixed case
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
179
|
+
expect(result.path).toBe('sse-hook.md');
|
|
180
|
+
expect(result.content).toMatchSnapshot();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('handles complex SSE endpoint with body and multiple params', async () => {
|
|
184
|
+
const descriptor = {
|
|
185
|
+
id: '9',
|
|
186
|
+
name: 'streamAnalytics',
|
|
187
|
+
type: 'rest' as const,
|
|
188
|
+
source: 'demo_api',
|
|
189
|
+
path: 'analytics/{dashboardId}/stream',
|
|
190
|
+
data: {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
paths: ['/api/analytics/{dashboardId}/stream'],
|
|
193
|
+
operationId: 'streamAnalytics',
|
|
194
|
+
requestBody: 'StreamAnalyticsRequest',
|
|
195
|
+
variables: [
|
|
196
|
+
{ name: 'dashboardId', in: 'path', ref: '#/components/schemas/DashboardId' },
|
|
197
|
+
{ name: 'real_time', in: 'query', ref: '#/components/schemas/Boolean' },
|
|
198
|
+
{ name: 'format', in: 'query', ref: '#/components/schemas/Format' },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = await reactSseHookDocs(descriptor as any);
|
|
204
|
+
expect(result.path).toBe('sse-hook.md');
|
|
205
|
+
expect(result.content).toMatchSnapshot();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { camelCase, mdLiteral, pascalCase, ResourceDescriptor, RestData } from "@intrig/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export function reactSseHookDocs(descriptor: ResourceDescriptor<RestData>) {
|
|
4
|
+
const md = mdLiteral("sse-hook.md");
|
|
5
|
+
|
|
6
|
+
// ===== Derived names =====
|
|
7
|
+
const hasPathParams = (descriptor.data.variables ?? []).some(
|
|
8
|
+
(v: any) => v.in?.toUpperCase() === "PATH",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const actionName = camelCase(descriptor.name); // e.g. streamBuildLogs
|
|
12
|
+
const respVar = `${actionName}Resp`; // e.g. streamBuildLogsResp
|
|
13
|
+
const clearName = `clear${pascalCase(descriptor.name)}`; // e.g. clearStreamBuildLogs
|
|
14
|
+
|
|
15
|
+
const requestBodyVar = descriptor.data.requestBody
|
|
16
|
+
? camelCase(descriptor.data.requestBody)
|
|
17
|
+
: undefined;
|
|
18
|
+
const requestBodyType = descriptor.data.requestBody
|
|
19
|
+
? pascalCase(descriptor.data.requestBody)
|
|
20
|
+
: undefined;
|
|
21
|
+
|
|
22
|
+
const paramsVar = hasPathParams ? `${actionName}Params` : undefined; // e.g. streamBuildLogsParams
|
|
23
|
+
const paramsType = hasPathParams ? `${pascalCase(descriptor.name)}Params` : undefined; // e.g. StreamBuildLogsParams
|
|
24
|
+
|
|
25
|
+
const responseTypeName = `${pascalCase(descriptor.name)}ResponseBody`; // if generated by your build
|
|
26
|
+
const callArgs = [requestBodyVar, paramsVar ?? "{}"].filter(Boolean).join(", ");
|
|
27
|
+
|
|
28
|
+
return md`
|
|
29
|
+
# Intrig SSE Hooks — Quick Guide
|
|
30
|
+
|
|
31
|
+
## When should I use the SSE hook?
|
|
32
|
+
- **Your endpoint streams events** (Server-Sent Events) and you want **incremental updates** in the UI → use this **SSE hook**.
|
|
33
|
+
- **You only need a final result** → use the regular **stateful hook**.
|
|
34
|
+
- **One-off validate/submit/update** with no shared state → use the **async hook**.
|
|
35
|
+
|
|
36
|
+
> Intrig SSE hooks are **stateful hooks** under the hood. **Events arrive while the hook is in \`Pending\`**. When the stream completes, the hook transitions to **\`Success\`** (or **\`Error\`**).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Copy-paste starter (fast lane)
|
|
41
|
+
|
|
42
|
+
### 1) Hook import
|
|
43
|
+
\`\`\`ts
|
|
44
|
+
import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
### 2) Utility guards
|
|
48
|
+
\`\`\`ts
|
|
49
|
+
import { isPending, isSuccess, isError } from '@intrig/react';
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
### 3) Hook instance (auto-clear on unmount)
|
|
53
|
+
\`\`\`ts
|
|
54
|
+
const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## TL;DR (copy–paste)
|
|
60
|
+
|
|
61
|
+
\`\`\`tsx
|
|
62
|
+
import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
|
|
63
|
+
import { isPending, isSuccess, isError } from '@intrig/react';
|
|
64
|
+
import { useEffect, useState } from 'react';
|
|
65
|
+
|
|
66
|
+
export default function Example() {
|
|
67
|
+
const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
|
|
68
|
+
const [messages, setMessages] = useState<any[]>([]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
${actionName}(${callArgs}); // start stream
|
|
72
|
+
}, [${actionName}]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
// SSE delivers messages while state is Pending
|
|
76
|
+
if (isPending(${respVar})) {
|
|
77
|
+
setMessages((prev) => [...prev, ${respVar}.data]);
|
|
78
|
+
}
|
|
79
|
+
}, [${respVar}]);
|
|
80
|
+
|
|
81
|
+
if (isError(${respVar})) return <>An error occurred</>;
|
|
82
|
+
if (isPending(${respVar})) return <pre>{JSON.stringify(messages, null, 2)}</pre>;
|
|
83
|
+
if (isSuccess(${respVar})) return <>Completed</>;
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
${(requestBodyType || paramsType) ? `### Optional types (if generated by your build)
|
|
90
|
+
\`\`\`ts
|
|
91
|
+
${requestBodyType ? `import type { ${requestBodyType} } from '@intrig/react/${descriptor.source}/components/schemas/${requestBodyType}';\n` : ''}${paramsType ? `import type { ${paramsType} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.params';\n` : ''}import type { ${responseTypeName} } from '@intrig/react/${descriptor.path}/${pascalCase(descriptor.name)}.response';
|
|
92
|
+
\`\`\`
|
|
93
|
+
` : ''}
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Hook API
|
|
98
|
+
|
|
99
|
+
\`\`\`ts
|
|
100
|
+
// Signature (shape shown; concrete generics vary per generated hook)
|
|
101
|
+
declare function use${pascalCase(descriptor.name)}(options?: {
|
|
102
|
+
fetchOnMount?: boolean;
|
|
103
|
+
clearOnUnmount?: boolean; // recommended for streams
|
|
104
|
+
key?: string; // isolate multiple subscriptions
|
|
105
|
+
params?: ${paramsType ?? 'unknown'};
|
|
106
|
+
body?: ${requestBodyType ?? 'unknown'};
|
|
107
|
+
}): [
|
|
108
|
+
// While streaming: isPending(state) === true and state.data is the latest event
|
|
109
|
+
NetworkState<${responseTypeName} /* or event payload type */, any>,
|
|
110
|
+
// Start streaming:
|
|
111
|
+
(req: { params?: ${paramsType ?? 'unknown'}; body?: ${requestBodyType ?? 'unknown'} }) => void,
|
|
112
|
+
// Clear/close stream:
|
|
113
|
+
() => void
|
|
114
|
+
];
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
> **Important:** For SSE, **each incoming event** is surfaced as \`${respVar}.data\` **only while** \`isPending(${respVar})\` is true. On stream completion the hook flips to \`isSuccess\`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Usage patterns
|
|
122
|
+
|
|
123
|
+
### 1) Lifecycle-bound stream (start on mount, auto-clear)
|
|
124
|
+
\`\`\`tsx
|
|
125
|
+
const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
${actionName}(${callArgs});
|
|
129
|
+
}, [${actionName}]);
|
|
130
|
+
\`\`\`
|
|
131
|
+
<details><summary>Description</summary>
|
|
132
|
+
Starts the stream when the component mounts and closes it when the component unmounts.
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
### 2) Collect messages into an array (simple collector)
|
|
136
|
+
\`\`\`tsx
|
|
137
|
+
const [messages, setMessages] = useState<any[]>([]);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (isPending(${respVar})) setMessages((m) => [...m, ${respVar}.data]);
|
|
141
|
+
}, [${respVar}]);
|
|
142
|
+
\`\`\`
|
|
143
|
+
<details><summary>Description</summary>
|
|
144
|
+
Appends each event to an in-memory array. Good for logs and chat-like feeds; consider capping length to avoid memory growth.
|
|
145
|
+
</details>
|
|
146
|
+
|
|
147
|
+
### 3) Keep only the latest event (cheap UI)
|
|
148
|
+
\`\`\`tsx
|
|
149
|
+
const latest = isPending(${respVar}) ? ${respVar}.data : undefined;
|
|
150
|
+
\`\`\`
|
|
151
|
+
<details><summary>Description</summary>
|
|
152
|
+
When you only need the most recent message (progress percentage, status line).
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
### 4) Controlled start/stop (user-triggered)
|
|
156
|
+
\`\`\`tsx
|
|
157
|
+
const [${respVar}, ${actionName}, ${clearName}] = use${pascalCase(descriptor.name)}();
|
|
158
|
+
|
|
159
|
+
const start = () => ${actionName}(${callArgs});
|
|
160
|
+
const stop = () => ${clearName}();
|
|
161
|
+
\`\`\`
|
|
162
|
+
<details><summary>Description</summary>
|
|
163
|
+
Expose play/pause UI for long streams or admin tools.
|
|
164
|
+
</details>
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Full example (with flushSync option)
|
|
169
|
+
|
|
170
|
+
\`\`\`tsx
|
|
171
|
+
import { use${pascalCase(descriptor.name)} } from '@intrig/react/${descriptor.path}/client';
|
|
172
|
+
import { isPending, isSuccess, isError } from '@intrig/react';
|
|
173
|
+
import { useEffect, useState } from 'react';
|
|
174
|
+
import { flushSync } from 'react-dom';
|
|
175
|
+
|
|
176
|
+
function MyComponent() {
|
|
177
|
+
const [${respVar}, ${actionName}] = use${pascalCase(descriptor.name)}({ clearOnUnmount: true });
|
|
178
|
+
const [events, setEvents] = useState<any[]>([]);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
${actionName}(${callArgs});
|
|
182
|
+
}, [${actionName}]);
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (isPending(${respVar})) {
|
|
186
|
+
// Use flushSync only if you must render every single event (high-frequency streams).
|
|
187
|
+
flushSync(() => setEvents((xs) => [...xs, ${respVar}.data]));
|
|
188
|
+
}
|
|
189
|
+
}, [${respVar}]);
|
|
190
|
+
|
|
191
|
+
if (isError(${respVar})) return <>Stream error</>;
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
{isPending(${respVar}) && <pre>{JSON.stringify(events, null, 2)}</pre>}
|
|
195
|
+
{isSuccess(${respVar}) && <>Completed ({events.length} events)</>}
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
\`\`\`
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Tips, anti-patterns & gotchas
|
|
204
|
+
|
|
205
|
+
- **Prefer \`clearOnUnmount: true\`** so the EventSource/web request is closed when the component disappears.
|
|
206
|
+
- **Don’t store unbounded arrays** for infinite streams—cap the length or batch to IndexedDB.
|
|
207
|
+
- **Avoid unnecessary \`flushSync\`**; it’s expensive. Use it only when you truly must render every event.
|
|
208
|
+
- **Multiple streams:** supply a unique \`key\` to isolate independent subscriptions.
|
|
209
|
+
- **Server requirements:** SSE endpoints should send \`Content-Type: text/event-stream\`, disable buffering, and flush regularly; add relevant CORS headers if needed.
|
|
210
|
+
- **Completion:** UI can switch from progress view (\`isPending\`) to final view (\`isSuccess\`) automatically.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
- **No intermediate messages:** ensure the server is truly streaming SSE (correct content type + flush) and that proxies/CDNs aren’t buffering responses.
|
|
217
|
+
- **UI not updating for each event:** remove expensive work from the event effect, consider throttling; only use \`flushSync\` if absolutely necessary.
|
|
218
|
+
- **Stream never completes:** check server end conditions and that you call \`${clearName}\` when appropriate.
|
|
219
|
+
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {typescript} from "@intrig/plugin-sdk";
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
export function reactExtraTemplate() {
|
|
5
|
+
const ts = typescript(path.resolve("src", "extra.ts"))
|
|
6
|
+
|
|
7
|
+
return ts`import {
|
|
8
|
+
BinaryFunctionHook,
|
|
9
|
+
BinaryHookOptions,
|
|
10
|
+
BinaryProduceHook,
|
|
11
|
+
ConstantHook,
|
|
12
|
+
error,
|
|
13
|
+
init,
|
|
14
|
+
IntrigHook,
|
|
15
|
+
IntrigHookOptions,
|
|
16
|
+
isSuccess,
|
|
17
|
+
NetworkState,
|
|
18
|
+
pending,
|
|
19
|
+
success,
|
|
20
|
+
UnaryFunctionHook,
|
|
21
|
+
UnaryHookOptions,
|
|
22
|
+
UnaryProduceHook,
|
|
23
|
+
UnitHook,
|
|
24
|
+
UnitHookOptions,
|
|
25
|
+
} from '@intrig/react/network-state';
|
|
26
|
+
import {
|
|
27
|
+
useCallback,
|
|
28
|
+
useEffect,
|
|
29
|
+
useId,
|
|
30
|
+
useMemo,
|
|
31
|
+
useState,
|
|
32
|
+
} from 'react';
|
|
33
|
+
import { useIntrigContext } from '@intrig/react/intrig-context';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A custom hook that manages and returns the network state of a promise-based function,
|
|
37
|
+
* providing a way to execute the function and clear its state.
|
|
38
|
+
*
|
|
39
|
+
* @param fn The promise-based function whose network state is to be managed. It should be a function that returns a promise.
|
|
40
|
+
* @param key An optional identifier for the network state. Defaults to 'default'.
|
|
41
|
+
* @return A tuple containing the current network state, a function to execute the promise, and a function to clear the state.
|
|
42
|
+
*/
|
|
43
|
+
export function useAsNetworkState<T, F extends (...args: any) => Promise<T>>(
|
|
44
|
+
fn: F,
|
|
45
|
+
options: any = {},
|
|
46
|
+
): [NetworkState<T>, (...params: Parameters<F>) => void, () => void] {
|
|
47
|
+
const id = useId();
|
|
48
|
+
|
|
49
|
+
const context = useIntrigContext();
|
|
50
|
+
|
|
51
|
+
const key = options.key ?? 'default';
|
|
52
|
+
|
|
53
|
+
const networkState = useMemo(() => {
|
|
54
|
+
return context.state?.[${"`promiseState:${id}:${key}}`"}] ?? init();
|
|
55
|
+
}, [context.state?.[${"`promiseState:${id}:${key}}`"}]]);
|
|
56
|
+
|
|
57
|
+
const dispatch = useCallback(
|
|
58
|
+
(state: NetworkState<T>) => {
|
|
59
|
+
context.dispatch({ key, operation: id, source: 'promiseState', state });
|
|
60
|
+
},
|
|
61
|
+
[key, context.dispatch],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const execute = useCallback((...args: any[]) => {
|
|
65
|
+
dispatch(pending());
|
|
66
|
+
return fn(...args).then(
|
|
67
|
+
(data) => {
|
|
68
|
+
dispatch(success(data));
|
|
69
|
+
},
|
|
70
|
+
(e) => {
|
|
71
|
+
dispatch(error(e));
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const clear = useCallback(() => {
|
|
77
|
+
dispatch(init());
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
return [networkState, execute, clear];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A custom hook that resolves the value from the provided hook's state and updates it whenever the state changes.
|
|
85
|
+
*
|
|
86
|
+
* @param {IntrigHook<P, B, T>} hook - The hook that provides the state to observe and resolve data from.
|
|
87
|
+
* @param options
|
|
88
|
+
* @return {T | undefined} The resolved value from the hook's state or undefined if the state is not successful.
|
|
89
|
+
*/
|
|
90
|
+
export function useResolvedValue(
|
|
91
|
+
hook: UnitHook,
|
|
92
|
+
options: UnitHookOptions,
|
|
93
|
+
): undefined;
|
|
94
|
+
|
|
95
|
+
export function useResolvedValue<T>(
|
|
96
|
+
hook: ConstantHook<T>,
|
|
97
|
+
options: UnitHookOptions,
|
|
98
|
+
): T | undefined;
|
|
99
|
+
|
|
100
|
+
export function useResolvedValue<P>(
|
|
101
|
+
hook: UnaryProduceHook<P>,
|
|
102
|
+
options: UnaryHookOptions<P>,
|
|
103
|
+
): undefined;
|
|
104
|
+
|
|
105
|
+
export function useResolvedValue<P, T>(
|
|
106
|
+
hook: UnaryFunctionHook<P, T>,
|
|
107
|
+
options: UnaryHookOptions<P>,
|
|
108
|
+
): T | undefined;
|
|
109
|
+
|
|
110
|
+
export function useResolvedValue<P, B>(
|
|
111
|
+
hook: BinaryProduceHook<P, B>,
|
|
112
|
+
options: BinaryHookOptions<P, B>,
|
|
113
|
+
): undefined;
|
|
114
|
+
|
|
115
|
+
export function useResolvedValue<P, B, T>(
|
|
116
|
+
hook: BinaryFunctionHook<P, B, T>,
|
|
117
|
+
options: BinaryHookOptions<P, B>,
|
|
118
|
+
): T | undefined;
|
|
119
|
+
|
|
120
|
+
// **Implementation**
|
|
121
|
+
export function useResolvedValue<P, B, T>(
|
|
122
|
+
hook: IntrigHook<P, B, T>,
|
|
123
|
+
options: IntrigHookOptions<P, B>,
|
|
124
|
+
): T | undefined {
|
|
125
|
+
const [value, setValue] = useState<T | undefined>();
|
|
126
|
+
|
|
127
|
+
const [state] = hook(options as any); // Ensure compatibility with different hook types
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (isSuccess(state)) {
|
|
131
|
+
setValue(state.data);
|
|
132
|
+
} else {
|
|
133
|
+
setValue(undefined);
|
|
134
|
+
}
|
|
135
|
+
}, [state]);
|
|
136
|
+
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A custom hook that resolves and caches the value from a successful state provided by the given hook.
|
|
142
|
+
* The state is updated only when it is in a successful state.
|
|
143
|
+
*
|
|
144
|
+
* @param {IntrigHook<P, B, T>} hook - The hook that provides the state to observe and cache data from.
|
|
145
|
+
* @param options
|
|
146
|
+
* @return {T | undefined} The cached value from the hook's state or undefined if the state is not successful.
|
|
147
|
+
*/
|
|
148
|
+
export function useResolvedCachedValue(
|
|
149
|
+
hook: UnitHook,
|
|
150
|
+
options: UnitHookOptions,
|
|
151
|
+
): undefined;
|
|
152
|
+
|
|
153
|
+
export function useResolvedCachedValue<T>(
|
|
154
|
+
hook: ConstantHook<T>,
|
|
155
|
+
options: UnitHookOptions,
|
|
156
|
+
): T | undefined;
|
|
157
|
+
|
|
158
|
+
export function useResolvedCachedValue<P>(
|
|
159
|
+
hook: UnaryProduceHook<P>,
|
|
160
|
+
options: UnaryHookOptions<P>,
|
|
161
|
+
): undefined;
|
|
162
|
+
|
|
163
|
+
export function useResolvedCachedValue<P, T>(
|
|
164
|
+
hook: UnaryFunctionHook<P, T>,
|
|
165
|
+
options: UnaryHookOptions<P>,
|
|
166
|
+
): T | undefined;
|
|
167
|
+
|
|
168
|
+
export function useResolvedCachedValue<P, B>(
|
|
169
|
+
hook: BinaryProduceHook<P, B>,
|
|
170
|
+
options: BinaryHookOptions<P, B>,
|
|
171
|
+
): undefined;
|
|
172
|
+
|
|
173
|
+
export function useResolvedCachedValue<P, B, T>(
|
|
174
|
+
hook: BinaryFunctionHook<P, B, T>,
|
|
175
|
+
options: BinaryHookOptions<P, B>,
|
|
176
|
+
): T | undefined;
|
|
177
|
+
|
|
178
|
+
// **Implementation**
|
|
179
|
+
export function useResolvedCachedValue<P, B, T>(
|
|
180
|
+
hook: IntrigHook<P, B, T>,
|
|
181
|
+
options: IntrigHookOptions<P, B>,
|
|
182
|
+
): T | undefined {
|
|
183
|
+
const [cachedValue, setCachedValue] = useState<T | undefined>();
|
|
184
|
+
|
|
185
|
+
const [state] = hook(options as any); // Ensure compatibility with different hook types
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (isSuccess(state)) {
|
|
189
|
+
setCachedValue(state.data);
|
|
190
|
+
}
|
|
191
|
+
// Do not clear cached value if state is unsuccessful
|
|
192
|
+
}, [state]);
|
|
193
|
+
|
|
194
|
+
return cachedValue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
`
|
|
198
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {typescript} from "@intrig/plugin-sdk";
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
export function indexTemplate(){
|
|
5
|
+
|
|
6
|
+
const ts = typescript(path.resolve("src", "index.ts"))
|
|
7
|
+
|
|
8
|
+
return ts`
|
|
9
|
+
export * from './intrig-provider-main';
|
|
10
|
+
export * from './network-state';
|
|
11
|
+
export * from './extra';
|
|
12
|
+
export * from './media-type-utils';
|
|
13
|
+
`
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { typescript } from "@intrig/plugin-sdk";
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
export function intrigMiddlewareTemplate() {
|
|
5
|
+
const ts = typescript(path.resolve('src', 'intrig-middleware.ts'))
|
|
6
|
+
|
|
7
|
+
return ts`
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
import { requestInterceptor } from 'intrig-hook';
|
|
10
|
+
|
|
11
|
+
export function getAxiosInstance(key: string) {
|
|
12
|
+
let axiosInstance = axios.create({
|
|
13
|
+
baseURL: process.env[${"`${key.toUpperCase()}_API_URL`"}],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
axiosInstance.interceptors.request.use(requestInterceptor);
|
|
17
|
+
|
|
18
|
+
return axiosInstance;
|
|
19
|
+
}
|
|
20
|
+
`
|
|
21
|
+
}
|