kitcn 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/aggregate/index.d.ts +1 -1
  2. package/dist/auth/client/index.js +1 -1
  3. package/dist/auth/index.js +19 -21
  4. package/dist/auth/nextjs/index.d.ts +1 -1
  5. package/dist/auth/nextjs/index.js +4 -4
  6. package/dist/{auth-store-ssZDPa37.js → auth-store-BnGZxmnY.js} +4 -1
  7. package/dist/{backend-core-DqPydYyx.mjs → backend-core-BsKP1LVg.mjs} +204 -164
  8. package/dist/{builder-DBgto1yn.js → builder-f4F_NRvK.js} +245 -153
  9. package/dist/{caller-factory-NEfgD5E0.js → caller-factory-DHywSoGZ.js} +7 -5
  10. package/dist/cli.mjs +14 -7
  11. package/dist/crpc/index.js +1 -127
  12. package/dist/{middleware-Bg-PdtrI.js → middleware-Cgrv2jIu.js} +1 -1
  13. package/dist/orm/index.d.ts +1 -1
  14. package/dist/orm/index.js +486 -121
  15. package/dist/plugins/index.js +1 -1
  16. package/dist/{procedure-caller-9m6NBxQu.js → procedure-caller-Rj6z3ai7.js} +1 -1
  17. package/dist/{procedure-name-Cy1AxayA.d.ts → procedure-name-Bo5KMcqc.d.ts} +15 -3
  18. package/dist/{query-context-ydn9kb6P.js → query-context-C90vNlc9.js} +131 -30
  19. package/dist/query-options-C_eBSIXG.js +247 -0
  20. package/dist/ratelimit/index.d.ts +26 -6
  21. package/dist/ratelimit/index.js +427 -100
  22. package/dist/ratelimit/react/index.d.ts +14 -0
  23. package/dist/ratelimit/react/index.js +149 -16
  24. package/dist/react/index.d.ts +3 -1
  25. package/dist/react/index.js +48 -15
  26. package/dist/rsc/index.js +22 -33
  27. package/dist/server/index.d.ts +1 -1
  28. package/dist/server/index.js +3 -3
  29. package/dist/solid/index.js +19 -5
  30. package/dist/watcher.mjs +2 -2
  31. package/dist/{where-clause-compiler-WF9UcrAB.d.ts → where-clause-compiler-BRhLW1dp.d.ts} +51 -0
  32. package/package.json +1 -1
  33. package/skills/kitcn/SKILL.md +1 -0
  34. package/skills/kitcn/references/features/create-plugins.md +1 -1
  35. package/skills/kitcn/references/features/orm.md +11 -1
  36. package/skills/kitcn/references/features/ratelimit.md +105 -0
  37. package/skills/kitcn/references/setup/server.md +1 -1
  38. package/dist/query-options-C96zLANM.js +0 -121
@@ -2,11 +2,25 @@
2
2
  import { FunctionReference } from "convex/server";
3
3
 
4
4
  //#region src/ratelimit/types.d.ts
5
+ type RatelimitStoredState = {
6
+ value: number;
7
+ ts: number;
8
+ auxValue?: number;
9
+ auxTs?: number;
10
+ };
11
+ type RatelimitShardState = {
12
+ shard: number;
13
+ state: RatelimitStoredState;
14
+ };
15
+ type RatelimitState = RatelimitStoredState & {
16
+ shards?: RatelimitShardState[];
17
+ };
5
18
  type RatelimitSnapshot = {
6
19
  value: number;
7
20
  ts: number;
8
21
  shard: number;
9
22
  config: ResolvedAlgorithm;
23
+ state: RatelimitState;
10
24
  };
