@typed/ui 1.0.0-beta.4 → 1.0.0-beta.6

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 (157) hide show
  1. package/README.md +53 -9
  2. package/dist/Alert.d.ts +78 -0
  3. package/dist/Alert.d.ts.map +1 -0
  4. package/dist/Alert.js +37 -0
  5. package/dist/Button.d.ts +144 -0
  6. package/dist/Button.d.ts.map +1 -0
  7. package/dist/Button.js +38 -0
  8. package/dist/Carousel.d.ts +547 -0
  9. package/dist/Carousel.d.ts.map +1 -0
  10. package/dist/Carousel.js +387 -0
  11. package/dist/Checkbox.d.ts +203 -0
  12. package/dist/Checkbox.d.ts.map +1 -0
  13. package/dist/Checkbox.js +140 -0
  14. package/dist/Collection.d.ts +254 -0
  15. package/dist/Collection.d.ts.map +1 -0
  16. package/dist/Collection.js +218 -0
  17. package/dist/Combobox.d.ts +516 -0
  18. package/dist/Combobox.d.ts.map +1 -0
  19. package/dist/Combobox.js +371 -0
  20. package/dist/Component.d.ts +127 -0
  21. package/dist/Component.d.ts.map +1 -0
  22. package/dist/Component.js +60 -0
  23. package/dist/Composite.d.ts +823 -0
  24. package/dist/Composite.d.ts.map +1 -0
  25. package/dist/Composite.js +615 -0
  26. package/dist/Dialog.d.ts +544 -0
  27. package/dist/Dialog.d.ts.map +1 -0
  28. package/dist/Dialog.js +355 -0
  29. package/dist/Disclosure.d.ts +219 -0
  30. package/dist/Disclosure.d.ts.map +1 -0
  31. package/dist/Disclosure.js +128 -0
  32. package/dist/Dom/Events.d.ts +122 -0
  33. package/dist/Dom/Events.d.ts.map +1 -0
  34. package/dist/Dom/Events.js +192 -0
  35. package/dist/Dom/Props.d.ts +161 -0
  36. package/dist/Dom/Props.d.ts.map +1 -0
  37. package/dist/Dom/Props.js +110 -0
  38. package/dist/Dom/Refs.d.ts +58 -0
  39. package/dist/Dom/Refs.d.ts.map +1 -0
  40. package/dist/Dom/Refs.js +61 -0
  41. package/dist/Dom/Render.d.ts +59 -0
  42. package/dist/Dom/Render.d.ts.map +1 -0
  43. package/dist/Dom/Render.js +71 -0
  44. package/dist/Dom/Types.d.ts +570 -0
  45. package/dist/Dom/Types.d.ts.map +1 -0
  46. package/dist/Dom/Types.js +1 -0
  47. package/dist/Dom/index.d.ts +20 -0
  48. package/dist/Dom/index.d.ts.map +1 -0
  49. package/dist/Dom/index.js +8 -0
  50. package/dist/Dom.d.ts +14 -0
  51. package/dist/Dom.d.ts.map +1 -0
  52. package/dist/Dom.js +13 -0
  53. package/dist/Focusable.d.ts +85 -0
  54. package/dist/Focusable.d.ts.map +1 -0
  55. package/dist/Focusable.js +35 -0
  56. package/dist/Form.d.ts +1695 -0
  57. package/dist/Form.d.ts.map +1 -0
  58. package/dist/Form.js +987 -0
  59. package/dist/Grid.d.ts +569 -0
  60. package/dist/Grid.d.ts.map +1 -0
  61. package/dist/Grid.js +379 -0
  62. package/dist/Group.d.ts +147 -0
  63. package/dist/Group.d.ts.map +1 -0
  64. package/dist/Group.js +63 -0
  65. package/dist/Heading.d.ts +86 -0
  66. package/dist/Heading.d.ts.map +1 -0
  67. package/dist/Heading.js +48 -0
  68. package/dist/Hovercard.d.ts +297 -0
  69. package/dist/Hovercard.d.ts.map +1 -0
  70. package/dist/Hovercard.js +188 -0
  71. package/dist/HttpRouter.d.ts +129 -6
  72. package/dist/HttpRouter.d.ts.map +1 -1
  73. package/dist/HttpRouter.js +196 -53
  74. package/dist/Link.d.ts +67 -28
  75. package/dist/Link.d.ts.map +1 -1
  76. package/dist/Link.js +91 -37
  77. package/dist/Listbox.d.ts +437 -0
  78. package/dist/Listbox.d.ts.map +1 -0
  79. package/dist/Listbox.js +316 -0
  80. package/dist/Menu.d.ts +972 -0
  81. package/dist/Menu.d.ts.map +1 -0
  82. package/dist/Menu.js +731 -0
  83. package/dist/Menubar.d.ts +367 -0
  84. package/dist/Menubar.d.ts.map +1 -0
  85. package/dist/Menubar.js +265 -0
  86. package/dist/Meter.d.ts +217 -0
  87. package/dist/Meter.d.ts.map +1 -0
  88. package/dist/Meter.js +94 -0
  89. package/dist/NativeDetails.d.ts +41 -0
  90. package/dist/NativeDetails.d.ts.map +1 -0
  91. package/dist/NativeDetails.js +40 -0
  92. package/dist/NativeDialog.d.ts +64 -0
  93. package/dist/NativeDialog.d.ts.map +1 -0
  94. package/dist/NativeDialog.js +50 -0
  95. package/dist/NativePopover.d.ts +41 -0
  96. package/dist/NativePopover.d.ts.map +1 -0
  97. package/dist/NativePopover.js +46 -0
  98. package/dist/Popover.d.ts +241 -0
  99. package/dist/Popover.d.ts.map +1 -0
  100. package/dist/Popover.js +141 -0
  101. package/dist/RadioGroup.d.ts +432 -0
  102. package/dist/RadioGroup.d.ts.map +1 -0
  103. package/dist/RadioGroup.js +291 -0
  104. package/dist/Role.d.ts +64 -0
  105. package/dist/Role.d.ts.map +1 -0
  106. package/dist/Role.js +27 -0
  107. package/dist/Select.d.ts +529 -0
  108. package/dist/Select.d.ts.map +1 -0
  109. package/dist/Select.js +407 -0
  110. package/dist/Separator.d.ts +54 -0
  111. package/dist/Separator.d.ts.map +1 -0
  112. package/dist/Separator.js +26 -0
  113. package/dist/Slider.d.ts +188 -0
  114. package/dist/Slider.d.ts.map +1 -0
  115. package/dist/Slider.js +96 -0
  116. package/dist/SpinButton.d.ts +188 -0
  117. package/dist/SpinButton.d.ts.map +1 -0
  118. package/dist/SpinButton.js +96 -0
  119. package/dist/Storybook.d.ts +76 -0
  120. package/dist/Storybook.d.ts.map +1 -0
  121. package/dist/Storybook.js +102 -0
  122. package/dist/Switch.d.ts +187 -0
  123. package/dist/Switch.d.ts.map +1 -0
  124. package/dist/Switch.js +116 -0
  125. package/dist/Tab.d.ts +26 -0
  126. package/dist/Tab.d.ts.map +1 -0
  127. package/dist/Tab.js +25 -0
  128. package/dist/Tabs.d.ts +591 -0
  129. package/dist/Tabs.d.ts.map +1 -0
  130. package/dist/Tabs.js +346 -0
  131. package/dist/Toolbar.d.ts +366 -0
  132. package/dist/Toolbar.d.ts.map +1 -0
  133. package/dist/Toolbar.js +245 -0
  134. package/dist/Tooltip.d.ts +296 -0
  135. package/dist/Tooltip.d.ts.map +1 -0
  136. package/dist/Tooltip.js +172 -0
  137. package/dist/Tree.d.ts +591 -0
  138. package/dist/Tree.d.ts.map +1 -0
  139. package/dist/Tree.js +434 -0
  140. package/dist/TreeGrid.d.ts +645 -0
  141. package/dist/TreeGrid.d.ts.map +1 -0
  142. package/dist/TreeGrid.js +420 -0
  143. package/dist/VisuallyHidden.d.ts +54 -0
  144. package/dist/VisuallyHidden.d.ts.map +1 -0
  145. package/dist/VisuallyHidden.js +27 -0
  146. package/dist/WindowSplitter.d.ts +376 -0
  147. package/dist/WindowSplitter.d.ts.map +1 -0
  148. package/dist/WindowSplitter.js +222 -0
  149. package/dist/index.d.ts +49 -0
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +49 -0
  152. package/package.json +45 -18
  153. package/src/HttpRouter.test.ts +0 -256
  154. package/src/HttpRouter.ts +0 -165
  155. package/src/Link.test.ts +0 -84
  156. package/src/Link.ts +0 -107
  157. package/src/index.ts +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"HttpRouter.d.ts","sourceRoot":"","sources":["../src/HttpRouter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAKxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,KAAK,UAAU,EAA4B,MAAM,iCAAiC,CAAC;AAK5F,OAAO,EAQL,KAAK,OAAO,EAGZ,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEvE,KAAK,cAAc,GAAG,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;AAE3C,eAAO,MAAM,UAAU,EAAE;IACvB,CAAC,CAAC,EAAE,CAAC,EACH,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAChC,CAAC,MAAM,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC;IAClF,CAAC,CAAC,EAAE,CAAC,EACH,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAChC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC;CAY1D,CAAC;AAEH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,yGAQvD"}
