@sjcx/vorma 0.0.0-pre.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.
- package/LICENSE +20 -0
- package/dist/build/assemble-artifacts.d.ts +23 -0
- package/dist/build/browser-contracts.d.ts +8 -0
- package/dist/build/bundle-client.d.ts +29 -0
- package/dist/build/cli.d.ts +2 -0
- package/dist/build/cli.js +1256 -0
- package/dist/build/client-inputs.d.ts +11 -0
- package/dist/build/client-output-hashing.d.ts +3 -0
- package/dist/build/client-public-urls.d.ts +20 -0
- package/dist/build/compare-text.d.ts +5 -0
- package/dist/build/compile-application.d.ts +40 -0
- package/dist/build/compile-server.d.ts +42 -0
- package/dist/build/contained-path.d.ts +29 -0
- package/dist/build/critical-css.d.ts +7 -0
- package/dist/build/css-public-assets.d.ts +15 -0
- package/dist/build/development-artifacts.d.ts +35 -0
- package/dist/build/development-browser-source.d.ts +3 -0
- package/dist/build/development-build.d.ts +30 -0
- package/dist/build/development-changes.d.ts +14 -0
- package/dist/build/development-client.d.ts +21 -0
- package/dist/build/development-generation.d.ts +20 -0
- package/dist/build/development-listener.d.ts +14 -0
- package/dist/build/development-patterns.d.ts +5 -0
- package/dist/build/development-protocol.d.ts +20 -0
- package/dist/build/development-server-process.d.ts +30 -0
- package/dist/build/development-sources.d.ts +23 -0
- package/dist/build/development-stylesheet.d.ts +15 -0
- package/dist/build/development-watcher.d.ts +15 -0
- package/dist/build/development-work.d.ts +32 -0
- package/dist/build/development.d.ts +3 -0
- package/dist/build/generate-application.d.ts +62 -0
- package/dist/build/index.d.ts +15 -0
- package/dist/build/index.js +2 -0
- package/dist/build/inspect-css-artifacts.d.ts +11 -0
- package/dist/build/inspect-projects.d.ts +69 -0
- package/dist/build/load-config.d.ts +56 -0
- package/dist/build/output-files.d.ts +8 -0
- package/dist/build/output-ownership.d.ts +21 -0
- package/dist/build/public-assets.d.ts +32 -0
- package/dist/build/publish-artifacts.d.ts +23 -0
- package/dist/build/render-client-contracts.d.ts +10 -0
- package/dist/build/render-client-transport.d.ts +11 -0
- package/dist/build/render-server-generated.d.ts +33 -0
- package/dist/build/render-source-text.d.ts +24 -0
- package/dist/build/render-types.d.ts +31 -0
- package/dist/build/resolve-config.d.ts +50 -0
- package/dist/build/source-layout.d.ts +24 -0
- package/dist/build/typescript-projects.d.ts +36 -0
- package/dist/build-JgARonaY.js +3214 -0
- package/dist/build-invocation-D7Fu8gBK.js +4 -0
- package/dist/client/assembly.d.ts +40 -0
- package/dist/client/browser-http-client.d.ts +10 -0
- package/dist/client/client-loaders.d.ts +93 -0
- package/dist/client/commit.d.ts +41 -0
- package/dist/client/contracts.d.ts +192 -0
- package/dist/client/core.d.ts +45 -0
- package/dist/client/development.d.ts +16 -0
- package/dist/client/document-recovery.d.ts +4 -0
- package/dist/client/endpoints.d.ts +54 -0
- package/dist/client/errors.d.ts +87 -0
- package/dist/client/freshness.d.ts +51 -0
- package/dist/client/head.d.ts +14 -0
- package/dist/client/history.d.ts +25 -0
- package/dist/client/index.d.ts +80 -0
- package/dist/client/index.js +150 -0
- package/dist/client/links.d.ts +48 -0
- package/dist/client/materialize.d.ts +21 -0
- package/dist/client/modules.d.ts +2 -0
- package/dist/client/navigation.d.ts +47 -0
- package/dist/client/payload.d.ts +54 -0
- package/dist/client/prefetch.d.ts +28 -0
- package/dist/client/prepare.d.ts +47 -0
- package/dist/client/projections.d.ts +26 -0
- package/dist/client/queries.d.ts +33 -0
- package/dist/client/react/application.d.ts +87 -0
- package/dist/client/react/hooks.d.ts +36 -0
- package/dist/client/react/index.d.ts +5 -0
- package/dist/client/react/index.js +3237 -0
- package/dist/client/react/link.d.ts +27 -0
- package/dist/client/react/view.d.ts +86 -0
- package/dist/client/request.d.ts +16 -0
- package/dist/client/route-context.d.ts +31 -0
- package/dist/client/route-matching.d.ts +22 -0
- package/dist/client/route-synchronization.d.ts +30 -0
- package/dist/client/route-url.d.ts +5 -0
- package/dist/client/runtime.d.ts +82 -0
- package/dist/client/scroll.d.ts +16 -0
- package/dist/client/services.d.ts +34 -0
- package/dist/client/state.d.ts +102 -0
- package/dist/client/static-assets.d.ts +3 -0
- package/dist/client/stylesheets.d.ts +7 -0
- package/dist/client/transport-type-entries.d.ts +2 -0
- package/dist/client/types.d.ts +152 -0
- package/dist/client/view-definition.d.ts +24 -0
- package/dist/client/work-indicator.d.ts +18 -0
- package/dist/compile-Me8EOyMK.js +870 -0
- package/dist/development-B_rxuwY7.js +128 -0
- package/dist/hash-set-BPx3iXec.js +141 -0
- package/dist/href-Cqyax8Y0.js +357 -0
- package/dist/materialize-DZ7VSLFk.js +739 -0
- package/dist/metadata-BoPKo-G2.js +32 -0
- package/dist/primitives/tasks/index.d.ts +1 -0
- package/dist/primitives/tasks/index.js +1 -0
- package/dist/route-context-CtY_9U_W.js +559 -0
- package/dist/route-matching-D_uSG2P9.js +33 -0
- package/dist/search-parse-0f7oqVsG.js +375 -0
- package/dist/server/config/index.d.ts +36 -0
- package/dist/server/config/index.js +30 -0
- package/dist/server/development/metadata.d.ts +11 -0
- package/dist/server/document.d.ts +37 -0
- package/dist/server/form-data.d.ts +21 -0
- package/dist/server/graph/compile.d.ts +118 -0
- package/dist/server/graph/declarations.d.ts +117 -0
- package/dist/server/graph/literal-error-tag.d.ts +14 -0
- package/dist/server/graph/pattern-conflicts.d.ts +36 -0
- package/dist/server/graph/search-schema.d.ts +13 -0
- package/dist/server/graph/transport-schema.d.ts +11 -0
- package/dist/server/index.d.ts +310 -0
- package/dist/server/index.js +2311 -0
- package/dist/server/public-assets.d.ts +18 -0
- package/dist/server/runtime/application.d.ts +15 -0
- package/dist/server/runtime/assets.d.ts +15 -0
- package/dist/server/runtime/context.d.ts +96 -0
- package/dist/server/runtime/domain-errors.d.ts +11 -0
- package/dist/server/runtime/effects.d.ts +83 -0
- package/dist/server/runtime/execute.d.ts +13 -0
- package/dist/server/runtime/finalize.d.ts +44 -0
- package/dist/server/runtime/handler.d.ts +106 -0
- package/dist/server/runtime/head.d.ts +22 -0
- package/dist/server/runtime/payload.d.ts +50 -0
- package/dist/server/runtime/pipeline.d.ts +27 -0
- package/dist/server/runtime/plan.d.ts +98 -0
- package/dist/server/runtime/render-document.d.ts +16 -0
- package/dist/server/runtime/startup-error.d.ts +11 -0
- package/dist/server/runtime/transport-type-entries.d.ts +2 -0
- package/dist/server/runtime/vercel-deployment.d.ts +2 -0
- package/dist/server/url-pathname.d.ts +13 -0
- package/dist/static-assets-BrH3O0M8.js +199 -0
- package/dist/universal/content-digest.d.ts +7 -0
- package/dist/universal/generated-types.d.ts +9 -0
- package/dist/universal/href.d.ts +17 -0
- package/dist/universal/http-method.d.ts +4 -0
- package/dist/universal/json-transport.d.ts +14 -0
- package/dist/universal/null-prototype-record.d.ts +7 -0
- package/dist/universal/path-wire.d.ts +71 -0
- package/dist/universal/pattern-language.d.ts +4 -0
- package/dist/universal/protocol-schemas.d.ts +99 -0
- package/dist/universal/protocol.d.ts +162 -0
- package/dist/universal/search-grammar.d.ts +7 -0
- package/dist/universal/search-parse.d.ts +22 -0
- package/dist/universal/search-serialize.d.ts +16 -0
- package/dist/universal/search-shape.d.ts +32 -0
- package/dist/universal/static-assets.d.ts +2 -0
- package/dist/universal/transport-types/big-decimal.d.ts +13 -0
- package/dist/universal/transport-types/chunk.d.ts +8 -0
- package/dist/universal/transport-types/date-time-utc.d.ts +13 -0
- package/dist/universal/transport-types/date-time-zoned.d.ts +13 -0
- package/dist/universal/transport-types/duration.d.ts +13 -0
- package/dist/universal/transport-types/hash-map.d.ts +8 -0
- package/dist/universal/transport-types/hash-set.d.ts +8 -0
- package/dist/universal/transport-types/option.d.ts +10 -0
- package/dist/universal/transport-types/redacted.d.ts +9 -0
- package/dist/universal/value-transport.d.ts +117 -0
- package/dist/value-transport-Bst4Hk-n.js +864 -0
- package/dist/vite/build-invocation.d.ts +4 -0
- package/dist/vite/index.d.ts +3 -0
- package/dist/vite/index.js +15 -0
- package/package.json +84 -0
|
@@ -0,0 +1,2311 @@
|
|
|
1
|
+
import { _ as DOCUMENT_PROTOCOL, b as HTTP_PROTOCOL, c as compileUiPattern, g as DEVELOPMENT_PROTOCOL, i as encodeTransportedValue, l as decodeMatchCaptures, m as ARTIFACT_PROTOCOL, o as canonicalizeRequestPathForMatcher, p as makeNullPrototypeRecord, r as decodeTransportedValue, x as PROCESS_ENVIRONMENT_PROTOCOL } from "../value-transport-Bst4Hk-n.js";
|
|
2
|
+
import { a as compileUiLoader, d as FormData, i as compileMiddleware, l as getLiteralErrorTag, n as checkEndpointPatternRegistrations, o as createNestedRouteChains, r as compileEndpoint, s as groupEndpointsByMethod, t as checkDuplicates, u as getInputTransport } from "../compile-Me8EOyMK.js";
|
|
3
|
+
import { n as getOwnEntries, r as projectSearchFields, t as buildHref } from "../href-Cqyax8Y0.js";
|
|
4
|
+
import { a as bigDecimalTransportType, c as redactedTransportType, i as dateTimeUtcTransportType, l as optionTransportType, n as hashMapTransportType, o as durationTransportType, r as dateTimeZonedTransportType, s as chunkTransportType, t as hashSetTransportType } from "../hash-set-BPx3iXec.js";
|
|
5
|
+
import { i as readTransportedValue, t as getStaticAssetDestination } from "../static-assets-BrH3O0M8.js";
|
|
6
|
+
import { t as parseSearchParams } from "../search-parse-0f7oqVsG.js";
|
|
7
|
+
import { Cause, Config, Context, Data, Deferred, Effect, Exit, Fiber, Layer, Option, Record, Ref, Result, Schema } from "effect";
|
|
8
|
+
import { base64 } from "@scure/base";
|
|
9
|
+
import * as Path$1 from "effect/Path";
|
|
10
|
+
import { createFlatMatcher, createFlatValueMatcher, createNestedValueMatcher, findBestMatch, findBestValueMatch, findNestedValueMatches } from "@vorma/primitives/matcher";
|
|
11
|
+
import { HttpServerError, HttpServerRequest, HttpServerResponse, UrlParams } from "effect/unstable/http";
|
|
12
|
+
import { TaskContext, TaskRuntime } from "@vorma/primitives/tasks";
|
|
13
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
14
|
+
import * as HttpPlatform from "effect/unstable/http/HttpPlatform";
|
|
15
|
+
import * as HttpServerRequest$1 from "effect/unstable/http/HttpServerRequest";
|
|
16
|
+
import * as HttpServerResponse$1 from "effect/unstable/http/HttpServerResponse";
|
|
17
|
+
import { memoize } from "effect/Function";
|
|
18
|
+
import * as Cookies from "effect/unstable/http/Cookies";
|
|
19
|
+
import * as Headers$1 from "effect/unstable/http/Headers";
|
|
20
|
+
//#region src/server/runtime/startup-error.ts
|
|
21
|
+
function createVormaStartupError(input) {
|
|
22
|
+
return {
|
|
23
|
+
_tag: "VormaStartupError",
|
|
24
|
+
operation: input.operation,
|
|
25
|
+
message: input.message,
|
|
26
|
+
...input.cause === void 0 ? {} : { cause: input.cause }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/server/runtime/application.ts
|
|
31
|
+
const ApplicationRuntime = Context.Service("vorma/server/ApplicationRuntime");
|
|
32
|
+
const APPLICATION_RUNTIME = Symbol("vorma/server/ApplicationRuntimeLayer");
|
|
33
|
+
/** One shared acquisition for an application's compiled registration and asset metadata. */
|
|
34
|
+
function makeApplicationRuntime(config) {
|
|
35
|
+
return Layer.effect(ApplicationRuntime, Effect.tryPromise({
|
|
36
|
+
try: async () => {
|
|
37
|
+
const [registration, metadata] = await Promise.all([import(new URL(`./${ARTIFACT_PROTOCOL.compiledRuntimeFileName}`, config.importMeta.url).href), import(new URL(`./${ARTIFACT_PROTOCOL.productionMetadataFileName}`, config.importMeta.url).href, { with: { type: "json" } })]);
|
|
38
|
+
const initial = {
|
|
39
|
+
...metadata.default,
|
|
40
|
+
publicAssetDirectoryUrl: registration.publicAssetDirectoryUrl
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
registeredDeclarations: registration.registeredDeclarations,
|
|
44
|
+
metadata: initial,
|
|
45
|
+
...initial.runtimeMode === "development" ? { readDevelopmentMetadata: (await import("../metadata-BoPKo-G2.js")).createDevelopmentMetadataReader({
|
|
46
|
+
url: new URL(`./${ARTIFACT_PROTOCOL.productionMetadataFileName}`, config.importMeta.url),
|
|
47
|
+
initial
|
|
48
|
+
}) } : {}
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
catch: (cause) => {
|
|
52
|
+
return createVormaStartupError({
|
|
53
|
+
operation: "loadApplicationRuntime",
|
|
54
|
+
message: "Vorma could not load its compiled application runtime. Build the application and start its compiled server entry.",
|
|
55
|
+
cause
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/server/public-assets.ts
|
|
62
|
+
/** A content-addressed public asset remains immutable at its published URL. */
|
|
63
|
+
const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
64
|
+
function resolvePublicAssetFilePaths(input) {
|
|
65
|
+
return Effect.gen(function* () {
|
|
66
|
+
const { metadata, path } = input;
|
|
67
|
+
const directoryUrl = yield* Schema.decodeUnknownEffect(Schema.URLFromString)(metadata.publicAssetDirectoryUrl).pipe(Effect.mapError((cause) => {
|
|
68
|
+
return createVormaStartupError({
|
|
69
|
+
operation: "resolvePublicAssetDirectory",
|
|
70
|
+
message: `Vorma production metadata does not carry a valid public asset directory URL: ${metadata.publicAssetDirectoryUrl}`,
|
|
71
|
+
cause
|
|
72
|
+
});
|
|
73
|
+
}));
|
|
74
|
+
const directory = yield* path.fromFileUrl(directoryUrl).pipe(Effect.mapError((cause) => {
|
|
75
|
+
return createVormaStartupError({
|
|
76
|
+
operation: "resolvePublicAssetDirectory",
|
|
77
|
+
message: `Vorma public asset bytes are not on a local filesystem: ${metadata.publicAssetDirectoryUrl}`,
|
|
78
|
+
cause
|
|
79
|
+
});
|
|
80
|
+
}));
|
|
81
|
+
const filePathsByUrl = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const file of metadata.publicFiles) filePathsByUrl.set(file.url, {
|
|
83
|
+
filePath: path.join(directory, ...file.fileName.split("/")),
|
|
84
|
+
immutable: file.immutable
|
|
85
|
+
});
|
|
86
|
+
return filePathsByUrl;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function serveFile(input) {
|
|
90
|
+
return HttpServerResponse$1.file(input.file.filePath, { headers: input.file.immutable ? { "cache-control": IMMUTABLE_CACHE_CONTROL } : {} }).pipe(Effect.provideService(HttpPlatform.HttpPlatform, input.platform));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Serves the build's public directory from the process filesystem. Requests for
|
|
94
|
+
* URLs outside the published set, and every other method than `GET` and `HEAD`,
|
|
95
|
+
* continue to the host's own router. The host passes its explicitly assembled
|
|
96
|
+
* application so that this constructor can read the build metadata. The caller
|
|
97
|
+
* explicitly registers the returned middleware through the host router.
|
|
98
|
+
*/
|
|
99
|
+
function makePublicAssetsFileMiddleware(input) {
|
|
100
|
+
return Effect.gen(function* () {
|
|
101
|
+
const path = yield* Path$1.Path;
|
|
102
|
+
const runtime = yield* ApplicationRuntime;
|
|
103
|
+
const { metadata } = runtime;
|
|
104
|
+
let resolvedMetadata = metadata;
|
|
105
|
+
let filePathsByUrl = yield* resolvePublicAssetFilePaths({
|
|
106
|
+
metadata,
|
|
107
|
+
path
|
|
108
|
+
});
|
|
109
|
+
const platform = yield* HttpPlatform.HttpPlatform;
|
|
110
|
+
return (httpEffect) => {
|
|
111
|
+
return Effect.gen(function* () {
|
|
112
|
+
const request = yield* HttpServerRequest$1.HttpServerRequest;
|
|
113
|
+
const method = request.method;
|
|
114
|
+
if (method !== "GET" && method !== "HEAD") return yield* httpEffect;
|
|
115
|
+
if (runtime.readDevelopmentMetadata !== void 0) {
|
|
116
|
+
const next = yield* runtime.readDevelopmentMetadata;
|
|
117
|
+
if (next !== resolvedMetadata) {
|
|
118
|
+
filePathsByUrl = yield* resolvePublicAssetFilePaths({
|
|
119
|
+
metadata: next,
|
|
120
|
+
path
|
|
121
|
+
});
|
|
122
|
+
resolvedMetadata = next;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const queryOffset = request.url.indexOf("?");
|
|
126
|
+
const file = filePathsByUrl.get(queryOffset === -1 ? request.url : request.url.slice(0, queryOffset));
|
|
127
|
+
if (file === void 0) return yield* httpEffect;
|
|
128
|
+
return yield* serveFile({
|
|
129
|
+
file,
|
|
130
|
+
platform
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
}).pipe(Effect.provide(input.app[APPLICATION_RUNTIME]));
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/server/runtime/effects.ts
|
|
138
|
+
const empty = {
|
|
139
|
+
status: null,
|
|
140
|
+
terminal: null,
|
|
141
|
+
headerOps: [],
|
|
142
|
+
cookies: Cookies.empty,
|
|
143
|
+
headElements: []
|
|
144
|
+
};
|
|
145
|
+
/** Whether these effects end the response for everything after them. */
|
|
146
|
+
function endsResponse(effects) {
|
|
147
|
+
return effects.terminal !== null;
|
|
148
|
+
}
|
|
149
|
+
function mergeResponseEffects(input) {
|
|
150
|
+
const { earlier, later } = input;
|
|
151
|
+
return {
|
|
152
|
+
status: later.status === null ? earlier.status : later.status,
|
|
153
|
+
terminal: later.terminal === null ? earlier.terminal : later.terminal,
|
|
154
|
+
headerOps: [...earlier.headerOps, ...later.headerOps],
|
|
155
|
+
cookies: Cookies.merge(earlier.cookies, later.cookies),
|
|
156
|
+
headElements: [...earlier.headElements, ...later.headElements]
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function setStatus(input) {
|
|
160
|
+
return {
|
|
161
|
+
...input.effects,
|
|
162
|
+
status: input.status
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function setHeader(input) {
|
|
166
|
+
return {
|
|
167
|
+
...input.effects,
|
|
168
|
+
headerOps: [...input.effects.headerOps, {
|
|
169
|
+
kind: "set",
|
|
170
|
+
name: input.name,
|
|
171
|
+
value: input.value
|
|
172
|
+
}]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function appendHeader(input) {
|
|
176
|
+
return {
|
|
177
|
+
...input.effects,
|
|
178
|
+
headerOps: [...input.effects.headerOps, {
|
|
179
|
+
kind: "append",
|
|
180
|
+
name: input.name,
|
|
181
|
+
value: input.value
|
|
182
|
+
}]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function setCookie(input) {
|
|
186
|
+
return {
|
|
187
|
+
...input.effects,
|
|
188
|
+
cookies: Cookies.setCookie(input.effects.cookies, input.cookie)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function appendHeadElements(input) {
|
|
192
|
+
return {
|
|
193
|
+
...input.effects,
|
|
194
|
+
headElements: [...input.effects.headElements, ...input.elements]
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function setRedirect(input) {
|
|
198
|
+
return {
|
|
199
|
+
...input.effects,
|
|
200
|
+
status: input.client ? 200 : input.status,
|
|
201
|
+
terminal: {
|
|
202
|
+
_tag: "Redirect",
|
|
203
|
+
location: input.location,
|
|
204
|
+
status: input.status,
|
|
205
|
+
client: input.client
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Made by the execute program from a handler's error, never by a handler. */
|
|
210
|
+
function setTerminalError(input) {
|
|
211
|
+
return {
|
|
212
|
+
...input.effects,
|
|
213
|
+
status: input.status,
|
|
214
|
+
terminal: {
|
|
215
|
+
_tag: "Error",
|
|
216
|
+
status: input.status,
|
|
217
|
+
text: input.text,
|
|
218
|
+
...input.domain === void 0 ? {} : { domain: input.domain }
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/server/runtime/handler.ts
|
|
224
|
+
/**
|
|
225
|
+
* How any handler fails on purpose. In a UI loader: the response carries the
|
|
226
|
+
* status and public message without the failed route's metadata or module.
|
|
227
|
+
* An available ancestor boundary or the application boundary renders the error.
|
|
228
|
+
* In an endpoint or
|
|
229
|
+
* task middleware: the response is the status and the message. `cause` never
|
|
230
|
+
* leaves the server.
|
|
231
|
+
*/
|
|
232
|
+
var HandlerError = class extends Data.TaggedError("HandlerError") {};
|
|
233
|
+
/**
|
|
234
|
+
* A declared domain failure on its way to the wire. A `HandlerError` with
|
|
235
|
+
* the encoded domain payload beside it, so the execute program keeps its
|
|
236
|
+
* single error channel while the response keeps the tag and data a generic
|
|
237
|
+
* rejection cannot carry. Constructed only by the endpoint runner after it
|
|
238
|
+
* matched the thrown value against the declaration's error schemas.
|
|
239
|
+
*/
|
|
240
|
+
var DeclaredDomainError = class extends HandlerError {
|
|
241
|
+
tag;
|
|
242
|
+
data;
|
|
243
|
+
constructor(input) {
|
|
244
|
+
super(input);
|
|
245
|
+
this.tag = input.tag;
|
|
246
|
+
this.data = input.data;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
/** The public message for a status the application did not explain. */
|
|
250
|
+
function getStatusText(status) {
|
|
251
|
+
switch (status) {
|
|
252
|
+
case 400: return "Bad Request";
|
|
253
|
+
case 401: return "Unauthorized";
|
|
254
|
+
case 403: return "Forbidden";
|
|
255
|
+
case 404: return "Not Found";
|
|
256
|
+
case 405: return "Method Not Allowed";
|
|
257
|
+
case 409: return "Conflict";
|
|
258
|
+
case 413: return "Payload Too Large";
|
|
259
|
+
case 415: return "Unsupported Media Type";
|
|
260
|
+
case 422: return "Unprocessable Content";
|
|
261
|
+
case 429: return "Too Many Requests";
|
|
262
|
+
case 500: return "Internal Server Error";
|
|
263
|
+
case 503: return "Service Unavailable";
|
|
264
|
+
default: return "Error";
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const HANDLER_ENDED_INTERRUPT_ID = -1;
|
|
268
|
+
/** Ends a handler without exposing private control flow in its error channel. */
|
|
269
|
+
const interruptHandler = Effect.failCause(Cause.interrupt(HANDLER_ENDED_INTERRUPT_ID));
|
|
270
|
+
function matchesHandlerEndInterruption(cause) {
|
|
271
|
+
const reason = cause.reasons[0];
|
|
272
|
+
return cause.reasons.length === 1 && reason !== void 0 && Cause.isInterruptReason(reason) && reason.fiberId === HANDLER_ENDED_INTERRUPT_ID;
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/server/runtime/transport-type-entries.ts
|
|
276
|
+
const SERVER_TRANSPORT_TYPE_ENTRIES = [
|
|
277
|
+
optionTransportType,
|
|
278
|
+
redactedTransportType,
|
|
279
|
+
chunkTransportType,
|
|
280
|
+
durationTransportType,
|
|
281
|
+
bigDecimalTransportType,
|
|
282
|
+
dateTimeUtcTransportType,
|
|
283
|
+
dateTimeZonedTransportType,
|
|
284
|
+
hashMapTransportType,
|
|
285
|
+
hashSetTransportType
|
|
286
|
+
];
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/server/runtime/context.ts
|
|
289
|
+
var ResponseInputError = class extends Data.TaggedError("ResponseInputError") {};
|
|
290
|
+
function makeIncomingRequestFacts(source) {
|
|
291
|
+
return {
|
|
292
|
+
request: source.request,
|
|
293
|
+
clientBuildId: source.clientBuildId,
|
|
294
|
+
getCookie: (name) => source.request.cookies[name],
|
|
295
|
+
getPublicUrl: (key) => {
|
|
296
|
+
const normalized = key.startsWith("/") ? key.slice(1) : key;
|
|
297
|
+
const { publicFilemap } = source.metadata;
|
|
298
|
+
const url = Object.hasOwn(publicFilemap, normalized) ? publicFilemap[normalized] : void 0;
|
|
299
|
+
if (url === void 0) throw new Error(`Vorma: ${JSON.stringify(key)} is not a public asset of this build`);
|
|
300
|
+
return url;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function makeRequestFacts(source, facts) {
|
|
305
|
+
return {
|
|
306
|
+
...makeIncomingRequestFacts(source),
|
|
307
|
+
pattern: facts.pattern,
|
|
308
|
+
params: facts.params,
|
|
309
|
+
splatValues: facts.splatValues,
|
|
310
|
+
input: facts.input
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function makeResponseOps(input) {
|
|
314
|
+
function updateEffects(f) {
|
|
315
|
+
return Ref.update(input.effects, f);
|
|
316
|
+
}
|
|
317
|
+
function redirectToLocation(location, status) {
|
|
318
|
+
return updateEffects((current) => {
|
|
319
|
+
return setRedirect({
|
|
320
|
+
effects: current,
|
|
321
|
+
location,
|
|
322
|
+
status,
|
|
323
|
+
client: input.acceptsClientRedirect
|
|
324
|
+
});
|
|
325
|
+
}).pipe(Effect.andThen(interruptHandler));
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
setStatus: (status) => {
|
|
329
|
+
return updateEffects((current) => setStatus({
|
|
330
|
+
effects: current,
|
|
331
|
+
status
|
|
332
|
+
}));
|
|
333
|
+
},
|
|
334
|
+
setHeader: ({ name, value }) => {
|
|
335
|
+
return updateEffects((current) => {
|
|
336
|
+
return setHeader({
|
|
337
|
+
effects: current,
|
|
338
|
+
name,
|
|
339
|
+
value
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
appendHeader: ({ name, value }) => {
|
|
344
|
+
return updateEffects((current) => {
|
|
345
|
+
return appendHeader({
|
|
346
|
+
effects: current,
|
|
347
|
+
name,
|
|
348
|
+
value
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
setCookie: (cookie) => {
|
|
353
|
+
return updateEffects((current) => {
|
|
354
|
+
return setCookie({
|
|
355
|
+
effects: current,
|
|
356
|
+
cookie
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
expireCookie: (options) => {
|
|
361
|
+
const cookie = Cookies.makeCookie(options.name, "", {
|
|
362
|
+
maxAge: 0,
|
|
363
|
+
expires: /* @__PURE__ */ new Date(0),
|
|
364
|
+
...options.path === void 0 ? {} : { path: options.path },
|
|
365
|
+
...options.domain === void 0 ? {} : { domain: options.domain }
|
|
366
|
+
});
|
|
367
|
+
if (Result.isFailure(cookie)) return new ResponseInputError({
|
|
368
|
+
operation: "expireCookie",
|
|
369
|
+
message: "Cookie expiration options are invalid",
|
|
370
|
+
cause: cookie.failure
|
|
371
|
+
});
|
|
372
|
+
return updateEffects((current) => setCookie({
|
|
373
|
+
effects: current,
|
|
374
|
+
cookie: cookie.success
|
|
375
|
+
}));
|
|
376
|
+
},
|
|
377
|
+
redirectToUrl: ({ url, status }) => redirectToLocation(String(url), status),
|
|
378
|
+
redirectToPattern: ({ pattern, params, splatValues, input: routeInput, hash, status }) => {
|
|
379
|
+
return input.resolveRouteTargetFn({
|
|
380
|
+
pattern,
|
|
381
|
+
search: routeInput
|
|
382
|
+
}).pipe(Effect.mapError((cause) => {
|
|
383
|
+
return new ResponseInputError({
|
|
384
|
+
operation: "redirectToPattern",
|
|
385
|
+
message: "Route search input does not satisfy its contract",
|
|
386
|
+
cause
|
|
387
|
+
});
|
|
388
|
+
}), Effect.flatMap((target) => {
|
|
389
|
+
const href = buildHref({
|
|
390
|
+
kind: "ui",
|
|
391
|
+
pattern: target.pattern,
|
|
392
|
+
transportTypeEntries: SERVER_TRANSPORT_TYPE_ENTRIES,
|
|
393
|
+
...params === void 0 ? {} : { params },
|
|
394
|
+
...splatValues === void 0 ? {} : { splatValues },
|
|
395
|
+
...target.search === void 0 ? {} : { search: target.search },
|
|
396
|
+
...hash === void 0 ? {} : { hash }
|
|
397
|
+
});
|
|
398
|
+
return Result.isFailure(href) ? Effect.fail(new ResponseInputError({
|
|
399
|
+
operation: "redirectToPattern",
|
|
400
|
+
message: href.failure.reason,
|
|
401
|
+
cause: href.failure
|
|
402
|
+
})) : redirectToLocation(href.success, status);
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function makeHeadOps(effects) {
|
|
408
|
+
function appendHeadElements$1(elements) {
|
|
409
|
+
return Ref.update(effects, (current) => appendHeadElements({
|
|
410
|
+
effects: current,
|
|
411
|
+
elements
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
setTitle: (title) => {
|
|
416
|
+
return appendHeadElements$1([{
|
|
417
|
+
tag: "title",
|
|
418
|
+
textContent: title
|
|
419
|
+
}]);
|
|
420
|
+
},
|
|
421
|
+
setDescription: (description) => {
|
|
422
|
+
return appendHeadElements$1([{
|
|
423
|
+
tag: "meta",
|
|
424
|
+
attributes: {
|
|
425
|
+
name: "description",
|
|
426
|
+
content: description
|
|
427
|
+
}
|
|
428
|
+
}]);
|
|
429
|
+
},
|
|
430
|
+
addMeta: (attributes) => appendHeadElements$1([{
|
|
431
|
+
tag: "meta",
|
|
432
|
+
attributes
|
|
433
|
+
}]),
|
|
434
|
+
addHead: appendHeadElements$1
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/server/runtime/domain-errors.ts
|
|
439
|
+
function matchDeclaredError(input) {
|
|
440
|
+
if (typeof input.value !== "object" || input.value === null) return;
|
|
441
|
+
for (const entry of input.errors) {
|
|
442
|
+
const tag = getLiteralErrorTag(entry.schema);
|
|
443
|
+
if (tag === void 0) continue;
|
|
444
|
+
const validated = Schema.decodeUnknownResult(Schema.toType(entry.schema))(input.value);
|
|
445
|
+
if (Result.isFailure(validated)) continue;
|
|
446
|
+
const data = encodeTransportedValue({
|
|
447
|
+
value: validated.success,
|
|
448
|
+
typeEntries: SERVER_TRANSPORT_TYPE_ENTRIES
|
|
449
|
+
});
|
|
450
|
+
if (Result.isFailure(data)) continue;
|
|
451
|
+
return {
|
|
452
|
+
tag: tag.tag,
|
|
453
|
+
status: entry.status,
|
|
454
|
+
data: data.success
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/server/runtime/execute.ts
|
|
460
|
+
function executeEndpoint(input) {
|
|
461
|
+
return Effect.gen(function* () {
|
|
462
|
+
const endpoint = yield* Effect.forkScoped(observeInvocation(input.endpoint));
|
|
463
|
+
const outcome = yield* Fiber.join(endpoint);
|
|
464
|
+
if (outcome._tag === "Failed") return toTerminalFailure({
|
|
465
|
+
effects: mergeResponseEffects({
|
|
466
|
+
earlier: input.effects,
|
|
467
|
+
later: outcome.effects
|
|
468
|
+
}),
|
|
469
|
+
cause: outcome.cause
|
|
470
|
+
});
|
|
471
|
+
return mergeOutput(input.effects, outcome);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function executeUi(input) {
|
|
475
|
+
return Effect.gen(function* () {
|
|
476
|
+
const phase = yield* runPhase(input.positions, input.effects);
|
|
477
|
+
if (phase._tag === "Terminal") return phase;
|
|
478
|
+
if (phase._tag === "Completed") return {
|
|
479
|
+
_tag: "Ui",
|
|
480
|
+
effects: phase.effects,
|
|
481
|
+
positions: phase.positions
|
|
482
|
+
};
|
|
483
|
+
const error = getHandlerErrorOf(phase.failed.cause);
|
|
484
|
+
return {
|
|
485
|
+
_tag: "Ui",
|
|
486
|
+
effects: setStatus({
|
|
487
|
+
effects: phase.effects,
|
|
488
|
+
status: error.status
|
|
489
|
+
}),
|
|
490
|
+
positions: [
|
|
491
|
+
...phase.before,
|
|
492
|
+
{
|
|
493
|
+
_tag: "Failed",
|
|
494
|
+
owner: phase.failed.owner,
|
|
495
|
+
error: {
|
|
496
|
+
id: phase.failed.id,
|
|
497
|
+
pattern: phase.failed.pattern,
|
|
498
|
+
index: phase.failed.index,
|
|
499
|
+
...error
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
...phase.after.map((owner) => {
|
|
503
|
+
return {
|
|
504
|
+
_tag: "Suppressed",
|
|
505
|
+
owner
|
|
506
|
+
};
|
|
507
|
+
})
|
|
508
|
+
]
|
|
509
|
+
};
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function executeMiddleware(invocations) {
|
|
513
|
+
return Effect.gen(function* () {
|
|
514
|
+
const phase = yield* runPhase(invocations.map((invocation) => {
|
|
515
|
+
return {
|
|
516
|
+
owner: void 0,
|
|
517
|
+
invocation
|
|
518
|
+
};
|
|
519
|
+
}), empty);
|
|
520
|
+
if (phase._tag === "Terminal") return phase;
|
|
521
|
+
if (phase._tag === "Failed") return toTerminalFailure({
|
|
522
|
+
effects: phase.effects,
|
|
523
|
+
cause: phase.failed.cause
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
_tag: "Continue",
|
|
527
|
+
data: void 0,
|
|
528
|
+
effects: phase.effects
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function runPhase(positions, initialEffects) {
|
|
533
|
+
return Effect.gen(function* () {
|
|
534
|
+
if (positions.length === 0) return {
|
|
535
|
+
_tag: "Completed",
|
|
536
|
+
effects: initialEffects,
|
|
537
|
+
positions: []
|
|
538
|
+
};
|
|
539
|
+
const fibersReady = yield* Deferred.make();
|
|
540
|
+
const forked = [];
|
|
541
|
+
for (const [index, { owner, invocation }] of positions.entries()) {
|
|
542
|
+
if (invocation === null) {
|
|
543
|
+
forked.push({
|
|
544
|
+
owner,
|
|
545
|
+
invocation: null,
|
|
546
|
+
fiber: null
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const body = observeInvocation(invocation).pipe(Effect.flatMap((outcome) => {
|
|
551
|
+
return (marksBoundary(outcome) ? Effect.flatMap(Deferred.await(fibersReady), (all) => {
|
|
552
|
+
return Fiber.interruptAll(all.slice(index + 1).filter((fiber) => fiber !== null));
|
|
553
|
+
}) : Effect.void).pipe(Effect.as(outcome));
|
|
554
|
+
}));
|
|
555
|
+
forked.push({
|
|
556
|
+
owner,
|
|
557
|
+
invocation,
|
|
558
|
+
fiber: yield* Effect.forkScoped(body)
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
yield* Deferred.succeed(fibersReady, forked.map((entry) => entry.fiber));
|
|
562
|
+
let effects = initialEffects;
|
|
563
|
+
const completed = [];
|
|
564
|
+
for (const [index, position] of forked.entries()) {
|
|
565
|
+
if (position.fiber === null) {
|
|
566
|
+
completed.push({
|
|
567
|
+
_tag: "NoHandler",
|
|
568
|
+
owner: position.owner
|
|
569
|
+
});
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const joined = yield* Effect.exit(Fiber.join(position.fiber));
|
|
573
|
+
if (Exit.isFailure(joined)) return yield* Effect.interrupt;
|
|
574
|
+
const outcome = joined.value;
|
|
575
|
+
if (outcome._tag === "Failed") return {
|
|
576
|
+
_tag: "Failed",
|
|
577
|
+
effects: mergeResponseEffects({
|
|
578
|
+
earlier: effects,
|
|
579
|
+
later: outcome.effects
|
|
580
|
+
}),
|
|
581
|
+
before: completed,
|
|
582
|
+
failed: {
|
|
583
|
+
owner: position.owner,
|
|
584
|
+
index,
|
|
585
|
+
id: position.invocation.id,
|
|
586
|
+
pattern: position.invocation.pattern,
|
|
587
|
+
cause: outcome.cause
|
|
588
|
+
},
|
|
589
|
+
after: forked.slice(index + 1).map((entry) => entry.owner)
|
|
590
|
+
};
|
|
591
|
+
const output = mergeOutput(effects, outcome);
|
|
592
|
+
if (output._tag === "Terminal") return output;
|
|
593
|
+
effects = output.effects;
|
|
594
|
+
completed.push({
|
|
595
|
+
_tag: "Completed",
|
|
596
|
+
owner: position.owner,
|
|
597
|
+
data: output.data
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
_tag: "Completed",
|
|
602
|
+
effects,
|
|
603
|
+
positions: completed
|
|
604
|
+
};
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
/** Handler work stays interruptible; logging a produced failure completes before suppression can discard it. */
|
|
608
|
+
function observeInvocation(invocation) {
|
|
609
|
+
return Effect.uninterruptibleMask((restore) => {
|
|
610
|
+
return Effect.flatMap(Effect.exit(restore(invocation.run)), (exit) => {
|
|
611
|
+
const outcome = Exit.isSuccess(exit) ? exit.value : {
|
|
612
|
+
_tag: "Failed",
|
|
613
|
+
cause: exit.cause,
|
|
614
|
+
effects: empty
|
|
615
|
+
};
|
|
616
|
+
if (outcome._tag === "Failed" && Cause.hasInterruptsOnly(outcome.cause)) return Effect.interrupt;
|
|
617
|
+
return logFailure(invocation, outcome).pipe(Effect.as(outcome));
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function logFailure(invocation, outcome) {
|
|
622
|
+
if (outcome._tag !== "Failed") return Effect.void;
|
|
623
|
+
return Effect.logError("Vorma handler failed", {
|
|
624
|
+
handlerId: invocation.id,
|
|
625
|
+
role: invocation.role,
|
|
626
|
+
pattern: invocation.pattern
|
|
627
|
+
}, outcome.cause);
|
|
628
|
+
}
|
|
629
|
+
function marksBoundary(outcome) {
|
|
630
|
+
return outcome._tag !== "Continue";
|
|
631
|
+
}
|
|
632
|
+
function mergeOutput(earlier, output) {
|
|
633
|
+
const effects = mergeResponseEffects({
|
|
634
|
+
earlier,
|
|
635
|
+
later: output.effects
|
|
636
|
+
});
|
|
637
|
+
return output._tag === "Terminal" ? {
|
|
638
|
+
_tag: "Terminal",
|
|
639
|
+
effects: {
|
|
640
|
+
...effects,
|
|
641
|
+
terminal: output.effects.terminal
|
|
642
|
+
}
|
|
643
|
+
} : {
|
|
644
|
+
...output,
|
|
645
|
+
effects
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function toTerminalFailure(input) {
|
|
649
|
+
const error = getHandlerErrorOf(input.cause);
|
|
650
|
+
const domain = getDeclaredDomainOf(input.cause);
|
|
651
|
+
return {
|
|
652
|
+
_tag: "Terminal",
|
|
653
|
+
effects: setTerminalError({
|
|
654
|
+
effects: input.effects,
|
|
655
|
+
status: error.status,
|
|
656
|
+
text: error.clientMessage,
|
|
657
|
+
...domain === void 0 ? {} : { domain }
|
|
658
|
+
})
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/** The domain payload when the failure is a matched declared error. */
|
|
662
|
+
function getDeclaredDomainOf(cause) {
|
|
663
|
+
const reason = cause.reasons[0];
|
|
664
|
+
if (cause.reasons.length === 1 && reason !== void 0 && Cause.isFailReason(reason) && reason.error instanceof DeclaredDomainError) return {
|
|
665
|
+
tag: reason.error.tag,
|
|
666
|
+
data: reason.error.data
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
/** The handler's public error when it failed on purpose; a generic 500 when it did not. */
|
|
670
|
+
function getHandlerErrorOf(cause) {
|
|
671
|
+
const reason = cause.reasons[0];
|
|
672
|
+
if (cause.reasons.length === 1 && reason !== void 0 && Cause.isFailReason(reason) && reason.error instanceof HandlerError) {
|
|
673
|
+
const { status, clientMessage } = reason.error;
|
|
674
|
+
return {
|
|
675
|
+
status,
|
|
676
|
+
clientMessage: clientMessage === void 0 ? getStatusText(status) : clientMessage
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
status: 500,
|
|
681
|
+
clientMessage: getStatusText(500)
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/server/runtime/finalize.ts
|
|
686
|
+
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
687
|
+
const HTML_CONTENT_TYPE = "text/html; charset=utf-8";
|
|
688
|
+
const TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
|
|
689
|
+
const UI_CACHE_CONTROL = "private, max-age=0, must-revalidate, no-cache";
|
|
690
|
+
const FRAMEWORK_OWNED_HEADERS = /* @__PURE__ */ new Set([
|
|
691
|
+
HTTP_PROTOCOL.responseHeaders.clientBuildId,
|
|
692
|
+
HTTP_PROTOCOL.responseHeaders.buildSkew,
|
|
693
|
+
HTTP_PROTOCOL.responseHeaders.clientRedirect,
|
|
694
|
+
HTTP_PROTOCOL.responseHeaders.clientRedirectStatus
|
|
695
|
+
]);
|
|
696
|
+
/** Replays the header operations and adds the framework's own headers. */
|
|
697
|
+
function getBaseHeaders(input) {
|
|
698
|
+
let headers = Headers$1.empty;
|
|
699
|
+
for (const op of input.effects.headerOps) {
|
|
700
|
+
const name = op.name.toLowerCase();
|
|
701
|
+
if (FRAMEWORK_OWNED_HEADERS.has(name)) return Result.fail({
|
|
702
|
+
_tag: "FrameworkOwnedHeader",
|
|
703
|
+
name
|
|
704
|
+
});
|
|
705
|
+
const existing = headers[name];
|
|
706
|
+
headers = Headers$1.set(headers, name, op.kind === "append" && existing !== void 0 ? `${existing}, ${op.value}` : op.value);
|
|
707
|
+
}
|
|
708
|
+
return Result.succeed(Headers$1.set(headers, HTTP_PROTOCOL.responseHeaders.clientBuildId, input.clientBuildId));
|
|
709
|
+
}
|
|
710
|
+
function finalizeWith(input) {
|
|
711
|
+
const baseHeaders = getBaseHeaders({
|
|
712
|
+
effects: input.effects,
|
|
713
|
+
clientBuildId: input.clientBuildId
|
|
714
|
+
});
|
|
715
|
+
if (Result.isFailure(baseHeaders)) return Result.fail(baseHeaders.failure);
|
|
716
|
+
let headers = baseHeaders.success;
|
|
717
|
+
const effects = input.effects;
|
|
718
|
+
let status = effects.status === null ? 200 : effects.status;
|
|
719
|
+
let body = input.body;
|
|
720
|
+
if (effects.terminal !== null) {
|
|
721
|
+
if (effects.terminal._tag === "Redirect") {
|
|
722
|
+
if (effects.terminal.client) {
|
|
723
|
+
status = 200;
|
|
724
|
+
headers = Headers$1.set(headers, HTTP_PROTOCOL.responseHeaders.clientRedirect, effects.terminal.location);
|
|
725
|
+
headers = Headers$1.set(headers, HTTP_PROTOCOL.responseHeaders.clientRedirectStatus, String(effects.terminal.status));
|
|
726
|
+
} else {
|
|
727
|
+
status = effects.terminal.status;
|
|
728
|
+
headers = Headers$1.set(headers, "location", effects.terminal.location);
|
|
729
|
+
}
|
|
730
|
+
body = null;
|
|
731
|
+
} else {
|
|
732
|
+
status = effects.terminal.status;
|
|
733
|
+
body = effects.terminal.text;
|
|
734
|
+
headers = Headers$1.set(headers, "content-type", TEXT_CONTENT_TYPE);
|
|
735
|
+
}
|
|
736
|
+
} else if (input.contentType !== null) headers = Headers$1.set(headers, "content-type", input.contentType);
|
|
737
|
+
const options = {
|
|
738
|
+
status,
|
|
739
|
+
headers,
|
|
740
|
+
cookies: effects.cookies
|
|
741
|
+
};
|
|
742
|
+
return Result.succeed(body === null ? HttpServerResponse$1.empty(options) : HttpServerResponse$1.text(body, options));
|
|
743
|
+
}
|
|
744
|
+
/** Prevents implicit reuse of request-dependent UI data without replacing an authored policy. */
|
|
745
|
+
function withUiCacheControl(response) {
|
|
746
|
+
return Headers$1.has(response.headers, "cache-control") ? response : HttpServerResponse$1.setHeader(response, "cache-control", UI_CACHE_CONTROL);
|
|
747
|
+
}
|
|
748
|
+
/** A navigation's JSON payload response with its application or conservative cache policy. */
|
|
749
|
+
function finalizeJson(input) {
|
|
750
|
+
return finalizeWith({
|
|
751
|
+
effects: input.effects,
|
|
752
|
+
clientBuildId: input.clientBuildId,
|
|
753
|
+
body: input.json,
|
|
754
|
+
contentType: JSON_CONTENT_TYPE
|
|
755
|
+
}).pipe(Result.map(withUiCacheControl));
|
|
756
|
+
}
|
|
757
|
+
/** A first load's HTML response with its application or conservative cache policy. */
|
|
758
|
+
function finalizeHtml(input) {
|
|
759
|
+
return finalizeWith({
|
|
760
|
+
effects: input.effects,
|
|
761
|
+
clientBuildId: input.clientBuildId,
|
|
762
|
+
body: input.html,
|
|
763
|
+
contentType: HTML_CONTENT_TYPE
|
|
764
|
+
}).pipe(Result.map(withUiCacheControl));
|
|
765
|
+
}
|
|
766
|
+
/** An endpoint's JSON response. No cache-control is assumed for endpoints. */
|
|
767
|
+
function finalizeEndpointJson(input) {
|
|
768
|
+
return finalizeWith({
|
|
769
|
+
effects: input.effects,
|
|
770
|
+
clientBuildId: input.clientBuildId,
|
|
771
|
+
body: input.json,
|
|
772
|
+
contentType: JSON_CONTENT_TYPE
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
/** An endpoint redirect, or its structured public handler rejection. */
|
|
776
|
+
function finalizeEndpointTerminal(input) {
|
|
777
|
+
const terminal = input.effects.terminal;
|
|
778
|
+
if (terminal === null || terminal._tag !== "Error") return finalizeTerminal(input);
|
|
779
|
+
const envelope = {
|
|
780
|
+
error: {
|
|
781
|
+
status: terminal.status,
|
|
782
|
+
client_message: terminal.text
|
|
783
|
+
},
|
|
784
|
+
...terminal.domain === void 0 ? {} : { domain: {
|
|
785
|
+
tag: terminal.domain.tag,
|
|
786
|
+
data: terminal.domain.data
|
|
787
|
+
} }
|
|
788
|
+
};
|
|
789
|
+
return finalizeWith({
|
|
790
|
+
effects: {
|
|
791
|
+
...input.effects,
|
|
792
|
+
status: terminal.status,
|
|
793
|
+
terminal: null
|
|
794
|
+
},
|
|
795
|
+
clientBuildId: input.clientBuildId,
|
|
796
|
+
body: JSON.stringify(envelope),
|
|
797
|
+
contentType: JSON_CONTENT_TYPE
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/** A response that ended before any body was produced: a redirect or an error. */
|
|
801
|
+
function finalizeTerminal(input) {
|
|
802
|
+
return finalizeWith({
|
|
803
|
+
effects: input.effects,
|
|
804
|
+
clientBuildId: input.clientBuildId,
|
|
805
|
+
body: null,
|
|
806
|
+
contentType: null
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
/** The answer to a client whose build does not match the server's. */
|
|
810
|
+
function finalizeBuildSkew(clientBuildId) {
|
|
811
|
+
return HttpServerResponse$1.text(HTTP_PROTOCOL.buildSkewBody, {
|
|
812
|
+
status: 200,
|
|
813
|
+
headers: [
|
|
814
|
+
["content-type", JSON_CONTENT_TYPE],
|
|
815
|
+
[HTTP_PROTOCOL.responseHeaders.clientBuildId, clientBuildId],
|
|
816
|
+
[HTTP_PROTOCOL.responseHeaders.buildSkew, "1"],
|
|
817
|
+
["cache-control", "no-store"]
|
|
818
|
+
]
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
/** A plain-text response for a Vorma protocol or execution failure. */
|
|
822
|
+
function finalizeStatus(input) {
|
|
823
|
+
const headers = [["content-type", TEXT_CONTENT_TYPE], [HTTP_PROTOCOL.responseHeaders.clientBuildId, input.clientBuildId]];
|
|
824
|
+
return HttpServerResponse$1.text(input.text, {
|
|
825
|
+
status: input.status,
|
|
826
|
+
headers
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/server/document.ts
|
|
831
|
+
const NAME_PATTERN = /^[A-Za-z][A-Za-z0-9:._-]*$/;
|
|
832
|
+
const VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
833
|
+
"area",
|
|
834
|
+
"base",
|
|
835
|
+
"br",
|
|
836
|
+
"col",
|
|
837
|
+
"embed",
|
|
838
|
+
"hr",
|
|
839
|
+
"img",
|
|
840
|
+
"input",
|
|
841
|
+
"link",
|
|
842
|
+
"meta",
|
|
843
|
+
"source",
|
|
844
|
+
"track",
|
|
845
|
+
"wbr"
|
|
846
|
+
]);
|
|
847
|
+
/** Escapes ordinary text once, when producing HTML. */
|
|
848
|
+
function escapeText(value) {
|
|
849
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
850
|
+
}
|
|
851
|
+
/** Escapes a value for a double-quoted HTML attribute. */
|
|
852
|
+
function escapeAttributeValue(value) {
|
|
853
|
+
return escapeText(value).replaceAll("\"", """);
|
|
854
|
+
}
|
|
855
|
+
/** Escapes serialized JSON for insertion through a script element's dangerousInnerHtml. */
|
|
856
|
+
function escapeJsonForScript(serializedJson) {
|
|
857
|
+
return serializedJson.replaceAll("&", "\\u0026").replaceAll("<", "\\u003c").replaceAll(">", "\\u003e").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
858
|
+
}
|
|
859
|
+
function renderElementContent(element) {
|
|
860
|
+
const tag = element.tag;
|
|
861
|
+
if (!NAME_PATTERN.test(element.tag)) return Result.fail({
|
|
862
|
+
_tag: "InvalidTagName",
|
|
863
|
+
tag: element.tag
|
|
864
|
+
});
|
|
865
|
+
for (const name of Object.keys(element.attributes ?? {})) if (!NAME_PATTERN.test(name)) return Result.fail({
|
|
866
|
+
_tag: "InvalidAttributeName",
|
|
867
|
+
attribute: name,
|
|
868
|
+
tag: element.tag
|
|
869
|
+
});
|
|
870
|
+
if (element.textContent !== void 0 && element.dangerousInnerHtml !== void 0) return Result.fail({
|
|
871
|
+
_tag: "MixedTextAndRawContent",
|
|
872
|
+
tag
|
|
873
|
+
});
|
|
874
|
+
const content = element.dangerousInnerHtml ?? escapeText(element.textContent ?? "");
|
|
875
|
+
if (VOID_ELEMENTS.has(element.tag.toLowerCase()) && content !== "") return Result.fail({
|
|
876
|
+
_tag: "VoidElementWithContent",
|
|
877
|
+
tag: element.tag
|
|
878
|
+
});
|
|
879
|
+
return Result.succeed(content);
|
|
880
|
+
}
|
|
881
|
+
/** Serializes one element without interpreting raw code or markup. */
|
|
882
|
+
function renderElement(element) {
|
|
883
|
+
return Result.map(renderElementContent(element), (content) => {
|
|
884
|
+
const attributes = Object.entries(element.attributes ?? {}).map(([name, value]) => {
|
|
885
|
+
return value === true ? ` ${name}` : ` ${name}="${escapeAttributeValue(value)}"`;
|
|
886
|
+
}).join("");
|
|
887
|
+
const start = `<${element.tag}${attributes}>`;
|
|
888
|
+
return VOID_ELEMENTS.has(element.tag.toLowerCase()) ? start : `${start}${content}</${element.tag}>`;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function renderElements(elements) {
|
|
892
|
+
const rendered = [];
|
|
893
|
+
for (const element of elements) {
|
|
894
|
+
const result = renderElement(element);
|
|
895
|
+
if (Result.isFailure(result)) return result;
|
|
896
|
+
rendered.push(result.success);
|
|
897
|
+
}
|
|
898
|
+
return Result.succeed(rendered.join("\n"));
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
901
|
+
//#region src/server/runtime/assets.ts
|
|
902
|
+
/** Combines static module metadata without running application code. */
|
|
903
|
+
function prepareAssets(input) {
|
|
904
|
+
function collectUrls(key) {
|
|
905
|
+
const entry = new Set(input.clientEntry[key]);
|
|
906
|
+
const combined = new Set(entry);
|
|
907
|
+
for (const view of input.views) if (view !== null) for (const url of view[key]) combined.add(url);
|
|
908
|
+
const document = [...combined];
|
|
909
|
+
return {
|
|
910
|
+
document,
|
|
911
|
+
navigation: document.slice(entry.size)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const deps = collectUrls("depUrls");
|
|
915
|
+
const css = collectUrls("cssBundleUrls");
|
|
916
|
+
const assets = collectUrls("staticAssetUrls");
|
|
917
|
+
return {
|
|
918
|
+
navigationDepUrls: deps.navigation,
|
|
919
|
+
documentDepUrls: deps.document,
|
|
920
|
+
navigationCssBundleUrls: css.navigation,
|
|
921
|
+
documentCssBundleUrls: css.document,
|
|
922
|
+
navigationStaticAssetUrls: assets.navigation,
|
|
923
|
+
documentStaticAssetUrls: assets.document
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/server/runtime/head.ts
|
|
928
|
+
/** The built-in identities. An element matches a rule when its tag matches and every listed attribute matches; `true` means present with any value. */
|
|
929
|
+
const defaultDedupeRules = [
|
|
930
|
+
{
|
|
931
|
+
tag: "title",
|
|
932
|
+
attributes: {}
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
tag: "meta",
|
|
936
|
+
attributes: { name: "description" }
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
tag: "meta",
|
|
940
|
+
attributes: { name: "viewport" }
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
tag: "meta",
|
|
944
|
+
attributes: { name: "robots" }
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
tag: "meta",
|
|
948
|
+
attributes: { charset: true }
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
tag: "link",
|
|
952
|
+
attributes: { rel: "icon" }
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
tag: "link",
|
|
956
|
+
attributes: { rel: "canonical" }
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
tag: "meta",
|
|
960
|
+
attributes: { property: "og:title" }
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
tag: "meta",
|
|
964
|
+
attributes: { property: "og:description" }
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
tag: "meta",
|
|
968
|
+
attributes: { property: "og:url" }
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
tag: "meta",
|
|
972
|
+
attributes: { property: "og:type" }
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
tag: "meta",
|
|
976
|
+
attributes: { property: "og:locale" }
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
tag: "meta",
|
|
980
|
+
attributes: { property: "og:site_name" }
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
tag: "meta",
|
|
984
|
+
attributes: { property: "og:determiner" }
|
|
985
|
+
}
|
|
986
|
+
];
|
|
987
|
+
/** Normalizes the application's static rules once, preserving priority within each tag. */
|
|
988
|
+
function compileHeadRules(rules) {
|
|
989
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
990
|
+
for (const [index, rule] of rules.entries()) {
|
|
991
|
+
const tag = rule.tag.toLowerCase();
|
|
992
|
+
const compiled = {
|
|
993
|
+
index,
|
|
994
|
+
attributes: Object.entries(rule.attributes).map(([name, value]) => {
|
|
995
|
+
return [name.toLowerCase(), value];
|
|
996
|
+
})
|
|
997
|
+
};
|
|
998
|
+
const existing = byTag.get(tag);
|
|
999
|
+
if (existing === void 0) byTag.set(tag, [compiled]);
|
|
1000
|
+
else existing.push(compiled);
|
|
1001
|
+
}
|
|
1002
|
+
return byTag;
|
|
1003
|
+
}
|
|
1004
|
+
/** Deduplicates managed defaults and ordered contributions before classification. */
|
|
1005
|
+
function prepareHead(input) {
|
|
1006
|
+
const deduped = dedupeHeadElements(input);
|
|
1007
|
+
let title = null;
|
|
1008
|
+
const meta = [];
|
|
1009
|
+
const rest = [];
|
|
1010
|
+
for (const element of deduped) if (element.tag === "title") title = element;
|
|
1011
|
+
else if (element.tag === "meta") meta.push(element);
|
|
1012
|
+
else rest.push(element);
|
|
1013
|
+
return {
|
|
1014
|
+
title,
|
|
1015
|
+
meta,
|
|
1016
|
+
rest
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function matchesRule(element, rule) {
|
|
1020
|
+
for (const [name, expected] of rule.attributes) {
|
|
1021
|
+
const actual = element.attributes?.[name];
|
|
1022
|
+
if (actual === void 0 || expected !== true && actual !== expected) return false;
|
|
1023
|
+
}
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
/** Later elements replace an identity's first position; unmatched identities use structural equality. */
|
|
1027
|
+
function dedupeHeadElements(input) {
|
|
1028
|
+
const elements = [];
|
|
1029
|
+
const indexByRule = /* @__PURE__ */ new Map();
|
|
1030
|
+
const indexByShape = /* @__PURE__ */ new Map();
|
|
1031
|
+
function appendElement(indexes, key, element) {
|
|
1032
|
+
const index = indexes.get(key);
|
|
1033
|
+
if (index === void 0) {
|
|
1034
|
+
indexes.set(key, elements.length);
|
|
1035
|
+
elements.push(element);
|
|
1036
|
+
} else elements[index] = element;
|
|
1037
|
+
}
|
|
1038
|
+
for (const authored of input.elements) {
|
|
1039
|
+
const element = {
|
|
1040
|
+
...authored,
|
|
1041
|
+
tag: authored.tag.toLowerCase(),
|
|
1042
|
+
attributes: Object.fromEntries(Object.entries(authored.attributes ?? {}).map(([name, value]) => [name.toLowerCase(), value]))
|
|
1043
|
+
};
|
|
1044
|
+
const matchedRule = input.rules.get(element.tag)?.find((rule) => matchesRule(element, rule));
|
|
1045
|
+
if (matchedRule !== void 0) appendElement(indexByRule, matchedRule.index, element);
|
|
1046
|
+
else appendElement(indexByShape, toShapeKey(element), element);
|
|
1047
|
+
}
|
|
1048
|
+
return elements;
|
|
1049
|
+
}
|
|
1050
|
+
function toShapeKey(node) {
|
|
1051
|
+
const attributes = Object.entries(node.attributes ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1052
|
+
return JSON.stringify([
|
|
1053
|
+
"Element",
|
|
1054
|
+
node.tag,
|
|
1055
|
+
attributes,
|
|
1056
|
+
node.textContent ?? null,
|
|
1057
|
+
node.dangerousInnerHtml ?? null
|
|
1058
|
+
]);
|
|
1059
|
+
}
|
|
1060
|
+
//#endregion
|
|
1061
|
+
//#region src/server/runtime/payload.ts
|
|
1062
|
+
function projectPayload(input) {
|
|
1063
|
+
const { report } = input;
|
|
1064
|
+
let serverError = null;
|
|
1065
|
+
let allEligible = true;
|
|
1066
|
+
const routes = [];
|
|
1067
|
+
for (const executed of report.positions) {
|
|
1068
|
+
if (executed._tag === "Suppressed") {
|
|
1069
|
+
allEligible = false;
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const { position, searchInput } = executed.owner;
|
|
1073
|
+
const { node } = position;
|
|
1074
|
+
if (executed._tag === "Failed") {
|
|
1075
|
+
allEligible = false;
|
|
1076
|
+
serverError = executed.error;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const module = node.clientViewModule;
|
|
1080
|
+
const loaderData = executed._tag === "Completed" ? executed.data : executed._tag === "NoHandler" ? null : void 0;
|
|
1081
|
+
routes.push({
|
|
1082
|
+
pattern: node.pattern,
|
|
1083
|
+
...Object.keys(position.params).length === 0 ? {} : { params: position.params },
|
|
1084
|
+
...position.splatValues.length === 0 ? {} : { splat_values: position.splatValues },
|
|
1085
|
+
compiled_pattern: {
|
|
1086
|
+
...node.registered.pattern === node.pattern ? {} : { pattern: node.registered.pattern },
|
|
1087
|
+
segments: node.registered.segments
|
|
1088
|
+
},
|
|
1089
|
+
...node.uiLoader === null || node.uiLoader.search === null ? {} : { search_contract: node.uiLoader.search.document },
|
|
1090
|
+
...searchInput === null ? {} : { search_input: searchInput },
|
|
1091
|
+
...module === void 0 ? {} : module === null ? { import_url: null } : {
|
|
1092
|
+
import_url: module.importUrl,
|
|
1093
|
+
export_names: module.exportNames
|
|
1094
|
+
},
|
|
1095
|
+
...loaderData === void 0 ? {} : { loader_data: loaderData }
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
const preparedHead = prepareHeadOf({
|
|
1099
|
+
report,
|
|
1100
|
+
headDefaults: input.headDefaults,
|
|
1101
|
+
dedupeRules: input.dedupeRules
|
|
1102
|
+
});
|
|
1103
|
+
return {
|
|
1104
|
+
route: {
|
|
1105
|
+
routes,
|
|
1106
|
+
...serverError === null ? {} : { server_error: {
|
|
1107
|
+
index: serverError.index,
|
|
1108
|
+
status: serverError.status,
|
|
1109
|
+
client_message: serverError.clientMessage
|
|
1110
|
+
} }
|
|
1111
|
+
},
|
|
1112
|
+
preparedHead,
|
|
1113
|
+
...allEligible ? input.assets : prepareAssets({
|
|
1114
|
+
clientEntry: input.clientEntry,
|
|
1115
|
+
views: report.positions.flatMap((executed) => {
|
|
1116
|
+
return executed._tag === "Failed" || executed._tag === "Suppressed" ? [] : [executed.owner.position.node.clientViewModule];
|
|
1117
|
+
})
|
|
1118
|
+
})
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function toNavigationPayload(projection) {
|
|
1122
|
+
return Result.map(toWireHead(projection.preparedHead), (head) => {
|
|
1123
|
+
return {
|
|
1124
|
+
...projection.route,
|
|
1125
|
+
head,
|
|
1126
|
+
...projection.navigationDepUrls.length === 0 ? {} : { deps: projection.navigationDepUrls },
|
|
1127
|
+
...projection.navigationCssBundleUrls.length === 0 ? {} : { css_bundles: projection.navigationCssBundleUrls },
|
|
1128
|
+
...projection.navigationStaticAssetUrls.length === 0 ? {} : { static_assets: projection.navigationStaticAssetUrls }
|
|
1129
|
+
};
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function toSsrPayload(input) {
|
|
1133
|
+
return {
|
|
1134
|
+
...input.projection.route,
|
|
1135
|
+
client_build_id: input.clientBuildId,
|
|
1136
|
+
...input.isDev ? { is_dev: true } : {},
|
|
1137
|
+
...input.deploymentId === void 0 ? {} : { deployment_id: input.deploymentId }
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
/** The deduped head for a response, with later contributions replacing the first position of each identity. */
|
|
1141
|
+
function prepareHeadOf(input) {
|
|
1142
|
+
const { effects } = input.report;
|
|
1143
|
+
return prepareHead({
|
|
1144
|
+
elements: [...input.headDefaults, ...effects.headElements],
|
|
1145
|
+
rules: input.dedupeRules
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* A head element on the wire: raw attribute values, which the client sets
|
|
1150
|
+
* through the DOM, and escaped text or trusted HTML, which the client sets
|
|
1151
|
+
* as inner HTML. The initial document uses the same serialization.
|
|
1152
|
+
*/
|
|
1153
|
+
function toWireHeadElement(element) {
|
|
1154
|
+
const attrs = element.attributes ?? {};
|
|
1155
|
+
const inner = renderElementContent(element);
|
|
1156
|
+
if (Result.isFailure(inner)) return Result.fail({
|
|
1157
|
+
_tag: "HeadRender",
|
|
1158
|
+
tag: element.tag,
|
|
1159
|
+
reason: inner.failure
|
|
1160
|
+
});
|
|
1161
|
+
const innerHtml = inner.success;
|
|
1162
|
+
return Result.succeed({
|
|
1163
|
+
tag: element.tag,
|
|
1164
|
+
...Object.keys(attrs).length === 0 ? {} : { attrs },
|
|
1165
|
+
...innerHtml === "" ? {} : { html: innerHtml }
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
function toWireHead(prepared) {
|
|
1169
|
+
const sections = Result.all({
|
|
1170
|
+
meta: toWireHeadElements(prepared.meta),
|
|
1171
|
+
rest: toWireHeadElements(prepared.rest)
|
|
1172
|
+
});
|
|
1173
|
+
if (Result.isFailure(sections)) return Result.fail(sections.failure);
|
|
1174
|
+
const title = prepared.title === null ? null : toWireHeadElement(prepared.title);
|
|
1175
|
+
if (title !== null && Result.isFailure(title)) return Result.fail(title.failure);
|
|
1176
|
+
return Result.succeed({
|
|
1177
|
+
...title === null ? {} : { title: title.success },
|
|
1178
|
+
...sections.success.meta.length === 0 ? {} : { meta: sections.success.meta },
|
|
1179
|
+
...sections.success.rest.length === 0 ? {} : { rest: sections.success.rest }
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
function toWireHeadElements(elements) {
|
|
1183
|
+
const out = [];
|
|
1184
|
+
for (const element of elements) {
|
|
1185
|
+
const wire = toWireHeadElement(element);
|
|
1186
|
+
if (Result.isFailure(wire)) return Result.fail(wire.failure);
|
|
1187
|
+
out.push(wire.success);
|
|
1188
|
+
}
|
|
1189
|
+
return Result.succeed(out);
|
|
1190
|
+
}
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/server/runtime/plan.ts
|
|
1193
|
+
/** Compiles application declarations and server-side view metadata into request matchers. */
|
|
1194
|
+
function compileRequestPlan(input) {
|
|
1195
|
+
const errors = [];
|
|
1196
|
+
const uiLoaders = [];
|
|
1197
|
+
const endpointNodes = [];
|
|
1198
|
+
const middleware = [];
|
|
1199
|
+
for (const declaration of new Set(input.handlers)) if (declaration._tag === "UiLoader") {
|
|
1200
|
+
const node = compileUiLoader(declaration, errors);
|
|
1201
|
+
if (node !== null) uiLoaders.push(node);
|
|
1202
|
+
} else {
|
|
1203
|
+
const node = compileEndpoint(declaration, errors);
|
|
1204
|
+
if (node !== null) endpointNodes.push(node);
|
|
1205
|
+
}
|
|
1206
|
+
for (const [index, declaration] of input.middleware.entries()) {
|
|
1207
|
+
const node = compileMiddleware(declaration, index, errors);
|
|
1208
|
+
if (node !== null) middleware.push(node);
|
|
1209
|
+
}
|
|
1210
|
+
checkDuplicates(uiLoaders, endpointNodes, errors);
|
|
1211
|
+
const viewErrors = [];
|
|
1212
|
+
const uiNodes = linkSearchOwners(buildUiNodes({
|
|
1213
|
+
uiLoaders,
|
|
1214
|
+
clientViews: input.clientViews,
|
|
1215
|
+
errors: viewErrors
|
|
1216
|
+
}));
|
|
1217
|
+
checkEndpointPatternRegistrations(endpointNodes, errors);
|
|
1218
|
+
if (errors.length > 0) return Result.fail(errors);
|
|
1219
|
+
const viewError = viewErrors[0];
|
|
1220
|
+
if (viewError !== void 0) return Result.fail(viewError);
|
|
1221
|
+
const endpoints = /* @__PURE__ */ new Map();
|
|
1222
|
+
for (const [method, nodes] of groupEndpointsByMethod(endpointNodes)) endpoints.set(method, createFlatValueMatcher(nodes.map((node) => {
|
|
1223
|
+
return {
|
|
1224
|
+
pattern: node.registered,
|
|
1225
|
+
value: node
|
|
1226
|
+
};
|
|
1227
|
+
})));
|
|
1228
|
+
return Result.succeed({
|
|
1229
|
+
clientEntry: input.clientEntry,
|
|
1230
|
+
preparedRequests: /* @__PURE__ */ new Map(),
|
|
1231
|
+
headDeduplicationRules: compileHeadRules([...defaultDedupeRules, ...input.document.headDeduplicationRules]),
|
|
1232
|
+
document: input.document,
|
|
1233
|
+
uiMatcher: createNestedValueMatcher(uiNodes.map((node) => {
|
|
1234
|
+
return {
|
|
1235
|
+
pattern: node.registered,
|
|
1236
|
+
value: node
|
|
1237
|
+
};
|
|
1238
|
+
})),
|
|
1239
|
+
uiByAuthoredPattern: new Map(uiNodes.map((node) => [node.pattern, node])),
|
|
1240
|
+
endpoints,
|
|
1241
|
+
middleware: middleware.map((node) => {
|
|
1242
|
+
return {
|
|
1243
|
+
node,
|
|
1244
|
+
matcher: createFlatMatcher(node.registered)
|
|
1245
|
+
};
|
|
1246
|
+
})
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
function buildUiNodes(input) {
|
|
1250
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1251
|
+
for (const uiLoader of input.uiLoaders) nodes.set(uiLoader.registered.pattern, {
|
|
1252
|
+
pattern: uiLoader.declaration.pattern,
|
|
1253
|
+
registered: uiLoader.registered,
|
|
1254
|
+
uiLoader,
|
|
1255
|
+
clientViewModule: null
|
|
1256
|
+
});
|
|
1257
|
+
for (const [pattern, clientViewModule] of Object.entries(input.clientViews)) {
|
|
1258
|
+
const registered = compileUiPattern(pattern);
|
|
1259
|
+
if (Result.isFailure(registered)) {
|
|
1260
|
+
input.errors.push({
|
|
1261
|
+
_tag: "PlanError",
|
|
1262
|
+
cause: registered.failure
|
|
1263
|
+
});
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const existing = nodes.get(registered.success.pattern);
|
|
1267
|
+
nodes.set(registered.success.pattern, {
|
|
1268
|
+
pattern,
|
|
1269
|
+
registered: registered.success,
|
|
1270
|
+
uiLoader: existing === void 0 ? null : existing.uiLoader,
|
|
1271
|
+
clientViewModule
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
return [...nodes.values()];
|
|
1275
|
+
}
|
|
1276
|
+
/** Each UI position's search owners: the loaders above and at it whose search fields compose into its input. */
|
|
1277
|
+
function linkSearchOwners(nodes) {
|
|
1278
|
+
return createNestedRouteChains({ nodes }).map(({ target, chain }) => {
|
|
1279
|
+
const owners = chain.filter((node) => node.uiLoader !== null && node.uiLoader.search !== null);
|
|
1280
|
+
return {
|
|
1281
|
+
...target,
|
|
1282
|
+
searchOwners: owners.map((owner) => owner.uiLoader)
|
|
1283
|
+
};
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
/** Reuses only operation/pathname-derived data within this compiled application. */
|
|
1287
|
+
function getRequestPreparation(input) {
|
|
1288
|
+
const { plan, operation, path } = input;
|
|
1289
|
+
const key = operation._tag === "Ui" ? `Ui ${path}` : `Endpoint ${operation.method} ${path}`;
|
|
1290
|
+
const cached = plan.preparedRequests.get(key);
|
|
1291
|
+
if (cached !== void 0) {
|
|
1292
|
+
plan.preparedRequests.delete(key);
|
|
1293
|
+
plan.preparedRequests.set(key, cached);
|
|
1294
|
+
return cached;
|
|
1295
|
+
}
|
|
1296
|
+
const prepared = Result.map(classifyRequest(input), (target) => {
|
|
1297
|
+
return {
|
|
1298
|
+
target,
|
|
1299
|
+
middleware: target._tag === "NotFound" ? [] : getMiddlewareFor({
|
|
1300
|
+
plan,
|
|
1301
|
+
method: operation._tag === "Ui" ? "GET" : operation.method,
|
|
1302
|
+
path
|
|
1303
|
+
})
|
|
1304
|
+
};
|
|
1305
|
+
});
|
|
1306
|
+
plan.preparedRequests.set(key, prepared);
|
|
1307
|
+
if (plan.preparedRequests.size > 4096) {
|
|
1308
|
+
const oldest = plan.preparedRequests.keys().next();
|
|
1309
|
+
if (!oldest.done) plan.preparedRequests.delete(oldest.value);
|
|
1310
|
+
}
|
|
1311
|
+
return prepared;
|
|
1312
|
+
}
|
|
1313
|
+
function classifyRequest(input) {
|
|
1314
|
+
const { plan, path, operation } = input;
|
|
1315
|
+
return (operation._tag === "Ui" ? matchUi({
|
|
1316
|
+
plan,
|
|
1317
|
+
path
|
|
1318
|
+
}) : matchEndpoint({
|
|
1319
|
+
plan,
|
|
1320
|
+
path,
|
|
1321
|
+
method: operation.method
|
|
1322
|
+
})).pipe(Result.map((target) => target ?? { _tag: "NotFound" }));
|
|
1323
|
+
}
|
|
1324
|
+
/** Selects task middleware for the mounted operation, in declaration order. */
|
|
1325
|
+
function getMiddlewareFor(input) {
|
|
1326
|
+
const selected = [];
|
|
1327
|
+
for (const entry of input.plan.middleware) {
|
|
1328
|
+
if (!entry.node.declaration.methods.some((allowed) => allowed === input.method)) continue;
|
|
1329
|
+
if (Option.isSome(findBestMatch(entry.matcher, input.path))) selected.push(entry.node);
|
|
1330
|
+
}
|
|
1331
|
+
return selected;
|
|
1332
|
+
}
|
|
1333
|
+
function matchEndpoint(input) {
|
|
1334
|
+
const table = input.plan.endpoints.get(input.method);
|
|
1335
|
+
if (table === void 0) return Result.succeed(null);
|
|
1336
|
+
const best = findBestValueMatch(table, input.path);
|
|
1337
|
+
if (Option.isNone(best)) return Result.succeed(null);
|
|
1338
|
+
const node = best.value.value;
|
|
1339
|
+
const captures = decodeMatchCaptures(best.value);
|
|
1340
|
+
if (Result.isFailure(captures)) return Result.fail(captures.failure);
|
|
1341
|
+
return Result.succeed({
|
|
1342
|
+
_tag: "Endpoint",
|
|
1343
|
+
node,
|
|
1344
|
+
...captures.success
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
function matchUi(input) {
|
|
1348
|
+
const matches = findNestedValueMatches(input.plan.uiMatcher, input.path);
|
|
1349
|
+
if (Option.isNone(matches)) return Result.succeed(null);
|
|
1350
|
+
const positions = [];
|
|
1351
|
+
for (const match of matches.value) {
|
|
1352
|
+
const node = match.value;
|
|
1353
|
+
const captures = decodeMatchCaptures(match);
|
|
1354
|
+
if (Result.isFailure(captures)) return Result.fail(captures.failure);
|
|
1355
|
+
positions.push({
|
|
1356
|
+
node,
|
|
1357
|
+
...captures.success
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
return Result.succeed({
|
|
1361
|
+
_tag: "Ui",
|
|
1362
|
+
positions,
|
|
1363
|
+
assets: prepareAssets({
|
|
1364
|
+
clientEntry: input.plan.clientEntry,
|
|
1365
|
+
views: positions.map((position) => position.node.clientViewModule)
|
|
1366
|
+
})
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
//#endregion
|
|
1370
|
+
//#region src/server/runtime/render-document.ts
|
|
1371
|
+
/** Serializes the fixed document structure after head deduplication and classification. */
|
|
1372
|
+
function renderDocument(input) {
|
|
1373
|
+
const assets = [];
|
|
1374
|
+
if (input.criticalCss !== "") assets.push({
|
|
1375
|
+
tag: "style",
|
|
1376
|
+
attributes: { id: DOCUMENT_PROTOCOL.elementIds.criticalCss },
|
|
1377
|
+
dangerousInnerHtml: input.criticalCss
|
|
1378
|
+
});
|
|
1379
|
+
for (const href of input.cssBundleUrls) assets.push({
|
|
1380
|
+
tag: "link",
|
|
1381
|
+
attributes: {
|
|
1382
|
+
rel: "stylesheet",
|
|
1383
|
+
href,
|
|
1384
|
+
[DOCUMENT_PROTOCOL.attributes.cssBundle]: href
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
for (const href of input.modulePreloadUrls) assets.push({
|
|
1388
|
+
tag: "link",
|
|
1389
|
+
attributes: {
|
|
1390
|
+
rel: "modulepreload",
|
|
1391
|
+
href
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
for (const href of input.staticAssetUrls) {
|
|
1395
|
+
const destination = getStaticAssetDestination(href);
|
|
1396
|
+
assets.push({
|
|
1397
|
+
tag: "link",
|
|
1398
|
+
attributes: {
|
|
1399
|
+
rel: "preload",
|
|
1400
|
+
href,
|
|
1401
|
+
as: destination,
|
|
1402
|
+
...destination === "image" ? {} : { crossorigin: "anonymous" },
|
|
1403
|
+
[DOCUMENT_PROTOCOL.attributes.staticAsset]: href
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
const sections = Result.all({
|
|
1408
|
+
title: renderElements(input.prepared.title === null ? [] : [input.prepared.title]),
|
|
1409
|
+
meta: renderElements(input.prepared.meta),
|
|
1410
|
+
rest: renderElements(input.prepared.rest),
|
|
1411
|
+
assets: renderElements(assets),
|
|
1412
|
+
body: renderElements([
|
|
1413
|
+
{
|
|
1414
|
+
tag: "div",
|
|
1415
|
+
attributes: { id: DOCUMENT_PROTOCOL.elementIds.root }
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
tag: "script",
|
|
1419
|
+
attributes: {
|
|
1420
|
+
id: DOCUMENT_PROTOCOL.elementIds.data,
|
|
1421
|
+
type: "application/json"
|
|
1422
|
+
},
|
|
1423
|
+
dangerousInnerHtml: escapeJsonForScript(input.payloadJson)
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
tag: "script",
|
|
1427
|
+
attributes: {
|
|
1428
|
+
type: "module",
|
|
1429
|
+
src: input.entryImportUrl
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
])
|
|
1433
|
+
});
|
|
1434
|
+
if (Result.isFailure(sections)) return Result.fail(sections.failure);
|
|
1435
|
+
const headHtml = [
|
|
1436
|
+
"title",
|
|
1437
|
+
"meta",
|
|
1438
|
+
"rest",
|
|
1439
|
+
"assets"
|
|
1440
|
+
].map((section) => `<!--${DOCUMENT_PROTOCOL.headComments[section]}-->\n${sections.success[section]}`).join("\n");
|
|
1441
|
+
const document = Result.all({
|
|
1442
|
+
head: renderElement({
|
|
1443
|
+
tag: "head",
|
|
1444
|
+
attributes: input.attributes.head,
|
|
1445
|
+
dangerousInnerHtml: headHtml + (input.developmentHeadHtml ?? "")
|
|
1446
|
+
}),
|
|
1447
|
+
body: renderElement({
|
|
1448
|
+
tag: "body",
|
|
1449
|
+
attributes: input.attributes.body,
|
|
1450
|
+
dangerousInnerHtml: sections.success.body
|
|
1451
|
+
})
|
|
1452
|
+
});
|
|
1453
|
+
if (Result.isFailure(document)) return Result.fail(document.failure);
|
|
1454
|
+
return Result.map(renderElement({
|
|
1455
|
+
tag: "html",
|
|
1456
|
+
attributes: input.attributes.html,
|
|
1457
|
+
dangerousInnerHtml: document.success.head + document.success.body
|
|
1458
|
+
}), (html) => `<!doctype html>\n${html}`);
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/server/runtime/pipeline.ts
|
|
1462
|
+
function handleRequest(input) {
|
|
1463
|
+
return Effect.gen(function* () {
|
|
1464
|
+
const { plan, metadata, request } = input;
|
|
1465
|
+
const clientBuildId = metadata.clientBuildId;
|
|
1466
|
+
const badRequest = finalizeStatus({
|
|
1467
|
+
status: 400,
|
|
1468
|
+
text: "Bad Request",
|
|
1469
|
+
clientBuildId
|
|
1470
|
+
});
|
|
1471
|
+
const queryOffset = request.url.indexOf("?");
|
|
1472
|
+
const rawPath = queryOffset === -1 ? request.url : request.url.slice(0, queryOffset);
|
|
1473
|
+
const matcherPath = canonicalizeRequestPathForMatcher(rawPath);
|
|
1474
|
+
if (Result.isFailure(matcherPath)) return badRequest;
|
|
1475
|
+
const url = new URL(`${matcherPath.success}${queryOffset === -1 ? "" : request.url.slice(queryOffset)}`, "http://vorma.invalid");
|
|
1476
|
+
const prepared = getRequestPreparation({
|
|
1477
|
+
plan,
|
|
1478
|
+
operation: input.operation,
|
|
1479
|
+
path: matcherPath.success
|
|
1480
|
+
});
|
|
1481
|
+
if (Result.isFailure(prepared)) return badRequest;
|
|
1482
|
+
const { target } = prepared.success;
|
|
1483
|
+
if (target._tag === "NotFound") return yield* Effect.fail(new HttpServerError.HttpServerError({ reason: new HttpServerError.RouteNotFound({ request }) }));
|
|
1484
|
+
const submittedClientBuildIds = target._tag === "Ui" ? url.searchParams.getAll(HTTP_PROTOCOL.queryKeys.viewPayload) : target._tag === "Endpoint" ? [request.headers[HTTP_PROTOCOL.requestHeaders.clientBuildId]].filter((value) => value !== void 0) : [];
|
|
1485
|
+
if (submittedClientBuildIds.length > 1) return badRequest;
|
|
1486
|
+
const submittedClientBuildId = submittedClientBuildIds[0];
|
|
1487
|
+
const isJsonRequest = submittedClientBuildId !== void 0 && submittedClientBuildId !== "";
|
|
1488
|
+
if (target._tag === "Ui" && isJsonRequest && submittedClientBuildId !== clientBuildId && submittedClientBuildId !== "_") return finalizeBuildSkew(clientBuildId);
|
|
1489
|
+
if (target._tag === "Ui") {
|
|
1490
|
+
url.searchParams.delete(HTTP_PROTOCOL.queryKeys.viewPayload);
|
|
1491
|
+
url.searchParams.delete(HTTP_PROTOCOL.queryKeys.deployment);
|
|
1492
|
+
}
|
|
1493
|
+
const source = {
|
|
1494
|
+
request,
|
|
1495
|
+
clientBuildId: isJsonRequest && submittedClientBuildId !== "_" ? submittedClientBuildId : void 0,
|
|
1496
|
+
acceptsClientRedirect: request.headers[HTTP_PROTOCOL.requestHeaders.clientAcceptsRedirect] !== void 0,
|
|
1497
|
+
metadata
|
|
1498
|
+
};
|
|
1499
|
+
const requestContext = input.requestServices === void 0 ? Context.empty() : yield* Layer.buildWithMemoMap(input.requestServices, Layer.makeMemoMapUnsafe(), yield* Effect.scope);
|
|
1500
|
+
const middleware = getMiddlewareInvocations({
|
|
1501
|
+
pipeline: input,
|
|
1502
|
+
source,
|
|
1503
|
+
nodes: prepared.success.middleware
|
|
1504
|
+
});
|
|
1505
|
+
switch (target._tag) {
|
|
1506
|
+
case "Endpoint": return yield* handleEndpoint({
|
|
1507
|
+
pipeline: input,
|
|
1508
|
+
source,
|
|
1509
|
+
url,
|
|
1510
|
+
middleware,
|
|
1511
|
+
node: target.node,
|
|
1512
|
+
params: target.params,
|
|
1513
|
+
splatValues: target.splatValues
|
|
1514
|
+
}).pipe(Effect.provideContext(requestContext));
|
|
1515
|
+
case "Ui": return yield* handleUi({
|
|
1516
|
+
pipeline: input,
|
|
1517
|
+
source,
|
|
1518
|
+
url,
|
|
1519
|
+
isJsonRequest,
|
|
1520
|
+
middleware,
|
|
1521
|
+
positions: target.positions,
|
|
1522
|
+
assets: target.assets
|
|
1523
|
+
}).pipe(Effect.provideContext(requestContext));
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
function makeInternalError(input) {
|
|
1528
|
+
return Effect.logError(input.message, input.detail).pipe(Effect.as(finalizeStatus({
|
|
1529
|
+
status: 500,
|
|
1530
|
+
text: "Internal Server Error",
|
|
1531
|
+
clientBuildId: input.clientBuildId
|
|
1532
|
+
})));
|
|
1533
|
+
}
|
|
1534
|
+
function describeFinalizeError(error) {
|
|
1535
|
+
switch (error._tag) {
|
|
1536
|
+
case "FrameworkOwnedHeader": return "a handler set a header the framework owns";
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
function finalizeResult(input) {
|
|
1540
|
+
if (Result.isFailure(input.result)) return makeInternalError({
|
|
1541
|
+
clientBuildId: input.clientBuildId,
|
|
1542
|
+
message: describeFinalizeError(input.result.failure),
|
|
1543
|
+
detail: input.result.failure
|
|
1544
|
+
});
|
|
1545
|
+
return Effect.succeed(input.result.success);
|
|
1546
|
+
}
|
|
1547
|
+
function runHandler(input) {
|
|
1548
|
+
return Effect.gen(function* () {
|
|
1549
|
+
const effects = yield* Ref.make(empty);
|
|
1550
|
+
const context = input.makeContextFn(effects);
|
|
1551
|
+
const body = input.fn(context);
|
|
1552
|
+
const provided = Effect.provideContext(body, input.services);
|
|
1553
|
+
const exit = yield* Effect.exit(provided);
|
|
1554
|
+
const contributions = yield* Ref.get(effects);
|
|
1555
|
+
if (Exit.isFailure(exit)) {
|
|
1556
|
+
if (matchesHandlerEndInterruption(exit.cause) && endsResponse(contributions)) return {
|
|
1557
|
+
_tag: "Terminal",
|
|
1558
|
+
effects: contributions
|
|
1559
|
+
};
|
|
1560
|
+
return {
|
|
1561
|
+
_tag: "Failed",
|
|
1562
|
+
effects: contributions,
|
|
1563
|
+
cause: resolveDeclaredCause({
|
|
1564
|
+
cause: exit.cause,
|
|
1565
|
+
errors: input.errors
|
|
1566
|
+
})
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const output = yield* Effect.exit(input.readOutputFn(exit.value));
|
|
1570
|
+
if (Exit.isFailure(output)) return {
|
|
1571
|
+
_tag: "Failed",
|
|
1572
|
+
effects: contributions,
|
|
1573
|
+
cause: output.cause
|
|
1574
|
+
};
|
|
1575
|
+
return endsResponse(contributions) ? {
|
|
1576
|
+
_tag: "Terminal",
|
|
1577
|
+
effects: contributions
|
|
1578
|
+
} : {
|
|
1579
|
+
_tag: "Continue",
|
|
1580
|
+
effects: contributions,
|
|
1581
|
+
data: output.value
|
|
1582
|
+
};
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
/** Reuses schema preparation by identity; each invocation still validates its own value. */
|
|
1586
|
+
const getTypeDecoder = memoize((schema) => {
|
|
1587
|
+
return Schema.decodeUnknownEffect(Schema.toType(schema));
|
|
1588
|
+
});
|
|
1589
|
+
/** Validates the decoded output before transporting its supported values. */
|
|
1590
|
+
function encodeHandlerOutput(input) {
|
|
1591
|
+
return Effect.gen(function* () {
|
|
1592
|
+
const validated = yield* getTypeDecoder(input.schema)(input.value).pipe(Effect.mapError((cause) => new HandlerError({
|
|
1593
|
+
status: 500,
|
|
1594
|
+
cause
|
|
1595
|
+
})));
|
|
1596
|
+
return yield* Effect.fromResult(encodeTransportedValue({
|
|
1597
|
+
value: validated,
|
|
1598
|
+
typeEntries: SERVER_TRANSPORT_TYPE_ENTRIES
|
|
1599
|
+
})).pipe(Effect.mapError((cause) => new HandlerError({
|
|
1600
|
+
status: 500,
|
|
1601
|
+
cause
|
|
1602
|
+
})));
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
function resolveDeclaredCause(input) {
|
|
1606
|
+
const reason = input.cause.reasons[0];
|
|
1607
|
+
if (input.cause.reasons.length === 1 && reason !== void 0 && Cause.isFailReason(reason) && !(reason.error instanceof HandlerError)) {
|
|
1608
|
+
const matched = matchDeclaredError({
|
|
1609
|
+
errors: input.errors,
|
|
1610
|
+
value: reason.error
|
|
1611
|
+
});
|
|
1612
|
+
if (matched !== void 0) return Cause.fail(new DeclaredDomainError({
|
|
1613
|
+
status: matched.status,
|
|
1614
|
+
clientMessage: getStatusText(matched.status),
|
|
1615
|
+
tag: matched.tag,
|
|
1616
|
+
data: matched.data
|
|
1617
|
+
}));
|
|
1618
|
+
}
|
|
1619
|
+
return input.cause;
|
|
1620
|
+
}
|
|
1621
|
+
/** The response operations every handler context shares: one effects ref, the request's redirect acceptance, and route targets resolved against the plan. */
|
|
1622
|
+
function makeSharedResponseOps(input) {
|
|
1623
|
+
return makeResponseOps({
|
|
1624
|
+
effects: input.effects,
|
|
1625
|
+
acceptsClientRedirect: input.source.acceptsClientRedirect,
|
|
1626
|
+
resolveRouteTargetFn: (options) => {
|
|
1627
|
+
return resolveRouteTarget({
|
|
1628
|
+
plan: input.plan,
|
|
1629
|
+
...options
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
function makeRouteContext(input) {
|
|
1635
|
+
return {
|
|
1636
|
+
...makeRequestFacts(input.source, input.facts),
|
|
1637
|
+
...makeSharedResponseOps(input)
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function makeUiLoaderContext(input) {
|
|
1641
|
+
return {
|
|
1642
|
+
...makeRouteContext(input),
|
|
1643
|
+
...makeHeadOps(input.effects)
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function makeMiddlewareContext(input) {
|
|
1647
|
+
return {
|
|
1648
|
+
...makeIncomingRequestFacts(input.source),
|
|
1649
|
+
...makeSharedResponseOps(input),
|
|
1650
|
+
...makeHeadOps(input.effects)
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
/** A UI pattern an application names, with the search it supplies validated by the pattern's owners. */
|
|
1654
|
+
function resolveRouteTarget(input) {
|
|
1655
|
+
const target = input.plan.uiByAuthoredPattern.get(`${input.pattern === "/" ? "" : input.pattern}/_index`) ?? input.plan.uiByAuthoredPattern.get(input.pattern);
|
|
1656
|
+
if (target === void 0) return Effect.fail({
|
|
1657
|
+
_tag: "UnknownRoutePattern",
|
|
1658
|
+
pattern: input.pattern
|
|
1659
|
+
});
|
|
1660
|
+
const pattern = target.pattern;
|
|
1661
|
+
const owners = target.searchOwners;
|
|
1662
|
+
if (owners.length === 0) return input.search === void 0 ? Effect.succeed({
|
|
1663
|
+
pattern,
|
|
1664
|
+
search: void 0
|
|
1665
|
+
}) : Effect.fail({
|
|
1666
|
+
_tag: "UnexpectedRouteSearch",
|
|
1667
|
+
pattern: input.pattern
|
|
1668
|
+
});
|
|
1669
|
+
const value = input.search === void 0 ? {} : input.search;
|
|
1670
|
+
return Effect.forEach(owners, (owner) => {
|
|
1671
|
+
return getTypeDecoder(owner.search.schema)(projectSearchFields(value, owner.search.fields.keys()));
|
|
1672
|
+
}, { concurrency: "unbounded" }).pipe(Effect.flatMap((validated) => {
|
|
1673
|
+
return Effect.gen(function* () {
|
|
1674
|
+
const merged = makeNullPrototypeRecord();
|
|
1675
|
+
for (const part of validated) {
|
|
1676
|
+
if (typeof part !== "object" || part === null) return yield* Effect.fail({
|
|
1677
|
+
_tag: "InvalidRouteSearchEncoding",
|
|
1678
|
+
pattern: input.pattern
|
|
1679
|
+
});
|
|
1680
|
+
for (const [key, child] of getOwnEntries(part)) merged[key] = child;
|
|
1681
|
+
}
|
|
1682
|
+
return {
|
|
1683
|
+
pattern,
|
|
1684
|
+
search: merged
|
|
1685
|
+
};
|
|
1686
|
+
});
|
|
1687
|
+
}));
|
|
1688
|
+
}
|
|
1689
|
+
function getMiddlewareInvocations(input) {
|
|
1690
|
+
const { pipeline, source } = input;
|
|
1691
|
+
return input.nodes.map((node, index) => {
|
|
1692
|
+
return {
|
|
1693
|
+
id: `middleware:${index}`,
|
|
1694
|
+
role: "middleware",
|
|
1695
|
+
pattern: node.declaration.patterns.join(" "),
|
|
1696
|
+
run: runHandler({
|
|
1697
|
+
makeContextFn: (effects) => {
|
|
1698
|
+
return makeMiddlewareContext({
|
|
1699
|
+
effects,
|
|
1700
|
+
source,
|
|
1701
|
+
plan: pipeline.plan
|
|
1702
|
+
});
|
|
1703
|
+
},
|
|
1704
|
+
fn: node.declaration.fn,
|
|
1705
|
+
readOutputFn: () => Effect.void,
|
|
1706
|
+
errors: [],
|
|
1707
|
+
services: pipeline.services.middleware
|
|
1708
|
+
})
|
|
1709
|
+
};
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
/** Parses a request's search through its check-free contract document; a grammar failure is the handler's 400. */
|
|
1713
|
+
function parseSearch(input) {
|
|
1714
|
+
return Result.mapError(parseSearchParams({
|
|
1715
|
+
document: input.document,
|
|
1716
|
+
params: input.params,
|
|
1717
|
+
typeEntries: SERVER_TRANSPORT_TYPE_ENTRIES
|
|
1718
|
+
}), (cause) => {
|
|
1719
|
+
return new HandlerError({
|
|
1720
|
+
status: 400,
|
|
1721
|
+
clientMessage: "Invalid search parameters",
|
|
1722
|
+
cause
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
/** Validates a parsed search against the declared codec's decoded side; a contract failure is the handler's 400. */
|
|
1727
|
+
function decodeParsedSearch(input) {
|
|
1728
|
+
return getTypeDecoder(input.schema)(input.parsed).pipe(Effect.mapError((cause) => {
|
|
1729
|
+
return new HandlerError({
|
|
1730
|
+
status: 400,
|
|
1731
|
+
clientMessage: "Invalid search parameters",
|
|
1732
|
+
cause
|
|
1733
|
+
});
|
|
1734
|
+
}));
|
|
1735
|
+
}
|
|
1736
|
+
function decodeSearch(input) {
|
|
1737
|
+
if (input.search === null) return Effect.succeed(void 0);
|
|
1738
|
+
return Effect.fromResult(parseSearch({
|
|
1739
|
+
document: input.search.document,
|
|
1740
|
+
params: input.params
|
|
1741
|
+
})).pipe(Effect.flatMap((parsed) => {
|
|
1742
|
+
return decodeParsedSearch({
|
|
1743
|
+
schema: input.search.schema,
|
|
1744
|
+
parsed
|
|
1745
|
+
});
|
|
1746
|
+
}));
|
|
1747
|
+
}
|
|
1748
|
+
function makeBadBodyError(cause) {
|
|
1749
|
+
return new HandlerError({
|
|
1750
|
+
status: 400,
|
|
1751
|
+
clientMessage: "Invalid body",
|
|
1752
|
+
cause
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
/** Acquires the body through native Effect readers and their request-context controls. */
|
|
1756
|
+
function readEndpointBody(input) {
|
|
1757
|
+
const transport = getInputTransport(input.declaration);
|
|
1758
|
+
if (transport.kind === "json") return input.request.json;
|
|
1759
|
+
if (transport.kind === "formData") return input.request.headers["content-type"]?.toLowerCase().includes("multipart/form-data") ? input.request.multipart : input.request.urlParamsBody.pipe(Effect.map(UrlParams.toRecord));
|
|
1760
|
+
return Effect.succeed(void 0);
|
|
1761
|
+
}
|
|
1762
|
+
function decodeBody(input) {
|
|
1763
|
+
const transport = getInputTransport(input.declaration);
|
|
1764
|
+
if (transport.kind !== "json" && transport.kind !== "formData") return Effect.succeed(void 0);
|
|
1765
|
+
const raw = transport.kind === "formData" ? Effect.succeed(input.value) : Effect.fromResult(readTransportedValue(input.value)).pipe(Effect.flatMap((transported) => {
|
|
1766
|
+
return Effect.fromResult(decodeTransportedValue({
|
|
1767
|
+
transported,
|
|
1768
|
+
typeEntries: SERVER_TRANSPORT_TYPE_ENTRIES
|
|
1769
|
+
}));
|
|
1770
|
+
}), Effect.mapError(makeBadBodyError));
|
|
1771
|
+
const decode = transport.kind === "formData" ? Schema.decodeUnknownEffect(transport.schema) : getTypeDecoder(transport.schema);
|
|
1772
|
+
return raw.pipe(Effect.flatMap((value) => {
|
|
1773
|
+
return decode(value).pipe(Effect.mapError(makeBadBodyError));
|
|
1774
|
+
}));
|
|
1775
|
+
}
|
|
1776
|
+
const SERVICES_FOR_ENDPOINT_KIND = {
|
|
1777
|
+
query: "queries",
|
|
1778
|
+
mutation: "mutations",
|
|
1779
|
+
stream: "streams"
|
|
1780
|
+
};
|
|
1781
|
+
function getServicesForKind(input) {
|
|
1782
|
+
return input.services[SERVICES_FOR_ENDPOINT_KIND[input.kind]];
|
|
1783
|
+
}
|
|
1784
|
+
function handleEndpoint(input) {
|
|
1785
|
+
return Effect.gen(function* () {
|
|
1786
|
+
const { pipeline, node, source } = input;
|
|
1787
|
+
const { metadata, request } = pipeline;
|
|
1788
|
+
const declaration = node.declaration;
|
|
1789
|
+
const clientBuildId = metadata.clientBuildId;
|
|
1790
|
+
if (declaration.kind === "stream") return yield* makeInternalError({
|
|
1791
|
+
clientBuildId,
|
|
1792
|
+
message: "stream endpoints are not implemented in this runtime yet",
|
|
1793
|
+
detail: declaration.pattern
|
|
1794
|
+
});
|
|
1795
|
+
const middleware = yield* executeMiddleware(input.middleware);
|
|
1796
|
+
if (middleware._tag === "Terminal") return yield* finalizeResult({
|
|
1797
|
+
clientBuildId,
|
|
1798
|
+
result: finalizeEndpointTerminal({
|
|
1799
|
+
effects: middleware.effects,
|
|
1800
|
+
clientBuildId
|
|
1801
|
+
})
|
|
1802
|
+
});
|
|
1803
|
+
const value = declaration.method === "GET" ? void 0 : yield* readEndpointBody({
|
|
1804
|
+
declaration,
|
|
1805
|
+
request
|
|
1806
|
+
});
|
|
1807
|
+
const services = getServicesForKind({
|
|
1808
|
+
services: pipeline.services,
|
|
1809
|
+
kind: declaration.kind
|
|
1810
|
+
});
|
|
1811
|
+
const decodeInput = (declaration.method === "GET" ? decodeSearch({
|
|
1812
|
+
search: node.search,
|
|
1813
|
+
params: input.url.searchParams
|
|
1814
|
+
}) : decodeBody({
|
|
1815
|
+
declaration,
|
|
1816
|
+
value
|
|
1817
|
+
})).pipe(Effect.provideContext(services));
|
|
1818
|
+
const invocation = {
|
|
1819
|
+
id: `endpoint:${declaration.method} ${declaration.pattern}`,
|
|
1820
|
+
role: "endpoint",
|
|
1821
|
+
pattern: declaration.pattern,
|
|
1822
|
+
run: decodeInput.pipe(Effect.flatMap((decoded) => {
|
|
1823
|
+
return runHandler({
|
|
1824
|
+
makeContextFn: (effects) => {
|
|
1825
|
+
return makeRouteContext({
|
|
1826
|
+
effects,
|
|
1827
|
+
source,
|
|
1828
|
+
plan: pipeline.plan,
|
|
1829
|
+
facts: {
|
|
1830
|
+
pattern: declaration.pattern,
|
|
1831
|
+
params: { ...input.params },
|
|
1832
|
+
splatValues: [...input.splatValues],
|
|
1833
|
+
input: decoded
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
},
|
|
1837
|
+
fn: declaration.fn,
|
|
1838
|
+
readOutputFn: (value) => {
|
|
1839
|
+
return encodeHandlerOutput({
|
|
1840
|
+
schema: declaration.output,
|
|
1841
|
+
value
|
|
1842
|
+
});
|
|
1843
|
+
},
|
|
1844
|
+
errors: declaration.errors,
|
|
1845
|
+
services
|
|
1846
|
+
});
|
|
1847
|
+
}))
|
|
1848
|
+
};
|
|
1849
|
+
return yield* executeEndpoint({
|
|
1850
|
+
effects: middleware.effects,
|
|
1851
|
+
endpoint: invocation
|
|
1852
|
+
}).pipe(Effect.flatMap((report) => {
|
|
1853
|
+
if (report._tag === "Terminal") return finalizeResult({
|
|
1854
|
+
clientBuildId,
|
|
1855
|
+
result: finalizeEndpointTerminal({
|
|
1856
|
+
effects: report.effects,
|
|
1857
|
+
clientBuildId
|
|
1858
|
+
})
|
|
1859
|
+
});
|
|
1860
|
+
const serialized = serializeResponseJson(report.data);
|
|
1861
|
+
if (Result.isFailure(serialized)) return makeInternalError({
|
|
1862
|
+
clientBuildId,
|
|
1863
|
+
message: "the endpoint response could not be serialized",
|
|
1864
|
+
detail: serialized.failure
|
|
1865
|
+
});
|
|
1866
|
+
return finalizeResult({
|
|
1867
|
+
clientBuildId,
|
|
1868
|
+
result: finalizeEndpointJson({
|
|
1869
|
+
json: serialized.success,
|
|
1870
|
+
effects: report.effects,
|
|
1871
|
+
clientBuildId
|
|
1872
|
+
})
|
|
1873
|
+
});
|
|
1874
|
+
}));
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
function prepareDocumentValue(input) {
|
|
1878
|
+
return Effect.suspend(() => {
|
|
1879
|
+
return Effect.provideContext(input.fn(makeIncomingRequestFacts(input.source)), input.services);
|
|
1880
|
+
}).pipe(Effect.catchCause((cause) => {
|
|
1881
|
+
return Cause.hasInterruptsOnly(cause) ? Effect.interrupt : Effect.fail({
|
|
1882
|
+
_tag: "DocumentPreparationError",
|
|
1883
|
+
cause
|
|
1884
|
+
});
|
|
1885
|
+
}));
|
|
1886
|
+
}
|
|
1887
|
+
function handleUi(input) {
|
|
1888
|
+
const { pipeline, source, positions } = input;
|
|
1889
|
+
const { plan, metadata } = pipeline;
|
|
1890
|
+
const executionPositions = positions.map((position) => {
|
|
1891
|
+
const search = position.node.uiLoader === null ? null : position.node.uiLoader.search;
|
|
1892
|
+
return {
|
|
1893
|
+
position,
|
|
1894
|
+
searchInput: search === null ? null : parseSearch({
|
|
1895
|
+
document: search.document,
|
|
1896
|
+
params: input.url.searchParams
|
|
1897
|
+
})
|
|
1898
|
+
};
|
|
1899
|
+
}).map((owner) => {
|
|
1900
|
+
const { position, searchInput } = owner;
|
|
1901
|
+
const uiLoader = position.node.uiLoader;
|
|
1902
|
+
if (uiLoader === null) return {
|
|
1903
|
+
owner,
|
|
1904
|
+
invocation: null
|
|
1905
|
+
};
|
|
1906
|
+
const search = uiLoader.search;
|
|
1907
|
+
return {
|
|
1908
|
+
owner,
|
|
1909
|
+
invocation: {
|
|
1910
|
+
id: `uiLoader:${uiLoader.declaration.pattern}`,
|
|
1911
|
+
role: "uiLoader",
|
|
1912
|
+
pattern: uiLoader.declaration.pattern,
|
|
1913
|
+
run: (search === null || searchInput === null ? Effect.succeed(void 0) : Effect.fromResult(searchInput).pipe(Effect.flatMap((parsed) => {
|
|
1914
|
+
return decodeParsedSearch({
|
|
1915
|
+
schema: search.schema,
|
|
1916
|
+
parsed
|
|
1917
|
+
});
|
|
1918
|
+
}))).pipe(Effect.flatMap((decoded) => {
|
|
1919
|
+
return runHandler({
|
|
1920
|
+
makeContextFn: (effects) => {
|
|
1921
|
+
return makeUiLoaderContext({
|
|
1922
|
+
effects,
|
|
1923
|
+
source,
|
|
1924
|
+
plan,
|
|
1925
|
+
facts: {
|
|
1926
|
+
pattern: uiLoader.declaration.pattern,
|
|
1927
|
+
params: { ...position.params },
|
|
1928
|
+
splatValues: [...position.splatValues],
|
|
1929
|
+
input: decoded
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
},
|
|
1933
|
+
fn: uiLoader.declaration.fn,
|
|
1934
|
+
readOutputFn: (value) => {
|
|
1935
|
+
return encodeHandlerOutput({
|
|
1936
|
+
schema: uiLoader.declaration.output,
|
|
1937
|
+
value
|
|
1938
|
+
});
|
|
1939
|
+
},
|
|
1940
|
+
errors: [],
|
|
1941
|
+
services: pipeline.services.uiLoaders
|
|
1942
|
+
});
|
|
1943
|
+
}))
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
});
|
|
1947
|
+
return Effect.scoped(Effect.gen(function* () {
|
|
1948
|
+
const middleware = yield* executeMiddleware(input.middleware);
|
|
1949
|
+
if (middleware._tag === "Terminal") return yield* finalizeResult({
|
|
1950
|
+
clientBuildId: metadata.clientBuildId,
|
|
1951
|
+
result: finalizeTerminal({
|
|
1952
|
+
effects: middleware.effects,
|
|
1953
|
+
clientBuildId: metadata.clientBuildId
|
|
1954
|
+
})
|
|
1955
|
+
});
|
|
1956
|
+
const defaults = yield* Effect.forkScoped(prepareDocumentValue({
|
|
1957
|
+
fn: plan.document.defaultHeadFn,
|
|
1958
|
+
source,
|
|
1959
|
+
services: pipeline.services.document
|
|
1960
|
+
}));
|
|
1961
|
+
const attributes = input.isJsonRequest ? null : yield* Effect.forkScoped(prepareDocumentValue({
|
|
1962
|
+
fn: plan.document.attributesFn,
|
|
1963
|
+
source,
|
|
1964
|
+
services: pipeline.services.document
|
|
1965
|
+
}));
|
|
1966
|
+
const routes = yield* Effect.forkScoped(executeUi({
|
|
1967
|
+
effects: middleware.effects,
|
|
1968
|
+
positions: executionPositions
|
|
1969
|
+
}));
|
|
1970
|
+
const report = yield* Fiber.join(routes);
|
|
1971
|
+
if (report._tag === "Terminal") return yield* finalizeResult({
|
|
1972
|
+
clientBuildId: metadata.clientBuildId,
|
|
1973
|
+
result: finalizeTerminal({
|
|
1974
|
+
effects: report.effects,
|
|
1975
|
+
clientBuildId: metadata.clientBuildId
|
|
1976
|
+
})
|
|
1977
|
+
});
|
|
1978
|
+
const headDefaults = yield* Fiber.join(defaults);
|
|
1979
|
+
return yield* respondUi({
|
|
1980
|
+
pipeline,
|
|
1981
|
+
assets: input.assets,
|
|
1982
|
+
output: attributes === null ? { kind: "navigation" } : {
|
|
1983
|
+
kind: "document",
|
|
1984
|
+
attributes: yield* Fiber.join(attributes)
|
|
1985
|
+
},
|
|
1986
|
+
report,
|
|
1987
|
+
headDefaults
|
|
1988
|
+
});
|
|
1989
|
+
})).pipe(Effect.catchTag("DocumentPreparationError", (error) => {
|
|
1990
|
+
return makeInternalError({
|
|
1991
|
+
clientBuildId: metadata.clientBuildId,
|
|
1992
|
+
message: "the document could not be prepared",
|
|
1993
|
+
detail: error.cause
|
|
1994
|
+
});
|
|
1995
|
+
}));
|
|
1996
|
+
}
|
|
1997
|
+
function respondUi(input) {
|
|
1998
|
+
const { pipeline, report } = input;
|
|
1999
|
+
const { metadata } = pipeline;
|
|
2000
|
+
const clientBuildId = metadata.clientBuildId;
|
|
2001
|
+
const dedupeRules = pipeline.plan.headDeduplicationRules;
|
|
2002
|
+
const positions = [];
|
|
2003
|
+
for (const executed of report.positions) {
|
|
2004
|
+
if (executed._tag === "Suppressed") continue;
|
|
2005
|
+
const route = executed.owner;
|
|
2006
|
+
let searchInput = null;
|
|
2007
|
+
if (executed._tag !== "Failed" && route.searchInput !== null && Result.isSuccess(route.searchInput)) {
|
|
2008
|
+
const transported = encodeTransportedValue({
|
|
2009
|
+
value: route.searchInput.success,
|
|
2010
|
+
typeEntries: SERVER_TRANSPORT_TYPE_ENTRIES
|
|
2011
|
+
});
|
|
2012
|
+
if (Result.isFailure(transported)) return makeInternalError({
|
|
2013
|
+
clientBuildId,
|
|
2014
|
+
message: "the route search input could not be transported",
|
|
2015
|
+
detail: transported.failure
|
|
2016
|
+
});
|
|
2017
|
+
searchInput = transported.success;
|
|
2018
|
+
}
|
|
2019
|
+
positions.push({
|
|
2020
|
+
...executed,
|
|
2021
|
+
owner: {
|
|
2022
|
+
position: route.position,
|
|
2023
|
+
searchInput
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
const payload = projectPayload({
|
|
2028
|
+
clientEntry: metadata.clientEntry,
|
|
2029
|
+
assets: input.assets,
|
|
2030
|
+
report: {
|
|
2031
|
+
...report,
|
|
2032
|
+
positions
|
|
2033
|
+
},
|
|
2034
|
+
headDefaults: input.headDefaults,
|
|
2035
|
+
dedupeRules
|
|
2036
|
+
});
|
|
2037
|
+
if (input.output.kind === "navigation") {
|
|
2038
|
+
const navigationPayload = toNavigationPayload(payload);
|
|
2039
|
+
if (Result.isFailure(navigationPayload)) return makeInternalError({
|
|
2040
|
+
clientBuildId,
|
|
2041
|
+
message: "the navigation head could not be serialized",
|
|
2042
|
+
detail: navigationPayload.failure
|
|
2043
|
+
});
|
|
2044
|
+
const serialized = serializeResponseJson(navigationPayload.success);
|
|
2045
|
+
if (Result.isFailure(serialized)) return makeInternalError({
|
|
2046
|
+
clientBuildId,
|
|
2047
|
+
message: "the UI payload could not be serialized",
|
|
2048
|
+
detail: serialized.failure
|
|
2049
|
+
});
|
|
2050
|
+
return finalizeResult({
|
|
2051
|
+
clientBuildId,
|
|
2052
|
+
result: finalizeJson({
|
|
2053
|
+
json: serialized.success,
|
|
2054
|
+
effects: report.effects,
|
|
2055
|
+
clientBuildId
|
|
2056
|
+
})
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
const serialized = serializeResponseJson(toSsrPayload({
|
|
2060
|
+
projection: payload,
|
|
2061
|
+
clientBuildId,
|
|
2062
|
+
isDev: pipeline.isDev,
|
|
2063
|
+
deploymentId: pipeline.deploymentId
|
|
2064
|
+
}));
|
|
2065
|
+
if (Result.isFailure(serialized)) return makeInternalError({
|
|
2066
|
+
clientBuildId,
|
|
2067
|
+
message: "the document payload could not be serialized",
|
|
2068
|
+
detail: serialized.failure
|
|
2069
|
+
});
|
|
2070
|
+
const html = renderDocument({
|
|
2071
|
+
attributes: input.output.attributes,
|
|
2072
|
+
prepared: payload.preparedHead,
|
|
2073
|
+
criticalCss: metadata.criticalCss,
|
|
2074
|
+
...metadata.developmentHeadHtml === void 0 ? {} : { developmentHeadHtml: metadata.developmentHeadHtml },
|
|
2075
|
+
cssBundleUrls: payload.documentCssBundleUrls,
|
|
2076
|
+
staticAssetUrls: payload.documentStaticAssetUrls,
|
|
2077
|
+
modulePreloadUrls: [
|
|
2078
|
+
metadata.clientEntry.importUrl,
|
|
2079
|
+
...payload.route.routes.flatMap((route) => {
|
|
2080
|
+
return route.import_url === void 0 || route.import_url === null ? [] : [route.import_url];
|
|
2081
|
+
}),
|
|
2082
|
+
...payload.documentDepUrls
|
|
2083
|
+
],
|
|
2084
|
+
entryImportUrl: metadata.clientEntry.importUrl,
|
|
2085
|
+
payloadJson: serialized.success
|
|
2086
|
+
});
|
|
2087
|
+
if (Result.isFailure(html)) return makeInternalError({
|
|
2088
|
+
clientBuildId,
|
|
2089
|
+
message: "the document failed to render",
|
|
2090
|
+
detail: html.failure
|
|
2091
|
+
});
|
|
2092
|
+
return finalizeResult({
|
|
2093
|
+
clientBuildId,
|
|
2094
|
+
result: finalizeHtml({
|
|
2095
|
+
html: html.success,
|
|
2096
|
+
effects: report.effects,
|
|
2097
|
+
clientBuildId
|
|
2098
|
+
})
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
/** Application data has been captured and parsed; serialization can still exceed engine limits. */
|
|
2102
|
+
function serializeResponseJson(payload) {
|
|
2103
|
+
return Result.flatMap(Result.try(() => JSON.stringify(payload)), (json) => {
|
|
2104
|
+
return json === void 0 ? Result.fail("The response has no JSON representation") : Result.succeed(json);
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
//#endregion
|
|
2108
|
+
//#region src/server/runtime/vercel-deployment.ts
|
|
2109
|
+
const VERCEL_SKEW_PROTECTION_ENABLED = Config.option(Config.Boolean(PROCESS_ENVIRONMENT_PROTOCOL.vercelSkewProtectionEnabledVariable));
|
|
2110
|
+
const VERCEL_DEPLOYMENT_ID = Config.option(Config.String(PROCESS_ENVIRONMENT_PROTOCOL.vercelDeploymentIdVariable));
|
|
2111
|
+
const getVercelDeploymentId = Effect.gen(function* () {
|
|
2112
|
+
const enabled = yield* VERCEL_SKEW_PROTECTION_ENABLED;
|
|
2113
|
+
if (Option.isNone(enabled) || enabled.value === false) return;
|
|
2114
|
+
return Option.getOrUndefined(yield* VERCEL_DEPLOYMENT_ID);
|
|
2115
|
+
});
|
|
2116
|
+
//#endregion
|
|
2117
|
+
//#region src/server/index.ts
|
|
2118
|
+
function makeUiLoader(definition) {
|
|
2119
|
+
return {
|
|
2120
|
+
_tag: "UiLoader",
|
|
2121
|
+
pattern: definition.pattern,
|
|
2122
|
+
input: definition.input,
|
|
2123
|
+
output: definition.output,
|
|
2124
|
+
fn: definition.fn
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
const UiLoader = { make: makeUiLoader };
|
|
2128
|
+
/** Query and mutation declarations have the same construction and HTTP method choices. */
|
|
2129
|
+
function makeResponseEndpointMaker(kind) {
|
|
2130
|
+
function make(definition) {
|
|
2131
|
+
return {
|
|
2132
|
+
_tag: "Endpoint",
|
|
2133
|
+
kind,
|
|
2134
|
+
method: definition.method,
|
|
2135
|
+
pattern: definition.pattern,
|
|
2136
|
+
input: definition.input,
|
|
2137
|
+
output: definition.output,
|
|
2138
|
+
event: void 0,
|
|
2139
|
+
errors: definition.errors === void 0 ? [] : definition.errors,
|
|
2140
|
+
fn: definition.fn
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
return make;
|
|
2144
|
+
}
|
|
2145
|
+
function makeStreamEndpoint(definition) {
|
|
2146
|
+
return {
|
|
2147
|
+
_tag: "Endpoint",
|
|
2148
|
+
kind: "stream",
|
|
2149
|
+
method: definition.method,
|
|
2150
|
+
pattern: definition.pattern,
|
|
2151
|
+
input: definition.input,
|
|
2152
|
+
output: void 0,
|
|
2153
|
+
event: definition.event,
|
|
2154
|
+
errors: [],
|
|
2155
|
+
fn: definition.fn
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
const QueryEndpoint = { make: makeResponseEndpointMaker("query") };
|
|
2159
|
+
const MutationEndpoint = { make: makeResponseEndpointMaker("mutation") };
|
|
2160
|
+
/** Unimplemented declaration scaffold. The build rejects registered stream endpoints. */
|
|
2161
|
+
const StreamEndpoint = { make: makeStreamEndpoint };
|
|
2162
|
+
function makeTaskMiddleware(definition) {
|
|
2163
|
+
return {
|
|
2164
|
+
_tag: "TaskMiddleware",
|
|
2165
|
+
patterns: definition.patterns,
|
|
2166
|
+
methods: definition.methods,
|
|
2167
|
+
fn: definition.fn
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
const TaskMiddleware = { make: makeTaskMiddleware };
|
|
2171
|
+
function makeHtmlDocument(definition) {
|
|
2172
|
+
return {
|
|
2173
|
+
_tag: "HtmlDocument",
|
|
2174
|
+
attributesFn: definition.attributesFn,
|
|
2175
|
+
defaultHeadFn: definition.defaultHeadFn,
|
|
2176
|
+
headDeduplicationRules: definition.headDeduplicationRules ?? []
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
const HtmlDocument = { make: makeHtmlDocument };
|
|
2180
|
+
/**
|
|
2181
|
+
* Connects configuration, the document, and ordered task middleware without
|
|
2182
|
+
* starting execution. Explicit startup acquires the compiled declarations and
|
|
2183
|
+
* metadata through one shared application Layer.
|
|
2184
|
+
*/
|
|
2185
|
+
function makeApplication(input) {
|
|
2186
|
+
return {
|
|
2187
|
+
_tag: "Application",
|
|
2188
|
+
[APPLICATION_RUNTIME]: makeApplicationRuntime(input.config),
|
|
2189
|
+
document: input.document,
|
|
2190
|
+
middleware: input.middleware
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
/** A startup failure: the deployment or its generated source is wrong, never a defect. */
|
|
2194
|
+
function failStartup(input) {
|
|
2195
|
+
return Effect.fail(createVormaStartupError(input));
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Acquires handlers for application-controlled mounting. Per-kind Layers provide
|
|
2199
|
+
* application services, and the optional request Layer supplies request services.
|
|
2200
|
+
*/
|
|
2201
|
+
function makeHandlers(options) {
|
|
2202
|
+
return Effect.gen(function* () {
|
|
2203
|
+
const { app } = options;
|
|
2204
|
+
const runtime = yield* ApplicationRuntime;
|
|
2205
|
+
const { metadata } = runtime;
|
|
2206
|
+
const isDev = metadata.runtimeMode === "development";
|
|
2207
|
+
function toCriticalCssDigest(css) {
|
|
2208
|
+
return base64.encode(sha256(new TextEncoder().encode(css)));
|
|
2209
|
+
}
|
|
2210
|
+
const criticalCssDigest = runtime.readDevelopmentMetadata === void 0 ? Effect.succeed(toCriticalCssDigest(metadata.criticalCss)) : runtime.readDevelopmentMetadata.pipe(Effect.map((current) => toCriticalCssDigest(current.criticalCss)));
|
|
2211
|
+
const deploymentId = yield* getVercelDeploymentId.pipe(Effect.mapError((cause) => {
|
|
2212
|
+
return createVormaStartupError({
|
|
2213
|
+
operation: "readDeploymentConfiguration",
|
|
2214
|
+
message: "Vorma could not read deployment configuration",
|
|
2215
|
+
cause
|
|
2216
|
+
});
|
|
2217
|
+
}));
|
|
2218
|
+
const plan = compileRequestPlan({
|
|
2219
|
+
clientEntry: metadata.clientEntry,
|
|
2220
|
+
document: app.document,
|
|
2221
|
+
handlers: runtime.registeredDeclarations,
|
|
2222
|
+
middleware: app.middleware,
|
|
2223
|
+
clientViews: metadata.clientViews
|
|
2224
|
+
});
|
|
2225
|
+
if (Result.isFailure(plan)) {
|
|
2226
|
+
const invalidDeclarations = Array.isArray(plan.failure);
|
|
2227
|
+
return yield* failStartup({
|
|
2228
|
+
operation: invalidDeclarations ? "validateApplicationGraph" : "buildRequestPlan",
|
|
2229
|
+
message: invalidDeclarations ? "The Vorma application graph is invalid" : "Vorma could not build the request plan",
|
|
2230
|
+
cause: plan.failure
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
const memoMap = Layer.CurrentMemoMap.forkOrCreate(yield* Effect.context());
|
|
2234
|
+
const scope = yield* Effect.scope;
|
|
2235
|
+
const serviceLayers = {
|
|
2236
|
+
document: options.documentServices,
|
|
2237
|
+
uiLoaders: options.uiLoaderServices,
|
|
2238
|
+
queries: options.queryServices,
|
|
2239
|
+
mutations: options.mutationServices,
|
|
2240
|
+
streams: options.streamServices,
|
|
2241
|
+
middleware: options.middlewareServices
|
|
2242
|
+
};
|
|
2243
|
+
const services = yield* Effect.all(Record.map(serviceLayers, (serviceLayer) => {
|
|
2244
|
+
return Layer.buildWithMemoMap(serviceLayer === void 0 ? Layer.empty : serviceLayer, memoMap, scope);
|
|
2245
|
+
}), { concurrency: "unbounded" });
|
|
2246
|
+
const taskRuntime = yield* Layer.buildWithMemoMap(TaskRuntime.layer, memoMap, scope);
|
|
2247
|
+
let compiledPlan = plan.success;
|
|
2248
|
+
let plannedMetadata = metadata;
|
|
2249
|
+
function forOperation(operation) {
|
|
2250
|
+
return Effect.gen(function* () {
|
|
2251
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
2252
|
+
const requestMetadata = runtime.readDevelopmentMetadata === void 0 ? metadata : yield* runtime.readDevelopmentMetadata;
|
|
2253
|
+
if (requestMetadata !== plannedMetadata) {
|
|
2254
|
+
const nextPlan = compileRequestPlan({
|
|
2255
|
+
clientEntry: requestMetadata.clientEntry,
|
|
2256
|
+
document: app.document,
|
|
2257
|
+
handlers: runtime.registeredDeclarations,
|
|
2258
|
+
middleware: app.middleware,
|
|
2259
|
+
clientViews: requestMetadata.clientViews
|
|
2260
|
+
});
|
|
2261
|
+
if (Result.isFailure(nextPlan)) return yield* failStartup({
|
|
2262
|
+
operation: "buildRequestPlan",
|
|
2263
|
+
message: "Vorma could not prepare the development request plan",
|
|
2264
|
+
cause: nextPlan.failure
|
|
2265
|
+
});
|
|
2266
|
+
compiledPlan = nextPlan.success;
|
|
2267
|
+
plannedMetadata = requestMetadata;
|
|
2268
|
+
}
|
|
2269
|
+
return yield* handleRequest({
|
|
2270
|
+
plan: compiledPlan,
|
|
2271
|
+
operation,
|
|
2272
|
+
metadata: requestMetadata,
|
|
2273
|
+
request,
|
|
2274
|
+
services,
|
|
2275
|
+
...options.requestServices === void 0 ? {} : { requestServices: options.requestServices },
|
|
2276
|
+
isDev,
|
|
2277
|
+
...deploymentId === void 0 ? {} : { deploymentId }
|
|
2278
|
+
});
|
|
2279
|
+
}).pipe(Effect.provideServiceEffect(TaskContext, TaskContext.make), Effect.provideContext(taskRuntime));
|
|
2280
|
+
}
|
|
2281
|
+
const ui = forOperation({ _tag: "Ui" });
|
|
2282
|
+
return {
|
|
2283
|
+
ui: isDev ? Effect.gen(function* () {
|
|
2284
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
2285
|
+
if (request.method === "GET" && request.url.split("?", 1)[0] === DEVELOPMENT_PROTOCOL.readinessPath) return HttpServerResponse.text("ok");
|
|
2286
|
+
return yield* ui;
|
|
2287
|
+
}) : ui,
|
|
2288
|
+
/** Base64 SHA-256 of the exact inline critical CSS; use it in a CSP hash source. */
|
|
2289
|
+
getCriticalCssContentSha256: () => {
|
|
2290
|
+
return criticalCssDigest;
|
|
2291
|
+
},
|
|
2292
|
+
forEndpointMethod: (method) => forOperation({
|
|
2293
|
+
_tag: "Endpoint",
|
|
2294
|
+
method
|
|
2295
|
+
})
|
|
2296
|
+
};
|
|
2297
|
+
}).pipe(Effect.provide(options.app[APPLICATION_RUNTIME]));
|
|
2298
|
+
}
|
|
2299
|
+
const Vorma = {
|
|
2300
|
+
/** Inert application construction from configuration, a document, and ordered task middleware. */
|
|
2301
|
+
makeApplication,
|
|
2302
|
+
/**
|
|
2303
|
+
* Acquires UI and endpoint handlers. The application selects each native
|
|
2304
|
+
* registration's method and path and owns serving and service composition.
|
|
2305
|
+
*/
|
|
2306
|
+
makeHandlers,
|
|
2307
|
+
/** Creates file-serving middleware that the application explicitly mounts. */
|
|
2308
|
+
makePublicAssetsFileMiddleware
|
|
2309
|
+
};
|
|
2310
|
+
//#endregion
|
|
2311
|
+
export { FormData, HandlerError, HtmlDocument, MutationEndpoint, QueryEndpoint, ResponseInputError, StreamEndpoint, TaskMiddleware, UiLoader, Vorma, escapeJsonForScript };
|