@xmachines/play-router 6.0.0 → 7.0.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 (40) hide show
  1. package/README.md +79 -23
  2. package/dist/base-route-map.js +240 -270
  3. package/dist/base-route-map.js.map +1 -1
  4. package/dist/build-tree.js +84 -96
  5. package/dist/build-tree.js.map +1 -1
  6. package/dist/create-route-map-from-tree.js +50 -47
  7. package/dist/create-route-map-from-tree.js.map +1 -1
  8. package/dist/errors.js +124 -133
  9. package/dist/errors.js.map +1 -1
  10. package/dist/find-route.js +88 -105
  11. package/dist/find-route.js.map +1 -1
  12. package/dist/index.js +10 -28
  13. package/dist/provider-lifecycle.js +109 -143
  14. package/dist/provider-lifecycle.js.map +1 -1
  15. package/dist/router-bridge-base.d.ts +5 -5
  16. package/dist/router-bridge-base.d.ts.map +1 -1
  17. package/dist/router-bridge-base.js +1026 -1601
  18. package/dist/router-bridge-base.js.map +1 -1
  19. package/dist/router-sync.js +107 -139
  20. package/dist/router-sync.js.map +1 -1
  21. package/dist/types.d.ts +17 -17
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/validate-routes.js +58 -66
  24. package/dist/validate-routes.js.map +1 -1
  25. package/dist/xstate/create-route-map.js +36 -35
  26. package/dist/xstate/create-route-map.js.map +1 -1
  27. package/dist/xstate/extract-routes.js +66 -83
  28. package/dist/xstate/extract-routes.js.map +1 -1
  29. package/dist/xstate/index.js +5 -33
  30. package/dist/xstate/machine-to-graph.js +98 -109
  31. package/dist/xstate/machine-to-graph.js.map +1 -1
  32. package/dist/xstate/query.js +190 -212
  33. package/dist/xstate/query.js.map +1 -1
  34. package/package.json +16 -16
  35. package/dist/index.js.map +0 -1
  36. package/dist/types.js +0 -2
  37. package/dist/types.js.map +0 -1
  38. package/dist/xstate/index.js.map +0 -1
  39. package/dist/xstate/machine-types.js +0 -2
  40. package/dist/xstate/machine-types.js.map +0 -1
@@ -1,280 +1,250 @@
1
- /**
2
- * RouteMap — the shared base class of the route map for both directions
3
- *
4
- * The class gives you the pattern match on buckets, and every framework adapter
5
- * uses it. An adapter extends this class, and it therefore holds no copy of the
6
- * pattern match logic.
7
- *
8
- * The algorithm: an exact match in O(1) through a Map, then a pattern match on the
9
- * buckets in O(k), where k is the number of the routes in the bucket of the first
10
- * segment, and that number is much smaller than the number of all the routes. The
11
- * class matches a parameterized route with URLPattern.
12
- */
13
- import { createLruCache, getCandidates, getCompiledPattern, getIndexKey, parsePattern, splitLocation, } from "@xmachines/play-url";
14
1
  import { InvalidRoutePatternError } from "./errors.js";
2
+ import { createLruCache, getCandidates, getCompiledPattern, getIndexKey, parsePattern, splitLocation } from "@xmachines/play-url";
3
+ //#region packages/play-router/src/base-route-map.ts
4
+ /**
5
+ * RouteMap — the shared base class of the route map for both directions
6
+ *
7
+ * The class gives you the pattern match on buckets, and every framework adapter
8
+ * uses it. An adapter extends this class, and it therefore holds no copy of the
9
+ * pattern match logic.
10
+ *
11
+ * The algorithm: an exact match in O(1) through a Map, then a pattern match on the
12
+ * buckets in O(k), where k is the number of the routes in the bucket of the first
13
+ * segment, and that number is much smaller than the number of all the routes. The
14
+ * class matches a parameterized route with URLPattern.
15
+ */
15
16
  /**
16
- * The canonical key of a stateId: the bare form, without a `#` at its start.
17
- *
18
- * `RouteMap` accepts a stateId in the form `"#stateId"` and in the form `"stateId"`,
19
- * on an insert and on a lookup. Inside the class, the key of every stateId is this
20
- * canonical bare form. Therefore a consumer writes no handler of the two forms. The
21
- * class keeps the *registered* form, and `getStateIdByPath` returns it. The map is
22
- * therefore honest about the declaration of each route.
23
- */
17
+ * The canonical key of a stateId: the bare form, without a `#` at its start.
18
+ *
19
+ * `RouteMap` accepts a stateId in the form `"#stateId"` and in the form `"stateId"`,
20
+ * on an insert and on a lookup. Inside the class, the key of every stateId is this
21
+ * canonical bare form. Therefore a consumer writes no handler of the two forms. The
22
+ * class keeps the *registered* form, and `getStateIdByPath` returns it. The map is
23
+ * therefore honest about the declaration of each route.
24
+ */
24
25
  function canonicalStateIdKey(stateId) {
25
- return stateId.startsWith("#") ? stateId.slice(1) : stateId;
26
+ return stateId.startsWith("#") ? stateId.slice(1) : stateId;
26
27
  }
