@kbve/droid 0.0.6 → 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 (108) hide show
  1. package/comlink.js +1 -7
  2. package/droid.mjs +191 -61
  3. package/index.d.ts +29 -0
  4. package/index.js +9054 -2763
  5. package/lib/gateway/SupabaseGateway.d.ts +38 -0
  6. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  7. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  8. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  9. package/lib/gateway/WorkerPool.d.ts +55 -0
  10. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  11. package/lib/gateway/capabilities.d.ts +23 -0
  12. package/lib/gateway/capabilities.d.ts.map +1 -0
  13. package/lib/gateway/index.d.ts +9 -0
  14. package/lib/gateway/index.d.ts.map +1 -0
  15. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  16. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  17. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  18. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  19. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  20. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  21. package/lib/gateway/types.d.ts +81 -0
  22. package/lib/gateway/types.d.ts.map +1 -0
  23. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  24. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  25. package/lib/workers/canvas-worker.d.ts +14 -0
  26. package/lib/workers/canvas-worker.d.ts.map +1 -0
  27. package/lib/workers/canvas-worker.js +291 -145
  28. package/lib/workers/data.d.ts +40 -0
  29. package/lib/workers/data.d.ts.map +1 -0
  30. package/lib/workers/db-worker.d.ts +69 -0
  31. package/lib/workers/db-worker.d.ts.map +1 -0
  32. package/lib/workers/db-worker.js +2680 -2147
  33. package/lib/workers/events.d.ts +11 -0
  34. package/lib/workers/events.d.ts.map +1 -0
  35. package/lib/workers/flexbuilder.d.ts +4 -0
  36. package/lib/workers/flexbuilder.d.ts.map +1 -0
  37. package/lib/workers/init.d.ts +11 -0
  38. package/lib/workers/init.d.ts.map +1 -0
  39. package/lib/workers/main.d.ts +107 -0
  40. package/lib/workers/main.d.ts.map +1 -0
  41. package/lib/workers/supabase-db-worker.d.ts +2 -0
  42. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  43. package/lib/workers/supabase-db-worker.js +9337 -0
  44. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  45. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  46. package/lib/workers/supabase-shared-worker.js +12635 -0
  47. package/lib/workers/tools.d.ts +4 -0
  48. package/lib/workers/tools.d.ts.map +1 -0
  49. package/lib/workers/validate.d.ts +15 -0
  50. package/lib/workers/validate.d.ts.map +1 -0
  51. package/lib/workers/ws-worker.d.ts +11 -0
  52. package/lib/workers/ws-worker.d.ts.map +1 -0
  53. package/lib/workers/ws-worker.js +174 -115
  54. package/main.js +4875 -0
  55. package/package.json +17 -6
  56. package/reference.js +688 -228
  57. package/src/index.ts +38 -13
  58. package/src/lib/droid.spec.ts +14 -19
  59. package/src/lib/droid.ts +48 -4
  60. package/src/lib/gateway/SupabaseGateway.ts +183 -0
  61. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  62. package/src/lib/gateway/WorkerPool.ts +304 -0
  63. package/src/lib/gateway/capabilities.ts +80 -0
  64. package/src/lib/gateway/index.ts +15 -0
  65. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  66. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  67. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  68. package/src/lib/gateway/types.ts +112 -0
  69. package/src/lib/mod/mod-manager.ts +13 -8
  70. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  71. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  72. package/src/lib/mod/module/supabase/mod-supabase.ts +68 -0
  73. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  74. package/src/lib/state/auth.spec.ts +58 -0
  75. package/src/lib/state/auth.ts +30 -0
  76. package/src/lib/state/index.ts +34 -0
  77. package/src/lib/state/overlay-manager.ts +125 -0
  78. package/src/lib/state/router.ts +24 -0
  79. package/src/lib/state/theme-sync.ts +75 -0
  80. package/src/lib/state/toasts.spec.ts +106 -0
  81. package/src/lib/state/toasts.ts +34 -0
  82. package/src/lib/state/ui.spec.ts +148 -0
  83. package/src/lib/state/ui.ts +45 -0
  84. package/src/lib/state/welcome-toast.ts +76 -0
  85. package/src/lib/types/bento.spec.ts +116 -0
  86. package/src/lib/types/bento.ts +94 -55
  87. package/src/lib/types/event-types.spec.ts +93 -0
  88. package/src/lib/types/event-types.ts +39 -3
  89. package/src/lib/types/modules.ts +12 -5
  90. package/src/lib/types/ui-event-types.ts +66 -0
  91. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  92. package/src/lib/workers/canvas-worker.ts +94 -9
  93. package/src/lib/workers/data.ts +59 -36
  94. package/src/lib/workers/db-worker.ts +79 -32
  95. package/src/lib/workers/events.spec.ts +208 -0
  96. package/src/lib/workers/events.ts +27 -14
  97. package/src/lib/workers/flexbuilder.ts +2 -2
  98. package/src/lib/workers/init.ts +25 -17
  99. package/src/lib/workers/main.ts +510 -316
  100. package/src/lib/workers/supabase-db-worker.ts +221 -0
  101. package/src/lib/workers/supabase-shared-worker.ts +572 -0
  102. package/src/lib/workers/validate.ts +64 -0
  103. package/src/lib/workers/ws-worker.ts +156 -25
  104. package/src/setup-vitest.ts +4 -2
  105. package/src/types.ts +30 -0
  106. package/src/vite-env.d.ts +1 -0
  107. package/workers/main.js +10 -1123
  108. package/src/types.d.ts +0 -31
