@svadmin/core 0.50.0 → 0.53.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/package.json +21 -4
- package/src/access-control-contract.ts +250 -0
- package/src/adapters/casbin.ts +20 -14
- package/src/adapters/casl.ts +22 -19
- package/src/admin-tool.ts +7 -7
- package/src/audit-contract.ts +105 -0
- package/src/audit.ts +88 -128
- package/src/auth-hooks.svelte.ts +593 -261
- package/src/auth-mutation-contract.ts +143 -0
- package/src/auth-query-contract.ts +97 -0
- package/src/captured-mutation.svelte.ts +45 -0
- package/src/chatProvider.svelte.ts +1 -1
- package/src/command-contract.ts +195 -0
- package/src/context.svelte.ts +217 -205
- package/src/contract-context.svelte.ts +37 -0
- package/src/contract-form.svelte.ts +566 -0
- package/src/create-contract.ts +56 -0
- package/src/create-hooks.svelte.ts +347 -0
- package/src/create-many-contract.ts +90 -0
- package/src/create-many-hooks.svelte.ts +396 -0
- package/src/data-transfer.svelte.ts +1 -215
- package/src/defined-options.ts +50 -0
- package/src/delete-contract.ts +50 -0
- package/src/delete-hooks.svelte.ts +403 -0
- package/src/delete-many-contract.ts +60 -0
- package/src/delete-many-hooks.svelte.ts +402 -0
- package/src/enterprise.ts +4 -4
- package/src/export-format.ts +16 -14
- package/src/export-hooks.svelte.ts +191 -0
- package/src/feature-gate-contract.ts +53 -0
- package/src/hook-utils.svelte.ts +53 -105
- package/src/hooks.svelte.ts +19 -776
- package/src/http.ts +88 -87
- package/src/i18n.svelte.ts +64 -2
- package/src/import-contract.ts +106 -0
- package/src/import-hooks.svelte.ts +310 -0
- package/src/index.ts +77 -30
- package/src/inferencer-graphql.ts +7 -6
- package/src/inferencer.ts +9 -0
- package/src/infinite-query.svelte.ts +228 -0
- package/src/invalidation-contract.ts +26 -0
- package/src/live-sse.ts +91 -127
- package/src/live-subscription.svelte.ts +130 -0
- package/src/live-transport.ts +127 -0
- package/src/live-websocket.ts +85 -118
- package/src/live.svelte.ts +66 -68
- package/src/notification.svelte.ts +17 -15
- package/src/permission-catalog.ts +121 -0
- package/src/permission-hints-contract.ts +48 -0
- package/src/permissions.svelte.ts +112 -59
- package/src/plain-data.ts +61 -0
- package/src/provider-bundle.ts +85 -79
- package/src/provider-response.ts +35 -0
- package/src/query-hooks.svelte.ts +241 -288
- package/src/query-invalidation.ts +172 -0
- package/src/query-keys.ts +221 -110
- package/src/query-session.svelte.ts +220 -0
- package/src/query-snapshot.ts +144 -0
- package/src/reactive-projection.ts +53 -0
- package/src/record-decoder.ts +63 -0
- package/src/resource-binding.svelte.ts +19 -0
- package/src/resource-contract.ts +595 -0
- package/src/resource-mutation-types.ts +30 -0
- package/src/resource-schemas.ts +371 -0
- package/src/resource-transport-config.ts +156 -0
- package/src/route-parsing.ts +11 -11
- package/src/router-provider.ts +64 -59
- package/src/router.ts +8 -3
- package/src/routing-hooks.svelte.ts +90 -62
- package/src/schema-validation.ts +92 -0
- package/src/schema.ts +13 -0
- package/src/select-options.ts +95 -0
- package/src/select-query.svelte.ts +191 -0
- package/src/session-query.svelte.ts +103 -0
- package/src/strict-command-hooks.svelte.ts +333 -0
- package/src/strict-hooks.svelte.ts +198 -0
- package/src/table-hooks.svelte.ts +180 -174
- package/src/table-state.svelte.ts +117 -117
- package/src/task-contract.ts +87 -0
- package/src/task-hooks.svelte.ts +190 -180
- package/src/task-provider.ts +115 -0
- package/src/task-subscription.ts +132 -0
- package/src/theme.svelte.ts +155 -154
- package/src/toast.svelte.ts +38 -37
- package/src/types.ts +68 -110
- package/src/unsafe.ts +7 -0
- package/src/update-contract.ts +65 -0
- package/src/update-hooks.svelte.ts +347 -0
- package/src/update-many-contract.ts +79 -0
- package/src/update-many-hooks.svelte.ts +373 -0
- package/src/url-sync.ts +11 -10
- package/src/useCan.ts +96 -31
- package/src/useParsed.svelte.ts +18 -14
- package/src/useStepsForm.svelte.ts +123 -63
- package/src/utility-hooks.svelte.ts +101 -158
- package/src/form-hooks.svelte.ts +0 -889
- package/src/mutation-hooks.svelte.ts +0 -561
- package/src/schema-form.ts +0 -31
package/src/task-hooks.svelte.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { definedOptions } from './defined-options';
|
|
2
|
+
import { createMutation,useQueryClient } from '@tanstack/svelte-query';
|
|
3
|
+
import { createSessionQuery } from './session-query.svelte';
|
|
4
|
+
import { supersededQuerySession } from './query-session.svelte';
|
|
2
5
|
import { getAdminOptions } from './options.svelte';
|
|
3
6
|
import { captureAdminContext } from './context.svelte';
|
|
4
7
|
import type { AdminContextAccessor } from './context.svelte';
|
|
5
8
|
import {
|
|
6
9
|
resolveTenantProviderMeta,
|
|
7
10
|
} from './provider-bundle';
|
|
8
|
-
import { queryKeyMatches } from './query-keys';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
+
import { queryKeyMatches, type QueryKey } from './query-keys';
|
|
12
|
+
import { replaceReactiveMembers } from './reactive-projection';
|
|
13
|
+
import { taskQueryParams, withValidatedTaskProvider } from './task-provider';
|
|
14
|
+
import { TaskError, decodeTaskRecord, decodeTaskList } from './task-contract';
|
|
15
|
+
import { checkError,createOvertimeTracker,fireErrorNotification,fireSuccessNotification } from './hook-utils.svelte';
|
|
16
|
+
import type { NotificationConfig,OvertimeOptions } from './hook-utils.svelte';
|
|
11
17
|
import { useTranslation } from './i18n.svelte';
|
|
12
18
|
import type {
|
|
13
|
-
HttpError,
|
|
14
19
|
SubmitTaskOptions,
|
|
15
20
|
TaskHandle,
|
|
16
21
|
TaskListResult,
|
|
@@ -18,100 +23,141 @@ import type {
|
|
|
18
23
|
TaskRecord,
|
|
19
24
|
} from './types';
|
|
20
25
|
|
|
21
|
-
function normalizeTaskProvider
|
|
22
|
-
provider?: TaskProvider
|
|
23
|
-
adminContext: AdminContextAccessor
|
|
24
|
-
): TaskProvider
|
|
25
|
-
const activeProvider
|
|
26
|
-
if
|
|
26
|
+
function normalizeTaskProvider(
|
|
27
|
+
provider?: TaskProvider,
|
|
28
|
+
adminContext: AdminContextAccessor=captureAdminContext(),
|
|
29
|
+
): TaskProvider {
|
|
30
|
+
const activeProvider=provider??adminContext.taskProvider;
|
|
31
|
+
if(!activeProvider) {
|
|
27
32
|
throw new Error('TaskProvider not found. Did you call setTaskProvider in App.svelte?');
|
|
28
33
|
}
|
|
29
|
-
return activeProvider
|
|
34
|
+
return withValidatedTaskProvider(activeProvider);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const providerKeys = new WeakMap<TaskProvider, number>();
|
|
38
|
+
let nextProviderKey = 0;
|
|
39
|
+
function taskProviderKey(provider: TaskProvider | undefined): number {
|
|
40
|
+
if (!provider) return 0;
|
|
41
|
+
let providerKey = providerKeys.get(provider);
|
|
42
|
+
if (providerKey === undefined) {
|
|
43
|
+
providerKey = ++nextProviderKey;
|
|
44
|
+
providerKeys.set(provider, providerKey);
|
|
45
|
+
}
|
|
46
|
+
return providerKey;
|
|
47
|
+
}
|
|
48
|
+
function scopedTaskKey(key: QueryKey, provider: TaskProvider | undefined): QueryKey {
|
|
49
|
+
const providerKey = taskProviderKey(provider);
|
|
50
|
+
return [{ ...key[0], params: definedOptions({ taskProvider: providerKey, query: key[0].params }) }];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function taskReadError(error: unknown): Error {
|
|
54
|
+
try {
|
|
55
|
+
if (typeof error === 'object' && error !== null) {
|
|
56
|
+
const code: unknown = Object.getOwnPropertyDescriptor(error, 'code')?.value;
|
|
57
|
+
if (code === 'QUERY_SESSION_SUPERSEDED') return supersededQuerySession();
|
|
58
|
+
if (code === 'INVALID_TASK_INPUT' || code === 'INVALID_TASK_RESPONSE' || code === 'TASK_PROVIDER_FAILED') {
|
|
59
|
+
return new TaskError(code);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch { /* 不执行错误对象中的 getter,也不回显上游诊断。 */ }
|
|
63
|
+
return new TaskError('TASK_PROVIDER_FAILED');
|
|
30
64
|
}
|
|
31
65
|
|
|
32
66
|
export interface UseSubmitTaskOptions {
|
|
33
67
|
mutationOptions?: {
|
|
34
|
-
onSuccess?: (data: unknown,
|
|
35
|
-
onError?: (error: unknown,
|
|
68
|
+
onSuccess?: (data: unknown,variables: unknown,context: unknown) => void;
|
|
69
|
+
onError?: (error: unknown,variables: unknown,context: unknown) => void;
|
|
36
70
|
};
|
|
37
71
|
overtimeOptions?: OvertimeOptions;
|
|
38
72
|
}
|
|
39
73
|
|
|
40
|
-
export interface UseSubmitTaskMutateParams
|
|
74
|
+
export interface UseSubmitTaskMutateParams {
|
|
41
75
|
taskName: string;
|
|
42
76
|
options?: SubmitTaskOptions;
|
|
43
|
-
taskProvider?: TaskProvider
|
|
77
|
+
taskProvider?: TaskProvider;
|
|
44
78
|
successNotification?: NotificationConfig;
|
|
45
79
|
errorNotification?: NotificationConfig;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
|
-
export function useSubmitTask
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const queryClient = useQueryClient();
|
|
56
|
-
const overtime = createOvertimeTracker(() => mutation.isPending, options.overtimeOptions ?? adminOptions.overtime);
|
|
82
|
+
export function useSubmitTask(options: UseSubmitTaskOptions={}) {
|
|
83
|
+
const adminContext=captureAdminContext();
|
|
84
|
+
const i18n=useTranslation();
|
|
85
|
+
const adminOptions=getAdminOptions();
|
|
86
|
+
const queryClient=useQueryClient();
|
|
87
|
+
const overtime=createOvertimeTracker(() => mutation.isPending,options.overtimeOptions??adminOptions.overtime);
|
|
88
|
+
const submissionScopes = new WeakMap<TaskHandle, ReturnType<AdminContextAccessor['queryKeyMatcher']>>();
|
|
57
89
|
|
|
58
|
-
const mutation
|
|
90
|
+
const mutation=createMutation<TaskHandle,Error,UseSubmitTaskMutateParams>(() => ({
|
|
59
91
|
mutationFn: async (params) => {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
92
|
+
const scope = adminContext.queryKeyMatcher();
|
|
93
|
+
const provider=normalizeTaskProvider(params.taskProvider,adminContext);
|
|
94
|
+
const providerMeta=adminContext.tenant
|
|
62
95
|
? {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
:
|
|
67
|
-
const taskOptions
|
|
96
|
+
...(params.options?.meta??{}),
|
|
97
|
+
...resolveTenantProviderMeta(adminContext.tenant,adminContext.tenantAdapter),
|
|
98
|
+
}
|
|
99
|
+
:params.options?.meta;
|
|
100
|
+
const taskOptions=providerMeta===params.options?.meta
|
|
68
101
|
? params.options
|
|
69
|
-
:
|
|
70
|
-
|
|
102
|
+
:definedOptions({
|
|
103
|
+
...params.options,
|
|
104
|
+
meta: providerMeta,
|
|
105
|
+
});
|
|
106
|
+
const handle = await provider.submit(params.taskName,taskOptions);
|
|
107
|
+
submissionScopes.set(handle, scope);
|
|
108
|
+
return handle;
|
|
71
109
|
},
|
|
72
|
-
onSuccess: (data,
|
|
110
|
+
onSuccess: (data,params,context) => {
|
|
73
111
|
fireSuccessNotification({
|
|
74
112
|
config: params.successNotification,
|
|
75
113
|
defaultMessage: i18n.t('task.submitSuccess'),
|
|
76
114
|
data,
|
|
77
115
|
values: params.options,
|
|
78
116
|
resource: params.taskName,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
predicate: (query) => queryKeyMatches(query.queryKey, {
|
|
83
|
-
...adminContext.queryKeyMatcher(),
|
|
84
|
-
kind: 'task',
|
|
85
|
-
action: 'list',
|
|
86
|
-
}),
|
|
117
|
+
...definedOptions({
|
|
118
|
+
provider: adminContext.notificationProvider
|
|
119
|
+
})
|
|
87
120
|
});
|
|
88
|
-
|
|
121
|
+
const scope = submissionScopes.get(data);
|
|
122
|
+
if (scope) {
|
|
123
|
+
submissionScopes.delete(data);
|
|
124
|
+
void queryClient.invalidateQueries({
|
|
125
|
+
predicate: (query) => queryKeyMatches(query.queryKey,{
|
|
126
|
+
...scope,
|
|
127
|
+
kind: 'task',
|
|
128
|
+
action: 'list',
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
options.mutationOptions?.onSuccess?.(data,params,context);
|
|
89
133
|
},
|
|
90
|
-
onError: (error,
|
|
91
|
-
checkError(error,
|
|
134
|
+
onError: (error,params,context) => {
|
|
135
|
+
checkError(error,adminContext);
|
|
92
136
|
fireErrorNotification({
|
|
93
137
|
config: params.errorNotification,
|
|
94
138
|
defaultMessage: i18n.t('task.submitFailed'),
|
|
95
139
|
error,
|
|
96
140
|
resource: params.taskName,
|
|
97
|
-
|
|
141
|
+
...definedOptions({
|
|
142
|
+
provider: adminContext.notificationProvider
|
|
143
|
+
})
|
|
98
144
|
});
|
|
99
|
-
options.mutationOptions?.onError?.(error,
|
|
145
|
+
options.mutationOptions?.onError?.(error,params,context);
|
|
100
146
|
},
|
|
101
147
|
}));
|
|
102
148
|
|
|
103
|
-
return { mutation,
|
|
149
|
+
return { mutation,get overtime() { return overtime; } };
|
|
104
150
|
}
|
|
105
151
|
|
|
106
|
-
export interface UseTaskOptions
|
|
152
|
+
export interface UseTaskOptions {
|
|
107
153
|
taskId?: string;
|
|
108
|
-
taskProvider?: TaskProvider
|
|
154
|
+
taskProvider?: TaskProvider;
|
|
109
155
|
queryOptions?: {
|
|
110
156
|
enabled?: boolean;
|
|
111
157
|
staleTime?: number;
|
|
112
158
|
gcTime?: number;
|
|
113
159
|
refetchOnWindowFocus?: boolean;
|
|
114
|
-
refetchInterval?: number
|
|
160
|
+
refetchInterval?: number|false;
|
|
115
161
|
refetchIntervalInBackground?: boolean;
|
|
116
162
|
};
|
|
117
163
|
successNotification?: NotificationConfig;
|
|
@@ -119,78 +165,58 @@ export interface UseTaskOptions<TTask extends TaskRecord = TaskRecord, _TError =
|
|
|
119
165
|
overtimeOptions?: OvertimeOptions;
|
|
120
166
|
}
|
|
121
167
|
|
|
122
|
-
export function useTask
|
|
123
|
-
options: UseTaskOptions
|
|
168
|
+
export function useTask(
|
|
169
|
+
options: UseTaskOptions={},
|
|
124
170
|
) {
|
|
125
|
-
const adminContext
|
|
126
|
-
const i18n
|
|
127
|
-
const adminOptions
|
|
171
|
+
const adminContext=captureAdminContext();
|
|
172
|
+
const i18n=useTranslation();
|
|
173
|
+
const adminOptions=getAdminOptions();
|
|
128
174
|
|
|
129
|
-
const query
|
|
130
|
-
const
|
|
131
|
-
const
|
|
175
|
+
const query=createSessionQuery<TaskRecord>(adminContext, () => {
|
|
176
|
+
const activeProvider=options.taskProvider??adminContext.taskProvider;
|
|
177
|
+
const provider=activeProvider? withValidatedTaskProvider(activeProvider):undefined;
|
|
178
|
+
const taskId=options.taskId;
|
|
179
|
+
const queryOptions=options.queryOptions;
|
|
132
180
|
return {
|
|
133
|
-
|
|
181
|
+
resource: taskId ?? '',
|
|
182
|
+
successNotification: options.successNotification,
|
|
183
|
+
errorNotification: options.errorNotification ?? i18n.t('task.fetchFailed'),
|
|
184
|
+
normalizeError: taskReadError,
|
|
185
|
+
decode: value => decodeTaskRecord(value, taskId),
|
|
186
|
+
queryKey: scopedTaskKey(adminContext.queryKeys().task.one(taskId??''),provider),
|
|
134
187
|
queryFn: async () => {
|
|
135
|
-
if
|
|
136
|
-
|
|
188
|
+
if(!provider) throw new Error('TaskProvider not found.');
|
|
189
|
+
if(!taskId) throw new Error('useTask requires a taskId');
|
|
190
|
+
return provider.get(taskId);
|
|
137
191
|
},
|
|
138
|
-
enabled: (queryOptions?.enabled
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
enabled: (queryOptions?.enabled??true)&&!!taskId,
|
|
193
|
+
...definedOptions({
|
|
194
|
+
staleTime: queryOptions?.staleTime??adminOptions.reactQuery?.staleTime,
|
|
195
|
+
gcTime: queryOptions?.gcTime??adminOptions.reactQuery?.gcTime,
|
|
196
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus??adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
197
|
+
}),
|
|
198
|
+
...definedOptions({
|
|
199
|
+
refetchInterval: queryOptions?.refetchInterval,
|
|
200
|
+
refetchIntervalInBackground: queryOptions?.refetchIntervalInBackground,
|
|
201
|
+
}),
|
|
144
202
|
};
|
|
145
203
|
});
|
|
146
204
|
|
|
147
|
-
const overtime
|
|
148
|
-
|
|
149
|
-
let lastSuccessAt = 0;
|
|
150
|
-
let lastErrorAt = 0;
|
|
151
|
-
$effect(() => {
|
|
152
|
-
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
153
|
-
lastSuccessAt = query.dataUpdatedAt;
|
|
154
|
-
if (options.successNotification) {
|
|
155
|
-
fireSuccessNotification({
|
|
156
|
-
config: options.successNotification,
|
|
157
|
-
defaultMessage: '',
|
|
158
|
-
data: query.data,
|
|
159
|
-
resource: options.taskId,
|
|
160
|
-
provider: adminContext.notificationProvider,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
164
|
-
lastErrorAt = query.errorUpdatedAt;
|
|
165
|
-
checkError(query.error, adminContext);
|
|
166
|
-
fireErrorNotification({
|
|
167
|
-
config: options.errorNotification,
|
|
168
|
-
defaultMessage: i18n.t('task.fetchFailed'),
|
|
169
|
-
error: query.error,
|
|
170
|
-
resource: options.taskId,
|
|
171
|
-
provider: adminContext.notificationProvider,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
});
|
|
205
|
+
const overtime=createOvertimeTracker(() => query.isLoading,options.overtimeOptions??adminOptions.overtime);
|
|
175
206
|
|
|
176
|
-
return
|
|
177
|
-
get(target, prop) {
|
|
178
|
-
if (prop === 'overtime') return overtime;
|
|
179
|
-
return target[prop as keyof typeof target];
|
|
180
|
-
},
|
|
181
|
-
}) as typeof query & { overtime: typeof overtime };
|
|
207
|
+
return replaceReactiveMembers(query,{ get overtime() { return overtime; } });
|
|
182
208
|
}
|
|
183
209
|
|
|
184
|
-
export interface UseTaskListOptions
|
|
185
|
-
params?: Record<string,
|
|
210
|
+
export interface UseTaskListOptions {
|
|
211
|
+
params?: Record<string,unknown>;
|
|
186
212
|
dlq?: boolean;
|
|
187
|
-
taskProvider?: TaskProvider
|
|
213
|
+
taskProvider?: TaskProvider;
|
|
188
214
|
queryOptions?: {
|
|
189
215
|
enabled?: boolean;
|
|
190
216
|
staleTime?: number;
|
|
191
217
|
gcTime?: number;
|
|
192
218
|
refetchOnWindowFocus?: boolean;
|
|
193
|
-
refetchInterval?: number
|
|
219
|
+
refetchInterval?: number|false;
|
|
194
220
|
refetchIntervalInBackground?: boolean;
|
|
195
221
|
};
|
|
196
222
|
successNotification?: NotificationConfig;
|
|
@@ -198,104 +224,88 @@ export interface UseTaskListOptions<TTask extends TaskRecord = TaskRecord, _TErr
|
|
|
198
224
|
overtimeOptions?: OvertimeOptions;
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
export function useTaskList
|
|
202
|
-
options: UseTaskListOptions
|
|
227
|
+
export function useTaskList(
|
|
228
|
+
options: UseTaskListOptions={},
|
|
203
229
|
) {
|
|
204
|
-
const adminContext
|
|
205
|
-
const i18n
|
|
206
|
-
const adminOptions
|
|
230
|
+
const adminContext=captureAdminContext();
|
|
231
|
+
const i18n=useTranslation();
|
|
232
|
+
const adminOptions=getAdminOptions();
|
|
207
233
|
|
|
208
|
-
const query
|
|
209
|
-
const
|
|
210
|
-
const
|
|
234
|
+
const query=createSessionQuery<TaskListResult>(adminContext, () => {
|
|
235
|
+
const activeProvider=options.taskProvider??adminContext.taskProvider;
|
|
236
|
+
const provider=activeProvider? withValidatedTaskProvider(activeProvider):undefined;
|
|
237
|
+
const params=options.params===undefined? undefined:taskQueryParams(options.params);
|
|
238
|
+
const dlq=options.dlq??false;
|
|
239
|
+
const queryOptions=options.queryOptions;
|
|
240
|
+
const resource = dlq ? 'taskDlq' : 'tasks';
|
|
241
|
+
const successNotification = options.successNotification;
|
|
211
242
|
return {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
243
|
+
resource,
|
|
244
|
+
successNotification: typeof successNotification === 'function'
|
|
245
|
+
? (data: unknown) => successNotification(data, params, resource)
|
|
246
|
+
: successNotification,
|
|
247
|
+
errorNotification: options.errorNotification ?? i18n.t('task.fetchListFailed'),
|
|
248
|
+
normalizeError: taskReadError,
|
|
249
|
+
decode: decodeTaskList,
|
|
250
|
+
queryKey: scopedTaskKey(adminContext.queryKeys().task.list(definedOptions({
|
|
251
|
+
list: dlq? 'dlq':'default',
|
|
252
|
+
params,
|
|
253
|
+
})),provider),
|
|
216
254
|
queryFn: async () => {
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
if(!provider) throw new Error('TaskProvider not found.');
|
|
256
|
+
if(dlq) {
|
|
257
|
+
if(!provider.listDlq) throw new Error('TaskProvider does not implement listDlq');
|
|
258
|
+
return provider.listDlq(params);
|
|
220
259
|
}
|
|
221
|
-
if
|
|
222
|
-
return provider.list(
|
|
260
|
+
if(!provider.list) throw new Error('TaskProvider does not implement list');
|
|
261
|
+
return provider.list(params);
|
|
223
262
|
},
|
|
224
|
-
enabled: queryOptions?.enabled
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
263
|
+
enabled: queryOptions?.enabled??true,
|
|
264
|
+
...definedOptions({
|
|
265
|
+
staleTime: queryOptions?.staleTime??adminOptions.reactQuery?.staleTime,
|
|
266
|
+
gcTime: queryOptions?.gcTime??adminOptions.reactQuery?.gcTime,
|
|
267
|
+
refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus??adminOptions.reactQuery?.refetchOnWindowFocus,
|
|
268
|
+
}),
|
|
269
|
+
...definedOptions({
|
|
270
|
+
refetchInterval: queryOptions?.refetchInterval,
|
|
271
|
+
refetchIntervalInBackground: queryOptions?.refetchIntervalInBackground,
|
|
272
|
+
}),
|
|
230
273
|
};
|
|
231
274
|
});
|
|
232
275
|
|
|
233
|
-
const overtime
|
|
276
|
+
const overtime=createOvertimeTracker(() => query.isLoading,options.overtimeOptions??adminOptions.overtime);
|
|
234
277
|
|
|
235
|
-
|
|
236
|
-
let lastErrorAt = 0;
|
|
237
|
-
$effect(() => {
|
|
238
|
-
if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
|
|
239
|
-
lastSuccessAt = query.dataUpdatedAt;
|
|
240
|
-
if (options.successNotification) {
|
|
241
|
-
fireSuccessNotification({
|
|
242
|
-
config: options.successNotification,
|
|
243
|
-
defaultMessage: '',
|
|
244
|
-
data: query.data,
|
|
245
|
-
values: options.params,
|
|
246
|
-
resource: options.dlq ? 'taskDlq' : 'tasks',
|
|
247
|
-
provider: adminContext.notificationProvider,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
251
|
-
lastErrorAt = query.errorUpdatedAt;
|
|
252
|
-
checkError(query.error, adminContext);
|
|
253
|
-
fireErrorNotification({
|
|
254
|
-
config: options.errorNotification,
|
|
255
|
-
defaultMessage: i18n.t('task.fetchListFailed'),
|
|
256
|
-
error: query.error,
|
|
257
|
-
resource: options.dlq ? 'taskDlq' : 'tasks',
|
|
258
|
-
provider: adminContext.notificationProvider,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
return new Proxy(query, {
|
|
264
|
-
get(target, prop) {
|
|
265
|
-
if (prop === 'overtime') return overtime;
|
|
266
|
-
return target[prop as keyof typeof target];
|
|
267
|
-
},
|
|
268
|
-
}) as typeof query & { overtime: typeof overtime };
|
|
278
|
+
return replaceReactiveMembers(query,{ get overtime() { return overtime; } });
|
|
269
279
|
}
|
|
270
280
|
|
|
271
|
-
export interface UseTaskSubscriptionOptions
|
|
281
|
+
export interface UseTaskSubscriptionOptions {
|
|
272
282
|
taskId: string;
|
|
273
|
-
taskProvider?: TaskProvider
|
|
283
|
+
taskProvider?: TaskProvider;
|
|
274
284
|
enabled?: boolean;
|
|
275
|
-
onTask: (task:
|
|
285
|
+
onTask: (task: TaskRecord) => void;
|
|
286
|
+
onError?: (error: TaskError) => void;
|
|
276
287
|
}
|
|
277
288
|
|
|
278
|
-
export function useTaskSubscription
|
|
279
|
-
options: UseTaskSubscriptionOptions
|
|
289
|
+
export function useTaskSubscription(
|
|
290
|
+
options: UseTaskSubscriptionOptions,
|
|
280
291
|
): void {
|
|
281
|
-
const adminContext
|
|
292
|
+
const adminContext=captureAdminContext();
|
|
282
293
|
$effect(() => {
|
|
283
|
-
const enabled
|
|
284
|
-
if
|
|
294
|
+
const enabled=options.enabled??true;
|
|
295
|
+
if(!enabled) return;
|
|
285
296
|
|
|
286
|
-
const provider
|
|
287
|
-
if
|
|
297
|
+
const provider=normalizeTaskProvider(options.taskProvider,adminContext);
|
|
298
|
+
if(!provider.subscribe) {
|
|
288
299
|
throw new Error('TaskProvider does not implement subscribe');
|
|
289
300
|
}
|
|
290
301
|
|
|
291
|
-
const subscription
|
|
292
|
-
if (!subscription) return;
|
|
302
|
+
const subscription=provider.subscribe(options.taskId,options.onTask,options.onError);
|
|
293
303
|
|
|
294
|
-
if
|
|
304
|
+
if(typeof subscription==='function') {
|
|
295
305
|
return subscription;
|
|
296
306
|
}
|
|
297
307
|
|
|
298
|
-
if
|
|
308
|
+
if(typeof subscription.unsubscribe==='function') {
|
|
299
309
|
return () => subscription.unsubscribe();
|
|
300
310
|
}
|
|
301
311
|
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { checkExact } from './schema-validation';
|
|
3
|
+
import { snapshotPlainData } from './plain-data';
|
|
4
|
+
import { TaskError, decodeTaskList, decodeTaskRecord, decodeTaskSubmitOptions, type SubmitTaskOptions } from './task-contract';
|
|
5
|
+
import { requiredTaskClientMethod, taskClientMethod, taskHandleField, taskHandleMethod, validatedTaskSubscription } from './task-subscription';
|
|
6
|
+
import type { TaskHandle, TaskProvider } from './types';
|
|
7
|
+
|
|
8
|
+
export interface TaskTransport {
|
|
9
|
+
submit(name: string, options?: SubmitTaskOptions): Promise<unknown>;
|
|
10
|
+
get(id: string): Promise<unknown>;
|
|
11
|
+
list?(params?: Record<string, unknown>): Promise<unknown>;
|
|
12
|
+
listDlq?(params?: Record<string, unknown>): Promise<unknown>;
|
|
13
|
+
cancel?(id: string): Promise<unknown>;
|
|
14
|
+
retry?(id: string): Promise<unknown>;
|
|
15
|
+
subscribe?(id: string, callback: (task: unknown) => void, onError?: (error: unknown) => void): unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const providers = new WeakMap<TaskTransport, TaskProvider>();
|
|
19
|
+
const paramsSchema = Type.Record(Type.String(), Type.Unknown());
|
|
20
|
+
|
|
21
|
+
export function taskInputText(value: unknown): string {
|
|
22
|
+
if (typeof value === 'string' && value.trim().length > 0) return value;
|
|
23
|
+
throw new TaskError('INVALID_TASK_INPUT');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function taskQueryParams(value: unknown): Record<string, unknown> {
|
|
27
|
+
try {
|
|
28
|
+
const candidate = snapshotPlainData(value);
|
|
29
|
+
if (checkExact(paramsSchema, candidate)) return candidate;
|
|
30
|
+
} catch {
|
|
31
|
+
// Query inputs must be detached plain data.
|
|
32
|
+
}
|
|
33
|
+
throw new TaskError('INVALID_TASK_INPUT');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function request(invoke: () => unknown, write = false): Promise<unknown> {
|
|
37
|
+
try { return await invoke(); }
|
|
38
|
+
catch { throw new TaskError('TASK_PROVIDER_FAILED', write); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function handle(value: unknown): TaskHandle {
|
|
42
|
+
const rawId = taskHandleField(value, 'id', true);
|
|
43
|
+
if (typeof rawId !== 'string' || !rawId.trim()) throw new TaskError('INVALID_TASK_RESPONSE', true);
|
|
44
|
+
const id = rawId;
|
|
45
|
+
const wait = taskHandleMethod(value, 'wait', true);
|
|
46
|
+
const result: TaskHandle = {
|
|
47
|
+
id,
|
|
48
|
+
async wait() { return decodeTaskRecord(await request(() => wait()), id); },
|
|
49
|
+
};
|
|
50
|
+
if (taskHandleField(value, 'cancel', true) !== undefined) {
|
|
51
|
+
const cancel = taskHandleMethod(value, 'cancel', true);
|
|
52
|
+
result.cancel = async () => decodeTaskRecord(await request(() => cancel(), true), id, true);
|
|
53
|
+
}
|
|
54
|
+
if (taskHandleField(value, 'retry', true) !== undefined) {
|
|
55
|
+
const retry = taskHandleMethod(value, 'retry', true);
|
|
56
|
+
result.retry = async () => decodeTaskRecord(await request(() => retry(), true), id, true);
|
|
57
|
+
}
|
|
58
|
+
if (taskHandleField(value, 'subscribe', true) !== undefined) {
|
|
59
|
+
const subscribe = taskHandleMethod(value, 'subscribe', true);
|
|
60
|
+
result.subscribe = (callback, onError) => validatedTaskSubscription(
|
|
61
|
+
(onUpdate, onFailure) => subscribe(onUpdate, onFailure),
|
|
62
|
+
value => decodeTaskRecord(value, id), callback, onError,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function withValidatedTaskProvider(transport: TaskTransport): TaskProvider {
|
|
69
|
+
const existing = providers.get(transport);
|
|
70
|
+
if (existing) return existing;
|
|
71
|
+
const submit = requiredTaskClientMethod(transport, 'submit');
|
|
72
|
+
const get = requiredTaskClientMethod(transport, 'get');
|
|
73
|
+
const list = taskClientMethod(transport, 'list', true);
|
|
74
|
+
const listDlq = taskClientMethod(transport, 'listDlq', true);
|
|
75
|
+
const cancel = taskClientMethod(transport, 'cancel', true);
|
|
76
|
+
const retry = taskClientMethod(transport, 'retry', true);
|
|
77
|
+
const subscribe = taskClientMethod(transport, 'subscribe', true);
|
|
78
|
+
const provider: TaskProvider = {
|
|
79
|
+
async submit(name, options) {
|
|
80
|
+
const taskName = taskInputText(name);
|
|
81
|
+
const input = options === undefined ? undefined : decodeTaskSubmitOptions(options);
|
|
82
|
+
return handle(await request(() => submit(taskName, input), true));
|
|
83
|
+
},
|
|
84
|
+
async get(taskId) {
|
|
85
|
+
const id = taskInputText(taskId);
|
|
86
|
+
return decodeTaskRecord(await request(() => get(id)), id);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
if (list) provider.list = async params => {
|
|
90
|
+
const input = params === undefined ? undefined : taskQueryParams(params);
|
|
91
|
+
return decodeTaskList(await request(() => list(input)));
|
|
92
|
+
};
|
|
93
|
+
if (listDlq) provider.listDlq = async params => {
|
|
94
|
+
const input = params === undefined ? undefined : taskQueryParams(params);
|
|
95
|
+
return decodeTaskList(await request(() => listDlq(input)));
|
|
96
|
+
};
|
|
97
|
+
if (cancel) provider.cancel = async taskId => {
|
|
98
|
+
const id = taskInputText(taskId);
|
|
99
|
+
return decodeTaskRecord(await request(() => cancel(id), true), id, true);
|
|
100
|
+
};
|
|
101
|
+
if (retry) provider.retry = async taskId => {
|
|
102
|
+
const id = taskInputText(taskId);
|
|
103
|
+
return decodeTaskRecord(await request(() => retry(id), true), id, true);
|
|
104
|
+
};
|
|
105
|
+
if (subscribe) provider.subscribe = (taskId, callback, onError) => {
|
|
106
|
+
const id = taskInputText(taskId);
|
|
107
|
+
return validatedTaskSubscription(
|
|
108
|
+
(onUpdate, onFailure) => subscribe(id, onUpdate, onFailure),
|
|
109
|
+
value => decodeTaskRecord(value, id), callback, onError,
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
providers.set(transport, provider);
|
|
113
|
+
providers.set(provider, provider);
|
|
114
|
+
return provider;
|
|
115
|
+
}
|