27
28
  /**
28
- * The bound of the path cache that a map builds for itself.
29
- *
30
- * `createLruCache` keeps two generations, so the cache holds twice this number and one
31
- * more at most. A caller that wants another bound passes a cache: `createLruCache(5000)`
32
- * builds one. A separate size option would configure THIS cache alone and say so nowhere,
33
- * and it would do nothing at all beside a cache that the caller passed.
34
- */
35
- const DEFAULT_PATH_CACHE_SIZE = 500;
29
+ * The bound of the path cache that a map builds for itself.
30
+ *
31
+ * `createLruCache` keeps two generations, so the cache holds twice this number and one
32
+ * more at most. A caller that wants another bound passes a cache: `createLruCache(5000)`
33
+ * builds one. A separate size option would configure THIS cache alone and say so nowhere,
34
+ * and it would do nothing at all beside a cache that the caller passed.
35
+ */
36
+ var DEFAULT_PATH_CACHE_SIZE = 500;
36
37
  /**
37
- * The id of one route table: a digest of its mappings, in order.
38
- *
39
- * It is the key space that each ROUTE TABLE owns inside a shared path cache. An entry of
40
- * that cache says which state a path resolves to, and the answer is a pure function of
41
- * the mappings and the path: two maps built from the SAME mappings resolve every path the
42
- * same way, and two maps built from different mappings do not. A map with no route for
43
- * `/settings/x` writes `null`, and a map that DOES hold `/settings/:s` then read that
44
- * `null` and reported no state for a route of its own.
45
- *
46
- * The key therefore names the TABLE, and not the instance. An instance counter kept the
47
- * two apart as well, and it also gave a server that rebuilds its map for each request a
48
- * cache that answered nothing: every request opened a key space of its own, and the dead
49
- * keys of the last one evicted the live entries of this one.
50
- *
51
- * Two maps built from the same mappings share the id, which is what a server that
52
- * rebuilds its map for each request needs, and two maps built from different mappings do
53
- * not — a map with no route for a path writes `null` for it, and a map that HOLDS one
54
- * must not read that.
55
- *
56
- * A REGISTER of the serialized tables answered this before, and it kept every table
57
- * string it had seen for the life of the process: a per-tenant table of fifty routes is a
58
- * few kilobytes, and a bounded register of them still retained megabytes in exactly the
59
- * per-request case that the cache exists for. A digest retains nothing.
60
- *
61
- * ## The collision
62
- *
63
- * Two DIFFERENT tables that share a digest share a key space, and the negative entries of
64
- * one then answer for the other. The digest is 64 bits over the full signature, so a
65
- * process that built ten thousand distinct route tables carries a chance of about 3 in
66
- * 10^12 of one collision — far below the chance of the bit flip that would corrupt the
67
- * cache anyway. This library already identifies by digest: an XMachines id is a Git OID.
68
- */
38
+ * The id of one route table: a digest of its mappings, in order.
39
+ *
40
+ * It is the key space that each ROUTE TABLE owns inside a shared path cache. An entry of
41
+ * that cache says which state a path resolves to, and the answer is a pure function of
42
+ * the mappings and the path: two maps built from the SAME mappings resolve every path the
43
+ * same way, and two maps built from different mappings do not. A map with no route for
44
+ * `/settings/x` writes `null`, and a map that DOES hold `/settings/:s` then read that
45
+ * `null` and reported no state for a route of its own.
46
+ *
47
+ * The key therefore names the TABLE, and not the instance. An instance counter kept the
48
+ * two apart as well, and it also gave a server that rebuilds its map for each request a
49
+ * cache that answered nothing: every request opened a key space of its own, and the dead
50
+ * keys of the last one evicted the live entries of this one.
51
+ *
52
+ * Two maps built from the same mappings share the id, which is what a server that
53
+ * rebuilds its map for each request needs, and two maps built from different mappings do
54
+ * not — a map with no route for a path writes `null` for it, and a map that HOLDS one
55
+ * must not read that.
56
+ *
57
+ * A REGISTER of the serialized tables answered this before, and it kept every table
58
+ * string it had seen for the life of the process: a per-tenant table of fifty routes is a
59
+ * few kilobytes, and a bounded register of them still retained megabytes in exactly the
60
+ * per-request case that the cache exists for. A digest retains nothing.
61
+ *
62
+ * ## The collision
63
+ *
64
+ * Two DIFFERENT tables that share a digest share a key space, and the negative entries of
65
+ * one then answer for the other. The digest is 64 bits over the full signature, so a
66
+ * process that built ten thousand distinct route tables carries a chance of about 3 in
67
+ * 10^12 of one collision — far below the chance of the bit flip that would corrupt the
68
+ * cache anyway. This library already identifies by digest: an XMachines id is a Git OID.
69
+ */
69
70
  function routeTableId(mappings) {
70
- // FNV-1a, in two 32-bit halves, because a bitwise operator of JavaScript truncates to
71
- // 32 bits and `BigInt` costs an allocation for each character.
72
- let hashLow = 0x811c9dc5;
73
- let hashHigh = 0x811c9dc5;
74
- for (const { stateId, path } of mappings) {
75
- // The ORDER is significant: it gives the priority when more than one pattern matches,
76
- // so the separators below keep `[{a, /x}, {b, /y}]` apart from `[{a, /x/b}, {/y}]`.
77
- const entry = `${stateId}\u0000${path}\u0001`;
78
- for (let index = 0; index < entry.length; index += 1) {
79
- const code = entry.charCodeAt(index);
80
- hashLow = Math.imul(hashLow ^ code, 0x01000193) >>> 0;
81
- hashHigh = Math.imul(hashHigh ^ (code + index), 0x01000193) >>> 0;
82
- }
83
- }
84
- return `${hashLow.toString(36)}.${hashHigh.toString(36)}`;
71
+ let hashLow = 2166136261;
72
+ let hashHigh = 2166136261;
73
+ for (const { stateId, path } of mappings) {
74
+ const entry = `${stateId}\u0000${path}\u0001`;
75
+ for (let index = 0; index < entry.length; index += 1) {
76
+ const code = entry.charCodeAt(index);
77
+ hashLow = Math.imul(hashLow ^ code, 16777619) >>> 0;
78
+ hashHigh = Math.imul(hashHigh ^ code + index, 16777619) >>> 0;
79
+ }
80
+ }
81
+ return `${hashLow.toString(36)}.${hashHigh.toString(36)}`;
85
82
  }
