@rangojs/router 0.23.0 → 0.24.1

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 (132) hide show
  1. package/dist/bin/rango.js +4 -3
  2. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  3. package/dist/types/browser/partial-update.d.ts +6 -4
  4. package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
  5. package/dist/types/browser/react/context.d.ts +3 -2
  6. package/dist/types/browser/types.d.ts +5 -1
  7. package/dist/types/build/route-types/scan-filter.d.ts +7 -1
  8. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  9. package/dist/types/cache/document-cache.d.ts +2 -10
  10. package/dist/types/client-urls/types.d.ts +8 -0
  11. package/dist/types/context-var.d.ts +12 -0
  12. package/dist/types/errors.d.ts +10 -0
  13. package/dist/types/handles/pushed-by.d.ts +8 -0
  14. package/dist/types/index.d.ts +4 -0
  15. package/dist/types/index.rsc.d.ts +2 -0
  16. package/dist/types/response-utils.d.ts +7 -0
  17. package/dist/types/reverse.d.ts +8 -8
  18. package/dist/types/route-definition/rewrite.d.ts +56 -0
  19. package/dist/types/route-map-builder.d.ts +7 -2
  20. package/dist/types/router/basename.d.ts +7 -0
  21. package/dist/types/router/handler-context.d.ts +1 -5
  22. package/dist/types/router/internal-params.d.ts +8 -0
  23. package/dist/types/router/middleware-types.d.ts +15 -0
  24. package/dist/types/router/revalidation.d.ts +1 -2
  25. package/dist/types/router/route-trie-builder.d.ts +10 -0
  26. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  27. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
  28. package/dist/types/rsc/full-payload.d.ts +11 -0
  29. package/dist/types/rsc/handler.d.ts +33 -0
  30. package/dist/types/rsc/helpers.d.ts +8 -0
  31. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  32. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  33. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  34. package/dist/types/server/request-context.d.ts +39 -2
  35. package/dist/types/server/rewrite-trail.d.ts +49 -0
  36. package/dist/types/server/slot-accessors.d.ts +29 -0
  37. package/dist/types/testing/index.d.ts +1 -1
  38. package/dist/types/testing/render-route.d.ts +8 -0
  39. package/dist/types/testing/run-middleware.d.ts +20 -0
  40. package/dist/types/testing/serve-shell-request.d.ts +2 -1
  41. package/dist/types/thenable-status.d.ts +6 -0
  42. package/dist/types/types/request-scope.d.ts +7 -0
  43. package/dist/types/vite/discovery/state.d.ts +12 -9
  44. package/dist/vite/index.js +164 -103
  45. package/package.json +1 -1
  46. package/skills/caching/SKILL.md +12 -0
  47. package/skills/catalog.json +1 -1
  48. package/skills/client-urls/SKILL.md +86 -1
  49. package/skills/hooks/SKILL.md +11 -6
  50. package/skills/hooks/data.md +3 -1
  51. package/skills/loader/SKILL.md +7 -1
  52. package/skills/middleware/SKILL.md +183 -2
  53. package/skills/ppr/SKILL.md +9 -0
  54. package/skills/rango/SKILL.md +1 -1
  55. package/skills/server-actions/SKILL.md +27 -0
  56. package/skills/testing/client-components.md +1 -0
  57. package/skills/testing/middleware.md +31 -10
  58. package/skills/testing/response-routes.md +1 -0
  59. package/skills/testing/serve-shell-request.md +19 -1
  60. package/skills/use-cache/SKILL.md +4 -2
  61. package/src/__augment-tests__/augmented.check.ts +20 -0
  62. package/src/browser/event-controller.ts +3 -0
  63. package/src/browser/merge-segment-loaders.ts +31 -18
  64. package/src/browser/navigation-bridge.ts +19 -3
  65. package/src/browser/partial-update.ts +27 -12
  66. package/src/browser/react/Link.tsx +7 -9
  67. package/src/browser/react/NavigationProvider.tsx +46 -26
  68. package/src/browser/react/context.ts +3 -2
  69. package/src/browser/react/use-router.ts +2 -4
  70. package/src/browser/server-action-bridge.ts +6 -0
  71. package/src/browser/types.ts +5 -1
  72. package/src/build/route-types/scan-filter.ts +11 -4
  73. package/src/cache/document-cache-policy.ts +131 -0
  74. package/src/cache/document-cache.ts +12 -119
  75. package/src/client-urls/client-root.tsx +3 -3
  76. package/src/client-urls/client-urls.ts +8 -1
  77. package/src/client-urls/types.ts +8 -0
  78. package/src/context-var.ts +32 -2
  79. package/src/errors.ts +19 -0
  80. package/src/handles/pushed-by.ts +26 -0
  81. package/src/index.rsc.ts +8 -0
  82. package/src/index.ts +7 -0
  83. package/src/response-utils.ts +19 -0
  84. package/src/reverse.ts +17 -14
  85. package/src/route-definition/redirect.ts +2 -15
  86. package/src/route-definition/rewrite.ts +146 -0
  87. package/src/route-map-builder.ts +14 -11
  88. package/src/router/basename.ts +20 -0
  89. package/src/router/handler-context.ts +3 -13
  90. package/src/router/intercept-resolution.ts +29 -12
  91. package/src/router/internal-params.ts +16 -0
  92. package/src/router/loader-resolution.ts +2 -0
  93. package/src/router/match-middleware/background-revalidation.ts +11 -9
  94. package/src/router/match-middleware/cache-lookup.ts +20 -15
  95. package/src/router/match-middleware/cache-store.ts +9 -11
  96. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  97. package/src/router/middleware-types.ts +16 -0
  98. package/src/router/middleware.ts +127 -49
  99. package/src/router/revalidation.ts +1 -1
  100. package/src/router/route-trie-builder.ts +28 -1
  101. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  102. package/src/router/segment-resolution/mask-nested.ts +8 -4
  103. package/src/router/segment-resolution/prefetch-deferral.ts +57 -17
  104. package/src/router/segment-resolution/revalidation.ts +57 -33
  105. package/src/rsc/full-payload.ts +37 -5
  106. package/src/rsc/handler.ts +82 -55
  107. package/src/rsc/helpers.ts +24 -0
  108. package/src/rsc/loader-fetch.ts +79 -24
  109. package/src/rsc/response-cache-serve.ts +18 -2
  110. package/src/rsc/response-route-handler.ts +4 -7
  111. package/src/rsc/rewrite-passes.ts +203 -0
  112. package/src/rsc/rsc-rendering.ts +4 -2
  113. package/src/rsc/ssr-setup.ts +11 -5
  114. package/src/server/cookie-parse.ts +36 -12
  115. package/src/server/cookie-store.ts +7 -3
  116. package/src/server/handle-store.ts +48 -17
  117. package/src/server/request-context.ts +154 -63
  118. package/src/server/request-headers.ts +4 -14
  119. package/src/server/rewrite-trail.ts +190 -0
  120. package/src/server/slot-accessors.ts +57 -0
  121. package/src/ssr/index.tsx +118 -16
  122. package/src/testing/dispatch.ts +138 -67
  123. package/src/testing/index.ts +1 -0
  124. package/src/testing/render-route.tsx +16 -3
  125. package/src/testing/run-loader.ts +2 -0
  126. package/src/testing/run-middleware.ts +56 -11
  127. package/src/testing/serve-shell-request.ts +15 -2
  128. package/src/thenable-status.ts +11 -0
  129. package/src/types/request-scope.ts +8 -0
  130. package/src/vite/discovery/state.ts +21 -13
  131. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  132. package/src/vite/router-discovery.ts +29 -27
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The pass loop that honors `throw rewrite()` (route-definition/rewrite.ts):
3
+ * one catch around the global middleware chain moves the same request
4
+ * context to the target (server/request-context.ts rebaseRequestContext) and
5
+ * runs the request again from the top, as a direct visit to the target
6
+ * would run it. Shared by the request handler (rsc/handler.ts) and the
7
+ * dispatch testing primitive, so the two cannot drift.
8
+ */
9
+
10
+ import { RewriteLoopError } from "../errors.js";
11
+ import {
12
+ describeRewriteTarget,
13
+ isRewrite,
14
+ rewriteNotSupported,
15
+ type Rewrite,
16
+ } from "../route-definition/rewrite.js";
17
+ import { prefixBasename } from "../router/basename.js";
18
+ import { createReverseFunction } from "../router/handler-context.js";
19
+ import { stripInternalParams } from "../router/internal-params.js";
20
+ import {
21
+ rebaseRequestContext,
22
+ type RequestContext,
23
+ } from "../server/request-context.js";
24
+ import { recordRewriteHop } from "../server/rewrite-trail.js";
25
+
26
+ /** Response header naming the route path a rewritten request rendered. */
27
+ export const REWRITE_HEADER = "x-rango-rewrite";
28
+
29
+ /**
30
+ * Resolve a rewrite's target against the router's route map (global names,
31
+ * no param auto-fill, as middleware `ctx.reverse`) and the request origin.
32
+ * A path is basename-prefixed like `redirect()` and must stay under the
33
+ * basename once normalized (`/../admin` cannot step out of it). The
34
+ * target's own `_rsc*` params are dropped: only the visitor's transport
35
+ * params carry over. Throws for an unknown name, a missing param, another
36
+ * origin or a path outside the basename.
37
+ */
38
+ export function resolveRewriteTarget(
39
+ rewrite: Rewrite,
40
+ routeMap: Record<string, string>,
41
+ base: URL,
42
+ basename?: string,
43
+ ): URL {
44
+ const { target } = rewrite;
45
+ const path =
46
+ "path" in target
47
+ ? prefixBasename(target.path, basename)
48
+ : // `{}`, not undefined: a route with params then throws for the
49
+ // missing ones instead of resolving to its raw pattern.
50
+ createReverseFunction(routeMap)(
51
+ target.name,
52
+ target.params ?? {},
53
+ target.search,
54
+ );
55
+ const resolved = stripInternalParams(new URL(path, base.origin));
56
+ if (resolved.origin !== base.origin) {
57
+ throw new TypeError(
58
+ `${describeRewriteTarget(target)} must name a path in this app, not another origin.`,
59
+ );
60
+ }
61
+ if (
62
+ basename &&
63
+ resolved.pathname !== basename &&
64
+ !resolved.pathname.startsWith(basename + "/")
65
+ ) {
66
+ throw new TypeError(
67
+ `${describeRewriteTarget(target)} resolves to ${resolved.pathname}, outside the router's basename ${basename}.`,
68
+ );
69
+ }
70
+ resolved.hash = "";
71
+ return resolved;
72
+ }
73
+
74
+ /**
75
+ * The raw URL of a rewritten pass: the target's path and search plus the
76
+ * visitor's internal `_rsc*` transport params, so a navigation stays a
77
+ * navigation (same segments, version, partial flag).
78
+ */
79
+ function rewriteTransportUrl(target: URL, visitorRaw: URL): URL {
80
+ const raw = new URL(target);
81
+ visitorRaw.searchParams.forEach((value, key) => {
82
+ if (key.startsWith("_rsc")) raw.searchParams.append(key, value);
83
+ });
84
+ return raw;
85
+ }
86
+
87
+ /** The next pass of a rewritten request. */
88
+ export interface RewritePass {
89
+ request: Request;
90
+ /** The pass's raw URL, `_rsc*` params included. */
91
+ url: URL;
92
+ /** The target's clean URL. */
93
+ target: URL;
94
+ hops: number;
95
+ }
96
+
97
+ /**
98
+ * Move the request context to `rewrite`'s target for the next pass: check
99
+ * that this request can be rewritten, resolve the target, count the hop,
100
+ * then rebase the context. Throws the site error for a mutation or a loader
101
+ * fetch, a resolution error, and RewriteLoopError.
102
+ */
103
+ export function applyRewrite(
104
+ rewrite: Rewrite,
105
+ passRequest: Request,
106
+ reqCtx: RequestContext<any>,
107
+ routeMap: Record<string, string>,
108
+ basename: string | undefined,
109
+ ): RewritePass {
110
+ // A loader fetch first: `load()` with a body is a POST, and its refusal
111
+ // is the loader fetch's.
112
+ if (reqCtx.originalUrl.searchParams.has("_rsc_loader")) {
113
+ throw rewriteNotSupported(rewrite, "loader-fetch");
114
+ }
115
+ if (passRequest.method !== "GET" && passRequest.method !== "HEAD") {
116
+ throw rewriteNotSupported(rewrite, "mutation");
117
+ }
118
+ const target = resolveRewriteTarget(rewrite, routeMap, reqCtx.url, basename);
119
+ const hops = recordRewriteHop(reqCtx, reqCtx.url, target);
120
+ const url = rewriteTransportUrl(target, reqCtx.originalUrl);
121
+ const request = new Request(url, passRequest);
122
+ rebaseRequestContext(reqCtx, request, url);
123
+ return { request, url, target, hops };
124
+ }
125
+
126
+ /** The response to a rewrite loop. */
127
+ export function loopDetectedResponse(): Response {
128
+ return new Response("Loop Detected", {
129
+ status: 508,
130
+ headers: { "content-type": "text/plain;charset=utf-8" },
131
+ });
132
+ }
133
+
134
+ export interface RewritePassesOptions {
135
+ reqCtx: RequestContext<any>;
136
+ /** The visitor's request and raw URL: the first pass. */
137
+ request: Request;
138
+ url: URL;
139
+ routeMap: () => Record<string, string>;
140
+ basename: string | undefined;
141
+ /** One pass: the global chain matched on `url`'s path around the core handler. */
142
+ runPass: (request: Request, url: URL) => Promise<Response>;
143
+ /** After a hop moved the context to its target. */
144
+ onHop?: (pass: RewritePass) => void;
145
+ /** Report a rewrite the request cannot take (onError, phase "routing"). */
146
+ report: (error: unknown, at: { request: Request; url: URL }) => void;
147
+ /**
148
+ * The response for a refused rewrite other than a loop (a loader fetch's
149
+ * error response); undefined rethrows the error.
150
+ */
151
+ refuse?: (error: unknown, rewrite: Rewrite) => Response | undefined;
152
+ }
153
+
154
+ export interface RewritePassesResult {
155
+ response: Response;
156
+ /** The pathname the final pass rendered, when a rewrite took the request there. */
157
+ rewrittenTo?: string;
158
+ }
159
+
160
+ /**
161
+ * Run the request pass by pass until one produces a response. A `Rewrite`
162
+ * that unwinds a pass moves the context to its target and runs the next;
163
+ * a loop answers 508; a rewrite the request cannot take is reported and
164
+ * becomes `refuse`'s response, or is thrown.
165
+ */
166
+ export async function runRewritePasses(
167
+ opts: RewritePassesOptions,
168
+ ): Promise<RewritePassesResult> {
169
+ let request = opts.request;
170
+ let url = opts.url;
171
+ let rewrittenTo: string | undefined;
172
+ for (;;) {
173
+ let rewrite: Rewrite;
174
+ try {
175
+ return { response: await opts.runPass(request, url), rewrittenTo };
176
+ } catch (error) {
177
+ if (!isRewrite(error)) throw error;
178
+ rewrite = error;
179
+ }
180
+ let pass: RewritePass;
181
+ try {
182
+ pass = applyRewrite(
183
+ rewrite,
184
+ request,
185
+ opts.reqCtx,
186
+ opts.routeMap(),
187
+ opts.basename,
188
+ );
189
+ } catch (error) {
190
+ opts.report(error, { request, url });
191
+ if (error instanceof RewriteLoopError) {
192
+ return { response: loopDetectedResponse() };
193
+ }
194
+ const refused = opts.refuse?.(error, rewrite);
195
+ if (refused) return { response: refused };
196
+ throw error;
197
+ }
198
+ request = pass.request;
199
+ url = pass.url;
200
+ rewrittenTo = pass.target.pathname;
201
+ opts.onHop?.(pass);
202
+ }
203
+ }
@@ -51,7 +51,7 @@ import {
51
51
  import type { HandlerContext } from "./handler-context.js";
52
52
  import { attachTransitionWhen } from "./attach-transition-when.js";
53
53
  import { publicRouteName } from "../route-name.js";
54
- import { buildFullPayload } from "./full-payload.js";
54
+ import { buildFullPayload, renderedLocation } from "./full-payload.js";
55
55
  import {
56
56
  capShellWindow,
57
57
  lastStoredCaptureSeq,
@@ -845,7 +845,7 @@ function* preparePayloadPlan<TEnv>(
845
845
  kind: "payload",
846
846
  payload: {
847
847
  metadata: {
848
- pathname: url.pathname,
848
+ pathname: renderedLocation(reqCtx, url).pathname,
849
849
  // routerId is serialized on every payload (including within-session
850
850
  // ones) so the frontend can read the current app/router identity. It
851
851
  // always equals the current app's id: a cross-app navigation is
@@ -1867,6 +1867,8 @@ function serveShellHit(
1867
1867
  payload.metadata.initialTheme = entry.initialTheme as
1868
1868
  | import("../theme/types.js").Theme
1869
1869
  | undefined;
1870
+ // Same for the location: the capture rendered the target's.
1871
+ payload.metadata.pathname = url.pathname;
1870
1872
  }
1871
1873
  // Full Flight render per request: hydration needs the whole payload (there
1872
1874
  // is no Flight-side resume — a React limitation, not ours).
@@ -22,6 +22,7 @@ import {
22
22
  RSC_WIRE_MIME,
23
23
  } from "../router/content-negotiation.js";
24
24
  import { _getRequestContext } from "../server/request-context.js";
25
+ import { renderedLocation } from "./full-payload.js";
25
26
 
26
27
  export type SSRSetup = readonly [SSRModule, SSRStreamMode];
27
28
 
@@ -38,6 +39,8 @@ export const SSR_SETUP_VAR = "__ssrSetup";
38
39
  * lazily so stores created after kickoff are still captured). Without a
39
40
  * getter the promises run bare — no `.then()` microtasks, no
40
41
  * `performance.now()` calls — keeping the non-debug hot path lean.
42
+ * `ssrModule` reuses a load already started (a rewritten request's earlier
43
+ * pass).
41
44
  */
42
45
  export function startSSRSetup<TEnv>(
43
46
  ctx: HandlerContext<TEnv>,
@@ -45,16 +48,17 @@ export function startSSRSetup<TEnv>(
45
48
  env: TEnv,
46
49
  url: URL,
47
50
  getMetricsStore?: () => MetricsStore | undefined,
51
+ ssrModule?: Promise<SSRModule>,
48
52
  ): Promise<SSRSetup> {
49
53
  if (!getMetricsStore) {
50
54
  return Promise.all([
51
- ctx.loadSSRModule(),
55
+ ssrModule ?? ctx.loadSSRModule(),
52
56
  ctx.resolveStreamMode(request, env, url),
53
57
  ]);
54
58
  }
55
59
  const start = performance.now();
56
60
  return Promise.all([
57
- ctx.loadSSRModule().then((mod) => {
61
+ (ssrModule ?? ctx.loadSSRModule()).then((mod) => {
58
62
  appendMetric(
59
63
  getMetricsStore(),
60
64
  "ssr:module-load",
@@ -110,7 +114,9 @@ export function createSsrHtmlStage<TEnv>(
110
114
  options.url,
111
115
  options.metricsStore,
112
116
  );
113
- const renderErrors = _getRequestContext()?._renderErrors;
117
+ const reqCtx = _getRequestContext();
118
+ const renderErrors = reqCtx?._renderErrors;
119
+ const location = renderedLocation(reqCtx, options.url);
114
120
  return {
115
121
  render: () =>
116
122
  // search: the LIVE request's query string, threaded out-of-band into
@@ -121,11 +127,11 @@ export function createSsrHtmlStage<TEnv>(
121
127
  ssrModule.renderHTML(flight.stream, {
122
128
  ...options.render,
123
129
  streamMode,
124
- search: options.url.search,
130
+ search: location.search,
125
131
  // origin: same out-of-band channel — seeds the SSR store location
126
132
  // so origin-dependent markup (Link's data-external) agrees with the
127
133
  // browser's window.location across hydration.
128
- origin: options.url.origin,
134
+ origin: location.origin,
129
135
  // A component that throws inside a Suspense boundary leaves an
130
136
  // errored boundary in a completed document; the document cache
131
137
  // reads the list before storing it.
@@ -10,23 +10,47 @@
10
10
  export function parseCookiesFromHeader(
11
11
  cookieHeader: string | null,
12
12
  ): Record<string, string> {
13
- if (!cookieHeader) return {};
14
-
15
13
  const cookies: Record<string, string> = {};
16
- const pairs = cookieHeader.split(";");
17
14
 
18
- for (const pair of pairs) {
19
- const [name, ...rest] = pair.trim().split("=");
20
- if (name) {
21
- const raw = rest.join("=");
22
- try {
23
- cookies[name] = decodeURIComponent(raw);
24
- } catch {
25
- // Malformed percent-encoding: fall back to raw value
26
- cookies[name] = raw;
15
+ if (cookieHeader) {
16
+ for (const pair of cookieHeader.split(";")) {
17
+ const [name, ...rest] = pair.trim().split("=");
18
+ if (name) {
19
+ const raw = rest.join("=");
20
+ let value: string;
21
+ try {
22
+ value = decodeURIComponent(raw);
23
+ } catch {
24
+ // Malformed percent-encoding: fall back to raw value
25
+ value = raw;
26
+ }
27
+ setCookie(cookies, name, value);
27
28
  }
28
29
  }
29
30
  }
30
31
 
32
+ // Null prototype (#1109): cookie names come from the request, so a lookup
33
+ // must never answer `constructor` or `toString` with an inherited member.
34
+ // Set once at the end: Object.create(null) starts in dictionary mode, which
35
+ // makes a spread of the jar several times slower.
36
+ Object.setPrototypeOf(cookies, null);
31
37
  return cookies;
32
38
  }
39
+
40
+ /** `cookies[name] = value`, a cookie named `__proto__` as an own key too. */
41
+ function setCookie(
42
+ cookies: Record<string, string>,
43
+ name: string,
44
+ value: string,
45
+ ): void {
46
+ if (name === "__proto__") {
47
+ Object.defineProperty(cookies, name, {
48
+ value,
49
+ writable: true,
50
+ enumerable: true,
51
+ configurable: true,
52
+ });
53
+ } else {
54
+ cookies[name] = value;
55
+ }
56
+ }
@@ -367,12 +367,16 @@ function createCookieStore(
367
367
 
368
368
  getAll(name?: string): Cookie[] {
369
369
  recordLoaderIdentityRead("cookies()");
370
- const all = ctx._readCookies();
371
370
  if (name !== undefined) {
372
- const value = all[name];
371
+ // The single-name read, not an index into the plain object
372
+ // _readCookies() returns: that would answer an inherited member.
373
+ const value = ctx._readCookie(name);
373
374
  return value !== undefined ? [{ name, value }] : [];
374
375
  }
375
- return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
376
+ return Object.entries(ctx._readCookies()).map(([n, v]) => ({
377
+ name: n,
378
+ value: v,
379
+ }));
376
380
  },
377
381
 
378
382
  has(name: string): boolean {
@@ -5,6 +5,7 @@ import {
5
5
  } from "./context.js";
6
6
  import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
7
7
  import { pushedByKey } from "../handles/pushed-by.js";
8
+ import { defineSlot } from "./slot-accessors.js";
8
9
 
9
10
  /**
10
11
  * Handle data structure: handleName -> segmentId -> entries[]
@@ -394,6 +395,34 @@ export interface HandleStore {
394
395
  ): void;
395
396
  }
396
397
 
398
+ /**
399
+ * settled and fullySettled: shared descriptors (slot-accessors.ts) that call
400
+ * the store's closure in SETTLEMENT_SLOT with the lane they wait for.
401
+ */
402
+ const SETTLEMENT_SLOT: unique symbol = Symbol("rango.handleStore.settlement");
403
+
404
+ type SettlementSlot = { [SETTLEMENT_SLOT]: (full: boolean) => Promise<void> };
405
+
406
+ const SETTLEMENT_ACCESSORS: Record<
407
+ "settled" | "fullySettled",
408
+ PropertyDescriptor
409
+ > = {
410
+ settled: {
411
+ get(this: SettlementSlot): Promise<void> {
412
+ return this[SETTLEMENT_SLOT](false);
413
+ },
414
+ enumerable: true,
415
+ configurable: true,
416
+ },
417
+ fullySettled: {
418
+ get(this: SettlementSlot): Promise<void> {
419
+ return this[SETTLEMENT_SLOT](true);
420
+ },
421
+ enumerable: true,
422
+ configurable: true,
423
+ },
424
+ };
425
+
397
426
  /**
398
427
  * Create a new HandleStore instance.
399
428
  *
@@ -774,7 +803,9 @@ export function createHandleStore(): HandleStore {
774
803
  });
775
804
  }
776
805
 
777
- return {
806
+ // Data members only: the accessors are SETTLEMENT_ACCESSORS.
807
+ const store: Omit<HandleStore, "settled" | "fullySettled"> &
808
+ ThisType<HandleStore> = {
778
809
  track<T>(promise: Promise<T>): Promise<T> {
779
810
  inflightCount++;
780
811
  return trackSettlement(promise, () => {
@@ -793,22 +824,6 @@ export function createHandleStore(): HandleStore {
793
824
  sealInternal();
794
825
  },
795
826
 
796
- get settled(): Promise<void> {
797
- if (sealed && inflightCount === 0) return Promise.resolve();
798
- return new Promise<void>((resolve) => {
799
- drainWaiters.push(resolve);
800
- });
801
- },
802
-
803
- get fullySettled(): Promise<void> {
804
- if (sealed && inflightCount === 0 && auxInflightCount === 0) {
805
- return Promise.resolve();
806
- }
807
- return new Promise<void>((resolve) => {
808
- fullDrainWaiters.push(resolve);
809
- });
810
- },
811
-
812
827
  push(
813
828
  handleName: string,
814
829
  segmentId: string,
@@ -1213,4 +1228,20 @@ export function createHandleStore(): HandleStore {
1213
1228
  signalEmission();
1214
1229
  },
1215
1230
  };
1231
+ // settled: the handler lane drained; fullySettled: the auxiliary lane too.
1232
+ defineSlot(store, SETTLEMENT_SLOT, (full: boolean): Promise<void> => {
1233
+ if (sealed && inflightCount === 0 && (!full || auxInflightCount === 0)) {
1234
+ return Promise.resolve();
1235
+ }
1236
+ return new Promise<void>((resolve) => {
1237
+ (full ? fullDrainWaiters : drainWaiters).push(resolve);
1238
+ });
1239
+ });
1240
+ Object.defineProperty(store, "settled", SETTLEMENT_ACCESSORS.settled);
1241
+ Object.defineProperty(
1242
+ store,
1243
+ "fullySettled",
1244
+ SETTLEMENT_ACCESSORS.fullySettled,
1245
+ );
1246
+ return store as HandleStore;
1216
1247
  }