11
25
  type FixedWindowAlgorithm = {
12
26
  kind: 'fixedWindow';
@@ -3,12 +3,134 @@ import { useConvex, useQuery } from "convex/react";
3
3
  import { useCallback, useEffect, useMemo, useState } from "react";
4
4
  import { makeFunctionReference } from "convex/server";
5
5
 
6
+ //#region src/ratelimit/core/algorithms.ts
7
+ /**
8
+ * Narrow the configured budget down to the slice one shard owns.
9
+ *
10
+ * Every shard stores its own row and spends only what it owns, so without this
11
+ * the effective limit would be multiplied by the shard count. Shares are dealt
12
+ * so they add back up to the configured budget exactly — see {@link shardShare}.
13
+ */
14
+ function shardAlgorithm(algorithm, shard) {
15
+ const { shards } = algorithm;
16
+ if (shards <= 1) return algorithm;
17
+ const share = (value) => shardShare(value, shards, shard);
18
+ const maxReserved = algorithm.maxReserved === void 0 ? void 0 : share(algorithm.maxReserved);
19
+ if (algorithm.kind === "tokenBucket") {
20
+ const maxTokens = share(algorithm.maxTokens);
21
+ return {
22
+ ...algorithm,
23
+ refillRate: algorithm.refillRate * (maxTokens / algorithm.maxTokens),
24
+ maxTokens,
25
+ maxReserved,
26
+ shards: 1
27
+ };
28
+ }
29
+ if (algorithm.kind === "fixedWindow") return {
30
+ ...algorithm,
31
+ limit: share(algorithm.limit),
32
+ capacity: share(algorithm.capacity),
33
+ maxReserved,
34
+ shards: 1
35
+ };
36
+ return {
37
+ ...algorithm,
38
+ limit: share(algorithm.limit),
39
+ maxReserved,
40
+ shards: 1
41
+ };
42
+ }
43
+ /** Tokens a shard-level config may spend, ignoring the per-window refill. */
44
+ function algorithmBudget(algorithm) {
45
+ return algorithm.kind === "tokenBucket" ? algorithm.maxTokens : algorithm.limit;
46
+ }
47
+ /** Tokens the algorithm can hold, including fixed-window burst capacity. */
48
+ function algorithmCapacity(algorithm) {
49
+ if (algorithm.kind === "fixedWindow") return algorithm.capacity;
50
+ return algorithmBudget(algorithm);
51
+ }
52
+ /**
53
+ * Deal a whole-token budget across `shards` so the shares sum back to `total`.
54
+ *
55
+ * A plain `total / shards` strands the fractional part of every shard: a shard
56
+ * holding `2.5` tokens only ever grants two whole requests and refills to `2.5`
57
+ * again, so `limit: 5` over two shards would enforce `4` forever. Whole budgets
58
+ * are dealt as `floor` plus one extra token to the first `total % shards` shards
59
+ * instead. Fractional budgets have no whole-token floor to hit, so they keep the
60
+ * even split.
61
+ */
62
+ function shardShare(total, shards, shard) {
63
+ const whole = Math.floor(total);
64
+ const fractional = total - whole;
65
+ return Math.floor(whole / shards) + (shard < whole % shards ? 1 : 0) + (shard === 0 ? fractional : 0);
66
+ }
67
+
68
+ //#endregion
6
69
  //#region src/ratelimit/core/calculate-rate-limit.ts
7
70
  function calculateRatelimit(state, algorithm, now, count) {
71
+ const shardStates = state?.shards;
72
+ if (algorithm.shards > 1 && count > maximumShardCapacity(algorithm)) return {
73
+ ...calculateSingleRatelimit(state, algorithm, now, count),
74
+ retryAfter: Number.POSITIVE_INFINITY,
75
+ reset: Number.POSITIVE_INFINITY
76
+ };
77
+ if (algorithm.shards > 1 && shardStates?.length === algorithm.shards) return calculateShardedRatelimit(shardStates, algorithm, now, count);
78
+ return calculateSingleRatelimit(state, algorithm, now, count);
79
+ }
80
+ function calculateSingleRatelimit(state, algorithm, now, count) {
8
81
  if (algorithm.kind === "fixedWindow") return calculateFixedWindow(state, algorithm, now, count);
9
82
  if (algorithm.kind === "tokenBucket") return calculateTokenBucket(state, algorithm, now, count);
10
83
  return calculateSlidingWindow(state, algorithm, now, count);
11
84
  }
85
+ function maximumShardCapacity(algorithm) {
86
+ let maximum = 0;
87
+ for (let shard = 0; shard < algorithm.shards; shard += 1) maximum = Math.max(maximum, algorithmCapacity(shardAlgorithm(algorithm, shard)));
88
+ return maximum;
89
+ }
90
+ function calculateShardedRatelimit(shardStates, algorithm, now, count) {
91
+ const candidates = shardStates.map(({ shard, state: shardState }) => {
92
+ const perShard = shardAlgorithm(algorithm, shard);
93
+ return {
94
+ baseline: calculateRatelimit(shardState, perShard, now, 0),
95
+ requested: calculateRatelimit(shardState, perShard, now, count),
96
+ shard
97
+ };
98
+ });
99
+ const selected = [...count === 0 ? candidates : candidates.filter((candidate) => candidate.requested.retryAfter === void 0)].sort((a, b) => b.requested.remainingRaw - a.requested.remainingRaw)[0];
100
+ const projected = candidates.map((candidate) => ({
101
+ evaluated: count !== 0 && candidate.shard === selected?.shard ? candidate.requested : candidate.baseline,
102
+ shard: candidate.shard
103
+ }));
104
+ const retryValues = candidates.flatMap((candidate) => candidate.requested.retryAfter === void 0 ? [] : [candidate.requested.retryAfter]);
105
+ const retryAfter = selected ? void 0 : Math.min(...retryValues);
106
+ const remaining = selected ? projected.reduce((total, candidate) => total + candidate.evaluated.remaining, 0) : 0;
107
+ const remainingRaw = selected ? projected.reduce((total, candidate) => total + Math.max(0, candidate.evaluated.remainingRaw), 0) : Math.max(...candidates.map((candidate) => candidate.requested.remainingRaw));
108
+ const auxTimestamps = projected.flatMap((candidate) => candidate.evaluated.state.auxTs === void 0 ? [] : [candidate.evaluated.state.auxTs]);
109
+ return {
110
+ state: {
111
+ value: selected || count === 0 ? projected.reduce((total, candidate) => total + candidate.evaluated.state.value, 0) : remainingRaw,
112
+ ts: Math.max(...projected.map((candidate) => candidate.evaluated.state.ts)),
113
+ ...auxTimestamps.length > 0 ? {
114
+ auxValue: projected.reduce((total, candidate) => total + (candidate.evaluated.state.auxValue ?? 0), 0),
115
+ auxTs: Math.max(...auxTimestamps)
116
+ } : {},
117
+ shards: projected.map(({ evaluated, shard }) => ({
118
+ shard,
119
+ state: {
120
+ value: evaluated.state.value,
121
+ ts: evaluated.state.ts,
122
+ auxValue: evaluated.state.auxValue,
123
+ auxTs: evaluated.state.auxTs
124
+ }
125
+ }))
126
+ },
127
+ retryAfter,
128
+ remaining,
129
+ remainingRaw,
130
+ reset: Math.min(...candidates.map((candidate) => candidate.requested.reset)),
131
+ limit: algorithmBudget(algorithm)
132
+ };
133
+ }
12
134
  function calculateTokenBucket(state, config, now, count) {
13
135
  const ratePerMs = config.refillRate / config.interval;
14
136
  const initial = state ?? {
@@ -17,7 +139,8 @@ function calculateTokenBucket(state, config, now, count) {
17
139
  };
18
140
  const elapsed = Math.max(0, now - initial.ts);
19
141
  const nextValue = Math.min(initial.value + elapsed * ratePerMs, config.maxTokens) - count;
20
- const retryAfter = nextValue < 0 ? Math.ceil(-nextValue / ratePerMs) : void 0;
142
+ let retryAfter;
143
+ if (nextValue < 0) retryAfter = count > config.maxTokens ? Number.POSITIVE_INFINITY : Math.ceil(-nextValue / ratePerMs);
21
144
  return {
22
145
  state: {
23
146
  value: nextValue,
@@ -25,6 +148,7 @@ function calculateTokenBucket(state, config, now, count) {
25
148
  },
26
149
  retryAfter,
27
150
  remaining: Math.max(0, Math.floor(nextValue)),
151
+ remainingRaw: nextValue,
28
152
  reset: retryAfter ? now + retryAfter : now,
29
153
  limit: config.maxTokens
30
154
  };
@@ -39,7 +163,8 @@ function calculateFixedWindow(state, config, now, count) {
39
163
  const replenished = Math.min(initial.value + config.limit * elapsedWindows, config.capacity);
40
164
  const ts = initial.ts + elapsedWindows * config.window;
41
165
  const nextValue = replenished - count;
42
- const retryAfter = nextValue < 0 ? ts + config.window * Math.ceil(-nextValue / config.limit) - now : void 0;
166
+ let retryAfter;
167
+ if (nextValue < 0) retryAfter = count > config.capacity ? Number.POSITIVE_INFINITY : ts + config.window * Math.ceil(-nextValue / config.limit) - now;
43
168
  return {
44
169
  state: {
45
170
  value: nextValue,
@@ -47,7 +172,8 @@ function calculateFixedWindow(state, config, now, count) {
47
172
  },
48
173
  retryAfter,
49
174
  remaining: Math.max(0, Math.floor(nextValue)),
50
- reset: ts + config.window,
175
+ remainingRaw: nextValue,
176
+ reset: retryAfter === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : ts + config.window,
51
177
  limit: config.limit
52
178
  };
53
179
  }
@@ -67,7 +193,8 @@ function calculateSlidingWindow(state, config, now, count) {
67
193
  const projectedCurrent = currentCount + count;
68
194
  const projectedUsed = projectedCurrent + previousCount * previousWeight;
69
195
  const remaining = config.limit - projectedUsed;
70
- const retryAfter = remaining < 0 ? Math.max(1, config.window - elapsedInWindow) : void 0;
196
+ let retryAfter;
197
+ if (remaining < 0) retryAfter = count > config.limit ? Number.POSITIVE_INFINITY : Math.max(1, config.window - elapsedInWindow);
71
198
  return {
72
199
  state: {
73
200
  value: projectedCurrent,
@@ -77,10 +204,22 @@ function calculateSlidingWindow(state, config, now, count) {
77
204
  },
78
205
  retryAfter,
79
206
  remaining: Math.max(0, Math.floor(remaining)),
80
- reset: windowStart + config.window,
207
+ remainingRaw: remaining,
208
+ reset: retryAfter === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : windowStart + config.window,
81
209
  limit: config.limit
82
210
  };
83
211
  }
212
+ /**
213
+ * Convert a {@link RatelimitSnapshot} back into the `RatelimitState` shape that
214
+ * {@link calculateRatelimit} consumes.
215
+ *
216
+ * Snapshot `value` is always "tokens left". Fixed window and token bucket store
217
+ * that directly, but sliding window state stores the used count, so it has to be
218
+ * inverted before it can be replayed.
219
+ */
220
+ function snapshotToState(snapshot) {
221
+ return { ...snapshot.state };
222
+ }
84
223
  function alignWindowStart(now, window, start = 0) {
85
224
  const offsetNow = now - start;
86
225
  return start + Math.floor(offsetNow / window) * window;
@@ -116,8 +255,8 @@ function useRatelimit(getRatelimitValueQuery, options) {
116
255
  ts: evaluation.ts - timeOffset,
117
256
  config: ratelimitData.config,
118
257
  shard: ratelimitData.shard,
119
- ok: evaluation.value >= 0,
120
- retryAt: evaluation.retryAfter ? serverTs + evaluation.retryAfter - timeOffset : void 0
258
+ ok: evaluation.retryAfter === void 0,
259
+ retryAt: evaluation.retryAfter === void 0 ? void 0 : serverTs + evaluation.retryAfter - timeOffset
121
260
  };
122
261
  }, [
123
262
  count,
@@ -130,7 +269,7 @@ function useRatelimit(getRatelimitValueQuery, options) {
130
269
  status: void 0,
131
270
  check
132
271
  };
133
- if (current.value < 0) return {
272
+ if (!current.ok) return {
134
273
  status: {
135
274
  ok: false,
136
275
  retryAt: current.retryAt
@@ -146,7 +285,7 @@ function useRatelimit(getRatelimitValueQuery, options) {
146
285
  };
147
286
  }, [check, current]);
148
287
  useEffect(() => {
149
- if (response.status?.ok !== false || !response.status.retryAt) return;
288
+ if (response.status?.ok !== false || !response.status.retryAt || !Number.isFinite(response.status.retryAt)) return;
150
289
  const timeout = setTimeout(() => setNow(Date.now()), Math.max(0, response.status.retryAt - now));
151
290
  return () => clearTimeout(timeout);
152
291
  }, [
@@ -165,13 +304,7 @@ function resolveGetServerTimeMutation(ref) {
165
304
  return ref;
166
305
  }
167
306
  function evaluateSnapshot(snapshot, now, count) {
168
- const evaluated = calculateRatelimit(snapshot.config.kind === "slidingWindow" ? {
169
- value: Math.max(0, snapshot.config.limit - snapshot.value),
170
- ts: snapshot.ts
171
- } : {
172
- value: snapshot.value,
173
- ts: snapshot.ts
174
- }, snapshot.config, now, count);
307
+ const evaluated = calculateRatelimit(snapshotToState(snapshot), snapshot.config, now, count);
175
308
  return {
176
309
  value: snapshot.config.kind === "slidingWindow" ? evaluated.retryAfter !== void 0 ? -1 : evaluated.remaining : evaluated.state.value,
177
310
  ts: evaluated.state.ts,
@@ -1060,7 +1060,9 @@ declare function useConvexInfiniteQueryOptions<T extends FunctionReference<'quer
1060
1060
  */
1061
1061
  declare function useConvexActionQueryOptions<Action extends FunctionReference<'action'>>(action: Action, args: FunctionArgs<Action> | SkipToken, options?: {
1062
1062
  skipUnauth?: boolean;
1063
- } & DistributiveOmit<UseQueryOptions<FunctionReturnType<Action>, DefaultError>, ReservedQueryOptions>): ConvexActionOptions<Action>;
1063
+ } & DistributiveOmit<UseQueryOptions<FunctionReturnType<Action>, DefaultError>, ReservedQueryOptions>): ConvexActionOptions<Action> & {
1064
+ meta: ConvexQueryMeta;
1065
+ };
1064
1066
  /**
1065
1067
  * Hook that returns mutation options for use with useMutation.
1066
1068
  * Wraps the Convex mutation with auth guard logic.
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { A as defaultIsUnauthorized, C as useSafeConvexAuth, D as writeAuthSessionFallbackData, O as writeAuthSessionFallbackToken, S as useMaybeAuth, _ as useAuthValue, a as ConvexAuthRecoveryError, b as useFetchAccessToken, c as MaybeAuthenticated, d as decodeJwtExp, f as isSessionSyncGraceActive, g as useAuthStore, h as useAuthState, i as ConvexAuthBridge, j as isCRPCClientError, k as CRPCClientError, l as MaybeUnauthenticated, m as useAuthGuard, n as AuthProvider, o as ConvexProviderWithAuth, p as useAuth, r as Authenticated, s as FetchAccessTokenContext, t as AUTH_SESSION_SYNC_GRACE_MS, u as Unauthenticated, v as useConvexAuthBridge, w as clearAuthSessionFallback, x as useIsAuth, y as useConvexAuthRecovery } from "../auth-store-ssZDPa37.js";
2
+ import { A as defaultIsUnauthorized, C as useSafeConvexAuth, D as writeAuthSessionFallbackData, O as writeAuthSessionFallbackToken, S as useMaybeAuth, _ as useAuthValue, a as ConvexAuthRecoveryError, b as useFetchAccessToken, c as MaybeAuthenticated, d as decodeJwtExp, f as isSessionSyncGraceActive, g as useAuthStore, h as useAuthState, i as ConvexAuthBridge, j as isCRPCClientError, k as CRPCClientError, l as MaybeUnauthenticated, m as useAuthGuard, n as AuthProvider, o as ConvexProviderWithAuth, p as useAuth, r as Authenticated, s as FetchAccessTokenContext, t as AUTH_SESSION_SYNC_GRACE_MS, u as Unauthenticated, v as useConvexAuthBridge, w as clearAuthSessionFallback, x as useIsAuth, y as useConvexAuthRecovery } from "../auth-store-BnGZxmnY.js";
3
3
  import { ConvexProvider, ConvexReactClient, ConvexReactClient as ConvexReactClient$1, useAction, useConvex, useMutation } from "convex/react";
4
4
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
@@ -418,6 +418,10 @@ const getTransformer = (transformer) => {
418
418
  transformerCache.set(cacheKey, resolved);
419
419
  return resolved;
420
420
  };
421
+ /**
422
+ * Encode request payloads (input direction).
423
+ */
424
+ const encodeWire = (value, transformer) => getTransformer(transformer).input.serialize(value);
421
425
 
422
426
  //#endregion
423
427
  //#region src/react/http-proxy.ts
@@ -673,7 +677,7 @@ function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
673
677
  cursor: null,
674
678
  limit
675
679
  };
676
- const finalEnabled = enabled === false || isSkip ? false : void 0;
680
+ const finalEnabled = isSkip ? false : enabled;
677
681
  return {
678
682
  queryKey: [
679
683
  "convexQuery",
@@ -686,7 +690,7 @@ function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
686
690
  refetchOnReconnect: false,
687
691
  refetchOnWindowFocus: false,
688
692
  ...queryOptions,
689
- ...finalEnabled === false ? { enabled: false } : {},
693
+ ...finalEnabled === void 0 ? {} : { enabled: finalEnabled },
690
694
  meta: {
691
695
  authType,
692
696
  skipUnauth,
@@ -723,6 +727,19 @@ function useAuthSkip(funcRef, opts) {
723
727
  };
724
728
  }
725
729
 
730
+ //#endregion
731
+ //#region src/internal/enabled.ts
732
+ /**
733
+ * Combine a computed gate with the caller's `enabled` option.
734
+ * A predicate is preserved (never collapsed to a boolean) so TanStack Query
735
+ * keeps evaluating it; `allowed: false` always wins.
736
+ */
737
+ function resolveEnabled(allowed, enabled) {
738
+ if (!allowed) return false;
739
+ if (typeof enabled === "function") return (query) => enabled(query) !== false;
740
+ return enabled ?? true;
741
+ }
742
+
726
743
  //#endregion
727
744
  //#region src/internal/query-key.ts
728
745
  /**
@@ -745,12 +762,19 @@ function isConvexAction$1(queryKey) {
745
762
  return queryKey.length >= 2 && queryKey[0] === "convexAction";
746
763
  }
747
764
  /**
765
+ * Serialize args the same way they go over the wire, so non-native Convex
766
+ * types the transformer supports (e.g. `Date`) can be hashed.
767
+ */
768
+ function hashArgs(args) {
769
+ return JSON.stringify(convexToJson(encodeWire(args)));
770
+ }
771
+ /**
748
772
  * Create stable hash for Convex query keys.
749
773
  * Uses Convex's JSON serialization for consistent argument hashing.
750
774
  */
751
775
  function hashConvexQuery(queryKey) {
752
776
  const [, funcName, args] = queryKey;
753
- return `convexQuery|${funcName}|${JSON.stringify(convexToJson(args))}`;
777
+ return `convexQuery|${funcName}|${hashArgs(args)}`;
754
778
  }
755
779
  /**
756
780
  * Create stable hash for Convex action keys.
@@ -758,7 +782,7 @@ function hashConvexQuery(queryKey) {
758
782
  */
759
783
  function hashConvexAction(queryKey) {
760
784
  const [, funcName, args] = queryKey;
761
- return `convexAction|${funcName}|${JSON.stringify(convexToJson(args))}`;
785
+ return `convexAction|${funcName}|${hashArgs(args)}`;
762
786
  }
763
787
 
764
788
  //#endregion
@@ -792,12 +816,13 @@ function getStableArgsByHash(hash, args) {
792
816
  stableArgsByHash.set(hash, stableArgs);
793
817
  return stableArgs;
794
818
  }
795
- stableArgsByHash.set(hash, args);
819
+ const stored = structuredClone(args);
820
+ stableArgsByHash.set(hash, stored);
796
821
  if (stableArgsByHash.size > MAX_STABLE_ARGS) {
797
822
  const oldestHash = stableArgsByHash.keys().next().value;
798
823
  if (oldestHash !== void 0) stableArgsByHash.delete(oldestHash);
799
824
  }
800
- return args;
825
+ return stored;
801
826
  }
802
827
  function useStableQueryArgs(prefix, funcRef, args) {
803
828
  const resolvedArgs = args === skipToken || args == null ? EMPTY_ARGS : args;
@@ -846,14 +871,15 @@ function useConvexQueryOptions(funcRef, args, options) {
846
871
  const stableArgs = useStableQueryArgs("convexQuery", funcRef, isSkipped ? EMPTY_ARGS : args);
847
872
  const baseOptions = useMemo(() => convexQuery(funcRef, stableArgs.value), [funcRef, stableArgs]);
848
873
  return useMemo(() => {
849
- const { skipUnauth: _, subscribe, ...queryOptions } = options ?? {};
874
+ const { enabled: userEnabled, skipUnauth, subscribe, ...queryOptions } = options ?? {};
850
875
  return {
851
876
  ...baseOptions,
852
877
  ...queryOptions,
853
- enabled: isSkipped ? false : !shouldSkip,
878
+ enabled: resolveEnabled(!(isSkipped || shouldSkip), userEnabled),
854
879
  meta: {
855
880
  ...baseOptions.meta,
856
881
  authType,
882
+ skipUnauth,
857
883
  subscribe: subscribe !== false
858
884
  }
859
885
  };
@@ -898,7 +924,7 @@ function useConvexInfiniteQueryOptions(funcRef, args, opts) {
898
924
  enabled: isSkipped ? false : enabledOpt,
899
925
  skipUnauth: opts.skipUnauth
900
926
  });
901
- const enabled = isSkipped || shouldSkip ? false : enabledOpt;
927
+ const enabled = resolveEnabled(!(isSkipped || shouldSkip), opts.enabled);
902
928
  const baseOptions = convexInfiniteQueryOptions(funcRef, isSkipped ? {} : args, {
903
929
  ...opts,
904
930
  enabled
@@ -933,20 +959,26 @@ function useConvexInfiniteQueryOptions(funcRef, args, opts) {
933
959
  function useConvexActionQueryOptions(action, args, options) {
934
960
  const isSkipped = args === skipToken;
935
961
  const enabled = typeof options?.enabled === "function" ? void 0 : options?.enabled;
936
- const { shouldSkip } = useAuthSkip(action, {
962
+ const { authType, shouldSkip } = useAuthSkip(action, {
937
963
  enabled: isSkipped ? false : enabled,
938
964
  skipUnauth: options?.skipUnauth
939
965
  });
940
966
  const stableArgs = useStableQueryArgs("convexAction", action, isSkipped ? EMPTY_ARGS : args);
941
967
  const baseOptions = useMemo(() => convexAction(action, stableArgs.value), [action, stableArgs]);
942
968
  return useMemo(() => {
943
- const { skipUnauth: _, ...queryOptions } = options ?? {};
969
+ const { enabled: userEnabled, skipUnauth, ...queryOptions } = options ?? {};
944
970
  return {
945
971
  ...baseOptions,
946
972
  ...queryOptions,
947
- enabled: isSkipped ? false : !shouldSkip
973
+ enabled: resolveEnabled(!(isSkipped || shouldSkip), userEnabled),
974
+ meta: {
975
+ ...baseOptions.meta,
976
+ authType,
977
+ skipUnauth
978
+ }
948
979
  };
949
980
  }, [
981
+ authType,
950
982
  baseOptions,
951
983
  isSkipped,
952
984
  options,
@@ -2380,7 +2412,7 @@ const useInfiniteQueryInternal = (query, args, options) => {
2380
2412
  const pageArgs = state.queries[key]?.args;
2381
2413
  return {
2382
2414
  ...convexQuery(query, pageArgs ? (({ __paginationId, ...rest }) => rest)(pageArgs) : "skip", meta),
2383
- enabled: !skip && !!state.queries[key],
2415
+ enabled: resolveEnabled(!skip && !!state.queries[key], enabled),
2384
2416
  structuralSharing: false,
2385
2417
  ...queryOptions ?? {},
2386
2418
  ...index === 0 && prefetchedFirstPage ? { initialData: prefetchedFirstPage } : {},
@@ -2395,6 +2427,7 @@ const useInfiniteQueryInternal = (query, args, options) => {
2395
2427
  state.pageKeys,
2396
2428
  state.queries,
2397
2429
  skip,
2430
+ enabled,
2398
2431
  meta,
2399
2432
  queryOptions,
2400
2433
  prefetchedFirstPage,
@@ -2578,7 +2611,7 @@ function useInfiniteQuery(infiniteOptions) {
2578
2611
  const result = useInfiniteQueryInternal(query, args, {
2579
2612
  limit,
2580
2613
  ...queryOptions,
2581
- enabled: !shouldSkip
2614
+ enabled: resolveEnabled(!shouldSkip, factoryEnabled)
2582
2615
  });
2583
2616
  const authLoadingApplies = authType === "optional" || authType === "required";
2584
2617
  const isClientError = isCRPCClientError(result.error);
package/dist/rsc/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { n as defaultIsUnauthorized } from "../error-Bvo7YEhk.js";
2
2
  import { n as getFuncRef, r as getFunctionMeta, t as buildMetaIndex } from "../meta-utils-D9K4fICl.js";
3
- import { i as decodeWire, s as getTransformer } from "../transformer-C6pGVHqx.js";
4
- import { n as convexInfiniteQueryOptions, r as convexQuery } from "../query-options-C96zLANM.js";
3
+ import { o as encodeWire, s as getTransformer } from "../transformer-C6pGVHqx.js";
4
+ import { n as convexInfiniteQueryOptions, o as executeHttpRequest, r as convexQuery } from "../query-options-C_eBSIXG.js";
5
5
  import { convexToJson } from "convex/values";
6
6
  import { getFunctionName } from "convex/server";
7
7
  import { fetchAction, fetchQuery } from "convex/nextjs";
@@ -28,37 +28,19 @@ function buildHttpQueryOptions(route, routeKey, args) {
28
28
  /**
29
29
  * Execute an HTTP route fetch.
30
30
  * Called by getServerQueryClientOptions queryFn.
31
+ *
32
+ * Shares the browser client's request builder so a prefetched entry hydrates
33
+ * the client cache instead of being refetched from a different URL.
31
34
  */
32
35
  async function fetchHttpRoute(convexSiteUrl, routeMeta, args, token, transformer) {
33
- const url = buildUrl(convexSiteUrl, routeMeta.path, args);
34
- const response = await fetch(url, {
35
- method: routeMeta.method,
36
- headers: {
37
- "Content-Type": "application/json",
38
- ...token ? { Authorization: `Bearer ${token}` } : {}
39
- }
40
- });
41
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
42
- if (response.headers.get("content-length") === "0" || response.status === 204) return null;
43
- return decodeWire(await response.json(), transformer);
44
- }
45
- /**
46
- * Build URL with path params and query params.
47
- */
48
- function buildUrl(convexSiteUrl, pathTemplate, args) {
49
- const remaining = { ...args };
50
- const path = pathTemplate.replace(/:(\w+)/g, (_, key) => {
51
- const value = remaining[key];
52
- delete remaining[key];
53
- return value !== null && value !== void 0 ? encodeURIComponent(String(value)) : "";
54
- });
55
- const queryEntries = Object.entries(remaining).filter(([_, v]) => v !== void 0 && v !== null);
56
- if (queryEntries.length > 0) {
57
- const params = new URLSearchParams();
58
- for (const [key, value] of queryEntries) params.set(key, String(value));
59
- return `${convexSiteUrl}${path}?${params.toString()}`;
60
- }
61
- return convexSiteUrl + path;
36
+ return await executeHttpRequest({
37
+ args,
38
+ baseHeaders: token ? { Authorization: `Bearer ${token}` } : void 0,
39
+ convexSiteUrl,
40
+ procedureName: `${routeMeta.method} ${routeMeta.path}`,
41
+ route: routeMeta,
42
+ transformer
43
+ }) ?? null;
62
44
  }
63
45
 
64
46
  //#endregion
@@ -150,12 +132,19 @@ function isConvexAction(queryKey) {
150
132
  return queryKey.length >= 2 && queryKey[0] === "convexAction";
151
133
  }
152
134
  /**
135
+ * Serialize args the same way they go over the wire, so non-native Convex
136
+ * types the transformer supports (e.g. `Date`) can be hashed.
137
+ */
138
+ function hashArgs(args) {
139
+ return JSON.stringify(convexToJson(encodeWire(args)));
140
+ }
141
+ /**
153
142
  * Create stable hash for Convex query keys.
154
143
  * Uses Convex's JSON serialization for consistent argument hashing.
155
144
  */
156
145
  function hashConvexQuery(queryKey) {
157
146
  const [, funcName, args] = queryKey;
158
- return `convexQuery|${funcName}|${JSON.stringify(convexToJson(args))}`;
147
+ return `convexQuery|${funcName}|${hashArgs(args)}`;
159
148
  }
160
149
  /**
161
150
  * Create stable hash for Convex action keys.
@@ -163,7 +152,7 @@ function hashConvexQuery(queryKey) {
163
152
  */
164
153
  function hashConvexAction(queryKey) {
165
154
  const [, funcName, args] = queryKey;
166
- return `convexAction|${funcName}|${JSON.stringify(convexToJson(args))}`;
155
+ return `convexAction|${funcName}|${hashArgs(args)}`;
167
156
  }
168
157
 
169
158
  //#endregion
@@ -1,4 +1,4 @@
1
- import { $ as ActionProcedureBuilder, A as CRPCError, B as RuntimeEnv, C as createProcedureHandlerFactory, Ct as zCustomQuery, D as WithHttpRouter, Dt as zodToConvex, E as typedProcedureResolver, Et as zodOutputToConvexFields, F as getCRPCErrorFromUnknown, G as createLazyCaller, H as ConvexContext, I as getHTTPStatusCodeFromError, J as ServerCaller, K as CallerMeta, L as isCRPCError, M as CRPCErrorData, N as CRPC_ERROR_CODES_BY_KEY, O as inferApiInputs, Ot as zodToConvexFields, P as CRPC_ERROR_CODE_TO_HTTP, Q as getGeneratedValue, R as toCRPCError, S as createProcedureCallerFactory, St as zCustomMutation, T as getGeneratedFunctionReference, Tt as zodOutputToConvex, U as createCallerFactory, V as createEnv, W as LazyCaller, X as createApiLeaf, Y as createServerCaller, Z as createGeneratedFunctionReference, _ as ProcedureSchedulableCallerFromRegistry, _t as ZodValidatorFromConvex, a as CreateProcedureCallerFactoryOptions, at as initCRPC, b as createGenericCallerFactory, bt as withSystemFields, c as GeneratedRegistryCallerFactory, ct as extractPathParams, d as GeneratedRegistryHandlerForContext, dt as ConvexValidatorFromZod, et as CRPCFunctionTypeHint, f as ProcedureActionCallerFromRegistry, ft as ConvexValidatorFromZodOutput, g as ProcedureFromFunctionReference, gt as ZodFromValidatorBase, h as ProcedureDefinition, ht as Zid, i as registerProcedureNameLookup, it as createMiddlewareFactory, j as CRPCErrorCode, k as inferApiOutputs, l as GeneratedRegistryCallerForContext, lt as handleHttpError, m as ProcedureCallerFromRegistry, mt as ZCustomCtx, n as ProcedureNameLookup, nt as ProcedureBuilder, o as GeneratedProcedureRegistry, ot as HttpProcedureBuilder, p as ProcedureCaller, pt as CustomBuilder, q as CallerOpts, r as inferProcedureNameFromCallsite, rt as QueryProcedureBuilder, s as GeneratedProcedureRegistryEntry, st as createHttpProcedureBuilder, t as ProcedureNameEntry, tt as MutationProcedureBuilder, u as GeneratedRegistryHandlerFactory, ut as matchPathParams, v as ProcedureScheduleCallerFromRegistry, vt as convexToZod, w as defineProcedure, wt as zid, x as createGenericHandlerFactory, xt as zCustomAction, y as createGeneratedRegistryRuntime, yt as convexToZodFields, z as CreateEnvOptions } from "../procedure-name-Cy1AxayA.js";
1
+ import { $ as ActionProcedureBuilder, A as CRPCError, B as RuntimeEnv, C as createProcedureHandlerFactory, Ct as zCustomQuery, D as WithHttpRouter, Dt as zodToConvex, E as typedProcedureResolver, Et as zodOutputToConvexFields, F as getCRPCErrorFromUnknown, G as createLazyCaller, H as ConvexContext, I as getHTTPStatusCodeFromError, J as ServerCaller, K as CallerMeta, L as isCRPCError, M as CRPCErrorData, N as CRPC_ERROR_CODES_BY_KEY, O as inferApiInputs, Ot as zodToConvexFields, P as CRPC_ERROR_CODE_TO_HTTP, Q as getGeneratedValue, R as toCRPCError, S as createProcedureCallerFactory, St as zCustomMutation, T as getGeneratedFunctionReference, Tt as zodOutputToConvex, U as createCallerFactory, V as createEnv, W as LazyCaller, X as createApiLeaf, Y as createServerCaller, Z as createGeneratedFunctionReference, _ as ProcedureSchedulableCallerFromRegistry, _t as ZodValidatorFromConvex, a as CreateProcedureCallerFactoryOptions, at as initCRPC, b as createGenericCallerFactory, bt as withSystemFields, c as GeneratedRegistryCallerFactory, ct as extractPathParams, d as GeneratedRegistryHandlerForContext, dt as ConvexValidatorFromZod, et as CRPCFunctionTypeHint, f as ProcedureActionCallerFromRegistry, ft as ConvexValidatorFromZodOutput, g as ProcedureFromFunctionReference, gt as ZodFromValidatorBase, h as ProcedureDefinition, ht as Zid, i as registerProcedureNameLookup, it as createMiddlewareFactory, j as CRPCErrorCode, k as inferApiOutputs, l as GeneratedRegistryCallerForContext, lt as handleHttpError, m as ProcedureCallerFromRegistry, mt as ZCustomCtx, n as ProcedureNameLookup, nt as ProcedureBuilder, o as GeneratedProcedureRegistry, ot as HttpProcedureBuilder, p as ProcedureCaller, pt as CustomBuilder, q as CallerOpts, r as inferProcedureNameFromCallsite, rt as QueryProcedureBuilder, s as GeneratedProcedureRegistryEntry, st as createHttpProcedureBuilder, t as ProcedureNameEntry, tt as MutationProcedureBuilder, u as GeneratedRegistryHandlerFactory, ut as matchPathParams, v as ProcedureScheduleCallerFromRegistry, vt as convexToZod, w as defineProcedure, wt as zid, x as createGenericHandlerFactory, xt as zCustomAction, y as createGeneratedRegistryRuntime, yt as convexToZodFields, z as CreateEnvOptions } from "../procedure-name-Bo5KMcqc.js";
2
2
  import { C as HttpProcedure, D as ProcedureMeta, E as InferHttpInput, S as HttpMethod, T as HttpRouteDefinition, _ as extractRouteMap, b as HttpActionHandler, d as CRPCHttpRouter, f as HttpRouterDef, g as createHttpRouterFactory, h as createHttpRouter, m as HttpRouterWithHono, p as HttpRouterRecord, v as CRPCHonoHandler, w as HttpProcedureBuilderDef, x as HttpHandlerOpts, y as HttpActionConstructor } from "../http-types-zsMHb_QN.js";
3
3
  import { a as MergeZodObjects, c as MiddlewareMarker, d as MiddlewareProcedureType, f as MiddlewareResult, g as UnsetMarker, h as Simplify, i as IntersectIfDefined, l as MiddlewareNext, m as ResolveIfSet, n as AnyMiddlewareBuilder, o as MiddlewareBuilder, p as Overwrite, r as GetRawInputFn, s as MiddlewareFunction, t as AnyMiddleware, u as MiddlewareProcedureInfo } from "../types-CnTpHR1F.js";
4
4
  import { a as isMutationCtx, c as isSchedulerCtx, d as requireQueryCtx, f as requireRunMutationCtx, i as isActionCtx, l as requireActionCtx, n as RunMutationCtx, o as isQueryCtx, p as requireSchedulerCtx, r as SchedulerCtx, s as isRunMutationCtx, t as GenericCtx, u as requireMutationCtx } from "../context-utils-BBUtBqjN.js";
@@ -1,6 +1,6 @@
1
1
  import { a as isMutationCtx, c as isSchedulerCtx, d as requireQueryCtx, f as requireRunMutationCtx, i as isActionCtx, l as requireActionCtx, n as createGeneratedFunctionReference, o as isQueryCtx, p as requireSchedulerCtx, r as getGeneratedValue, s as isRunMutationCtx, t as createApiLeaf, u as requireMutationCtx } from "../api-entry-N3nBOlI2.js";
2
- import { n as createLazyCaller, r as createServerCaller, t as createCallerFactory } from "../caller-factory-NEfgD5E0.js";
3
- import { A as zid, C as toCRPCError, D as zCustomAction, E as withSystemFields, M as zodOutputToConvexFields, N as zodToConvex, O as zCustomMutation, P as zodToConvexFields, S as isCRPCError, T as convexToZodFields, _ as CRPCError, a as createMiddlewareFactory, b as getCRPCErrorFromUnknown, c as registerProcedureNameLookup, d as createHttpRouterFactory, f as extractRouteMap, g as matchPathParams, h as handleHttpError, i as QueryProcedureBuilder, j as zodOutputToConvex, k as zCustomQuery, l as HttpRouterWithHono, m as extractPathParams, n as MutationProcedureBuilder, o as initCRPC, p as createHttpProcedureBuilder, r as ProcedureBuilder, s as inferProcedureNameFromCallsite, t as ActionProcedureBuilder, u as createHttpRouter, v as CRPC_ERROR_CODES_BY_KEY, w as convexToZod, x as getHTTPStatusCodeFromError, y as CRPC_ERROR_CODE_TO_HTTP } from "../builder-DBgto1yn.js";
4
- import { a as createProcedureHandlerFactory, c as typedProcedureResolver, i as createProcedureCallerFactory, l as createEnv, n as createGenericCallerFactory, o as defineProcedure, r as createGenericHandlerFactory, s as getGeneratedFunctionReference, t as createGeneratedRegistryRuntime } from "../procedure-caller-9m6NBxQu.js";
2
+ import { n as createLazyCaller, r as createServerCaller, t as createCallerFactory } from "../caller-factory-DHywSoGZ.js";
3
+ import { A as zid, C as toCRPCError, D as zCustomAction, E as withSystemFields, M as zodOutputToConvexFields, N as zodToConvex, O as zCustomMutation, P as zodToConvexFields, S as isCRPCError, T as convexToZodFields, _ as CRPCError, a as createMiddlewareFactory, b as getCRPCErrorFromUnknown, c as registerProcedureNameLookup, d as createHttpRouterFactory, f as extractRouteMap, g as matchPathParams, h as handleHttpError, i as QueryProcedureBuilder, j as zodOutputToConvex, k as zCustomQuery, l as HttpRouterWithHono, m as extractPathParams, n as MutationProcedureBuilder, o as initCRPC, p as createHttpProcedureBuilder, r as ProcedureBuilder, s as inferProcedureNameFromCallsite, t as ActionProcedureBuilder, u as createHttpRouter, v as CRPC_ERROR_CODES_BY_KEY, w as convexToZod, x as getHTTPStatusCodeFromError, y as CRPC_ERROR_CODE_TO_HTTP } from "../builder-f4F_NRvK.js";
4
+ import { a as createProcedureHandlerFactory, c as typedProcedureResolver, i as createProcedureCallerFactory, l as createEnv, n as createGenericCallerFactory, o as defineProcedure, r as createGenericHandlerFactory, s as getGeneratedFunctionReference, t as createGeneratedRegistryRuntime } from "../procedure-caller-Rj6z3ai7.js";
5
5
 
6
6
  export { ActionProcedureBuilder, CRPCError, CRPC_ERROR_CODES_BY_KEY, CRPC_ERROR_CODE_TO_HTTP, HttpRouterWithHono, MutationProcedureBuilder, ProcedureBuilder, QueryProcedureBuilder, convexToZod, convexToZodFields, createApiLeaf, createCallerFactory, createEnv, createGeneratedFunctionReference, createGeneratedRegistryRuntime, createGenericCallerFactory, createGenericHandlerFactory, createHttpProcedureBuilder, createHttpRouter, createHttpRouterFactory, createLazyCaller, createMiddlewareFactory, createProcedureCallerFactory, createProcedureHandlerFactory, createServerCaller, defineProcedure, extractPathParams, extractRouteMap, getCRPCErrorFromUnknown, getGeneratedFunctionReference, getGeneratedValue, getHTTPStatusCodeFromError, handleHttpError, inferProcedureNameFromCallsite, initCRPC, isActionCtx, isCRPCError, isMutationCtx, isQueryCtx, isRunMutationCtx, isSchedulerCtx, matchPathParams, registerProcedureNameLookup, requireActionCtx, requireMutationCtx, requireQueryCtx, requireRunMutationCtx, requireSchedulerCtx, toCRPCError, typedProcedureResolver, withSystemFields, zCustomAction, zCustomMutation, zCustomQuery, zid, zodOutputToConvex, zodOutputToConvexFields, zodToConvex, zodToConvexFields };
@@ -57,7 +57,10 @@ const useConvexAuthBridge = () => useContext(ConvexAuthBridgeContext);
57
57
  /** Decode JWT expiration (ms timestamp) from token */
58
58
  function decodeJwtExp(token) {
59
59
  try {
60
- const payload = JSON.parse(atob(token.split(".")[1]));
60
+ const segment = token.split(".")[1];
61
+ if (!segment) return null;
62
+ const binary = atob(segment.replaceAll("-", "+").replaceAll("_", "/"));
63
+ const payload = JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0))));
61
64
  return payload.exp ? payload.exp * 1e3 : null;
62
65
  } catch {
63
66
  return null;
@@ -699,6 +702,10 @@ const getTransformer = (transformer) => {
699
702
  transformerCache.set(cacheKey, resolved);
700
703
  return resolved;
701
704
  };
705
+ /**
706
+ * Encode request payloads (input direction).
707
+ */
708
+ const encodeWire = (value, transformer) => getTransformer(transformer).input.serialize(value);
702
709
 
703
710
  //#endregion
704
711
  //#region src/solid/http-proxy.ts
@@ -954,7 +961,7 @@ function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
954
961
  cursor: null,
955
962
  limit
956
963
  };
957
- const finalEnabled = enabled === false || isSkip ? false : void 0;
964
+ const finalEnabled = isSkip ? false : enabled;
958
965
  return {
959
966
  queryKey: [
960
967
  "convexQuery",
@@ -967,7 +974,7 @@ function convexInfiniteQueryOptions(funcRef, args, opts = {}, meta) {
967
974
  refetchOnReconnect: false,
968
975
  refetchOnWindowFocus: false,
969
976
  ...queryOptions,
970
- ...finalEnabled === false ? { enabled: false } : {},
977
+ ...finalEnabled === void 0 ? {} : { enabled: finalEnabled },
971
978
  meta: {
972
979
  authType,
973
980
  skipUnauth,
@@ -1785,12 +1792,19 @@ function isConvexAction$1(queryKey) {
1785
1792
  return queryKey.length >= 2 && queryKey[0] === "convexAction";
1786
1793
  }
1787
1794
  /**
1795
+ * Serialize args the same way they go over the wire, so non-native Convex
1796
+ * types the transformer supports (e.g. `Date`) can be hashed.
1797
+ */
1798
+ function hashArgs(args) {
1799
+ return JSON.stringify(convexToJson(encodeWire(args)));
1800
+ }
1801
+ /**
1788
1802
  * Create stable hash for Convex query keys.
1789
1803
  * Uses Convex's JSON serialization for consistent argument hashing.
1790
1804
  */
1791
1805
  function hashConvexQuery(queryKey) {
1792
1806
  const [, funcName, args] = queryKey;
1793
- return `convexQuery|${funcName}|${JSON.stringify(convexToJson(args))}`;
1807
+ return `convexQuery|${funcName}|${hashArgs(args)}`;
1794
1808
  }
1795
1809
  /**
1796
1810
  * Create stable hash for Convex action keys.
@@ -1798,7 +1812,7 @@ function hashConvexQuery(queryKey) {
1798
1812
  */
1799
1813
  function hashConvexAction(queryKey) {
1800
1814
  const [, funcName, args] = queryKey;
1801
- return `convexAction|${funcName}|${JSON.stringify(convexToJson(args))}`;
1815
+ return `convexAction|${funcName}|${hashArgs(args)}`;
1802
1816
  }
1803
1817
 
1804
1818
  //#endregion