@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.
Files changed (98) hide show
  1. package/package.json +21 -4
  2. package/src/access-control-contract.ts +250 -0
  3. package/src/adapters/casbin.ts +20 -14
  4. package/src/adapters/casl.ts +22 -19
  5. package/src/admin-tool.ts +7 -7
  6. package/src/audit-contract.ts +105 -0
  7. package/src/audit.ts +88 -128
  8. package/src/auth-hooks.svelte.ts +593 -261
  9. package/src/auth-mutation-contract.ts +143 -0
  10. package/src/auth-query-contract.ts +97 -0
  11. package/src/captured-mutation.svelte.ts +45 -0
  12. package/src/chatProvider.svelte.ts +1 -1
  13. package/src/command-contract.ts +195 -0
  14. package/src/context.svelte.ts +217 -205
  15. package/src/contract-context.svelte.ts +37 -0
  16. package/src/contract-form.svelte.ts +566 -0
  17. package/src/create-contract.ts +56 -0
  18. package/src/create-hooks.svelte.ts +347 -0
  19. package/src/create-many-contract.ts +90 -0
  20. package/src/create-many-hooks.svelte.ts +396 -0
  21. package/src/data-transfer.svelte.ts +1 -215
  22. package/src/defined-options.ts +50 -0
  23. package/src/delete-contract.ts +50 -0
  24. package/src/delete-hooks.svelte.ts +403 -0
  25. package/src/delete-many-contract.ts +60 -0
  26. package/src/delete-many-hooks.svelte.ts +402 -0
  27. package/src/enterprise.ts +4 -4
  28. package/src/export-format.ts +16 -14
  29. package/src/export-hooks.svelte.ts +191 -0
  30. package/src/feature-gate-contract.ts +53 -0
  31. package/src/hook-utils.svelte.ts +53 -105
  32. package/src/hooks.svelte.ts +19 -776
  33. package/src/http.ts +88 -87
  34. package/src/i18n.svelte.ts +64 -2
  35. package/src/import-contract.ts +106 -0
  36. package/src/import-hooks.svelte.ts +310 -0
  37. package/src/index.ts +77 -30
  38. package/src/inferencer-graphql.ts +7 -6
  39. package/src/inferencer.ts +9 -0
  40. package/src/infinite-query.svelte.ts +228 -0
  41. package/src/invalidation-contract.ts +26 -0
  42. package/src/live-sse.ts +91 -127
  43. package/src/live-subscription.svelte.ts +130 -0
  44. package/src/live-transport.ts +127 -0
  45. package/src/live-websocket.ts +85 -118
  46. package/src/live.svelte.ts +66 -68
  47. package/src/notification.svelte.ts +17 -15
  48. package/src/permission-catalog.ts +121 -0
  49. package/src/permission-hints-contract.ts +48 -0
  50. package/src/permissions.svelte.ts +112 -59
  51. package/src/plain-data.ts +61 -0
  52. package/src/provider-bundle.ts +85 -79
  53. package/src/provider-response.ts +35 -0
  54. package/src/query-hooks.svelte.ts +241 -288
  55. package/src/query-invalidation.ts +172 -0
  56. package/src/query-keys.ts +221 -110
  57. package/src/query-session.svelte.ts +220 -0
  58. package/src/query-snapshot.ts +144 -0
  59. package/src/reactive-projection.ts +53 -0
  60. package/src/record-decoder.ts +63 -0
  61. package/src/resource-binding.svelte.ts +19 -0
  62. package/src/resource-contract.ts +595 -0
  63. package/src/resource-mutation-types.ts +30 -0
  64. package/src/resource-schemas.ts +371 -0
  65. package/src/resource-transport-config.ts +156 -0
  66. package/src/route-parsing.ts +11 -11
  67. package/src/router-provider.ts +64 -59
  68. package/src/router.ts +8 -3
  69. package/src/routing-hooks.svelte.ts +90 -62
  70. package/src/schema-validation.ts +92 -0
  71. package/src/schema.ts +13 -0
  72. package/src/select-options.ts +95 -0
  73. package/src/select-query.svelte.ts +191 -0
  74. package/src/session-query.svelte.ts +103 -0
  75. package/src/strict-command-hooks.svelte.ts +333 -0
  76. package/src/strict-hooks.svelte.ts +198 -0
  77. package/src/table-hooks.svelte.ts +180 -174
  78. package/src/table-state.svelte.ts +117 -117
  79. package/src/task-contract.ts +87 -0
  80. package/src/task-hooks.svelte.ts +190 -180
  81. package/src/task-provider.ts +115 -0
  82. package/src/task-subscription.ts +132 -0
  83. package/src/theme.svelte.ts +155 -154
  84. package/src/toast.svelte.ts +38 -37
  85. package/src/types.ts +68 -110
  86. package/src/unsafe.ts +7 -0
  87. package/src/update-contract.ts +65 -0
  88. package/src/update-hooks.svelte.ts +347 -0
  89. package/src/update-many-contract.ts +79 -0
  90. package/src/update-many-hooks.svelte.ts +373 -0
  91. package/src/url-sync.ts +11 -10
  92. package/src/useCan.ts +96 -31
  93. package/src/useParsed.svelte.ts +18 -14
  94. package/src/useStepsForm.svelte.ts +123 -63
  95. package/src/utility-hooks.svelte.ts +101 -158
  96. package/src/form-hooks.svelte.ts +0 -889
  97. package/src/mutation-hooks.svelte.ts +0 -561
  98. package/src/schema-form.ts +0 -31
