@kbve/droid 0.0.5 → 0.1.1

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 (109) hide show
  1. package/README.md +3 -1
  2. package/comlink.js +1 -7
  3. package/droid.mjs +191 -61
  4. package/index.d.ts +29 -0
  5. package/index.js +9054 -2763
  6. package/lib/gateway/SupabaseGateway.d.ts +38 -0
  7. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  8. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  9. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  10. package/lib/gateway/WorkerPool.d.ts +55 -0
  11. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  12. package/lib/gateway/capabilities.d.ts +23 -0
  13. package/lib/gateway/capabilities.d.ts.map +1 -0
  14. package/lib/gateway/index.d.ts +9 -0
  15. package/lib/gateway/index.d.ts.map +1 -0
  16. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  17. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  18. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  19. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  20. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  21. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  22. package/lib/gateway/types.d.ts +81 -0
  23. package/lib/gateway/types.d.ts.map +1 -0
  24. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  25. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  26. package/lib/workers/canvas-worker.d.ts +14 -0
  27. package/lib/workers/canvas-worker.d.ts.map +1 -0
  28. package/lib/workers/canvas-worker.js +291 -145
  29. package/lib/workers/data.d.ts +40 -0
  30. package/lib/workers/data.d.ts.map +1 -0
  31. package/lib/workers/db-worker.d.ts +69 -0
  32. package/lib/workers/db-worker.d.ts.map +1 -0
  33. package/lib/workers/db-worker.js +2680 -2147
  34. package/lib/workers/events.d.ts +11 -0
  35. package/lib/workers/events.d.ts.map +1 -0
  36. package/lib/workers/flexbuilder.d.ts +4 -0
  37. package/lib/workers/flexbuilder.d.ts.map +1 -0
  38. package/lib/workers/init.d.ts +11 -0
  39. package/lib/workers/init.d.ts.map +1 -0
  40. package/lib/workers/main.d.ts +107 -0
  41. package/lib/workers/main.d.ts.map +1 -0
  42. package/lib/workers/supabase-db-worker.d.ts +2 -0
  43. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  44. package/lib/workers/supabase-db-worker.js +9337 -0
  45. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  46. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  47. package/lib/workers/supabase-shared-worker.js +12635 -0
  48. package/lib/workers/tools.d.ts +4 -0
  49. package/lib/workers/tools.d.ts.map +1 -0
  50. package/lib/workers/validate.d.ts +15 -0
  51. package/lib/workers/validate.d.ts.map +1 -0
  52. package/lib/workers/ws-worker.d.ts +11 -0
  53. package/lib/workers/ws-worker.d.ts.map +1 -0
  54. package/lib/workers/ws-worker.js +174 -115
  55. package/main.js +4875 -0
  56. package/package.json +17 -6
  57. package/reference.js +688 -228
  58. package/src/index.ts +38 -13
  59. package/src/lib/droid.spec.ts +14 -19
  60. package/src/lib/droid.ts +48 -4
  61. package/src/lib/gateway/SupabaseGateway.ts +183 -0
  62. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  63. package/src/lib/gateway/WorkerPool.ts +304 -0
  64. package/src/lib/gateway/capabilities.ts +80 -0
  65. package/src/lib/gateway/index.ts +15 -0
  66. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  67. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  68. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  69. package/src/lib/gateway/types.ts +112 -0
  70. package/src/lib/mod/mod-manager.ts +13 -8
  71. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  72. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  73. package/src/lib/mod/module/supabase/mod-supabase.ts +68 -0
  74. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  75. package/src/lib/state/auth.spec.ts +58 -0
  76. package/src/lib/state/auth.ts +30 -0
  77. package/src/lib/state/index.ts +34 -0
  78. package/src/lib/state/overlay-manager.ts +125 -0
  79. package/src/lib/state/router.ts +24 -0
  80. package/src/lib/state/theme-sync.ts +75 -0
  81. package/src/lib/state/toasts.spec.ts +106 -0
  82. package/src/lib/state/toasts.ts +34 -0
  83. package/src/lib/state/ui.spec.ts +148 -0
  84. package/src/lib/state/ui.ts +45 -0
  85. package/src/lib/state/welcome-toast.ts +76 -0
  86. package/src/lib/types/bento.spec.ts +116 -0
  87. package/src/lib/types/bento.ts +94 -55
  88. package/src/lib/types/event-types.spec.ts +93 -0
  89. package/src/lib/types/event-types.ts +39 -3
  90. package/src/lib/types/modules.ts +12 -5
  91. package/src/lib/types/ui-event-types.ts +66 -0
  92. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  93. package/src/lib/workers/canvas-worker.ts +94 -9
  94. package/src/lib/workers/data.ts +59 -36
  95. package/src/lib/workers/db-worker.ts +79 -32
  96. package/src/lib/workers/events.spec.ts +208 -0
  97. package/src/lib/workers/events.ts +27 -14
  98. package/src/lib/workers/flexbuilder.ts +2 -2
  99. package/src/lib/workers/init.ts +25 -17
  100. package/src/lib/workers/main.ts +509 -272
  101. package/src/lib/workers/supabase-db-worker.ts +221 -0
  102. package/src/lib/workers/supabase-shared-worker.ts +572 -0
  103. package/src/lib/workers/validate.ts +64 -0
  104. package/src/lib/workers/ws-worker.ts +156 -25
  105. package/src/setup-vitest.ts +4 -2
  106. package/src/types.ts +30 -0
  107. package/src/vite-env.d.ts +1 -0
  108. package/workers/main.js +10 -1089
  109. package/src/types.d.ts +0 -31
