@azure-net/kit 5.3.0 → 5.4.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.
@@ -20,7 +20,7 @@ export declare class ResponseBuilder<TData = unknown, TMeta = object, TWrapper =
20
20
  toPlainObject(): TResource;
21
21
  }): Omit<this, keyof ResponseBuilder<unknown, unknown, unknown>> & ResponseBuilder<TResource[], TMeta, TWrapper>;
22
22
  extract<TPath extends DeepKeys<TData>>(path: TPath): Omit<this, keyof ResponseBuilder<unknown, unknown, unknown>> & ResponseBuilder<DeepValue<TData, TPath>, TMeta, TWrapper>;
23
- addMeta<TNewMeta extends Record<string, unknown>>(metaData: TNewMeta | ((current: TMeta) => TNewMeta)): Omit<this, keyof ResponseBuilder<unknown, unknown, unknown>> & ResponseBuilder<TData, TMeta & TNewMeta, TWrapper>;
23
+ addMeta<TNewMeta extends Record<string, unknown>>(metaData: TNewMeta | ((current: TMeta & TNewMeta) => TMeta & TNewMeta)): Omit<this, keyof ResponseBuilder<unknown, unknown, unknown>> & ResponseBuilder<TData, TMeta & TNewMeta, TWrapper>;
24
24
  getData(): TData;
