@effindomv2/runtime 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 (92) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/bridge.js +4 -0
  3. package/dist/bridge.js.map +7 -0
  4. package/dist/effindom.v2.manifest.json +68 -0
  5. package/dist/fonts/NotoColorEmoji.ttf +0 -0
  6. package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
  7. package/dist/fonts/NotoSans-Bold.ttf +0 -0
  8. package/dist/fonts/NotoSans-BoldItalic.ttf +0 -0
  9. package/dist/fonts/NotoSans-Italic.ttf +0 -0
  10. package/dist/fonts/NotoSans-Regular.ttf +0 -0
  11. package/dist/fonts/NotoSansMono-Bold.ttf +0 -0
  12. package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
  13. package/dist/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  14. package/dist/harness.js +2 -0
  15. package/dist/harness.js.map +7 -0
  16. package/dist/index.html +53 -0
  17. package/dist/runtime/effindom-core-v2.wasm32-simd.JQXIaRaN0-JahfIVFiSLE49WzzCENvef_2EDEm09nJs.wasm +0 -0
  18. package/dist/runtime/effindom-core-v2.wasm32-simd.y7RzpkMARiFeRkpgiqKQsAfv4Hf17NYdpni-6aLNhMs.js.symbols +10079 -0
  19. package/dist/runtime/effindom-core-v2.wasm32-simd.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  20. package/dist/runtime/effindom-core-v2.wasm32.JSfMkp9ertJzSZxA-_xz3yacrJUhswxlwbqbJLRIuqw.wasm +0 -0
  21. package/dist/runtime/effindom-core-v2.wasm32.xNgsQv7dCwf8Uy-PfJSoRNyk9-q1OSogUwkk5g6ZBjk.js.symbols +10088 -0
  22. package/dist/runtime/effindom-core-v2.wasm32.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  23. package/dist/runtime/effindom-core-v2.wasm64-simd.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  24. package/dist/runtime/effindom-core-v2.wasm64-simd.p4P98oRu2wEWxtRRW8RHr27JhGeWvWlziZXDM_z3Nc4.js.symbols +10286 -0
  25. package/dist/runtime/effindom-core-v2.wasm64-simd.y75FYXRwhQrpaDGYbZWrohGDv0AmjTb-EjXwOjBIgnM.wasm +0 -0
  26. package/dist/runtime/effindom-core-v2.wasm64.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  27. package/dist/runtime/effindom-core-v2.wasm64.emhE1_CJs4_zXp8wiQS_5lYpUQ0OchmXgxksi0ykaBs.js.symbols +10298 -0
  28. package/dist/runtime/effindom-core-v2.wasm64.sO-Yu70cfN8Qs3a5iEp6cbFPaiOchqcMKUzryu4npNo.wasm +0 -0
  29. package/dist/runtime/effindom-ui-v2.wasm32-simd.0Mas1XD03eYvemryTioWaZOBuBA5ij7MFlTa8CgEZWs.wasm +0 -0
  30. package/dist/runtime/effindom-ui-v2.wasm32-simd.ThSDClMnSWdwf9d89JZfYor0G1Z6OxR4lOc75rNRuD4.js.symbols +1890 -0
  31. package/dist/runtime/effindom-ui-v2.wasm32-simd.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  32. package/dist/runtime/effindom-ui-v2.wasm32.H7kYg99bT9ADGh0uUvj6H9Dk1L058nVFLv_4R79IXW8.js.symbols +1900 -0
  33. package/dist/runtime/effindom-ui-v2.wasm32.tp53X7nHfG_EUq29naDyElfnqhMw2D1Tr1T-BJAYO7w.wasm +0 -0
  34. package/dist/runtime/effindom-ui-v2.wasm32.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  35. package/dist/runtime/effindom-ui-v2.wasm64-simd.86tk9Z3xIpgTOykET_8Nn9iUVJnp1AzOHW4fVQRGtQE.wasm +0 -0
  36. package/dist/runtime/effindom-ui-v2.wasm64-simd.RQaXil22Chu63-vxK9oOuX8wUY044kbo190oYIbBU4M.js.symbols +1918 -0
  37. package/dist/runtime/effindom-ui-v2.wasm64-simd.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  38. package/dist/runtime/effindom-ui-v2.wasm64.YSwpMFbr-Q1SBe0Ze8mub1u1PqsvSz3QIYuA3eaUMME.js.symbols +1924 -0
  39. package/dist/runtime/effindom-ui-v2.wasm64.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  40. package/dist/runtime/effindom-ui-v2.wasm64.ioQ9DuM6gR_EjlfRHdF8EvNPBcKCs0PQbbY9-cjTV6Y.wasm +0 -0
  41. package/dist/runtime/icudt_minimal.962CX1q0-Nbv-OqXPaub5piYTOLumUk-nEvemcvvnpw.dat +0 -0
  42. package/package.json +62 -0
  43. package/scripts/build.sh +279 -0
  44. package/scripts/build_assets.sh +51 -0
  45. package/scripts/font_assets.sh +52 -0
  46. package/scripts/generate_manifest.py +121 -0
  47. package/scripts/stage_package_assets.sh +42 -0
  48. package/src/bridge/commit-policy.ts +10 -0
  49. package/src/bridge/events/canvas-geometry.ts +78 -0
  50. package/src/bridge/events/key-router.ts +187 -0
  51. package/src/bridge/events/pointer-router.ts +619 -0
  52. package/src/bridge/events/semantic-hit-testing.ts +27 -0
  53. package/src/bridge/events.ts +54 -0
  54. package/src/bridge/find-dialog.ts +690 -0
  55. package/src/bridge/find-session.ts +158 -0
  56. package/src/bridge/font-catalog.ts +51 -0
  57. package/src/bridge/google-fonts.ts +63 -0
  58. package/src/bridge/incremental-font-packages.ts +216 -0
  59. package/src/bridge/init.ts +77 -0
  60. package/src/bridge/interaction/editor-model.ts +371 -0
  61. package/src/bridge/interaction/editor-mutations.ts +495 -0
  62. package/src/bridge/interaction/editor-session.ts +628 -0
  63. package/src/bridge/interaction/logs.ts +23 -0
  64. package/src/bridge/interaction/text-encoding.ts +51 -0
  65. package/src/bridge/interaction.ts +86 -0
  66. package/src/bridge/local-types.ts +105 -0
  67. package/src/bridge/platform.ts +68 -0
  68. package/src/bridge/pointer-move-coalescer.ts +41 -0
  69. package/src/bridge/pull-to-refresh.ts +124 -0
  70. package/src/bridge/render-loop.ts +268 -0
  71. package/src/bridge/runtime/asset-manager.ts +202 -0
  72. package/src/bridge/runtime/find-controller.ts +269 -0
  73. package/src/bridge/runtime/font-manager.ts +691 -0
  74. package/src/bridge/runtime/open-canvas-api.ts +72 -0
  75. package/src/bridge/runtime/semantic-controller.ts +133 -0
  76. package/src/bridge/runtime/text-documents.ts +234 -0
  77. package/src/bridge/runtime.ts +315 -0
  78. package/src/bridge/touch-gesture.ts +159 -0
  79. package/src/bridge/utils/assets.ts +572 -0
  80. package/src/bridge/utils/backends.ts +163 -0
  81. package/src/bridge/utils/encoding.ts +128 -0
  82. package/src/bridge/utils/fetch.ts +147 -0
  83. package/src/bridge/utils/heap.ts +118 -0
  84. package/src/bridge.ts +93 -0
  85. package/src/clipboard.ts +139 -0
  86. package/src/core-types.ts +595 -0
  87. package/src/find-on-page.ts +284 -0
  88. package/src/harness.ts +53 -0
  89. package/src/index.ts +40 -0
  90. package/src/open-canvas.ts +108 -0
  91. package/src/runtime-config.ts +96 -0
  92. package/src/semantic.ts +905 -0