@@ -1,107 +1,326 @@
1
- import { wrap, transfer, proxy } from 'comlink';
1
+ import { wrap, proxy } from 'comlink';
2
2
  import type { Remote } from 'comlink';
3
3
  import { persistentMap } from '@nanostores/persistent';
4
4
  import type { LocalStorageAPI } from './db-worker';
5
5
  import type { WSInstance } from './ws-worker';
6
- import { initializeWorkerDatabase, type InitWorkerOptions } from './init';
6
+ import { initializeWorkerDatabase } from './init';
7
7
  import type { CanvasWorkerAPI } from './canvas-worker';
8
8
  import { getModManager } from '../mod/mod-manager';
9
9
  import { scopeData } from './data';
10
10
  import { dispatchAsync, renderVNode } from './tools';
11
+ import type { VirtualNode, ModManager } from '../types/modules';
11
12
  import type { PanelPayload, PanelId } from '../types/panel-types';
12
13
  import { DroidEvents } from './events';
14
+ import {
15
+ addToast as _addToast,
16
+ removeToast as _removeToast,
17
+ } from '../state/toasts';
18
+ import { openTooltip, closeTooltip, openModal, closeModal } from '../state/ui';
19
+ import {
20
+ ToastPayloadSchema,
21
+ TooltipPayloadSchema,
22
+ ModalPayloadSchema,
23
+ } from '../types/ui-event-types';
24
+ import { SupabaseGateway } from '../gateway/SupabaseGateway';
25
+ import type { GatewayConfig } from '../gateway/types';
26
+ import { observeThemeChanges } from '../state/theme-sync';
27
+ import { OverlayManager } from '../state/overlay-manager';
28
+ import { showWelcomeToast } from '../state/welcome-toast';
29
+
30
+ /** Messages emitted from workers to the main thread for UI side-effects. */
31
+ type WorkerUIMessage =
32
+ | { type: 'injectVNode'; vnode: VirtualNode }
33
+ | { type: 'toast'; payload: unknown }
34
+ | { type: 'toast-remove'; payload: { id: string } }
35
+ | { type: 'tooltip-open'; payload: unknown }
36
+ | { type: 'tooltip-close'; payload?: { id: string } }
37
+ | { type: 'modal-open'; payload: unknown }
38
+ | { type: 'modal-close'; payload?: { id: string } }
39
+ | { type: string; [key: string]: unknown };
13
40
 
14
41
  const EXPECTED_DB_VERSION = '1.0.3';
15
42
  let initialized = false;