@@ -1,16 +1,21 @@
1
- import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query';
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 { checkError, createOvertimeTracker, fireErrorNotification, fireSuccessNotification } from './hook-utils.svelte';
10
- import type { NotificationConfig, OvertimeOptions } from './hook-utils.svelte';
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<TTask extends TaskRecord = TaskRecord>(
22
- provider?: TaskProvider<TTask>,
23
- adminContext: AdminContextAccessor = captureAdminContext(),
24
- ): TaskProvider<TTask> {
25
- const activeProvider = provider ?? adminContext.taskProvider;
26
- if (!activeProvider) {
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 as TaskProvider<TTask>;
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, variables: unknown, context: unknown) => void;
35
- onError?: (error: unknown, variables: unknown, context: unknown) => void;
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<TTask extends TaskRecord = TaskRecord> {
74
+ export interface UseSubmitTaskMutateParams {
41
75
  taskName: string;
42
76
  options?: SubmitTaskOptions;
43
- taskProvider?: TaskProvider<TTask>;
77
+ taskProvider?: TaskProvider;
44
78
  successNotification?: NotificationConfig;
45
79
  errorNotification?: NotificationConfig;
46
80
  }
47
81
 
48
- export function useSubmitTask<
49
- TTask extends TaskRecord = TaskRecord,
50
- TError = HttpError,
51
- >(options: UseSubmitTaskOptions = {}) {
52
- const adminContext = captureAdminContext();
53
- const i18n = useTranslation();
54
- const adminOptions = getAdminOptions();
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 = createMutation<TaskHandle<TTask>, TError, UseSubmitTaskMutateParams<TTask>>(() => ({
90
+ const mutation=createMutation<TaskHandle,Error,UseSubmitTaskMutateParams>(() => ({
59
91
  mutationFn: async (params) => {
60
- const provider = normalizeTaskProvider<TTask>(params.taskProvider, adminContext);
61
- const providerMeta = adminContext.tenant
92
+ const scope = adminContext.queryKeyMatcher();
93
+ const provider=normalizeTaskProvider(params.taskProvider,adminContext);
94
+ const providerMeta=adminContext.tenant
62
95
  ? {
63
- ...(params.options?.meta ?? {}),
64
- ...resolveTenantProviderMeta(adminContext.tenant, adminContext.tenantAdapter),
65
- }
66
- : params.options?.meta;
67
- const taskOptions = providerMeta === params.options?.meta
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
- : { ...params.options, meta: providerMeta };
70
- return provider.submit(params.taskName, taskOptions);
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, params, context) => {
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
- provider: adminContext.notificationProvider,
80
- });
81
- void queryClient.invalidateQueries({
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
- options.mutationOptions?.onSuccess?.(data, params, context);
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, params, context) => {
91
- checkError(error, adminContext);
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
- provider: adminContext.notificationProvider,
141
+ ...definedOptions({
142
+ provider: adminContext.notificationProvider
143
+ })
98
144
  });
99
- options.mutationOptions?.onError?.(error, params, context);
145
+ options.mutationOptions?.onError?.(error,params,context);
100
146
  },
101
147
  }));
102
148
 
103
- return { mutation, get overtime() { return overtime; } };
149
+ return { mutation,get overtime() { return overtime; } };
104
150
  }
105
151
 
106
- export interface UseTaskOptions<TTask extends TaskRecord = TaskRecord, _TError = HttpError> {
152
+ export interface UseTaskOptions {
107
153
  taskId?: string;
108
- taskProvider?: TaskProvider<TTask>;
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 | false;
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<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
123
- options: UseTaskOptions<TTask, TError> = {},
168
+ export function useTask(
169
+ options: UseTaskOptions={},
124
170
  ) {
125
- const adminContext = captureAdminContext();
126
- const i18n = useTranslation();
127
- const adminOptions = getAdminOptions();
171
+ const adminContext=captureAdminContext();
172
+ const i18n=useTranslation();
173
+ const adminOptions=getAdminOptions();
128
174
 
129
- const query = createQuery<TTask, TError>(() => {
130
- const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
131
- const queryOptions = options.queryOptions;
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
- queryKey: adminContext.queryKeys().task.one(options.taskId ?? ''),
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 (!options.taskId) throw new Error('useTask requires a taskId');
136
- return provider.get(options.taskId);
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 ?? true) && !!options.taskId,
139
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
140
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
141
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
142
- refetchInterval: queryOptions?.refetchInterval,
143
- refetchIntervalInBackground: queryOptions?.refetchIntervalInBackground,
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 = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.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 new Proxy(query, {
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<TTask extends TaskRecord = TaskRecord, _TError = HttpError> {
185
- params?: Record<string, unknown>;
210
+ export interface UseTaskListOptions {
211
+ params?: Record<string,unknown>;
186
212
  dlq?: boolean;
187
- taskProvider?: TaskProvider<TTask>;
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 | false;
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<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
202
- options: UseTaskListOptions<TTask, TError> = {},
227
+ export function useTaskList(
228
+ options: UseTaskListOptions={},
203
229
  ) {
204
- const adminContext = captureAdminContext();
205
- const i18n = useTranslation();
206
- const adminOptions = getAdminOptions();
230
+ const adminContext=captureAdminContext();
231
+ const i18n=useTranslation();
232
+ const adminOptions=getAdminOptions();
207
233
 
208
- const query = createQuery<TaskListResult<TTask>, TError>(() => {
209
- const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
210
- const queryOptions = options.queryOptions;
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
- queryKey: adminContext.queryKeys().task.list({
213
- list: options.dlq ? 'dlq' : 'default',
214
- params: options.params,
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 (options.dlq) {
218
- if (!provider.listDlq) throw new Error('TaskProvider does not implement listDlq');
219
- return provider.listDlq(options.params);
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 (!provider.list) throw new Error('TaskProvider does not implement list');
222
- return provider.list(options.params);
260
+ if(!provider.list) throw new Error('TaskProvider does not implement list');
261
+ return provider.list(params);
223
262
  },
224
- enabled: queryOptions?.enabled ?? true,
225
- staleTime: queryOptions?.staleTime ?? adminOptions.reactQuery?.staleTime,
226
- gcTime: queryOptions?.gcTime ?? adminOptions.reactQuery?.gcTime,
227
- refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus ?? adminOptions.reactQuery?.refetchOnWindowFocus,
228
- refetchInterval: queryOptions?.refetchInterval,
229
- refetchIntervalInBackground: queryOptions?.refetchIntervalInBackground,
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 = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
276
+ const overtime=createOvertimeTracker(() => query.isLoading,options.overtimeOptions??adminOptions.overtime);
234
277
 
235
- let lastSuccessAt = 0;
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<TTask extends TaskRecord = TaskRecord> {
281
+ export interface UseTaskSubscriptionOptions {
272
282
  taskId: string;
273
- taskProvider?: TaskProvider<TTask>;
283
+ taskProvider?: TaskProvider;
274
284
  enabled?: boolean;
275
- onTask: (task: TTask) => void;
285
+ onTask: (task: TaskRecord) => void;
286
+ onError?: (error: TaskError) => void;
276
287
  }
277
288
 
278
- export function useTaskSubscription<TTask extends TaskRecord = TaskRecord>(
279
- options: UseTaskSubscriptionOptions<TTask>,
289
+ export function useTaskSubscription(
290
+ options: UseTaskSubscriptionOptions,
280
291
  ): void {
281
- const adminContext = captureAdminContext();
292
+ const adminContext=captureAdminContext();
282
293
  $effect(() => {
283
- const enabled = options.enabled ?? true;
284
- if (!enabled) return;
294
+ const enabled=options.enabled??true;
295
+ if(!enabled) return;
285
296
 
286
- const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
287
- if (!provider.subscribe) {
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 = provider.subscribe(options.taskId, options.onTask);
292
- if (!subscription) return;
302
+ const subscription=provider.subscribe(options.taskId,options.onTask,options.onError);
293
303
 
294
- if (typeof subscription === 'function') {
304
+ if(typeof subscription==='function') {
295
305
  return subscription;
296
306
  }
297
307
 
298
- if (typeof subscription.unsubscribe === 'function') {
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
+ }