@azure-net/kit 5.3.0 → 5.5.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/dist/infra/response/BaseResponse.d.ts +1 -1
- package/dist/svelte/active-form/ActiveForm.svelte.d.ts +43 -15
- package/dist/svelte/active-form/ActiveForm.svelte.js +156 -42
- package/dist/svelte/async-signal/AsyncSignal.svelte.d.ts +10 -0
- package/dist/svelte/async-signal/AsyncSignal.svelte.js +119 -2
- package/dist/svelte/effect/Effect.svelte.d.ts +4 -0
- package/dist/svelte/effect/Effect.svelte.js +27 -0
- package/dist/svelte/effect/index.d.ts +1 -0
- package/dist/svelte/effect/index.js +1 -0
- package/dist/svelte/index.d.ts +4 -3
- package/dist/svelte/index.js +2 -1
- package/dist/svelte/query/Query.svelte.d.ts +14 -11
- package/dist/svelte/query/Query.svelte.js +78 -68
- package/dist/svelte/query/SearchParamsSync.d.ts +2 -0
- package/dist/svelte/query/SearchParamsSync.js +119 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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<
|
|
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"],
|
|
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
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
79
|
-
if (runId
|
|
80
|
-
|
|
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
|
-
|
|
195
|
+
await config?.onError?.({ form: activeForm, response: result });
|
|
196
|
+
if (runId === submitRunId)
|
|
197
|
+
status = 'error';
|
|
88
198
|
}
|
|
89
199
|
return result;
|
|
90
200
|
}
|
|
91
|
-
|
|
201
|
+
catch (error) {
|
|
92
202
|
if (runId === submitRunId)
|
|
93
|
-
|
|
203
|
+
status = 'error';
|
|
204
|
+
throw error;
|
|
94
205
|
}
|
|
95
206
|
};
|
|
96
|
-
const
|
|
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
|
-
|
|
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
|
};
|
|
@@ -23,6 +23,16 @@ export interface AsyncSignalSvelte<TData, TError = Error> {
|
|
|
23
23
|
reset: () => void;
|
|
24
24
|
abort: () => void;
|
|
25
25
|
}
|
|
26
|
+
export interface AsyncSignalBatchOptions {
|
|
27
|
+
parallel?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface AsyncSignalBatchFactory {
|
|
30
|
+
<TData, TError = Error>(handler: (signal?: AbortSignal) => Promise<TData>, options?: Omit<AsyncSignalOptions<TData, TError>, 'immediate'>): AsyncSignalSvelte<TData, TError>;
|
|
31
|
+
}
|
|
32
|
+
export interface AsyncSignalBatch<TSignals extends readonly AsyncSignalSvelte<unknown, unknown>[]> {
|
|
33
|
+
execute: () => TSignals;
|
|
34
|
+
}
|
|
26
35
|
export declare const createAsyncSignal: <TData, TError = Error>(handler: (signal?: AbortSignal) => Promise<TData>, options?: AsyncSignalOptions<TData, TError>) => AsyncSignalSvelte<TData, TError>;
|
|
36
|
+
export declare const createAsyncSignalBatch: <const TSignals extends readonly AsyncSignalSvelte<unknown, unknown>[]>(factory: (createSignal: AsyncSignalBatchFactory) => TSignals, options?: AsyncSignalBatchOptions) => AsyncSignalBatch<TSignals>;
|
|
27
37
|
export declare const refreshAsyncSignal: (key: string) => Promise<void>;
|
|
28
38
|
export declare const refreshAllAsyncSignals: () => Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { untrack } from 'svelte';
|
|
2
2
|
import { BROWSER } from '../../external/tools/index.js';
|
|
3
|
+
const ASYNC_SIGNAL_BATCH_INTERNAL = Symbol('async-signal-batch-internal');
|
|
3
4
|
const createAsyncSignalManager = () => {
|
|
4
5
|
const instances = BROWSER ? new Map() : undefined;
|
|
5
6
|
const generateUid = () => {
|
|
@@ -56,6 +57,9 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
56
57
|
let currentPromise = null;
|
|
57
58
|
let currentRunId = 0;
|
|
58
59
|
let started = false;
|
|
60
|
+
let batchLocked = false;
|
|
61
|
+
let batchGate = null;
|
|
62
|
+
let batchSkipRequested = false;
|
|
59
63
|
const run = async (runId, source) => {
|
|
60
64
|
const initial = !started;
|
|
61
65
|
started = true;
|
|
@@ -117,7 +121,16 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
117
121
|
currentPromise = localPromise;
|
|
118
122
|
return localPromise;
|
|
119
123
|
};
|
|
124
|
+
const startWhenUnlocked = (source) => {
|
|
125
|
+
if (batchLocked)
|
|
126
|
+
return batchGate ?? Promise.resolve(data);
|
|
127
|
+
return start(source);
|
|
128
|
+
};
|
|
120
129
|
const execute = async () => {
|
|
130
|
+
if (batchLocked) {
|
|
131
|
+
await batchGate;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
121
134
|
if (currentPromise) {
|
|
122
135
|
await currentPromise;
|
|
123
136
|
return;
|
|
@@ -125,11 +138,15 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
125
138
|
await start('manual');
|
|
126
139
|
};
|
|
127
140
|
const refresh = async () => {
|
|
141
|
+
if (batchLocked) {
|
|
142
|
+
await batchGate;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
128
145
|
await start('manual');
|
|
129
146
|
};
|
|
130
147
|
if (BROWSER) {
|
|
131
148
|
const signalKey = key ?? asyncSignalManager.generateKey();
|
|
132
|
-
const callback = (source) =>
|
|
149
|
+
const callback = (source) => startWhenUnlocked(source);
|
|
133
150
|
asyncSignalManager.register(signalKey, callback);
|
|
134
151
|
$effect(() => {
|
|
135
152
|
return () => {
|
|
@@ -147,7 +164,7 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
147
164
|
void start('auto');
|
|
148
165
|
}
|
|
149
166
|
}
|
|
150
|
-
|
|
167
|
+
const asyncSignal = {
|
|
151
168
|
get data() {
|
|
152
169
|
return data;
|
|
153
170
|
},
|
|
@@ -161,6 +178,8 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
161
178
|
return pending;
|
|
162
179
|
},
|
|
163
180
|
get ready() {
|
|
181
|
+
if (batchLocked)
|
|
182
|
+
return (batchGate ?? Promise.resolve(data));
|
|
164
183
|
if (currentPromise)
|
|
165
184
|
return currentPromise;
|
|
166
185
|
return start('auto');
|
|
@@ -168,6 +187,8 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
168
187
|
execute,
|
|
169
188
|
refresh,
|
|
170
189
|
reset: () => {
|
|
190
|
+
if (batchLocked)
|
|
191
|
+
batchSkipRequested = true;
|
|
171
192
|
currentRunId += 1;
|
|
172
193
|
abortController?.abort();
|
|
173
194
|
abortController = null;
|
|
@@ -177,6 +198,8 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
177
198
|
status = 'idle';
|
|
178
199
|
},
|
|
179
200
|
abort: () => {
|
|
201
|
+
if (batchLocked)
|
|
202
|
+
batchSkipRequested = true;
|
|
180
203
|
currentRunId += 1;
|
|
181
204
|
abortController?.abort();
|
|
182
205
|
abortController = null;
|
|
@@ -185,6 +208,100 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
185
208
|
status = 'idle';
|
|
186
209
|
}
|
|
187
210
|
};
|
|
211
|
+
Object.defineProperty(asyncSignal, ASYNC_SIGNAL_BATCH_INTERNAL, {
|
|
212
|
+
value: {
|
|
213
|
+
lock: (gate) => {
|
|
214
|
+
batchLocked = true;
|
|
215
|
+
batchGate = gate ?? null;
|
|
216
|
+
batchSkipRequested = false;
|
|
217
|
+
},
|
|
218
|
+
unlock: () => {
|
|
219
|
+
batchLocked = false;
|
|
220
|
+
batchGate = null;
|
|
221
|
+
},
|
|
222
|
+
start: (source) => {
|
|
223
|
+
if (batchSkipRequested)
|
|
224
|
+
return Promise.resolve(undefined);
|
|
225
|
+
return start(source);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return asyncSignal;
|
|
230
|
+
};
|
|
231
|
+
const createDeferred = () => {
|
|
232
|
+
let resolve;
|
|
233
|
+
const promise = new Promise((promiseResolve) => {
|
|
234
|
+
resolve = promiseResolve;
|
|
235
|
+
});
|
|
236
|
+
return { promise, resolve };
|
|
237
|
+
};
|
|
238
|
+
export const createAsyncSignalBatch = (factory, options = {}) => {
|
|
239
|
+
const internals = new Map();
|
|
240
|
+
const createSignal = (handler, signalOptions = {}) => {
|
|
241
|
+
const signal = createAsyncSignal(handler, { ...signalOptions, immediate: false });
|
|
242
|
+
const internal = signal[ASYNC_SIGNAL_BATCH_INTERNAL];
|
|
243
|
+
internal.lock();
|
|
244
|
+
internals.set(signal, internal);
|
|
245
|
+
return signal;
|
|
246
|
+
};
|
|
247
|
+
const signals = factory(createSignal);
|
|
248
|
+
const uniqueSignals = new Set(signals);
|
|
249
|
+
if (uniqueSignals.size !== signals.length || signals.some((signal) => !internals.has(signal))) {
|
|
250
|
+
throw new Error('[AsyncSignalBatch] Factory must return each signal created by createSignal exactly once.');
|
|
251
|
+
}
|
|
252
|
+
if (internals.size !== signals.length) {
|
|
253
|
+
throw new Error('[AsyncSignalBatch] Factory must return every signal created by createSignal.');
|
|
254
|
+
}
|
|
255
|
+
let running = false;
|
|
256
|
+
const execute = () => {
|
|
257
|
+
if (running || signals.length === 0)
|
|
258
|
+
return signals;
|
|
259
|
+
running = true;
|
|
260
|
+
const gates = signals.map(() => createDeferred());
|
|
261
|
+
signals.forEach((signal, index) => {
|
|
262
|
+
internals.get(signal)?.lock(gates[index].promise);
|
|
263
|
+
});
|
|
264
|
+
queueMicrotask(() => {
|
|
265
|
+
if (options.parallel !== false) {
|
|
266
|
+
let remaining = signals.length;
|
|
267
|
+
signals.forEach((signal, index) => {
|
|
268
|
+
const internal = internals.get(signal);
|
|
269
|
+
void internal
|
|
270
|
+
.start('manual')
|
|
271
|
+
.catch(() => undefined)
|
|
272
|
+
.then((result) => {
|
|
273
|
+
gates[index].resolve(result);
|
|
274
|
+
internal.unlock();
|
|
275
|
+
remaining -= 1;
|
|
276
|
+
if (remaining === 0)
|
|
277
|
+
running = false;
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
void (async () => {
|
|
283
|
+
for (let index = 0; index < signals.length; index += 1) {
|
|
284
|
+
const signal = signals[index];
|
|
285
|
+
const internal = internals.get(signal);
|
|
286
|
+
const result = await internal.start('manual').catch(() => undefined);
|
|
287
|
+
gates[index].resolve(result);
|
|
288
|
+
internal.unlock();
|
|
289
|
+
if (signal.status !== 'success') {
|
|
290
|
+
for (let skippedIndex = index + 1; skippedIndex < signals.length; skippedIndex += 1) {
|
|
291
|
+
const skippedSignal = signals[skippedIndex];
|
|
292
|
+
gates[skippedIndex].resolve(undefined);
|
|
293
|
+
internals.get(skippedSignal)?.unlock();
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
})().finally(() => {
|
|
299
|
+
running = false;
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
return signals;
|
|
303
|
+
};
|
|
304
|
+
return { execute };
|
|
188
305
|
};
|
|
189
306
|
export const refreshAsyncSignal = async (key) => {
|
|
190
307
|
return await asyncSignalManager.refreshByKey(key);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type EffectCleanup = () => void;
|
|
2
|
+
export type EffectCallback = () => EffectCleanup | Promise<unknown> | void;
|
|
3
|
+
export type EffectDependency<T = unknown> = () => T;
|
|
4
|
+
export declare const createEffect: (callback: EffectCallback, dependencies: readonly EffectDependency[]) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { untrack } from 'svelte';
|
|
2
|
+
export const createEffect = (callback, dependencies) => {
|
|
3
|
+
let cleanup;
|
|
4
|
+
let initialized = false;
|
|
5
|
+
let previousValues = [];
|
|
6
|
+
$effect(() => {
|
|
7
|
+
const values = dependencies.map((dependency) => dependency());
|
|
8
|
+
const changed = !initialized || values.some((value, index) => !Object.is(value, previousValues[index]));
|
|
9
|
+
if (!changed)
|
|
10
|
+
return;
|
|
11
|
+
initialized = true;
|
|
12
|
+
previousValues = values;
|
|
13
|
+
untrack(() => {
|
|
14
|
+
cleanup?.();
|
|
15
|
+
cleanup = undefined;
|
|
16
|
+
const result = callback();
|
|
17
|
+
if (typeof result === 'function')
|
|
18
|
+
cleanup = result;
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
$effect(() => {
|
|
22
|
+
return () => {
|
|
23
|
+
untrack(() => cleanup?.());
|
|
24
|
+
cleanup = undefined;
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Effect.svelte.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Effect.svelte.js';
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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';
|
|
3
|
-
export {
|
|
1
|
+
export { createAsyncSignal, createAsyncSignalBatch, refreshAsyncSignal, refreshAllAsyncSignals, type AsyncSignalBatch, type AsyncSignalBatchFactory, type AsyncSignalBatchOptions, type AsyncSignalOptions, type AsyncSignalSvelte, type AsyncStatus, type AsyncSignalSource } from './async-signal/AsyncSignal.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
|
+
export { createEffect, type EffectCallback, type EffectCleanup, type EffectDependency } from './effect/Effect.svelte.js';
|
|
4
|
+
export { createQuery, type CreateQueryOptions, type QueryController, type QueryPath, type QuerySearchParamsSync } from './query/Query.svelte.js';
|
package/dist/svelte/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { createAsyncSignal, refreshAsyncSignal, refreshAllAsyncSignals } from './async-signal/AsyncSignal.svelte.js';
|
|
1
|
+
export { createAsyncSignal, createAsyncSignalBatch, refreshAsyncSignal, refreshAllAsyncSignals } from './async-signal/AsyncSignal.svelte.js';
|
|
2
2
|
export { createActiveForm } from './active-form/ActiveForm.svelte.js';
|
|
3
|
+
export { createEffect } from './effect/Effect.svelte.js';
|
|
3
4
|
export { createQuery } from './query/Query.svelte.js';
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { type EffectCallback } from '../effect/Effect.svelte.js';
|
|
2
|
+
type QueryPathDepth = readonly [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown];
|
|
3
|
+
type QueryLeaf = bigint | boolean | Date | File | Blob | ((...args: never[]) => unknown) | Map<unknown, unknown> | null | number | readonly unknown[] | RegExp | Set<unknown> | string | symbol | undefined | URL | URLSearchParams;
|
|
4
|
+
export type QueryPath<T, Depth extends readonly unknown[] = QueryPathDepth> = Depth extends readonly [unknown, ...infer Rest] ? T extends QueryLeaf ? never : T extends object ? {
|
|
5
|
+
[K in Extract<keyof T, string>]: NonNullable<T[K]> extends QueryLeaf ? K : K | `${K}.${QueryPath<NonNullable<T[K]>, Rest>}`;
|
|
6
|
+
}[Extract<keyof T, string>] : never : never;
|
|
7
|
+
export interface QuerySearchParamsSync<T extends Record<string, unknown>> {
|
|
8
|
+
fromSearchParams?: (params: URLSearchParams, initial: Readonly<T>) => Partial<T>;
|
|
9
|
+
toSearchParams?: (data: Readonly<T>, initial: Readonly<T>) => URLSearchParams;
|
|
9
10
|
}
|
|
10
11
|
export interface CreateQueryOptions<T extends Record<string, unknown>> {
|
|
11
|
-
|
|
12
|
+
syncWithSearchParams?: boolean | QuerySearchParamsSync<T>;
|
|
12
13
|
}
|
|
13
14
|
export interface QueryController<T extends Record<string, unknown>> {
|
|
14
15
|
data: T;
|
|
15
16
|
patch: (value: Partial<T>) => void;
|
|
16
17
|
reset: () => void;
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
createSnapshot: () => T;
|
|
19
|
+
getInitial: () => T;
|
|
20
|
+
createEffect: (callback: EffectCallback, dependencies?: readonly QueryPath<T>[]) => void;
|
|
19
21
|
}
|
|
20
22
|
export declare const createQuery: <T extends Record<string, unknown>>(initialValue: T, options?: CreateQueryOptions<T>) => QueryController<T>;
|
|
23
|
+
export {};
|
|
@@ -1,97 +1,106 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { replaceState } from '$app/navigation';
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import { createEffect } from '../effect/Effect.svelte.js';
|
|
3
4
|
import { cloneStateValue } from '../shared/cloneStateValue.js';
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
import { fromSearchParams, toSearchParams } from './SearchParamsSync.js';
|
|
6
|
+
const trackDeep = (value, seen) => {
|
|
7
|
+
if (value === null || typeof value !== 'object')
|
|
6
8
|
return;
|
|
7
|
-
seen
|
|
9
|
+
const visited = seen ?? new WeakSet();
|
|
10
|
+
if (visited.has(value))
|
|
11
|
+
return;
|
|
12
|
+
visited.add(value);
|
|
8
13
|
if (Array.isArray(value)) {
|
|
9
14
|
for (const item of value)
|
|
10
|
-
trackDeep(item,
|
|
15
|
+
trackDeep(item, visited);
|
|
11
16
|
return;
|
|
12
17
|
}
|
|
13
18
|
if (value instanceof Map) {
|
|
14
19
|
for (const [key, item] of value) {
|
|
15
|
-
trackDeep(key,
|
|
16
|
-
trackDeep(item,
|
|
20
|
+
trackDeep(key, visited);
|
|
21
|
+
trackDeep(item, visited);
|
|
17
22
|
}
|
|
18
23
|
return;
|
|
19
24
|
}
|
|
20
25
|
if (value instanceof Set) {
|
|
21
26
|
for (const item of value)
|
|
22
|
-
trackDeep(item,
|
|
27
|
+
trackDeep(item, visited);
|
|
23
28
|
return;
|
|
24
29
|
}
|
|
25
30
|
const prototype = Object.getPrototypeOf(value);
|
|
26
31
|
if (prototype !== Object.prototype && prototype !== null)
|
|
27
32
|
return;
|
|
28
|
-
for (const key of Reflect.ownKeys(value))
|
|
29
|
-
trackDeep(Reflect.get(value, key),
|
|
33
|
+
for (const key of Reflect.ownKeys(value))
|
|
34
|
+
trackDeep(Reflect.get(value, key), visited);
|
|
35
|
+
};
|
|
36
|
+
const readPath = (source, path) => {
|
|
37
|
+
let value = source;
|
|
38
|
+
for (const key of path.split('.')) {
|
|
39
|
+
if (value === null || typeof value !== 'object')
|
|
40
|
+
return undefined;
|
|
41
|
+
value = Reflect.get(value, key);
|
|
30
42
|
}
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
const createDeepDependency = (read) => {
|
|
46
|
+
let version = 0;
|
|
47
|
+
return () => {
|
|
48
|
+
const value = read();
|
|
49
|
+
if (value === null || typeof value !== 'object')
|
|
50
|
+
return value;
|
|
51
|
+
trackDeep(value);
|
|
52
|
+
return ++version;
|
|
53
|
+
};
|
|
31
54
|
};
|
|
32
55
|
export const createQuery = (initialValue, options) => {
|
|
33
56
|
const baseInitial = cloneStateValue(initialValue);
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
includeKeys.forEach((key) => {
|
|
42
|
-
lastSnapshots.set(key, cloneStateValue(data[key]));
|
|
43
|
-
});
|
|
44
|
-
$effect(() => {
|
|
45
|
-
const changedKeys = [];
|
|
46
|
-
includeKeys.forEach((key) => {
|
|
47
|
-
const nextValue = data[key];
|
|
48
|
-
trackDeep(nextValue);
|
|
49
|
-
const nextSnapshot = cloneStateValue(nextValue);
|
|
50
|
-
if (!ObjectUtil.equals(lastSnapshots.get(key), nextSnapshot)) {
|
|
51
|
-
changedKeys.push(key);
|
|
52
|
-
lastSnapshots.set(key, nextSnapshot);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
if (!changedKeys.length)
|
|
56
|
-
return;
|
|
57
|
-
const previousController = onChangeController;
|
|
58
|
-
if (previousController)
|
|
59
|
-
previousController.abort();
|
|
60
|
-
const localController = new AbortController();
|
|
61
|
-
onChangeController = localController;
|
|
62
|
-
const localRunId = ++onChangeRunId;
|
|
63
|
-
const run = async () => {
|
|
64
|
-
await options.onChange.handler({
|
|
65
|
-
changedKeys,
|
|
66
|
-
signal: localController.signal,
|
|
67
|
-
snapshot: cloneStateValue(data)
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
untrack(() => {
|
|
71
|
-
void run()
|
|
72
|
-
.catch(() => undefined)
|
|
73
|
-
.finally(() => {
|
|
74
|
-
if (onChangeRunId === localRunId && onChangeController === localController) {
|
|
75
|
-
onChangeController = null;
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
$effect(() => {
|
|
81
|
-
return () => {
|
|
82
|
-
if (onChangeController)
|
|
83
|
-
onChangeController.abort();
|
|
84
|
-
};
|
|
85
|
-
});
|
|
86
|
-
}
|
|
57
|
+
const searchParamsSync = options?.syncWithSearchParams;
|
|
58
|
+
const syncSettings = typeof searchParamsSync === 'object' ? searchParamsSync : undefined;
|
|
59
|
+
const syncEnabled = Boolean(searchParamsSync);
|
|
60
|
+
const parseSearchParams = syncSettings?.fromSearchParams ?? (fromSearchParams);
|
|
61
|
+
const serializeSearchParams = syncSettings?.toSearchParams ?? (toSearchParams);
|
|
62
|
+
const parsedInitial = syncEnabled ? parseSearchParams(new URLSearchParams(page.url.searchParams), cloneStateValue(baseInitial)) : undefined;
|
|
63
|
+
let data = $state(parsedInitial ? { ...cloneStateValue(baseInitial), ...cloneStateValue(parsedInitial) } : cloneStateValue(baseInitial));
|
|
87
64
|
const reset = () => {
|
|
88
65
|
data = cloneStateValue(baseInitial);
|
|
89
66
|
};
|
|
90
67
|
const patch = (value) => {
|
|
91
68
|
data = { ...data, ...value };
|
|
92
69
|
};
|
|
93
|
-
const
|
|
94
|
-
const
|
|
70
|
+
const createSnapshot = () => cloneStateValue(data);
|
|
71
|
+
const getInitial = () => cloneStateValue(baseInitial);
|
|
72
|
+
const createQueryEffect = (callback, dependencies) => {
|
|
73
|
+
const effectDependencies = dependencies === undefined
|
|
74
|
+
? [createDeepDependency(() => data)]
|
|
75
|
+
: [...new Set(dependencies)].map((path) => createDeepDependency(() => readPath(data, path)));
|
|
76
|
+
createEffect(callback, effectDependencies);
|
|
77
|
+
};
|
|
78
|
+
if (syncEnabled) {
|
|
79
|
+
const managedKeys = new Set(Object.keys(baseInitial));
|
|
80
|
+
const captureInitialSyncedData = () => cloneStateValue(data);
|
|
81
|
+
const initialSyncedData = captureInitialSyncedData();
|
|
82
|
+
let initialized = false;
|
|
83
|
+
createEffect(() => {
|
|
84
|
+
const nextParams = serializeSearchParams(data, baseInitial);
|
|
85
|
+
for (const key of nextParams.keys())
|
|
86
|
+
managedKeys.add(key);
|
|
87
|
+
if (!initialized) {
|
|
88
|
+
initialized = true;
|
|
89
|
+
const initialParams = serializeSearchParams(initialSyncedData, baseInitial);
|
|
90
|
+
for (const key of initialParams.keys())
|
|
91
|
+
managedKeys.add(key);
|
|
92
|
+
if (nextParams.toString() === initialParams.toString())
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const url = new URL(page.url);
|
|
96
|
+
for (const key of managedKeys)
|
|
97
|
+
url.searchParams.delete(key);
|
|
98
|
+
for (const [key, value] of nextParams)
|
|
99
|
+
url.searchParams.append(key, value);
|
|
100
|
+
if (url.search !== page.url.search)
|
|
101
|
+
replaceState(url, page.state);
|
|
102
|
+
}, [createDeepDependency(() => data)]);
|
|
103
|
+
}
|
|
95
104
|
return {
|
|
96
105
|
get data() {
|
|
97
106
|
return data;
|
|
@@ -101,7 +110,8 @@ export const createQuery = (initialValue, options) => {
|
|
|
101
110
|
},
|
|
102
111
|
patch,
|
|
103
112
|
reset,
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
createSnapshot,
|
|
114
|
+
getInitial,
|
|
115
|
+
createEffect: createQueryEffect
|
|
106
116
|
};
|
|
107
117
|
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const INVALID_VALUE = Symbol('invalid-query-search-param');
|
|
2
|
+
const isPlainObject = (value) => {
|
|
3
|
+
if (value === null || typeof value !== 'object')
|
|
4
|
+
return false;
|
|
5
|
+
const prototype = Object.getPrototypeOf(value);
|
|
6
|
+
return prototype === Object.prototype || prototype === null;
|
|
7
|
+
};
|
|
8
|
+
const parseScalar = (value, initial) => {
|
|
9
|
+
if (typeof initial === 'string')
|
|
10
|
+
return value;
|
|
11
|
+
if (typeof initial === 'number') {
|
|
12
|
+
if (!value.trim())
|
|
13
|
+
return INVALID_VALUE;
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
return Number.isFinite(parsed) ? parsed : INVALID_VALUE;
|
|
16
|
+
}
|
|
17
|
+
if (typeof initial === 'boolean') {
|
|
18
|
+
const normalized = value.toLowerCase();
|
|
19
|
+
if (normalized === 'true' || normalized === '1')
|
|
20
|
+
return true;
|
|
21
|
+
if (normalized === 'false' || normalized === '0')
|
|
22
|
+
return false;
|
|
23
|
+
return INVALID_VALUE;
|
|
24
|
+
}
|
|
25
|
+
if (typeof initial === 'bigint') {
|
|
26
|
+
if (!value.trim())
|
|
27
|
+
return INVALID_VALUE;
|
|
28
|
+
try {
|
|
29
|
+
return BigInt(value);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return INVALID_VALUE;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (initial instanceof Date) {
|
|
36
|
+
const parsed = new Date(value);
|
|
37
|
+
return Number.isNaN(parsed.getTime()) ? INVALID_VALUE : parsed;
|
|
38
|
+
}
|
|
39
|
+
if (initial instanceof URL) {
|
|
40
|
+
try {
|
|
41
|
+
return new URL(value, initial);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return INVALID_VALUE;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (initial instanceof URLSearchParams)
|
|
48
|
+
return new URLSearchParams(value);
|
|
49
|
+
if (isPlainObject(initial)) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(value);
|
|
52
|
+
return isPlainObject(parsed) ? parsed : INVALID_VALUE;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return INVALID_VALUE;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return INVALID_VALUE;
|
|
59
|
+
};
|
|
60
|
+
const parseValue = (params, key, initial) => {
|
|
61
|
+
if (Array.isArray(initial)) {
|
|
62
|
+
const values = params.getAll(key);
|
|
63
|
+
if (!values.length)
|
|
64
|
+
return INVALID_VALUE;
|
|
65
|
+
if (!initial.length)
|
|
66
|
+
return values;
|
|
67
|
+
const parsed = values.map((value) => parseScalar(value, initial[0]));
|
|
68
|
+
return parsed.some((value) => value === INVALID_VALUE) ? INVALID_VALUE : parsed;
|
|
69
|
+
}
|
|
70
|
+
const value = params.get(key);
|
|
71
|
+
return value === null ? INVALID_VALUE : parseScalar(value, initial);
|
|
72
|
+
};
|
|
73
|
+
const serializeScalar = (value) => {
|
|
74
|
+
if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'bigint')
|
|
75
|
+
return String(value);
|
|
76
|
+
if (typeof value === 'number')
|
|
77
|
+
return Number.isFinite(value) ? String(value) : undefined;
|
|
78
|
+
if (value instanceof Date)
|
|
79
|
+
return Number.isNaN(value.getTime()) ? undefined : value.toISOString();
|
|
80
|
+
if (value instanceof URL || value instanceof URLSearchParams)
|
|
81
|
+
return value.toString();
|
|
82
|
+
if (isPlainObject(value)) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.stringify(value);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
};
|
|
92
|
+
export const fromSearchParams = (params, initial) => {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const key of Object.keys(initial)) {
|
|
95
|
+
if (!params.has(key))
|
|
96
|
+
continue;
|
|
97
|
+
const parsed = parseValue(params, key, initial[key]);
|
|
98
|
+
if (parsed !== INVALID_VALUE)
|
|
99
|
+
Reflect.set(result, key, parsed);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
export const toSearchParams = (data) => {
|
|
104
|
+
const params = new URLSearchParams();
|
|
105
|
+
for (const [key, value] of Object.entries(data)) {
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
for (const item of value) {
|
|
108
|
+
const serialized = serializeScalar(item);
|
|
109
|
+
if (serialized !== undefined)
|
|
110
|
+
params.append(key, serialized);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const serialized = serializeScalar(value);
|
|
115
|
+
if (serialized !== undefined)
|
|
116
|
+
params.set(key, serialized);
|
|
117
|
+
}
|
|
118
|
+
return params;
|
|
119
|
+
};
|