sparkbun 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 (131) hide show
  1. package/bin/sparkbun.cjs +18 -0
  2. package/dist-linux-arm64/bsdiff +0 -0
  3. package/dist-linux-arm64/bspatch +0 -0
  4. package/dist-linux-arm64/libElectrobunCore.so +0 -0
  5. package/dist-linux-arm64/libNativeWrapper.so +0 -0
  6. package/dist-linux-arm64/libasar.so +0 -0
  7. package/dist-linux-x64/bsdiff +0 -0
  8. package/dist-linux-x64/bspatch +0 -0
  9. package/dist-linux-x64/libElectrobunCore.so +0 -0
  10. package/dist-linux-x64/libNativeWrapper.so +0 -0
  11. package/dist-linux-x64/libasar.so +0 -0
  12. package/dist-macos-arm64/bsdiff +0 -0
  13. package/dist-macos-arm64/bspatch +0 -0
  14. package/dist-macos-arm64/libElectrobunCore.dylib +0 -0
  15. package/dist-macos-arm64/libNativeWrapper.dylib +0 -0
  16. package/dist-macos-arm64/libasar.dylib +0 -0
  17. package/dist-macos-arm64/libwebgpu_dawn.dylib +0 -0
  18. package/dist-macos-arm64/preload-full.js +885 -0
  19. package/dist-macos-arm64/preload-sandboxed.js +111 -0
  20. package/dist-macos-arm64/process_helper +0 -0
  21. package/dist-win-x64/ElectrobunCore.dll +0 -0
  22. package/dist-win-x64/WebView2Loader.dll +0 -0
  23. package/dist-win-x64/bsdiff.exe +0 -0
  24. package/dist-win-x64/bspatch.exe +0 -0
  25. package/dist-win-x64/libNativeWrapper.dll +0 -0
  26. package/dist-win-x64/zig-asar/arm64/libasar.dll +0 -0
  27. package/dist-win-x64/zig-asar/x64/libasar.dll +0 -0
  28. package/package.json +47 -0
  29. package/scripts/build-and-upload-artifacts.js +207 -0
  30. package/scripts/gen-webgpu-ffi.mjs +162 -0
  31. package/scripts/install-windows-deps.ps1 +80 -0
  32. package/scripts/package-release.js +237 -0
  33. package/scripts/push-version.js +84 -0
  34. package/scripts/update-bun-version.ts +122 -0
  35. package/scripts/update-cef-version.ts +145 -0
  36. package/src/browser/builtinrpcSchema.ts +19 -0
  37. package/src/browser/global.d.ts +36 -0
  38. package/src/browser/index.ts +234 -0
  39. package/src/browser/webviewtag.ts +88 -0
  40. package/src/browser/wgputag.ts +48 -0
  41. package/src/bun/SparkBunConfig.ts +497 -0
  42. package/src/bun/__tests__/ffi-contract.test.ts +105 -0
  43. package/src/bun/core/ApplicationMenu.ts +70 -0
  44. package/src/bun/core/BrowserView.ts +416 -0
  45. package/src/bun/core/BrowserWindow.ts +396 -0
  46. package/src/bun/core/BuildConfig.ts +71 -0
  47. package/src/bun/core/ContextMenu.ts +75 -0
  48. package/src/bun/core/GpuWindow.ts +289 -0
  49. package/src/bun/core/Paths.ts +5 -0
  50. package/src/bun/core/Socket.ts +22 -0
  51. package/src/bun/core/Tray.ts +197 -0
  52. package/src/bun/core/Updater.ts +1131 -0
  53. package/src/bun/core/Utils.ts +487 -0
  54. package/src/bun/core/WGPUView.ts +167 -0
  55. package/src/bun/core/menuRoles.ts +181 -0
  56. package/src/bun/events/ApplicationEvents.ts +22 -0
  57. package/src/bun/events/event.ts +27 -0
  58. package/src/bun/events/eventEmitter.ts +45 -0
  59. package/src/bun/events/trayEvents.ts +11 -0
  60. package/src/bun/events/webviewEvents.ts +39 -0
  61. package/src/bun/events/windowEvents.ts +23 -0
  62. package/src/bun/index.ts +120 -0
  63. package/src/bun/preload/.generated/compiled.ts +2 -0
  64. package/src/bun/preload/build.ts +65 -0
  65. package/src/bun/preload/dragRegions.ts +41 -0
  66. package/src/bun/preload/encryption.ts +86 -0
  67. package/src/bun/preload/events.ts +171 -0
  68. package/src/bun/preload/globals.d.ts +45 -0
  69. package/src/bun/preload/index-sandboxed.ts +28 -0
  70. package/src/bun/preload/index.ts +77 -0
  71. package/src/bun/preload/internalRpc.ts +80 -0
  72. package/src/bun/preload/overlaySync.ts +107 -0
  73. package/src/bun/preload/webviewTag.ts +451 -0
  74. package/src/bun/preload/wgpuTag.ts +246 -0
  75. package/src/bun/proc/linux.md +43 -0
  76. package/src/bun/proc/native.ts +3253 -0
  77. package/src/bun/webGPU.ts +346 -0
  78. package/src/bun/webgpuAdapter.ts +3011 -0
  79. package/src/cli/bun.lockb +0 -0
  80. package/src/cli/index.ts +4653 -0
  81. package/src/cli/package-lock.json +81 -0
  82. package/src/cli/package.json +11 -0
  83. package/src/cli/templates/embedded.ts +2 -0
  84. package/src/core/build.zig +16 -0
  85. package/src/core/main.zig +3378 -0
  86. package/src/extractor/build.zig +22 -0
  87. package/src/installer/installer-template.ts +216 -0
  88. package/src/launcher/main.ts +221 -0
  89. package/src/native/build/libNativeWrapper.so +0 -0
  90. package/src/native/linux/build/nativeWrapper.o +0 -0
  91. package/src/native/linux/cef_loader.cpp +110 -0
  92. package/src/native/linux/cef_loader.h +28 -0
  93. package/src/native/linux/cef_process_helper_linux.cpp +160 -0
  94. package/src/native/linux/nativeWrapper.cpp +11768 -0
  95. package/src/native/macos/cef_process_helper_mac.cc +160 -0
  96. package/src/native/macos/nativeWrapper.mm +9172 -0
  97. package/src/native/shared/accelerator_parser.h +72 -0
  98. package/src/native/shared/app_paths.h +110 -0
  99. package/src/native/shared/asar.h +35 -0
  100. package/src/native/shared/cache_migration.h +244 -0
  101. package/src/native/shared/callbacks.h +57 -0
  102. package/src/native/shared/cef_response_filter.h +189 -0
  103. package/src/native/shared/chromium_flags.h +181 -0
  104. package/src/native/shared/config.h +66 -0
  105. package/src/native/shared/download_event.h +197 -0
  106. package/src/native/shared/ffi_helpers.h +139 -0
  107. package/src/native/shared/glob_match.h +59 -0
  108. package/src/native/shared/json_menu_parser.h +223 -0
  109. package/src/native/shared/mime_types.h +101 -0
  110. package/src/native/shared/navigation_rules.h +98 -0
  111. package/src/native/shared/partition_context.h +137 -0
  112. package/src/native/shared/pending_resize_queue.h +45 -0
  113. package/src/native/shared/permissions.h +118 -0
  114. package/src/native/shared/permissions_cef.h +74 -0
  115. package/src/native/shared/preload_script.h +71 -0
  116. package/src/native/shared/shutdown_guard.h +134 -0
  117. package/src/native/shared/thread_safe_map.h +138 -0
  118. package/src/native/shared/webview_storage.h +91 -0
  119. package/src/native/win/cef_process_helper_win.cpp +143 -0
  120. package/src/native/win/dcomp_compositor.h +352 -0
  121. package/src/native/win/nativeWrapper.cpp +12434 -0
  122. package/src/npmbin/index.js +34 -0
  123. package/src/shared/bun-version.ts +3 -0
  124. package/src/shared/cef-version.ts +5 -0
  125. package/src/shared/naming.test.ts +327 -0
  126. package/src/shared/naming.ts +188 -0
  127. package/src/shared/platform.ts +48 -0
  128. package/src/shared/rpc.ts +541 -0
  129. package/src/shared/sparkbun-version.ts +2 -0
  130. package/src/types/three.d.ts +1 -0
  131. package/tsconfig.json +31 -0