25
25
  get(): {
26
26
  data: TData;
@@ -1,6 +1,7 @@
1
1
  import type { RequestErrors } from '../../delivery/schema/index.js';
2
2
  import type { AsyncActionResponse } from '../../delivery/injectable-dependencies/AsyncHelpers.js';
3
- type InitialData<FormData, Initial extends Partial<FormData>> = () => Initial | Promise<Initial>;
3
+ import type { AsyncSignalSvelte } from '../async-signal/AsyncSignal.svelte.js';
4
+ type MaybePromise<T> = T | Promise<T>;
4
5
  type PathRequiredShape<T, P extends string> = P extends `${infer Head}.${infer Tail}` ? Head extends keyof T ? {
5
6
  [K in Head]-?: PathRequiredShape<NonNullable<T[K]>, Tail>;
6
7
  } : unknown : P extends keyof T ? {
@@ -8,32 +9,59 @@ type PathRequiredShape<T, P extends string> = P extends `${infer Head}.${infer T
8
9
  } : unknown;
9
10
  type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never;
10
11
  type RequiredByPaths<T, P extends string> = [P] extends [never] ? object : UnionToIntersection<PathRequiredShape<T, P>>;
11
- type ResetBehaviors = 'clear' | 'initial' | 'reloadInitial' | 'default';
12
- export interface FormConfig<FormData, Response, Initial extends Partial<FormData> = Partial<FormData>, RequiredPath extends string = never> {
13
- initialData?: InitialData<FormData, Initial>;
14
- required?: readonly RequiredPath[];
15
- onSuccess?: (response: Response) => Promise<void> | void;
16
- onError?: () => Promise<void> | void;
17
- beforeSubmit?: (actions: {
18
- form: ActiveFormController<FormData, RequiredPath>;
19
- abort: () => void;
20
- }) => Promise<void> | void;
21
- successBehavior?: ResetBehaviors;
12
+ export type ResetBehaviors = 'clear' | 'initial' | 'reloadInitial' | 'default';
13
+ export type ActiveFormStatus = 'idle' | 'waitingForInitialValues' | 'pending' | 'success' | 'error';
14
+ export type ActiveFormResponse<Response, FormData, Custom = Record<never, never>> = AsyncActionResponse<Response, FormData, Custom>;
15
+ export interface LinkSignalOptions<TValue = unknown, MappedValues = unknown> {
16
+ watch?: boolean;
17
+ mapValues?: (data: TValue | undefined) => MappedValues;
18
+ }
19
+ export interface LinkPromise<FormData> {
20
+ <TValue extends Partial<FormData>>(source: () => Promise<TValue>): Promise<Partial<FormData>>;
21
+ <TValue>(source: () => Promise<TValue>, map: (value: TValue) => MaybePromise<Partial<FormData>>): Promise<Partial<FormData>>;
22
+ }
23
+ export interface LinkSignal<FormData> {
24
+ <TValue, MappedValues extends Partial<FormData>>(signal: AsyncSignalSvelte<TValue, unknown>, options: LinkSignalOptions<TValue, MappedValues> & {
25
+ mapValues: (data: TValue | undefined) => MappedValues;
26
+ }): Promise<MappedValues>;
27
+ <TValue extends Partial<FormData>>(signal: AsyncSignalSvelte<TValue, unknown>, options?: {
28
+ watch?: boolean;
29
+ mapValues?: never;
30
+ }): Promise<TValue | undefined>;
31
+ }
32
+ export interface InitialValuesHelpers<FormData> {
33
+ linkPromise: LinkPromise<FormData>;
34
+ linkSignal: LinkSignal<FormData>;
22
35
  }
36
+ type InitialValues<FormData> = (helpers: InitialValuesHelpers<FormData>) => MaybePromise<Partial<FormData> | undefined>;
23
37
  export interface ActiveForm<FormData, Response, Custom, RequiredPath extends string = never> {
24
38
  data: Partial<FormData> & RequiredByPaths<FormData, RequiredPath>;
25
39
  errors: RequestErrors<FormData>;
26
- submit: () => Promise<AsyncActionResponse<Response, FormData, Custom>>;
40
+ submit: () => Promise<ActiveFormResponse<Response, FormData, Custom>>;
27
41
  reset: (behavior?: ResetBehaviors) => Promise<void>;
28
42
  pending: boolean;
43
+ status: ActiveFormStatus;
29
44
  dirty: boolean;
30
- ready: Promise<Partial<FormData>>;
31
45
  }
32
46
  export interface ActiveFormController<FormData, RequiredPath extends string = never> {
33
47
  data: Partial<FormData> & RequiredByPaths<FormData, RequiredPath>;
34
48
  errors: RequestErrors<FormData>;
35
49
  reset: (behavior?: ResetBehaviors) => Promise<void>;
36
50
  }
51
+ export interface ActiveFormCallbackContext<FormData, Response, Custom, RequiredPath extends string = never> {
52
+ form: ActiveForm<FormData, Response, Custom, RequiredPath>;
53
+ response: ActiveFormResponse<Response, FormData, Custom>;
54
+ }
55
+ export interface FormConfig<FormData, Response, Custom = Record<never, never>, RequiredPath extends string = never> {
56
+ initialValues?: InitialValues<FormData>;
57
+ required?: readonly RequiredPath[];
58
+ onSuccess?: (context: ActiveFormCallbackContext<FormData, Response, Custom, RequiredPath>) => Promise<void> | void;
59
+ onError?: (context: ActiveFormCallbackContext<FormData, Response, Custom, RequiredPath>) => Promise<void> | void;
60
+ beforeSubmit?: (actions: {
61
+ form: ActiveFormController<FormData, RequiredPath>;
62
+ abort: () => void;
63
+ }) => Promise<void> | void;
64
+ }
37
65
  type ExtractResponse<T> = T extends AsyncActionResponse<infer R, unknown, unknown> ? R : never;
38
66
  type ExtractFormData<T> = T extends AsyncActionResponse<unknown, infer D, unknown> ? D : never;
39
67
  type ExtractCustom<T> = T extends AsyncActionResponse<unknown, unknown, infer C> ? C : never;
@@ -43,5 +71,5 @@ type ExtractFromSubmit<T> = {
43
71
  formData: ExtractFormData<UnwrapPromise<T>>;
44
72
  custom: ExtractCustom<UnwrapPromise<T>>;
45
73
  };
46
- export declare const createActiveForm: <SubmitReturn extends Promise<AsyncActionResponse<unknown, unknown, unknown>>, RequiredPath extends string = never>(onSubmit: (formData: Partial<ExtractFromSubmit<SubmitReturn>["formData"]>) => SubmitReturn, config?: FormConfig<ExtractFromSubmit<SubmitReturn>["formData"], ExtractFromSubmit<SubmitReturn>["response"], Partial<ExtractFromSubmit<SubmitReturn>["formData"]>, RequiredPath>) => ActiveForm<ExtractFromSubmit<SubmitReturn>["formData"], ExtractFromSubmit<SubmitReturn>["response"], ExtractFromSubmit<SubmitReturn>["custom"], RequiredPath>;
74
+ export declare const createActiveForm: <SubmitReturn extends Promise<AsyncActionResponse<unknown, unknown, unknown>>, RequiredPath extends string = never>(onSubmit: (formData: Partial<ExtractFromSubmit<SubmitReturn>["formData"]>) => SubmitReturn, config?: FormConfig<ExtractFromSubmit<SubmitReturn>["formData"], ExtractFromSubmit<SubmitReturn>["response"], ExtractFromSubmit<SubmitReturn>["custom"], RequiredPath>) => ActiveForm<ExtractFromSubmit<SubmitReturn>["formData"], ExtractFromSubmit<SubmitReturn>["response"], ExtractFromSubmit<SubmitReturn>["custom"], RequiredPath>;
47
75
  export {};
@@ -1,35 +1,133 @@
1
1
  import { ObjectUtil } from '../../external/tools/index.js';
2
2
  import { cloneStateValue } from '../shared/cloneStateValue.js';
3
+ const NO_INITIAL_VALUES_ERROR = Symbol('no-initial-values-error');
4
+ const isPromiseLike = (value) => {
5
+ return typeof value?.then === 'function';
6
+ };
3
7
  export const createActiveForm = (onSubmit, config) => {
4
- const isPromise = (value) => {
5
- return typeof value?.then === 'function';
6
- };
7
8
  let initial = {};
8
9
  let formData = $state(cloneStateValue(initial));
9
10
  let formErrors = $state({});
10
- let pending = $state(false);
11
+ let status = $state('idle');
12
+ let signalBindings = $state.raw([]);
11
13
  let submitRunId = 0;
12
14
  let initialLoadRunId = 0;
15
+ let initialValuesTask = Promise.resolve();
16
+ let initialValuesError = NO_INITIAL_VALUES_ERROR;
17
+ let bindingsBySignal;
13
18
  const dirty = $derived(!ObjectUtil.equals(formData, initial));
14
- const loadInitialData = async () => {
15
- const runId = ++initialLoadRunId;
16
- const source = config?.initialData;
17
- if (!source) {
18
- initial = {};
19
- formData = cloneStateValue(initial);
20
- formErrors = {};
21
- return cloneStateValue(initial);
19
+ const pending = $derived(status === 'waitingForInitialValues' || status === 'pending');
20
+ const setValues = (values, invalidateSubmission = false) => {
21
+ if (invalidateSubmission) {
22
+ submitRunId += 1;
23
+ status = 'idle';
22
24
  }
23
- const value = source();
24
- const nextInitial = isPromise(value) ? await value : value;
25
- if (runId !== initialLoadRunId)
26
- return cloneStateValue(initial);
27
- initial = cloneStateValue((nextInitial ?? {}));
25
+ initial = cloneStateValue(values);
28
26
  formData = cloneStateValue(initial);
29
27
  formErrors = {};
30
- return cloneStateValue(initial);
31
28
  };
32
- const ready = loadInitialData();
29
+ $effect(() => {
30
+ for (const binding of signalBindings) {
31
+ const status = binding.signal.status;
32
+ const data = binding.signal.data;
33
+ const shouldUpdate = binding.active && status === 'success' && (binding.lastStatus !== 'success' || !Object.is(binding.lastData, data));
34
+ binding.lastStatus = status;
35
+ binding.lastData = data;
36
+ if (shouldUpdate)
37
+ setValues(binding.map(data), true);
38
+ }
39
+ });
40
+ const loadInitialValues = async () => {
41
+ const runId = ++initialLoadRunId;
42
+ const source = config?.initialValues;
43
+ initialValuesError = NO_INITIAL_VALUES_ERROR;
44
+ if (!source) {
45
+ bindingsBySignal?.clear();
46
+ bindingsBySignal = undefined;
47
+ signalBindings = [];
48
+ setValues({});
49
+ status = 'idle';
50
+ return;
51
+ }
52
+ try {
53
+ const linkPromise = (async (promiseSource, map) => {
54
+ const value = await promiseSource();
55
+ return map ? await map(value) : value;
56
+ });
57
+ const linkSignal = (async (signal, options = {}) => {
58
+ const map = options.mapValues ?? ((value) => value);
59
+ let binding;
60
+ if (options.watch && runId === initialLoadRunId) {
61
+ const untypedSignal = signal;
62
+ const bindings = (bindingsBySignal ??= new Map());
63
+ binding = bindings.get(untypedSignal);
64
+ if (!binding) {
65
+ binding = {
66
+ signal: untypedSignal,
67
+ map: map,
68
+ active: false,
69
+ loadRunId: runId,
70
+ lastData: signal.data,
71
+ lastStatus: signal.status
72
+ };
73
+ bindings.set(untypedSignal, binding);
74
+ }
75
+ else {
76
+ binding.map = map;
77
+ binding.active = false;
78
+ binding.loadRunId = runId;
79
+ }
80
+ signalBindings = [...bindings.values()];
81
+ }
82
+ if (signal.status === 'idle' || signal.status === 'pending')
83
+ await signal.ready;
84
+ const value = signal.data;
85
+ if (binding && runId === initialLoadRunId && binding.loadRunId === runId) {
86
+ binding.active = true;
87
+ binding.lastData = value;
88
+ binding.lastStatus = signal.status;
89
+ signalBindings = bindingsBySignal ? [...bindingsBySignal.values()] : [];
90
+ }
91
+ return map(value);
92
+ });
93
+ const sourceResult = source({ linkPromise, linkSignal });
94
+ let nextInitial;
95
+ if (isPromiseLike(sourceResult)) {
96
+ status = 'waitingForInitialValues';
97
+ nextInitial = await sourceResult;
98
+ }
99
+ else {
100
+ nextInitial = sourceResult;
101
+ }
102
+ if (runId !== initialLoadRunId)
103
+ return;
104
+ if (bindingsBySignal) {
105
+ for (const [signal, binding] of bindingsBySignal) {
106
+ if (binding.loadRunId !== runId)
107
+ bindingsBySignal.delete(signal);
108
+ }
109
+ if (bindingsBySignal.size === 0)
110
+ bindingsBySignal = undefined;
111
+ }
112
+ signalBindings = bindingsBySignal ? [...bindingsBySignal.values()] : [];
113
+ setValues(nextInitial ?? {});
114
+ status = 'idle';
115
+ }
116
+ catch (error) {
117
+ if (runId === initialLoadRunId) {
118
+ initialValuesError = error;
119
+ status = 'error';
120
+ }
121
+ throw error;
122
+ }
123
+ };
124
+ const startInitialValues = () => {
125
+ const task = loadInitialValues();
126
+ initialValuesTask = task.then(() => undefined, () => undefined);
127
+ return task;
128
+ };
129
+ if (config?.initialValues)
130
+ void startInitialValues();
33
131
  const abortedResponse = () => ({
34
132
  success: false,
35
133
  response: undefined
@@ -37,10 +135,13 @@ export const createActiveForm = (onSubmit, config) => {
37
135
  const resetForm = async (behavior = 'clear', invalidateSubmission = true) => {
38
136
  if (invalidateSubmission) {
39
137
  submitRunId += 1;
40
- pending = false;
138
+ status = 'idle';
41
139
  }
42
- if (behavior !== 'reloadInitial')
140
+ if (behavior !== 'reloadInitial') {
43
141
  initialLoadRunId += 1;
142
+ initialValuesTask = Promise.resolve();
143
+ initialValuesError = NO_INITIAL_VALUES_ERROR;
144
+ }
44
145
  switch (behavior) {
45
146
  case 'clear':
46
147
  formData = {};
@@ -49,7 +150,7 @@ export const createActiveForm = (onSubmit, config) => {
49
150
  formData = cloneStateValue(initial);
50
151
  break;
51
152
  case 'reloadInitial':
52
- await loadInitialData();
153
+ await startInitialValues();
53
154
  break;
54
155
  }
55
156
  formErrors = {};
@@ -57,43 +158,53 @@ export const createActiveForm = (onSubmit, config) => {
57
158
  const reset = async (behavior = 'clear') => resetForm(behavior, true);
58
159
  const submit = async () => {
59
160
  const runId = ++submitRunId;
60
- pending = true;
61
161
  try {
62
- await ready;
162
+ await initialValuesTask;
63
163
  if (runId !== submitRunId)
64
164
  return abortedResponse();
165
+ if (initialValuesError !== NO_INITIAL_VALUES_ERROR)
166
+ throw initialValuesError;
167
+ status = 'pending';
65
168
  if (config?.beforeSubmit) {
66
169
  let aborted = false;
67
- const abort = () => {
68
- aborted = true;
69
- };
70
- await config.beforeSubmit({ form: formApi, abort: () => abort() });
71
- if (aborted || runId !== submitRunId)
170
+ await config.beforeSubmit({
171
+ form: formController,
172
+ abort: () => {
173
+ aborted = true;
174
+ }
175
+ });
176
+ if (aborted) {
177
+ if (runId === submitRunId)
178
+ status = 'idle';
179
+ return abortedResponse();
180
+ }
181
+ if (runId !== submitRunId)
72
182
  return abortedResponse();
73
183
  }
74
- const result = await onSubmit(cloneStateValue(formData));
184
+ const result = (await onSubmit(cloneStateValue(formData)));
75
185
  if (runId !== submitRunId)
76
186
  return result;
77
187
  if (result.success) {
78
- await config?.onSuccess?.(result.response);
79
- if (runId !== submitRunId)
80
- return result;
81
- await resetForm(config?.successBehavior ?? 'default', false);
188
+ await config?.onSuccess?.({ form: activeForm, response: result });
189
+ if (runId === submitRunId)
190
+ status = 'success';
82
191
  }
83
192
  else {
84
- if (result.error?.validation) {
193
+ if (result.error?.validation)
85
194
  formErrors = result.error.validation;
86
- }
87
- await config?.onError?.();
195
+ await config?.onError?.({ form: activeForm, response: result });
196
+ if (runId === submitRunId)
197
+ status = 'error';
88
198
  }
89
199
  return result;
90
200
  }
91
- finally {
201
+ catch (error) {
92
202
  if (runId === submitRunId)
93
- pending = false;
203
+ status = 'error';
204
+ throw error;
94
205
  }
95
206
  };
96
- const formApi = {
207
+ const formController = {
97
208
  get data() {
98
209
  return formData;
99
210
  },
@@ -108,7 +219,7 @@ export const createActiveForm = (onSubmit, config) => {
108
219
  },
109
220
  reset
110
221
  };
111
- return {
222
+ const activeForm = {
112
223
  get data() {
113
224
  return formData;
114
225
  },
@@ -127,8 +238,11 @@ export const createActiveForm = (onSubmit, config) => {
127
238
  get pending() {
128
239
  return pending;
129
240
  },
241
+ get status() {
242
+ return status;
243
+ },
130
244
  submit,
131
- ready,
132
245
  reset
133
246
  };
247
+ return activeForm;
134
248
  };
@@ -1,3 +1,3 @@
1
1
  export { createAsyncSignal, refreshAsyncSignal, refreshAllAsyncSignals, type AsyncSignalOptions, type AsyncSignalSvelte, type AsyncStatus, type AsyncSignalSource } from './async-signal/AsyncSignal.svelte.js';
2
- export { createActiveForm } from './active-form/ActiveForm.svelte.js';
2
+ export { createActiveForm, type ActiveForm, type ActiveFormCallbackContext, type ActiveFormController, type ActiveFormResponse, type ActiveFormStatus, type FormConfig, type InitialValuesHelpers, type LinkPromise, type LinkSignal, type LinkSignalOptions, type ResetBehaviors } from './active-form/ActiveForm.svelte.js';
3
3
  export { createQuery, type QueryController, type CreateQueryOptions } from './query/Query.svelte.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-net/kit",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "license": "MIT",
5
5
  "author": "Pixel1917",
6
6
  "description": "Universal toolkit and library collection, developed as part of the Azure-net ecosystem",