86
83
  /**
87
- * The shared base class of the route map for both directions.
88
- *
89
- * Every framework adapter uses this class as its route map. An adapter adds no logic
90
- * of its own, and it inherits the complete public API from here.
91
- *
92
- * **The strategy of a lookup:**
93
- * - A static path, without a `:param` → a `Map` lookup in O(1)
94
- * - A dynamic path → a scan of the bucket index in O(k), with `URLPattern`, where
95
- * `k` is the number of the routes with the same first path segment
96
- * - The class keeps each result of a first match in an LRU cache. The default cache
97
- * holds 1001 entries at most, and the `pathCache` constructor option replaces it
98
- *
99
- * **The syntax of a pattern:** the whole URLPattern pathname grammar. The forms used most
100
- * often are:
101
- * - `:param` — a necessary segment. It matches exactly one segment without a `/`
102
- * - `:param?` — an optional segment. It matches zero segments or one segment without a `/`
103
- * - `*` — a wildcard. It matches each number of segments, as URLPattern defines
104
- *
105
- * A `{…}` group, a `:param(\d+)` constraint, a `+` modifier and a `\:` escape are read
106
- * too. The [routing guide](../../docs/guides/routing.md) states every form, the prefix
107
- * rule, and the one divergence from the standard: a param name of this library can hold a
108
- * hyphen.
109
- *
110
- * **The forms of a stateId:** you can register a stateId, and you can look one up,
111
- * in the form `"#stateId"` or in the form `"stateId"`. `RouteMap` makes the
112
- * canonical form itself. `getStateIdByPath` returns the stateId exactly as you
113
- * registered it, and `getPathByStateId` accepts both forms. A registration of the
114
- * same stateId in both forms gives one entry, and the later registration wins for
115
- * the lookup in the other direction.
116
- *
117
- * @example
118
- * ```typescript
119
- * import { RouteMap } from "@xmachines/play-router";
120
- *
121
- * const map = new RouteMap([
122
- * { stateId: "home", path: "/" },
123
- * { stateId: "profile", path: "/profile/:userId" },
124
- * { stateId: "settings", path: "/settings/:section?" },
125
- * ]);
126
- *
127
- * map.getStateIdByPath("/"); // "home"
128
- * map.getStateIdByPath("/profile/123"); // "profile"
129
- * map.getStateIdByPath("/settings"); // "settings"
130
- * map.getStateIdByPath("/unknown"); // null
131
- *
132
- * map.getPathByStateId("profile"); // "/profile/:userId"
133
- * map.getPathByStateId("missing"); // null
134
- * ```
135
- */
136
- export class RouteMap {
137
- /** The key is the canonical stateId form, which is bare and without a `#`. */
138
- stateIdToPath;
139
- pathToStateId;
140
- patternBuckets;
141
- pathMatchCache;
142
- /** The key space of this ROUTE TABLE inside {@link pathMatchCache}, which a caller may share. */
143
- pathCacheKey;
144
- /**
145
- * The caches that every parse and every compilation of this map reads.
146
- *
147
- * Each default stays SHARED on purpose. A map that built one of its own would keep its
148
- * patterns out of the cache that `find-route.ts` and `router-sync.ts` read, so the same
149
- * pattern would parse two times. A caller that wants the isolation passes a cache.
150
- */
151
- compileOptions;
152
- /**
153
- * Builds a route map from an array of the mappings between a state ID and a path.
154
- *
155
- * The constructor puts each static path, which holds no `:param`, into a `Map` for a
156
- * lookup in O(1). It compiles each parameterized path to a `URLPattern`, and it
157
- * groups the patterns into the buckets of the first segment. The selection of the
158
- * candidates is therefore efficient.
159
- *
160
- * @param mappings - The array of the `{ stateId, path }` entries. The order gives the
161
- * priority when more than one pattern can match the same path.
162
- * @param options - See {@link RouteMapOptions}. A map holds TWO caches, so no option is
163
- * named `cache`: each one names the cache that it carries.
164
- */
165
- constructor(mappings, options = {}) {
166
- const { pathCache, ...compileOptions } = options;
167
- this.compileOptions = compileOptions;
168
- this.stateIdToPath = new Map();
169
- this.pathToStateId = new Map();
170
- this.patternBuckets = new Map();
171
- this.pathMatchCache = pathCache ?? createLruCache(DEFAULT_PATH_CACHE_SIZE);
172
- // A map that BUILDS its own cache owns every key in it, and a prefix would only make
173
- // each key longer. A map that RECEIVES one shares it, so its keys carry the prefix.
174
- if (pathCache === undefined) {
175
- this.pathCacheKey = (path) => path;
176
- }
177
- else {
178
- const prefix = `${routeTableId(mappings)}\u0002`;
179
- this.pathCacheKey = (path) => prefix + path;
180
- }
181
- let patternOrder = 0;
182
- for (const { stateId, path } of mappings) {
183
- // The key is the canonical bare form. Therefore getPathByStateId accepts
184
- // "#stateId" and also "stateId". The map keeps the registered form for its return
185
- // values.
186
- this.stateIdToPath.set(canonicalStateIdKey(stateId), path);
187
- // ONE parse answers both questions: is the path a pattern, and which bucket does
188
- // it belong to. The key comes from that parse, and not from a split of the raw
189
- // string. A pattern such as `/books{/:id}?` keeps the literal first segment
190
- // `books`, and a split on `/` read the key `books{` and put the route in a bucket
191
- // that the path `/books/1` reaches never.
192
- const parsed = parsePattern(path, this.compileOptions);
193
- if (parsed.parameterized) {
194
- const bucketKey = parsed.indexKey;
195
- const bucket = this.patternBuckets.get(bucketKey) ?? [];
196
- try {
197
- bucket.push({
198
- pattern: getCompiledPattern(path, this.compileOptions),
199
- stateId,
200
- order: patternOrder++,
201
- });
202
- }
203
- catch (err) {
204
- // `getCompiledPattern` reports an invalid pattern itself, and with the REASON
205
- // when it knows one. A second wrap here replaced that message with the
206
- // generic one, so the explanation reached the caller never.
207
- if (err instanceof InvalidRoutePatternError)
208
- throw err;
209
- throw new InvalidRoutePatternError(path, { cause: err });
210
- }
211
- this.patternBuckets.set(bucketKey, bucket);
212
- }
213
- else {
214
- this.pathToStateId.set(path, stateId);
215
- }
216
- }
217
- }
218
- /**
219
- * Resolves a URL path to its state ID.
220
- *
221
- * The method removes the query string and the hash fragment before the match. It
222
- * tries an exact lookup in O(1) first, then it uses the pattern match on the bucket
223
- * index. It keeps each result of a first pattern match in the cache.
224
- *
225
- * @param path - The URL pathname. It can hold a query and a hash, for example `"/profile/123?ref=nav"`
226
- * @returns The state ID of the path, or `null` when no route matches
227
- *
228
- * @example
229
- * ```typescript
230
- * map.getStateIdByPath("/profile/123"); // "profile"
231
- * map.getStateIdByPath("/unknown"); // null
232
- * ```
233
- */
234
- getStateIdByPath(path) {
235
- // Remove the query string and the hash fragment before the match. `splitLocation` is
236
- // the ONE answer to "where does a pathname end", and `RouterBridgeBase`,
237
- // `framework-params.ts` and `sanitizePathname` read it too, so the four cannot
238
- // disagree. Two splits allocated two arrays of every part on the hottest lookup of
239
- // the library, and this function then read one element of each.
240
- const cleanPath = splitLocation(path).pathname;
241
- const exactMatch = this.pathToStateId.get(cleanPath);
242
- if (exactMatch !== undefined)
243
- return exactMatch;
244
- const cacheKey = this.pathCacheKey(cleanPath);
245
- const cachedMatch = this.pathMatchCache.get(cacheKey);
246
- if (cachedMatch !== undefined)
247
- return cachedMatch;
248
- const candidates = getCandidates(this.patternBuckets, getIndexKey(cleanPath));
249
- for (const { pattern, stateId } of candidates) {
250
- const match = pattern.exec({ pathname: cleanPath });
251
- if (match) {
252
- this.pathMatchCache.set(cacheKey, stateId);
253
- return stateId;
254
- }
255
- }
256
- this.pathMatchCache.set(cacheKey, null);
257
- return null;
258
- }
259
- /**
260
- * Returns the path pattern of a state ID.
261
- *
262
- * The method accepts the stateId in the form `"#stateId"` and in the form
263
- * `"stateId"`, and the form of the registration has no effect. The method makes the
264
- * canonical form itself. Therefore a consumer tries never both forms.
265
- *
266
- * @param stateId - The state ID of the state machine, for example `"profile"` or `"#settings"`
267
- * @returns The registered path pattern, or `null` when the state ID is unknown
268
- *
269
- * @example
270
- * ```typescript
271
- * map.getPathByStateId("profile"); // "/profile/:userId"
272
- * map.getPathByStateId("#profile"); // "/profile/:userId" — the same entry
273
- * map.getPathByStateId("missing"); // null
274
- * ```
275
- */
276
- getPathByStateId(stateId) {
277
- return this.stateIdToPath.get(canonicalStateIdKey(stateId)) ?? null;
278
- }
279
- }
84
+ * The shared base class of the route map for both directions.
85
+ *
86
+ * Every framework adapter uses this class as its route map. An adapter adds no logic
87
+ * of its own, and it inherits the complete public API from here.
88
+ *
89
+ * **The strategy of a lookup:**
90
+ * - A static path, without a `:param` → a `Map` lookup in O(1)
91
+ * - A dynamic path → a scan of the bucket index in O(k), with `URLPattern`, where
92
+ * `k` is the number of the routes with the same first path segment
93
+ * - The class keeps each result of a first match in an LRU cache. The default cache
94
+ * holds 1001 entries at most, and the `pathCache` constructor option replaces it
95
+ *
96
+ * **The syntax of a pattern:** the whole URLPattern pathname grammar. The forms used most
97
+ * often are:
98
+ * - `:param` — a necessary segment. It matches exactly one segment without a `/`
99
+ * - `:param?` — an optional segment. It matches zero segments or one segment without a `/`
100
+ * - `*` — a wildcard. It matches each number of segments, as URLPattern defines
101
+ *
102
+ * A `{…}` group, a `:param(\d+)` constraint, a `+` modifier and a `\:` escape are read
103
+ * too. The [routing guide](../../docs/guides/routing.md) states every form, the prefix
104
+ * rule, and the one divergence from the standard: a param name of this library can hold a
105
+ * hyphen.
106
+ *
107
+ * **The forms of a stateId:** you can register a stateId, and you can look one up,
108
+ * in the form `"#stateId"` or in the form `"stateId"`. `RouteMap` makes the
109
+ * canonical form itself. `getStateIdByPath` returns the stateId exactly as you
110
+ * registered it, and `getPathByStateId` accepts both forms. A registration of the
111
+ * same stateId in both forms gives one entry, and the later registration wins for
112
+ * the lookup in the other direction.
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * import { RouteMap } from "@xmachines/play-router";
117
+ *
118
+ * const map = new RouteMap([
119
+ * { stateId: "home", path: "/" },
120
+ * { stateId: "profile", path: "/profile/:userId" },
121
+ * { stateId: "settings", path: "/settings/:section?" },
122
+ * ]);
123
+ *
124
+ * map.getStateIdByPath("/"); // "home"
125
+ * map.getStateIdByPath("/profile/123"); // "profile"
126
+ * map.getStateIdByPath("/settings"); // "settings"
127
+ * map.getStateIdByPath("/unknown"); // null
128
+ *
129
+ * map.getPathByStateId("profile"); // "/profile/:userId"
130
+ * map.getPathByStateId("missing"); // null
131
+ * ```
132
+ */
133
+ var RouteMap = class {
134
+ /** The key is the canonical stateId form, which is bare and without a `#`. */
135
+ stateIdToPath;
136
+ pathToStateId;
137
+ patternBuckets;
138
+ pathMatchCache;
139
+ /** The key space of this ROUTE TABLE inside {@link pathMatchCache}, which a caller may share. */
140
+ pathCacheKey;
141
+ /**
142
+ * The caches that every parse and every compilation of this map reads.
143
+ *
144
+ * Each default stays SHARED on purpose. A map that built one of its own would keep its
145
+ * patterns out of the cache that `find-route.ts` and `router-sync.ts` read, so the same
146
+ * pattern would parse two times. A caller that wants the isolation passes a cache.
147
+ */
148
+ compileOptions;
149
+ /**
150
+ * Builds a route map from an array of the mappings between a state ID and a path.
151
+ *
152
+ * The constructor puts each static path, which holds no `:param`, into a `Map` for a
153
+ * lookup in O(1). It compiles each parameterized path to a `URLPattern`, and it
154
+ * groups the patterns into the buckets of the first segment. The selection of the
155
+ * candidates is therefore efficient.
156
+ *
157
+ * @param mappings - The array of the `{ stateId, path }` entries. The order gives the
158
+ * priority when more than one pattern can match the same path.
159
+ * @param options - See {@link RouteMapOptions}. A map holds TWO caches, so no option is
160
+ * named `cache`: each one names the cache that it carries.
161
+ */
162
+ constructor(mappings, options = {}) {
163
+ const { pathCache, ...compileOptions } = options;
164
+ this.compileOptions = compileOptions;
165
+ this.stateIdToPath = /* @__PURE__ */ new Map();
166
+ this.pathToStateId = /* @__PURE__ */ new Map();
167
+ this.patternBuckets = /* @__PURE__ */ new Map();
168
+ this.pathMatchCache = pathCache ?? createLruCache(DEFAULT_PATH_CACHE_SIZE);
169
+ if (pathCache === void 0) this.pathCacheKey = (path) => path;
170
+ else {
171
+ const prefix = `${routeTableId(mappings)}\u0002`;
172
+ this.pathCacheKey = (path) => prefix + path;
173
+ }
174
+ let patternOrder = 0;
175
+ for (const { stateId, path } of mappings) {
176
+ this.stateIdToPath.set(canonicalStateIdKey(stateId), path);
177
+ const parsed = parsePattern(path, this.compileOptions);
178
+ if (parsed.parameterized) {
179
+ const bucketKey = parsed.indexKey;
180
+ const bucket = this.patternBuckets.get(bucketKey) ?? [];
181
+ try {
182
+ bucket.push({
183
+ pattern: getCompiledPattern(path, this.compileOptions),
184
+ stateId,
185
+ order: patternOrder++
186
+ });
187
+ } catch (err) {
188
+ if (err instanceof InvalidRoutePatternError) throw err;
189
+ throw new InvalidRoutePatternError(path, { cause: err });
190
+ }
191
+ this.patternBuckets.set(bucketKey, bucket);
192
+ } else this.pathToStateId.set(path, stateId);
193
+ }
194
+ }
195
+ /**
196
+ * Resolves a URL path to its state ID.
197
+ *
198
+ * The method removes the query string and the hash fragment before the match. It
199
+ * tries an exact lookup in O(1) first, then it uses the pattern match on the bucket
200
+ * index. It keeps each result of a first pattern match in the cache.
201
+ *
202
+ * @param path - The URL pathname. It can hold a query and a hash, for example `"/profile/123?ref=nav"`
203
+ * @returns The state ID of the path, or `null` when no route matches
204
+ *
205
+ * @example
206
+ * ```typescript
207
+ * map.getStateIdByPath("/profile/123"); // "profile"
208
+ * map.getStateIdByPath("/unknown"); // null
209
+ * ```
210
+ */
211
+ getStateIdByPath(path) {
212
+ const cleanPath = splitLocation(path).pathname;
213
+ const exactMatch = this.pathToStateId.get(cleanPath);
214
+ if (exactMatch !== void 0) return exactMatch;
215
+ const cacheKey = this.pathCacheKey(cleanPath);
216
+ const cachedMatch = this.pathMatchCache.get(cacheKey);
217
+ if (cachedMatch !== void 0) return cachedMatch;
218
+ const candidates = getCandidates(this.patternBuckets, getIndexKey(cleanPath));
219
+ for (const { pattern, stateId } of candidates) if (pattern.exec({ pathname: cleanPath })) {
220
+ this.pathMatchCache.set(cacheKey, stateId);
221
+ return stateId;
222
+ }
223
+ this.pathMatchCache.set(cacheKey, null);
224
+ return null;
225
+ }
226
+ /**
227
+ * Returns the path pattern of a state ID.
228
+ *
229
+ * The method accepts the stateId in the form `"#stateId"` and in the form
230
+ * `"stateId"`, and the form of the registration has no effect. The method makes the
231
+ * canonical form itself. Therefore a consumer tries never both forms.
232
+ *
233
+ * @param stateId - The state ID of the state machine, for example `"profile"` or `"#settings"`
234
+ * @returns The registered path pattern, or `null` when the state ID is unknown
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * map.getPathByStateId("profile"); // "/profile/:userId"
239
+ * map.getPathByStateId("#profile"); // "/profile/:userId" — the same entry
240
+ * map.getPathByStateId("missing"); // null
241
+ * ```
242
+ */
243
+ getPathByStateId(stateId) {
244
+ return this.stateIdToPath.get(canonicalStateIdKey(stateId)) ?? null;
245
+ }
246
+ };
247
+ //#endregion
248
+ export { RouteMap };
249
+
280
250
  //# sourceMappingURL=base-route-map.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"base-route-map.js","sourceRoot":"","sources":["../src/base-route-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACN,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,YAAY,EACZ,aAAa,GAIb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAGvD;;;;;;;;GAQG;AACH,SAAS,mBAAmB,CAAC,OAAe;IAC3C,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC7D,CAAC;AAyBD;;;;;;;GAOG;AACH,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,SAAS,YAAY,CAAC,QAAiC;IACtD,sFAAsF;IACtF,+DAA+D;IAC/D,IAAI,OAAO,GAAG,UAAU,CAAC;IACzB,IAAI,QAAQ,GAAG,UAAU,CAAC;IAC1B,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1C,sFAAsF;QACtF,oFAAoF;QACpF,MAAM,KAAK,GAAG,GAAG,OAAO,SAAS,IAAI,QAAQ,CAAC;QAC9C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YACrC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;YACtD,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;QACnE,CAAC;IACF,CAAC;IACD,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,OAAO,QAAQ;IACpB,8EAA8E;IACtE,aAAa,CAAsB;IACnC,aAAa,CAAsB;IACnC,cAAc,CAGpB;IACM,cAAc,CAA+B;IACrD,iGAAiG;IAChF,YAAY,CAA2B;IACxD;;;;;;OAMG;IACK,cAAc,CAAiB;IAEvC;;;;;;;;;;;;OAYG;IACH,YAAY,QAAwB,EAAE,UAA2B,EAAE;QAClE,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,GAAG,OAAO,CAAC;QACjD,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,aAAa,GAAG,IAAI,GAAG,EAAE,CAAC;QAC/B,IAAI,CAAC,aAAa,GAAG,IAAI,GAAG,EAAE,CAAC;QAC/B,IAAI,CAAC,cAAc,GAAG,IAAI,GAAG,EAAE,CAAC;QAChC,IAAI,CAAC,cAAc,GAAG,SAAS,IAAI,cAAc,CAAgB,uBAAuB,CAAC,CAAC;QAC1F,qFAAqF;QACrF,oFAAoF;QACpF,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC;QACpC,CAAC;aAAM,CAAC;YACP,MAAM,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjD,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC;QAC7C,CAAC;QACD,IAAI,YAAY,GAAG,CAAC,CAAC;QAErB,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC1C,yEAAyE;YACzE,kFAAkF;YAClF,UAAU;YACV,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,mBAAmB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;YAE3D,iFAAiF;YACjF,+EAA+E;YAC/E,4EAA4E;YAC5E,kFAAkF;YAClF,0CAA0C;YAC1C,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;YACvD,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;gBAC1B,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC;gBAClC,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBACxD,IAAI,CAAC;oBACJ,MAAM,CAAC,IAAI,CAAC;wBACX,OAAO,EAAE,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC;wBACtD,OAAO;wBACP,KAAK,EAAE,YAAY,EAAE;qBACrB,CAAC,CAAC;gBACJ,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,8EAA8E;oBAC9E,uEAAuE;oBACvE,4DAA4D;oBAC5D,IAAI,GAAG,YAAY,wBAAwB;wBAAE,MAAM,GAAG,CAAC;oBACvD,MAAM,IAAI,wBAAwB,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC1D,CAAC;gBACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAC5C,CAAC;iBAAM,CAAC;gBACP,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACvC,CAAC;QACF,CAAC;IACF,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,IAAY;QAC5B,qFAAqF;QACrF,yEAAyE;QACzE,+EAA+E;QAC/E,mFAAmF;QACnF,gEAAgE;QAChE,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;QAE/C,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrD,IAAI,UAAU,KAAK,SAAS;YAAE,OAAO,UAAU,CAAC;QAEhD,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtD,IAAI,WAAW,KAAK,SAAS;YAAE,OAAO,WAAW,CAAC;QAElD,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;QAC9E,KAAK,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,UAAU,EAAE,CAAC;YAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;YACpD,IAAI,KAAK,EAAE,CAAC;gBACX,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAC3C,OAAO,OAAO,CAAC;YAChB,CAAC;QACF,CAAC;QAED,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACxC,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,gBAAgB,CAAC,OAAe;QAC/B,OAAO,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,IAAI,IAAI,CAAC;IACrE,CAAC;CACD"}
