@rangojs/router 0.0.0-experimental.fa8a383a → 0.0.0-experimental.ffbe1b7f

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 (37) hide show
  1. package/dist/vite/index.js +17 -2
  2. package/package.json +1 -1
  3. package/skills/cache-guide/SKILL.md +32 -0
  4. package/skills/caching/SKILL.md +8 -0
  5. package/skills/loader/SKILL.md +52 -42
  6. package/skills/parallel/SKILL.md +67 -0
  7. package/skills/route/SKILL.md +31 -0
  8. package/skills/typesafety/SKILL.md +10 -0
  9. package/src/browser/partial-update.ts +11 -0
  10. package/src/browser/prefetch/queue.ts +61 -29
  11. package/src/browser/prefetch/resource-ready.ts +77 -0
  12. package/src/browser/react/NavigationProvider.tsx +5 -3
  13. package/src/cache/cache-runtime.ts +15 -11
  14. package/src/cache/cache-scope.ts +46 -5
  15. package/src/cache/taint.ts +55 -0
  16. package/src/context-var.ts +72 -2
  17. package/src/route-definition/helpers-types.ts +6 -5
  18. package/src/router/handler-context.ts +31 -8
  19. package/src/router/loader-resolution.ts +7 -1
  20. package/src/router/match-middleware/background-revalidation.ts +12 -1
  21. package/src/router/match-middleware/cache-lookup.ts +46 -6
  22. package/src/router/match-middleware/cache-store.ts +21 -4
  23. package/src/router/match-result.ts +11 -5
  24. package/src/router/metrics.ts +6 -1
  25. package/src/router/middleware-types.ts +6 -2
  26. package/src/router/middleware.ts +2 -2
  27. package/src/router/router-context.ts +1 -0
  28. package/src/router/segment-resolution/fresh.ts +37 -14
  29. package/src/router/segment-resolution/helpers.ts +29 -24
  30. package/src/router/segment-resolution/revalidation.ts +43 -19
  31. package/src/router/types.ts +1 -0
  32. package/src/router.ts +1 -0
  33. package/src/rsc/handler.ts +0 -9
  34. package/src/server/context.ts +12 -0
  35. package/src/server/request-context.ts +42 -8
  36. package/src/types/handler-context.ts +120 -22
  37. package/src/types/loader-types.ts +4 -4
@@ -15,7 +15,12 @@ function formatMs(value: number): string {
15
15
  }
16
16
 
17
17
  function sortMetrics(metrics: PerformanceMetric[]): PerformanceMetric[] {
18
- return [...metrics].sort((a, b) => a.startTime - b.startTime);
18
+ return [...metrics].sort((a, b) => {
19
+ // handler:total always goes last (it wraps everything)
20
+ if (a.label === "handler:total") return 1;
21
+ if (b.label === "handler:total") return -1;
22
+ return a.startTime - b.startTime;
23
+ });
19
24
  }
20
25
 
