@betterinternship/components 2.9.6 → 2.9.7

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.
Files changed (34) hide show
  1. package/README.md +50 -50
  2. package/dist/components/ui/accordion.js.map +1 -1
  3. package/dist/components/ui/animated-count.js.map +1 -1
  4. package/dist/components/ui/app-header.js.map +1 -1
  5. package/dist/components/ui/avatar.js.map +1 -1
  6. package/dist/components/ui/badge.js.map +1 -1
  7. package/dist/components/ui/button.js.map +1 -1
  8. package/dist/components/ui/card.js.map +1 -1
  9. package/dist/components/ui/checkbox.js.map +1 -1
  10. package/dist/components/ui/dialog.js.map +1 -1
  11. package/dist/components/ui/dropdown-menu.js.map +1 -1
  12. package/dist/components/ui/fab.js +9 -9
  13. package/dist/components/ui/icons.js.map +1 -1
  14. package/dist/components/ui/input.js.map +1 -1
  15. package/dist/components/ui/label.js.map +1 -1
  16. package/dist/components/ui/page-header.js.map +1 -1
  17. package/dist/components/ui/popover.js.map +1 -1
  18. package/dist/components/ui/separator.js.map +1 -1
  19. package/dist/components/ui/skeleton.js.map +1 -1
  20. package/dist/components/ui/status-notice.js +5 -2
  21. package/dist/components/ui/status-notice.js.map +1 -1
  22. package/dist/components/ui/switch.js.map +1 -1
  23. package/dist/components/ui/table.js.map +1 -1
  24. package/dist/components/ui/textarea.js.map +1 -1
  25. package/dist/components/ui/tooltip.js.map +1 -1
  26. package/dist/config.d.ts +6 -6
  27. package/dist/config.js +9 -9
  28. package/dist/hooks/useClientProcess.d.ts +69 -69
  29. package/dist/hooks/useClientProcess.js +255 -255
  30. package/dist/hooks/useMQJob.d.ts +2 -2
  31. package/dist/hooks/useMQJob.js.map +1 -1
  32. package/dist/utils.js.map +1 -1
  33. package/package.json +249 -249
  34. package/theme.css +228 -228
@@ -1,255 +1,255 @@
1
- "use client";
2
- import { jsx } from 'react/jsx-runtime';
3
- import { createContext, useRef, useState, useEffect, useContext } from 'react';
4
- import { useQueryClient, useQueries } from '@tanstack/react-query';
5
- import { config } from '../config.js';
6
-
7
- /**
8
- * The orchestrator service to get the process from.
9
- *
10
- * @param processId
11
- * @returns
12
- */
13
- const processStatusChecker = (processId) => {
14
- if (!processId)
15
- return { process: null };
16
- return fetch(`${config.orchestratorApi}/${encodeURIComponent(processId)}`, {
17
- method: 'GET',
18
- headers: {
19
- 'Content-Type': 'application/json',
20
- },
21
- }).then((response) => response.json());
22
- };
23
- /**
24
- * This fetches from the callback endpoint.
25
- *
26
- * @param processId
27
- * @returns
28
- */
29
- const processCallbackResultFetcher = (processId, processName, processCallbackUrl) => fetch(`${processCallbackUrl}`, {
30
- method: 'POST',
31
- headers: {
32
- 'Content-Type': 'application/json',
33
- },
34
- body: JSON.stringify({
35
- processId,
36
- processName,
37
- }),
38
- }).then((response) => response.json());
39
- /**
40
- * We need a context to keep track of all client processes.
41
- * This helps us manage the state of the toasts as well.
42
- */
43
- const clientProcessesContext = createContext({});
44
- /**
45
- * This should be at the root layout.
46
- *
47
- * @context
48
- */
49
- const ClientProcessesProvider = ({ children, }) => {
50
- const queryClient = useQueryClient();
51
- const clientProcessAttempts = useRef({});
52
- const [clientProcessDtos, setClientProcessDtos] = useState({});
53
- // Populates the state object with cached data on mount
54
- useEffect(() => {
55
- setClientProcessDtos((oldData) => {
56
- if (Object.keys(oldData).length > 0)
57
- return oldData;
58
- const cachedClientProcesses = queryClient
59
- .getQueryCache()
60
- .findAll({ queryKey: ['.__process'], exact: false });
61
- const seededClientProcesses = {};
62
- for (const query of cachedClientProcesses) {
63
- const processId = query.queryKey[2];
64
- if (typeof processId !== 'string')
65
- continue;
66
- const data = query.state.data;
67
- if (!data?.dto || !data?.metadata)
68
- continue;
69
- seededClientProcesses[processId] = {
70
- dto: data.dto,
71
- metadata: data.metadata,
72
- options: data.options,
73
- };
74
- }
75
- return { ...oldData, ...seededClientProcesses };
76
- });
77
- }, [queryClient]);
78
- // The query for all the processes, pending and sht
79
- const clientProcesses = useQueries({
80
- queries: Object.entries(clientProcessDtos).map(([processId, { dto, metadata, options }]) => ({
81
- staleTime: Infinity,
82
- gcTime: 3 * 60 * 1000,
83
- refetchOnWindowFocus: true,
84
- refetchIntervalInBackground: true,
85
- refetchOnMount: true,
86
- refetchOnReconnect: true,
87
- queryKey: ['.__process', metadata.filterKey, processId],
88
- queryFn: async () => {
89
- if (!processId)
90
- throw new Error('No processId');
91
- const { process } = await processStatusChecker(processId);
92
- // Handled! Return the proper response too
93
- if (process?.status === 'handled') {
94
- const result = await processCallbackResultFetcher(process.id, process.name, dto.processCallbackUrl);
95
- // Log and call runner
96
- console.warn(`Handled ${process.name} (${process.id})!`);
97
- options?.onSuccess?.(process.id, process.name, result);
98
- return {
99
- dto,
100
- metadata,
101
- options,
102
- success: true,
103
- pending: false,
104
- result,
105
- };
106
- }
107
- // Pending or failed
108
- if (process?.status === 'pending') {
109
- console.log(`Running process ${process.name} (${process.id})...`);
110
- options?.onLoading?.(process.id, process.name);
111
- return { dto, metadata, options, pending: true };
112
- }
113
- else if (process?.status === 'failed') {
114
- console.error(`Failed ${process.name} (${process.id}) - ${process.error}.`);
115
- return {
116
- dto,
117
- metadata,
118
- options,
119
- success: false,
120
- pending: false,
121
- error: process.error,
122
- };
123
- }
124
- // Return null since no response
125
- return { dto, metadata, options, pending: true };
126
- },
127
- // Returns the next interval length to use to wait for fetch
128
- refetchInterval: (query) => {
129
- const data = query.state.data;
130
- if (data?.success !== undefined)
131
- return false;
132
- // If not defined, init to 0
133
- if (!clientProcessAttempts.current[processId])
134
- clientProcessAttempts.current[processId] = 0;
135
- const interval = Math.min(500 * 2 ** clientProcessAttempts.current[processId], 10000);
136
- clientProcessAttempts.current[processId] += 1;
137
- return interval;
138
- },
139
- })),
140
- });
141
- /**
142
- * Registers a new process to handle
143
- *
144
- * @param processId
145
- * @param processCallbackDto
146
- * @param options
147
- */
148
- const registerProcess = (processId, processCallbackDto, metadata, options) => {
149
- setClientProcessDtos({
150
- ...clientProcessDtos,
151
- [processId]: { dto: processCallbackDto, metadata, options },
152
- });
153
- };
154
- /**
155
- * Retrieves a process
156
- *
157
- * @param processId
158
- * @returns
159
- */
160
- const getProcess = (processId) => {
161
- const clientProcess = clientProcesses.find((clientProcess) => clientProcess.data?.dto?.processId === processId)?.data ?? null;
162
- return clientProcess ?? null;
163
- };
164
- /**
165
- * Filters processes by the provided attribute.
166
- *
167
- * @param filterKey
168
- */
169
- const filterProcesses = (filterKey) => {
170
- return clientProcesses
171
- .filter((clientProcess) => !!clientProcess.data?.dto)
172
- .map((clientProcess) => clientProcess.data)
173
- .filter((clientProcessData) => clientProcessData?.metadata.filterKey === filterKey);
174
- };
175
- /**
176
- * Refreshes the cache and invalidates all related processes.
177
- */
178
- const invalidateBasedOnResult = async (filter, invalidator) => {
179
- await queryClient.invalidateQueries({
180
- queryKey: ['.__process', filter],
181
- exact: false,
182
- });
183
- const cachedClientProcesses = queryClient
184
- .getQueryCache()
185
- .findAll({ queryKey: ['.__process', filter], exact: false });
186
- for (const query of cachedClientProcesses) {
187
- const processId = query.queryKey[2];
188
- if (typeof processId !== 'string')
189
- continue;
190
- const data = query.state.data;
191
- if (!data.result)
192
- continue;
193
- // If invalidator says we gotta clear the cache, we clear it
194
- if (invalidator && invalidator?.(data.result)) {
195
- queryClient.removeQueries({ queryKey: query.queryKey });
196
- }
197
- }
198
- };
199
- return (jsx(clientProcessesContext.Provider, { value: {
200
- registerProcess,
201
- getProcess,
202
- filterProcesses,
203
- invalidateBasedOnResult,
204
- }, children: children }));
205
- };
206
- /**
207
- * A simpler version, where u just pass the async function that returns process data.
208
- *
209
- * @param caller
210
- * @returns
211
- */
212
- function useClientProcess({ caller, invalidator, filterKey, onLoading, onSuccess, onFailure, }) {
213
- const clientProcessesApi = useContext(clientProcessesContext);
214
- // Note that the exposed function doesn't return process ids
215
- // Client should be oblivious to those
216
- // Abstraction BABY!
217
- const wrappedCaller = async (inputs, metadata) => {
218
- const response = await caller?.(inputs);
219
- if (response) {
220
- clientProcessesApi.registerProcess(response.processId, response, { filterKey, metadata }, {
221
- onLoading,
222
- onSuccess,
223
- onFailure,
224
- });
225
- return {
226
- id: response.processId,
227
- success: response.success,
228
- message: response.message,
229
- };
230
- }
231
- return {
232
- id: null,
233
- success: false,
234
- message: 'No `caller` specified for useClientProcess().',
235
- };
236
- };
237
- return {
238
- run: wrappedCaller,
239
- get: (id) => clientProcessesApi.getProcess(id),
240
- getAll: () => clientProcessesApi.filterProcesses(filterKey),
241
- getAllPending: () => clientProcessesApi
242
- .filterProcesses(filterKey)
243
- .filter((process) => process.pending),
244
- getAllHandled: () => clientProcessesApi
245
- .filterProcesses(filterKey)
246
- .filter((process) => process.success),
247
- getAllFailed: () => clientProcessesApi
248
- .filterProcesses(filterKey)
249
- .filter((process) => !process.success),
250
- refreshCache: () => clientProcessesApi.invalidateBasedOnResult(filterKey, invalidator),
251
- };
252
- }
253
-
254
- export { ClientProcessesProvider, useClientProcess };
255
- //# sourceMappingURL=useClientProcess.js.map
1
+ "use client";
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { createContext, useRef, useState, useEffect, useContext } from 'react';
4
+ import { useQueryClient, useQueries } from '@tanstack/react-query';
5
+ import { config } from '../config.js';
6
+
7
+ /**
8
+ * The orchestrator service to get the process from.
9
+ *
10
+ * @param processId
11
+ * @returns
12
+ */
13
+ const processStatusChecker = (processId) => {
14
+ if (!processId)
15
+ return { process: null };
16
+ return fetch(`${config.orchestratorApi}/${encodeURIComponent(processId)}`, {
17
+ method: 'GET',
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ }).then((response) => response.json());
22
+ };
23
+ /**
24
+ * This fetches from the callback endpoint.
25
+ *
26
+ * @param processId
27
+ * @returns
28
+ */
29
+ const processCallbackResultFetcher = (processId, processName, processCallbackUrl) => fetch(`${processCallbackUrl}`, {
30
+ method: 'POST',
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ },
34
+ body: JSON.stringify({
35
+ processId,
36
+ processName,
37
+ }),
38
+ }).then((response) => response.json());
39
+ /**
40
+ * We need a context to keep track of all client processes.
41
+ * This helps us manage the state of the toasts as well.
42
+ */
43
+ const clientProcessesContext = createContext({});
44
+ /**
45
+ * This should be at the root layout.
46
+ *
47
+ * @context
48
+ */
49
+ const ClientProcessesProvider = ({ children, }) => {
50
+ const queryClient = useQueryClient();
51
+ const clientProcessAttempts = useRef({});
52
+ const [clientProcessDtos, setClientProcessDtos] = useState({});
53
+ // Populates the state object with cached data on mount
54
+ useEffect(() => {
55
+ setClientProcessDtos((oldData) => {
56
+ if (Object.keys(oldData).length > 0)
57
+ return oldData;
58
+ const cachedClientProcesses = queryClient
59
+ .getQueryCache()
60
+ .findAll({ queryKey: ['.__process'], exact: false });
61
+ const seededClientProcesses = {};
62
+ for (const query of cachedClientProcesses) {
63
+ const processId = query.queryKey[2];
64
+ if (typeof processId !== 'string')
65
+ continue;
66
+ const data = query.state.data;
67
+ if (!data?.dto || !data?.metadata)
68
+ continue;
69
+ seededClientProcesses[processId] = {
70
+ dto: data.dto,
71
+ metadata: data.metadata,
72
+ options: data.options,
73
+ };
74
+ }
75
+ return { ...oldData, ...seededClientProcesses };
76
+ });
77
+ }, [queryClient]);
78
+ // The query for all the processes, pending and sht
79
+ const clientProcesses = useQueries({
80
+ queries: Object.entries(clientProcessDtos).map(([processId, { dto, metadata, options }]) => ({
81
+ staleTime: Infinity,
82
+ gcTime: 3 * 60 * 1000,
83
+ refetchOnWindowFocus: true,
84
+ refetchIntervalInBackground: true,
85
+ refetchOnMount: true,
86
+ refetchOnReconnect: true,
87
+ queryKey: ['.__process', metadata.filterKey, processId],
88
+ queryFn: async () => {
89
+ if (!processId)
90
+ throw new Error('No processId');
91
+ const { process } = await processStatusChecker(processId);
92
+ // Handled! Return the proper response too
93
+ if (process?.status === 'handled') {
94
+ const result = await processCallbackResultFetcher(process.id, process.name, dto.processCallbackUrl);
95
+ // Log and call runner
96
+ console.warn(`Handled ${process.name} (${process.id})!`);
97
+ options?.onSuccess?.(process.id, process.name, result);
98
+ return {
99
+ dto,
100
+ metadata,
101
+ options,
102
+ success: true,
103
+ pending: false,
104
+ result,
105
+ };
106
+ }
107
+ // Pending or failed
108
+ if (process?.status === 'pending') {
109
+ console.log(`Running process ${process.name} (${process.id})...`);
110
+ options?.onLoading?.(process.id, process.name);
111
+ return { dto, metadata, options, pending: true };
112
+ }
113
+ else if (process?.status === 'failed') {
114
+ console.error(`Failed ${process.name} (${process.id}) - ${process.error}.`);
115
+ return {
116
+ dto,
117
+ metadata,
118
+ options,
119
+ success: false,
120
+ pending: false,
121
+ error: process.error,
122
+ };
123
+ }
124
+ // Return null since no response
125
+ return { dto, metadata, options, pending: true };
126
+ },
127
+ // Returns the next interval length to use to wait for fetch
128
+ refetchInterval: (query) => {
129
+ const data = query.state.data;
130
+ if (data?.success !== undefined)
131
+ return false;
132
+ // If not defined, init to 0
133
+ if (!clientProcessAttempts.current[processId])
134
+ clientProcessAttempts.current[processId] = 0;
135
+ const interval = Math.min(500 * 2 ** clientProcessAttempts.current[processId], 10000);
136
+ clientProcessAttempts.current[processId] += 1;
137
+ return interval;
138
+ },
139
+ })),
140
+ });
141
+ /**
142
+ * Registers a new process to handle
143
+ *
144
+ * @param processId
145
+ * @param processCallbackDto
146
+ * @param options
147
+ */
148
+ const registerProcess = (processId, processCallbackDto, metadata, options) => {
149
+ setClientProcessDtos({
150
+ ...clientProcessDtos,
151
+ [processId]: { dto: processCallbackDto, metadata, options },
152
+ });
153
+ };
154
+ /**
155
+ * Retrieves a process
156
+ *
157
+ * @param processId
158
+ * @returns
159
+ */
160
+ const getProcess = (processId) => {
161
+ const clientProcess = clientProcesses.find((clientProcess) => clientProcess.data?.dto?.processId === processId)?.data ?? null;
162
+ return clientProcess ?? null;
163
+ };
164
+ /**
165
+ * Filters processes by the provided attribute.
166
+ *
167
+ * @param filterKey
168
+ */
169
+ const filterProcesses = (filterKey) => {
170
+ return clientProcesses
171
+ .filter((clientProcess) => !!clientProcess.data?.dto)
172
+ .map((clientProcess) => clientProcess.data)
173
+ .filter((clientProcessData) => clientProcessData?.metadata.filterKey === filterKey);
174
+ };
175
+ /**
176
+ * Refreshes the cache and invalidates all related processes.
177
+ */
178
+ const invalidateBasedOnResult = async (filter, invalidator) => {
179
+ await queryClient.invalidateQueries({
180
+ queryKey: ['.__process', filter],
181
+ exact: false,
182
+ });
183
+ const cachedClientProcesses = queryClient
184
+ .getQueryCache()
185
+ .findAll({ queryKey: ['.__process', filter], exact: false });
186
+ for (const query of cachedClientProcesses) {
187
+ const processId = query.queryKey[2];
188
+ if (typeof processId !== 'string')
189
+ continue;
190
+ const data = query.state.data;
191
+ if (!data.result)
192
+ continue;
193
+ // If invalidator says we gotta clear the cache, we clear it
194
+ if (invalidator && invalidator?.(data.result)) {
195
+ queryClient.removeQueries({ queryKey: query.queryKey });
196
+ }
197
+ }
198
+ };
199
+ return (jsx(clientProcessesContext.Provider, { value: {
200
+ registerProcess,
201
+ getProcess,
202
+ filterProcesses,
203
+ invalidateBasedOnResult,
204
+ }, children: children }));
205
+ };
206
+ /**
207
+ * A simpler version, where u just pass the async function that returns process data.
208
+ *
209
+ * @param caller
210
+ * @returns
211
+ */
212
+ function useClientProcess({ caller, invalidator, filterKey, onLoading, onSuccess, onFailure, }) {
213
+ const clientProcessesApi = useContext(clientProcessesContext);
214
+ // Note that the exposed function doesn't return process ids
215
+ // Client should be oblivious to those
216
+ // Abstraction BABY!
217
+ const wrappedCaller = async (inputs, metadata) => {
218
+ const response = await caller?.(inputs);
219
+ if (response) {
220
+ clientProcessesApi.registerProcess(response.processId, response, { filterKey, metadata }, {
221
+ onLoading,
222
+ onSuccess,
223
+ onFailure,
224
+ });
225
+ return {
226
+ id: response.processId,
227
+ success: response.success,
228
+ message: response.message,
229
+ };
230
+ }
231
+ return {
232
+ id: null,
233
+ success: false,
234
+ message: 'No `caller` specified for useClientProcess().',
235
+ };
236
+ };
237
+ return {
238
+ run: wrappedCaller,
239
+ get: (id) => clientProcessesApi.getProcess(id),
240
+ getAll: () => clientProcessesApi.filterProcesses(filterKey),
241
+ getAllPending: () => clientProcessesApi
242
+ .filterProcesses(filterKey)
243
+ .filter((process) => process.pending),
244
+ getAllHandled: () => clientProcessesApi
245
+ .filterProcesses(filterKey)
246
+ .filter((process) => process.success),
247
+ getAllFailed: () => clientProcessesApi
248
+ .filterProcesses(filterKey)
249
+ .filter((process) => !process.success),
250
+ refreshCache: () => clientProcessesApi.invalidateBasedOnResult(filterKey, invalidator),
251
+ };
252
+ }
253
+
254
+ export { ClientProcessesProvider, useClientProcess };
255
+ //# sourceMappingURL=useClientProcess.js.map
@@ -30,9 +30,9 @@ export declare const MQJobsProvider: ({ poll, children, }: MQJobsConfig & {
30
30
  * `status` is terminal.
31
31
  */
32
32
  export declare function useMQJob<T = unknown>(jobId?: string | null): {
33
- job: MQJob<T> | undefined;
33
+ job: MQJob<unknown> | undefined;
34
34
  status: MQJobStatus | undefined;
35
- result: T | undefined;
35
+ result: unknown;
36
36
  error: string | undefined;
37
37
  isPending: boolean;
38
38
  isDone: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"useMQJob.js","sources":["../../src/hooks/useMQJob.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, useContext, useRef } from 'react';\nimport { useQueries, useQuery, Query } from '@tanstack/react-query';\n\nexport type MQJobStatus = 'queued' | 'processing' | 'done' | 'failed';\n\nexport interface MQJob<T = unknown> {\n id: string;\n type: string;\n status: MQJobStatus;\n progress?: { total: number; done: number; failed: number };\n result?: T;\n error?: string;\n}\n\ninterface MQJobsConfig {\n /**\n * Fetches the current state of a job. The package knows no URL and no\n * response envelope — the consuming app owns both (plan §5.2). Reject/throw\n * on a transport or not-found failure; React Query's own retry handles it.\n */\n poll: (jobId: string) => Promise<MQJob>;\n}\n\nconst MQJobsContext = createContext<MQJobsConfig | null>(null);\n\n/** Mount once near the root, alongside the app's other data providers. */\nexport const MQJobsProvider = ({\n poll,\n children,\n}: MQJobsConfig & { children: React.ReactNode }) => (\n <MQJobsContext.Provider value={{ poll }}>{children}</MQJobsContext.Provider>\n);\n\nconst useMQJobsConfig = (caller: string): MQJobsConfig => {\n const ctx = useContext(MQJobsContext);\n if (!ctx)\n throw new Error(`${caller}() must be used within a <MQJobsProvider>.`);\n return ctx;\n};\n\nconst BASE_INTERVAL_MS = 2000;\nconst MAX_INTERVAL_MS = 10000;\n\n/** Capped backoff keyed per job id, so each job restarts its own count. */\nconst nextInterval = (\n attempts: { current: Record<string, number> },\n key: string,\n status: MQJobStatus | undefined,\n): number | false => {\n if (status === 'done' || status === 'failed') return false;\n const attempt = attempts.current[key] ?? 0;\n attempts.current[key] = attempt + 1;\n return Math.min(BASE_INTERVAL_MS * 1.5 ** attempt, MAX_INTERVAL_MS);\n};\n\nconst summarize = <T,>(job: MQJob<T> | undefined) => ({\n job,\n status: job?.status,\n result: job?.result,\n error: job?.error,\n isPending: !job || job.status === 'queued' || job.status === 'processing',\n isDone: job?.status === 'done',\n isFailed: job?.status === 'failed',\n});\n\n/**\n * Polls a single `internal.mq_jobs` row to completion. Replaces\n * `useClientProcess` for anything queued through the broker: no\n * callback-URL dance, the result comes back inline on the job itself once\n * `status` is terminal.\n */\nexport function useMQJob<T = unknown>(jobId?: string | null) {\n const { poll } = useMQJobsConfig('useMQJob');\n const attempts = useRef<Record<string, number>>({});\n\n const query = useQuery<MQJob<T>>({\n queryKey: ['.__mq_job', jobId],\n // `poll`'s result field is caller-typed via `T`; the transport itself\n // doesn't know or care what shape a given job's result has.\n queryFn: () => poll(jobId as string) as Promise<MQJob<T>>,\n enabled: !!jobId,\n staleTime: Infinity,\n refetchOnWindowFocus: true,\n refetchIntervalInBackground: true,\n refetchInterval: (query: Query<MQJob<T>>) =>\n nextInterval(attempts, jobId ?? '', query.state.data?.status),\n });\n\n return summarize(query.data);\n}\n\n/**\n * Polls a dynamic set of `internal.mq_jobs` rows to completion — for a\n * feature that can have several jobs in flight at once (e.g. a list of\n * pending/handled items). Built on `useQueries` directly rather than\n * composing `useMQJob` in a loop, since the number of tracked ids changes\n * over time and that would violate the rules of hooks.\n */\nexport function useMQJobs<T = unknown>(jobIds: readonly string[]) {\n const { poll } = useMQJobsConfig('useMQJobs');\n const attempts = useRef<Record<string, number>>({});\n\n const queries = useQueries({\n queries: jobIds.map((jobId) => ({\n queryKey: ['.__mq_job', jobId],\n queryFn: () => poll(jobId) as Promise<MQJob<T>>,\n staleTime: Infinity,\n refetchOnWindowFocus: true,\n refetchIntervalInBackground: true,\n refetchInterval: (query: Query<MQJob<T>>) =>\n nextInterval(attempts, jobId, query.state.data?.status),\n })),\n });\n\n return jobIds.map((jobId, index) => ({\n jobId,\n ...summarize(queries[index]?.data as MQJob<T> | undefined),\n }));\n}\n"],"names":[],"mappings":";;;;;AAyBA;AAEA;AACO;AAOP;AACE;AACA;AACE;AACF;AACF;AAEA;AACA;AAEA;AACA;AAKE;AAA8C;;;AAG9C;AACF;AAEA;;;;;AAKE;AACA;AACA;AACD;AAED;;;;;AAKG;AACG;;AAEJ;;AAGE;;;AAGA;;AAEA;AACA;AACA;;AAGD;AAED;AACF;AAEA;;;;;;AAMG;AACG;;AAEJ;;;AAII;AACA;AACA;AACA;AACA;AACA;AAED;AACF;;;;AAKA;AACH;;"}
1
+ {"version":3,"file":"useMQJob.js","sources":["../../src/hooks/useMQJob.tsx"],"sourcesContent":["'use client';\r\n\r\nimport { createContext, useContext, useRef } from 'react';\r\nimport { useQueries, useQuery, Query } from '@tanstack/react-query';\r\n\r\nexport type MQJobStatus = 'queued' | 'processing' | 'done' | 'failed';\r\n\r\nexport interface MQJob<T = unknown> {\r\n id: string;\r\n type: string;\r\n status: MQJobStatus;\r\n progress?: { total: number; done: number; failed: number };\r\n result?: T;\r\n error?: string;\r\n}\r\n\r\ninterface MQJobsConfig {\r\n /**\r\n * Fetches the current state of a job. The package knows no URL and no\r\n * response envelope — the consuming app owns both (plan §5.2). Reject/throw\r\n * on a transport or not-found failure; React Query's own retry handles it.\r\n */\r\n poll: (jobId: string) => Promise<MQJob>;\r\n}\r\n\r\nconst MQJobsContext = createContext<MQJobsConfig | null>(null);\r\n\r\n/** Mount once near the root, alongside the app's other data providers. */\r\nexport const MQJobsProvider = ({\r\n poll,\r\n children,\r\n}: MQJobsConfig & { children: React.ReactNode }) => (\r\n <MQJobsContext.Provider value={{ poll }}>{children}</MQJobsContext.Provider>\r\n);\r\n\r\nconst useMQJobsConfig = (caller: string): MQJobsConfig => {\r\n const ctx = useContext(MQJobsContext);\r\n if (!ctx)\r\n throw new Error(`${caller}() must be used within a <MQJobsProvider>.`);\r\n return ctx;\r\n};\r\n\r\nconst BASE_INTERVAL_MS = 2000;\r\nconst MAX_INTERVAL_MS = 10000;\r\n\r\n/** Capped backoff keyed per job id, so each job restarts its own count. */\r\nconst nextInterval = (\r\n attempts: { current: Record<string, number> },\r\n key: string,\r\n status: MQJobStatus | undefined,\r\n): number | false => {\r\n if (status === 'done' || status === 'failed') return false;\r\n const attempt = attempts.current[key] ?? 0;\r\n attempts.current[key] = attempt + 1;\r\n return Math.min(BASE_INTERVAL_MS * 1.5 ** attempt, MAX_INTERVAL_MS);\r\n};\r\n\r\nconst summarize = <T,>(job: MQJob<T> | undefined) => ({\r\n job,\r\n status: job?.status,\r\n result: job?.result,\r\n error: job?.error,\r\n isPending: !job || job.status === 'queued' || job.status === 'processing',\r\n isDone: job?.status === 'done',\r\n isFailed: job?.status === 'failed',\r\n});\r\n\r\n/**\r\n * Polls a single `internal.mq_jobs` row to completion. Replaces\r\n * `useClientProcess` for anything queued through the broker: no\r\n * callback-URL dance, the result comes back inline on the job itself once\r\n * `status` is terminal.\r\n */\r\nexport function useMQJob<T = unknown>(jobId?: string | null) {\r\n const { poll } = useMQJobsConfig('useMQJob');\r\n const attempts = useRef<Record<string, number>>({});\r\n\r\n const query = useQuery<MQJob<T>>({\r\n queryKey: ['.__mq_job', jobId],\r\n // `poll`'s result field is caller-typed via `T`; the transport itself\r\n // doesn't know or care what shape a given job's result has.\r\n queryFn: () => poll(jobId as string) as Promise<MQJob<T>>,\r\n enabled: !!jobId,\r\n staleTime: Infinity,\r\n refetchOnWindowFocus: true,\r\n refetchIntervalInBackground: true,\r\n refetchInterval: (query: Query<MQJob<T>>) =>\r\n nextInterval(attempts, jobId ?? '', query.state.data?.status),\r\n });\r\n\r\n return summarize(query.data);\r\n}\r\n\r\n/**\r\n * Polls a dynamic set of `internal.mq_jobs` rows to completion — for a\r\n * feature that can have several jobs in flight at once (e.g. a list of\r\n * pending/handled items). Built on `useQueries` directly rather than\r\n * composing `useMQJob` in a loop, since the number of tracked ids changes\r\n * over time and that would violate the rules of hooks.\r\n */\r\nexport function useMQJobs<T = unknown>(jobIds: readonly string[]) {\r\n const { poll } = useMQJobsConfig('useMQJobs');\r\n const attempts = useRef<Record<string, number>>({});\r\n\r\n const queries = useQueries({\r\n queries: jobIds.map((jobId) => ({\r\n queryKey: ['.__mq_job', jobId],\r\n queryFn: () => poll(jobId) as Promise<MQJob<T>>,\r\n staleTime: Infinity,\r\n refetchOnWindowFocus: true,\r\n refetchIntervalInBackground: true,\r\n refetchInterval: (query: Query<MQJob<T>>) =>\r\n nextInterval(attempts, jobId, query.state.data?.status),\r\n })),\r\n });\r\n\r\n return jobIds.map((jobId, index) => ({\r\n jobId,\r\n ...summarize(queries[index]?.data as MQJob<T> | undefined),\r\n }));\r\n}\r\n"],"names":[],"mappings":";;;;;AAyBA;AAEA;AACO;AAOP;AACE;AACA;AACE;AACF;AACF;AAEA;AACA;AAEA;AACA;AAKE;AAA8C;;;AAG9C;AACF;AAEA;;;;;AAKE;AACA;AACA;AACD;AAED;;;;;AAKG;AACG;;AAEJ;;AAGE;;;AAGA;;AAEA;AACA;AACA;;AAGD;AAED;AACF;AAEA;;;;;;AAMG;AACG;;AAEJ;;;AAII;AACA;AACA;AACA;AACA;AACA;AAED;AACF;;;;AAKA;AACH;;"}
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../src/utils.ts"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}"],"names":[],"mappings":";;;AAGM,SAAU,EAAE,CAAC,GAAG,MAAoB,EAAA;AACxC,IAAA,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC9B;;;;"}
1
+ {"version":3,"file":"utils.js","sources":["../src/utils.ts"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: ClassValue[]) {\r\n return twMerge(clsx(inputs));\r\n}"],"names":[],"mappings":";;;AAGM,SAAU,EAAE,CAAC,GAAG,MAAoB,EAAA;AACxC,IAAA,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC9B;;;;"}