@effindomv2/fui-as 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE.md +7 -0
  2. package/browser/src/common-harness/host-imports.ts +430 -0
  3. package/browser/src/common-harness/interop.ts +39 -0
  4. package/browser/src/common-harness/managed-harness-bitmap-host.ts +92 -0
  5. package/browser/src/common-harness/managed-harness-fetch-host.ts +201 -0
  6. package/browser/src/common-harness/managed-harness-file-host.ts +1101 -0
  7. package/browser/src/common-harness/managed-harness-file-payloads.ts +143 -0
  8. package/browser/src/common-harness/managed-harness-file-types.ts +106 -0
  9. package/browser/src/common-harness/managed-harness-session.ts +15 -0
  10. package/browser/src/common-harness/managed-harness.ts +1323 -0
  11. package/browser/src/common-harness/managed-history.ts +168 -0
  12. package/browser/src/common-harness/persisted-restore-policy.ts +50 -0
  13. package/browser/src/common-harness/persisted-ui-state-controller.ts +309 -0
  14. package/browser/src/common-harness/text-session-bridge.ts +452 -0
  15. package/browser/src/common-harness/types.ts +205 -0
  16. package/browser/src/common-harness/ui-chrome.ts +191 -0
  17. package/browser/src/common-harness/ui-imports.ts +529 -0
  18. package/browser/src/common-harness/wasm-module-cache.ts +47 -0
  19. package/browser/src/common-harness.ts +27 -0
  20. package/browser/src/file-processing-worker.ts +89 -0
  21. package/browser/src/host-events.ts +97 -0
  22. package/browser/src/host-services.ts +203 -0
  23. package/browser/src/index.ts +62 -0
  24. package/browser/src/persisted-ui-state.ts +206 -0
  25. package/browser/src/routed-harness.ts +198 -0
  26. package/browser/src/worker-bootstrap.ts +483 -0
  27. package/browser/src/worker-manager.ts +230 -0
  28. package/browser/src/worker-types.ts +50 -0
  29. package/package.json +89 -0
  30. package/scripts/build-demo-as.sh +91 -0
  31. package/scripts/build.sh +325 -0
  32. package/scripts/generate-host-events.ts +175 -0
  33. package/scripts/generate-host-services.ts +157 -0
  34. package/src/Fui.ts +205 -0
  35. package/src/FuiExports.ts +55 -0
  36. package/src/FuiPrimitives.ts +15 -0
  37. package/src/FuiWorker.ts +3 -0
  38. package/src/FuiWorkerExports.ts +6 -0
  39. package/src/bindings/ui.ts +531 -0
  40. package/src/color.ts +86 -0
  41. package/src/controls/AntiSelectionArea.ts +23 -0
  42. package/src/controls/Button.ts +750 -0
  43. package/src/controls/Checkbox.ts +181 -0
  44. package/src/controls/ContextMenu.ts +885 -0
  45. package/src/controls/ControlTemplateSet.ts +37 -0
  46. package/src/controls/Dialog.ts +355 -0
  47. package/src/controls/Dropdown.ts +856 -0
  48. package/src/controls/Form.ts +110 -0
  49. package/src/controls/NavLink.ts +211 -0
  50. package/src/controls/Popup.ts +129 -0
  51. package/src/controls/ProgressBar.ts +180 -0
  52. package/src/controls/RadioButton.ts +135 -0
  53. package/src/controls/RadioGroup.ts +244 -0
  54. package/src/controls/SelectionArea.ts +75 -0
  55. package/src/controls/Slider.ts +471 -0
  56. package/src/controls/Switch.ts +132 -0
  57. package/src/controls/TextArea.ts +20 -0
  58. package/src/controls/TextInput.ts +7 -0
  59. package/src/controls/index.ts +18 -0
  60. package/src/controls/internal/ButtonPresenter.ts +95 -0
  61. package/src/controls/internal/CheckboxIndicatorPresenter.ts +93 -0
  62. package/src/controls/internal/DropdownChevronPresenter.ts +67 -0
  63. package/src/controls/internal/DropdownFieldPresenter.ts +110 -0
  64. package/src/controls/internal/DropdownOptionRowPresenter.ts +82 -0
  65. package/src/controls/internal/PopupPresenter.ts +198 -0
  66. package/src/controls/internal/PressableIndicatorPresenter.ts +32 -0
  67. package/src/controls/internal/PressableLabeledControl.ts +221 -0
  68. package/src/controls/internal/RadioIndicatorPresenter.ts +73 -0
  69. package/src/controls/internal/SliderPresenter.ts +157 -0
  70. package/src/controls/internal/SwitchIndicatorPresenter.ts +72 -0
  71. package/src/controls/internal/TextInputCore.ts +695 -0
  72. package/src/controls/internal/TextInputPresenter.ts +72 -0
  73. package/src/controls/templating.ts +54 -0
  74. package/src/core/Action.ts +94 -0
  75. package/src/core/Actions.ts +37 -0
  76. package/src/core/Animation.ts +412 -0
  77. package/src/core/Application.ts +328 -0
  78. package/src/core/Assets.ts +264 -0
  79. package/src/core/AttachedProperties.ts +32 -0
  80. package/src/core/Bitmap.ts +70 -0
  81. package/src/core/BoundCallback.ts +104 -0
  82. package/src/core/Callbacks.ts +17 -0
  83. package/src/core/ContextMenuManager.ts +466 -0
  84. package/src/core/DebugApi.ts +30 -0
  85. package/src/core/Disposable.ts +10 -0
  86. package/src/core/DragDropManager.ts +179 -0
  87. package/src/core/DragGesture.ts +184 -0
  88. package/src/core/DynamicAssetIds.ts +24 -0
  89. package/src/core/Errors.ts +48 -0
  90. package/src/core/EventRouter.ts +408 -0
  91. package/src/core/ExternalDropManager.ts +122 -0
  92. package/src/core/Fetch.ts +264 -0
  93. package/src/core/FetchFfi.ts +15 -0
  94. package/src/core/File.ts +1002 -0
  95. package/src/core/FocusAdornerManager.ts +263 -0
  96. package/src/core/FocusVisibility.ts +36 -0
  97. package/src/core/FrameScheduler.ts +28 -0
  98. package/src/core/KeyboardScroll.ts +161 -0
  99. package/src/core/KeyboardScrollTracker.ts +386 -0
  100. package/src/core/Logger.ts +80 -0
  101. package/src/core/Navigation.ts +13 -0
  102. package/src/core/Node.ts +1708 -0
  103. package/src/core/PersistedState.ts +102 -0
  104. package/src/core/PersistedUiState.ts +142 -0
  105. package/src/core/Platform.ts +219 -0
  106. package/src/core/Signal.ts +89 -0
  107. package/src/core/Theme.ts +365 -0
  108. package/src/core/Timers.ts +129 -0
  109. package/src/core/ToolTip.ts +122 -0
  110. package/src/core/ToolTipManager.ts +459 -0
  111. package/src/core/Transitions.ts +34 -0
  112. package/src/core/Typography.ts +204 -0
  113. package/src/core/Worker.ts +196 -0
  114. package/src/core/bind.ts +37 -0
  115. package/src/core/event_exports.ts +596 -0
  116. package/src/core/ffi.ts +728 -0
  117. package/src/host-services/runtime.ts +25 -0
  118. package/src/nodes/FlexBox.ts +789 -0
  119. package/src/nodes/GradientStop.ts +9 -0
  120. package/src/nodes/Grid.ts +183 -0
  121. package/src/nodes/Image.ts +189 -0
  122. package/src/nodes/Portal.ts +14 -0
  123. package/src/nodes/RichText.ts +312 -0
  124. package/src/nodes/ScrollBar.ts +570 -0
  125. package/src/nodes/ScrollBox.ts +415 -0
  126. package/src/nodes/ScrollState.ts +10 -0
  127. package/src/nodes/ScrollView.ts +511 -0
  128. package/src/nodes/Svg.ts +142 -0
  129. package/src/nodes/Text.ts +145 -0
  130. package/src/nodes/TextCore.ts +558 -0
  131. package/src/nodes/VirtualList.ts +431 -0
  132. package/src/nodes/helpers.ts +25 -0
  133. package/src/nodes/index.ts +14 -0
  134. package/src/tsconfig.json +7 -0
  135. package/src/worker/Worker.ts +169 -0
  136. package/src/worker/WorkerJob.ts +65 -0
  137. package/src/worker/ffi.ts +23 -0