@@ -0,0 +1,163 @@
1
+ import { EdBackendType, EdDeviceState } from '../../core-types';
2
+ import type { BridgeLoaderInfo, CoreModule, EdBackendType as EdBackendTypeValue } from '../../core-types';
3
+ import { normalizeBackendType, normalizeDeviceState } from './encoding';
4
+
5
+ const WEBGPU_INIT_TIMEOUT_MS = 1_500;
6
+
7
+ // TEMPORARY: keep WebGPU disabled from the bridge until resize/device-loss stability
8
+ // issues are resolved. Default to WebGL2 with software fallback.
9
+ export const DEFAULT_BACKEND_LADDER = [EdBackendType.WEBGL2, EdBackendType.CPU] as const;
10
+
11
+ export function backendTypeToRenderer(backendType: EdBackendTypeValue): BridgeLoaderInfo['activeRenderer'] {
12
+ switch (backendType) {
13
+ case EdBackendType.WEBGPU:
14
+ return 'webgpu';
15
+ case EdBackendType.WEBGL2:
16
+ return 'webgl2';
17
+ case EdBackendType.CPU:
18
+ return 'cpu';
19
+ default:
20
+ return 'none';
21
+ }
22
+ }
23
+
24
+ export function setActiveRenderer(loaderInfo: BridgeLoaderInfo, backendType: EdBackendTypeValue): void {
25
+ loaderInfo.activeRenderer = backendTypeToRenderer(backendType);
26
+ window.__bridgeLoaderInfo = loaderInfo;
27
+ }
28
+
29
+ async function waitForAnimationFrame(): Promise<void> {
30
+ await new Promise<void>((resolve) => {
31
+ requestAnimationFrame(() => {
32
+ resolve();
33
+ });
34
+ });
35
+ }
36
+
37
+ export async function waitForWebGpuInit(core: CoreModule): Promise<EdBackendTypeValue> {
38
+ const deadline = performance.now() + WEBGPU_INIT_TIMEOUT_MS;
39
+ while (performance.now() < deadline) {
40
+ const backendType = normalizeBackendType(core._ed_get_backend_type());
41
+ const deviceState = normalizeDeviceState(core._ed_get_device_state());
42
+ if (backendType === EdBackendType.WEBGPU) {
43
+ return backendType;
44
+ }
45
+ if (deviceState !== EdDeviceState.RECOVERING) {
46
+ return backendType;
47
+ }
48
+ await waitForAnimationFrame();
49
+ }
50
+ return normalizeBackendType(core._ed_get_backend_type());
51
+ }
52
+
53
+ export async function probeWebGpuAdapter(): Promise<boolean> {
54
+ const nav = navigator as Navigator & { gpu?: { requestAdapter?: () => Promise<unknown> } };
55
+ if (nav.gpu?.requestAdapter === undefined) {
56
+ return false;
57
+ }
58
+ try {
59
+ const adapter = await nav.gpu.requestAdapter();
60
+ return adapter !== null && adapter !== undefined;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ export function backendLabel(backend: EdBackendTypeValue): string {
67
+ if (backend === EdBackendType.WEBGPU) return 'WebGPU';
68
+ if (backend === EdBackendType.WEBGL2) return 'WebGL2';
69
+ if (backend === EdBackendType.CPU) return 'Software/Raster';
70
+ return 'None';
71
+ }
72
+
73
+ // Attempts to initialise exactly one backend. Returns true on success.
74
+ // Safe to call after a device loss — wraps all init calls in try/catch.
75
+ export async function tryReviveBackend(
76
+ core: CoreModule,
77
+ canvas: HTMLCanvasElement,
78
+ dpr: number,
79
+ backend: EdBackendTypeValue,
80
+ ): Promise<boolean> {
81
+ const w = canvas.width;
82
+ const h = canvas.height;
83
+ try {
84
+ if (backend === EdBackendType.WEBGPU) {
85
+ if (!await probeWebGpuAdapter()) return false;
86
+ core._ed_init(w, h, dpr);
87
+ return await waitForWebGpuInit(core) === EdBackendType.WEBGPU;
88
+ }
89
+ if (backend === EdBackendType.WEBGL2) {
90
+ core._ed_init_webgl(w, h, dpr);
91
+ return normalizeBackendType(core._ed_get_backend_type()) === EdBackendType.WEBGL2;
92
+ }
93
+ core._ed_init_sw(w, h, dpr);
94
+ return normalizeBackendType(core._ed_get_backend_type()) === EdBackendType.CPU;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ export async function initRenderer(
101
+ core: CoreModule,
102
+ canvas: HTMLCanvasElement,
103
+ dpr: number,
104
+ loaderInfo: BridgeLoaderInfo,
105
+ backendLadder: readonly EdBackendTypeValue[] = DEFAULT_BACKEND_LADDER,
106
+ ): Promise<EdBackendTypeValue> {
107
+ const physicalWidth = canvas.width;
108
+ const physicalHeight = canvas.height;
109
+ const ladder = backendLadder.length > 0 ? backendLadder : DEFAULT_BACKEND_LADDER;
110
+ const firstBackend = ladder[0] ?? EdBackendType.WEBGPU;
111
+ let webGpuAttempted = false;
112
+ let webGl2Attempted = false;
113
+
114
+ for (const backend of ladder) {
115
+ if (backend === EdBackendType.WEBGPU) {
116
+ // Probe the adapter before calling ed_init. On platforms like Ubuntu where
117
+ // navigator.gpu exists but has no adapter (Vulkan blocklist, no GPU, etc.),
118
+ // requestAdapter() returns null. Calling ed_init without checking locks the
119
+ // canvas to a WebGPU context (even on failure), preventing WebGL2 init.
120
+ if (!await probeWebGpuAdapter()) {
121
+ continue;
122
+ }
123
+ webGpuAttempted = true;
124
+ core._ed_init(physicalWidth, physicalHeight, dpr);
125
+ const resolvedBackend = await waitForWebGpuInit(core);
126
+ if (resolvedBackend === EdBackendType.WEBGPU) {
127
+ setActiveRenderer(loaderInfo, resolvedBackend);
128
+ return resolvedBackend;
129
+ }
130
+ continue;
131
+ }
132
+
133
+ if (backend === EdBackendType.WEBGL2) {
134
+ webGl2Attempted = true;
135
+ core._ed_init_webgl(physicalWidth, physicalHeight, dpr);
136
+ if (normalizeBackendType(core._ed_get_backend_type()) === EdBackendType.WEBGL2) {
137
+ if (webGpuAttempted || firstBackend === EdBackendType.WEBGPU) {
138
+ console.warn('RENDERER FALLBACK: WebGPU failed to initialize or unavailable! Fell back to WebGL2');
139
+ }
140
+ setActiveRenderer(loaderInfo, EdBackendType.WEBGL2);
141
+ return EdBackendType.WEBGL2;
142
+ }
143
+ continue;
144
+ }
145
+
146
+ core._ed_init_sw(physicalWidth, physicalHeight, dpr);
147
+ if (normalizeBackendType(core._ed_get_backend_type()) === EdBackendType.CPU) {
148
+ const triedBackends = [
149
+ webGpuAttempted ? 'WebGPU' : null,
150
+ webGl2Attempted ? 'WebGL2' : null,
151
+ ].filter(Boolean).join(' and ');
152
+ const reason = triedBackends.length > 0
153
+ ? `${triedBackends} failed to initialize or unavailable!`
154
+ : 'No GPU backend available.';
155
+ console.error(`RENDERER FALLBACK: ${reason} Fell back to Software/Raster - performance will be painfully slow`);
156
+ setActiveRenderer(loaderInfo, EdBackendType.CPU);
157
+ return EdBackendType.CPU;
158
+ }
159
+ }
160
+
161
+ setActiveRenderer(loaderInfo, EdBackendType.NONE);
162
+ throw new Error('Failed to initialize any renderer backend.');
163
+ }
@@ -0,0 +1,128 @@
1
+ import { EdBackendType, EdDeviceState } from '../../core-types';
2
+ import type {
3
+ CoreModule,
4
+ EdBackendType as EdBackendTypeValue,
5
+ EdDeviceState as EdDeviceStateValue,
6
+ UiModule,
7
+ WasmHandleLike,
8
+ } from '../../core-types';
9
+
10
+ export type WasmModuleMemoryView = Pick<UiModule | CoreModule, 'usesMemory64'>;
11
+
12
+ function extractHandlePrimitive(handle: WasmHandleLike): bigint | number | string {
13
+ if (typeof handle === 'bigint' || typeof handle === 'number' || typeof handle === 'string') {
14
+ return handle;
15
+ }
16
+ const symbolPrimitive = (handle as { [Symbol.toPrimitive]?: (hint: string) => unknown })[Symbol.toPrimitive]?.('default');
17
+ if (
18
+ typeof symbolPrimitive === 'bigint' ||
19
+ typeof symbolPrimitive === 'number' ||
20
+ typeof symbolPrimitive === 'string'
21
+ ) {
22
+ return symbolPrimitive;
23
+ }
24
+ const primitive = handle.valueOf();
25
+ if (typeof primitive === 'bigint' || typeof primitive === 'number' || typeof primitive === 'string') {
26
+ return primitive;
27
+ }
28
+ const stringified = handle.toString();
29
+ if (typeof stringified === 'string') {
30
+ return stringified;
31
+ }
32
+ throw new TypeError(`Cannot convert ${String(handle)} to BigInt.`);
33
+ }
34
+
35
+ export function handleToBigInt(handle: WasmHandleLike): bigint {
36
+ const primitive = extractHandlePrimitive(handle);
37
+ if (typeof primitive === 'bigint') {
38
+ return primitive;
39
+ }
40
+ if (typeof primitive === 'number') {
41
+ if (!Number.isInteger(primitive)) {
42
+ throw new TypeError(`Cannot convert non-integer handle ${String(primitive)} to BigInt.`);
43
+ }
44
+ return BigInt(primitive);
45
+ }
46
+ if (typeof primitive === 'string') {
47
+ return BigInt(primitive);
48
+ }
49
+ throw new TypeError(`Cannot convert ${String(handle)} to BigInt.`);
50
+ }
51
+
52
+ export function handleToString(handle: WasmHandleLike): string {
53
+ return handleToBigInt(handle).toString();
54
+ }
55
+
56
+ export function pointerToHeapOffset(pointer: WasmHandleLike): number {
57
+ if (typeof pointer === 'number') {
58
+ if (!Number.isInteger(pointer)) {
59
+ throw new TypeError(`Cannot convert non-integer pointer ${String(pointer)} to a heap offset.`);
60
+ }
61
+ return pointer;
62
+ }
63
+ const value = handleToBigInt(pointer);
64
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
65
+ throw new RangeError(`Pointer ${value.toString()} exceeds JavaScript heap offset precision.`);
66
+ }
67
+ return Number(value);
68
+ }
69
+
70
+ export function normalizePointerForWasm(
71
+ module: WasmModuleMemoryView,
72
+ pointer: WasmHandleLike,
73
+ ): number | bigint {
74
+ return module.usesMemory64 === true ? handleToBigInt(pointer) : pointerToHeapOffset(pointer);
75
+ }
76
+
77
+ export function toHeapPointer(
78
+ module: WasmModuleMemoryView,
79
+ pointer: WasmHandleLike,
80
+ ): { readonly ptr: number | bigint; readonly offset: number } {
81
+ const ptr = normalizePointerForWasm(module, pointer);
82
+ return {
83
+ ptr,
84
+ offset: pointerToHeapOffset(ptr),
85
+ };
86
+ }
87
+
88
+ export function normalizeBackendType(value: number): EdBackendTypeValue {
89
+ switch (value) {
90
+ case EdBackendType.WEBGPU:
91
+ case EdBackendType.WEBGL2:
92
+ case EdBackendType.CPU:
93
+ return value;
94
+ default:
95
+ return EdBackendType.NONE;
96
+ }
97
+ }
98
+
99
+ export function normalizeDeviceState(value: number): EdDeviceStateValue {
100
+ switch (value) {
101
+ case EdDeviceState.LOST:
102
+ case EdDeviceState.RECOVERING:
103
+ return value;
104
+ default:
105
+ return EdDeviceState.OK;
106
+ }
107
+ }
108
+
109
+ type ModifierSource =
110
+ | Pick<KeyboardEvent, 'shiftKey' | 'ctrlKey' | 'altKey' | 'metaKey'>
111
+ | Pick<PointerEvent, 'shiftKey' | 'ctrlKey' | 'altKey' | 'metaKey'>;
112
+
113
+ export function computeModifiers(event: ModifierSource): number {
114
+ let modifiers = 0;
115
+ if (event.shiftKey) {
116
+ modifiers |= 1 << 0;
117
+ }
118
+ if (event.ctrlKey) {
119
+ modifiers |= 1 << 1;
120
+ }
121
+ if (event.altKey) {
122
+ modifiers |= 1 << 2;
123
+ }
124
+ if (event.metaKey) {
125
+ modifiers |= 1 << 3;
126
+ }
127
+ return modifiers;
128
+ }
@@ -0,0 +1,147 @@
1
+ export const ASSET_FETCH_ATTEMPTS = 4;
2
+ const ASSET_RETRY_DELAY_MS = 100;
3
+ const scriptSourceCache = new Map<string, Promise<string>>();
4
+
5
+ function delay(ms: number): Promise<void> {
6
+ return new Promise<void>((resolve) => {
7
+ window.setTimeout(resolve, ms);
8
+ });
9
+ }
10
+
11
+ export function resolveAssetUrl(url: string): string {
12
+ return new URL(url, document.baseURI).toString();
13
+ }
14
+
15
+ export function normalizeFetchIntegrity(integrity: string | null | undefined): string | null {
16
+ if (integrity === null || integrity === undefined || integrity.length === 0) {
17
+ return null;
18
+ }
19
+ const value = integrity;
20
+ if (!value.startsWith('sha256-')) {
21
+ return value;
22
+ }
23
+ let digest = value.slice(7).replace(/-/g, '+').replace(/_/g, '/');
24
+ while ((digest.length % 4) !== 0) {
25
+ digest += '=';
26
+ }
27
+ return `sha256-${digest}`;
28
+ }
29
+
30
+ export function bytesToBase64(bytes: Uint8Array): string {
31
+ let binary = '';
32
+ for (let index = 0; index < bytes.length; index += 0x8000) {
33
+ const chunk = bytes.subarray(index, Math.min(index + 0x8000, bytes.length));
34
+ binary += String.fromCharCode(...chunk);
35
+ }
36
+ return window.btoa(binary);
37
+ }
38
+
39
+ export async function verifyFetchedIntegrity(assetUrl: string, buffer: ArrayBuffer, integrity: string | null | undefined): Promise<ArrayBuffer> {
40
+ const normalizedIntegrity = normalizeFetchIntegrity(integrity);
41
+ if (normalizedIntegrity === null) {
42
+ return buffer;
43
+ }
44
+ const digestBuffer = await globalThis.crypto.subtle.digest('SHA-256', buffer);
45
+ const actualIntegrity = `sha256-${bytesToBase64(new Uint8Array(digestBuffer))}`;
46
+ if (actualIntegrity !== normalizedIntegrity) {
47
+ throw new Error(`Integrity mismatch for ${assetUrl}`);
48
+ }
49
+ return buffer;
50
+ }
51
+
52
+ export function buildFetchInit(integrity: string | null | undefined, cache: RequestCache = 'force-cache'): RequestInit {
53
+ const fetchIntegrity = normalizeFetchIntegrity(integrity);
54
+ const init: RequestInit = {
55
+ credentials: 'same-origin',
56
+ cache,
57
+ };
58
+ if (fetchIntegrity !== null) {
59
+ init.integrity = fetchIntegrity;
60
+ }
61
+ return init;
62
+ }
63
+
64
+ export async function fetchWithRetry<T>(
65
+ url: string,
66
+ attempts: number,
67
+ read: (response: Response) => T | Promise<T>,
68
+ init?: RequestInit,
69
+ ): Promise<T> {
70
+ let lastError: unknown = null;
71
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
72
+ try {
73
+ const response = await fetch(url, init);
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch ${url}: ${String(response.status)}`);
76
+ }
77
+ return await read(response);
78
+ } catch (error: unknown) {
79
+ lastError = error;
80
+ if (attempt === attempts) {
81
+ break;
82
+ }
83
+ await delay(ASSET_RETRY_DELAY_MS * attempt);
84
+ }
85
+ }
86
+ throw lastError instanceof Error ? lastError : new Error(`Failed to fetch ${url}`);
87
+ }
88
+
89
+ export async function fetchBinaryAsset(url: string, integrity: string | null | undefined): Promise<Uint8Array> {
90
+ const assetUrl = resolveAssetUrl(url);
91
+ const buffer = await fetchWithRetry<ArrayBuffer>(
92
+ assetUrl,
93
+ ASSET_FETCH_ATTEMPTS,
94
+ async (response) => await response.arrayBuffer(),
95
+ buildFetchInit(integrity),
96
+ );
97
+ return new Uint8Array(await verifyFetchedIntegrity(assetUrl, buffer, integrity));
98
+ }
99
+
100
+ export async function fetchResponseWithRetry(url: string, integrity: string | null | undefined): Promise<Response> {
101
+ const assetUrl = resolveAssetUrl(url);
102
+ return await fetchWithRetry<Response>(
103
+ assetUrl,
104
+ ASSET_FETCH_ATTEMPTS,
105
+ (response) => response,
106
+ buildFetchInit(integrity),
107
+ );
108
+ }
109
+
110
+ export async function loadScriptResource(scriptUrl: string, integrity: string | null | undefined): Promise<void> {
111
+ const absoluteUrl = resolveAssetUrl(scriptUrl);
112
+ const sourceText = await fetchScriptSource(absoluteUrl, integrity);
113
+ const blobSource = sourceText.includes('//# sourceURL=')
114
+ ? sourceText
115
+ : `${sourceText}\n//# sourceURL=${absoluteUrl.replace(/\s/g, '%20')}`;
116
+ const blobUrl = URL.createObjectURL(new Blob([blobSource], { type: 'application/javascript' }));
117
+ try {
118
+ await new Promise<void>((resolve, reject) => {
119
+ const script = document.createElement('script');
120
+ script.src = blobUrl;
121
+ script.async = true;
122
+ script.addEventListener('load', () => {
123
+ script.remove();
124
+ resolve();
125
+ });
126
+ script.addEventListener('error', () => {
127
+ script.remove();
128
+ reject(new Error(`Failed to execute ${absoluteUrl}`));
129
+ });
130
+ document.head.appendChild(script);
131
+ });
132
+ } finally {
133
+ URL.revokeObjectURL(blobUrl);
134
+ }
135
+ }
136
+
137
+ export async function fetchScriptSource(scriptUrl: string, integrity: string | null | undefined): Promise<string> {
138
+ const absoluteUrl = resolveAssetUrl(scriptUrl);
139
+ const cacheKey = `${absoluteUrl}::${integrity ?? ''}`;
140
+ let sourcePromise = scriptSourceCache.get(cacheKey);
141
+ if (sourcePromise === undefined) {
142
+ sourcePromise = fetchBinaryAsset(absoluteUrl, integrity).then((scriptBytes) =>
143
+ new TextDecoder('utf-8').decode(scriptBytes));
144
+ scriptSourceCache.set(cacheKey, sourcePromise);
145
+ }
146
+ return await sourcePromise;
147
+ }
@@ -0,0 +1,118 @@
1
+ import type { CoreModule, UiModule, WasmHandleLike } from '../../core-types';
2
+ import { normalizePointerForWasm, pointerToHeapOffset } from './encoding';
3
+
4
+ const textEncoder = new TextEncoder();
5
+
6
+ export function writeUtf8ToHeap(
7
+ module: Pick<UiModule | CoreModule, 'HEAPU8' | '_malloc' | '_free' | 'refreshHeapViews' | 'usesMemory64'>,
8
+ text: string,
9
+ ): { readonly ptr: WasmHandleLike; readonly offset: number; readonly len: number; dispose(): void } {
10
+ const bytes = textEncoder.encode(text);
11
+ const ptr = normalizePointerForWasm(module, bytes.byteLength === 0 ? 0 : module._malloc(bytes.byteLength));
12
+ const offset = pointerToHeapOffset(ptr);
13
+ module.refreshHeapViews?.();
14
+ if (bytes.byteLength > 0 && offset === 0) {
15
+ throw new Error('WASM string malloc failed.');
16
+ }
17
+ if (bytes.byteLength > 0) {
18
+ module.HEAPU8.set(bytes, offset);
19
+ }
20
+ return {
21
+ ptr,
22
+ offset,
23
+ len: bytes.byteLength,
24
+ dispose: () => {
25
+ if (offset !== 0) {
26
+ module._free(ptr);
27
+ }
28
+ },
29
+ };
30
+ }
31
+
32
+ export function writeBytesToHeap(
33
+ module: Pick<UiModule | CoreModule, 'HEAPU8' | '_malloc' | '_free' | 'refreshHeapViews' | 'usesMemory64'>,
34
+ bytes: Uint8Array,
35
+ ): { readonly ptr: WasmHandleLike; readonly offset: number; readonly len: number; dispose(): void } {
36
+ const ptr = normalizePointerForWasm(module, bytes.byteLength === 0 ? 0 : module._malloc(bytes.byteLength));
37
+ const offset = pointerToHeapOffset(ptr);
38
+ module.refreshHeapViews?.();
39
+ if (bytes.byteLength > 0 && offset === 0) {
40
+ throw new Error('WASM bytes malloc failed.');
41
+ }
42
+ if (bytes.byteLength > 0) {
43
+ module.HEAPU8.set(bytes, offset);
44
+ }
45
+ return {
46
+ ptr,
47
+ offset,
48
+ len: bytes.byteLength,
49
+ dispose: () => {
50
+ if (offset !== 0) {
51
+ module._free(ptr);
52
+ }
53
+ },
54
+ };
55
+ }
56
+
57
+ export function extractCommandBuffer(ui: UiModule): Uint32Array {
58
+ const lengthPtr = normalizePointerForWasm(ui, ui._malloc(4));
59
+ const lengthOffset = pointerToHeapOffset(lengthPtr);
60
+ if (lengthOffset === 0) {
61
+ throw new Error('ui length malloc failed.');
62
+ }
63
+
64
+ try {
65
+ const bufferPtr = ui._ui_get_command_buffer(lengthPtr);
66
+ ui.refreshHeapViews?.();
67
+ const wordCount = ui.HEAPU32[lengthOffset >>> 2] ?? 0;
68
+ const bufferOffset = pointerToHeapOffset(normalizePointerForWasm(ui, bufferPtr));
69
+ if (bufferOffset === 0 || wordCount === 0) {
70
+ return new Uint32Array();
71
+ }
72
+ const wordOffset = bufferOffset >>> 2;
73
+ return ui.HEAPU32.slice(wordOffset, wordOffset + wordCount);
74
+ } finally {
75
+ ui._free(lengthPtr);
76
+ }
77
+ }
78
+
79
+ export function extractSemanticBuffer(ui: UiModule): Uint32Array {
80
+ const lengthPtr = normalizePointerForWasm(ui, ui._malloc(4));
81
+ const lengthOffset = pointerToHeapOffset(lengthPtr);
82
+ if (lengthOffset === 0) {
83
+ throw new Error('ui semantic length malloc failed.');
84
+ }
85
+
86
+ try {
87
+ const bufferPtr = ui._ui_get_semantic_buffer(lengthPtr);
88
+ ui.refreshHeapViews?.();
89
+ const wordCount = ui.HEAPU32[lengthOffset >>> 2] ?? 0;
90
+ const bufferOffset = pointerToHeapOffset(normalizePointerForWasm(ui, bufferPtr));
91
+ if (bufferOffset === 0 || wordCount === 0) {
92
+ return new Uint32Array();
93
+ }
94
+ const wordOffset = bufferOffset >>> 2;
95
+ return ui.HEAPU32.slice(wordOffset, wordOffset + wordCount);
96
+ } finally {
97
+ ui._free(lengthPtr);
98
+ }
99
+ }
100
+
101
+ export function executeCommandBuffer(core: CoreModule, words: Uint32Array): void {
102
+ if (words.length === 0) {
103
+ return;
104
+ }
105
+ const ptr = normalizePointerForWasm(core, core._malloc(words.byteLength));
106
+ const offset = pointerToHeapOffset(ptr);
107
+ core.refreshHeapViews?.();
108
+ if (offset === 0) {
109
+ throw new Error('core command malloc failed.');
110
+ }
111
+
112
+ try {
113
+ core.HEAPU32.set(words, offset >>> 2);
114
+ core._ed_execute_command_buffer(ptr, words.length);
115
+ } finally {
116
+ core._free(ptr);
117
+ }
118
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type { BridgeRuntime, BridgeState } from './core-types';
2
+ import { createBridgeSession, type BridgeSession } from './bridge/init';
3
+ import { installCallbacks } from './bridge/interaction';
4
+ import { prepareRuntimeAssets } from './bridge/utils/assets';
5
+ import {
6
+ handleToBigInt,
7
+ handleToString,
8
+ normalizePointerForWasm,
9
+ pointerToHeapOffset,
10
+ toHeapPointer,
11
+ } from './bridge/utils/encoding';
12
+
13
+ let currentRuntime: BridgeRuntime | null = null;
14
+ let currentSession: BridgeSession | null = null;
15
+ let bridgeReadyPromise: Promise<BridgeRuntime> | null = null;
16
+ let sessionChain: Promise<void> = Promise.resolve();
17
+
18
+ const runtimeRef: { current: BridgeRuntime | null } = { current: null };
19
+ const interactionState = installCallbacks(runtimeRef);
20
+ const preparedAssetsPromise = prepareRuntimeAssets();
21
+
22
+ async function bootRuntimeSession(): Promise<BridgeRuntime> {
23
+ currentRuntime?.resetLogs();
24
+ currentSession?.destroy();
25
+ currentSession = null;
26
+
27
+ const preparedAssets = await preparedAssetsPromise;
28
+ const session = await createBridgeSession({
29
+ interactionState,
30
+ preparedAssets,
31
+ runtimeRef,
32
+ });
33
+ currentSession = session;
34
+ currentRuntime = session.runtime;
35
+ window.__bridgeReady = true;
36
+ window.__bridgeDebug = {
37
+ forceDeviceLost() {
38
+ session.runtime.core._ed_debug_simulate_device_lost?.();
39
+ session.runtime.requestFrame();
40
+ },
41
+ };
42
+ delete window.__bridgeError;
43
+ return session.runtime;
44
+ }
45
+
46
+ function queueRuntimeBoot(): Promise<BridgeRuntime> {
47
+ sessionChain = sessionChain
48
+ .catch(() => undefined)
49
+ .then(async () => {
50
+ await bootRuntimeSession();
51
+ });
52
+ return sessionChain.then(() => {
53
+ if (currentRuntime === null) {
54
+ throw new Error('Bridge runtime failed to boot.');
55
+ }
56
+ return currentRuntime;
57
+ });
58
+ }
59
+
60
+ bridgeReadyPromise = queueRuntimeBoot().catch((error: unknown) => {
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ window.__bridgeReady = false;
63
+ window.__bridgeError = message;
64
+ throw error;
65
+ });
66
+
67
+ const bridgeState: BridgeState = {
68
+ get ready(): Promise<BridgeRuntime> {
69
+ return bridgeReadyPromise ?? Promise.reject(new Error('Bridge runtime is not booting.'));
70
+ },
71
+ getRuntime: () => currentRuntime,
72
+ recreateRuntime: async () => {
73
+ bridgeReadyPromise = queueRuntimeBoot();
74
+ return await bridgeReadyPromise;
75
+ },
76
+ resetLogs: () => {
77
+ if (currentRuntime !== null) {
78
+ currentRuntime.resetLogs();
79
+ }
80
+ },
81
+ handleToBigInt,
82
+ handleToString,
83
+ pointerToHeapOffset,
84
+ normalizePointerForWasm,
85
+ toHeapPointer,
86
+ };
87
+
88
+ window.__bridgeReady = false;
89
+ window.EffinDomBrowserBridge = bridgeState;
90
+
91
+ void bridgeReadyPromise.catch(() => undefined);
92
+
93
+ export {};