toiljs 0.0.116 → 0.0.117
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/CHANGELOG.md +5 -0
- package/README.md +16 -7
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- package/tests/data-parity/spec.ts +0 -46
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { type Request, type Response, type Server } from '@dacely/hyper-express';
|
|
5
|
-
import pc from 'picocolors';
|
|
6
|
-
|
|
7
|
-
import { type EnvelopeRequest, METHOD_CODES } from './envelope.js';
|
|
8
|
-
import { applyCacheRule, lookupCache, type CacheableResult } from './cache.js';
|
|
9
|
-
import { type WasmDispatchResult, WasmServerModule } from '../runtime/module.js';
|
|
10
|
-
import { assembleSsr, type SsrResult, type SsrRoute } from '../ssr.js';
|
|
11
|
-
|
|
12
|
-
export const DEFAULT_MAX_BODY_LENGTH = 1024 * 1024 * 8;
|
|
13
|
-
export const MAX_BODY_BUFFER = 1024 * 32;
|
|
14
|
-
export const HTTP_IDLE_TIMEOUT = 60;
|
|
15
|
-
export const HTTP_RESPONSE_TIMEOUT = 120;
|
|
16
|
-
|
|
17
|
-
export const MIME: Readonly<Record<string, string>> = {
|
|
18
|
-
'.html': 'text/html; charset=utf-8',
|
|
19
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
20
|
-
'.mjs': 'text/javascript; charset=utf-8',
|
|
21
|
-
'.css': 'text/css; charset=utf-8',
|
|
22
|
-
'.json': 'application/json; charset=utf-8',
|
|
23
|
-
'.txt': 'text/plain; charset=utf-8',
|
|
24
|
-
'.svg': 'image/svg+xml',
|
|
25
|
-
'.png': 'image/png',
|
|
26
|
-
'.jpg': 'image/jpeg',
|
|
27
|
-
'.jpeg': 'image/jpeg',
|
|
28
|
-
'.webp': 'image/webp',
|
|
29
|
-
'.avif': 'image/avif',
|
|
30
|
-
'.gif': 'image/gif',
|
|
31
|
-
'.ico': 'image/x-icon',
|
|
32
|
-
'.wasm': 'application/wasm',
|
|
33
|
-
'.woff2': 'font/woff2',
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export interface RuntimeServerOptions {
|
|
37
|
-
readonly maxBodyLength?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function runtimeServerOptions(options: RuntimeServerOptions): {
|
|
41
|
-
max_body_length: number;
|
|
42
|
-
max_body_buffer: number;
|
|
43
|
-
fast_abort: true;
|
|
44
|
-
idle_timeout: number;
|
|
45
|
-
response_timeout: number;
|
|
46
|
-
} {
|
|
47
|
-
return {
|
|
48
|
-
max_body_length: options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH,
|
|
49
|
-
max_body_buffer: MAX_BODY_BUFFER,
|
|
50
|
-
fast_abort: true,
|
|
51
|
-
idle_timeout: HTTP_IDLE_TIMEOUT,
|
|
52
|
-
response_timeout: HTTP_RESPONSE_TIMEOUT,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function installRuntimeErrorHandler(app: Server): void {
|
|
57
|
-
app.set_error_handler((_request: Request, response: Response, error: Error) => {
|
|
58
|
-
if (response.completed) return;
|
|
59
|
-
response.atomic(() => {
|
|
60
|
-
response.status(500).send(`internal error: ${error.message}\n`);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function isDispatchableMethod(method: string): boolean {
|
|
66
|
-
return METHOD_CODES[method] !== undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function resolveFileInside(root: string, file: string): string | null {
|
|
70
|
-
const resolved = path.resolve(root, file);
|
|
71
|
-
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
|
|
72
|
-
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) return null;
|
|
73
|
-
return resolved;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function resolveStaticFile(root: string, requestPath: string): string | null {
|
|
77
|
-
let decoded: string;
|
|
78
|
-
try {
|
|
79
|
-
decoded = decodeURIComponent(requestPath);
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
if (decoded === '/' || decoded === '') return null;
|
|
84
|
-
return resolveFileInside(root, decoded.replace(/^\/+/, ''));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// --- Dynamic bracket-template routing (mirrors the toil-backend edge) ---------
|
|
88
|
-
//
|
|
89
|
-
// The build emits a dynamic route `blog/[id].tsx` as a literal `blog/[id].html`
|
|
90
|
-
// template that boots the SPA for any concrete `/blog/<x>`. When no exact file
|
|
91
|
-
// matches, walk the URL segments and, at each directory, serve a bracket sibling
|
|
92
|
-
// using the client router's precedence: a real subdir wins (static > dynamic),
|
|
93
|
-
// then `[x].html` (one segment), then `[x]/` (descend), then `[...x].html` (1+),
|
|
94
|
-
// then `[[...x]].html` (0+). Directory scans are memoized.
|
|
95
|
-
|
|
96
|
-
interface DirDynamic {
|
|
97
|
-
singleFile: string | null;
|
|
98
|
-
singleDir: string | null;
|
|
99
|
-
catchall: string | null;
|
|
100
|
-
optCatchall: string | null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const dynDirCache = new Map<string, DirDynamic>();
|
|
104
|
-
|
|
105
|
-
function classifyBracket(name: string, isDir: boolean): keyof DirDynamic | null {
|
|
106
|
-
if (isDir) {
|
|
107
|
-
if (name.startsWith('[') && name.endsWith(']') && !name.includes('...') && name.length > 2)
|
|
108
|
-
return 'singleDir';
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
if (!name.endsWith('.html')) return null;
|
|
112
|
-
const stem = name.slice(0, -'.html'.length);
|
|
113
|
-
if (stem.startsWith('[[...') && stem.endsWith(']]') && stem.length > 7) return 'optCatchall';
|
|
114
|
-
if (stem.startsWith('[...') && stem.endsWith(']') && stem.length > 5) return 'catchall';
|
|
115
|
-
if (stem.startsWith('[') && stem.endsWith(']') && !stem.startsWith('[..') && stem.length > 2)
|
|
116
|
-
return 'singleFile';
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function scanDirDynamic(absDir: string): DirDynamic {
|
|
121
|
-
const cached = dynDirCache.get(absDir);
|
|
122
|
-
if (cached) return cached;
|
|
123
|
-
const d: DirDynamic = { singleFile: null, singleDir: null, catchall: null, optCatchall: null };
|
|
124
|
-
try {
|
|
125
|
-
for (const e of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
126
|
-
const kind = classifyBracket(e.name, e.isDirectory());
|
|
127
|
-
if (kind === null) continue;
|
|
128
|
-
const cur = d[kind];
|
|
129
|
-
if (cur === null || e.name < cur) d[kind] = e.name;
|
|
130
|
-
}
|
|
131
|
-
} catch {
|
|
132
|
-
// missing/unreadable directory: no templates
|
|
133
|
-
}
|
|
134
|
-
dynDirCache.set(absDir, d);
|
|
135
|
-
return d;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const joinRel = (dir: string, name: string): string => (dir === '' ? name : `${dir}/${name}`);
|
|
139
|
-
|
|
140
|
-
function isDirInside(root: string, rel: string): boolean {
|
|
141
|
-
const resolved = path.resolve(root, rel);
|
|
142
|
-
if (resolved !== root && !resolved.startsWith(root + path.sep)) return false;
|
|
143
|
-
try {
|
|
144
|
-
return fs.statSync(resolved).isDirectory();
|
|
145
|
-
} catch {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function resolveDynamicWalk(root: string, dir: string, segs: string[], i: number): string | null {
|
|
151
|
-
if (i >= segs.length) {
|
|
152
|
-
// Nothing left: only an optional catch-all (0 segments) serves the bare dir.
|
|
153
|
-
const oc = scanDirDynamic(path.join(root, dir)).optCatchall;
|
|
154
|
-
return oc === null ? null : joinRel(dir, oc);
|
|
155
|
-
}
|
|
156
|
-
const isLast = i + 1 === segs.length;
|
|
157
|
-
// A real static subdirectory wins over any dynamic template at this level.
|
|
158
|
-
const child = joinRel(dir, segs[i]);
|
|
159
|
-
if (isDirInside(root, child)) {
|
|
160
|
-
const r = resolveDynamicWalk(root, child, segs, i + 1);
|
|
161
|
-
if (r !== null) return r;
|
|
162
|
-
}
|
|
163
|
-
const d = scanDirDynamic(path.join(root, dir));
|
|
164
|
-
if (isLast && d.singleFile !== null) return joinRel(dir, d.singleFile);
|
|
165
|
-
if (d.singleDir !== null) {
|
|
166
|
-
const r = resolveDynamicWalk(root, joinRel(dir, d.singleDir), segs, i + 1);
|
|
167
|
-
if (r !== null) return r;
|
|
168
|
-
}
|
|
169
|
-
if (d.catchall !== null) return joinRel(dir, d.catchall);
|
|
170
|
-
if (d.optCatchall !== null) return joinRel(dir, d.optCatchall);
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Resolve a request path to a bracket-template file (`blog/[id].html`) if one matches, else null. */
|
|
175
|
-
export function resolveDynamicFile(root: string, requestPath: string): string | null {
|
|
176
|
-
let decoded: string;
|
|
177
|
-
try {
|
|
178
|
-
decoded = decodeURIComponent(requestPath);
|
|
179
|
-
} catch {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
const segs = decoded.split('/').filter(Boolean);
|
|
183
|
-
const rel = resolveDynamicWalk(root, '', segs, 0);
|
|
184
|
-
return rel === null ? null : resolveFileInside(root, rel);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export interface PreparedHttpResponse {
|
|
188
|
-
readonly status: number;
|
|
189
|
-
readonly headers: readonly (readonly [string, string])[];
|
|
190
|
-
readonly body: Uint8Array;
|
|
191
|
-
readonly sendfile: string | null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export async function toEnvelopeRequest(request: Request): Promise<EnvelopeRequest> {
|
|
195
|
-
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
196
|
-
const body = hasBody ? new Uint8Array(await request.buffer()) : new Uint8Array(0);
|
|
197
|
-
const xff = request.headers['x-forwarded-for'];
|
|
198
|
-
const clientIp =
|
|
199
|
-
typeof xff === 'string' && xff.length > 0 ? xff.split(',')[0]!.trim() : '127.0.0.1';
|
|
200
|
-
return {
|
|
201
|
-
method: request.method,
|
|
202
|
-
path: request.url,
|
|
203
|
-
headers: Object.entries(request.headers),
|
|
204
|
-
body,
|
|
205
|
-
clientIp,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function prepareWasmResponse(
|
|
210
|
-
root: string,
|
|
211
|
-
result: Pick<CacheableResult, 'status' | 'headers' | 'body' | 'sendfile'>,
|
|
212
|
-
serverHeader: string,
|
|
213
|
-
): PreparedHttpResponse {
|
|
214
|
-
const headers: (readonly [string, string])[] = [];
|
|
215
|
-
let hasContentType = false;
|
|
216
|
-
for (const [name, value] of result.headers) {
|
|
217
|
-
if (name.toLowerCase() === 'content-type') hasContentType = true;
|
|
218
|
-
headers.push([name, value]);
|
|
219
|
-
}
|
|
220
|
-
headers.push(['server', serverHeader]);
|
|
221
|
-
|
|
222
|
-
if (result.sendfile !== null) {
|
|
223
|
-
const file = resolveFileInside(root, result.sendfile);
|
|
224
|
-
if (file === null) {
|
|
225
|
-
return {
|
|
226
|
-
status: 404,
|
|
227
|
-
headers: [
|
|
228
|
-
['server', serverHeader],
|
|
229
|
-
['content-type', 'text/plain; charset=utf-8'],
|
|
230
|
-
],
|
|
231
|
-
body: new TextEncoder().encode('not found\n'),
|
|
232
|
-
sendfile: null,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
if (!hasContentType) {
|
|
236
|
-
headers.push([
|
|
237
|
-
'content-type',
|
|
238
|
-
MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
|
|
239
|
-
]);
|
|
240
|
-
}
|
|
241
|
-
return { status: result.status, headers, body: new Uint8Array(0), sendfile: file };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (!hasContentType) headers.push(['content-type', 'text/plain; charset=utf-8']);
|
|
245
|
-
return { status: result.status, headers, body: result.body, sendfile: null };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function sendPreparedResponse(response: Response, out: PreparedHttpResponse): void {
|
|
249
|
-
response.status(out.status);
|
|
250
|
-
for (const [name, value] of out.headers) response.header(name, value);
|
|
251
|
-
if (out.sendfile !== null) {
|
|
252
|
-
response.sendFile(out.sendfile);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
response.send(Buffer.from(out.body.buffer, out.body.byteOffset, out.body.length));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function sendWasmResponse(
|
|
259
|
-
response: Response,
|
|
260
|
-
root: string,
|
|
261
|
-
result: Pick<CacheableResult, 'status' | 'headers' | 'body' | 'sendfile'>,
|
|
262
|
-
serverHeader: string,
|
|
263
|
-
): void {
|
|
264
|
-
sendPreparedResponse(response, prepareWasmResponse(root, result, serverHeader));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function prepareSsrResponse(
|
|
268
|
-
out: SsrResult,
|
|
269
|
-
headOnly: boolean,
|
|
270
|
-
serverHeader: string,
|
|
271
|
-
): PreparedHttpResponse {
|
|
272
|
-
const headers: (readonly [string, string])[] = [];
|
|
273
|
-
let hasContentType = false;
|
|
274
|
-
for (const [name, value] of out.headers) {
|
|
275
|
-
if (name.toLowerCase() === 'content-type') hasContentType = true;
|
|
276
|
-
headers.push([name, value]);
|
|
277
|
-
}
|
|
278
|
-
if (!hasContentType) headers.push(['content-type', 'text/html; charset=utf-8']);
|
|
279
|
-
headers.push(['server', serverHeader]);
|
|
280
|
-
return {
|
|
281
|
-
status: out.status,
|
|
282
|
-
headers,
|
|
283
|
-
body: headOnly ? new Uint8Array(0) : out.html,
|
|
284
|
-
sendfile: null,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export function sendSsr(
|
|
289
|
-
response: Response,
|
|
290
|
-
out: SsrResult,
|
|
291
|
-
headOnly: boolean,
|
|
292
|
-
serverHeader: string,
|
|
293
|
-
): void {
|
|
294
|
-
sendPreparedResponse(response, prepareSsrResponse(out, headOnly, serverHeader));
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export interface WasmDispatchOutcome {
|
|
298
|
-
readonly envelopeReq: EnvelopeRequest;
|
|
299
|
-
readonly handled: boolean;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export interface EnvelopeDispatchOutcome {
|
|
303
|
-
readonly result: CacheableResult | null;
|
|
304
|
-
readonly handled: boolean;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function dispatchEnvelopeRequest(options: {
|
|
308
|
-
readonly module: WasmServerModule;
|
|
309
|
-
readonly envelopeReq: EnvelopeRequest;
|
|
310
|
-
readonly method: string;
|
|
311
|
-
readonly url: string;
|
|
312
|
-
readonly cacheHost: string;
|
|
313
|
-
readonly hasAuth: boolean;
|
|
314
|
-
}): EnvelopeDispatchOutcome {
|
|
315
|
-
const cached = lookupCache(
|
|
316
|
-
options.cacheHost,
|
|
317
|
-
options.method,
|
|
318
|
-
options.url,
|
|
319
|
-
options.envelopeReq.body,
|
|
320
|
-
);
|
|
321
|
-
if (cached !== null) return { result: cached, handled: true };
|
|
322
|
-
|
|
323
|
-
const result = options.module.dispatch(options.envelopeReq);
|
|
324
|
-
if (result.unhandled) return { result: null, handled: false };
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
result: applyCacheRule(
|
|
328
|
-
options.cacheHost,
|
|
329
|
-
options.method,
|
|
330
|
-
options.url,
|
|
331
|
-
options.envelopeReq.body,
|
|
332
|
-
options.hasAuth,
|
|
333
|
-
result,
|
|
334
|
-
),
|
|
335
|
-
handled: true,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export async function dispatchWasmRequest(options: {
|
|
340
|
-
readonly module: WasmServerModule;
|
|
341
|
-
readonly request: Request;
|
|
342
|
-
readonly response: Response;
|
|
343
|
-
readonly root: string;
|
|
344
|
-
readonly cacheHost: string;
|
|
345
|
-
readonly serverHeader: string;
|
|
346
|
-
readonly errorPrefix: string;
|
|
347
|
-
}): Promise<WasmDispatchOutcome> {
|
|
348
|
-
const envelopeReq = await toEnvelopeRequest(options.request);
|
|
349
|
-
const hasAuth =
|
|
350
|
-
options.request.headers.cookie !== undefined ||
|
|
351
|
-
options.request.headers.authorization !== undefined;
|
|
352
|
-
|
|
353
|
-
try {
|
|
354
|
-
const dispatch = dispatchEnvelopeRequest({
|
|
355
|
-
module: options.module,
|
|
356
|
-
envelopeReq,
|
|
357
|
-
method: options.request.method,
|
|
358
|
-
url: options.request.url,
|
|
359
|
-
cacheHost: options.cacheHost,
|
|
360
|
-
hasAuth,
|
|
361
|
-
});
|
|
362
|
-
if (dispatch.result !== null) {
|
|
363
|
-
sendWasmResponse(options.response, options.root, dispatch.result, options.serverHeader);
|
|
364
|
-
return { envelopeReq, handled: true };
|
|
365
|
-
}
|
|
366
|
-
} catch (e) {
|
|
367
|
-
process.stdout.write(
|
|
368
|
-
pc.red(
|
|
369
|
-
` ${options.errorPrefix} ${options.request.method} ${options.request.path} server error: ${String(e)}`,
|
|
370
|
-
) + '\n',
|
|
371
|
-
);
|
|
372
|
-
options.response.status(500).send('internal error\n');
|
|
373
|
-
return { envelopeReq, handled: true };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return { envelopeReq, handled: false };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export function assembleRouteSsr(
|
|
380
|
-
route: SsrRoute,
|
|
381
|
-
module: WasmServerModule | null,
|
|
382
|
-
envelopeReq: EnvelopeRequest,
|
|
383
|
-
): SsrResult | null {
|
|
384
|
-
if (route.entries.length === 0) return { status: 200, headers: [], html: route.tmpl };
|
|
385
|
-
if (module === null || !module.available) return null;
|
|
386
|
-
return assembleSsr(route, module.dispatchRender(envelopeReq));
|
|
387
|
-
}
|
package/src/devserver/index.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public barrel for the toiljs WASM dev server. The folder is organized into:
|
|
3
|
-
*
|
|
4
|
-
* - `server.ts` the uWebSockets.js front + `startDevServer` entrypoint
|
|
5
|
-
* - `runtime/` the wasm module loader, host import surface, and crypto shims
|
|
6
|
-
* - `db/` the in-process ToilDB emulation (the {@link DevDatabase} class)
|
|
7
|
-
* - `http/` the envelope codec, response cache, and Vite proxy
|
|
8
|
-
* - `config/` the dotenv loader, `Environment.get` source, and rate limiter
|
|
9
|
-
* - `email/` the dev / self-host email pipeline
|
|
10
|
-
*
|
|
11
|
-
* The production edge backs the SAME wasm ABI (envelope layout, `handle(ofs,
|
|
12
|
-
* len) -> i64`, host import surface, trap isolation), so a server that runs here
|
|
13
|
-
* runs there. See `server.ts` for the request flow.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export { startDevServer } from './server.js';
|
|
17
|
-
export type { DevServerOptions, RunningDevServer } from './server.js';
|
|
18
|
-
export { loadBuiltSsrTemplates, startBuiltServer } from './production.js';
|
|
19
|
-
export type { BuiltServerOptions, RunningBuiltServer } from './production.js';
|
|
20
|
-
|
|
21
|
-
export {
|
|
22
|
-
METHOD_CODES,
|
|
23
|
-
encodeRequestEnvelope,
|
|
24
|
-
decodeResponseEnvelope,
|
|
25
|
-
unpackHandleResult,
|
|
26
|
-
} from './http/envelope.js';
|
|
27
|
-
export type { EnvelopeRequest, EnvelopeResponse } from './http/envelope.js';
|
|
28
|
-
export { WasmServerModule, WasmAbortError, UNHANDLED_HEADER } from './runtime/module.js';
|
|
29
|
-
export type { WasmDispatchResult } from './runtime/module.js';
|
|
30
|
-
export { buildHostImports, freshDispatchState } from './runtime/host.js';
|
|
31
|
-
export type { DispatchState, MemoryRef } from './runtime/host.js';
|
|
32
|
-
export type { ViteTarget } from './http/proxy.js';
|
|
33
|
-
|
|
34
|
-
// Dev DAEMON (L4) emulation (RECONCILIATION Part 2/5; doc 08 section 5).
|
|
35
|
-
export { DaemonHost, daemonEmulationEnabled } from './daemon/index.js';
|
|
36
|
-
export { parseDaemonCatalog } from './daemon/catalog.js';
|
|
37
|
-
export type { DaemonCatalog, ScheduledTask, CronMasks } from './daemon/catalog.js';
|
|
38
|
-
export { buildDaemonImports, freshDaemonState } from './daemon/host.js';
|
|
39
|
-
export type { DaemonState, DaemonRuntime, ResolvedDaemonConfig } from './daemon/host.js';
|
|
40
|
-
export { cronMatches, cronNeverFires, nextCronFireMs } from './daemon/cron.js';
|
|
41
|
-
export { parseSurface } from './wasm/surface.js';
|
|
42
|
-
export type { Surface, SurfaceFlags } from './wasm/surface.js';
|
|
43
|
-
export { customSection } from './wasm/sections.js';
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
export interface ThreadedRequest {
|
|
2
|
-
readonly id: number;
|
|
3
|
-
readonly method: string;
|
|
4
|
-
readonly url: string;
|
|
5
|
-
readonly path: string;
|
|
6
|
-
readonly headers: readonly (readonly [string, string])[];
|
|
7
|
-
readonly body: string;
|
|
8
|
-
readonly clientIp: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ThreadedHttpResponse {
|
|
12
|
-
readonly kind: 'response';
|
|
13
|
-
readonly status: number;
|
|
14
|
-
readonly headers: readonly (readonly [string, string])[];
|
|
15
|
-
readonly body: string;
|
|
16
|
-
readonly sendfile: string | null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ThreadedFallbackResponse {
|
|
20
|
-
readonly kind: 'fallback';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type ThreadedReply = ThreadedHttpResponse | ThreadedFallbackResponse;
|
|
24
|
-
|
|
25
|
-
export type WorkerToPrimaryMessage =
|
|
26
|
-
| { readonly toil: 'ready'; readonly port: number; readonly workerId: number }
|
|
27
|
-
| { readonly toil: 'clientCount'; readonly count: number; readonly workerId: number }
|
|
28
|
-
| { readonly toil: 'request'; readonly request: ThreadedRequest };
|
|
29
|
-
|
|
30
|
-
export type PrimaryToWorkerMessage =
|
|
31
|
-
| {
|
|
32
|
-
readonly toil: 'start';
|
|
33
|
-
readonly workerId: number;
|
|
34
|
-
readonly options: unknown;
|
|
35
|
-
}
|
|
36
|
-
| { readonly toil: 'reply'; readonly id: number; readonly reply: ThreadedReply }
|
|
37
|
-
| { readonly toil: 'broadcast'; readonly message: string }
|
|
38
|
-
| { readonly toil: 'shutdown' };
|
|
39
|
-
|
|
40
|
-
export function encodeBody(body: Uint8Array): string {
|
|
41
|
-
return Buffer.from(body.buffer, body.byteOffset, body.length).toString('base64');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function decodeBody(body: string): Uint8Array {
|
|
45
|
-
return Buffer.from(body, 'base64');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function isWorkerToPrimaryMessage(value: unknown): value is WorkerToPrimaryMessage {
|
|
49
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
50
|
-
const message = value as Record<string, unknown>;
|
|
51
|
-
return message.toil === 'ready' || message.toil === 'clientCount' || message.toil === 'request';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function isPrimaryToWorkerMessage(value: unknown): value is PrimaryToWorkerMessage {
|
|
55
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
56
|
-
const message = value as Record<string, unknown>;
|
|
57
|
-
return (
|
|
58
|
-
message.toil === 'start' ||
|
|
59
|
-
message.toil === 'reply' ||
|
|
60
|
-
message.toil === 'broadcast' ||
|
|
61
|
-
message.toil === 'shutdown'
|
|
62
|
-
);
|
|
63
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { BuiltServerOptions, RunningBuiltServer } from './production.js';
|
|
2
|
-
import { startBuiltServerWorker } from './production.js';
|
|
3
|
-
import {
|
|
4
|
-
isPrimaryToWorkerMessage,
|
|
5
|
-
type PrimaryToWorkerMessage,
|
|
6
|
-
type ThreadedReply,
|
|
7
|
-
type ThreadedRequest,
|
|
8
|
-
} from './production-ipc.js';
|
|
9
|
-
|
|
10
|
-
let running: RunningBuiltServer | null = null;
|
|
11
|
-
let workerId = 0;
|
|
12
|
-
const pending = new Map<number, (reply: ThreadedReply) => void>();
|
|
13
|
-
|
|
14
|
-
function send(message: object): void {
|
|
15
|
-
try {
|
|
16
|
-
process.send?.(message);
|
|
17
|
-
} catch {
|
|
18
|
-
// The parent is gone; normal process shutdown will follow.
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function requestPrimary(request: ThreadedRequest): Promise<ThreadedReply> {
|
|
23
|
-
const id = request.id;
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
const timeout = setTimeout(() => {
|
|
26
|
-
pending.delete(id);
|
|
27
|
-
reject(new Error('primary request timed out'));
|
|
28
|
-
}, 120_000);
|
|
29
|
-
timeout.unref();
|
|
30
|
-
|
|
31
|
-
pending.set(id, (reply) => {
|
|
32
|
-
clearTimeout(timeout);
|
|
33
|
-
resolve(reply);
|
|
34
|
-
});
|
|
35
|
-
send({ toil: 'request', request });
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function start(message: Extract<PrimaryToWorkerMessage, { toil: 'start' }>): Promise<void> {
|
|
40
|
-
if (running !== null) return;
|
|
41
|
-
workerId = message.workerId;
|
|
42
|
-
const options = message.options as BuiltServerOptions;
|
|
43
|
-
running = await startBuiltServerWorker(options, {
|
|
44
|
-
request: requestPrimary,
|
|
45
|
-
clientCount: (count) => send({ toil: 'clientCount', workerId, count }),
|
|
46
|
-
});
|
|
47
|
-
send({ toil: 'ready', workerId, port: running.port });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function shutdown(): Promise<void> {
|
|
51
|
-
const server = running;
|
|
52
|
-
running = null;
|
|
53
|
-
if (server !== null) await server.close();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
process.on('message', (value: unknown) => {
|
|
57
|
-
if (!isPrimaryToWorkerMessage(value)) return;
|
|
58
|
-
switch (value.toil) {
|
|
59
|
-
case 'start':
|
|
60
|
-
void start(value).catch((e: unknown) => {
|
|
61
|
-
process.stderr.write(`toiljs production worker failed: ${String(e)}\n`);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
});
|
|
64
|
-
return;
|
|
65
|
-
case 'reply': {
|
|
66
|
-
const resolve = pending.get(value.id);
|
|
67
|
-
if (resolve === undefined) return;
|
|
68
|
-
pending.delete(value.id);
|
|
69
|
-
resolve(value.reply);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
case 'broadcast':
|
|
73
|
-
running?.broadcast(value.message);
|
|
74
|
-
return;
|
|
75
|
-
case 'shutdown':
|
|
76
|
-
void shutdown().finally(() => process.exit(0));
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
process.once('disconnect', () => {
|
|
82
|
-
void shutdown().finally(() => process.exit(0));
|
|
83
|
-
});
|