@nicnocquee/dataqueue-react 0.2.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 +91 -0
- package/dist/index.cjs +103 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @nicnocquee/dataqueue-react
|
|
2
|
+
|
|
3
|
+
React hooks for subscribing to [dataqueue](https://github.com/nicnocquee/dataqueue) job status and progress.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nicnocquee/dataqueue-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires React 18+.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { useJob } from '@nicnocquee/dataqueue-react';
|
|
17
|
+
|
|
18
|
+
function JobTracker({ jobId }: { jobId: number }) {
|
|
19
|
+
const { status, progress, isLoading, error } = useJob(jobId, {
|
|
20
|
+
fetcher: (id) =>
|
|
21
|
+
fetch(`/api/jobs/${id}`)
|
|
22
|
+
.then((r) => r.json())
|
|
23
|
+
.then((d) => d.job),
|
|
24
|
+
pollingInterval: 1000,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (isLoading) return <p>Loading...</p>;
|
|
28
|
+
if (error) return <p>Error: {error.message}</p>;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
<p>Status: {status}</p>
|
|
33
|
+
<progress value={progress ?? 0} max={100} />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Provider
|
|
40
|
+
|
|
41
|
+
Use `DataqueueProvider` to share config across your app:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { DataqueueProvider } from '@nicnocquee/dataqueue-react';
|
|
45
|
+
|
|
46
|
+
function App() {
|
|
47
|
+
return (
|
|
48
|
+
<DataqueueProvider
|
|
49
|
+
fetcher={(id) =>
|
|
50
|
+
fetch(`/api/jobs/${id}`)
|
|
51
|
+
.then((r) => r.json())
|
|
52
|
+
.then((d) => d.job)
|
|
53
|
+
}
|
|
54
|
+
pollingInterval={2000}
|
|
55
|
+
>
|
|
56
|
+
<YourApp />
|
|
57
|
+
</DataqueueProvider>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then use `useJob` anywhere without repeating the fetcher:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
const { status, progress } = useJob(jobId);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## API
|
|
69
|
+
|
|
70
|
+
### useJob(jobId, options?)
|
|
71
|
+
|
|
72
|
+
| Option | Type | Default | Description |
|
|
73
|
+
| ----------------- | ---------------------------------- | ------------- | --------------------------------- |
|
|
74
|
+
| `fetcher` | `(id: number) => Promise<JobData>` | from provider | Function that fetches a job by ID |
|
|
75
|
+
| `pollingInterval` | `number` | `1000` | Milliseconds between polls |
|
|
76
|
+
| `enabled` | `boolean` | `true` | Set to `false` to pause polling |
|
|
77
|
+
| `onStatusChange` | `(newStatus, oldStatus) => void` | — | Called when status changes |
|
|
78
|
+
| `onComplete` | `(job) => void` | — | Called when job completes |
|
|
79
|
+
| `onFailed` | `(job) => void` | — | Called when job fails |
|
|
80
|
+
|
|
81
|
+
Returns `{ data, status, progress, isLoading, error }`.
|
|
82
|
+
|
|
83
|
+
Polling stops automatically when the job reaches a terminal status (`completed`, `failed`, `cancelled`).
|
|
84
|
+
|
|
85
|
+
## Documentation
|
|
86
|
+
|
|
87
|
+
Full documentation: [dataqueue docs](https://dataqueue.nico.fyi/docs/usage/react-sdk)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/context.tsx
|
|
7
|
+
var DataqueueContext = react.createContext(null);
|
|
8
|
+
function DataqueueProvider({
|
|
9
|
+
children,
|
|
10
|
+
...config
|
|
11
|
+
}) {
|
|
12
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DataqueueContext.Provider, { value: config, children });
|
|
13
|
+
}
|
|
14
|
+
function useDataqueueConfig() {
|
|
15
|
+
return react.useContext(DataqueueContext);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/types.ts
|
|
19
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
20
|
+
"completed",
|
|
21
|
+
"failed",
|
|
22
|
+
"cancelled"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// src/use-job.ts
|
|
26
|
+
var DEFAULT_POLLING_INTERVAL = 1e3;
|
|
27
|
+
function useJob(jobId, options = {}) {
|
|
28
|
+
const providerConfig = useDataqueueConfig();
|
|
29
|
+
const fetcher = options.fetcher ?? providerConfig?.fetcher;
|
|
30
|
+
const pollingInterval = options.pollingInterval ?? providerConfig?.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
31
|
+
const enabled = options.enabled !== false;
|
|
32
|
+
const [data, setData] = react.useState(null);
|
|
33
|
+
const [error, setError] = react.useState(null);
|
|
34
|
+
const [isLoading, setIsLoading] = react.useState(jobId != null && enabled);
|
|
35
|
+
const prevStatusRef = react.useRef(null);
|
|
36
|
+
const inFlightRef = react.useRef(false);
|
|
37
|
+
const onStatusChangeRef = react.useRef(options.onStatusChange);
|
|
38
|
+
onStatusChangeRef.current = options.onStatusChange;
|
|
39
|
+
const onCompleteRef = react.useRef(options.onComplete);
|
|
40
|
+
onCompleteRef.current = options.onComplete;
|
|
41
|
+
const onFailedRef = react.useRef(options.onFailed);
|
|
42
|
+
onFailedRef.current = options.onFailed;
|
|
43
|
+
const terminalRef = react.useRef(false);
|
|
44
|
+
const fetchJob = react.useCallback(async () => {
|
|
45
|
+
if (!fetcher || jobId == null || inFlightRef.current) return;
|
|
46
|
+
inFlightRef.current = true;
|
|
47
|
+
try {
|
|
48
|
+
const result = await fetcher(jobId);
|
|
49
|
+
setData(result);
|
|
50
|
+
setError(null);
|
|
51
|
+
setIsLoading(false);
|
|
52
|
+
const newStatus = result.status;
|
|
53
|
+
const prevStatus = prevStatusRef.current;
|
|
54
|
+
if (prevStatus !== newStatus) {
|
|
55
|
+
prevStatusRef.current = newStatus;
|
|
56
|
+
onStatusChangeRef.current?.(newStatus, prevStatus);
|
|
57
|
+
if (newStatus === "completed") {
|
|
58
|
+
onCompleteRef.current?.(result);
|
|
59
|
+
} else if (newStatus === "failed") {
|
|
60
|
+
onFailedRef.current?.(result);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (TERMINAL_STATUSES.has(newStatus)) {
|
|
64
|
+
terminalRef.current = true;
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
} finally {
|
|
70
|
+
inFlightRef.current = false;
|
|
71
|
+
}
|
|
72
|
+
}, [fetcher, jobId]);
|
|
73
|
+
react.useEffect(() => {
|
|
74
|
+
setData(null);
|
|
75
|
+
setError(null);
|
|
76
|
+
prevStatusRef.current = null;
|
|
77
|
+
terminalRef.current = false;
|
|
78
|
+
inFlightRef.current = false;
|
|
79
|
+
setIsLoading(jobId != null && enabled);
|
|
80
|
+
}, [jobId, enabled]);
|
|
81
|
+
react.useEffect(() => {
|
|
82
|
+
if (!fetcher || jobId == null || !enabled) return;
|
|
83
|
+
fetchJob();
|
|
84
|
+
const id = setInterval(() => {
|
|
85
|
+
if (!terminalRef.current) {
|
|
86
|
+
fetchJob();
|
|
87
|
+
}
|
|
88
|
+
}, pollingInterval);
|
|
89
|
+
return () => clearInterval(id);
|
|
90
|
+
}, [fetchJob, pollingInterval, jobId, enabled, fetcher]);
|
|
91
|
+
return {
|
|
92
|
+
data,
|
|
93
|
+
status: data?.status ?? null,
|
|
94
|
+
progress: data?.progress ?? null,
|
|
95
|
+
isLoading,
|
|
96
|
+
error
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
exports.DataqueueProvider = DataqueueProvider;
|
|
101
|
+
exports.useJob = useJob;
|
|
102
|
+
//# sourceMappingURL=index.cjs.map
|
|
103
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/types.ts","../src/use-job.ts"],"names":["createContext","useContext","useState","useRef","useCallback","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["'use client';\n\nimport { createContext, useContext } from 'react';\nimport type { DataqueueConfig } from './types.js';\n\nconst DataqueueContext = createContext<DataqueueConfig | null>(null);\n\n/**\n * Provides default configuration (fetcher, pollingInterval) to all\n * `useJob` hooks in the subtree. Optional -- hooks can also accept\n * inline config via their options parameter.\n */\nexport function DataqueueProvider({\n children,\n ...config\n}: DataqueueConfig & { children: React.ReactNode }) {\n return (\n <DataqueueContext.Provider value={config}>\n {children}\n </DataqueueContext.Provider>\n );\n}\n\n/**\n * Internal hook to read the provider config. Returns null when no provider\n * is present (hooks fall back to their own options).\n */\nexport function useDataqueueConfig(): DataqueueConfig | null {\n return useContext(DataqueueContext);\n}\n","/**\n * Job status values matching the core dataqueue package.\n * Redefined here so the React SDK has zero runtime dependency on the server package.\n */\nexport type JobStatus =\n | 'pending'\n | 'processing'\n | 'completed'\n | 'failed'\n | 'cancelled'\n | 'waiting';\n\n/** Terminal statuses where polling should stop automatically. */\nexport const TERMINAL_STATUSES: ReadonlySet<JobStatus> = new Set([\n 'completed',\n 'failed',\n 'cancelled',\n]);\n\n/**\n * Minimal job data shape returned by the fetcher.\n * Users can return a full JobRecord from the server or any object\n * that includes at least `status` and optionally `progress`.\n */\nexport interface JobData {\n id: number;\n status: JobStatus;\n progress?: number | null;\n [key: string]: unknown;\n}\n\n/**\n * A fetcher function that retrieves a job by its ID.\n * Should return a `JobData`-compatible object or throw on error.\n */\nexport type JobFetcher = (jobId: number) => Promise<JobData>;\n\n/**\n * Configuration provided to `DataqueueProvider`.\n */\nexport interface DataqueueConfig {\n /** Fetcher function to retrieve a job by ID from your API. */\n fetcher: JobFetcher;\n /** Default polling interval in milliseconds. Defaults to 1000. */\n pollingInterval?: number;\n}\n\n/**\n * Options for the `useJob` hook.\n */\nexport interface UseJobOptions {\n /** Override the provider's fetcher for this specific hook instance. */\n fetcher?: JobFetcher;\n /** Override the provider's polling interval (ms) for this hook instance. */\n pollingInterval?: number;\n /** Whether polling is enabled. Defaults to true. Set to false to pause. */\n enabled?: boolean;\n /** Called when the job's status changes. */\n onStatusChange?: (newStatus: JobStatus, prevStatus: JobStatus | null) => void;\n /** Called when the job reaches 'completed' status. */\n onComplete?: (data: JobData) => void;\n /** Called when the job reaches 'failed' status. */\n onFailed?: (data: JobData) => void;\n}\n\n/**\n * Return value of the `useJob` hook.\n */\nexport interface UseJobReturn {\n /** The full job data from the last successful fetch, or null if not yet loaded. */\n data: JobData | null;\n /** The current job status, or null if not yet loaded. */\n status: JobStatus | null;\n /** The current progress percentage (0-100), or null if not reported. */\n progress: number | null;\n /** True during the initial fetch (before any data is available). */\n isLoading: boolean;\n /** The error from the last failed fetch, or null. */\n error: Error | null;\n}\n","'use client';\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport { useDataqueueConfig } from './context.js';\nimport type {\n JobData,\n JobFetcher,\n JobStatus,\n UseJobOptions,\n UseJobReturn,\n} from './types.js';\nimport { TERMINAL_STATUSES } from './types.js';\n\nconst DEFAULT_POLLING_INTERVAL = 1000;\n\n/**\n * Subscribe to a job's status and progress via polling.\n *\n * @param jobId - The numeric job ID to subscribe to, or `null`/`undefined` to skip polling.\n * @param options - Optional overrides and callbacks.\n * @returns An object with `data`, `status`, `progress`, `isLoading`, and `error`.\n *\n * @example\n * ```tsx\n * const { status, progress, data, error } = useJob(jobId, {\n * fetcher: (id) => fetch(`/api/jobs/${id}`).then(r => r.json()).then(d => d.job),\n * pollingInterval: 1000,\n * onComplete: (job) => console.log('Done!', job),\n * });\n * ```\n */\nexport function useJob(\n jobId: number | null | undefined,\n options: UseJobOptions = {},\n): UseJobReturn {\n const providerConfig = useDataqueueConfig();\n\n // Resolve fetcher: hook option > provider > missing (will skip polling)\n const fetcher: JobFetcher | undefined =\n options.fetcher ?? providerConfig?.fetcher;\n\n // Resolve polling interval\n const pollingInterval =\n options.pollingInterval ??\n providerConfig?.pollingInterval ??\n DEFAULT_POLLING_INTERVAL;\n\n const enabled = options.enabled !== false;\n\n const [data, setData] = useState<JobData | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [isLoading, setIsLoading] = useState<boolean>(jobId != null && enabled);\n\n // Track previous status for onStatusChange callback\n const prevStatusRef = useRef<JobStatus | null>(null);\n\n // Track whether a fetch is already in-flight to avoid overlapping requests\n const inFlightRef = useRef(false);\n\n // Store the latest callbacks in refs so the polling effect doesn't\n // need to re-subscribe when callbacks change identity.\n const onStatusChangeRef = useRef(options.onStatusChange);\n onStatusChangeRef.current = options.onStatusChange;\n const onCompleteRef = useRef(options.onComplete);\n onCompleteRef.current = options.onComplete;\n const onFailedRef = useRef(options.onFailed);\n onFailedRef.current = options.onFailed;\n\n // Whether we've reached a terminal state and should stop polling\n const terminalRef = useRef(false);\n\n const fetchJob = useCallback(async () => {\n if (!fetcher || jobId == null || inFlightRef.current) return;\n\n inFlightRef.current = true;\n try {\n const result = await fetcher(jobId);\n setData(result);\n setError(null);\n setIsLoading(false);\n\n // Status change detection\n const newStatus = result.status;\n const prevStatus = prevStatusRef.current;\n\n if (prevStatus !== newStatus) {\n prevStatusRef.current = newStatus;\n onStatusChangeRef.current?.(newStatus, prevStatus);\n\n if (newStatus === 'completed') {\n onCompleteRef.current?.(result);\n } else if (newStatus === 'failed') {\n onFailedRef.current?.(result);\n }\n }\n\n // Stop polling on terminal status\n if (TERMINAL_STATUSES.has(newStatus)) {\n terminalRef.current = true;\n }\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsLoading(false);\n } finally {\n inFlightRef.current = false;\n }\n }, [fetcher, jobId]);\n\n // Reset state when jobId changes\n useEffect(() => {\n setData(null);\n setError(null);\n prevStatusRef.current = null;\n terminalRef.current = false;\n inFlightRef.current = false;\n setIsLoading(jobId != null && enabled);\n }, [jobId, enabled]);\n\n // Main polling effect\n useEffect(() => {\n if (!fetcher || jobId == null || !enabled) return;\n\n // Initial fetch immediately\n fetchJob();\n\n const id = setInterval(() => {\n if (!terminalRef.current) {\n fetchJob();\n }\n }, pollingInterval);\n\n return () => clearInterval(id);\n }, [fetchJob, pollingInterval, jobId, enabled, fetcher]);\n\n return {\n data,\n status: data?.status ?? null,\n progress: data?.progress ?? null,\n isLoading,\n error,\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Job status values matching the core dataqueue package.
|
|
5
|
+
* Redefined here so the React SDK has zero runtime dependency on the server package.
|
|
6
|
+
*/
|
|
7
|
+
type JobStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'waiting';
|
|
8
|
+
/**
|
|
9
|
+
* Minimal job data shape returned by the fetcher.
|
|
10
|
+
* Users can return a full JobRecord from the server or any object
|
|
11
|
+
* that includes at least `status` and optionally `progress`.
|
|
12
|
+
*/
|
|
13
|
+
interface JobData {
|
|
14
|
+
id: number;
|
|
15
|
+
status: JobStatus;
|
|
16
|
+
progress?: number | null;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A fetcher function that retrieves a job by its ID.
|
|
21
|
+
* Should return a `JobData`-compatible object or throw on error.
|
|
22
|
+
*/
|
|
23
|
+
type JobFetcher = (jobId: number) => Promise<JobData>;
|
|
24
|
+
/**
|
|
25
|
+
* Configuration provided to `DataqueueProvider`.
|
|
26
|
+
*/
|
|
27
|
+
interface DataqueueConfig {
|
|
28
|
+
/** Fetcher function to retrieve a job by ID from your API. */
|
|
29
|
+
fetcher: JobFetcher;
|
|
30
|
+
/** Default polling interval in milliseconds. Defaults to 1000. */
|
|
31
|
+
pollingInterval?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Options for the `useJob` hook.
|
|
35
|
+
*/
|
|
36
|
+
interface UseJobOptions {
|
|
37
|
+
/** Override the provider's fetcher for this specific hook instance. */
|
|
38
|
+
fetcher?: JobFetcher;
|
|
39
|
+
/** Override the provider's polling interval (ms) for this hook instance. */
|
|
40
|
+
pollingInterval?: number;
|
|
41
|
+
/** Whether polling is enabled. Defaults to true. Set to false to pause. */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
/** Called when the job's status changes. */
|
|
44
|
+
onStatusChange?: (newStatus: JobStatus, prevStatus: JobStatus | null) => void;
|
|
45
|
+
/** Called when the job reaches 'completed' status. */
|
|
46
|
+
onComplete?: (data: JobData) => void;
|
|
47
|
+
/** Called when the job reaches 'failed' status. */
|
|
48
|
+
onFailed?: (data: JobData) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Return value of the `useJob` hook.
|
|
52
|
+
*/
|
|
53
|
+
interface UseJobReturn {
|
|
54
|
+
/** The full job data from the last successful fetch, or null if not yet loaded. */
|
|
55
|
+
data: JobData | null;
|
|
56
|
+
/** The current job status, or null if not yet loaded. */
|
|
57
|
+
status: JobStatus | null;
|
|
58
|
+
/** The current progress percentage (0-100), or null if not reported. */
|
|
59
|
+
progress: number | null;
|
|
60
|
+
/** True during the initial fetch (before any data is available). */
|
|
61
|
+
isLoading: boolean;
|
|
62
|
+
/** The error from the last failed fetch, or null. */
|
|
63
|
+
error: Error | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Provides default configuration (fetcher, pollingInterval) to all
|
|
68
|
+
* `useJob` hooks in the subtree. Optional -- hooks can also accept
|
|
69
|
+
* inline config via their options parameter.
|
|
70
|
+
*/
|
|
71
|
+
declare function DataqueueProvider({ children, ...config }: DataqueueConfig & {
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
}): react_jsx_runtime.JSX.Element;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Subscribe to a job's status and progress via polling.
|
|
77
|
+
*
|
|
78
|
+
* @param jobId - The numeric job ID to subscribe to, or `null`/`undefined` to skip polling.
|
|
79
|
+
* @param options - Optional overrides and callbacks.
|
|
80
|
+
* @returns An object with `data`, `status`, `progress`, `isLoading`, and `error`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* const { status, progress, data, error } = useJob(jobId, {
|
|
85
|
+
* fetcher: (id) => fetch(`/api/jobs/${id}`).then(r => r.json()).then(d => d.job),
|
|
86
|
+
* pollingInterval: 1000,
|
|
87
|
+
* onComplete: (job) => console.log('Done!', job),
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function useJob(jobId: number | null | undefined, options?: UseJobOptions): UseJobReturn;
|
|
92
|
+
|
|
93
|
+
export { type DataqueueConfig, DataqueueProvider, type JobData, type JobFetcher, type JobStatus, type UseJobOptions, type UseJobReturn, useJob };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Job status values matching the core dataqueue package.
|
|
5
|
+
* Redefined here so the React SDK has zero runtime dependency on the server package.
|
|
6
|
+
*/
|
|
7
|
+
type JobStatus = 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'waiting';
|
|
8
|
+
/**
|
|
9
|
+
* Minimal job data shape returned by the fetcher.
|
|
10
|
+
* Users can return a full JobRecord from the server or any object
|
|
11
|
+
* that includes at least `status` and optionally `progress`.
|
|
12
|
+
*/
|
|
13
|
+
interface JobData {
|
|
14
|
+
id: number;
|
|
15
|
+
status: JobStatus;
|
|
16
|
+
progress?: number | null;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A fetcher function that retrieves a job by its ID.
|
|
21
|
+
* Should return a `JobData`-compatible object or throw on error.
|
|
22
|
+
*/
|
|
23
|
+
type JobFetcher = (jobId: number) => Promise<JobData>;
|
|
24
|
+
/**
|
|
25
|
+
* Configuration provided to `DataqueueProvider`.
|
|
26
|
+
*/
|
|
27
|
+
interface DataqueueConfig {
|
|
28
|
+
/** Fetcher function to retrieve a job by ID from your API. */
|
|
29
|
+
fetcher: JobFetcher;
|
|
30
|
+
/** Default polling interval in milliseconds. Defaults to 1000. */
|
|
31
|
+
pollingInterval?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Options for the `useJob` hook.
|
|
35
|
+
*/
|
|
36
|
+
interface UseJobOptions {
|
|
37
|
+
/** Override the provider's fetcher for this specific hook instance. */
|
|
38
|
+
fetcher?: JobFetcher;
|
|
39
|
+
/** Override the provider's polling interval (ms) for this hook instance. */
|
|
40
|
+
pollingInterval?: number;
|
|
41
|
+
/** Whether polling is enabled. Defaults to true. Set to false to pause. */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
/** Called when the job's status changes. */
|
|
44
|
+
onStatusChange?: (newStatus: JobStatus, prevStatus: JobStatus | null) => void;
|
|
45
|
+
/** Called when the job reaches 'completed' status. */
|
|
46
|
+
onComplete?: (data: JobData) => void;
|
|
47
|
+
/** Called when the job reaches 'failed' status. */
|
|
48
|
+
onFailed?: (data: JobData) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Return value of the `useJob` hook.
|
|
52
|
+
*/
|
|
53
|
+
interface UseJobReturn {
|
|
54
|
+
/** The full job data from the last successful fetch, or null if not yet loaded. */
|
|
55
|
+
data: JobData | null;
|
|
56
|
+
/** The current job status, or null if not yet loaded. */
|
|
57
|
+
status: JobStatus | null;
|
|
58
|
+
/** The current progress percentage (0-100), or null if not reported. */
|
|
59
|
+
progress: number | null;
|
|
60
|
+
/** True during the initial fetch (before any data is available). */
|
|
61
|
+
isLoading: boolean;
|
|
62
|
+
/** The error from the last failed fetch, or null. */
|
|
63
|
+
error: Error | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Provides default configuration (fetcher, pollingInterval) to all
|
|
68
|
+
* `useJob` hooks in the subtree. Optional -- hooks can also accept
|
|
69
|
+
* inline config via their options parameter.
|
|
70
|
+
*/
|
|
71
|
+
declare function DataqueueProvider({ children, ...config }: DataqueueConfig & {
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
}): react_jsx_runtime.JSX.Element;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Subscribe to a job's status and progress via polling.
|
|
77
|
+
*
|
|
78
|
+
* @param jobId - The numeric job ID to subscribe to, or `null`/`undefined` to skip polling.
|
|
79
|
+
* @param options - Optional overrides and callbacks.
|
|
80
|
+
* @returns An object with `data`, `status`, `progress`, `isLoading`, and `error`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```tsx
|
|
84
|
+
* const { status, progress, data, error } = useJob(jobId, {
|
|
85
|
+
* fetcher: (id) => fetch(`/api/jobs/${id}`).then(r => r.json()).then(d => d.job),
|
|
86
|
+
* pollingInterval: 1000,
|
|
87
|
+
* onComplete: (job) => console.log('Done!', job),
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function useJob(jobId: number | null | undefined, options?: UseJobOptions): UseJobReturn;
|
|
92
|
+
|
|
93
|
+
export { type DataqueueConfig, DataqueueProvider, type JobData, type JobFetcher, type JobStatus, type UseJobOptions, type UseJobReturn, useJob };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createContext, useState, useRef, useCallback, useEffect, useContext } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/context.tsx
|
|
5
|
+
var DataqueueContext = createContext(null);
|
|
6
|
+
function DataqueueProvider({
|
|
7
|
+
children,
|
|
8
|
+
...config
|
|
9
|
+
}) {
|
|
10
|
+
return /* @__PURE__ */ jsx(DataqueueContext.Provider, { value: config, children });
|
|
11
|
+
}
|
|
12
|
+
function useDataqueueConfig() {
|
|
13
|
+
return useContext(DataqueueContext);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
18
|
+
"completed",
|
|
19
|
+
"failed",
|
|
20
|
+
"cancelled"
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// src/use-job.ts
|
|
24
|
+
var DEFAULT_POLLING_INTERVAL = 1e3;
|
|
25
|
+
function useJob(jobId, options = {}) {
|
|
26
|
+
const providerConfig = useDataqueueConfig();
|
|
27
|
+
const fetcher = options.fetcher ?? providerConfig?.fetcher;
|
|
28
|
+
const pollingInterval = options.pollingInterval ?? providerConfig?.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
29
|
+
const enabled = options.enabled !== false;
|
|
30
|
+
const [data, setData] = useState(null);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const [isLoading, setIsLoading] = useState(jobId != null && enabled);
|
|
33
|
+
const prevStatusRef = useRef(null);
|
|
34
|
+
const inFlightRef = useRef(false);
|
|
35
|
+
const onStatusChangeRef = useRef(options.onStatusChange);
|
|
36
|
+
onStatusChangeRef.current = options.onStatusChange;
|
|
37
|
+
const onCompleteRef = useRef(options.onComplete);
|
|
38
|
+
onCompleteRef.current = options.onComplete;
|
|
39
|
+
const onFailedRef = useRef(options.onFailed);
|
|
40
|
+
onFailedRef.current = options.onFailed;
|
|
41
|
+
const terminalRef = useRef(false);
|
|
42
|
+
const fetchJob = useCallback(async () => {
|
|
43
|
+
if (!fetcher || jobId == null || inFlightRef.current) return;
|
|
44
|
+
inFlightRef.current = true;
|
|
45
|
+
try {
|
|
46
|
+
const result = await fetcher(jobId);
|
|
47
|
+
setData(result);
|
|
48
|
+
setError(null);
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
const newStatus = result.status;
|
|
51
|
+
const prevStatus = prevStatusRef.current;
|
|
52
|
+
if (prevStatus !== newStatus) {
|
|
53
|
+
prevStatusRef.current = newStatus;
|
|
54
|
+
onStatusChangeRef.current?.(newStatus, prevStatus);
|
|
55
|
+
if (newStatus === "completed") {
|
|
56
|
+
onCompleteRef.current?.(result);
|
|
57
|
+
} else if (newStatus === "failed") {
|
|
58
|
+
onFailedRef.current?.(result);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (TERMINAL_STATUSES.has(newStatus)) {
|
|
62
|
+
terminalRef.current = true;
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
} finally {
|
|
68
|
+
inFlightRef.current = false;
|
|
69
|
+
}
|
|
70
|
+
}, [fetcher, jobId]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
setData(null);
|
|
73
|
+
setError(null);
|
|
74
|
+
prevStatusRef.current = null;
|
|
75
|
+
terminalRef.current = false;
|
|
76
|
+
inFlightRef.current = false;
|
|
77
|
+
setIsLoading(jobId != null && enabled);
|
|
78
|
+
}, [jobId, enabled]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!fetcher || jobId == null || !enabled) return;
|
|
81
|
+
fetchJob();
|
|
82
|
+
const id = setInterval(() => {
|
|
83
|
+
if (!terminalRef.current) {
|
|
84
|
+
fetchJob();
|
|
85
|
+
}
|
|
86
|
+
}, pollingInterval);
|
|
87
|
+
return () => clearInterval(id);
|
|
88
|
+
}, [fetchJob, pollingInterval, jobId, enabled, fetcher]);
|
|
89
|
+
return {
|
|
90
|
+
data,
|
|
91
|
+
status: data?.status ?? null,
|
|
92
|
+
progress: data?.progress ?? null,
|
|
93
|
+
isLoading,
|
|
94
|
+
error
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { DataqueueProvider, useJob };
|
|
99
|
+
//# sourceMappingURL=index.js.map
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/types.ts","../src/use-job.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\nimport { createContext, useContext } from 'react';\nimport type { DataqueueConfig } from './types.js';\n\nconst DataqueueContext = createContext<DataqueueConfig | null>(null);\n\n/**\n * Provides default configuration (fetcher, pollingInterval) to all\n * `useJob` hooks in the subtree. Optional -- hooks can also accept\n * inline config via their options parameter.\n */\nexport function DataqueueProvider({\n children,\n ...config\n}: DataqueueConfig & { children: React.ReactNode }) {\n return (\n <DataqueueContext.Provider value={config}>\n {children}\n </DataqueueContext.Provider>\n );\n}\n\n/**\n * Internal hook to read the provider config. Returns null when no provider\n * is present (hooks fall back to their own options).\n */\nexport function useDataqueueConfig(): DataqueueConfig | null {\n return useContext(DataqueueContext);\n}\n","/**\n * Job status values matching the core dataqueue package.\n * Redefined here so the React SDK has zero runtime dependency on the server package.\n */\nexport type JobStatus =\n | 'pending'\n | 'processing'\n | 'completed'\n | 'failed'\n | 'cancelled'\n | 'waiting';\n\n/** Terminal statuses where polling should stop automatically. */\nexport const TERMINAL_STATUSES: ReadonlySet<JobStatus> = new Set([\n 'completed',\n 'failed',\n 'cancelled',\n]);\n\n/**\n * Minimal job data shape returned by the fetcher.\n * Users can return a full JobRecord from the server or any object\n * that includes at least `status` and optionally `progress`.\n */\nexport interface JobData {\n id: number;\n status: JobStatus;\n progress?: number | null;\n [key: string]: unknown;\n}\n\n/**\n * A fetcher function that retrieves a job by its ID.\n * Should return a `JobData`-compatible object or throw on error.\n */\nexport type JobFetcher = (jobId: number) => Promise<JobData>;\n\n/**\n * Configuration provided to `DataqueueProvider`.\n */\nexport interface DataqueueConfig {\n /** Fetcher function to retrieve a job by ID from your API. */\n fetcher: JobFetcher;\n /** Default polling interval in milliseconds. Defaults to 1000. */\n pollingInterval?: number;\n}\n\n/**\n * Options for the `useJob` hook.\n */\nexport interface UseJobOptions {\n /** Override the provider's fetcher for this specific hook instance. */\n fetcher?: JobFetcher;\n /** Override the provider's polling interval (ms) for this hook instance. */\n pollingInterval?: number;\n /** Whether polling is enabled. Defaults to true. Set to false to pause. */\n enabled?: boolean;\n /** Called when the job's status changes. */\n onStatusChange?: (newStatus: JobStatus, prevStatus: JobStatus | null) => void;\n /** Called when the job reaches 'completed' status. */\n onComplete?: (data: JobData) => void;\n /** Called when the job reaches 'failed' status. */\n onFailed?: (data: JobData) => void;\n}\n\n/**\n * Return value of the `useJob` hook.\n */\nexport interface UseJobReturn {\n /** The full job data from the last successful fetch, or null if not yet loaded. */\n data: JobData | null;\n /** The current job status, or null if not yet loaded. */\n status: JobStatus | null;\n /** The current progress percentage (0-100), or null if not reported. */\n progress: number | null;\n /** True during the initial fetch (before any data is available). */\n isLoading: boolean;\n /** The error from the last failed fetch, or null. */\n error: Error | null;\n}\n","'use client';\n\nimport { useState, useEffect, useRef, useCallback } from 'react';\nimport { useDataqueueConfig } from './context.js';\nimport type {\n JobData,\n JobFetcher,\n JobStatus,\n UseJobOptions,\n UseJobReturn,\n} from './types.js';\nimport { TERMINAL_STATUSES } from './types.js';\n\nconst DEFAULT_POLLING_INTERVAL = 1000;\n\n/**\n * Subscribe to a job's status and progress via polling.\n *\n * @param jobId - The numeric job ID to subscribe to, or `null`/`undefined` to skip polling.\n * @param options - Optional overrides and callbacks.\n * @returns An object with `data`, `status`, `progress`, `isLoading`, and `error`.\n *\n * @example\n * ```tsx\n * const { status, progress, data, error } = useJob(jobId, {\n * fetcher: (id) => fetch(`/api/jobs/${id}`).then(r => r.json()).then(d => d.job),\n * pollingInterval: 1000,\n * onComplete: (job) => console.log('Done!', job),\n * });\n * ```\n */\nexport function useJob(\n jobId: number | null | undefined,\n options: UseJobOptions = {},\n): UseJobReturn {\n const providerConfig = useDataqueueConfig();\n\n // Resolve fetcher: hook option > provider > missing (will skip polling)\n const fetcher: JobFetcher | undefined =\n options.fetcher ?? providerConfig?.fetcher;\n\n // Resolve polling interval\n const pollingInterval =\n options.pollingInterval ??\n providerConfig?.pollingInterval ??\n DEFAULT_POLLING_INTERVAL;\n\n const enabled = options.enabled !== false;\n\n const [data, setData] = useState<JobData | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [isLoading, setIsLoading] = useState<boolean>(jobId != null && enabled);\n\n // Track previous status for onStatusChange callback\n const prevStatusRef = useRef<JobStatus | null>(null);\n\n // Track whether a fetch is already in-flight to avoid overlapping requests\n const inFlightRef = useRef(false);\n\n // Store the latest callbacks in refs so the polling effect doesn't\n // need to re-subscribe when callbacks change identity.\n const onStatusChangeRef = useRef(options.onStatusChange);\n onStatusChangeRef.current = options.onStatusChange;\n const onCompleteRef = useRef(options.onComplete);\n onCompleteRef.current = options.onComplete;\n const onFailedRef = useRef(options.onFailed);\n onFailedRef.current = options.onFailed;\n\n // Whether we've reached a terminal state and should stop polling\n const terminalRef = useRef(false);\n\n const fetchJob = useCallback(async () => {\n if (!fetcher || jobId == null || inFlightRef.current) return;\n\n inFlightRef.current = true;\n try {\n const result = await fetcher(jobId);\n setData(result);\n setError(null);\n setIsLoading(false);\n\n // Status change detection\n const newStatus = result.status;\n const prevStatus = prevStatusRef.current;\n\n if (prevStatus !== newStatus) {\n prevStatusRef.current = newStatus;\n onStatusChangeRef.current?.(newStatus, prevStatus);\n\n if (newStatus === 'completed') {\n onCompleteRef.current?.(result);\n } else if (newStatus === 'failed') {\n onFailedRef.current?.(result);\n }\n }\n\n // Stop polling on terminal status\n if (TERMINAL_STATUSES.has(newStatus)) {\n terminalRef.current = true;\n }\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n setIsLoading(false);\n } finally {\n inFlightRef.current = false;\n }\n }, [fetcher, jobId]);\n\n // Reset state when jobId changes\n useEffect(() => {\n setData(null);\n setError(null);\n prevStatusRef.current = null;\n terminalRef.current = false;\n inFlightRef.current = false;\n setIsLoading(jobId != null && enabled);\n }, [jobId, enabled]);\n\n // Main polling effect\n useEffect(() => {\n if (!fetcher || jobId == null || !enabled) return;\n\n // Initial fetch immediately\n fetchJob();\n\n const id = setInterval(() => {\n if (!terminalRef.current) {\n fetchJob();\n }\n }, pollingInterval);\n\n return () => clearInterval(id);\n }, [fetchJob, pollingInterval, jobId, enabled, fetcher]);\n\n return {\n data,\n status: data?.status ?? null,\n progress: data?.progress ?? null,\n isLoading,\n error,\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nicnocquee/dataqueue-react",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React hooks for subscribing to dataqueue job status and progress",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/index.d.cts",
|
|
15
|
+
"default": "./dist/index.cjs"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"dev": "tsup --watch",
|
|
26
|
+
"lint": "tsc",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"check-exports": "attw --pack .",
|
|
29
|
+
"ci": "npm run build && npm run check-exports && npm run lint && npm run test",
|
|
30
|
+
"local-release": "changeset version && changeset publish",
|
|
31
|
+
"changeset:add": "changeset",
|
|
32
|
+
"changeset:version": "changeset version && find .changeset -type f -name '*.md' ! -name 'README.md' -delete"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"react",
|
|
36
|
+
"hooks",
|
|
37
|
+
"job-queue",
|
|
38
|
+
"dataqueue",
|
|
39
|
+
"polling",
|
|
40
|
+
"progress"
|
|
41
|
+
],
|
|
42
|
+
"author": "Nico Prananta",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/nicnocquee/dataqueue.git",
|
|
47
|
+
"directory": "packages/react"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/nicnocquee/dataqueue/tree/main/packages/react#readme",
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
55
|
+
"@changesets/cli": "^2.29.5",
|
|
56
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
57
|
+
"@testing-library/react": "^16.3.2",
|
|
58
|
+
"@types/react": "^19.0.0",
|
|
59
|
+
"jsdom": "^28.1.0",
|
|
60
|
+
"react": "^19.0.0",
|
|
61
|
+
"tsup": "^8.5.0",
|
|
62
|
+
"typescript": "^5.8.3",
|
|
63
|
+
"vitest": "^3.2.4"
|
|
64
|
+
}
|
|
65
|
+
}
|