21
26
  interface Span {
@@ -27,8 +27,12 @@ type GetVariableFn = {
27
27
  * Set variable function type
28
28
  */
29
29
  type SetVariableFn = {
30
- <T>(contextVar: ContextVar<T>, value: T): void;
31
- <K extends keyof DefaultVars>(key: K, value: DefaultVars[K]): void;
30
+ <T>(contextVar: ContextVar<T>, value: T, options?: { cache?: boolean }): void;
31
+ <K extends keyof DefaultVars>(
32
+ key: K,
33
+ value: DefaultVars[K],
34
+ options?: { cache?: boolean },
35
+ ): void;
32
36
  };
33
37
 
34
38
  /**
@@ -204,8 +204,8 @@ export function createMiddlewareContext<TEnv>(
204
204
  get: ((keyOrVar: any) =>
205
205
  contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
206
206
 
207
- set: ((keyOrVar: any, value: unknown) => {
208
- contextSet(variables, keyOrVar, value);
207
+ set: ((keyOrVar: any, value: unknown, options?: any) => {
208
+ contextSet(variables, keyOrVar, value, options);
209
209
  }) as MiddlewareContext<TEnv>["set"],
210
210
 
211
211
  var: variables as MiddlewareContext<TEnv>["var"],
@@ -210,6 +210,7 @@ export interface RouterContext<TEnv = any> {
210
210
  params: Record<string, string>,
211
211
  handlerContext: HandlerContext<any, TEnv>,
212
212
  loaderPromises: Map<string, Promise<any>>,
213
+ options?: { skipLoaders?: boolean },
213
214
  ) => Promise<ResolvedSegment[]>;
214
215
 
215
216
  // Generator-based simple resolution
@@ -30,7 +30,7 @@ import {
30
30
  } from "./helpers.js";
31
31
  import { getRouterContext } from "../router-context.js";
32
32
  import { resolveSink, safeEmit } from "../telemetry.js";
33
- import { track } from "../../server/context.js";
33
+ import { track, RSCRouterContext } from "../../server/context.js";
34
34
 
35
35
  // ---------------------------------------------------------------------------
36
36
  // Streamed handler telemetry
@@ -100,9 +100,7 @@ export async function resolveLoaders<TEnv>(
100
100
 
101
101
  if (!loadingDisabled) {
102
102
  // Streaming loaders: promises kick off now, settle during RSC serialization.
103
- // No per-loader timing here settlement happens asynchronously during
104
- // RSC/SSR stream consumption, after the perf timeline is logged.
105
- return loaderEntries.map((loaderEntry, i) => {
103
+ const segments = loaderEntries.map((loaderEntry, i) => {
106
104
  const { loader } = loaderEntry;
107
105
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
108
106
  return {
@@ -122,11 +120,12 @@ export async function resolveLoaders<TEnv>(
122
120
  belongsToRoute,
123
121
  };
124
122
  });
123
+
124
+ return segments;
125
125
  }
126
126
 
127
127
  // Loading disabled: still start all loaders in parallel, but only emit
128
128
  // settled promises so handlers don't stream loading placeholders.
129
- // We can measure actual execution time here since we await all loaders.
130
129
  const pendingLoaderData = loaderEntries.map((loaderEntry) => {
131
130
  const start = performance.now();
132
131
  const promise = resolveLoaderData(loaderEntry, ctx, ctx.pathname);
@@ -344,7 +343,7 @@ export async function resolveSegment<TEnv>(
344
343
  namespace: entry.id,
345
344
  type: "route",
346
345
  index: 0,
347
- component,
346
+ component: component ?? null,
348
347
  loading: entry.loading === false ? null : entry.loading,
349
348
  transition: entry.transition,
350
349
  params,
@@ -580,6 +579,13 @@ export async function resolveAllSegments<TEnv>(
580
579
  } catch {}
581
580
 
582
581
  for (const entry of entries) {
582
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
583
+ // can guard non-cacheable variable reads. Also guards response-level
584
+ // side effects (headers.set). Persists for all descendant entries.
585
+ if (entry.type === "cache") {
586
+ const store = RSCRouterContext.getStore();
587
+ if (store) store.insideCacheScope = true;
588
+ }
583
589
  const doneEntry = track(`segment:${entry.id}`, 1);
584
590
  const resolvedSegments = await resolveWithErrorBoundary(
585
591
  entry,
@@ -624,20 +630,37 @@ export async function resolveLoadersOnly<TEnv>(
624
630
  deps: SegmentResolutionDeps<TEnv>,
625
631
  ): Promise<ResolvedSegment[]> {
626
632
  const loaderSegments: ResolvedSegment[] = [];
633
+ const seenIds = new Set<string>();
627
634
 
628
635
  async function collectEntryLoaders(
629
636
  entry: EntryData,
630
637
  belongsToRoute: boolean,
631
638
  shortCodeOverride?: string,
632
639
  ): Promise<void> {
633
- const segments = await resolveLoaders(
634
- entry,
635
- context,
636
- belongsToRoute,
637
- deps,
638
- shortCodeOverride,
639
- );
640
- loaderSegments.push(...segments);
640
+ // Skip if all loaders from this entry have already been resolved
641
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
642
+ const entryLoaders = entry.loader ?? [];
643
+ const sc = shortCodeOverride ?? entry.shortCode;
644
+ const allAlreadySeen =
645
+ entryLoaders.length > 0 &&
646
+ entryLoaders.every((le, i) =>
647
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
648
+ );
649
+ if (!allAlreadySeen) {
650
+ const segments = await resolveLoaders(
651
+ entry,
652
+ context,
653
+ belongsToRoute,
654
+ deps,
655
+ shortCodeOverride,
656
+ );
657
+ for (const seg of segments) {
658
+ if (!seenIds.has(seg.id)) {
659
+ seenIds.add(seg.id);
660
+ loaderSegments.push(seg);
661
+ }
662
+ }
663
+ }
641
664
 
642
665
  const seenParallelEntryIds = new Set<string>();
643
666
  for (const parallelEntry of getParallelEntries(entry.parallel)) {
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -180,34 +180,39 @@ export function catchSegmentError<TEnv>(
180
180
 
181
181
  if (error instanceof DataNotFoundError) {
182
182
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
183
+ // Fall back to router's notFound component, then a plain default
184
+ const notFoundOption = deps.notFoundComponent;
185
+ const defaultFallback =
186
+ typeof notFoundOption === "function"
187
+ ? notFoundOption({ pathname: pathname ?? "" })
188
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
189
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
190
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
191
+ const notFoundInfo = createNotFoundInfo(
192
+ error,
193
+ entry.shortCode,
194
+ entry.type,
195
+ pathname,
196
+ );
191
197
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
198
+ reportError(true, {
199
+ notFound: true,
200
+ message: notFoundInfo.message,
201
+ });
196
202
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
203
+ debugLog("segment", "notFound boundary handled error", {
204
+ segmentId: entry.shortCode,
205
+ message: notFoundInfo.message,
206
+ });
201
207
 
202
- setResponseStatus(404);
208
+ setResponseStatus(404);
203
209
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
210
+ return createNotFoundSegment(
211
+ notFoundInfo,
212
+ effectiveNotFoundFallback,
213
+ entry,
214
+ params,
215
+ );
211
216
  }
212
217
 
213
218
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -42,6 +42,7 @@ import {
42
42
  import { getRouterContext } from "../router-context.js";
43
43
  import { resolveSink, safeEmit } from "../telemetry.js";
44
44
  import { track } from "../../server/context.js";
45
+ import { RSCRouterContext } from "../../server/context.js";
45
46
 
46
47
  // ---------------------------------------------------------------------------
47
48
  // Telemetry helpers
@@ -262,29 +263,46 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
262
263
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
263
264
  const allLoaderSegments: ResolvedSegment[] = [];
264
265
  const allMatchedIds: string[] = [];
266
+ const seenIds = new Set<string>();
265
267
 
266
268
  async function collectEntryLoaders(
267
269
  entry: EntryData,
268
270
  belongsToRoute: boolean,
269
271
  shortCodeOverride?: string,
270
272
  ): Promise<void> {
271
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
272
- entry,
273
- context,
274
- belongsToRoute,
275
- clientSegmentIds,
276
- prevParams,
277
- request,
278
- prevUrl,
279
- nextUrl,
280
- routeKey,
281
- deps,
282
- actionContext,
283
- shortCodeOverride,
284
- stale,
285
- );
286
- allLoaderSegments.push(...segments);
287
- allMatchedIds.push(...matchedIds);
273
+ // Skip if all loaders from this entry have already been resolved
274
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
275
+ const loaderEntries = entry.loader ?? [];
276
+ const sc = shortCodeOverride ?? entry.shortCode;
277
+ const allAlreadySeen =
278
+ loaderEntries.length > 0 &&
279
+ loaderEntries.every((le, i) =>
280
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
281
+ );
282
+ if (!allAlreadySeen) {
283
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
284
+ entry,
285
+ context,
286
+ belongsToRoute,
287
+ clientSegmentIds,
288
+ prevParams,
289
+ request,
290
+ prevUrl,
291
+ nextUrl,
292
+ routeKey,
293
+ deps,
294
+ actionContext,
295
+ shortCodeOverride,
296
+ stale,
297
+ );
298
+ for (const seg of segments) {
299
+ if (!seenIds.has(seg.id)) {
300
+ seenIds.add(seg.id);
301
+ allLoaderSegments.push(seg);
302
+ }
303
+ }
304
+ allMatchedIds.push(...matchedIds);
305
+ }
288
306
 
289
307
  const seenParallelEntryIds = new Set<string>();
290
308
  for (const parallelEntry of getParallelEntries(entry.parallel)) {
@@ -705,10 +723,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
705
723
  () => null,
706
724
  );
707
725
 
726
+ // Normalize void handlers (undefined) to null so the reconciler's
727
+ // component === null checks work consistently for both void and explicit null.
708
728
  const resolvedComponent =
709
729
  component && typeof component === "object" && "content" in component
710
- ? (component as { content: ReactNode }).content
711
- : component;
730
+ ? ((component as { content: ReactNode }).content ?? null)
731
+ : (component ?? null);
712
732
 
713
733
  const segment: ResolvedSegment = {
714
734
  id: entry.shortCode,
@@ -1229,6 +1249,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1229
1249
  }
1230
1250
 
1231
1251
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1252
+ if (entry.type === "cache") {
1253
+ const store = RSCRouterContext.getStore();
1254
+ if (store) store.insideCacheScope = true;
1255
+ }
1232
1256
  const doneEntry = track(`segment:${entry.id}`, 1);
1233
1257
  const resolved = await resolveWithErrorBoundary(
1234
1258
  nonParallelEntry,
@@ -96,6 +96,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
96
96
  findNearestNotFoundBoundary: (
97
97
  entry: EntryData | null,
98
98
  ) => ReactNode | NotFoundBoundaryHandler | null;
99
+ notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
99
100
  callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
100
101
  }
101
102
 
package/src/router.ts CHANGED
@@ -526,6 +526,7 @@ export function createRouter<TEnv = any>(
526
526
  trackHandler,
527
527
  findNearestErrorBoundary,
528
528
  findNearestNotFoundBoundary,
529
+ notFoundComponent: notFound,
529
530
  callOnError,
530
531
  };
531
532
 
@@ -490,7 +490,6 @@ export function createRSCHandler<
490
490
  // has completed so :post spans are captured in the timeline.
491
491
  // Handler timing parts are always emitted (even without debug metrics)
492
492
  // so non-debug requests still get bootstrap Server-Timing entries.
493
- const finalizeStart = performance.now();
494
493
  const handlerTimingArr: string[] = variables.__handlerTiming || [];
495
494
  // Preserve any existing Server-Timing set by response routes or middleware
496
495
  const existingTiming = response.headers.get("Server-Timing");
@@ -507,14 +506,6 @@ export function createRSCHandler<
507
506
  const totalStart = earlyMetricsStore
508
507
  ? handlerStart
509
508
  : metricsStore.requestStart;
510
- // response-finalize measures the gap between render completion and
511
- // handler return: header assembly, onResponse callbacks, etc.
512
- appendMetric(
513
- metricsStore,
514
- "response-finalize",
515
- finalizeStart,
516
- performance.now() - finalizeStart,
517
- );
518
509
  appendMetric(
519
510
  metricsStore,
520
511
  "handler:total",
@@ -273,6 +273,9 @@ interface HelperContext {
273
273
  string,
274
274
  import("../cache/profile-registry.js").CacheProfile
275
275
  >;
276
+ /** True when resolving handlers inside a cache() DSL boundary.
277
+ * Read by ctx.get() to guard non-cacheable variable reads. */
278
+ insideCacheScope?: boolean;
276
279
  }
277
280
  // Use a global symbol key so the AsyncLocalStorage instance survives HMR
278
281
  // module re-evaluation. Without this, Vite's RSC module runner may create
@@ -666,3 +669,12 @@ export function track(label: string, depth?: number): () => void {
666
669
  });