@@ -0,0 +1,86 @@
1
+ // Encryption/Decryption for secure RPC
2
+ // Uses per-webview secret key set in window.__sparkbunSecretKeyBytes
3
+
4
+ import "./globals.d.ts";
5
+
6
+ function base64ToUint8Array(base64: string): Uint8Array {
7
+ return new Uint8Array(
8
+ atob(base64)
9
+ .split("")
10
+ .map((char) => char.charCodeAt(0)),
11
+ );
12
+ }
13
+
14
+ function uint8ArrayToBase64(uint8Array: Uint8Array): string {
15
+ let binary = "";
16
+ for (let i = 0; i < uint8Array.length; i++) {
17
+ binary += String.fromCharCode(uint8Array[i]!);
18
+ }
19
+ return btoa(binary);
20
+ }
21
+
22
+ async function generateKeyFromBytes(rawKey: Uint8Array): Promise<CryptoKey> {
23
+ return await window.crypto.subtle.importKey(
24
+ "raw",
25
+ rawKey as unknown as ArrayBuffer,
26
+ { name: "AES-GCM" },
27
+ true,
28
+ ["encrypt", "decrypt"],
29
+ );
30
+ }
31
+
32
+ export async function initEncryption(): Promise<void> {
33
+ const secretKey = await generateKeyFromBytes(
34
+ new Uint8Array(window.__sparkbunSecretKeyBytes),
35
+ );
36
+
37
+ const encryptString = async (
38
+ plaintext: string,
39
+ ): Promise<{ encryptedData: string; iv: string; tag: string }> => {
40
+ const encoder = new TextEncoder();
41
+ const encodedText = encoder.encode(plaintext);
42
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
43
+ const encryptedBuffer = await window.crypto.subtle.encrypt(
44
+ { name: "AES-GCM", iv },
45
+ secretKey,
46
+ encodedText,
47
+ );
48
+
49
+ // Split the tag (last 16 bytes) from the ciphertext
50
+ const encryptedData = new Uint8Array(encryptedBuffer.slice(0, -16));
51
+ const tag = new Uint8Array(encryptedBuffer.slice(-16));
52
+
53
+ return {
54
+ encryptedData: uint8ArrayToBase64(encryptedData),
55
+ iv: uint8ArrayToBase64(iv),
56
+ tag: uint8ArrayToBase64(tag),
57
+ };
58
+ };
59
+
60
+ const decryptString = async (
61
+ encryptedDataB64: string,
62
+ ivB64: string,
63
+ tagB64: string,
64
+ ): Promise<string> => {
65
+ const encryptedData = base64ToUint8Array(encryptedDataB64);
66
+ const iv = base64ToUint8Array(ivB64);
67
+ const tag = base64ToUint8Array(tagB64);
68
+
69
+ // Combine encrypted data and tag to match the format expected by SubtleCrypto
70
+ const combinedData = new Uint8Array(encryptedData.length + tag.length);
71
+ combinedData.set(encryptedData);
72
+ combinedData.set(tag, encryptedData.length);
73
+
74
+ const decryptedBuffer = await window.crypto.subtle.decrypt(
75
+ { name: "AES-GCM", iv: iv as unknown as ArrayBuffer },
76
+ secretKey,
77
+ combinedData as unknown as ArrayBuffer,
78
+ );
79
+
80
+ const decoder = new TextDecoder();
81
+ return decoder.decode(decryptedBuffer);
82
+ };
83
+
84
+ window.__sparkbun_encrypt = encryptString;
85
+ window.__sparkbun_decrypt = decryptString;
86
+ }
@@ -0,0 +1,171 @@
1
+ // Shared Event Emission for webview lifecycle events
2
+ // Uses __sparkbunEventBridge which is available on ALL webviews (including sandboxed)
3
+ // Falls back to __sparkbunInternalBridge for backwards compatibility until native code
4
+ // is updated to include the eventBridge handler
5
+ // This is a one-way channel for emitting events to native/bun - no RPC capability
6
+
7
+ import "./globals.d.ts";
8
+
9
+ // Emit a webview event to native code
10
+ export function emitWebviewEvent(eventName: string, detail: string) {
11
+ // setTimeout works around a race condition with Bun FFI
12
+ setTimeout(() => {
13
+ // Prefer eventBridge (available on all webviews), fall back to internalBridge
14
+ // (for backwards compatibility until native code adds eventBridge handler)
15
+ const bridge =
16
+ window.__sparkbunEventBridge || window.__sparkbunInternalBridge;
17
+ bridge?.postMessage(
18
+ JSON.stringify({
19
+ id: "webviewEvent",
20
+ type: "message",
21
+ payload: {
22
+ id: window.__sparkbunWebviewId,
23
+ eventName,
24
+ detail,
25
+ },
26
+ }),
27
+ );
28
+ });
29
+ }
30
+
31
+ // Set up standard lifecycle event listeners
32
+ export function initLifecycleEvents() {
33
+ // Emit dom-ready when page loads (top-level window only)
34
+ window.addEventListener("load", () => {
35
+ if (window === window.top) {
36
+ emitWebviewEvent("dom-ready", document.location.href);
37
+ }
38
+ });
39
+
40
+ // Track in-page navigation
41
+ window.addEventListener("popstate", () => {
42
+ emitWebviewEvent("did-navigate-in-page", window.location.href);
43
+ });
44
+
45
+ window.addEventListener("hashchange", () => {
46
+ emitWebviewEvent("did-navigate-in-page", window.location.href);
47
+ });
48
+ }
49
+
50
+ // Track cmd key state for SPA navigation detection
51
+ let cmdKeyHeld = false;
52
+ let cmdKeyTimestamp = 0;
53
+ const CMD_KEY_THRESHOLD_MS = 500;
54
+
55
+ export function isCmdHeld(): boolean {
56
+ if (cmdKeyHeld) return true;
57
+ return (
58
+ Date.now() - cmdKeyTimestamp < CMD_KEY_THRESHOLD_MS && cmdKeyTimestamp > 0
59
+ );
60
+ }
61
+
62
+ // Set up cmd+click detection for opening links in new windows
63
+ export function initCmdClickHandling() {
64
+ window.addEventListener(
65
+ "keydown",
66
+ (event) => {
67
+ if (event.key === "Meta" || event.metaKey) {
68
+ cmdKeyHeld = true;
69
+ cmdKeyTimestamp = Date.now();
70
+ }
71
+ },
72
+ true,
73
+ );
74
+
75
+ window.addEventListener(
76
+ "keyup",
77
+ (event) => {
78
+ if (event.key === "Meta") {
79
+ cmdKeyHeld = false;
80
+ cmdKeyTimestamp = Date.now();
81
+ }
82
+ },
83
+ true,
84
+ );
85
+
86
+ window.addEventListener("blur", () => {
87
+ cmdKeyHeld = false;
88
+ });
89
+
90
+ // Intercept cmd+clicks on anchors before SPA frameworks can handle them
91
+ window.addEventListener(
92
+ "click",
93
+ (event) => {
94
+ if (event.metaKey || event.ctrlKey) {
95
+ const anchor = (event.target as HTMLElement)?.closest?.("a");
96
+ if (anchor && (anchor as HTMLAnchorElement).href) {
97
+ event.preventDefault();
98
+ event.stopPropagation();
99
+ event.stopImmediatePropagation();
100
+ emitWebviewEvent(
101
+ "new-window-open",
102
+ JSON.stringify({
103
+ url: (anchor as HTMLAnchorElement).href,
104
+ isCmdClick: true,
105
+ isSPANavigation: false,
106
+ }),
107
+ );
108
+ }
109
+ }
110
+ },
111
+ true,
112
+ );
113
+ }
114
+
115
+ // Intercept SPA navigation (history.pushState/replaceState) when cmd is held
116
+ export function initSPANavigationInterception() {
117
+ const originalPushState = history.pushState;
118
+ const originalReplaceState = history.replaceState;
119
+
120
+ history.pushState = function (
121
+ state: unknown,
122
+ title: string,
123
+ url?: string | URL | null,
124
+ ) {
125
+ if (isCmdHeld() && url) {
126
+ const resolvedUrl = new URL(String(url), window.location.href).href;
127
+ emitWebviewEvent(
128
+ "new-window-open",
129
+ JSON.stringify({
130
+ url: resolvedUrl,
131
+ isCmdClick: true,
132
+ isSPANavigation: true,
133
+ }),
134
+ );
135
+ return;
136
+ }
137
+ return originalPushState.apply(this, [state, title, url]);
138
+ };
139
+
140
+ history.replaceState = function (
141
+ state: unknown,
142
+ title: string,
143
+ url?: string | URL | null,
144
+ ) {
145
+ if (isCmdHeld() && url) {
146
+ const resolvedUrl = new URL(String(url), window.location.href).href;
147
+ emitWebviewEvent(
148
+ "new-window-open",
149
+ JSON.stringify({
150
+ url: resolvedUrl,
151
+ isCmdClick: true,
152
+ isSPANavigation: true,
153
+ }),
154
+ );
155
+ return;
156
+ }
157
+ return originalReplaceState.apply(this, [state, title, url]);
158
+ };
159
+ }
160
+
161
+ // Prevent overscroll bounce effect
162
+ export function initOverscrollPrevention() {
163
+ document.addEventListener("DOMContentLoaded", () => {
164
+ const style = document.createElement("style");
165
+ style.type = "text/css";
166
+ style.appendChild(
167
+ document.createTextNode("html, body { overscroll-behavior: none; }"),
168
+ );
169
+ document.head.appendChild(style);
170
+ });
171
+ }
@@ -0,0 +1,45 @@
1
+ // Type declarations for SparkBun preload globals
2
+ // These are set dynamically per-webview before the preload script runs
3
+
4
+ declare global {
5
+ interface Window {
6
+ __sparkbunWebviewId: number;
7
+ __sparkbunWindowId: number;
8
+ __sparkbunRpcSocketPort: number;
9
+ __sparkbunHostSocketPort?: number;
10
+ __sparkbunSecretKeyBytes: number[];
11
+ // Event-only bridge (all webviews, including sandboxed)
12
+ __sparkbunEventBridge?: {
13
+ postMessage: (message: string) => void;
14
+ };
15
+ // Internal RPC bridge (trusted webviews only)
16
+ __sparkbunInternalBridge?: {
17
+ postMessage: (message: string) => void;
18
+ };
19
+ // User RPC bridge (trusted webviews only)
20
+ __sparkbunHostBridge?: {
21
+ postMessage: (message: string) => void;
22
+ };
23
+ __sparkbunBunBridge?: {
24
+ postMessage: (message: string) => void;
25
+ };
26
+ __sparkbun_encrypt: (
27
+ plaintext: string,
28
+ ) => Promise<{ encryptedData: string; iv: string; tag: string }>;
29
+ __sparkbun_decrypt: (
30
+ encryptedData: string,
31
+ iv: string,
32
+ tag: string,
33
+ ) => Promise<string>;
34
+ __sparkbunSendToHost: (message: unknown) => void;
35
+ __sparkbunPendingHostMessages?: unknown[];
36
+ __sparkbun: {
37
+ receiveMessageFromHost: (msg: unknown) => void;
38
+ receiveInternalMessageFromHost: (msg: unknown) => void;
39
+ receiveMessageFromBun: (msg: unknown) => void;
40
+ receiveInternalMessageFromBun: (msg: unknown) => void;
41
+ };
42
+ }
43
+ }
44
+
45
+ export {};
@@ -0,0 +1,28 @@
1
+ // SparkBun Sandboxed Preload Script (for untrusted webviews)
2
+ // This is compiled to JS and injected into webviews that ARE sandboxed
3
+ //
4
+ // Minimal functionality for security: NO RPC, NO encryption, NO webview tags
5
+ // Only includes: lifecycle events, cmd+click handling, overscroll prevention
6
+ //
7
+ // Before this script runs, the following must be set:
8
+ // - window.__sparkbunWebviewId
9
+ // - window.__sparkbunWindowId
10
+ // - window.__sparkbunEventBridge (event emission only)
11
+
12
+ import "./globals.d.ts";
13
+ import {
14
+ initLifecycleEvents,
15
+ initCmdClickHandling,
16
+ initSPANavigationInterception,
17
+ initOverscrollPrevention,
18
+ } from "./events";
19
+
20
+ // Initialize minimal features for sandboxed webviews
21
+ // No RPC handlers - sandboxed webviews cannot communicate with Bun
22
+ // No drag regions - sandboxed content shouldn't control window movement
23
+ // No webview tags - sandboxed content cannot create OOPIFs
24
+
25
+ initLifecycleEvents();
26
+ initCmdClickHandling();
27
+ initSPANavigationInterception();
28
+ initOverscrollPrevention();
@@ -0,0 +1,77 @@
1
+ // SparkBun Full Preload Script (for trusted webviews)
2
+ // This is compiled to JS and injected into webviews that are NOT sandboxed
3
+ //
4
+ // Includes: RPC, encryption, drag regions, webview tags, lifecycle events
5
+ //
6
+ // Before this script runs, the following must be set:
7
+ // - window.__sparkbunWebviewId
8
+ // - window.__sparkbunWindowId
9
+ // - window.__sparkbunRpcSocketPort
10
+ // - window.__sparkbunHostSocketPort (optional alias)
11
+ // - window.__sparkbunSecretKeyBytes
12
+ // - window.__sparkbunEventBridge (event emission - all webviews)
13
+ // - window.__sparkbunInternalBridge (internal RPC - trusted only)
14
+ // - window.__sparkbunHostBridge (user RPC - trusted only)
15
+ // - window.__sparkbunBunBridge (legacy alias)
16
+
17
+ import "./globals.d.ts";
18
+ import { initEncryption } from "./encryption";
19
+ import { handleResponse } from "./internalRpc";
20
+ import { initDragRegions } from "./dragRegions";
21
+ import { initWebviewTag } from "./webviewTag";
22
+ import { initWgpuTag } from "./wgpuTag";
23
+ import {
24
+ emitWebviewEvent,
25
+ initLifecycleEvents,
26
+ initCmdClickHandling,
27
+ initSPANavigationInterception,
28
+ initOverscrollPrevention,
29
+ } from "./events";
30
+
31
+ // Initialize encryption first (async)
32
+ initEncryption().catch((err) =>
33
+ console.error("Failed to initialize encryption:", err),
34
+ );
35
+
36
+ // Set up global handlers for bun to call back
37
+ // Wrapper to satisfy the (msg: unknown) => void type
38
+ const internalMessageHandler = (msg: unknown) => {
39
+ handleResponse(msg as { type: string; id: string; success: boolean; payload: unknown });
40
+ };
41
+
42
+ const defaultUserMessageHandler = (msg: unknown) => {
43
+ // Buffer user RPC packets that arrive before the page-specific Electroview
44
+ // instance installs the real handler.
45
+ if (!window.__sparkbunPendingHostMessages) {
46
+ window.__sparkbunPendingHostMessages = [];
47
+ }
48
+ window.__sparkbunPendingHostMessages.push(msg);
49
+ };
50
+
51
+ if (!window.__sparkbun) {
52
+ window.__sparkbun = {
53
+ receiveInternalMessageFromHost: internalMessageHandler,
54
+ receiveMessageFromHost: defaultUserMessageHandler,
55
+ receiveInternalMessageFromBun: internalMessageHandler,
56
+ receiveMessageFromBun: defaultUserMessageHandler,
57
+ };
58
+ } else {
59
+ window.__sparkbun.receiveInternalMessageFromHost = internalMessageHandler;
60
+ window.__sparkbun.receiveMessageFromHost = defaultUserMessageHandler;
61
+ window.__sparkbun.receiveInternalMessageFromBun = internalMessageHandler;
62
+ window.__sparkbun.receiveMessageFromBun = defaultUserMessageHandler;
63
+ }
64
+
65
+ // Allow preload scripts to send custom messages to the host webview
66
+ window.__sparkbunSendToHost = (message: unknown) => {
67
+ emitWebviewEvent("host-message", JSON.stringify(message));
68
+ };
69
+
70
+ // Initialize all features
71
+ initLifecycleEvents();
72
+ initCmdClickHandling();
73
+ initSPANavigationInterception();
74
+ initOverscrollPrevention();
75
+ initDragRegions();
76
+ initWebviewTag();
77
+ initWgpuTag();
@@ -0,0 +1,80 @@
1
+ // Internal RPC System for webview tags, drag regions, etc.
2
+ // Communicates with Bun via __sparkbunInternalBridge
3
+
4
+ import "./globals.d.ts";
5
+
6
+ interface PendingRequest {
7
+ resolve: (value: unknown) => void;
8
+ reject: (reason: unknown) => void;
9
+ }
10
+
11
+ const pendingRequests: Record<string, PendingRequest> = {};
12
+ let requestId = 0;
13
+ let isProcessingQueue = false;
14
+ const sendQueue: string[] = [];
15
+
16
+ function processQueue() {
17
+ if (isProcessingQueue) {
18
+ setTimeout(processQueue);
19
+ return;
20
+ }
21
+ if (sendQueue.length === 0) return;
22
+
23
+ isProcessingQueue = true;
24
+ const batch = JSON.stringify(sendQueue);
25
+ sendQueue.length = 0;
26
+ window.__sparkbunInternalBridge?.postMessage(batch);
27
+
28
+ // 2ms delay to work around Bun JSCallback threading issue
29
+ setTimeout(() => {
30
+ isProcessingQueue = false;
31
+ }, 2);
32
+ }
33
+
34
+ export function send(type: string, payload: unknown) {
35
+ // Format: { type: 'message', id: handlerName, payload: data }
36
+ sendQueue.push(JSON.stringify({ type: "message", id: type, payload }));
37
+ processQueue();
38
+ }
39
+
40
+ export function request(type: string, payload: unknown): Promise<unknown> {
41
+ return new Promise((resolve, reject) => {
42
+ const id = `req_${++requestId}_${Date.now()}`;
43
+ pendingRequests[id] = { resolve, reject };
44
+ // Format: { type: 'request', method: handlerName, id: requestId, params: data, hostWebviewId: ... }
45
+ sendQueue.push(
46
+ JSON.stringify({
47
+ type: "request",
48
+ method: type,
49
+ id,
50
+ params: payload,
51
+ hostWebviewId: window.__sparkbunWebviewId,
52
+ }),
53
+ );
54
+ processQueue();
55
+ // Timeout after 10s
56
+ setTimeout(() => {
57
+ if (pendingRequests[id]) {
58
+ delete pendingRequests[id];
59
+ reject(new Error(`Request timeout: ${type}`));
60
+ }
61
+ }, 10000);
62
+ });
63
+ }
64
+
65
+ export function handleResponse(msg: {
66
+ type: string;
67
+ id: string;
68
+ success: boolean;
69
+ payload: unknown;
70
+ }) {
71
+ // msg format: { type: 'response', id: requestId, success: bool, payload: data }
72
+ if (msg && msg.type === "response" && msg.id) {
73
+ const pending = pendingRequests[msg.id];
74
+ if (pending) {
75
+ delete pendingRequests[msg.id];
76
+ if (msg.success) pending.resolve(msg.payload);
77
+ else pending.reject(msg.payload);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,107 @@
1
+ import "./globals.d.ts";
2
+
3
+ export interface Rect {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+
10
+ export type MaskRect = Rect;
11
+
12
+ export type OverlaySyncOptions = {
13
+ onSync: (rect: Rect, masksJson: string) => void;
14
+ getMasks?: () => MaskRect[];
15
+ burstIntervalMs?: number;
16
+ baseIntervalMs?: number;
17
+ burstDurationMs?: number;
18
+ };
19
+
20
+ export class OverlaySyncController {
21
+ private element: HTMLElement;
22
+ private options: Required<OverlaySyncOptions>;
23
+ private lastRect: Rect = { x: 0, y: 0, width: 0, height: 0 };
24
+ private resizeObserver: ResizeObserver | null = null;
25
+ private positionLoop: ReturnType<typeof setTimeout> | null = null;
26
+ private resizeHandler: (() => void) | null = null;
27
+ private burstUntil = 0;
28
+
29
+ constructor(element: HTMLElement, options: OverlaySyncOptions) {
30
+ this.element = element;
31
+ this.options = {
32
+ onSync: options.onSync,
33
+ getMasks: options.getMasks ?? (() => []),
34
+ burstIntervalMs: options.burstIntervalMs ?? 50,
35
+ baseIntervalMs: options.baseIntervalMs ?? 100,
36
+ burstDurationMs: options.burstDurationMs ?? 500,
37
+ };
38
+ }
39
+
40
+ start() {
41
+ this.resizeObserver = new ResizeObserver(() => this.sync());
42
+ this.resizeObserver.observe(this.element);
43
+
44
+ const loop = () => {
45
+ this.sync();
46
+ const now = performance.now();
47
+ const interval =
48
+ now < this.burstUntil
49
+ ? this.options.burstIntervalMs
50
+ : this.options.baseIntervalMs;
51
+ this.positionLoop = setTimeout(loop, interval);
52
+ };
53
+ this.positionLoop = setTimeout(loop, this.options.baseIntervalMs);
54
+
55
+ this.resizeHandler = () => this.sync(true);
56
+ window.addEventListener("resize", this.resizeHandler);
57
+ }
58
+
59
+ stop() {
60
+ if (this.resizeObserver) this.resizeObserver.disconnect();
61
+ if (this.positionLoop) clearTimeout(this.positionLoop);
62
+ if (this.resizeHandler) {
63
+ window.removeEventListener("resize", this.resizeHandler);
64
+ }
65
+ this.resizeObserver = null;
66
+ this.positionLoop = null;
67
+ this.resizeHandler = null;
68
+ }
69
+
70
+ forceSync() {
71
+ this.sync(true);
72
+ }
73
+
74
+ setLastRect(rect: Rect) {
75
+ this.lastRect = rect;
76
+ }
77
+
78
+ private sync(force = false) {
79
+ const rect = this.element.getBoundingClientRect();
80
+ const newRect: Rect = {
81
+ x: rect.x,
82
+ y: rect.y,
83
+ width: rect.width,
84
+ height: rect.height,
85
+ };
86
+
87
+ if (newRect.width === 0 && newRect.height === 0) {
88
+ return;
89
+ }
90
+
91
+ if (
92
+ !force &&
93
+ newRect.x === this.lastRect.x &&
94
+ newRect.y === this.lastRect.y &&
95
+ newRect.width === this.lastRect.width &&
96
+ newRect.height === this.lastRect.height
97
+ ) {
98
+ return;
99
+ }
100
+
101
+ this.burstUntil = performance.now() + this.options.burstDurationMs;
102
+ this.lastRect = newRect;
103
+
104
+ const masks = this.options.getMasks();
105
+ this.options.onSync(newRect, JSON.stringify(masks));
106
+ }
107
+ }