@@ -1,119 +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;
33
-
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;
213
+ // --- Worker Init Helpers ---
214
+
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
  }
285
+
44
286
  async function initWsComlink(opts?: {
45
- workerRef?: SharedWorker;
46
- workerURL?: string;
47
- }): Promise<Remote<WSInstance>> {
48
- let api: Remote<WSInstance> | null = null;
49
-
50
- // 1. Try provided SharedWorker reference
51
- if (opts?.workerRef) {
52
- try {
53
- opts.workerRef.port.start();
54
- api = wrap<WSInstance>(opts.workerRef.port);
55
- return api;
56
- } catch (err) {
57
- console.warn('[DROID] ws-worker workerRef failed:', err);
58
- }
59
- }
60
-
61
- // 2. Try provided URL (e.g. from CDN or blob)
62
- if (opts?.workerURL) {
63
- try {
64
- const worker = new SharedWorker(opts.workerURL, { type: 'module' });
65
- worker.port.start();
66
- api = wrap<WSInstance>(worker.port);
67
- return api;
68
- } catch (err) {
69
- console.warn('[DROID] ws-worker workerURL failed:', err);
70
- }
71
- }
72
-
73
- // 3. Vite-style TS import
74
- try {
75
- const worker = new SharedWorker(
76
- new URL('./ws-worker.ts', import.meta.url),
77
- { type: 'module' },
78
- );
79
- worker.port.start();
80
- api = wrap<WSInstance>(worker.port);
81
- return api;
82
- } catch (err) {
83
- console.warn('[DROID] ws-worker vite-style .ts import failed:', err);
84
- }
85
-
86
- // 4. Same-dir JS fallback (post-build)
87
- try {
88
- const worker = new SharedWorker(
89
- new URL('./ws-worker.js', import.meta.url),
90
- { type: 'module' },
91
- );
92
- worker.port.start();
93
- api = wrap<WSInstance>(worker.port);
94
- return api;
95
- } catch (err) {
96
- console.warn('[DROID] ws-worker ./ws-worker.js fallback failed:', err);
97
- }
98
-
99
- // 5. Browser-root hardcoded path (static /public/)
100
- try {
101
- const worker = new SharedWorker('/ws-worker.js', { type: 'module' });
102
- worker.port.start();
103
- api = wrap<WSInstance>(worker.port);
104
- return api;
105
- } catch (err) {
106
- console.warn('[DROID] ws-worker absolute hardcoded fallback failed:', err);
107
- }
108
-
109
- // 6. Final failure
110
- console.error('[DROID] No WS Worker Comlink Initialized');
111
- throw new Error('[DROID] Failed to initialize ws-worker');
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 };
112
295
  }