667
670
  };
668
671
  }
672
+
673
+ /**
674
+ * Check if the current execution is inside a cache() DSL boundary.
675
+ * Returns false inside loader execution — loaders are always fresh
676
+ * (never cached), so non-cacheable reads are safe.
677
+ */
678
+ export function isInsideCacheScope(): boolean {
679
+ return RSCRouterContext.getStore()?.insideCacheScope === true;
680
+ }
@@ -20,7 +20,12 @@ import type {
20
20
  DefaultRouteName,
21
21
  } from "../types/global-namespace.js";
22
22
  import type { Handle } from "../handle.js";
23
- import { type ContextVar, contextGet, contextSet } from "../context-var.js";
23
+ import {
24
+ type ContextVar,
25
+ contextGet,
26
+ contextSet,
27
+ isNonCacheable,
28
+ } from "../context-var.js";
24
29
  import { createHandleStore, type HandleStore } from "./handle-store.js";
25
30
  import { isHandle } from "../handle.js";
26
31
  import { track, type MetricsStore } from "./context.js";
@@ -30,6 +35,7 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
30
35
  import { THEME_COOKIE } from "../theme/constants.js";
31
36
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
32
37
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
38
+ import { isInsideCacheScope } from "./context.js";
33
39
  import {
34
40
  createReverseFunction,
35
41
  stripInternalParams,
@@ -72,8 +78,12 @@ export interface RequestContext<
72
78
  };
