@azure-net/kit 5.4.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/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 +3 -2
- 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
|
@@ -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';
|
|
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
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 {
|
|
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
|
+
};
|