@@ -0,0 +1,198 @@
1
+ import {
2
+ pushManagedHistoryEntry,
3
+ replaceManagedHistoryEntry,
4
+ startManagedHarness,
5
+ type HarnessAppOptions,
6
+ type HarnessController,
7
+ type HarnessExports,
8
+ type HarnessState,
9
+ } from './common-harness';
10
+ import type { HostEventsDefinition } from './host-events';
11
+ import type { HostServicesDefinition } from './host-services';
12
+ import type { WorkerHostServicesBundleConfig } from './worker-types';
13
+
14
+ type NavigationMode = 'push' | 'replace' | 'pop';
15
+
16
+ declare global {
17
+ interface Window {
18
+ __fuiAsReady?: boolean;
19
+ __fuiAsError?: string;
20
+ }
21
+ }
22
+
23
+ export interface RoutedHarnessRoute {
24
+ readonly routePath: string;
25
+ readonly matchPath?: string;
26
+ readonly wasmPath: string;
27
+ readonly title: string;
28
+ }
29
+
30
+ export interface RoutedHarnessManagerState {
31
+ readonly shellId: string;
32
+ readonly routePath: string;
33
+ readonly activeWasmPath: string;
34
+ readonly routeLoads: Readonly<Record<string, number>>;
35
+ }
36
+
37
+ export interface RoutedHarnessConfig<
38
+ TExports extends HarnessExports,
39
+ TRoute extends RoutedHarnessRoute = RoutedHarnessRoute,
40
+ > {
41
+ readonly shellId: string;
42
+ readonly routeBase: string;
43
+ readonly routes: readonly TRoute[];
44
+ readonly hostEvents?: HostEventsDefinition;
45
+ readonly hostServices?: HostServicesDefinition;
46
+ readonly workerHostServices?: WorkerHostServicesBundleConfig;
47
+ readonly recreateRuntimeOnWarmRouteSwap?: boolean;
48
+ readonly showLoadingOverlay?: (isWarmRouteSwap: boolean, route: TRoute) => boolean | undefined;
49
+ readonly onBooting?: () => void;
50
+ readonly onRouteLoading?: (route: TRoute) => void;
51
+ readonly onRouteReady?: (state: RoutedHarnessManagerState, route: TRoute) => void;
52
+ readonly onHarnessStateUpdated?: (state: HarnessState) => void;
53
+ readonly onHarnessError?: (error: unknown) => void;
54
+ readonly run: (exports: TExports, route: TRoute) => void;
55
+ readonly onDispose?: (exports: TExports, route: TRoute) => void;
56
+ }
57
+
58
+ function normalizeRoutePath(
59
+ pathname: string,
60
+ routeBase: string,
61
+ routeByPath: ReadonlyMap<string, RoutedHarnessRoute>,
62
+ fallbackRoutePath: string,
63
+ ): string {
64
+ let normalized = pathname;
65
+ if (normalized.endsWith('/index.html')) {
66
+ normalized = normalized.slice(0, -'index.html'.length);
67
+ }
68
+ if (!normalized.endsWith('/')) {
69
+ normalized += '/';
70
+ }
71
+ if (normalized === routeBase) {
72
+ return fallbackRoutePath;
73
+ }
74
+ return routeByPath.has(normalized) ? normalized : fallbackRoutePath;
75
+ }
76
+
77
+ function resolveRouteMatchPath(route: RoutedHarnessRoute): string {
78
+ return route.matchPath ?? route.routePath;
79
+ }
80
+
81
+ function currentBrowserPath(): string {
82
+ let pathname = window.location.pathname;
83
+ if (pathname.endsWith('/index.html')) {
84
+ pathname = pathname.slice(0, -'index.html'.length);
85
+ }
86
+ return pathname.endsWith('/') ? pathname : `${pathname}/`;
87
+ }
88
+
89
+ export function startRoutedHarness<
90
+ TExports extends HarnessExports,
91
+ TRoute extends RoutedHarnessRoute = RoutedHarnessRoute,
92
+ >(config: RoutedHarnessConfig<TExports, TRoute>): void {
93
+ const routeByPath = new Map<string, TRoute>(config.routes.map((route) => [resolveRouteMatchPath(route), route]));
94
+ const fallbackRoute = config.routes[0];
95
+ const routeLoads: Record<string, number> = {};
96
+ let activeRoute: TRoute | null = null;
97
+ let navigationQueue: Promise<void> = Promise.resolve();
98
+
99
+ if (fallbackRoute === undefined) {
100
+ throw new Error('startRoutedHarness requires at least one route.');
101
+ }
102
+
103
+ function resolveRoute(pathname: string): TRoute {
104
+ const normalizedPath = normalizeRoutePath(
105
+ pathname,
106
+ config.routeBase,
107
+ routeByPath,
108
+ resolveRouteMatchPath(fallbackRoute),
109
+ );
110
+ return routeByPath.get(normalizedPath) ?? fallbackRoute;
111
+ }
112
+
113
+ function sameBrowserLocation(route: TRoute): boolean {
114
+ return currentBrowserPath() === resolveRouteMatchPath(route);
115
+ }
116
+
117
+ function buildManagerState(route: TRoute): RoutedHarnessManagerState {
118
+ return {
119
+ shellId: config.shellId,
120
+ routePath: route.routePath,
121
+ activeWasmPath: route.wasmPath,
122
+ routeLoads: { ...routeLoads },
123
+ };
124
+ }
125
+
126
+ async function navigateToRoute(
127
+ controller: HarnessController,
128
+ targetUrl: URL,
129
+ mode: NavigationMode,
130
+ ): Promise<void> {
131
+ const route = resolveRoute(targetUrl.pathname);
132
+ if (mode !== 'pop' && sameBrowserLocation(route) && activeRoute?.routePath === route.routePath) {
133
+ config.onRouteReady?.(buildManagerState(route), route);
134
+ window.__fuiAsReady = true;
135
+ delete window.__fuiAsError;
136
+ return;
137
+ }
138
+
139
+ const destination = new URL(route.routePath, window.location.origin);
140
+ destination.search = targetUrl.search;
141
+ destination.hash = targetUrl.hash;
142
+ if (mode === 'push') {
143
+ pushManagedHistoryEntry(destination);
144
+ } else if (mode === 'replace' && !sameBrowserLocation(route)) {
145
+ replaceManagedHistoryEntry(destination);
146
+ }
147
+
148
+ window.__fuiAsReady = false;
149
+ config.onRouteLoading?.(route);
150
+ const isWarmRouteSwap = activeRoute !== null;
151
+ if (isWarmRouteSwap && config.recreateRuntimeOnWarmRouteSwap === true) {
152
+ await controller.recreateRuntime();
153
+ }
154
+
155
+ const appOptions: HarnessAppOptions<TExports> = {
156
+ wasmPath: route.wasmPath,
157
+ persistedRestoreMode: activeRoute === null ? 'initial' : (mode === 'pop' ? 'pop' : 'none'),
158
+ hostEvents: config.hostEvents,
159
+ hostServices: config.hostServices,
160
+ workerHostServices: config.workerHostServices,
161
+ run(exports): void {
162
+ config.run(exports, route);
163
+ },
164
+ onDispose(exports): void {
165
+ config.onDispose?.(exports, route);
166
+ },
167
+ onStateUpdated(state): void {
168
+ config.onHarnessStateUpdated?.(state);
169
+ },
170
+ };
171
+ const showLoadingOverlay = config.showLoadingOverlay?.(isWarmRouteSwap, route);
172
+ if (showLoadingOverlay !== undefined) {
173
+ appOptions.showLoadingOverlay = showLoadingOverlay;
174
+ }
175
+
176
+ await controller.loadApp(appOptions);
177
+ routeLoads[route.routePath] = (routeLoads[route.routePath] ?? 0) + 1;
178
+ activeRoute = route;
179
+ config.onRouteReady?.(buildManagerState(route), route);
180
+ window.__fuiAsReady = true;
181
+ delete window.__fuiAsError;
182
+ }
183
+
184
+ startManagedHarness({
185
+ onReady: async (controller): Promise<void> => {
186
+ controller.setSameOriginNavigationHandler((target, mode) => {
187
+ navigationQueue = navigationQueue.then(() => navigateToRoute(controller, target, mode));
188
+ return navigationQueue;
189
+ });
190
+
191
+ config.onBooting?.();
192
+ await navigateToRoute(controller, new URL(window.location.href), 'replace');
193
+ },
194
+ onError(error): void {
195
+ config.onHarnessError?.(error);
196
+ },
197
+ });
198
+ }
@@ -0,0 +1,483 @@
1
+ import type {
2
+ WorkerBootstrapInboundMessage,
3
+ WorkerBootstrapOutboundMessage,
4
+ WorkerHostServicesBundleConfig,
5
+ WorkerBootstrapStartMessage,
6
+ } from './worker-types';
7
+ import {
8
+ createHostServiceImportModule,
9
+ getHostServiceImportNames,
10
+ type HostServicesDefinition,
11
+ } from './host-services';
12
+
13
+ const workerScope = globalThis as typeof globalThis & {
14
+ importScripts(...urls: string[]): void;
15
+ postMessage(message: WorkerBootstrapOutboundMessage): void;
16
+ onmessage: ((event: MessageEvent<WorkerBootstrapInboundMessage>) => void) | null;
17
+ __fuiWorkerHostServicesModule?: Record<string, unknown>;
18
+ };
19
+
20
+ const decoder = new TextDecoder();
21
+ const encoder = new TextEncoder();
22
+ let activeWorkerId: number | null = null;
23
+ let activeCancellationRequested = false;
24
+ let pendingCancellationRequested = false;
25
+
26
+ const allowedWorkerHostImports = new Set([
27
+ 'fui_fetch_start',
28
+ 'fui_fetch_cancel',
29
+ 'fui_worker_input_length',
30
+ 'fui_worker_copy_input',
31
+ 'fui_worker_report_progress',
32
+ 'fui_worker_complete_string',
33
+ 'fui_worker_fail',
34
+ 'fui_worker_is_cancelled',
35
+ 'fui_worker_request_yield',
36
+ 'fui_worker_request_yield_delay',
37
+ ]);
38
+
39
+ function describeError(error: unknown): string {
40
+ return error instanceof Error ? error.message : String(error);
41
+ }
42
+
43
+ function loadWorkerHostServices(config: WorkerHostServicesBundleConfig | undefined): HostServicesDefinition | undefined {
44
+ if (config === undefined) {
45
+ return undefined;
46
+ }
47
+ workerScope.importScripts(config.scriptUrl);
48
+ const bundle = workerScope.__fuiWorkerHostServicesModule;
49
+ if (typeof bundle !== 'object' || bundle === null) {
50
+ throw new Error(`Worker host-services bundle ${config.scriptUrl} did not initialize __fuiWorkerHostServicesModule.`);
51
+ }
52
+ const exported = bundle[config.exportName];
53
+ if (typeof exported !== 'object' || exported === null) {
54
+ throw new Error(`Worker host-services bundle ${config.scriptUrl} does not export "${config.exportName}".`);
55
+ }
56
+ return exported as HostServicesDefinition;
57
+ }
58
+
59
+ function validateWorkerImports(module: WebAssembly.Module, hostServices: HostServicesDefinition | undefined): void {
60
+ const allowedHostServiceImports = getHostServiceImportNames(hostServices);
61
+ const imports = WebAssembly.Module.imports(module);
62
+ for (const imported of imports) {
63
+ if (imported.kind !== 'function') {
64
+ throw new Error(`Worker import ${imported.module}.${imported.name} is not allowed.`);
65
+ }
66
+ if (imported.module === 'env' && imported.name === 'abort') {
67
+ continue;
68
+ }
69
+ if (imported.module === 'fui_worker_host' && allowedWorkerHostImports.has(imported.name)) {
70
+ continue;
71
+ }
72
+ if (imported.module === 'fui_fetch_host' && allowedWorkerHostImports.has(imported.name)) {
73
+ continue;
74
+ }
75
+ if (imported.module === 'fui_host_service' && allowedHostServiceImports.has(imported.name)) {
76
+ continue;
77
+ }
78
+ throw new Error(`Worker import ${imported.module}.${imported.name} is not allowed.`);
79
+ }
80
+ }
81
+
82
+ function readUtf8(memory: WebAssembly.Memory | null, ptr: number, len: number): string {
83
+ if (memory === null || len <= 0) {
84
+ return '';
85
+ }
86
+ return decoder.decode(new Uint8Array(memory.buffer, ptr, len));
87
+ }
88
+
89
+ function writeUtf8(memory: WebAssembly.Memory | null, ptr: number, capacity: number, text: string, context: string): number {
90
+ if (memory === null) {
91
+ throw new Error(`${context} requires worker memory.`);
92
+ }
93
+ if (capacity <= 0) {
94
+ if (text.length === 0) {
95
+ return 0;
96
+ }
97
+ throw new Error(`${context} cannot write into a zero-length worker host-service buffer.`);
98
+ }
99
+ const encoded = encoder.encode(text);
100
+ if (encoded.length > capacity) {
101
+ throw new Error(`${context} exceeds the worker host-service result buffer.`);
102
+ }
103
+ if (encoded.length > 0) {
104
+ new Uint8Array(memory.buffer, ptr, encoded.length).set(encoded);
105
+ }
106
+ return encoded.length;
107
+ }
108
+
109
+ function encodeTextPartsPayload(values: readonly string[]): Uint8Array {
110
+ const encodedValues = values.map((value) => encoder.encode(value));
111
+ let totalBytes = 4;
112
+ for (const encoded of encodedValues) {
113
+ totalBytes += 4 + encoded.length;
114
+ }
115
+ const bytes = new Uint8Array(totalBytes);
116
+ const dataView = new DataView(bytes.buffer);
117
+ let byteOffset = 0;
118
+ dataView.setUint32(byteOffset, values.length >>> 0, true);
119
+ byteOffset += 4;
120
+ for (const encoded of encodedValues) {
121
+ dataView.setUint32(byteOffset, encoded.length >>> 0, true);
122
+ byteOffset += 4;
123
+ if (encoded.length > 0) {
124
+ bytes.set(encoded, byteOffset);
125
+ byteOffset += encoded.length;
126
+ }
127
+ }
128
+ return bytes;
129
+ }
130
+
131
+ async function startWorker(message: WorkerBootstrapStartMessage): Promise<void> {
132
+ let memory: WebAssembly.Memory | null = null;
133
+ let terminalSent = false;
134
+ let yieldRequested = false;
135
+ let requestedYieldDelayMs = 0;
136
+ let resumeScheduled = false;
137
+ let callbackBufferPtr = 0;
138
+ let callbackBufferSize = 0;
139
+ let wasmExports: (Record<string, unknown> & {
140
+ memory?: WebAssembly.Memory;
141
+ __fui_worker_text_buffer?: () => number;
142
+ __fui_worker_text_buffer_size?: () => number;
143
+ }) | null = null;
144
+ const activeFetchRequests = new Map<number, AbortController>();
145
+ const inputBytes = new TextEncoder().encode(message.input);
146
+ const hostServices = loadWorkerHostServices(message.workerHostServices);
147
+ let entry: (() => void) | null = null;
148
+ activeWorkerId = message.workerId;
149
+ activeCancellationRequested = pendingCancellationRequested;
150
+ pendingCancellationRequested = false;
151
+
152
+ function readCancelFlag(): boolean {
153
+ return activeWorkerId === message.workerId && activeCancellationRequested;
154
+ }
155
+
156
+ function cancelAllFetchRequests(): void {
157
+ for (const controller of activeFetchRequests.values()) {
158
+ controller.abort();
159
+ }
160
+ activeFetchRequests.clear();
161
+ }
162
+
163
+ function writeCallbackBytes(bytes: Uint8Array, context: string): { ptr: number; len: number } {
164
+ if (callbackBufferSize <= 0) {
165
+ throw new Error(`${context} requires the worker callback buffer.`);
166
+ }
167
+ if (bytes.length > callbackBufferSize) {
168
+ throw new Error(`${context} exceeds the worker callback buffer.`);
169
+ }
170
+ if (memory === null) {
171
+ throw new Error(`${context} requires worker memory.`);
172
+ }
173
+ if (bytes.length > 0) {
174
+ new Uint8Array(memory.buffer, callbackBufferPtr, bytes.length).set(bytes);
175
+ }
176
+ return {
177
+ ptr: bytes.length > 0 ? callbackBufferPtr : 0,
178
+ len: bytes.length,
179
+ };
180
+ }
181
+
182
+ function emitFetchComplete(
183
+ requestId: number,
184
+ ok: boolean,
185
+ status: number,
186
+ statusText: string,
187
+ url: string,
188
+ exports: Record<string, unknown>,
189
+ ): void {
190
+ const callback = exports.__fui_on_fetch_complete;
191
+ if (typeof callback !== 'function') {
192
+ throw new Error('Worker module is missing __fui_on_fetch_complete.');
193
+ }
194
+ const payload = writeCallbackBytes(encodeTextPartsPayload([statusText, url]), 'Worker fetch completion payload');
195
+ (callback as (requestId: number, ok: boolean, status: number, payloadPtr: number, payloadLen: number) => void)(
196
+ requestId,
197
+ ok,
198
+ status,
199
+ payload.ptr,
200
+ payload.len,
201
+ );
202
+ }
203
+
204
+ function emitFetchError(
205
+ requestId: number,
206
+ message: string,
207
+ exports: Record<string, unknown>,
208
+ ): void {
209
+ const callback = exports.__fui_on_fetch_error;
210
+ if (typeof callback !== 'function') {
211
+ throw new Error('Worker module is missing __fui_on_fetch_error.');
212
+ }
213
+ const payload = writeCallbackBytes(encoder.encode(message), 'Worker fetch failure payload');
214
+ (callback as (requestId: number, payloadPtr: number, payloadLen: number) => void)(
215
+ requestId,
216
+ payload.ptr,
217
+ payload.len,
218
+ );
219
+ }
220
+
221
+ function scheduleResume(): void {
222
+ if (resumeScheduled || terminalSent || entry === null) {
223
+ return;
224
+ }
225
+ resumeScheduled = true;
226
+ const delayMs = requestedYieldDelayMs > 0 ? requestedYieldDelayMs : 0;
227
+ requestedYieldDelayMs = 0;
228
+ setTimeout(() => {
229
+ resumeScheduled = false;
230
+ if (terminalSent || entry === null) {
231
+ return;
232
+ }
233
+ runEntry();
234
+ }, delayMs);
235
+ }
236
+
237
+ function runEntry(): void {
238
+ if (entry === null || terminalSent) {
239
+ return;
240
+ }
241
+ yieldRequested = false;
242
+ entry();
243
+ if (terminalSent) {
244
+ return;
245
+ }
246
+ if (yieldRequested) {
247
+ scheduleResume();
248
+ return;
249
+ }
250
+ workerScope.postMessage({
251
+ type: 'error',
252
+ workerId: message.workerId,
253
+ text: 'Worker exited without calling Worker.complete(...), Worker.fail(...), or Worker.yield(...).',
254
+ });
255
+ terminalSent = true;
256
+ cancelAllFetchRequests();
257
+ activeWorkerId = null;
258
+ activeCancellationRequested = false;
259
+ }
260
+ try {
261
+ const response = await fetch(message.wasmUrl, {
262
+ cache: 'no-store',
263
+ credentials: 'same-origin',
264
+ });
265
+ if (!response.ok) {
266
+ throw new Error(`Failed to load worker wasm from ${message.wasmUrl}.`);
267
+ }
268
+ const bytes = await response.arrayBuffer();
269
+ const module = await WebAssembly.compile(bytes);
270
+ validateWorkerImports(module, hostServices);
271
+ const instance = await WebAssembly.instantiate(module, {
272
+ env: {
273
+ abort(_message?: number, _fileName?: number, line?: number, column?: number): never {
274
+ throw new Error(`Worker aborted at ${String(line ?? 0)}:${String(column ?? 0)}.`);
275
+ },
276
+ },
277
+ fui_host_service: createHostServiceImportModule(hostServices, {
278
+ readString: (ptr, len) => readUtf8(memory, ptr, len),
279
+ writeString: (ptr, capacity, text, context) => writeUtf8(memory, ptr, capacity, text, context),
280
+ }),
281
+ fui_worker_host: {
282
+ fui_worker_input_length(): number {
283
+ return inputBytes.length;
284
+ },
285
+ fui_worker_copy_input(ptr: number, capacity: number): number {
286
+ if (memory === null || capacity <= 0) {
287
+ return 0;
288
+ }
289
+ const copyLength = Math.min(capacity, inputBytes.length);
290
+ if (copyLength <= 0) {
291
+ return 0;
292
+ }
293
+ new Uint8Array(memory.buffer, ptr, copyLength).set(inputBytes.subarray(0, copyLength));
294
+ return copyLength;
295
+ },
296
+ fui_worker_report_progress(ptr: number, len: number): void {
297
+ if (terminalSent) {
298
+ return;
299
+ }
300
+ workerScope.postMessage({
301
+ type: 'progress',
302
+ workerId: message.workerId,
303
+ text: readUtf8(memory, ptr, len),
304
+ });
305
+ },
306
+ fui_worker_complete_string(ptr: number, len: number): void {
307
+ if (terminalSent) {
308
+ return;
309
+ }
310
+ terminalSent = true;
311
+ cancelAllFetchRequests();
312
+ activeWorkerId = null;
313
+ activeCancellationRequested = false;
314
+ workerScope.postMessage({
315
+ type: 'complete',
316
+ workerId: message.workerId,
317
+ text: readUtf8(memory, ptr, len),
318
+ });
319
+ },
320
+ fui_worker_fail(ptr: number, len: number): void {
321
+ if (terminalSent) {
322
+ return;
323
+ }
324
+ terminalSent = true;
325
+ cancelAllFetchRequests();
326
+ activeWorkerId = null;
327
+ activeCancellationRequested = false;
328
+ workerScope.postMessage({
329
+ type: 'error',
330
+ workerId: message.workerId,
331
+ text: readUtf8(memory, ptr, len),
332
+ });
333
+ },
334
+ fui_worker_is_cancelled(): number {
335
+ return readCancelFlag() ? 1 : 0;
336
+ },
337
+ fui_worker_request_yield(): void {
338
+ yieldRequested = true;
339
+ requestedYieldDelayMs = 0;
340
+ },
341
+ fui_worker_request_yield_delay(delayMs: number): void {
342
+ yieldRequested = true;
343
+ requestedYieldDelayMs = Number.isFinite(delayMs) && delayMs > 0 ? Math.floor(delayMs) : 0;
344
+ },
345
+ },
346
+ fui_fetch_host: {
347
+ fui_fetch_start(
348
+ requestId: number,
349
+ methodPtr: number,
350
+ methodLen: number,
351
+ urlPtr: number,
352
+ urlLen: number,
353
+ headersPtr: number,
354
+ headersLen: number,
355
+ bodyPtr: number,
356
+ bodyLen: number,
357
+ ): void {
358
+ const controller = new AbortController();
359
+ const method = readUtf8(memory, methodPtr, methodLen);
360
+ const url = readUtf8(memory, urlPtr, urlLen);
361
+ const headerBytes = memory === null || headersLen <= 0
362
+ ? new Uint8Array(0)
363
+ : new Uint8Array(memory.buffer.slice(headersPtr, headersPtr + headersLen));
364
+ if (headerBytes.byteLength < 4 && headersLen > 0) {
365
+ throw new Error('Worker fetch header payload was truncated.');
366
+ }
367
+ const headers = new Headers();
368
+ if (headerBytes.byteLength >= 4) {
369
+ const dataView = new DataView(headerBytes.buffer, headerBytes.byteOffset, headerBytes.byteLength);
370
+ let byteOffset = 0;
371
+ const count = dataView.getUint32(byteOffset, true);
372
+ byteOffset += 4;
373
+ const values: string[] = [];
374
+ for (let index = 0; index < count; index += 1) {
375
+ if (byteOffset + 4 > headerBytes.byteLength) {
376
+ throw new Error('Worker fetch header length was truncated.');
377
+ }
378
+ const partLen = dataView.getUint32(byteOffset, true);
379
+ byteOffset += 4;
380
+ if (byteOffset + partLen > headerBytes.byteLength) {
381
+ throw new Error('Worker fetch header value was truncated.');
382
+ }
383
+ values.push(partLen > 0 ? decoder.decode(headerBytes.subarray(byteOffset, byteOffset + partLen)) : '');
384
+ byteOffset += partLen;
385
+ }
386
+ if ((values.length & 1) != 0) {
387
+ throw new Error('Worker fetch headers were malformed.');
388
+ }
389
+ for (let index = 0; index < values.length; index += 2) {
390
+ headers.append(values[index] ?? '', values[index + 1] ?? '');
391
+ }
392
+ }
393
+ const body = memory === null || bodyLen <= 0
394
+ ? undefined
395
+ : memory.buffer.slice(bodyPtr, bodyPtr + bodyLen);
396
+ activeFetchRequests.set(requestId, controller);
397
+ void fetch(url, {
398
+ method,
399
+ headers,
400
+ body,
401
+ signal: controller.signal,
402
+ }).then((response) => {
403
+ const active = activeFetchRequests.get(requestId);
404
+ if (active === undefined || active !== controller || terminalSent) {
405
+ return;
406
+ }
407
+ activeFetchRequests.delete(requestId);
408
+ if (wasmExports === null) {
409
+ throw new Error('Worker fetch completed before wasm exports were ready.');
410
+ }
411
+ emitFetchComplete(requestId, response.ok, response.status, response.statusText, response.url, wasmExports);
412
+ }).catch((error: unknown) => {
413
+ const active = activeFetchRequests.get(requestId);
414
+ if (active === undefined || active !== controller) {
415
+ return;
416
+ }
417
+ activeFetchRequests.delete(requestId);
418
+ if (controller.signal.aborted || terminalSent) {
419
+ return;
420
+ }
421
+ if (wasmExports === null) {
422
+ throw new Error('Worker fetch failed before wasm exports were ready.');
423
+ }
424
+ emitFetchError(requestId, describeError(error), wasmExports);
425
+ });
426
+ },
427
+ fui_fetch_cancel(requestId: number): void {
428
+ const controller = activeFetchRequests.get(requestId);
429
+ if (controller === undefined) {
430
+ return;
431
+ }
432
+ activeFetchRequests.delete(requestId);
433
+ controller.abort();
434
+ },
435
+ },
436
+ });
437
+ const exports = instance.exports as Record<string, unknown> & {
438
+ memory?: WebAssembly.Memory;
439
+ __fui_worker_text_buffer?: () => number;
440
+ __fui_worker_text_buffer_size?: () => number;
441
+ };
442
+ wasmExports = exports;
443
+ if (!(exports.memory instanceof WebAssembly.Memory)) {
444
+ throw new Error('Worker module did not export memory.');
445
+ }
446
+ memory = exports.memory;
447
+ if (typeof exports.__fui_worker_text_buffer !== 'function' || typeof exports.__fui_worker_text_buffer_size !== 'function') {
448
+ throw new Error('Worker module did not export the fetch callback buffer.');
449
+ }
450
+ callbackBufferPtr = exports.__fui_worker_text_buffer();
451
+ callbackBufferSize = exports.__fui_worker_text_buffer_size();
452
+ const exportedEntry = exports[message.entryName];
453
+ if (typeof exportedEntry !== 'function') {
454
+ throw new Error(`Worker export "${message.entryName}" is missing.`);
455
+ }
456
+ entry = exportedEntry as () => void;
457
+ runEntry();
458
+ } catch (error: unknown) {
459
+ cancelAllFetchRequests();
460
+ activeWorkerId = null;
461
+ activeCancellationRequested = false;
462
+ workerScope.postMessage({
463
+ type: 'error',
464
+ workerId: message.workerId,
465
+ text: describeError(error),
466
+ });
467
+ }
468
+ }
469
+
470
+ workerScope.onmessage = (event: MessageEvent<WorkerBootstrapInboundMessage>) => {
471
+ const message = event.data;
472
+ if (message.type === 'start') {
473
+ void startWorker(message);
474
+ return;
475
+ }
476
+ if (message.type === 'cancel') {
477
+ if (activeWorkerId === message.workerId) {
478
+ activeCancellationRequested = true;
479
+ return;
480
+ }
481
+ pendingCancellationRequested = true;
482
+ }
483
+ };