73
79
  /** Set a variable (shared with middleware and handlers) */
74
80
  set: {
75
- <T>(contextVar: ContextVar<T>, value: T): void;
76
- <K extends string>(key: K, value: any): void;
81
+ <T>(
82
+ contextVar: ContextVar<T>,
83
+ value: T,
84
+ options?: { cache?: boolean },
85
+ ): void;
86
+ <K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
77
87
  };
78
88
  /**
79
89
  * Route params (populated after route matching)
@@ -506,6 +516,18 @@ export function createRequestContext<TEnv>(
506
516
  responseCookieCache = null;
507
517
  };
508
518
 
519
+ // Guard: throw if a response-level side effect is called inside a cache() scope.
520
+ // Uses ALS to detect the scope (set during segment resolution).
521
+ function assertNotInsideCacheScopeALS(methodName: string): void {
522
+ if (isInsideCacheScope()) {
523
+ throw new Error(
524
+ `ctx.${methodName}() cannot be called inside a cache() boundary. ` +
525
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
526
+ `Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
527
+ );
528
+ }
529
+ }
530
+
509
531
  // Effective cookie read: response stub Set-Cookie wins, then original header.
510
532
  // The stub IS the source of truth for same-request mutations.
511
533
  const effectiveCookie = (name: string): string | undefined => {
@@ -570,11 +592,19 @@ export function createRequestContext<TEnv>(
570
592
  pathname: url.pathname,
571
593
  searchParams: cleanUrl.searchParams,
572
594
  var: variables,
573
- get: ((keyOrVar: any) =>
574
- contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
575
- set: ((keyOrVar: any, value: any) => {
595
+ get: ((keyOrVar: any) => {
596
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
597
+ throw new Error(
598
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
599
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
600
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
601
+ );
602
+ }
603
+ return contextGet(variables, keyOrVar);
604
+ }) as RequestContext<TEnv>["get"],
605
+ set: ((keyOrVar: any, value: any, options?: any) => {
576
606
  assertNotInsideCacheExec(ctx, "set");
577
- contextSet(variables, keyOrVar, value);
607
+ contextSet(variables, keyOrVar, value, options);
578
608
  }) as RequestContext<TEnv>["set"],
579
609
  params: {} as Record<string, string>,
580
610
 
@@ -612,6 +642,7 @@ export function createRequestContext<TEnv>(
612
642
 
613
643
  setCookie(name: string, value: string, options?: CookieOptions): void {
614
644
  assertNotInsideCacheExec(ctx, "setCookie");
645
+ assertNotInsideCacheScopeALS("setCookie");
615
646
  stubResponse.headers.append(
616
647
  "Set-Cookie",
617
648
  serializeCookieValue(name, value, options),
@@ -624,6 +655,7 @@ export function createRequestContext<TEnv>(
624
655
  options?: Pick<CookieOptions, "domain" | "path">,
625
656
  ): void {
626
657
  assertNotInsideCacheExec(ctx, "deleteCookie");
658
+ assertNotInsideCacheScopeALS("deleteCookie");
627
659
  stubResponse.headers.append(
628
660
  "Set-Cookie",
629
661
  serializeCookieValue(name, "", { ...options, maxAge: 0 }),
@@ -633,11 +665,13 @@ export function createRequestContext<TEnv>(
633
665
 
634
666
  header(name: string, value: string): void {
635
667
  assertNotInsideCacheExec(ctx, "header");
668
+ assertNotInsideCacheScopeALS("header");
636
669
  stubResponse.headers.set(name, value);
637
670
  },
638
671
 
639
672
  setStatus(status: number): void {
640
673
  assertNotInsideCacheExec(ctx, "setStatus");
674
+ assertNotInsideCacheScopeALS("setStatus");
641
675
  stubResponse = new Response(null, {
642
676
  status,
643
677
  headers: stubResponse.headers,
@@ -676,6 +710,7 @@ export function createRequestContext<TEnv>(
676
710
 
677
711
  onResponse(callback: (response: Response) => Response): void {
678
712
  assertNotInsideCacheExec(ctx, "onResponse");
713
+ assertNotInsideCacheScopeALS("onResponse");
679
714
  this._onResponseCallbacks.push(callback);
680
715
  },
681
716
 
@@ -906,7 +941,6 @@ export function createUseFunction<TEnv>(
906
941
  ),
907
942
  };
908
943
 
909
- // Start loader execution with tracking
910
944
  const doneLoader = track(`loader:${loader.$$id}`, 2);
911
945
  const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
912
946
  doneLoader();