113
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
+ }
114
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 ---
115
323
 
116
- // * UIUX
117
324
  const uiuxState = persistentMap<{
118
325
  panelManager: Record<
119
326
  PanelId,
@@ -123,7 +330,7 @@ const uiuxState = persistentMap<{
123
330
  }
124
331
  >;
125
332
  themeManager: { theme: 'light' | 'dark' | 'auto' };
126
- toastManager: Record<string, any>;
333
+ toastManager: Record<string, unknown>;
127
334
  scrollY: number;
128
335
  }>(
129
336
  'uiux-state',
@@ -144,84 +351,20 @@ const uiuxState = persistentMap<{
144
351
  },
145
352
  );
146
353
 
147
- async function initCanvasComlink(opts?: {
148
- workerRef?: Worker;
149
- workerURL?: string;
150
- }): Promise<Remote<CanvasWorkerAPI>> {
151
- let api: Remote<CanvasWorkerAPI> | null = null;
152
-
153
- // 1. Try direct Worker reference
154
- if (opts?.workerRef) {
155
- try {
156
- api = wrap<CanvasWorkerAPI>(opts.workerRef);
157
- return api;
158
- } catch (err) {
159
- console.warn('[DROID] Provided canvasWorkerRef failed:', err);
160
- }
161
- }
162
-
163
- // 2. Try provided worker URL
164
- if (opts?.workerURL) {
165
- try {
166
- const worker = new Worker(opts.workerURL, { type: 'module' });
167
- api = wrap<CanvasWorkerAPI>(worker);
168
- return api;
169
- } catch (err) {
170
- console.warn('[DROID] Provided canvasWorkerURL failed:', err);
171
- }
172
- }
173
-
174
- // 3. Vite-style dev import (.ts)
175
- try {
176
- const worker = new Worker(
177
- new URL('./canvas-worker.ts', import.meta.url),
178
- { type: 'module' },
179
- );
180
- api = wrap<CanvasWorkerAPI>(worker);
181
- return api;
182
- } catch (err) {
183
- console.warn('[DROID] Vite-style canvas-worker.ts import failed:', err);
184
- }
185
-
186
- // 4. Same-dir .js fallback (build output)
187
- try {
188
- const worker = new Worker(
189
- new URL('./canvas-worker.js', import.meta.url),
190
- { type: 'module' },
191
- );
192
- api = wrap<CanvasWorkerAPI>(worker);
193
- return api;
194
- } catch (err) {
195
- console.warn('[DROID] canvas-worker.js (same dir) fallback failed:', err);
196
- }
197
-
198
- // 5. Runtime hardcoded absolute path (public/)
199
- try {
200
- const worker = new Worker('/canvas-worker.js', { type: 'module' });
201
- api = wrap<CanvasWorkerAPI>(worker);
202
- return api;
203
- } catch (err) {
204
- console.warn('[DROID] canvas-worker.js (absolute root) fallback failed:', err);
205
- }
206
-
207
- // 6. Final failure
208
- console.error('[DROID] No Canvas Comlink Initialized');
209
- throw new Error('[DROID] Failed to initialize canvas worker');
210
- }
211
-
212
-
213
354
  export const uiux = {
214
355
  state: uiuxState,
215
356
  openPanel(id: PanelId, payload?: PanelPayload) {
216
357
  const panels = { ...uiuxState.get().panelManager };
217
358
  panels[id] = { open: true, payload };
218
359
  uiuxState.setKey('panelManager', panels);
360
+ DroidEvents.emit('panel-open', { id, payload });
219
361
  },
220
362
 
221
363
  closePanel(id: PanelId) {
222
364
  const panels = { ...uiuxState.get().panelManager };
223
365
  panels[id] = { open: false, payload: undefined };
224
366
  uiuxState.setKey('panelManager', panels);
367
+ DroidEvents.emit('panel-close', { id });
225
368
  },
226
369
 
227
370
  togglePanel(id: PanelId, payload?: PanelPayload) {
@@ -229,44 +372,61 @@ export const uiux = {
229
372
  const isOpen = panels[id]?.open ?? false;
230
373
  panels[id] = { open: !isOpen, payload: !isOpen ? payload : undefined };
231
374
  uiuxState.setKey('panelManager', panels);
375
+ if (!isOpen) {
376
+ DroidEvents.emit('panel-open', { id, payload });
377
+ } else {
378
+ DroidEvents.emit('panel-close', { id });
379
+ }
232
380
  },
233
381
 
234
382
  setTheme(theme: 'light' | 'dark' | 'auto') {
235
383
  uiuxState.setKey('themeManager', { theme });
236
384
  },
237
385
 
238
- 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
+ );
239
391
  const toasts = { ...uiuxState.get().toastManager, [id]: data };
240
392
  uiuxState.setKey('toastManager', toasts);
241
393
  },
242
394
 
395
+ /** @deprecated Use removeToast() from '@kbve/droid' state exports instead. */
243
396
  removeToast(id: string) {
397
+ console.warn(
398
+ '[KBVE] uiux.removeToast is deprecated. Use removeToast() from @kbve/droid.',
399
+ );
244
400
  const toasts = { ...uiuxState.get().toastManager };
245
401
  delete toasts[id];
246
402
  uiuxState.setKey('toastManager', toasts);
247
403
  },
404
+
248
405
  async dispatchCanvasRequest(
249
406
  panelId: PanelId,
250
407
  canvasEl: HTMLCanvasElement,
251
408
  mode: 'static' | 'animated' | 'dynamic' = 'animated',
252
409
  ) {
253
410
  const offscreen = canvasEl.transferControlToOffscreen();
254
- 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);
255
415
  },
256
416
 
257
417
  closeAllPanels() {
258
418
  const panels = { ...uiuxState.get().panelManager };
259
- console.log('error panel is closing');
260
-
261
419
  for (const id of Object.keys(panels) as Array<PanelId>) {
262
420
  panels[id] = { open: false, payload: undefined };
263
421
  }
264
-
265
422
  uiuxState.setKey('panelManager', panels);
266
423
  },
267
424
 
268
- emitFromWorker(msg: any) {
269
- 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;
270
430
  dispatchAsync(() => {
271
431
  const target = document.getElementById('bento-grid-inject');
272
432
  if (!target) {
@@ -276,20 +436,80 @@ export const uiux = {
276
436
  return;
277
437
  }
278
438
 
279
- const el = renderVNode(msg.vnode);
439
+ const el = renderVNode(vnode);
280
440
  el.classList.add('animate-fade-in');
281
- if (msg.vnode.id) {
282
- const existing = document.getElementById(msg.vnode.id);
441
+ if (vnode.id) {
442
+ const existing = document.getElementById(vnode.id);
283
443
  if (existing) existing.remove();
284
444
  }
285
445
 
286
446
  target.appendChild(el);
287
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;
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;
288
506
  }
507
+
508
+ console.warn('[KBVE] Unknown worker UI message type:', msg.type);
289
509
  },
290
510
  };
291
511
 
292
- // * i18n
512
+ // --- i18n ---
293
513
 
294
514
  const i18nStore = persistentMap<Record<string, string>>(
295
515
  'i18n-cache',
@@ -348,7 +568,6 @@ export const i18n = {
348
568
  const translations = await this.api.getTranslations(localeKeys);
349
569
 
350
570
  for (const [key, value] of Object.entries(translations)) {
351
- console.log(`[i18n.setKey] ${key} = ${value}`);
352
571
  this.store.setKey(key, value);
353
572
  }
354
573
  },
@@ -362,107 +581,24 @@ function initSWComlink() {
362
581
  ]);
363
582
  channel.port1.start();
364
583
  }
365
- async function initStorageComlink(opts?: {
366
- workerRef?: SharedWorker;
367
- workerURL?: string;
368
- }): Promise<Remote<LocalStorageAPI>> {
369
- let api: Remote<LocalStorageAPI> | null = null;
370
-
371
- // 1. Try provided SharedWorker reference
372
- if (opts?.workerRef) {
373
- try {
374
- opts.workerRef.port.start();
375
- api = wrap<LocalStorageAPI>(opts.workerRef.port);
376
- return await finalize(api);
377
- } catch (err) {
378
- console.warn('[DROID] db-worker workerRef failed:', err);
379
- }
380
- }
381
-
382
- // 2. Try provided worker URL
383
- if (opts?.workerURL) {
384
- try {
385
- const worker = new SharedWorker(opts.workerURL, { type: 'module' });
386
- worker.port.start();
387
- api = wrap<LocalStorageAPI>(worker.port);
388
- return await finalize(api);
389
- } catch (err) {
390
- console.warn('[DROID] db-worker workerURL failed:', err);
391
- }
392
- }
393
-
394
- // 3. Vite-style TS dev import
395
- try {
396
- const worker = new SharedWorker(
397
- new URL('./db-worker.ts', import.meta.url),
398
- { type: 'module' },
399
- );
400
- worker.port.start();
401
- api = wrap<LocalStorageAPI>(worker.port);
402
- return await finalize(api);
403
- } catch (err) {
404
- console.warn('[DROID] db-worker .ts vite-style import failed:', err);
405
- }
406
-
407
- // 4. Same-dir .js fallback (production build output)
408
- try {
409
- const worker = new SharedWorker(
410
- new URL('./db-worker.js', import.meta.url),
411
- { type: 'module' },
412
- );
413
- worker.port.start();
414
- api = wrap<LocalStorageAPI>(worker.port);
415
- return await finalize(api);
416
- } catch (err) {
417
- console.warn('[DROID] db-worker.js same-dir fallback failed:', err);
418
- }
419
-
420
- // 5. Hardcoded browser-root fallback (e.g. /public)
421
- try {
422
- const worker = new SharedWorker('/db-worker.js', { type: 'module' });
423
- worker.port.start();
424
- api = wrap<LocalStorageAPI>(worker.port);
425
- return await finalize(api);
426
- } catch (err) {
427
- console.warn('[DROID] db-worker.js absolute hardcoded fallback failed:', err);
428
- }
429
-
430
- // 6. Final failure
431
- console.error('[DROID] No DB Worker Comlink Initialized');
432
- throw new Error('[DROID] Failed to initialize db-worker');
433
- }
434
-
435
-
436
- // Internal helper to finalize storage worker setup
437
- async function finalize(api: Remote<LocalStorageAPI>): Promise<Remote<LocalStorageAPI>> {
438
- const version = await api.getVersion();
439
- if (version !== EXPECTED_DB_VERSION) {
440
- await initializeWorkerDatabase(api, {
441
- version: EXPECTED_DB_VERSION,
442
- i18nPath: 'https://discord.sh/i18n/db.json',
443
- locale: 'en',
444
- defaults: { welcome: 'Welcome!', theme: 'dark' },
445
- });
446
- }
447
- return api;
448
- }
449
584
 
450
- // * Bridge
585
+ // --- Bridge ---
451
586
  export function bridgeWsToDb(
452
587
  ws: Remote<WSInstance>,
453
588
  db: Remote<LocalStorageAPI>,
454
589
  ) {
455
- const handler = proxy(async (buf: ArrayBuffer) => {
590
+ const handler = proxy(async (data: string | ArrayBuffer) => {
591
+ if (typeof data === 'string') return;
456
592
  dispatchAsync(() => {
457
593
  const key = `ws:${Date.now()}`;
458
- void db.storeWsMessage(key, buf);
594
+ void db.storeWsMessage(key, data);
459
595
  });
460
596
  });
461
597
 
462
- ws.onMessage(transfer(handler, [0]));
598
+ ws.onMessage(handler);
463
599
  }
464
600
 
465
- // * MAIN
601
+ // --- MAIN ---
466
602
  export async function main(opts?: {
467
603
  workerURLs?: Record<string, string>;
468
604
  workerRefs?: {
@@ -470,13 +606,15 @@ export async function main(opts?: {
470
606
  dbWorker?: SharedWorker;
471
607
  wsWorker?: SharedWorker;
472
608
  };
609
+ gateway?: GatewayConfig;
610
+ i18nPath?: string;
611
+ dataPath?: string;
473
612
  }) {
474
613
  console.log('[DROID]: Main<T>');
475
614
 
476
615
  if (!initialized) {
477
616
  initialized = true;
478
617
 
479
- // Attach to existing service worker (or wait for one to take control)
480
618
  if (navigator.serviceWorker?.controller) {
481
619
  initSWComlink();
482
620
  } else {
@@ -487,87 +625,143 @@ export async function main(opts?: {
487
625
  }
488
626
  }
489
627
 
490
- console.log('[DROID] Main<T> => Worker URLs', opts?.workerURLs);
628
+ if (_initPromise) {
629
+ await _initPromise;
630
+ return;
631
+ }
491
632
 
492
633
  const needsInit =
493
634
  !window.kbve?.api || !window.kbve?.i18n || !window.kbve?.uiux;
494
635
 
495
636
  if (needsInit) {
496
- try {
497
- console.log('[DROID] Main<T> => Worker => CanvasComlink');
498
- const canvas = await initCanvasComlink({
499
- workerRef: opts?.workerRefs?.canvasWorker,
500
- workerURL: opts?.workerURLs?.['canvasWorker'],
501
- });
502
- console.log('[DROID] Main<T> => Worker => StorageComlink');
503
- const api = await initStorageComlink({
504
- workerURL: typeof opts?.workerURLs?.['dbWorker'] === 'string'
505
- ? opts.workerURLs['dbWorker']
506
- : undefined,
507
- workerRef: opts?.workerRefs?.dbWorker,
508
- });
509
- console.log('[DROID] Main<T> => Worker => WsComlink');
510
- const ws = await initWsComlink({
511
- workerRef: opts?.workerRefs?.wsWorker,
512
- workerURL: opts?.workerURLs?.['wsWorker'],
513
- });
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
+ });
514
669
 
515
- console.log('[DROID] Main<T> => Worker => ModManager');
516
- const mod = await getModManager(
517
- (url) => opts?.workerURLs?.[url] ?? url,
518
- );
519
- const events = DroidEvents;
670
+ const { ws, isFirstConnection: wsFirst } = await initWsComlink({
671
+ workerRef: opts?.workerRefs?.wsWorker,
672
+ workerURL: opts?.workerURLs?.['wsWorker'],
673
+ });
520
674
 
521
- for (const handle of Object.values(mod.registry)) {
522
- if (typeof handle.instance.init === 'function') {
523
- await handle.instance.init({
524
- 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(),
525
694
  });
526
695
  }
527
- console.log('[Event] -> Fire Mod Ready');
528
- events.emit('droid-mod-ready', {
529
- meta: handle.meta,
530
- timestamp: Date.now(),
531
- });
532
- }
533
-
534
- bridgeWsToDb(ws, api);
535
696
 
536
- const data = scopeData;
537
- i18n.api = api;
538
- i18n.ready = i18n.hydrateLocale('en');
697
+ bridgeWsToDb(ws, api);
539
698
 
540
- window.kbve = {
541
- ...(window.kbve || {}),
542
- api,
543
- i18n,
544
- uiux: { ...uiux, worker: canvas },
545
- ws,
546
- data,
547
- mod,
548
- events,
549
- };
699
+ const data = scopeData;
700
+ i18n.api = api;
701
+ i18n.ready = i18n.hydrateLocale('en');
550
702
 
551
- 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
+ }
552
710
 
553
- window.kbve.events.emit('droid-ready', {
554
- timestamp: Date.now(),
555
- });
711
+ const overlay = new OverlayManager({
712
+ preferredPath: 'auto',
713
+ canvasWorker: canvas,
714
+ });
556
715
 
557
- document.addEventListener('astro:page-load', () => {
558
- console.debug(
559
- '[KBVE] Re-dispatched droid-ready after astro:page-load',
560
- );
561
- 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', {
562
738
  timestamp: Date.now(),
563
739
  });
564
- });
565
740
 
566
- console.log('[KBVE] Global API ready');
567
- } catch (err) {
568
- console.error('[DROID] Initialization error:', err);
569
- throw err;
570
- }
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;
571
765
  } else {
572
766
  console.log('[KBVE] Already initialized');
573
767
  }