43
+ let _initPromise: Promise<void> | null = null;
44
+
45
+ // --- Scale State ---
46
+ export type DroidScaleLevel = 'full' | 'minimal';
47
+
48
+ interface DroidWorkerRefs {
49
+ canvasWorkerRaw?: Worker;
50
+ canvasWorkerProxy?: Remote<CanvasWorkerAPI>;
51
+ canvasWorkerOpts?: { workerRef?: Worker; workerURL?: string };
52
+ modInitOpts?: { resolver?: (url: string) => string };
53
+ gatewayConfig?: GatewayConfig;
54
+ }
55
+
56
+ let _scaleLevel: DroidScaleLevel = 'full';
57
+ const _workerRefs: DroidWorkerRefs = {};
58
+
59
+ /**
60
+ * Returns the current droid scale level.
61
+ */
62
+ export function getScaleLevel(): DroidScaleLevel {
63
+ return _scaleLevel;
64
+ }
65
+
66
+ /**
67
+ * Downscale droid to minimal mode: terminates canvas worker, mod workers,
68
+ * gateway worker pool, and overlay manager. Keeps DB SharedWorker + WS
69
+ * SharedWorker alive for session/auth persistence.
70
+ *
71
+ * Call this before loading heavy WASM/pthread workloads.
72
+ */
73
+ export async function downscale(): Promise<void> {
74
+ if (_scaleLevel === 'minimal') {
75
+ console.log('[DROID] Already in minimal mode');
76
+ return;
77
+ }
78
+
79
+ console.log('[DROID] Downscaling to minimal mode...');
80
+
81
+ // 1. Destroy overlay manager (unbinds canvas)
82
+ if (window.kbve?.overlay) {
83
+ try {
84
+ await (window.kbve.overlay as OverlayManager).destroy();
85
+ } catch (e) {
86
+ console.warn('[DROID] Overlay destroy error:', e);
87
+ }
88
+ delete window.kbve.overlay;
89
+ }
90
+
91
+ // 2. Terminate all mods
92
+ if (window.kbve?.mod) {
93
+ const mod = window.kbve.mod as ModManager;
94
+ for (const handle of Object.values(mod.registry)) {
95
+ try {
96
+ mod.unload(handle.id);
97
+ } catch (e) {
98
+ console.warn('[DROID] Mod unload error:', e);
99
+ }
100
+ }
101
+ }
102
+
103
+ // 3. Terminate gateway (worker pool + strategy workers)
104
+ if (window.kbve?.gateway) {
105
+ try {
106
+ (window.kbve.gateway as SupabaseGateway).terminate();
107
+ } catch (e) {
108
+ console.warn('[DROID] Gateway terminate error:', e);
109
+ }
110
+ delete window.kbve.gateway;
111
+ }
112
+
113
+ // 4. Terminate canvas worker
114
+ if (_workerRefs.canvasWorkerRaw) {
115
+ try {
116
+ _workerRefs.canvasWorkerRaw.terminate();
117
+ } catch (e) {
118
+ console.warn('[DROID] Canvas worker terminate error:', e);
119
+ }
120
+ _workerRefs.canvasWorkerRaw = undefined;
121
+ _workerRefs.canvasWorkerProxy = undefined;
122
+ if (window.kbve?.uiux) {
123
+ (window.kbve.uiux as Record<string, unknown>)['worker'] = undefined;
124
+ }
125
+ }
126
+
127
+ _scaleLevel = 'minimal';
128
+
129
+ window.kbve?.events?.emit('droid-downscale', {
130
+ timestamp: Date.now(),
131
+ level: 'minimal',
132
+ });
133
+
134
+ console.log('[DROID] Downscaled to minimal mode (DB + WS workers active)');
135
+ }
136
+
137
+ /**
138
+ * Upscale droid back to full mode: re-initializes canvas worker, mod manager,
139
+ * overlay manager, and gateway.
140
+ *
141
+ * Call this when the heavy workload is done and you want full droid features.
142
+ */
143
+ export async function upscale(): Promise<void> {
144
+ if (_scaleLevel === 'full') {
145
+ console.log('[DROID] Already in full mode');
146
+ return;
147
+ }
148
+
149
+ console.log('[DROID] Upscaling to full mode...');
150
+
151
+ try {
152
+ // 1. Re-init canvas worker
153
+ const canvasWorker = initDedicatedWorker(
154
+ 'canvas-worker',
155
+ _workerRefs.canvasWorkerOpts,
156
+ );
157
+ _workerRefs.canvasWorkerRaw = canvasWorker;
158
+ const canvasProxy = wrap<CanvasWorkerAPI>(canvasWorker);
159
+ _workerRefs.canvasWorkerProxy = canvasProxy;
160
+
161
+ if (window.kbve?.uiux) {
162
+ (window.kbve.uiux as Record<string, unknown>)['worker'] =
163
+ canvasProxy;
164
+ }
165
+
166
+ // 2. Re-init mod manager
167
+ if (window.kbve?.mod) {
168
+ const mod = await getModManager(_workerRefs.modInitOpts?.resolver);
169
+ window.kbve.mod = mod;
170
+ }
171
+
172
+ // 3. Re-init gateway if we had one
173
+ if (_workerRefs.gatewayConfig) {
174
+ const gateway = new SupabaseGateway(_workerRefs.gatewayConfig);
175
+ window.kbve.gateway = gateway;
176
+ }
177
+
178
+ // 4. Re-init overlay manager
179
+ const overlay = new OverlayManager({
180
+ preferredPath: 'auto',
181
+ canvasWorker: canvasProxy,
182
+ });
183
+ window.kbve.overlay = overlay;
184
+
185
+ _scaleLevel = 'full';
186
+
187
+ window.kbve?.events?.emit('droid-upscale', {
188
+ timestamp: Date.now(),
189
+ level: 'full',
190
+ });
191
+
192
+ console.log('[DROID] Upscaled to full mode');
193
+ } catch (err) {
194
+ console.error('[DROID] Upscale failed:', err);
195
+ throw err;
196
+ }
197
+ }
16
198
 
17
199
  export function resolveWorkerURL(name: string, fallback?: string): string {
18
200
  if (!name)
19
201
  throw new Error('[resolveWorkerURL] Worker name must be defined');
20
202
 
21
203
  if (typeof window !== 'undefined') {
22
- const globalMap = (window as any).kbveWorkerURLs;
204
+ const globalMap = (
205
+ window as unknown as Record<string, Record<string, string>>
206
+ )['kbveWorkerURLs'];
23
207
  if (globalMap?.[name]) return globalMap[name];
24
208
  }
25
209
 
26
- // No bundler-specific resolution: just return fallback or root-relative
27
210
  return fallback ?? `/workers/${name}`;
28
211
  }
29
212
 
