@svadmin/core 0.50.0 → 0.52.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 (97) 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 +56 -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/infinite-query.svelte.ts +228 -0
  40. package/src/invalidation-contract.ts +26 -0
  41. package/src/live-sse.ts +91 -127
  42. package/src/live-subscription.svelte.ts +130 -0
  43. package/src/live-transport.ts +127 -0
  44. package/src/live-websocket.ts +85 -118
  45. package/src/live.svelte.ts +66 -68
  46. package/src/notification.svelte.ts +17 -15
  47. package/src/permission-catalog.ts +121 -0
  48. package/src/permission-hints-contract.ts +48 -0
  49. package/src/permissions.svelte.ts +112 -59
  50. package/src/plain-data.ts +61 -0
  51. package/src/provider-bundle.ts +85 -79
  52. package/src/provider-response.ts +35 -0
  53. package/src/query-hooks.svelte.ts +241 -288
  54. package/src/query-invalidation.ts +172 -0
  55. package/src/query-keys.ts +221 -110
  56. package/src/query-session.svelte.ts +216 -0
  57. package/src/query-snapshot.ts +144 -0
  58. package/src/reactive-projection.ts +53 -0
  59. package/src/record-decoder.ts +63 -0
  60. package/src/resource-binding.svelte.ts +19 -0
  61. package/src/resource-contract.ts +595 -0
  62. package/src/resource-mutation-types.ts +30 -0
  63. package/src/resource-schemas.ts +371 -0
  64. package/src/resource-transport-config.ts +156 -0
  65. package/src/route-parsing.ts +11 -11
  66. package/src/router-provider.ts +64 -59
  67. package/src/router.ts +8 -3
  68. package/src/routing-hooks.svelte.ts +90 -62
  69. package/src/schema-validation.ts +92 -0
  70. package/src/schema.ts +13 -0
  71. package/src/select-options.ts +95 -0
  72. package/src/select-query.svelte.ts +191 -0
  73. package/src/session-query.svelte.ts +88 -0
  74. package/src/strict-command-hooks.svelte.ts +333 -0
  75. package/src/strict-hooks.svelte.ts +198 -0
  76. package/src/table-hooks.svelte.ts +180 -174
  77. package/src/table-state.svelte.ts +117 -117
  78. package/src/task-contract.ts +87 -0
  79. package/src/task-hooks.svelte.ts +193 -149
  80. package/src/task-provider.ts +115 -0
  81. package/src/task-subscription.ts +132 -0
  82. package/src/theme.svelte.ts +155 -154
  83. package/src/toast.svelte.ts +38 -37
  84. package/src/types.ts +55 -108
  85. package/src/unsafe.ts +7 -0
  86. package/src/update-contract.ts +65 -0
  87. package/src/update-hooks.svelte.ts +347 -0
  88. package/src/update-many-contract.ts +79 -0
  89. package/src/update-many-hooks.svelte.ts +373 -0
  90. package/src/url-sync.ts +11 -10
  91. package/src/useCan.ts +96 -31
  92. package/src/useParsed.svelte.ts +18 -14
  93. package/src/useStepsForm.svelte.ts +123 -63
  94. package/src/utility-hooks.svelte.ts +101 -158
  95. package/src/form-hooks.svelte.ts +0 -889
  96. package/src/mutation-hooks.svelte.ts +0 -561
  97. package/src/schema-form.ts +0 -31
@@ -1,16 +1,19 @@
1
- import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query';
1
+ import { definedOptions } from './defined-options';
2
+ import { createMutation,createQuery,useQueryClient } from '@tanstack/svelte-query';
2
3
  import { getAdminOptions } from './options.svelte';
3
4
  import { captureAdminContext } from './context.svelte';
4
5
  import type { AdminContextAccessor } from './context.svelte';
5
6
  import {
6
7
  resolveTenantProviderMeta,
7
8
  } 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';