1
+ {"version":3,"file":"HttpRouter.d.ts","sourceRoot":"","sources":["../src/HttpRouter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAOxC,OAAO,EACL,KAAK,UAAU,EACf,KAAK,QAAQ,IAAI,kBAAkB,EACnC,KAAK,OAAO,IAAI,iBAAiB,EAGlC,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,eAAe,MAAM,sCAAsC,CAAC;AAGxE,OAAO,EAML,KAAK,OAAO,EAKZ,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAoC,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAErF,KAAK,cAAc,GAAG,kBAAkB,GAAG,UAAU,GAAG,MAAM,CAAC;AAE/D,KAAK,qBAAqB,CAAC,CAAC,EAAE,CAAC,IAC3B,OAAO,CAAC,CAAC,EAAE,cAAc,CAAC,GAC1B,iBAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,eAAe,CAAC,eAAe,CAAC,CAAC;AAEzE,KAAK,gBAAgB,CAAC,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AA+EtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,UAAU,EAAE;IACvB,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1F,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAClD,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,mBAAmB,EAAE;IAChC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1F,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CACjD,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,yEAQvD"}
@@ -1,36 +1,182 @@
1
+ import { Fx } from "@typed/fx";
1
2
  import * as Effect from "effect/Effect";
2
- import * as Exit from "effect/Exit";
3
3
  import { dual } from "effect/Function";
4
4
  import * as Layer from "effect/Layer";
5
5
  import * as Option from "effect/Option";
6
- import * as Scope from "effect/Scope";
7
6
  import * as Context from "effect/Context";
8
- import { RouteContext } from "effect/unstable/http/HttpRouter";
7
+ import { hasProperty } from "effect/Predicate";
8
+ import * as Stream from "effect/Stream";
9
+ import { RouteContext, } from "effect/unstable/http/HttpRouter";
9
10
  import * as HttpServerError from "effect/unstable/http/HttpServerError";
10
11
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
11
12
  import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
12
- import { RefSubject } from "@typed/fx";
13
- import { compile, CurrentRoute, makeCatchManager, makeLayerManager, makeLayoutManager, Join, Parse, } from "@typed/router";
13
+ import { compile, CurrentRoute, makeRouteExecutor, Join, Parse, } from "@typed/router";
14
+ import { Ids } from "@typed/id";
14
15
  import { initialMemory } from "@typed/navigation";
15
- import { renderToHtmlString } from "@typed/template";
16
- export const ssrForHttp = dual(2, (router, input) => {
17
- return Effect.gen(function* () {
18
- const matcher = Option.match(yield* Effect.serviceOption(CurrentRoute), {
19
- onNone: () => input,
20
- onSome: (parent) => input.prefix(parent.route),
16
+ import { renderToHtml, renderToHtmlString } from "@typed/template";
17
+ const htmlResponseHeaders = { "content-type": "text/html; charset=utf-8" };
18
+ const bufferedResponse = () => Effect.fn((rendered, requestServices) => Effect.gen(function* () {
19
+ const html = yield* renderToHtmlString(rendered).pipe(Effect.provideContext(requestServices));
20
+ return HttpServerResponse.text(html, { headers: htmlResponseHeaders });
21
+ }));
22
+ const streamingResponse = () => Effect.fn((rendered, requestServices) => Effect.gen(function* () {
23
+ const context = Context.merge(requestServices, yield* Effect.context());
24
+ return HttpServerResponse.stream(Stream.provideContext(renderToHtml(rendered).pipe(Fx.provideContext(requestServices), Fx.toStream, Stream.encodeText), context), { headers: htmlResponseHeaders });
25
+ }));
26
+ function makeSsrForHttp(createResponse) {
27
+ return dual(2, (router, input) => {
28
+ return Effect.gen(function* () {
29
+ const matcher = Option.match(yield* Effect.serviceOption(CurrentRoute), {
30
+ onNone: () => input,
31
+ onSome: (parent) => input.prefix(parent.route),
32
+ });
33
+ const entries = compile(matcher.cases);
34
+ const currentServices = yield* Effect.context();
35
+ const candidatesByPath = new Map();
36
+ for (const entry of entries) {
37
+ const path = getMatcherPath(entry.route.path);
38
+ const candidates = candidatesByPath.get(path);
39
+ if (candidates === undefined) {
40
+ candidatesByPath.set(path, [entry]);
41
+ }
42
+ else {
43
+ candidates.push(entry);
44
+ }
45
+ }
46
+ yield* router.addAll(Array.from(candidatesByPath, ([path, candidates]) => toRoute(path, candidates, currentServices, createResponse)));
21
47
  });
22
- const entries = compile(matcher.cases);
23
- const currentServices = yield* Effect.context();
24
- yield* router.addAll(entries.map((e) => toRoute(e, currentServices)));
25
48
  });
26
- });
49
+ }
50
+ function getMatcherPath(path) {
51
+ const queryStart = path.search(/\?[^/?]+=/);
52
+ return (queryStart < 0 ? path : path.slice(0, queryStart));
53
+ }
54
+ /**
55
+ * Registers buffered, GET-only HTML routes for a matcher.
56
+ *
57
+ * Path captures take precedence over same-named query parameters. Rendering
58
+ * completes before the response is created; this adapter does not stream.
59
+ *
60
+ * @remarks
61
+ * ## Why
62
+ *
63
+ * Typed route matchers already describe decoding, guards, parent prefixes, and
64
+ * render output. `ssrForHttp` installs that same model in Effect's HttpRouter
65
+ * and buffers one complete HTML document for runtimes that require a known body.
66
+ *
67
+ * ## Ownership and lifetime
68
+ *
69
+ * Registration adds GET routes to the supplied Effect HttpRouter. Each request
70
+ * creates request-local memory navigation and current-route services; the
71
+ * request Effect owns rendering and all finalizers. Buffered rendering must
72
+ * complete before the response is returned. Rendering HTML does not mount or
73
+ * hydrate a browser client.
74
+ *
75
+ * ## Routing behavior
76
+ *
77
+ * Matcher cases sharing a path are tried in their compiled order. Path params
78
+ * overwrite same-named query params. Decode/guard failures continue candidate
79
+ * selection; a matched renderer failure stays in the typed request error channel.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import { ssrForHttp } from "@typed/ui/HttpRouter"
84
+ * import { Effect } from "effect"
85
+ * import * as HttpRouter from "effect/unstable/http/HttpRouter"
86
+ * import { Parse, match } from "@typed/router"
87
+ * import { html } from "@typed/template"
88
+ *
89
+ * const pages = match(Parse("/"), html`<h1>Home</h1>`)
90
+ * const routes = Effect.gen(function* () {
91
+ * const router = yield* HttpRouter.make
92
+ * yield* ssrForHttp(router, pages)
93
+ * return router
94
+ * })
95
+ * ```
96
+ *
97
+ * @since 1.0.0
98
+ * @category http
99
+ */
100
+ export const ssrForHttp = makeSsrForHttp(bufferedResponse());
101
+ /**
102
+ * Registers streaming, GET-only HTML routes for a matcher.
103
+ *
104
+ * Path captures take precedence over same-named query parameters. HTML chunks
105
+ * are emitted as they are rendered via `renderToHtml` and `HttpServerResponse.stream`.
106
+ *
107
+ * @remarks
108
+ * ## Why
109
+ *
110
+ * Streaming lets the server send ordered renderer-owned HTML chunks before the
111
+ * whole page completes while preserving the same Typed matcher and Effect HTTP model.
112
+ *
113
+ * ## Ownership and lifetime
114
+ *
115
+ * Registration adds GET routes to the supplied router. Each request owns its
116
+ * navigation/router services and stream Scope. Client cancellation interrupts
117
+ * rendering and runs finalizers. The stream carries typed renderer failures;
118
+ * producing HTML alone does not attach client hydration behavior.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * import { streamingSsrForHttp } from "@typed/ui/HttpRouter"
123
+ * import { Effect } from "effect"
124
+ * import * as HttpRouter from "effect/unstable/http/HttpRouter"
125
+ * import { Parse, match } from "@typed/router"
126
+ * import { html } from "@typed/template"
127
+ *
128
+ * const pages = match(Parse("/"), html`<h1>Streamed home</h1>`)
129
+ * const routes = Effect.gen(function* () {
130
+ * const router = yield* HttpRouter.make
131
+ * yield* streamingSsrForHttp(router, pages)
132
+ * return router
133
+ * })
134
+ * ```
135
+ *
136
+ * @since 1.0.0
137
+ * @category http
138
+ */
139
+ export const streamingSsrForHttp = makeSsrForHttp(streamingResponse());
140
+ /**
141
+ * Converts Effect HTTP server errors into empty 400, 404, or 500 responses.
142
+ * Other failures remain in the global error channel.
143
+ *
144
+ * @remarks
145
+ * ## Why
146
+ *
147
+ * Effect's structured server failures have a safe transport status, whereas
148
+ * application/domain errors must remain visible for deliberate handling.
149
+ *
150
+ * ## Ownership and lifetime
151
+ *
152
+ * The middleware is installed on the supplied router and runs in each request
153
+ * lifetime. It allocates no long-lived resource and does not log, swallow, or
154
+ * reclassify non-HttpServerError failures.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * import { handleHttpServerError } from "@typed/ui/HttpRouter"
159
+ * import { Effect } from "effect"
160
+ * import * as HttpRouter from "effect/unstable/http/HttpRouter"
161
+ *
162
+ * const router = Effect.gen(function* () {
163
+ * const router = yield* HttpRouter.make
164
+ * yield* handleHttpServerError(router)
165
+ * return router
166
+ * })
167
+ * ```
168
+ *
169
+ * @since 1.0.0
170
+ * @category http
171
+ */
27
172
  export function handleHttpServerError(router) {
28
173
  return router.addGlobalMiddleware(Effect.catch((error) => HttpServerError.isHttpServerError(error)
29
- ? Effect.succeed(HttpServerResponse.text(error.message, { status: getStatus(error) }))
174
+ ? Effect.succeed(HttpServerResponse.empty({ status: getStatus(error) }))
30
175
  : Effect.fail(error)));
31
176
  }
32
177
  function getStatus(error) {
33
- switch (error.reason._tag) {
178
+ const reason = error.reason;
179
+ switch (reason._tag) {
34
180
  case "RouteNotFound":
35
181
  return 404;
36
182
  case "RequestParseError":
@@ -38,20 +184,23 @@ function getStatus(error) {
38
184
  case "InternalError":
39
185
  case "ResponseError":
40
186
  return 500;
187
+ default: {
188
+ const _exhaustive = reason;
189
+ return 500;
190
+ }
41
191
  }
42
192
  }
43
- function toRoute(entry, currentServices) {
193
+ function toRoute(path, candidates, currentServices, createResponse) {
44
194
  return {
45
195
  ["~effect/http/HttpRouter/Route"]: "~effect/http/HttpRouter/Route",
46
196
  method: "GET",
47
- path: entry.route.path,
197
+ path,
48
198
  handler: Effect.gen(function* () {
49
- const fiberId = yield* Effect.fiberId;
50
- const rootScope = yield* Effect.scope;
51
199
  const routeContext = yield* RouteContext;
52
200
  const request = yield* HttpServerRequest.HttpServerRequest;
53
201
  const searchParams = yield* HttpServerRequest.ParsedSearchParams;
54
- const provided = Layer.mergeAll(initialMemory({ url: request.url }), Layer.succeed(CurrentRoute, yield* Effect.serviceOption(CurrentRoute).pipe(Effect.map(Option.match({
202
+ const requestUrl = Option.getOrElse(HttpServerRequest.toURL(request), () => new URL(request.url, "http://localhost"));
203
+ const provided = Layer.mergeAll(initialMemory({ url: requestUrl, origin: requestUrl.origin }).pipe(Layer.provide(Ids.Default)), Layer.succeed(CurrentRoute, yield* Effect.serviceOption(CurrentRoute).pipe(Effect.map(Option.match({
55
204
  onNone: () => ({
56
205
  route: Parse(request.url),
57
206
  parent: undefined,
@@ -61,39 +210,33 @@ function toRoute(entry, currentServices) {
61
210
  parent,
62
211
  }),
63
212
  })))));
64
- const input = { ...routeContext.params, ...searchParams };
65
- const params = yield* Effect.mapError(entry.decode(input), (cause) => new HttpServerError.HttpServerError({
66
- reason: new HttpServerError.RequestParseError({ request, cause }),
67
- }));
68
- const memoMap = yield* Layer.makeMemoMap;
69
- const layerManager = makeLayerManager(memoMap, rootScope, fiberId);
70
- const layoutManager = makeLayoutManager(rootScope, fiberId);
71
- const catchManager = makeCatchManager(rootScope, fiberId);
72
- const prepared = yield* layerManager.prepare(entry.layers.concat(provided));
73
- const guardExit = yield* entry
74
- .guard(params)
75
- .pipe(Effect.provideContext(prepared.services), Effect.exit);
76
- if (Exit.isFailure(guardExit) || Option.isNone(guardExit.value)) {
77
- yield* prepared.rollback;
78
- return yield* new HttpServerError.HttpServerError({
79
- reason: new HttpServerError.RouteNotFound({ request }),
80
- });
81
- }
82
- const matchedParams = guardExit.value.value;
83
- yield* prepared.commit;
84
- const scope = yield* Scope.fork(rootScope);
85
- const paramsRef = yield* RefSubject.make(matchedParams).pipe(Scope.provide(scope));
86
- const preparedServices = prepared.services;
87
- const handlerServices = Context.merge(Context.merge(currentServices, preparedServices), Context.make(Scope.Scope, scope));
88
- const handlerFx = entry.handler(paramsRef);
89
- const withLayouts = yield* layoutManager.apply(entry.layouts, matchedParams, handlerFx, preparedServices);
90
- const withCatches = yield* catchManager.apply(entry.catches, withLayouts, preparedServices);
91
- const html = yield* renderToHtmlString(withCatches).pipe(Effect.provideContext(handlerServices));
92
- return HttpServerResponse.text(html, {
93
- headers: { "content-type": "text/html; charset=utf-8" },
94
- });
213
+ const input = { ...searchParams, ...routeContext.params };
214
+ const requestServices = Context.merge(currentServices, yield* Effect.context());
215
+ const executor = yield* makeRouteExecutor().pipe(Effect.provideContext(requestServices));
216
+ const rendered = yield* executor
217
+ .transition({
218
+ path: request.url,
219
+ input,
220
+ candidates,
221
+ layers: [provided],
222
+ })
223
+ .pipe(Effect.catchIf(isRouteDecodeError, (error) => new HttpServerError.HttpServerError({
224
+ reason: new HttpServerError.RequestParseError({ request, cause: error }),
225
+ })), Effect.catchIf(isRouteGuardError, () => new HttpServerError.HttpServerError({
226
+ reason: new HttpServerError.RouteNotFound({ request }),
227
+ })));
228
+ return yield* createResponse(rendered, requestServices);
95
229
  }),
96
230
  uninterruptible: false,
97
231
  prefix: Option.none(),
98
232
  };
99
233
  }
234
+ function hasTag(value, tag) {
235
+ return hasProperty(value, "_tag") && value._tag === tag;
236
+ }
237
+ function isRouteDecodeError(value) {
238
+ return hasTag(value, "RouteDecodeError");
239
+ }
240
+ function isRouteGuardError(value) {
241
+ return hasTag(value, "RouteGuardError");
242
+ }
package/dist/Link.d.ts CHANGED
@@ -1,36 +1,75 @@
1
- import * as Effect from "effect/Effect";
2
1
  import type { Scope } from "effect/Scope";
3
- import type { Stream } from "effect/Stream";
4
- import { type Fx } from "@typed/fx/Fx";
5
- import { EventHandler, type Renderable, type RenderEvent, type RenderTemplate } from "@typed/template";
6
- type EventHandlerProperty = `on${string}`;
7
- type AnchorEventHandlers = {
8
- readonly [K in keyof HTMLAnchorElement as K extends EventHandlerProperty ? K : never]?: Effect.Effect<unknown, any, any> | EventHandler.EventHandler<Event, any, any>;
9
- };
10
- type AnchorRef = {
11
- readonly ref?: (element: HTMLAnchorElement) => void | Effect.Effect<unknown, any, any> | Stream<unknown, any, any> | Fx<unknown, any, any>;
12
- };
13
- type IfEquals<X, Y, Output> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? Output : never;
14
- type WritableKeys<T> = {
15
- [P in keyof T]-?: IfEquals<{
16
- [Q in P]: T[P];
17
- }, {
18
- -readonly [Q in P]: T[P];
19
- }, P>;
20
- }[keyof T];
21
- type AnchorProperties = {
22
- readonly [K in WritableKeys<HTMLAnchorElement> as K extends EventHandlerProperty | "ref" ? never : K]?: Renderable<HTMLAnchorElement[K], any, any>;
23
- };
24
- export interface LinkOptions extends AnchorEventHandlers, AnchorRef, AnchorProperties {
2
+ import * as Fx from "@typed/fx/Fx";
3
+ import { type NavigationError, Navigation } from "@typed/navigation";
4
+ import { type Renderable, type RenderEvent, type RenderTemplate } from "@typed/template";
5
+ import * as Dom from "./Dom.js";
6
+ import type { HostResult } from "./Dom/Types.js";
7
+ /**
8
+ * Native anchor options plus Typed navigation behavior.
9
+ *
10
+ * @remarks
11
+ * ## Why
12
+ *
13
+ * `href` and content remain renderable, while `replace` selects history policy.
14
+ * Ordinary anchor options—including target, download, rel, and user click
15
+ * handlers—stay available through the DOM host model.
16
+ *
17
+ * ## Ownership and lifetime
18
+ *
19
+ * Options are inert. Rendering owns subscriptions to renderable values and the
20
+ * click listener for its Effect Scope; the `Navigation` service owns history.
21
+ *
22
+ * @since 1.0.0
23
+ * @category models
24
+ */
25
+ export type LinkOptions = Dom.HostOptions<HTMLAnchorElement> & Dom.ElementOptions<HTMLAnchorElement> & {
25
26
  readonly href: Renderable<string, any, any>;
26
27
  readonly content: Renderable<string | number | boolean | null | undefined | void | RenderEvent, any, any>;
27
28
  readonly replace?: boolean;
28
- }
29
+ };
30
+ type LinkError<Options extends object> = Renderable.ErrorFromObject<Options> | NavigationError;
31
+ type LinkServices<Options extends object> = Renderable.ServicesFromObject<Options> | Navigation | Scope | RenderTemplate;
29
32
  /**
30
- * Renders an `<a href="...">` that intercepts same-origin, same-document clicks
31
- * and navigates via `Navigation.navigate` instead of full page load. Requires
32
- * `Navigation` and `RenderTemplate` in the Effect context (e.g. `BrowserRouter`).
33
+ * Renders a native anchor with safe, eligible same-origin SPA navigation.
34
+ *
35
+ * Modified clicks, downloads, external URLs, non-HTTP(S) destinations, and
36
+ * non-self targets retain browser behavior. Unsafe executable schemes are
37
+ * rendered as `about:blank` and their clicks are canceled. A user click handler
38
+ * runs first through DOM host event chaining; `preventDefault()` prevents the
39
+ * internal navigation decision.
40
+ *
41
+ * @remarks
42
+ * ## Why
43
+ *
44
+ * `Link` keeps the anchor as the integration boundary: native opening, copying,
45
+ * status previews, modifier keys, downloads, and external navigation continue
46
+ * to work. Only an unmodified primary click to same-origin HTTP(S) is handed to
47
+ * Typed's Effect-native `Navigation` service.
48
+ *
49
+ * ## Ownership and lifetime
50
+ *
51
+ * Calling `Link` starts no work. Running the returned Fx subscribes to dynamic
52
+ * href/content and installs a real, non-passive DOM click listener in its
53
+ * Effect Scope. Scope finalization removes only those resources. Navigation
54
+ * failures and required services remain visible in the Fx type. A custom host
55
+ * must preserve the sanitized href, chained click handler, content, and native
56
+ * anchor behavior.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { Link } from "@typed/ui/Link"
61
+ *
62
+ * const profile = Link({
63
+ * href: "/account/profile",
64
+ * content: "Profile",
65
+ * replace: false
66
+ * })
67
+ * ```
68
+ *
69
+ * @since 1.0.0
70
+ * @category components
33
71
  */
34
- export declare function Link<const Opts extends LinkOptions>(options: Opts): Fx<RenderEvent, Renderable.ErrorFromObject<Opts>, Renderable.ServicesFromObject<Opts> | Scope | RenderTemplate>;
72
+ export declare function Link<const Options extends object>(options: Options, ..._: Options extends LinkOptions ? [] : ["Link options must satisfy LinkOptions"]): Fx.Fx<RenderEvent, LinkError<Options>, LinkServices<Options>>;
73
+ export declare function Link<const Options extends object, const Host extends HostResult>(options: Options, host: Dom.HostOverride<Dom.HostProps<HTMLAnchorElement>, Renderable.Any, Host>, ..._: Options extends LinkOptions ? [] : ["Link options must satisfy LinkOptions"]): Fx.Fx<RenderEvent, LinkError<Options> | Fx.Error<Host>, LinkServices<Options> | Fx.Services<Host>>;
35
74
  export {};
36
75
  //# sourceMappingURL=Link.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../src/Link.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,EAAO,MAAM,cAAc,CAAC;AAG5C,OAAO,EACL,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AAEzB,KAAK,oBAAoB,GAAG,KAAK,MAAM,EAAE,CAAC;AAE1C,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,CAAC,IAAI,MAAM,iBAAiB,IAAI,CAAC,SAAS,oBAAoB,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,EAClF,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAChC,YAAY,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC;CAC/C,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,QAAQ,CAAC,GAAG,CAAC,EAAE,CACb,OAAO,EAAE,iBAAiB,KACvB,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;CAClG,CAAC;AAEF,KAAK,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,IACxB,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC;AAEvF,KAAK,YAAY,CAAC,CAAC,IAAI;KACpB,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;SAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAAE,EAAE;QAAE,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;KAAE,EAAE,CAAC,CAAC;CAChF,CAAC,MAAM,CAAC,CAAC,CAAC;AAEX,KAAK,gBAAgB,GAAG;IACtB,QAAQ,EAAE,CAAC,IAAI,YAAY,CAAC,iBAAiB,CAAC,IAAI,CAAC,SAAS,oBAAoB,GAAG,KAAK,GACpF,KAAK,GACL,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC;CACpD,CAAC;AAEF,MAAM,WAAW,WAAY,SAAQ,mBAAmB,EAAE,SAAS,EAAE,gBAAgB;IACnF,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,UAAU,CAC1B,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,WAAW,EACjE,GAAG,EACH,GAAG,CACJ,CAAC;IACF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAyBD;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,WAAW,EACjD,OAAO,EAAE,IAAI,GACZ,EAAE,CACH,WAAW,EACX,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,EAChC,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,KAAK,GAAG,cAAc,CAC7D,CAqBA"}
1
+ {"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../src/Link.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,KAAK,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EAEpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC,iBAAiB,CAAC,GAC1D,GAAG,CAAC,cAAc,CAAC,iBAAiB,CAAC,GAAG;IACtC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,UAAU,CAC1B,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,WAAW,EACjE,GAAG,EACH,GAAG,CACJ,CAAC;IACF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEJ,KAAK,SAAS,CAAC,OAAO,SAAS,MAAM,IAAI,UAAU,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,eAAe,CAAC;AAE/F,KAAK,YAAY,CAAC,OAAO,SAAS,MAAM,IACpC,UAAU,CAAC,kBAAkB,CAAC,OAAO,CAAC,GACtC,UAAU,GACV,KAAK,GACL,cAAc,CAAC;AA2EnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,OAAO,SAAS,MAAM,EAC/C,OAAO,EAAE,OAAO,EAChB,GAAG,CAAC,EAAE,OAAO,SAAS,WAAW,GAAG,EAAE,GAAG,CAAC,uCAAuC,CAAC,GACjF,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;AACjE,wBAAgB,IAAI,CAAC,KAAK,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,CAAC,IAAI,SAAS,UAAU,EAC9E,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,EAC9E,GAAG,CAAC,EAAE,OAAO,SAAS,WAAW,GAAG,EAAE,GAAG,CAAC,uCAAuC,CAAC,GACjF,EAAE,CAAC,EAAE,CACN,WAAW,EACX,SAAS,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EACnC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAC1C,CAAC"}
package/dist/Link.js CHANGED
@@ -1,45 +1,99 @@
1
1
  import * as Effect from "effect/Effect";
2
- import { gen } from "@typed/fx/Fx";
3
- import { RefSubject } from "@typed/fx/RefSubject";
4
- import { getUrl, Navigation } from "@typed/navigation";
2
+ import * as Stream from "effect/Stream";
3
+ import * as Fx from "@typed/fx/Fx";
4
+ import { Navigation } from "@typed/navigation";
5
5
  import { EventHandler, html, } from "@typed/template";
6
- function makeLinkClickHandler(replace$) {
7
- return EventHandler.make((ev) => Effect.gen(function* () {
8
- const href = ev.currentTarget.href;
9
- if (ev.ctrlKey || ev.metaKey || ev.shiftKey)
6
+ import * as Dom from "./Dom.js";
7
+ function makeLinkClickHandler(replace) {
8
+ // Classification and native cancellation must run during event dispatch;
9
+ // only the resulting router navigation is lazy.
10
+ return EventHandler.make((event) => {
11
+ const anchor = Dom.currentTarget(event);
12
+ const href = anchor.href;
13
+ if (event.defaultPrevented ||
14
+ event.button !== 0 ||
15
+ event.metaKey ||
16
+ event.altKey ||
17
+ event.ctrlKey ||
18
+ event.shiftKey)
10
19
  return;
11
- const t = ev.currentTarget.target;
12
- if (t && t !== "_self")
20
+ const target = anchor.target;
21
+ if (target && target !== "_self")
13
22
  return;
14
- const nav = yield* Navigation;
15
- const target = getUrl(nav.origin, href);
16
- if (target.origin !== nav.origin)
23
+ if (anchor.hasAttribute("download"))
17
24
  return;
18
- ev.preventDefault();
19
- const replace = yield* replace$;
20
- yield* nav.navigate(href, { history: replace ? "replace" : "push" });
21
- }));
25
+ if (!isSafeLinkHref(href)) {
26
+ event.preventDefault();
27
+ return;
28
+ }
29
+ const destination = new URL(href);
30
+ if (destination.protocol !== "http:" && destination.protocol !== "https:")
31
+ return;
32
+ if (destination.origin !== anchor.ownerDocument.defaultView?.location.origin)
33
+ return;
34
+ event.preventDefault();
35
+ return Effect.flatMap(Navigation, (navigation) => navigation.navigate(href, { history: replace ? "replace" : "push" }));
36
+ }, { passive: false });
37
+ }
38
+ function legacyProps(options) {
39
+ const modern = options.props;
40
+ const props = {};
41
+ for (const [key, value] of Object.entries(options)) {
42
+ if (key === "content" ||
43
+ key === "href" ||
44
+ key === "props" ||
45
+ key === "ref" ||
46
+ key === "replace" ||
47
+ key[0] === "@" ||
48
+ (key[0] === "o" && key[1] === "n") ||
49
+ (modern !== undefined && Object.hasOwn(modern, key)))
50
+ continue;
51
+ props[key] = value;
52
+ }
53
+ return props;
22
54
  }
23
- /**
24
- * Renders an `<a href="...">` that intercepts same-origin, same-document clicks
25
- * and navigates via `Navigation.navigate` instead of full page load. Requires
26
- * `Navigation` and `RenderTemplate` in the Effect context (e.g. `BrowserRouter`).
27
- */
28
- export function Link(options) {
29
- return gen(function* () {
30
- const { replace = false, onclick, content: children, ...rest } = options;
31
- const replace$ = yield* RefSubject.make(replace);
32
- const navigationHandler = makeLinkClickHandler(replace$);
33
- const userHandler = onclick ? EventHandler.fromEffectOrEventHandler(onclick) : undefined;
34
- const clickHandler = userHandler
35
- ? EventHandler.make(Effect.fn(function* (ev) {
36
- yield* userHandler.handler(ev);
37
- if (ev.defaultPrevented)
38
- return;
39
- yield* navigationHandler.handler(ev);
40
- }), { ...userHandler.options, preventDefault: true })
41
- : navigationHandler;
42
- const props = { ...rest, onclick: clickHandler };
43
- return html `<a ...${props}>${children}</a>`;
55
+ function internalProps(options) {
56
+ return () => ({
57
+ ...legacyProps(options),
58
+ href: sanitizeLinkHref(options.href),
59
+ onclick: makeLinkClickHandler(options.replace ?? false),
44
60
  });
45
61
  }
62
+ // Keep the implementation erased: constraining it to LinkOptions widens the
63
+ // Renderable `any` slots and loses the exact error/service inference above.
64
+ export function Link(options, host, ..._) {
65
+ const render = Dom.renderHost();
66
+ return render(options, host, internalProps(options), options.content, (props, content) => html `<a ...${props}>${content}</a>`);
67
+ }
68
+ function sanitizeLinkHref(href) {
69
+ if (Fx.isFx(href))
70
+ return Fx.map(href, neutralizeExecutableHref);
71
+ if (Stream.isStream(href)) {
72
+ return Stream.map(href, neutralizeExecutableHref);
73
+ }
74
+ if (Effect.isEffect(href))
75
+ return Effect.map(href, neutralizeExecutableHref);
76
+ if (Array.isArray(href)) {
77
+ return href.map((value) => sanitizeLinkHref(value));
78
+ }
79
+ return (typeof href === "string" ? neutralizeExecutableHref(href) : href);
80
+ }
81
+ function neutralizeExecutableHref(href) {
82
+ return isSafeLinkHref(href) ? href : "about:blank";
83
+ }
84
+ function isSafeLinkHref(href) {
85
+ try {
86
+ switch (new URL(href, "http://localhost").protocol) {
87
+ case "http:":
88
+ case "https:":
89
+ case "mailto:":
90
+ case "tel:":
91
+ return true;
92
+ default:
93
+ return false;
94
+ }
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }