@echojs-ecosystem/url-state 0.1.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/index.js ADDED
@@ -0,0 +1,993 @@
1
+ import { signal } from '@echojs-ecosystem/reactivity';
2
+
3
+ // src/utils/equality.ts
4
+ var defaultEquals = (a, b) => Object.is(a, b);
5
+
6
+ // src/core/parser-meta.ts
7
+ var hasDefault = (parser) => Object.prototype.hasOwnProperty.call(parser, "defaultValue");
8
+
9
+ // src/core/default-visibility.ts
10
+ var isVisibilityMode = (value) => value === "hide" || value === "show";
11
+ var resolveDefaultVisibility = (options) => {
12
+ if (isVisibilityMode(options.defaultVisibility)) return options.defaultVisibility;
13
+ if (isVisibilityMode(options.defaultParamsBehavior)) return options.defaultParamsBehavior;
14
+ if (options.clearOnDefault === false) return "show";
15
+ if (options.clearOnDefault === true) return "hide";
16
+ return "hide";
17
+ };
18
+ var shouldHideDefaultInUrl = (value, parser, options) => {
19
+ if (!hasDefault(parser)) return false;
20
+ if (resolveDefaultVisibility(options) === "show") return false;
21
+ const parserEq = parser.eq;
22
+ const equals = options.equals === false ? null : parserEq ?? options.equals ?? defaultEquals;
23
+ return equals ? equals(value, parser.defaultValue) : false;
24
+ };
25
+
26
+ // src/core/options.ts
27
+ var DEFAULT_QUERY_STATE_OPTIONS = {
28
+ history: "replace",
29
+ clearOnDefault: true,
30
+ shallow: true,
31
+ scroll: false,
32
+ limitUrlUpdates: false
33
+ };
34
+ var resolveSetOptions = (args) => {
35
+ const merged = {
36
+ ...DEFAULT_QUERY_STATE_OPTIONS,
37
+ ...args.createOptions ?? {},
38
+ ...args.parserOptions ?? {},
39
+ ...args.setOptions ?? {}
40
+ };
41
+ if (merged.defaultVisibility === "show") merged.clearOnDefault = false;
42
+ else if (merged.defaultVisibility === "hide") merged.clearOnDefault = true;
43
+ return merged;
44
+ };
45
+
46
+ // src/core/url.ts
47
+ var normalizeSearch = (search) => {
48
+ if (!search) return "";
49
+ if (search === "?") return "";
50
+ return search.startsWith("?") ? search : `?${search}`;
51
+ };
52
+ var parseSearch = (search) => new URLSearchParams(normalizeSearch(search));
53
+ var paramsToMap = (params) => {
54
+ const map = /* @__PURE__ */ new Map();
55
+ for (const [key, value] of params.entries()) {
56
+ const arr = map.get(key);
57
+ if (arr) arr.push(value);
58
+ else map.set(key, [value]);
59
+ }
60
+ return map;
61
+ };
62
+ var stringifySearch = (params) => {
63
+ const map = paramsToMap(params);
64
+ const keys = [...map.keys()].sort();
65
+ const next = new URLSearchParams();
66
+ for (const key of keys) {
67
+ const values = map.get(key);
68
+ for (const value of values) next.append(key, value);
69
+ }
70
+ const str = next.toString();
71
+ return str ? `?${str}` : "";
72
+ };
73
+ var getSearchParam = (search, key) => {
74
+ const params = parseSearch(search);
75
+ const all = params.getAll(key);
76
+ if (all.length === 0) return null;
77
+ if (all.length === 1) return all[0];
78
+ return all;
79
+ };
80
+ var setSearchParam = (search, key, value) => {
81
+ const params = parseSearch(search);
82
+ params.delete(key);
83
+ if (value === null) return stringifySearch(params);
84
+ if (Array.isArray(value)) {
85
+ for (const item of value) params.append(key, item);
86
+ } else {
87
+ params.set(key, value);
88
+ }
89
+ return stringifySearch(params);
90
+ };
91
+
92
+ // src/core/update-queue.ts
93
+ var queues = /* @__PURE__ */ new WeakMap();
94
+ var getQueue = (adapter) => {
95
+ const existing = queues.get(adapter);
96
+ if (existing) return existing;
97
+ const next = { pending: null, microtaskQueued: false };
98
+ queues.set(adapter, next);
99
+ return next;
100
+ };
101
+ var mergePending = (prev, next) => {
102
+ if (!prev) return next;
103
+ const history = prev.options.history === "push" || next.options.history === "push" ? "push" : next.options.history ?? prev.options.history;
104
+ return {
105
+ ops: [...prev.ops, ...next.ops],
106
+ options: {
107
+ ...prev.options,
108
+ ...next.options,
109
+ history
110
+ }
111
+ };
112
+ };
113
+ var queueUrlUpdate = (adapter, ops, options) => {
114
+ const state = getQueue(adapter);
115
+ state.pending = mergePending(state.pending, { ops, options });
116
+ if (state.microtaskQueued) return;
117
+ state.microtaskQueued = true;
118
+ queueMicrotask(() => {
119
+ state.microtaskQueued = false;
120
+ const pending = state.pending;
121
+ state.pending = null;
122
+ if (!pending) return;
123
+ const doSet = () => {
124
+ let nextSearch = adapter.getSearch();
125
+ for (const op of pending.ops) {
126
+ nextSearch = setSearchParam(nextSearch, op.key, op.value);
127
+ }
128
+ adapter.setSearch(nextSearch, {
129
+ history: pending.options.history,
130
+ shallow: pending.options.shallow,
131
+ scroll: pending.options.scroll
132
+ });
133
+ };
134
+ const limiter = pending.options.limitUrlUpdates;
135
+ if (limiter) limiter.schedule(doSet);
136
+ else doSet();
137
+ });
138
+ };
139
+
140
+ // src/core/query-state.ts
141
+ var parseRaw = (parser, raw) => {
142
+ const parsed = parser.parse(raw);
143
+ if (parsed === null && hasDefault(parser)) return parser.defaultValue;
144
+ return parsed;
145
+ };
146
+ var rawEquals = (a, b) => {
147
+ if (a === b) return true;
148
+ if (a === null || b === null) return false;
149
+ if (Array.isArray(a) && Array.isArray(b)) {
150
+ if (a.length !== b.length) return false;
151
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
152
+ return true;
153
+ }
154
+ return false;
155
+ };
156
+ var readRawValue = (adapter, key) => {
157
+ return getSearchParam(adapter.getSearch(), key);
158
+ };
159
+ var createSyncedSignals = (args) => {
160
+ const getOptions = (setOptions) => resolveSetOptions({ createOptions: args.options, parserOptions: args.parser.options, setOptions });
161
+ const initialRaw = readRawValue(args.adapter, args.key);
162
+ const initialParsed = parseRaw(args.parser, initialRaw);
163
+ const $value = signal(initialParsed ?? (hasDefault(args.parser) ? args.parser.defaultValue : null));
164
+ const $rawValue = signal(initialRaw);
165
+ const syncFromAdapter = () => {
166
+ const adapterRaw = readRawValue(args.adapter, args.key);
167
+ if (!rawEquals(adapterRaw, $rawValue.peek())) $rawValue.set(adapterRaw);
168
+ const parsed = parseRaw(args.parser, adapterRaw);
169
+ const next = parsed ?? (hasDefault(args.parser) ? args.parser.defaultValue : null);
170
+ const equals = getOptions().equals === false ? null : getOptions().equals ?? defaultEquals;
171
+ if (!equals || !equals(next, $value.peek())) $value.set(next);
172
+ };
173
+ const unsubscribe = args.adapter.subscribe(syncFromAdapter);
174
+ const write = (next, setOptions) => {
175
+ const options = getOptions(setOptions);
176
+ const parsed = parseRaw(args.parser, next);
177
+ const nextValue = parsed ?? (hasDefault(args.parser) ? args.parser.defaultValue : null);
178
+ const equals = options.equals === false ? null : options.equals ?? defaultEquals;
179
+ if (!rawEquals(next, $rawValue.peek())) $rawValue.set(next);
180
+ if (!equals || !equals(nextValue, $value.peek())) $value.set(nextValue);
181
+ queueUrlUpdate(args.adapter, [{ key: args.key, value: next }], options);
182
+ };
183
+ return {
184
+ $rawValue,
185
+ $value,
186
+ getOptions,
187
+ write,
188
+ dispose: unsubscribe
189
+ };
190
+ };
191
+
192
+ // src/adapters/browser-adapter.ts
193
+ var createBrowserUrlStateAdapter = () => {
194
+ if (typeof window === "undefined" || typeof window.history === "undefined" || typeof window.location === "undefined") {
195
+ throw new Error("@echojs-ecosystem/url-state: Browser adapter requires `window`");
196
+ }
197
+ const listeners = /* @__PURE__ */ new Set();
198
+ const notify = () => {
199
+ for (const listener of listeners) listener();
200
+ };
201
+ window.addEventListener("popstate", notify);
202
+ return {
203
+ kind: "browser",
204
+ getSearch() {
205
+ return normalizeSearch(window.location.search);
206
+ },
207
+ setSearch(search, options) {
208
+ const normalized = normalizeSearch(search);
209
+ const pathname = window.location.pathname;
210
+ const hash = window.location.hash ?? "";
211
+ const nextUrl = `${pathname}${normalized}${hash}`;
212
+ const historyMode = options?.history ?? "replace";
213
+ if (historyMode === "push") window.history.pushState({}, "", nextUrl);
214
+ else window.history.replaceState({}, "", nextUrl);
215
+ notify();
216
+ },
217
+ subscribe(listener) {
218
+ listeners.add(listener);
219
+ return () => {
220
+ listeners.delete(listener);
221
+ if (listeners.size === 0) ;
222
+ };
223
+ }
224
+ };
225
+ };
226
+
227
+ // src/adapters/memory-adapter.ts
228
+ var createMemoryUrlStateAdapter = (initialSearch = "") => {
229
+ let search = normalizeSearch(initialSearch);
230
+ const listeners = /* @__PURE__ */ new Set();
231
+ return {
232
+ kind: "memory",
233
+ getSearch() {
234
+ return search;
235
+ },
236
+ setSearch(next) {
237
+ const normalized = normalizeSearch(next);
238
+ if (normalized === search) return;
239
+ search = normalized;
240
+ for (const listener of listeners) listener();
241
+ },
242
+ subscribe(listener) {
243
+ listeners.add(listener);
244
+ return () => listeners.delete(listener);
245
+ }
246
+ };
247
+ };
248
+
249
+ // src/adapters/adapter.ts
250
+ var getDefaultUrlStateAdapter = () => {
251
+ if (typeof window !== "undefined" && typeof window.location !== "undefined") {
252
+ return createBrowserUrlStateAdapter();
253
+ }
254
+ return createMemoryUrlStateAdapter("");
255
+ };
256
+
257
+ // src/core/create-query-param.ts
258
+ var createQueryParam = (key, parser, options = { defaultVisibility: "show" }) => {
259
+ const adapter = options.adapter ?? getDefaultUrlStateAdapter();
260
+ const state = createSyncedSignals({
261
+ adapter,
262
+ key,
263
+ parser,
264
+ options
265
+ });
266
+ const subscribe = (listener) => {
267
+ let prev = state.$value.peek();
268
+ return state.$value.subscribe(() => {
269
+ const next = state.$value.peek();
270
+ if (Object.is(next, prev)) return;
271
+ const p = prev;
272
+ prev = next;
273
+ listener(next, p);
274
+ });
275
+ };
276
+ const set = (value, setOptions) => {
277
+ const resolved = state.getOptions(setOptions);
278
+ if (value === null) {
279
+ state.write(null, resolved);
280
+ return;
281
+ }
282
+ if (shouldHideDefaultInUrl(value, parser, resolved)) {
283
+ state.write(null, resolved);
284
+ return;
285
+ }
286
+ const raw = parser.serialize(value);
287
+ state.write(raw, resolved);
288
+ };
289
+ const update = (updater, setOptions) => {
290
+ const current = state.$value.peek();
291
+ set(updater(current), setOptions);
292
+ };
293
+ const reset = (setOptions) => {
294
+ if (hasDefault(parser)) set(parser.defaultValue, setOptions);
295
+ else set(null, setOptions);
296
+ };
297
+ const clear = (setOptions) => set(null, setOptions);
298
+ return {
299
+ kind: "query-param",
300
+ key,
301
+ value: () => state.$value.value(),
302
+ set,
303
+ update,
304
+ reset,
305
+ clear,
306
+ $value: state.$value,
307
+ $rawValue: state.$rawValue,
308
+ subscribe
309
+ };
310
+ };
311
+
312
+ // src/adapters/router-adapter.ts
313
+ var splitFullPath = (fullPath) => {
314
+ const hashIndex = fullPath.indexOf("#");
315
+ const beforeHash = hashIndex >= 0 ? fullPath.slice(0, hashIndex) : fullPath;
316
+ const hash = hashIndex >= 0 ? fullPath.slice(hashIndex) : "";
317
+ const qIndex = beforeHash.indexOf("?");
318
+ const pathname = qIndex >= 0 ? beforeHash.slice(0, qIndex) : beforeHash;
319
+ const search = qIndex >= 0 ? beforeHash.slice(qIndex) : "";
320
+ return { pathname: pathname || "/", search: normalizeSearch(search), hash };
321
+ };
322
+ var createRouterUrlStateAdapter = (router) => {
323
+ if (!router) return void 0;
324
+ return {
325
+ kind: "router",
326
+ getSearch() {
327
+ const { search } = splitFullPath(router.$fullPath.value());
328
+ return search;
329
+ },
330
+ setSearch(search, options) {
331
+ const { pathname, hash } = splitFullPath(router.$fullPath.value());
332
+ const normalized = normalizeSearch(search);
333
+ const next = `${pathname}${normalized}${hash}`;
334
+ const historyMode = options?.history ?? "replace";
335
+ if (historyMode === "replace" && router.replace) router.replace(next);
336
+ else router.go(next, { replace: historyMode === "replace" });
337
+ },
338
+ subscribe(listener) {
339
+ return router.$fullPath.subscribe(listener);
340
+ }
341
+ };
342
+ };
343
+
344
+ // src/adapters/router-registry.ts
345
+ var activeRouter = null;
346
+ var onRouterReady = [];
347
+ var pendingSubscriptions = [];
348
+ var registerUrlStateRouter = (router) => {
349
+ activeRouter = router;
350
+ const adapter = createRouterUrlStateAdapter(router);
351
+ for (const pending of pendingSubscriptions) {
352
+ pending.dispose = adapter.subscribe(pending.listener);
353
+ pending.listener();
354
+ }
355
+ for (const sync of onRouterReady) sync();
356
+ onRouterReady.length = 0;
357
+ };
358
+ var getUrlStateRouter = () => activeRouter;
359
+ var onUrlStateRouterReady = (listener) => {
360
+ if (activeRouter) {
361
+ listener();
362
+ return () => {
363
+ };
364
+ }
365
+ onRouterReady.push(listener);
366
+ return () => {
367
+ const index = onRouterReady.indexOf(listener);
368
+ if (index >= 0) onRouterReady.splice(index, 1);
369
+ };
370
+ };
371
+
372
+ // src/adapters/auto-router-adapter.ts
373
+ var createAutoRouterUrlStateAdapter = () => {
374
+ const fallback = getDefaultUrlStateAdapter();
375
+ const activeAdapter = () => {
376
+ const router = getUrlStateRouter();
377
+ if (router) return createRouterUrlStateAdapter(router);
378
+ return fallback;
379
+ };
380
+ return {
381
+ kind: "auto-router",
382
+ getSearch: () => activeAdapter().getSearch(),
383
+ setSearch: (search, options) => activeAdapter().setSearch(search, options),
384
+ subscribe(listener) {
385
+ let unsub = activeAdapter().subscribe(listener);
386
+ const offReady = onUrlStateRouterReady(() => {
387
+ unsub();
388
+ listener();
389
+ unsub = activeAdapter().subscribe(listener);
390
+ });
391
+ return () => {
392
+ offReady();
393
+ unsub();
394
+ };
395
+ }
396
+ };
397
+ };
398
+
399
+ // src/core/create-query-params.ts
400
+ var getUrlKey = (schemaKey, options) => {
401
+ const override = options?.urlKeys?.[schemaKey];
402
+ return override ?? schemaKey;
403
+ };
404
+ var createQueryParams = (schema, options = {}) => {
405
+ const adapter = options.adapter ?? createAutoRouterUrlStateAdapter();
406
+ const usesAutoRouter = options.adapter === void 0;
407
+ const resolveOptions = (setOptions) => resolveSetOptions({ createOptions: options, parserOptions: void 0, setOptions });
408
+ const readSchema = () => {
409
+ const search = adapter.getSearch();
410
+ const out = {};
411
+ for (const key of Object.keys(schema)) {
412
+ const parser = schema[key];
413
+ const urlKey = getUrlKey(key, options);
414
+ const raw = getSearchParam(search, urlKey);
415
+ const parsed = parseRaw(parser, raw);
416
+ out[key] = parsed ?? (hasDefault(parser) ? parser.defaultValue : null);
417
+ }
418
+ return out;
419
+ };
420
+ const $value = signal(readSchema());
421
+ const syncFromAdapter = () => {
422
+ const next = readSchema();
423
+ const equals = resolveOptions().equals === false ? null : resolveOptions().equals ?? defaultEquals;
424
+ if (!equals || !equals(next, $value.peek())) $value.set(next);
425
+ };
426
+ adapter.subscribe(syncFromAdapter);
427
+ const emit = (next, prev, listener) => {
428
+ if (Object.is(next, prev)) return;
429
+ listener(next, prev);
430
+ };
431
+ const subscribe = (listener) => {
432
+ let prev = $value.peek();
433
+ return $value.subscribe(() => {
434
+ const next = $value.peek();
435
+ const p = prev;
436
+ prev = next;
437
+ emit(next, p, listener);
438
+ });
439
+ };
440
+ const writeOps = (ops, nextValue, setOptions) => {
441
+ const resolved = resolveOptions(setOptions);
442
+ const equals = resolved.equals === false ? null : resolved.equals ?? defaultEquals;
443
+ if (!equals || !equals(nextValue, $value.peek())) $value.set(nextValue);
444
+ queueUrlUpdate(adapter, ops, resolved);
445
+ };
446
+ const resetValue = () => {
447
+ const out = {};
448
+ for (const key of Object.keys(schema)) {
449
+ const parser = schema[key];
450
+ out[key] = hasDefault(parser) ? parser.defaultValue : null;
451
+ }
452
+ return out;
453
+ };
454
+ const buildUrlOpsFromSnapshot = (snapshot, resolved) => {
455
+ const ops = [];
456
+ for (const key of Object.keys(schema)) {
457
+ const parser = schema[key];
458
+ const fieldValue = snapshot[key];
459
+ const urlKey = getUrlKey(key, options);
460
+ if (fieldValue === null) {
461
+ ops.push({ key: urlKey, value: null });
462
+ continue;
463
+ }
464
+ if (shouldHideDefaultInUrl(fieldValue, parser, resolved)) {
465
+ ops.push({ key: urlKey, value: null });
466
+ } else {
467
+ ops.push({ key: urlKey, value: parser.serialize(fieldValue) });
468
+ }
469
+ }
470
+ return ops;
471
+ };
472
+ const syncDefaultsToUrl = (setOptions) => {
473
+ const resolved = resolveOptions(setOptions);
474
+ if (resolveDefaultVisibility(resolved) !== "show") return;
475
+ const snapshot = $value.peek();
476
+ writeOps(buildUrlOpsFromSnapshot(snapshot, resolved), snapshot, resolved);
477
+ };
478
+ if (usesAutoRouter) {
479
+ onUrlStateRouterReady(() => {
480
+ syncFromAdapter();
481
+ syncDefaultsToUrl();
482
+ });
483
+ } else {
484
+ queueMicrotask(() => syncDefaultsToUrl());
485
+ }
486
+ const set = (partialOrNull, setOptions) => {
487
+ const resolved = resolveOptions(setOptions);
488
+ if (partialOrNull === null) {
489
+ const ops2 = [];
490
+ for (const key of Object.keys(schema)) {
491
+ ops2.push({ key: getUrlKey(key, options), value: null });
492
+ }
493
+ writeOps(ops2, resetValue(), resolved);
494
+ return;
495
+ }
496
+ const prev = $value.peek();
497
+ const next = { ...prev };
498
+ const ops = [];
499
+ for (const key of Object.keys(partialOrNull)) {
500
+ const schemaKey = key;
501
+ const parser = schema[schemaKey];
502
+ const urlKey = getUrlKey(schemaKey, options);
503
+ const value = partialOrNull[schemaKey];
504
+ if (value === void 0) continue;
505
+ if (value === null) {
506
+ next[schemaKey] = hasDefault(parser) ? parser.defaultValue : null;
507
+ ops.push({ key: urlKey, value: null });
508
+ continue;
509
+ }
510
+ next[schemaKey] = value;
511
+ if (shouldHideDefaultInUrl(value, parser, resolved)) {
512
+ ops.push({ key: urlKey, value: null });
513
+ } else {
514
+ ops.push({ key: urlKey, value: parser.serialize(value) });
515
+ }
516
+ }
517
+ writeOps(ops, next, resolved);
518
+ };
519
+ const update = (updater, setOptions) => {
520
+ const current = $value.peek();
521
+ set(updater(current), setOptions);
522
+ };
523
+ const reset = (setOptions) => {
524
+ const resolved = resolveOptions(setOptions);
525
+ const ops = [];
526
+ const next = resetValue();
527
+ for (const key of Object.keys(schema)) {
528
+ const parser = schema[key];
529
+ const urlKey = getUrlKey(key, options);
530
+ const value = next[key];
531
+ if (value === null) {
532
+ ops.push({ key: urlKey, value: null });
533
+ continue;
534
+ }
535
+ if (shouldHideDefaultInUrl(value, parser, resolved)) ops.push({ key: urlKey, value: null });
536
+ else ops.push({ key: urlKey, value: parser.serialize(value) });
537
+ }
538
+ writeOps(ops, next, resolved);
539
+ };
540
+ const clear = (setOptions) => set(null, setOptions);
541
+ return {
542
+ kind: "query-params",
543
+ value: () => $value.value(),
544
+ set,
545
+ update,
546
+ reset,
547
+ clear,
548
+ $value,
549
+ subscribe
550
+ };
551
+ };
552
+
553
+ // src/adapters/bind-router-query-params.ts
554
+ var attachRouterQueryParams = (router) => {
555
+ registerUrlStateRouter(router);
556
+ const bound = (schema, options) => createQueryParams(schema, {
557
+ ...options,
558
+ adapter: createRouterUrlStateAdapter(router)
559
+ });
560
+ return Object.assign(router, { createQueryParams: bound });
561
+ };
562
+
563
+ // src/core/parser.ts
564
+ var createParser = (impl) => {
565
+ const base = {
566
+ parserMode: "single",
567
+ parse: impl.parse,
568
+ serialize: impl.serialize,
569
+ withDefault(defaultValue) {
570
+ return createParserWithDefault(base, defaultValue);
571
+ },
572
+ withOptions(options) {
573
+ return createParserWithOptions(base, options);
574
+ }
575
+ };
576
+ return base;
577
+ };
578
+ var createParserWithDefault = (parser, defaultValue) => {
579
+ const next = {
580
+ ...parser,
581
+ defaultValue,
582
+ withDefault(value) {
583
+ return createParserWithDefault(next, value);
584
+ },
585
+ withOptions(options) {
586
+ return createParserWithOptions(next, options);
587
+ }
588
+ };
589
+ return next;
590
+ };
591
+ var createParserWithOptions = (parser, options) => {
592
+ const next = {
593
+ ...parser,
594
+ options: { ...parser.options ?? {}, ...options },
595
+ withDefault(defaultValue) {
596
+ return createParserWithDefault(next, defaultValue);
597
+ },
598
+ withOptions(more) {
599
+ return createParserWithOptions(next, more);
600
+ }
601
+ };
602
+ return next;
603
+ };
604
+
605
+ // src/parsers/string.ts
606
+ var first = (value) => {
607
+ if (value === null) return null;
608
+ if (Array.isArray(value)) return value[0] ?? null;
609
+ return value;
610
+ };
611
+ var parseAsString = createParser({
612
+ parse(value) {
613
+ const v = first(value);
614
+ if (v === null) return null;
615
+ return v;
616
+ },
617
+ serialize(value) {
618
+ return value;
619
+ }
620
+ });
621
+
622
+ // src/parsers/integer.ts
623
+ var first2 = (value) => {
624
+ if (value === null) return null;
625
+ if (Array.isArray(value)) return value[0] ?? null;
626
+ return value;
627
+ };
628
+ var parseAsInteger = createParser({
629
+ parse(value) {
630
+ const v = first2(value);
631
+ if (v === null) return null;
632
+ if (!/^[+-]?\d+$/.test(v)) return null;
633
+ const n = Number.parseInt(v, 10);
634
+ return Number.isFinite(n) ? n : null;
635
+ },
636
+ serialize(value) {
637
+ if (!Number.isFinite(value)) return null;
638
+ return String(Math.trunc(value));
639
+ }
640
+ });
641
+
642
+ // src/parsers/float.ts
643
+ var first3 = (value) => {
644
+ if (value === null) return null;
645
+ if (Array.isArray(value)) return value[0] ?? null;
646
+ return value;
647
+ };
648
+ var parseAsFloat = createParser({
649
+ parse(value) {
650
+ const v = first3(value);
651
+ if (v === null || v === "") return null;
652
+ const n = Number(v);
653
+ return Number.isFinite(n) ? n : null;
654
+ },
655
+ serialize(value) {
656
+ if (!Number.isFinite(value)) return null;
657
+ return String(value);
658
+ }
659
+ });
660
+
661
+ // src/parsers/boolean.ts
662
+ var first4 = (value) => {
663
+ if (value === null) return null;
664
+ if (Array.isArray(value)) return value[0] ?? null;
665
+ return value;
666
+ };
667
+ var parseAsBoolean = createParser({
668
+ parse(value) {
669
+ const v = first4(value);
670
+ if (v === null) return null;
671
+ const lowered = v.toLowerCase();
672
+ if (lowered === "true") return true;
673
+ if (lowered === "false") return false;
674
+ if (lowered === "1") return true;
675
+ if (lowered === "0") return false;
676
+ return null;
677
+ },
678
+ serialize(value) {
679
+ return value ? "true" : "false";
680
+ }
681
+ });
682
+
683
+ // src/parsers/literal.ts
684
+ var first5 = (value) => {
685
+ if (value === null) return null;
686
+ if (Array.isArray(value)) return value[0] ?? null;
687
+ return value;
688
+ };
689
+ var parseAsLiteral = (values) => {
690
+ const allowed = new Set(values.map((v) => String(v)));
691
+ return createParser({
692
+ parse(value) {
693
+ const v = first5(value);
694
+ if (v === null) return null;
695
+ if (!allowed.has(v)) return null;
696
+ return values.find((item) => String(item) === v) ?? null;
697
+ },
698
+ serialize(value) {
699
+ return String(value);
700
+ }
701
+ });
702
+ };
703
+ var parseAsStringLiteral = (values) => parseAsLiteral(values);
704
+ var parseAsNumberLiteral = (values) => parseAsLiteral(values);
705
+
706
+ // src/core/custom-multi-parser.ts
707
+ var toQueryValues = (raw) => {
708
+ if (raw === null) return [];
709
+ if (typeof raw === "string") return [raw];
710
+ return [...raw];
711
+ };
712
+ var createMultiParserWithDefault = (parser, defaultValue) => {
713
+ const next = {
714
+ ...parser,
715
+ parserMode: "multi",
716
+ defaultValue,
717
+ withDefault(value) {
718
+ return createMultiParserWithDefault(parser, value);
719
+ },
720
+ withOptions(options) {
721
+ return createMultiParserWithOptions(parser, options);
722
+ }
723
+ };
724
+ return next;
725
+ };
726
+ var createMultiParserWithOptions = (parser, options) => ({
727
+ ...parser,
728
+ options: { ...parser.options ?? {}, ...options },
729
+ withDefault(defaultValue) {
730
+ return createMultiParserWithDefault(parser, defaultValue);
731
+ },
732
+ withOptions(more) {
733
+ return createMultiParserWithOptions(parser, more);
734
+ }
735
+ });
736
+ var createCustomMultiParser = (config) => {
737
+ const parser = {
738
+ parserMode: "multi",
739
+ parse(raw) {
740
+ return config.parse(toQueryValues(raw));
741
+ },
742
+ serialize(value) {
743
+ return [...config.serialize(value)];
744
+ },
745
+ withDefault(defaultValue) {
746
+ return createMultiParserWithDefault(parser, defaultValue);
747
+ },
748
+ withOptions(options) {
749
+ return createMultiParserWithOptions(parser, options);
750
+ }
751
+ };
752
+ if (config.eq) parser.eq = config.eq;
753
+ return parser;
754
+ };
755
+ var isMultiParser = (parser) => parser.parserMode === "multi";
756
+
757
+ // src/parsers/array.ts
758
+ var parseAsArrayOf = (item, separator) => createCustomMultiParser({
759
+ parse(value) {
760
+ let rawItems;
761
+ if (value.length === 0) return [];
762
+ if (separator && value.length === 1) {
763
+ rawItems = value[0].split(separator).filter(Boolean);
764
+ } else {
765
+ rawItems = value;
766
+ }
767
+ const out = [];
768
+ for (const raw of rawItems) {
769
+ const parsed = item.parse(raw);
770
+ if (parsed === null) return null;
771
+ out.push(parsed);
772
+ }
773
+ return out;
774
+ },
775
+ serialize(items) {
776
+ if (separator) {
777
+ const parts = [];
778
+ for (const entry of items) {
779
+ const serialized = item.serialize(entry);
780
+ if (serialized === null) return [];
781
+ if (Array.isArray(serialized)) parts.push(...serialized);
782
+ else parts.push(serialized);
783
+ }
784
+ return parts.length ? [parts.join(separator)] : [];
785
+ }
786
+ const out = [];
787
+ for (const entry of items) {
788
+ const serialized = item.serialize(entry);
789
+ if (serialized === null) return [];
790
+ if (Array.isArray(serialized)) out.push(...serialized);
791
+ else out.push(serialized);
792
+ }
793
+ return out;
794
+ },
795
+ eq(a, b) {
796
+ if (a.length !== b.length) return false;
797
+ for (let i = 0; i < a.length; i += 1) {
798
+ if (!Object.is(a[i], b[i])) return false;
799
+ }
800
+ return true;
801
+ }
802
+ });
803
+
804
+ // src/parsers/native-array.ts
805
+ var serializeItem = (item, value) => {
806
+ const serialized = item.serialize(value);
807
+ if (serialized === null) return null;
808
+ if (Array.isArray(serialized)) return serialized[0] ?? null;
809
+ return serialized;
810
+ };
811
+ var parseAsNativeArrayOf = (item) => createCustomMultiParser({
812
+ parse(values) {
813
+ const out = [];
814
+ for (const raw of values) {
815
+ const parsed = item.parse(raw);
816
+ if (parsed === null) return null;
817
+ out.push(parsed);
818
+ }
819
+ return out;
820
+ },
821
+ serialize(items) {
822
+ const out = [];
823
+ for (const entry of items) {
824
+ const next = serializeItem(item, entry);
825
+ if (next === null) return [];
826
+ out.push(next);
827
+ }
828
+ return out;
829
+ },
830
+ eq(a, b) {
831
+ if (a.length !== b.length) return false;
832
+ for (let i = 0; i < a.length; i += 1) {
833
+ if (!Object.is(a[i], b[i])) return false;
834
+ }
835
+ return true;
836
+ }
837
+ }).withDefault([]);
838
+
839
+ // src/core/custom-parser.ts
840
+ var createCustomParser = (config) => {
841
+ const parser = createParser({
842
+ parse: config.parse,
843
+ serialize: (value) => config.serialize(value)
844
+ });
845
+ if (config.eq) parser.eq = config.eq;
846
+ parser.parserMode = "single";
847
+ return parser;
848
+ };
849
+
850
+ // src/core/standard-schema.ts
851
+ var validateJsonSchema = (schema, value) => {
852
+ if (!schema) return value;
853
+ if (typeof schema === "function") {
854
+ try {
855
+ return schema(value);
856
+ } catch {
857
+ return null;
858
+ }
859
+ }
860
+ const result = schema["~standard"].validate(value);
861
+ if (result && "value" in result && result.issues === void 0) {
862
+ return result.value;
863
+ }
864
+ return null;
865
+ };
866
+
867
+ // src/parsers/json.ts
868
+ var first6 = (value) => {
869
+ if (value === null) return null;
870
+ if (Array.isArray(value)) return value[0] ?? null;
871
+ return value;
872
+ };
873
+ var parseAsJson = (schema) => createCustomParser({
874
+ parse(value) {
875
+ const raw = first6(value);
876
+ if (raw === null) return null;
877
+ try {
878
+ const data = JSON.parse(raw);
879
+ return validateJsonSchema(schema, data);
880
+ } catch {
881
+ return null;
882
+ }
883
+ },
884
+ serialize(value) {
885
+ try {
886
+ return JSON.stringify(value);
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+ });
892
+
893
+ // src/parsers/date.ts
894
+ var first7 = (value) => {
895
+ if (value === null) return null;
896
+ if (Array.isArray(value)) return value[0] ?? null;
897
+ return value;
898
+ };
899
+ var parseAsIsoDate = createParser({
900
+ parse(value) {
901
+ const v = first7(value);
902
+ if (v === null) return null;
903
+ const date = new Date(v);
904
+ return Number.isNaN(date.getTime()) ? null : date;
905
+ },
906
+ serialize(value) {
907
+ const t = value.getTime();
908
+ if (Number.isNaN(t)) return null;
909
+ return value.toISOString();
910
+ }
911
+ });
912
+
913
+ // src/parsers/timestamp.ts
914
+ var first8 = (value) => {
915
+ if (value === null) return null;
916
+ if (Array.isArray(value)) return value[0] ?? null;
917
+ return value;
918
+ };
919
+ var parseAsTimestamp = createParser({
920
+ parse(value) {
921
+ const v = first8(value);
922
+ if (v === null) return null;
923
+ if (!/^[+-]?\d+(\.\d+)?$/.test(v)) return null;
924
+ const n = Number(v);
925
+ return Number.isFinite(n) ? n : null;
926
+ },
927
+ serialize(value) {
928
+ if (!Number.isFinite(value)) return null;
929
+ return String(value);
930
+ }
931
+ });
932
+
933
+ // src/utils/throttle.ts
934
+ var throttle = (ms) => {
935
+ let timer = null;
936
+ let pending = null;
937
+ const flush = () => {
938
+ if (!pending) return;
939
+ const fn = pending;
940
+ pending = null;
941
+ fn();
942
+ };
943
+ return {
944
+ kind: "throttle",
945
+ ms,
946
+ schedule(fn) {
947
+ pending = fn;
948
+ if (timer) return;
949
+ timer = setTimeout(() => {
950
+ timer = null;
951
+ flush();
952
+ }, ms);
953
+ },
954
+ cancel() {
955
+ if (timer) clearTimeout(timer);
956
+ timer = null;
957
+ pending = null;
958
+ }
959
+ };
960
+ };
961
+
962
+ // src/utils/debounce.ts
963
+ var debounce = (ms) => {
964
+ let timer = null;
965
+ let pending = null;
966
+ const flush = () => {
967
+ if (!pending) return;
968
+ const fn = pending;
969
+ pending = null;
970
+ fn();
971
+ };
972
+ return {
973
+ kind: "debounce",
974
+ ms,
975
+ schedule(fn) {
976
+ pending = fn;
977
+ if (timer) clearTimeout(timer);
978
+ timer = setTimeout(() => {
979
+ timer = null;
980
+ flush();
981
+ }, ms);
982
+ },
983
+ cancel() {
984
+ if (timer) clearTimeout(timer);
985
+ timer = null;
986
+ pending = null;
987
+ }
988
+ };
989
+ };
990
+
991
+ export { attachRouterQueryParams, createBrowserUrlStateAdapter, createCustomMultiParser, createCustomParser, createMemoryUrlStateAdapter, createQueryParam, createQueryParams, createRouterUrlStateAdapter, debounce, getUrlStateRouter, isMultiParser, parseAsArrayOf, parseAsBoolean, parseAsFloat, parseAsInteger, parseAsIsoDate, parseAsJson, parseAsLiteral, parseAsNativeArrayOf, parseAsNumberLiteral, parseAsString, parseAsStringLiteral, parseAsTimestamp, throttle };
992
+ //# sourceMappingURL=index.js.map
993
+ //# sourceMappingURL=index.js.map