9
+ import { queryKeyMatches, type QueryKey } from './query-keys';
10
+ import { replaceReactiveMembers } from './reactive-projection';
11
+ import { taskQueryParams, withValidatedTaskProvider } from './task-provider';
12
+ import type { TaskError } from './task-contract';
13
+ import { checkError,createOvertimeTracker,fireErrorNotification,fireSuccessNotification } from './hook-utils.svelte';
14
+ import type { NotificationConfig,OvertimeOptions } from './hook-utils.svelte';
11
15
  import { useTranslation } from './i18n.svelte';
12
16
  import type {
13
- HttpError,
14
17
  SubmitTaskOptions,
15
18
  TaskHandle,
16
19
  TaskListResult,
@@ -18,100 +21,128 @@ import type {
18
21
  TaskRecord,
19
22
  } from './types';
20
23
 
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) {
24
+ function normalizeTaskProvider(
25
+ provider?: TaskProvider,
26
+ adminContext: AdminContextAccessor=captureAdminContext(),
27
+ ): TaskProvider {
28
+ const activeProvider=provider??adminContext.taskProvider;
29
+ if(!activeProvider) {
27
30
  throw new Error('TaskProvider not found. Did you call setTaskProvider in App.svelte?');
28
31
  }
29
- return activeProvider as TaskProvider<TTask>;
32
+ return withValidatedTaskProvider(activeProvider);
33
+ }
34
+
35
+ const providerKeys = new WeakMap<TaskProvider, number>();
36
+ let nextProviderKey = 0;
37
+ function taskProviderKey(provider: TaskProvider | undefined): number {
38
+ if (!provider) return 0;
39
+ let providerKey = providerKeys.get(provider);
40
+ if (providerKey === undefined) {
41
+ providerKey = ++nextProviderKey;
42
+ providerKeys.set(provider, providerKey);
43
+ }
44
+ return providerKey;
45
+ }
46
+ function scopedTaskKey(key: QueryKey, provider: TaskProvider | undefined): QueryKey {
47
+ const providerKey = taskProviderKey(provider);
48
+ return [{ ...key[0], params: definedOptions({ taskProvider: providerKey, query: key[0].params }) }];
30
49
  }
31
50
 
32
51
  export interface UseSubmitTaskOptions {
33
52
  mutationOptions?: {
34
- onSuccess?: (data: unknown, variables: unknown, context: unknown) => void;
35
- onError?: (error: unknown, variables: unknown, context: unknown) => void;
53
+ onSuccess?: (data: unknown,variables: unknown,context: unknown) => void;
54
+ onError?: (error: unknown,variables: unknown,context: unknown) => void;
36
55
  };
37
56
  overtimeOptions?: OvertimeOptions;
38
57
  }
39
58
 
40
- export interface UseSubmitTaskMutateParams<TTask extends TaskRecord = TaskRecord> {
59
+ export interface UseSubmitTaskMutateParams {
41
60
  taskName: string;
42
61
  options?: SubmitTaskOptions;
43
- taskProvider?: TaskProvider<TTask>;
62
+ taskProvider?: TaskProvider;
44
63
  successNotification?: NotificationConfig;
45
64
  errorNotification?: NotificationConfig;
46
65
  }
47
66
 
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);
67
+ export function useSubmitTask(options: UseSubmitTaskOptions={}) {
68
+ const adminContext=captureAdminContext();
69
+ const i18n=useTranslation();
70
+ const adminOptions=getAdminOptions();
71
+ const queryClient=useQueryClient();
72
+ const overtime=createOvertimeTracker(() => mutation.isPending,options.overtimeOptions??adminOptions.overtime);
73
+ const submissionScopes = new WeakMap<TaskHandle, ReturnType<AdminContextAccessor['queryKeyMatcher']>>();
57
74
 
58
- const mutation = createMutation<TaskHandle<TTask>, TError, UseSubmitTaskMutateParams<TTask>>(() => ({
75
+ const mutation=createMutation<TaskHandle,Error,UseSubmitTaskMutateParams>(() => ({
59
76
  mutationFn: async (params) => {
60
- const provider = normalizeTaskProvider<TTask>(params.taskProvider, adminContext);
61
- const providerMeta = adminContext.tenant
77
+ const scope = adminContext.queryKeyMatcher();
78
+ const provider=normalizeTaskProvider(params.taskProvider,adminContext);
79
+ const providerMeta=adminContext.tenant
62
80
  ? {
63
- ...(params.options?.meta ?? {}),
64
- ...resolveTenantProviderMeta(adminContext.tenant, adminContext.tenantAdapter),
65
- }
66
- : params.options?.meta;
67
- const taskOptions = providerMeta === params.options?.meta
81
+ ...(params.options?.meta??{}),
82
+ ...resolveTenantProviderMeta(adminContext.tenant,adminContext.tenantAdapter),
83
+ }
84
+ :params.options?.meta;
85
+ const taskOptions=providerMeta===params.options?.meta
68
86
  ? params.options
69
- : { ...params.options, meta: providerMeta };
70
- return provider.submit(params.taskName, taskOptions);
87
+ :definedOptions({
88
+ ...params.options,
89
+ meta: providerMeta,
90
+ });
91
+ const handle = await provider.submit(params.taskName,taskOptions);
92
+ submissionScopes.set(handle, scope);
93
+ return handle;
71
94
  },
72
- onSuccess: (data, params, context) => {
95
+ onSuccess: (data,params,context) => {
73
96
  fireSuccessNotification({
74
97
  config: params.successNotification,
75
98
  defaultMessage: i18n.t('task.submitSuccess'),
76
99
  data,
77
100
  values: params.options,
78
101
  resource: params.taskName,
79
- provider: adminContext.notificationProvider,
102
+ ...definedOptions({
103
+ provider: adminContext.notificationProvider
104
+ })
80
105
  });
81
- void queryClient.invalidateQueries({
82
- predicate: (query) => queryKeyMatches(query.queryKey, {
83
- ...adminContext.queryKeyMatcher(),
84
- kind: 'task',
85
- action: 'list',
86
- }),
87
- });
88
- options.mutationOptions?.onSuccess?.(data, params, context);
106
+ const scope = submissionScopes.get(data);
107
+ if (scope) {
108
+ submissionScopes.delete(data);
109
+ void queryClient.invalidateQueries({
110
+ predicate: (query) => queryKeyMatches(query.queryKey,{
111
+ ...scope,
112
+ kind: 'task',
113
+ action: 'list',
114
+ }),
115
+ });
116
+ }
117
+ options.mutationOptions?.onSuccess?.(data,params,context);
89
118
  },
90
- onError: (error, params, context) => {
91
- checkError(error, adminContext);
119
+ onError: (error,params,context) => {
120
+ checkError(error,adminContext);
92
121
  fireErrorNotification({
93
122
  config: params.errorNotification,
94
123
  defaultMessage: i18n.t('task.submitFailed'),
95
124
  error,
96
125
  resource: params.taskName,
97
- provider: adminContext.notificationProvider,
126
+ ...definedOptions({
127
+ provider: adminContext.notificationProvider
128
+ })
98
129
  });
99
- options.mutationOptions?.onError?.(error, params, context);
130
+ options.mutationOptions?.onError?.(error,params,context);
100
131
  },
101
132
  }));
102
133
 
103
- return { mutation, get overtime() { return overtime; } };
134
+ return { mutation,get overtime() { return overtime; } };
104
135
  }
105
136
 
106
- export interface UseTaskOptions<TTask extends TaskRecord = TaskRecord, _TError = HttpError> {
137
+ export interface UseTaskOptions {
107
138
  taskId?: string;
108
- taskProvider?: TaskProvider<TTask>;
139
+ taskProvider?: TaskProvider;
109
140
  queryOptions?: {
110
141
  enabled?: boolean;
111
142
  staleTime?: number;
112
143
  gcTime?: number;
113
144
  refetchOnWindowFocus?: boolean;
114
- refetchInterval?: number | false;
145
+ refetchInterval?: number|false;
115
146
  refetchIntervalInBackground?: boolean;
116
147
  };
117
148
  successNotification?: NotificationConfig;
@@ -119,78 +150,84 @@ export interface UseTaskOptions<TTask extends TaskRecord = TaskRecord, _TError =
119
150
  overtimeOptions?: OvertimeOptions;
120
151
  }
121
152
 
122
- export function useTask<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
123
- options: UseTaskOptions<TTask, TError> = {},
153
+ export function useTask(
154
+ options: UseTaskOptions={},
124
155
  ) {
125
- const adminContext = captureAdminContext();
126
- const i18n = useTranslation();
127
- const adminOptions = getAdminOptions();
156
+ const adminContext=captureAdminContext();
157
+ const i18n=useTranslation();
158
+ const adminOptions=getAdminOptions();
128
159
 
129
- const query = createQuery<TTask, TError>(() => {
130
- const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
131
- const queryOptions = options.queryOptions;
160
+ const query=createQuery<TaskRecord,Error>(() => {
161
+ const activeProvider=options.taskProvider??adminContext.taskProvider;
162
+ const provider=activeProvider? withValidatedTaskProvider(activeProvider):undefined;
163
+ const taskId=options.taskId;
164
+ const queryOptions=options.queryOptions;
132
165
  return {
133
- queryKey: adminContext.queryKeys().task.one(options.taskId ?? ''),
166
+ queryKey: scopedTaskKey(adminContext.queryKeys().task.one(taskId??''),provider),
134
167
  queryFn: async () => {
135
- if (!options.taskId) throw new Error('useTask requires a taskId');
136
- return provider.get(options.taskId);
168
+ if(!provider) throw new Error('TaskProvider not found.');
169
+ if(!taskId) throw new Error('useTask requires a taskId');
170
+ return provider.get(taskId);
137
171
  },
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,
172
+ enabled: (queryOptions?.enabled??true)&&!!taskId,
173
+ ...definedOptions({
174
+ staleTime: queryOptions?.staleTime??adminOptions.reactQuery?.staleTime,
175
+ gcTime: queryOptions?.gcTime??adminOptions.reactQuery?.gcTime,
176
+ refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus??adminOptions.reactQuery?.refetchOnWindowFocus,
177
+ }),
178
+ ...definedOptions({
179
+ refetchInterval: queryOptions?.refetchInterval,
180
+ refetchIntervalInBackground: queryOptions?.refetchIntervalInBackground,
181
+ }),
144
182
  };
145
183
  });
146
184
 
147
- const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
185
+ const overtime=createOvertimeTracker(() => query.isLoading,options.overtimeOptions??adminOptions.overtime);
148
186
 
149
- let lastSuccessAt = 0;
150
- let lastErrorAt = 0;
187
+ let lastSuccessAt=0;
188
+ let lastErrorAt=0;
151
189
  $effect(() => {
152
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
153
- lastSuccessAt = query.dataUpdatedAt;
154
- if (options.successNotification) {
190
+ if(query.isSuccess&&query.dataUpdatedAt>lastSuccessAt) {
191
+ lastSuccessAt=query.dataUpdatedAt;
192
+ if(options.successNotification) {
155
193
  fireSuccessNotification({
156
194
  config: options.successNotification,
157
195
  defaultMessage: '',
158
196
  data: query.data,
159
- resource: options.taskId,
160
- provider: adminContext.notificationProvider,
197
+ ...definedOptions({ resource: options.taskId }),
198
+ ...definedOptions({
199
+ provider: adminContext.notificationProvider
200
+ })
161
201
  });
162
202
  }
163
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
164
- lastErrorAt = query.errorUpdatedAt;
165
- checkError(query.error, adminContext);
203
+ } else if(query.isError&&query.errorUpdatedAt>lastErrorAt) {
204
+ lastErrorAt=query.errorUpdatedAt;
205
+ checkError(query.error,adminContext);
166
206
  fireErrorNotification({
167
207
  config: options.errorNotification,
168
208
  defaultMessage: i18n.t('task.fetchFailed'),
169
209
  error: query.error,
170
- resource: options.taskId,
171
- provider: adminContext.notificationProvider,
210
+ ...definedOptions({ resource: options.taskId }),
211
+ ...definedOptions({
212
+ provider: adminContext.notificationProvider
213
+ })
172
214
  });
173
215
  }
174
216
  });
175
217
 
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 };
218
+ return replaceReactiveMembers(query,{ get overtime() { return overtime; } });
182
219
  }
183
220
 
184
- export interface UseTaskListOptions<TTask extends TaskRecord = TaskRecord, _TError = HttpError> {
185
- params?: Record<string, unknown>;
221
+ export interface UseTaskListOptions {
222
+ params?: Record<string,unknown>;
186
223
  dlq?: boolean;
187
- taskProvider?: TaskProvider<TTask>;
224
+ taskProvider?: TaskProvider;
188
225
  queryOptions?: {
189
226
  enabled?: boolean;
190
227
  staleTime?: number;
191
228
  gcTime?: number;
192
229
  refetchOnWindowFocus?: boolean;
193
- refetchInterval?: number | false;
230
+ refetchInterval?: number|false;
194
231
  refetchIntervalInBackground?: boolean;
195
232
  };
196
233
  successNotification?: NotificationConfig;
@@ -198,104 +235,111 @@ export interface UseTaskListOptions<TTask extends TaskRecord = TaskRecord, _TErr
198
235
  overtimeOptions?: OvertimeOptions;
199
236
  }
200
237
 
201
- export function useTaskList<TTask extends TaskRecord = TaskRecord, TError = HttpError>(
202
- options: UseTaskListOptions<TTask, TError> = {},
238
+ export function useTaskList(
239
+ options: UseTaskListOptions={},
203
240
  ) {
204
- const adminContext = captureAdminContext();
205
- const i18n = useTranslation();
206
- const adminOptions = getAdminOptions();
241
+ const adminContext=captureAdminContext();
242
+ const i18n=useTranslation();
243
+ const adminOptions=getAdminOptions();
207
244
 
208
- const query = createQuery<TaskListResult<TTask>, TError>(() => {
209
- const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
210
- const queryOptions = options.queryOptions;
245
+ const query=createQuery<TaskListResult,Error>(() => {
246
+ const activeProvider=options.taskProvider??adminContext.taskProvider;
247
+ const provider=activeProvider? withValidatedTaskProvider(activeProvider):undefined;
248
+ const params=options.params===undefined? undefined:taskQueryParams(options.params);
249
+ const dlq=options.dlq??false;
250
+ const queryOptions=options.queryOptions;
211
251
  return {
212
- queryKey: adminContext.queryKeys().task.list({
213
- list: options.dlq ? 'dlq' : 'default',
214
- params: options.params,
215
- }),
252
+ queryKey: scopedTaskKey(adminContext.queryKeys().task.list(definedOptions({
253
+ list: dlq? 'dlq':'default',
254
+ params,
255
+ })),provider),
216
256
  queryFn: async () => {
217
- if (options.dlq) {
218
- if (!provider.listDlq) throw new Error('TaskProvider does not implement listDlq');
219
- return provider.listDlq(options.params);
257
+ if(!provider) throw new Error('TaskProvider not found.');
258
+ if(dlq) {
259
+ if(!provider.listDlq) throw new Error('TaskProvider does not implement listDlq');
260
+ return provider.listDlq(params);
220
261
  }
221
- if (!provider.list) throw new Error('TaskProvider does not implement list');
222
- return provider.list(options.params);
262
+ if(!provider.list) throw new Error('TaskProvider does not implement list');
263
+ return provider.list(params);
223
264
  },
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,
265
+ enabled: queryOptions?.enabled??true,
266
+ ...definedOptions({
267
+ staleTime: queryOptions?.staleTime??adminOptions.reactQuery?.staleTime,
268
+ gcTime: queryOptions?.gcTime??adminOptions.reactQuery?.gcTime,
269
+ refetchOnWindowFocus: queryOptions?.refetchOnWindowFocus??adminOptions.reactQuery?.refetchOnWindowFocus,
270
+ }),
271
+ ...definedOptions({
272
+ refetchInterval: queryOptions?.refetchInterval,
273
+ refetchIntervalInBackground: queryOptions?.refetchIntervalInBackground,
274
+ }),
230
275
  };
231
276
  });
232
277
 
233
- const overtime = createOvertimeTracker(() => query.isLoading, options.overtimeOptions ?? adminOptions.overtime);
278
+ const overtime=createOvertimeTracker(() => query.isLoading,options.overtimeOptions??adminOptions.overtime);
234
279
 
235
- let lastSuccessAt = 0;
236
- let lastErrorAt = 0;
280
+ let lastSuccessAt=0;
281
+ let lastErrorAt=0;
237
282
  $effect(() => {
238
- if (query.isSuccess && query.dataUpdatedAt > lastSuccessAt) {
239
- lastSuccessAt = query.dataUpdatedAt;
240
- if (options.successNotification) {
283
+ if(query.isSuccess&&query.dataUpdatedAt>lastSuccessAt) {
284
+ lastSuccessAt=query.dataUpdatedAt;
285
+ if(options.successNotification) {
241
286
  fireSuccessNotification({
242
287
  config: options.successNotification,
243
288
  defaultMessage: '',
244
289
  data: query.data,
245
290
  values: options.params,
246
- resource: options.dlq ? 'taskDlq' : 'tasks',
247
- provider: adminContext.notificationProvider,
291
+ resource: options.dlq? 'taskDlq':'tasks',
292
+ ...definedOptions({
293
+ provider: adminContext.notificationProvider
294
+ })
248
295
  });
249
296
  }
250
- } else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
251
- lastErrorAt = query.errorUpdatedAt;
252
- checkError(query.error, adminContext);
297
+ } else if(query.isError&&query.errorUpdatedAt>lastErrorAt) {
298
+ lastErrorAt=query.errorUpdatedAt;
299
+ checkError(query.error,adminContext);
253
300
  fireErrorNotification({
254
301
  config: options.errorNotification,
255
302
  defaultMessage: i18n.t('task.fetchListFailed'),
256
303
  error: query.error,
257
- resource: options.dlq ? 'taskDlq' : 'tasks',
258
- provider: adminContext.notificationProvider,
304
+ resource: options.dlq? 'taskDlq':'tasks',
305
+ ...definedOptions({
306
+ provider: adminContext.notificationProvider
307
+ })
259
308
  });
260
309
  }
261
310
  });
262
311
 
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 };
312
+ return replaceReactiveMembers(query,{ get overtime() { return overtime; } });
269
313
  }
270
314
 
271
- export interface UseTaskSubscriptionOptions<TTask extends TaskRecord = TaskRecord> {
315
+ export interface UseTaskSubscriptionOptions {
272
316
  taskId: string;
273
- taskProvider?: TaskProvider<TTask>;
317
+ taskProvider?: TaskProvider;
274
318
  enabled?: boolean;
275
- onTask: (task: TTask) => void;
319
+ onTask: (task: TaskRecord) => void;
320
+ onError?: (error: TaskError) => void;
276
321
  }
277
322
 
278
- export function useTaskSubscription<TTask extends TaskRecord = TaskRecord>(
279
- options: UseTaskSubscriptionOptions<TTask>,
323
+ export function useTaskSubscription(
324
+ options: UseTaskSubscriptionOptions,
280
325
  ): void {
281
- const adminContext = captureAdminContext();
326
+ const adminContext=captureAdminContext();
282
327
  $effect(() => {
283
- const enabled = options.enabled ?? true;
284
- if (!enabled) return;
328
+ const enabled=options.enabled??true;
329
+ if(!enabled) return;
285
330
 
286
- const provider = normalizeTaskProvider<TTask>(options.taskProvider, adminContext);
287
- if (!provider.subscribe) {
331
+ const provider=normalizeTaskProvider(options.taskProvider,adminContext);
332
+ if(!provider.subscribe) {
288
333
  throw new Error('TaskProvider does not implement subscribe');
289
334
  }
290
335
 
291
- const subscription = provider.subscribe(options.taskId, options.onTask);
292
- if (!subscription) return;
336
+ const subscription=provider.subscribe(options.taskId,options.onTask,options.onError);
293
337
 
294
- if (typeof subscription === 'function') {
338
+ if(typeof subscription==='function') {
295
339
  return subscription;
296
340
  }
297
341
 
298
- if (typeof subscription.unsubscribe === 'function') {
342
+ if(typeof subscription.unsubscribe==='function') {
299
343
  return () => subscription.unsubscribe();
300
344
  }
301
345
  });
@@ -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
+ }