1
+ {"version":3,"file":"base-route-map.js","names":[],"sources":["../src/base-route-map.ts"],"sourcesContent":["/**\n * RouteMap — the shared base class of the route map for both directions\n *\n * The class gives you the pattern match on buckets, and every framework adapter\n * uses it. An adapter extends this class, and it therefore holds no copy of the\n * pattern match logic.\n *\n * The algorithm: an exact match in O(1) through a Map, then a pattern match on the\n * buckets in O(k), where k is the number of the routes in the bucket of the first\n * segment, and that number is much smaller than the number of all the routes. The\n * class matches a parameterized route with URLPattern.\n */\n\nimport {\n\tcreateLruCache,\n\tgetCandidates,\n\tgetCompiledPattern,\n\tgetIndexKey,\n\tparsePattern,\n\tsplitLocation,\n\ttype CompileOptions,\n\ttype KeyValueCache,\n\ttype URLPatternLike,\n} from \"@xmachines/play-url\";\n\nimport { InvalidRoutePatternError } from \"./errors.js\";\nimport type { RouteMapOptions } from \"./types.js\";\n\n/**\n * The canonical key of a stateId: the bare form, without a `#` at its start.\n *\n * `RouteMap` accepts a stateId in the form `\"#stateId\"` and in the form `\"stateId\"`,\n * on an insert and on a lookup. Inside the class, the key of every stateId is this\n * canonical bare form. Therefore a consumer writes no handler of the two forms. The\n * class keeps the *registered* form, and `getStateIdByPath` returns it. The map is\n * therefore honest about the declaration of each route.\n */\nfunction canonicalStateIdKey(stateId: string): string {\n\treturn stateId.startsWith(\"#\") ? stateId.slice(1) : stateId;\n}\n\n/**\n * One entry of the map between a state ID and a path.\n *\n * Both fields are `readonly`, because a mapping is immutable after the caller gives\n * it to `RouteMap`. An adapter package re-exports a structurally compatible\n * `RouteMapping` type under its own name. `@xmachines/play-router` publishes this\n * type as `RouteMapping`, and its name therefore does not collide with such a local\n * type of an adapter.\n *\n * @example\n * ```typescript\n * const mapping: RouteMapping = { stateId: \"home\", path: \"/\" };\n * const paramMapping: RouteMapping = { stateId: \"profile\", path: \"/profile/:userId\" };\n * const optionalMapping: RouteMapping = { stateId: \"settings\", path: \"/settings/:section?\" };\n * ```\n */\nexport interface RouteMapping {\n\t/** The state ID of the state machine, for example `\"home\"` or `\"#profile\"` */\n\treadonly stateId: string;\n\t/** The pattern of the URL path, for example `\"/\"`, `\"/profile/:userId\"`, or `\"/settings/:section?\"` */\n\treadonly path: string;\n}\n\n/**\n * The bound of the path cache that a map builds for itself.\n *\n * `createLruCache` keeps two generations, so the cache holds twice this number and one\n * more at most. A caller that wants another bound passes a cache: `createLruCache(5000)`\n * builds one. A separate size option would configure THIS cache alone and say so nowhere,\n * and it would do nothing at all beside a cache that the caller passed.\n */\nconst DEFAULT_PATH_CACHE_SIZE = 500;\n\n/**\n * The id of one route table: a digest of its mappings, in order.\n *\n * It is the key space that each ROUTE TABLE owns inside a shared path cache. An entry of\n * that cache says which state a path resolves to, and the answer is a pure function of\n * the mappings and the path: two maps built from the SAME mappings resolve every path the\n * same way, and two maps built from different mappings do not. A map with no route for\n * `/settings/x` writes `null`, and a map that DOES hold `/settings/:s` then read that\n * `null` and reported no state for a route of its own.\n *\n * The key therefore names the TABLE, and not the instance. An instance counter kept the\n * two apart as well, and it also gave a server that rebuilds its map for each request a\n * cache that answered nothing: every request opened a key space of its own, and the dead\n * keys of the last one evicted the live entries of this one.\n *\n * Two maps built from the same mappings share the id, which is what a server that\n * rebuilds its map for each request needs, and two maps built from different mappings do\n * not — a map with no route for a path writes `null` for it, and a map that HOLDS one\n * must not read that.\n *\n * A REGISTER of the serialized tables answered this before, and it kept every table\n * string it had seen for the life of the process: a per-tenant table of fifty routes is a\n * few kilobytes, and a bounded register of them still retained megabytes in exactly the\n * per-request case that the cache exists for. A digest retains nothing.\n *\n * ## The collision\n *\n * Two DIFFERENT tables that share a digest share a key space, and the negative entries of\n * one then answer for the other. The digest is 64 bits over the full signature, so a\n * process that built ten thousand distinct route tables carries a chance of about 3 in\n * 10^12 of one collision — far below the chance of the bit flip that would corrupt the\n * cache anyway. This library already identifies by digest: an XMachines id is a Git OID.\n */\nfunction routeTableId(mappings: readonly RouteMapping[]): string {\n\t// FNV-1a, in two 32-bit halves, because a bitwise operator of JavaScript truncates to\n\t// 32 bits and `BigInt` costs an allocation for each character.\n\tlet hashLow = 0x811c9dc5;\n\tlet hashHigh = 0x811c9dc5;\n\tfor (const { stateId, path } of mappings) {\n\t\t// The ORDER is significant: it gives the priority when more than one pattern matches,\n\t\t// so the separators below keep `[{a, /x}, {b, /y}]` apart from `[{a, /x/b}, {/y}]`.\n\t\tconst entry = `${stateId}\\u0000${path}\\u0001`;\n\t\tfor (let index = 0; index < entry.length; index += 1) {\n\t\t\tconst code = entry.charCodeAt(index);\n\t\t\thashLow = Math.imul(hashLow ^ code, 0x01000193) >>> 0;\n\t\t\thashHigh = Math.imul(hashHigh ^ (code + index), 0x01000193) >>> 0;\n\t\t}\n\t}\n\treturn `${hashLow.toString(36)}.${hashHigh.toString(36)}`;\n}\n\n/**\n * The shared base class of the route map for both directions.\n *\n * Every framework adapter uses this class as its route map. An adapter adds no logic\n * of its own, and it inherits the complete public API from here.\n *\n * **The strategy of a lookup:**\n * - A static path, without a `:param` → a `Map` lookup in O(1)\n * - A dynamic path → a scan of the bucket index in O(k), with `URLPattern`, where\n * `k` is the number of the routes with the same first path segment\n * - The class keeps each result of a first match in an LRU cache. The default cache\n * holds 1001 entries at most, and the `pathCache` constructor option replaces it\n *\n * **The syntax of a pattern:** the whole URLPattern pathname grammar. The forms used most\n * often are:\n * - `:param` — a necessary segment. It matches exactly one segment without a `/`\n * - `:param?` — an optional segment. It matches zero segments or one segment without a `/`\n * - `*` — a wildcard. It matches each number of segments, as URLPattern defines\n *\n * A `{…}` group, a `:param(\\d+)` constraint, a `+` modifier and a `\\:` escape are read\n * too. The [routing guide](../../docs/guides/routing.md) states every form, the prefix\n * rule, and the one divergence from the standard: a param name of this library can hold a\n * hyphen.\n *\n * **The forms of a stateId:** you can register a stateId, and you can look one up,\n * in the form `\"#stateId\"` or in the form `\"stateId\"`. `RouteMap` makes the\n * canonical form itself. `getStateIdByPath` returns the stateId exactly as you\n * registered it, and `getPathByStateId` accepts both forms. A registration of the\n * same stateId in both forms gives one entry, and the later registration wins for\n * the lookup in the other direction.\n *\n * @example\n * ```typescript\n * import { RouteMap } from \"@xmachines/play-router\";\n *\n * const map = new RouteMap([\n * { stateId: \"home\", path: \"/\" },\n * { stateId: \"profile\", path: \"/profile/:userId\" },\n * { stateId: \"settings\", path: \"/settings/:section?\" },\n * ]);\n *\n * map.getStateIdByPath(\"/\"); // \"home\"\n * map.getStateIdByPath(\"/profile/123\"); // \"profile\"\n * map.getStateIdByPath(\"/settings\"); // \"settings\"\n * map.getStateIdByPath(\"/unknown\"); // null\n *\n * map.getPathByStateId(\"profile\"); // \"/profile/:userId\"\n * map.getPathByStateId(\"missing\"); // null\n * ```\n */\nexport class RouteMap {\n\t/** The key is the canonical stateId form, which is bare and without a `#`. */\n\tprivate stateIdToPath: Map<string, string>;\n\tprivate pathToStateId: Map<string, string>;\n\tprivate patternBuckets: Map<\n\t\tstring,\n\t\tArray<{ pattern: URLPatternLike; stateId: string; order: number }>\n\t>;\n\tprivate pathMatchCache: KeyValueCache<string | null>;\n\t/** The key space of this ROUTE TABLE inside {@link pathMatchCache}, which a caller may share. */\n\tprivate readonly pathCacheKey: (path: string) => string;\n\t/**\n\t * The caches that every parse and every compilation of this map reads.\n\t *\n\t * Each default stays SHARED on purpose. A map that built one of its own would keep its\n\t * patterns out of the cache that `find-route.ts` and `router-sync.ts` read, so the same\n\t * pattern would parse two times. A caller that wants the isolation passes a cache.\n\t */\n\tprivate compileOptions: CompileOptions;\n\n\t/**\n\t * Builds a route map from an array of the mappings between a state ID and a path.\n\t *\n\t * The constructor puts each static path, which holds no `:param`, into a `Map` for a\n\t * lookup in O(1). It compiles each parameterized path to a `URLPattern`, and it\n\t * groups the patterns into the buckets of the first segment. The selection of the\n\t * candidates is therefore efficient.\n\t *\n\t * @param mappings - The array of the `{ stateId, path }` entries. The order gives the\n\t * priority when more than one pattern can match the same path.\n\t * @param options - See {@link RouteMapOptions}. A map holds TWO caches, so no option is\n\t * named `cache`: each one names the cache that it carries.\n\t */\n\tconstructor(mappings: RouteMapping[], options: RouteMapOptions = {}) {\n\t\tconst { pathCache, ...compileOptions } = options;\n\t\tthis.compileOptions = compileOptions;\n\t\tthis.stateIdToPath = new Map();\n\t\tthis.pathToStateId = new Map();\n\t\tthis.patternBuckets = new Map();\n\t\tthis.pathMatchCache = pathCache ?? createLruCache<string | null>(DEFAULT_PATH_CACHE_SIZE);\n\t\t// A map that BUILDS its own cache owns every key in it, and a prefix would only make\n\t\t// each key longer. A map that RECEIVES one shares it, so its keys carry the prefix.\n\t\tif (pathCache === undefined) {\n\t\t\tthis.pathCacheKey = (path) => path;\n\t\t} else {\n\t\t\tconst prefix = `${routeTableId(mappings)}\\u0002`;\n\t\t\tthis.pathCacheKey = (path) => prefix + path;\n\t\t}\n\t\tlet patternOrder = 0;\n\n\t\tfor (const { stateId, path } of mappings) {\n\t\t\t// The key is the canonical bare form. Therefore getPathByStateId accepts\n\t\t\t// \"#stateId\" and also \"stateId\". The map keeps the registered form for its return\n\t\t\t// values.\n\t\t\tthis.stateIdToPath.set(canonicalStateIdKey(stateId), path);\n\n\t\t\t// ONE parse answers both questions: is the path a pattern, and which bucket does\n\t\t\t// it belong to. The key comes from that parse, and not from a split of the raw\n\t\t\t// string. A pattern such as `/books{/:id}?` keeps the literal first segment\n\t\t\t// `books`, and a split on `/` read the key `books{` and put the route in a bucket\n\t\t\t// that the path `/books/1` reaches never.\n\t\t\tconst parsed = parsePattern(path, this.compileOptions);\n\t\t\tif (parsed.parameterized) {\n\t\t\t\tconst bucketKey = parsed.indexKey;\n\t\t\t\tconst bucket = this.patternBuckets.get(bucketKey) ?? [];\n\t\t\t\ttry {\n\t\t\t\t\tbucket.push({\n\t\t\t\t\t\tpattern: getCompiledPattern(path, this.compileOptions),\n\t\t\t\t\t\tstateId,\n\t\t\t\t\t\torder: patternOrder++,\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// `getCompiledPattern` reports an invalid pattern itself, and with the REASON\n\t\t\t\t\t// when it knows one. A second wrap here replaced that message with the\n\t\t\t\t\t// generic one, so the explanation reached the caller never.\n\t\t\t\t\tif (err instanceof InvalidRoutePatternError) throw err;\n\t\t\t\t\tthrow new InvalidRoutePatternError(path, { cause: err });\n\t\t\t\t}\n\t\t\t\tthis.patternBuckets.set(bucketKey, bucket);\n\t\t\t} else {\n\t\t\t\tthis.pathToStateId.set(path, stateId);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Resolves a URL path to its state ID.\n\t *\n\t * The method removes the query string and the hash fragment before the match. It\n\t * tries an exact lookup in O(1) first, then it uses the pattern match on the bucket\n\t * index. It keeps each result of a first pattern match in the cache.\n\t *\n\t * @param path - The URL pathname. It can hold a query and a hash, for example `\"/profile/123?ref=nav\"`\n\t * @returns The state ID of the path, or `null` when no route matches\n\t *\n\t * @example\n\t * ```typescript\n\t * map.getStateIdByPath(\"/profile/123\"); // \"profile\"\n\t * map.getStateIdByPath(\"/unknown\"); // null\n\t * ```\n\t */\n\tgetStateIdByPath(path: string): string | null {\n\t\t// Remove the query string and the hash fragment before the match. `splitLocation` is\n\t\t// the ONE answer to \"where does a pathname end\", and `RouterBridgeBase`,\n\t\t// `framework-params.ts` and `sanitizePathname` read it too, so the four cannot\n\t\t// disagree. Two splits allocated two arrays of every part on the hottest lookup of\n\t\t// the library, and this function then read one element of each.\n\t\tconst cleanPath = splitLocation(path).pathname;\n\n\t\tconst exactMatch = this.pathToStateId.get(cleanPath);\n\t\tif (exactMatch !== undefined) return exactMatch;\n\n\t\tconst cacheKey = this.pathCacheKey(cleanPath);\n\t\tconst cachedMatch = this.pathMatchCache.get(cacheKey);\n\t\tif (cachedMatch !== undefined) return cachedMatch;\n\n\t\tconst candidates = getCandidates(this.patternBuckets, getIndexKey(cleanPath));\n\t\tfor (const { pattern, stateId } of candidates) {\n\t\t\tconst match = pattern.exec({ pathname: cleanPath });\n\t\t\tif (match) {\n\t\t\t\tthis.pathMatchCache.set(cacheKey, stateId);\n\t\t\t\treturn stateId;\n\t\t\t}\n\t\t}\n\n\t\tthis.pathMatchCache.set(cacheKey, null);\n\t\treturn null;\n\t}\n\n\t/**\n\t * Returns the path pattern of a state ID.\n\t *\n\t * The method accepts the stateId in the form `\"#stateId\"` and in the form\n\t * `\"stateId\"`, and the form of the registration has no effect. The method makes the\n\t * canonical form itself. Therefore a consumer tries never both forms.\n\t *\n\t * @param stateId - The state ID of the state machine, for example `\"profile\"` or `\"#settings\"`\n\t * @returns The registered path pattern, or `null` when the state ID is unknown\n\t *\n\t * @example\n\t * ```typescript\n\t * map.getPathByStateId(\"profile\"); // \"/profile/:userId\"\n\t * map.getPathByStateId(\"#profile\"); // \"/profile/:userId\" — the same entry\n\t * map.getPathByStateId(\"missing\"); // null\n\t * ```\n\t */\n\tgetPathByStateId(stateId: string): string | null {\n\t\treturn this.stateIdToPath.get(canonicalStateIdKey(stateId)) ?? null;\n\t}\n}\n"],"mappings":"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"}