30
- // * DeepProxy
31
- function deepProxy<T>(obj: T): T {
32
- if (typeof obj === 'function') return proxy(obj) as T;
213
+ // --- Worker Init Helpers ---
33
214
 
34
- if (obj && typeof obj === 'object') {
35
- const result: any = Array.isArray(obj) ? [] : {};
36
- for (const key in obj) {
37
- result[key] = deepProxy(obj[key]);
38
- }
39
- return result;
215
+ /**
216
+ * Simplified worker init tries provided ref/URL, then falls back to
217
+ * resolveWorkerURL which checks window.kbveWorkerURLs or /workers/{name}.
218
+ */
219
+ function initSharedWorker(
220
+ name: string,
221
+ opts?: { workerRef?: SharedWorker; workerURL?: string },
222
+ ): SharedWorker {
223
+ if (opts?.workerRef) {
224
+ opts.workerRef.port.start();
225
+ return opts.workerRef;
40
226
  }
41
227
 
42
- return obj;
228
+ const url = opts?.workerURL ?? resolveWorkerURL(name);
229
+ const worker = new SharedWorker(url, { type: 'module' });
230
+ worker.port.start();
231
+
232
+ // Notify worker to clean up this port on tab close/reload
233
+ window.addEventListener('beforeunload', () => {
234
+ worker.port.postMessage({ type: 'close' });
235
+ });
236
+
237
+ return worker;
238
+ }
239
+
240
+ function initDedicatedWorker(
241
+ name: string,
242
+ opts?: { workerRef?: Worker; workerURL?: string },
243
+ ): Worker {
244
+ if (opts?.workerRef) return opts.workerRef;
245
+
246
+ const url = opts?.workerURL ?? resolveWorkerURL(name);
247
+ return new Worker(url, { type: 'module' });
248
+ }
249
+
250
+ function listenFirstConnect(port: MessagePort): Promise<boolean> {
251
+ return new Promise<boolean>((resolve) => {
252
+ const handler = (e: MessageEvent) => {
253
+ if (e.data?.type === 'first-connect') {
254
+ resolve(true);
255
+ port.removeEventListener('message', handler);
256
+ } else if (e.data?.type === 'reconnect') {
257
+ resolve(false);
258
+ port.removeEventListener('message', handler);
259
+ }
260
+ };
261
+ port.addEventListener('message', handler);
262
+ setTimeout(() => {
263
+ port.removeEventListener('message', handler);
264
+ resolve(false);
265
+ }, 5_000);
266
+ });
267
+ }
268
+
269
+ async function initStorageComlink(opts?: {
270
+ workerRef?: SharedWorker;
271
+ workerURL?: string;
272
+ i18nPath?: string;
273
+ dataPath?: string;
274
+ }): Promise<{ api: Remote<LocalStorageAPI>; isFirstConnection: boolean }> {
275
+ const worker = initSharedWorker('db-worker', opts);
276
+ const firstPromise = listenFirstConnect(worker.port);
277
+ const api = wrap<LocalStorageAPI>(worker.port);
278
+ const finalApi = await finalize(api, {
279
+ i18nPath: opts?.i18nPath,
280
+ dataPath: opts?.dataPath,
281
+ });
282
+ const isFirstConnection = await firstPromise;
283
+ return { api: finalApi, isFirstConnection };
43
284
  }
44
285
 
45
286
  async function initWsComlink(opts?: {
46
- workerRef?: SharedWorker;
47
- workerURL?: string;
48
- }): Promise<Remote<WSInstance>> {
49
- let api: Remote<WSInstance> | null = null;
50
-
51
- // 1. Try Vite-style import
52
- try {
53
- const worker = new SharedWorker(new URL('./ws-worker.ts', import.meta.url), {
54
- type: 'module',
55
- });
56
- worker.port.start();
57
- api = wrap<WSInstance>(worker.port);
58
- } catch (err) {
59
- console.warn('[DROID] ws-worker import.meta fallback failed:', err);
60
- }
61
-
62
- // 2. Try hardcoded path
63
- if (!api) {
64
- try {
65
- const worker = new SharedWorker('/ws-worker.js', { type: 'module' });
66
- worker.port.start();
67
- api = wrap<WSInstance>(worker.port);
68
- } catch (err) {
69
- console.warn('[DROID] ws-worker hardcoded fallback failed:', err);
70
- }
71
- }
72
-
73
- // 3. Try provided SharedWorker reference
74
- if (!api && opts?.workerRef) {
75
- try {
76
- opts.workerRef.port.start();
77
- api = wrap<WSInstance>(opts.workerRef.port);
78
- } catch (err) {
79
- console.warn('[DROID] ws-worker workerRef failed:', err);
80
- }
81
- }
82
-
83
- // 4. Try provided URL
84
- if (!api && opts?.workerURL) {
85
- try {
86
- const worker = new SharedWorker(opts.workerURL, { type: 'module' });
87
- worker.port.start();
88
- api = wrap<WSInstance>(worker.port);
89
- } catch (err) {
90
- console.warn('[DROID] ws-worker workerURL failed:', err);
91
- }
92
- }
93
-
94
- // 5. Final failure
95
- if (!api) {
96
- console.error('[DROID] No WS Worker Comlink Initialized');
97
- throw new Error('[DROID] Failed to initialize ws-worker');
98
- }
99
-
100
- return api;
287
+ workerRef?: SharedWorker;
288
+ workerURL?: string;
289
+ }): Promise<{ ws: Remote<WSInstance>; isFirstConnection: boolean }> {
290
+ const worker = initSharedWorker('ws-worker', opts);
291
+ const firstPromise = listenFirstConnect(worker.port);
292
+ const ws = wrap<WSInstance>(worker.port);
293
+ const isFirstConnection = await firstPromise;
294
+ return { ws, isFirstConnection };
101
295
  }
102
296
 
297
+ async function initCanvasComlink(opts?: {
298
+ workerRef?: Worker;
299
+ workerURL?: string;
300
+ }): Promise<{ proxy: Remote<CanvasWorkerAPI>; raw: Worker }> {
301
+ const worker = initDedicatedWorker('canvas-worker', opts);
302
+ return { proxy: wrap<CanvasWorkerAPI>(worker), raw: worker };
303
+ }
304
+
305
+ async function finalize(
306
+ api: Remote<LocalStorageAPI>,
307
+ initOpts?: { i18nPath?: string; dataPath?: string },
308
+ ): Promise<Remote<LocalStorageAPI>> {
309
+ const version = await api.getVersion();
310
+ if (version !== EXPECTED_DB_VERSION) {
311
+ await initializeWorkerDatabase(api, {
312
+ version: EXPECTED_DB_VERSION,
313
+ i18nPath: initOpts?.i18nPath,
314
+ dataPath: initOpts?.dataPath,
315
+ locale: 'en',
316
+ defaults: { welcome: 'Welcome!', theme: 'dark' },
317
+ });
318
+ }
319
+ return api;
320
+ }
321
+
322
+ // --- UIUX ---
103
323
 
104
- // * UIUX
105
324
  const uiuxState = persistentMap<{
106
325
  panelManager: Record<
107
326
  PanelId,
@@ -111,7 +330,7 @@ const uiuxState = persistentMap<{
111
330
  }
112
331
  >;
113
332
  themeManager: { theme: 'light' | 'dark' | 'auto' };
114
- toastManager: Record<string, any>;
333
+ toastManager: Record<string, unknown>;
115
334
  scrollY: number;
116
335
  }>(
117
336
  'uiux-state',
@@ -132,65 +351,20 @@ const uiuxState = persistentMap<{
132
351
  },
133
352
  );
134
353
 
135
- async function initCanvasComlink(opts?: {
136
- workerRef?: Worker;
137
- workerURL?: string;
138
- }): Promise<Remote<CanvasWorkerAPI>> {
139
- // 1. Try Vite-style import resolution
140
- try {
141
- const worker = new Worker(
142
- new URL('./canvas-worker.ts', import.meta.url),
143
- { type: 'module' },
144
- );
145
- return wrap<CanvasWorkerAPI>(worker);
146
- } catch (err) {
147
- console.warn('[DROID] Vite-style canvas-worker import failed:', err);
148
- }
149
-
150
- // 2. Try hardcoded path fallback
151
- try {
152
- const worker = new Worker('./canvas-worker.js', { type: 'module' });
153
- return wrap<CanvasWorkerAPI>(worker);
154
- } catch (err) {
155
- console.warn('[DROID] Fallback /canvas-worker.js failed:', err);
156
- }
157
-
158
- // 3. Try direct Worker instance
159
- if (opts?.workerRef) {
160
- try {
161
- return wrap<CanvasWorkerAPI>(opts.workerRef);
162
- } catch (err) {
163
- console.warn('[DROID] Provided workerRef failed:', err);
164
- }
165
- }
166
-
167
- // 4. Try provided URL
168
- if (opts?.workerURL) {
169
- try {
170
- const worker = new Worker(opts.workerURL, { type: 'module' });
171
- return wrap<CanvasWorkerAPI>(worker);
172
- } catch (err) {
173
- console.warn('[DROID] Provided workerURL failed:', err);
174
- }
175
- }
176
-
177
- // 5. Failure
178
- console.error('[DROID] No Canvas Comlink Initialized');
179
- throw new Error('[DROID] Failed to initialize canvas worker');
180
- }
181
-
182
354
  export const uiux = {
183
355
  state: uiuxState,
184
356
  openPanel(id: PanelId, payload?: PanelPayload) {
185
357
  const panels = { ...uiuxState.get().panelManager };
186
358
  panels[id] = { open: true, payload };
187
359
  uiuxState.setKey('panelManager', panels);
360
+ DroidEvents.emit('panel-open', { id, payload });
188
361
  },
189
362
 
190
363
  closePanel(id: PanelId) {
191
364
  const panels = { ...uiuxState.get().panelManager };
192
365
  panels[id] = { open: false, payload: undefined };
193
366
  uiuxState.setKey('panelManager', panels);
367
+ DroidEvents.emit('panel-close', { id });
194
368
  },
195
369
 
196
370
  togglePanel(id: PanelId, payload?: PanelPayload) {
@@ -198,44 +372,61 @@ export const uiux = {
198
372
  const isOpen = panels[id]?.open ?? false;
199
373
  panels[id] = { open: !isOpen, payload: !isOpen ? payload : undefined };
200
374
  uiuxState.setKey('panelManager', panels);
375
+ if (!isOpen) {
376
+ DroidEvents.emit('panel-open', { id, payload });
377
+ } else {
378
+ DroidEvents.emit('panel-close', { id });
379
+ }
201
380
  },
202
381
 
203
382
  setTheme(theme: 'light' | 'dark' | 'auto') {
204
383
  uiuxState.setKey('themeManager', { theme });
205
384
  },
206
385
 
207
- addToast(id: string, data: any) {
386
+ /** @deprecated Use addToast() from '@kbve/droid' state exports instead. */
387
+ addToast(id: string, data: unknown) {
388
+ console.warn(
389
+ '[KBVE] uiux.addToast is deprecated. Use addToast() from @kbve/droid.',
390
+ );
208
391
  const toasts = { ...uiuxState.get().toastManager, [id]: data };
209
392
  uiuxState.setKey('toastManager', toasts);
210
393
  },
211
394
 
395
+ /** @deprecated Use removeToast() from '@kbve/droid' state exports instead. */
212
396
  removeToast(id: string) {
397
+ console.warn(
398
+ '[KBVE] uiux.removeToast is deprecated. Use removeToast() from @kbve/droid.',
399
+ );
213
400
  const toasts = { ...uiuxState.get().toastManager };
214
401
  delete toasts[id];
215
402
  uiuxState.setKey('toastManager', toasts);
216
403
  },
404
+
217
405
  async dispatchCanvasRequest(
218
406
  panelId: PanelId,
219
407
  canvasEl: HTMLCanvasElement,
220
408
  mode: 'static' | 'animated' | 'dynamic' = 'animated',
221
409
  ) {
222
410
  const offscreen = canvasEl.transferControlToOffscreen();
223
- await window.kbve?.uiux?.worker?.bindCanvas(panelId, offscreen, mode);
411
+ const worker = (window.kbve?.uiux as Record<string, unknown>)?.[
412
+ 'worker'
413
+ ] as Remote<CanvasWorkerAPI> | undefined;
414
+ await worker?.bindCanvas(panelId, offscreen, mode);
224
415
  },
225
416
 
226
417
  closeAllPanels() {
227
418
  const panels = { ...uiuxState.get().panelManager };
228
- console.log('error panel is closing');
229
-
230
419
  for (const id of Object.keys(panels) as Array<PanelId>) {
231
420
  panels[id] = { open: false, payload: undefined };
232
421
  }
233
-
234
422
  uiuxState.setKey('panelManager', panels);
235
423
  },
236
424
 
237
- emitFromWorker(msg: any) {
238
- if (msg.type === 'injectVNode' && msg.vnode) {
425
+ emitFromWorker(msg: WorkerUIMessage) {
426
+ // Existing: VNode injection
427
+ if (msg.type === 'injectVNode') {
428
+ const vnode = msg.vnode as VirtualNode | undefined;
429
+ if (!vnode) return;
239
430
  dispatchAsync(() => {
240
431
  const target = document.getElementById('bento-grid-inject');
241
432
  if (!target) {
@@ -245,20 +436,80 @@ export const uiux = {
245
436
  return;
246
437
  }
247
438
 
248
- const el = renderVNode(msg.vnode);
439
+ const el = renderVNode(vnode);
249
440
  el.classList.add('animate-fade-in');
250
- if (msg.vnode.id) {
251
- const existing = document.getElementById(msg.vnode.id);
441
+ if (vnode.id) {
442
+ const existing = document.getElementById(vnode.id);
252
443
  if (existing) existing.remove();
253
444
  }
254
445
 
255
446
  target.appendChild(el);
256
447
  });
448
+ return;
449
+ }
450
+
451
+ // Toast from worker
452
+ if (msg.type === 'toast' && msg.payload) {
453
+ const parsed = ToastPayloadSchema.safeParse(msg.payload);
454
+ if (!parsed.success) {
455
+ console.error(
456
+ '[KBVE] Invalid toast payload from worker:',
457
+ parsed.error,
458
+ );
459
+ return;
460
+ }
461
+ _addToast(parsed.data);
462
+ return;
463
+ }
464
+ if (msg.type === 'toast-remove') {
465
+ const payload = msg.payload as { id: string } | undefined;
466
+ if (payload?.id) _removeToast(payload.id);
467
+ return;
257
468
  }
469
+
470
+ // Tooltip from worker
471
+ if (msg.type === 'tooltip-open' && msg.payload) {
472
+ const parsed = TooltipPayloadSchema.safeParse(msg.payload);
473
+ if (!parsed.success) {
474
+ console.error(
475
+ '[KBVE] Invalid tooltip payload from worker:',
476
+ parsed.error,
477
+ );
478
+ return;
479
+ }
480
+ openTooltip(parsed.data.id);
481
+ return;
482
+ }
483
+ if (msg.type === 'tooltip-close') {
484
+ const payload = msg.payload as { id?: string } | undefined;
485
+ closeTooltip(payload?.id);
486
+ return;
487
+ }
488
+
489
+ // Modal from worker
490
+ if (msg.type === 'modal-open' && msg.payload) {
491
+ const parsed = ModalPayloadSchema.safeParse(msg.payload);
492
+ if (!parsed.success) {
493
+ console.error(
494
+ '[KBVE] Invalid modal payload from worker:',
495
+ parsed.error,
496
+ );
497
+ return;
498
+ }
499
+ openModal(parsed.data.id);
500
+ return;
501
+ }
502
+ if (msg.type === 'modal-close') {
503
+ const payload = msg.payload as { id?: string } | undefined;
504
+ closeModal(payload?.id);
505
+ return;
506
+ }
507
+
508
+ console.warn('[KBVE] Unknown worker UI message type:', msg.type);
258
509
  },
259
510
  };
260
511
 
261
- // * i18n
512
+ // --- i18n ---
262
513
 
263
514
  const i18nStore = persistentMap<Record<string, string>>(
264
515
  'i18n-cache',
@@ -317,7 +568,6 @@ export const i18n = {
317
568
  const translations = await this.api.getTranslations(localeKeys);
318
569
 
319
570
  for (const [key, value] of Object.entries(translations)) {
320
- console.log(`[i18n.setKey] ${key} = ${value}`);
321
571
  this.store.setKey(key, value);
322
572
  }
323
573
  },
@@ -332,94 +582,23 @@ function initSWComlink() {
332
582
  channel.port1.start();
333
583
  }
334
584
 
335
- async function initStorageComlink(opts?: {
336
- workerRef?: SharedWorker;
337
- workerURL?: string;
338
- }): Promise<Remote<LocalStorageAPI>> {
339
- let api: Remote<LocalStorageAPI> | null = null;
340
- // 1. Try Vite-style import
341
- try {
342
- const worker = new SharedWorker(
343
- new URL('./db-worker.ts', import.meta.url),
344
- {
345
- type: 'module',
346
- },
347
- );
348
- worker.port.start();
349
- api = wrap<LocalStorageAPI>(worker.port);
350
- } catch (err) {
351
- console.warn('[DROID] db-worker import.meta fallback failed:', err);
352
- }
353
-
354
- // 2. Try hardcoded path
355
- if (!api) {
356
- try {
357
- const worker = new SharedWorker('/db-worker.js', {
358
- type: 'module',
359
- });
360
- worker.port.start();
361
- api = wrap<LocalStorageAPI>(worker.port);
362
- } catch (err) {
363
- console.warn('[DROID] db-worker hardcoded fallback failed:', err);
364
- }
365
- }
366
-
367
- // 3. Try provided SharedWorker reference
368
- if (!api && opts?.workerRef) {
369
- try {
370
- opts.workerRef.port.start();
371
- api = wrap<LocalStorageAPI>(opts.workerRef.port);
372
- } catch (err) {
373
- console.warn('[DROID] db-worker workerRef failed:', err);
374
- }
375
- }
376
-
377
- // 4. Try provided URL
378
- if (!api && opts?.workerURL) {
379
- try {
380
- const worker = new SharedWorker(opts.workerURL, { type: 'module' });
381
- worker.port.start();
382
- api = wrap<LocalStorageAPI>(worker.port);
383
- } catch (err) {
384
- console.warn('[DROID] db-worker workerURL failed:', err);
385
- }
386
- }
387
-
388
- // 5. Final failure
389
- if (!api) {
390
- console.error('[DROID] No DB Worker Comlink Initialized');
391
- throw new Error('[DROID] Failed to initialize db-worker');
392
- }
393
-
394
- const version = await api.getVersion();
395
- if (version !== EXPECTED_DB_VERSION) {
396
- await initializeWorkerDatabase(api, {
397
- version: EXPECTED_DB_VERSION,
398
- i18nPath: 'https://discord.sh/i18n/db.json',
399
- locale: 'en',
400
- defaults: { welcome: 'Welcome!', theme: 'dark' },
401
- });
402
- }
403
-
404
- return api;
405
- }
406
-
407
- // * Bridge
585
+ // --- Bridge ---
408
586
  export function bridgeWsToDb(
409
587
  ws: Remote<WSInstance>,
410
588
  db: Remote<LocalStorageAPI>,
411
589
  ) {
412
- const handler = proxy(async (buf: ArrayBuffer) => {
590
+ const handler = proxy(async (data: string | ArrayBuffer) => {
591
+ if (typeof data === 'string') return;
413
592
  dispatchAsync(() => {
414
593
  const key = `ws:${Date.now()}`;
415
- void db.storeWsMessage(key, buf);
594
+ void db.storeWsMessage(key, data);
416
595
  });
417
596
  });
418
597
 
419
- ws.onMessage(transfer(handler, [0]));
598
+ ws.onMessage(handler);
420
599
  }
421
600
 
422
- // * MAIN
601
+ // --- MAIN ---
423
602
  export async function main(opts?: {
424
603
  workerURLs?: Record<string, string>;
425
604
  workerRefs?: {
@@ -427,13 +606,15 @@ export async function main(opts?: {
427
606
  dbWorker?: SharedWorker;
428
607
  wsWorker?: SharedWorker;
429
608
  };
609
+ gateway?: GatewayConfig;
610
+ i18nPath?: string;
611
+ dataPath?: string;
430
612
  }) {
431
613
  console.log('[DROID]: Main<T>');
432
614
 
433
615
  if (!initialized) {
434
616
  initialized = true;
435
617
 
436
- // Attach to existing service worker (or wait for one to take control)
437
618
  if (navigator.serviceWorker?.controller) {
438
619
  initSWComlink();
439
620
  } else {
@@ -444,87 +625,143 @@ export async function main(opts?: {
444
625
  }
445
626
  }
446
627
 
447
- console.log('[DROID] Main<T> => Worker URLs', opts?.workerURLs);
628
+ if (_initPromise) {
629
+ await _initPromise;
630
+ return;
631
+ }
448
632
 
449
633
  const needsInit =
450
634
  !window.kbve?.api || !window.kbve?.i18n || !window.kbve?.uiux;
451
635
 
452
636
  if (needsInit) {
453
- try {
454
- console.log('[DROID] Main<T> => Worker => CanvasComlink');
455
- const canvas = await initCanvasComlink({
456
- workerRef: opts?.workerRefs?.canvasWorker,
457
- workerURL: opts?.workerURLs?.['canvasWorker'],
458
- });
459
- console.log('[DROID] Main<T> => Worker => StorageComlink');
460
- const api = await initStorageComlink({
461
- workerURL: typeof opts?.workerURLs?.['dbWorker'] === 'string'
462
- ? opts.workerURLs['dbWorker']
463
- : undefined,
464
- workerRef: opts?.workerRefs?.dbWorker,
465
- });
466
- console.log('[DROID] Main<T> => Worker => WsComlink');
467
- const ws = await initWsComlink({
468
- workerRef: opts?.workerRefs?.wsWorker,
469
- workerURL: opts?.workerURLs?.['wsWorker'],
470
- });
637
+ // Fire welcome toast early — it only depends on $auth (set by
638
+ // initSupa/bootAuth in the NavBar island) and must not wait for
639
+ // the full worker-init chain which can hang if a SharedWorker
640
+ // fails to respond (Comlink calls have no timeout).
641
+ showWelcomeToast();
642
+
643
+ _initPromise = (async () => {
644
+ try {
645
+ console.log('[DROID] Initializing workers...');
646
+
647
+ const canvasOpts = {
648
+ workerRef: opts?.workerRefs?.canvasWorker,
649
+ workerURL: opts?.workerURLs?.['canvasWorker'],
650
+ };
651
+ const { proxy: canvas, raw: canvasRaw } =
652
+ await initCanvasComlink(canvasOpts);
653
+
654
+ // Store refs for downscale/upscale
655
+ _workerRefs.canvasWorkerRaw = canvasRaw;
656
+ _workerRefs.canvasWorkerProxy = canvas;
657
+ _workerRefs.canvasWorkerOpts = canvasOpts;
658
+
659
+ const { api, isFirstConnection: dbFirst } =
660
+ await initStorageComlink({
661
+ workerURL:
662
+ typeof opts?.workerURLs?.['dbWorker'] === 'string'
663
+ ? opts.workerURLs['dbWorker']
664
+ : undefined,
665
+ workerRef: opts?.workerRefs?.dbWorker,
666
+ i18nPath: opts?.i18nPath,
667
+ dataPath: opts?.dataPath,
668
+ });
471
669
 
472
- console.log('[DROID] Main<T> => Worker => ModManager');
473
- const mod = await getModManager(
474
- (url) => opts?.workerURLs?.[url] ?? url,
475
- );
476
- const events = DroidEvents;
670
+ const { ws, isFirstConnection: wsFirst } = await initWsComlink({
671
+ workerRef: opts?.workerRefs?.wsWorker,
672
+ workerURL: opts?.workerURLs?.['wsWorker'],
673
+ });
477
674
 
478
- for (const handle of Object.values(mod.registry)) {
479
- if (typeof handle.instance.init === 'function') {
480
- await handle.instance.init({
481
- emitFromWorker: uiux.emitFromWorker,
675
+ const isLeaderTab = dbFirst && wsFirst;
676
+
677
+ console.log('[DROID] Initializing mod manager...');
678
+ const modResolver = (url: string) =>
679
+ opts?.workerURLs?.[url] ?? url;
680
+ _workerRefs.modInitOpts = { resolver: modResolver };
681
+ const mod = await getModManager(modResolver);
682
+ const events = DroidEvents;
683
+
684
+ for (const handle of Object.values(mod.registry)) {
685
+ if (typeof handle.instance.init === 'function') {
686
+ await handle.instance.init({
687
+ emitFromWorker: (msg: unknown) =>
688
+ uiux.emitFromWorker(msg as WorkerUIMessage),
689
+ });
690
+ }
691
+ events.emit('droid-mod-ready', {
692
+ meta: handle.meta,
693
+ timestamp: Date.now(),
482
694
  });
483
695
  }
484
- console.log('[Event] -> Fire Mod Ready');
485
- events.emit('droid-mod-ready', {
486
- meta: handle.meta,
487
- timestamp: Date.now(),
488
- });
489
- }
490
696
 
491
- bridgeWsToDb(ws, api);
697
+ bridgeWsToDb(ws, api);
492
698
 
493
- const data = scopeData;
494
- i18n.api = api;
495
- i18n.ready = i18n.hydrateLocale('en');
699
+ const data = scopeData;
700
+ i18n.api = api;
701
+ i18n.ready = i18n.hydrateLocale('en');
496
702
 
497
- window.kbve = {
498
- ...(window.kbve || {}),
499
- api,
500
- i18n,
501
- uiux: { ...uiux, worker: canvas },
502
- ws,
503
- data,
504
- mod,
505
- events,
506
- };
507
-
508
- await i18n.ready;
703
+ // Initialize SupabaseGateway if config is provided
704
+ let gateway: SupabaseGateway | undefined;
705
+ if (opts?.gateway) {
706
+ console.log('[DROID] Initializing SupabaseGateway...');
707
+ _workerRefs.gatewayConfig = opts.gateway;
708
+ gateway = new SupabaseGateway(opts.gateway);
709
+ }
509
710
 
510
- window.kbve.events.emit('droid-ready', {
511
- timestamp: Date.now(),
512
- });
711
+ const overlay = new OverlayManager({
712
+ preferredPath: 'auto',
713
+ canvasWorker: canvas,
714
+ });
513
715
 
514
- document.addEventListener('astro:page-load', () => {
515
- console.debug(
516
- '[KBVE] Re-dispatched droid-ready after astro:page-load',
517
- );
518
- window.kbve?.events.emit('droid-ready', {
716
+ window.kbve = {
717
+ ...(window.kbve || {}),
718
+ api,
719
+ i18n,
720
+ uiux: { ...uiux, worker: canvas },
721
+ ws,
722
+ data,
723
+ mod,
724
+ events,
725
+ overlay,
726
+ downscale,
727
+ upscale,
728
+ scaleLevel: getScaleLevel,
729
+ ...(gateway ? { gateway } : {}),
730
+ };
731
+
732
+ // Sync theme CSS vars to Dexie for worker access
733
+ observeThemeChanges(api);
734
+
735
+ await i18n.ready;
736
+
737
+ window.kbve.events.emit('droid-ready', {
519
738
  timestamp: Date.now(),
520
739
  });
521
- });
522
740
 
523
- console.log('[KBVE] Global API ready');
524
- } catch (err) {
525
- console.error('[DROID] Initialization error:', err);
526
- throw err;
527
- }
741
+ document.addEventListener('astro:page-load', () => {
742
+ console.debug(
743
+ '[KBVE] Re-dispatched droid-ready after astro:page-load',
744
+ );
745
+ window.kbve?.events.emit('droid-ready', {
746
+ timestamp: Date.now(),
747
+ });
748
+ });
749
+
750
+ if (isLeaderTab) {
751
+ events.emit('droid-first-connect', {
752
+ timestamp: Date.now(),
753
+ workersFirst: { db: dbFirst, ws: wsFirst },
754
+ });
755
+ }
756
+
757
+ console.log('[KBVE] Global API ready');
758
+ } catch (err) {
759
+ _initPromise = null;
760
+ console.error('[DROID] Initialization error:', err);
761
+ throw err;
762
+ }
763
+ })();
764
+ await _initPromise;
528
765
  } else {
529
766
  console.log('[KBVE] Already initialized');
530
767
  }