@kbve/droid 0.0.6 → 0.1.2

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 (190) hide show
  1. package/droid.mjs +2312 -74
  2. package/index.d.ts +32 -0
  3. package/lib/agents/api-types.d.ts +121 -0
  4. package/lib/agents/api-types.d.ts.map +1 -0
  5. package/lib/agents/auth/init.d.ts +8 -0
  6. package/lib/agents/auth/init.d.ts.map +1 -0
  7. package/lib/agents/cache.d.ts +9 -0
  8. package/lib/agents/cache.d.ts.map +1 -0
  9. package/lib/agents/client/callGuildVault.d.ts +3 -0
  10. package/lib/agents/client/callGuildVault.d.ts.map +1 -0
  11. package/lib/agents/client/callJson.d.ts +3 -0
  12. package/lib/agents/client/callJson.d.ts.map +1 -0
  13. package/lib/agents/config.d.ts +36 -0
  14. package/lib/agents/config.d.ts.map +1 -0
  15. package/lib/agents/constants.d.ts +11 -0
  16. package/lib/agents/constants.d.ts.map +1 -0
  17. package/lib/agents/createAgents.d.ts +4 -0
  18. package/lib/agents/createAgents.d.ts.map +1 -0
  19. package/lib/agents/ctx.d.ts +46 -0
  20. package/lib/agents/ctx.d.ts.map +1 -0
  21. package/lib/agents/discord/bot.d.ts +21 -0
  22. package/lib/agents/discord/bot.d.ts.map +1 -0
  23. package/lib/agents/discord/guilds.d.ts +8 -0
  24. package/lib/agents/discord/guilds.d.ts.map +1 -0
  25. package/lib/agents/formDrafts.d.ts +5 -0
  26. package/lib/agents/formDrafts.d.ts.map +1 -0
  27. package/lib/agents/generated/agents-schema.d.ts +73 -0
  28. package/lib/agents/generated/agents-schema.d.ts.map +1 -0
  29. package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
  30. package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
  31. package/lib/agents/github/backfill.d.ts +23 -0
  32. package/lib/agents/github/backfill.d.ts.map +1 -0
  33. package/lib/agents/github/events.d.ts +10 -0
  34. package/lib/agents/github/events.d.ts.map +1 -0
  35. package/lib/agents/github/pat.d.ts +13 -0
  36. package/lib/agents/github/pat.d.ts.map +1 -0
  37. package/lib/agents/github/repoConfig.d.ts +25 -0
  38. package/lib/agents/github/repoConfig.d.ts.map +1 -0
  39. package/lib/agents/github/tokens.d.ts +22 -0
  40. package/lib/agents/github/tokens.d.ts.map +1 -0
  41. package/lib/agents/github/webhook.d.ts +37 -0
  42. package/lib/agents/github/webhook.d.ts.map +1 -0
  43. package/lib/agents/index.d.ts +8 -0
  44. package/lib/agents/index.d.ts.map +1 -0
  45. package/lib/agents/state/atoms.d.ts +122 -0
  46. package/lib/agents/state/atoms.d.ts.map +1 -0
  47. package/lib/agents/types.d.ts +65 -0
  48. package/lib/agents/types.d.ts.map +1 -0
  49. package/lib/gateway/SupabaseGateway.d.ts +38 -0
  50. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  51. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  52. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  53. package/lib/gateway/WorkerPool.d.ts +55 -0
  54. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  55. package/lib/gateway/capabilities.d.ts +23 -0
  56. package/lib/gateway/capabilities.d.ts.map +1 -0
  57. package/lib/gateway/index.d.ts +9 -0
  58. package/lib/gateway/index.d.ts.map +1 -0
  59. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  60. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  61. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  62. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  63. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  64. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  65. package/lib/gateway/types.d.ts +81 -0
  66. package/lib/gateway/types.d.ts.map +1 -0
  67. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  68. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  69. package/lib/workers/canvas-worker.d.ts +14 -0
  70. package/lib/workers/canvas-worker.d.ts.map +1 -0
  71. package/lib/workers/canvas-worker.js +5 -302
  72. package/lib/workers/data.d.ts +40 -0
  73. package/lib/workers/data.d.ts.map +1 -0
  74. package/lib/workers/db-worker.d.ts +69 -0
  75. package/lib/workers/db-worker.d.ts.map +1 -0
  76. package/lib/workers/db-worker.js +13 -3636
  77. package/lib/workers/events.d.ts +11 -0
  78. package/lib/workers/events.d.ts.map +1 -0
  79. package/lib/workers/flexbuilder.d.ts +4 -0
  80. package/lib/workers/flexbuilder.d.ts.map +1 -0
  81. package/lib/workers/init.d.ts +11 -0
  82. package/lib/workers/init.d.ts.map +1 -0
  83. package/lib/workers/main.d.ts +105 -0
  84. package/lib/workers/main.d.ts.map +1 -0
  85. package/lib/workers/supabase-db-worker.d.ts +2 -0
  86. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  87. package/lib/workers/supabase-db-worker.js +23 -0
  88. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  89. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  90. package/lib/workers/supabase-shared-worker.js +25 -0
  91. package/lib/workers/tools.d.ts +4 -0
  92. package/lib/workers/tools.d.ts.map +1 -0
  93. package/lib/workers/validate.d.ts +15 -0
  94. package/lib/workers/validate.d.ts.map +1 -0
  95. package/lib/workers/ws-worker.d.ts +10 -0
  96. package/lib/workers/ws-worker.d.ts.map +1 -0
  97. package/lib/workers/ws-worker.js +5 -273
  98. package/main.js +5711 -0
  99. package/package.json +18 -6
  100. package/src/index.ts +41 -13
  101. package/src/lib/agents/api-types.ts +170 -0
  102. package/src/lib/agents/auth/init.ts +200 -0
  103. package/src/lib/agents/cache.ts +127 -0
  104. package/src/lib/agents/client/callGuildVault.ts +71 -0
  105. package/src/lib/agents/client/callJson.ts +64 -0
  106. package/src/lib/agents/config.ts +83 -0
  107. package/src/lib/agents/constants.ts +11 -0
  108. package/src/lib/agents/createAgents.ts +62 -0
  109. package/src/lib/agents/ctx.ts +63 -0
  110. package/src/lib/agents/discord/bot.ts +169 -0
  111. package/src/lib/agents/discord/guilds.ts +106 -0
  112. package/src/lib/agents/formDrafts.ts +55 -0
  113. package/src/lib/agents/generated/agents-schema.ts +155 -0
  114. package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
  115. package/src/lib/agents/github/backfill.ts +126 -0
  116. package/src/lib/agents/github/events.ts +131 -0
  117. package/src/lib/agents/github/pat.ts +150 -0
  118. package/src/lib/agents/github/repoConfig.ts +240 -0
  119. package/src/lib/agents/github/tokens.ts +222 -0
  120. package/src/lib/agents/github/webhook.ts +349 -0
  121. package/src/lib/agents/index.ts +35 -0
  122. package/src/lib/agents/state/atoms.ts +122 -0
  123. package/src/lib/agents/types.ts +83 -0
  124. package/src/lib/api.ts +71 -0
  125. package/src/lib/droid.spec.ts +14 -19
  126. package/src/lib/droid.ts +48 -4
  127. package/src/lib/gateway/SupabaseGateway.ts +236 -0
  128. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  129. package/src/lib/gateway/WorkerPool.ts +304 -0
  130. package/src/lib/gateway/capabilities.spec.ts +144 -0
  131. package/src/lib/gateway/capabilities.ts +80 -0
  132. package/src/lib/gateway/index.ts +15 -0
  133. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  134. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  135. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  136. package/src/lib/gateway/types.ts +139 -0
  137. package/src/lib/mod/mod-manager.ts +13 -8
  138. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  139. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  140. package/src/lib/mod/module/supabase/mod-supabase.ts +73 -0
  141. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  142. package/src/lib/state/auth.spec.ts +58 -0
  143. package/src/lib/state/auth.ts +148 -0
  144. package/src/lib/state/bento.spec.ts +76 -0
  145. package/src/lib/state/bento.ts +51 -0
  146. package/src/lib/state/index.ts +98 -0
  147. package/src/lib/state/overlay-manager.ts +125 -0
  148. package/src/lib/state/profile-sync.ts +214 -0
  149. package/src/lib/state/profile.ts +155 -0
  150. package/src/lib/state/router.ts +24 -0
  151. package/src/lib/state/staff.ts +128 -0
  152. package/src/lib/state/sync-bus.ts +109 -0
  153. package/src/lib/state/theme-sync.ts +75 -0
  154. package/src/lib/state/toasts.spec.ts +106 -0
  155. package/src/lib/state/toasts.ts +34 -0
  156. package/src/lib/state/ui.spec.ts +148 -0
  157. package/src/lib/state/ui.ts +45 -0
  158. package/src/lib/state/welcome-toast.spec.ts +101 -0
  159. package/src/lib/state/welcome-toast.ts +76 -0
  160. package/src/lib/sw-recovery.spec.ts +165 -0
  161. package/src/lib/sw-recovery.ts +141 -0
  162. package/src/lib/types/bento.spec.ts +116 -0
  163. package/src/lib/types/bento.ts +94 -55
  164. package/src/lib/types/event-schemas.spec.ts +120 -0
  165. package/src/lib/types/event-types.spec.ts +96 -0
  166. package/src/lib/types/event-types.ts +113 -3
  167. package/src/lib/types/modules.ts +12 -5
  168. package/src/lib/types/ui-event-types.ts +66 -0
  169. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  170. package/src/lib/workers/canvas-worker.ts +94 -9
  171. package/src/lib/workers/data.ts +64 -37
  172. package/src/lib/workers/db-worker.ts +105 -32
  173. package/src/lib/workers/events.spec.ts +208 -0
  174. package/src/lib/workers/events.ts +27 -14
  175. package/src/lib/workers/flexbuilder.ts +2 -2
  176. package/src/lib/workers/init.ts +25 -17
  177. package/src/lib/workers/main.ts +506 -327
  178. package/src/lib/workers/supabase-db-worker.ts +243 -0
  179. package/src/lib/workers/supabase-shared-worker.ts +607 -0
  180. package/src/lib/workers/validate.ts +64 -0
  181. package/src/lib/workers/ws-worker.spec.ts +41 -0
  182. package/src/lib/workers/ws-worker.ts +255 -31
  183. package/src/setup-vitest.ts +4 -2
  184. package/src/types.ts +30 -0
  185. package/src/vite-env.d.ts +1 -0
  186. package/workers/main.js +2 -1125
  187. package/comlink.js +0 -253
  188. package/index.js +0 -2972
  189. package/reference.js +0 -259
  190. package/src/types.d.ts +0 -31
@@ -1,119 +1,326 @@
1
- import { wrap, transfer, proxy } from 'comlink';
1
+ import { wrap } 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;
43
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 };
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 };
295
+ }
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 };
112
303
  }
113
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
+ }
114
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;
288
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);
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,8 @@ 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
584
 
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
-
450
- // * Bridge
451
- export function bridgeWsToDb(
452
- ws: Remote<WSInstance>,
453
- db: Remote<LocalStorageAPI>,
454
- ) {
455
- const handler = proxy(async (buf: ArrayBuffer) => {
456
- dispatchAsync(() => {
457
- const key = `ws:${Date.now()}`;
458
- void db.storeWsMessage(key, buf);
459
- });
460
- });
461
-
462
- ws.onMessage(transfer(handler, [0]));
463
- }
464
-
465
- // * MAIN
585
+ // --- MAIN ---
466
586
  export async function main(opts?: {
467
587
  workerURLs?: Record<string, string>;
468
588
  workerRefs?: {
@@ -470,13 +590,15 @@ export async function main(opts?: {
470
590
  dbWorker?: SharedWorker;
471
591
  wsWorker?: SharedWorker;
472
592
  };
593
+ gateway?: GatewayConfig;
594
+ i18nPath?: string;
595
+ dataPath?: string;
473
596
  }) {
474
597
  console.log('[DROID]: Main<T>');
475
598
 
476
599
  if (!initialized) {
477
600
  initialized = true;
478
601
 
479
- // Attach to existing service worker (or wait for one to take control)
480
602
  if (navigator.serviceWorker?.controller) {
481
603
  initSWComlink();
482
604
  } else {
@@ -487,87 +609,144 @@ export async function main(opts?: {
487
609
  }
488
610
  }
489
611
 
490
- console.log('[DROID] Main<T> => Worker URLs', opts?.workerURLs);
612
+ if (_initPromise) {
613
+ await _initPromise;
614
+ return;
615
+ }
491
616
 
492
617
  const needsInit =
493
618
  !window.kbve?.api || !window.kbve?.i18n || !window.kbve?.uiux;
494
619
 
495
620
  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
- });
621
+ // Fire welcome toast early — it only depends on $auth (set by
622
+ // initSupa/bootAuth in the NavBar island) and must not wait for
623
+ // the full worker-init chain which can hang if a SharedWorker
624
+ // fails to respond (Comlink calls have no timeout).
625
+ showWelcomeToast();
626
+
627
+ _initPromise = (async () => {
628
+ try {
629
+ console.log('[DROID] Initializing workers...');
630
+
631
+ const canvasOpts = {
632
+ workerRef: opts?.workerRefs?.canvasWorker,
633
+ workerURL: opts?.workerURLs?.['canvasWorker'],
634
+ };
635
+ const { proxy: canvas, raw: canvasRaw } =
636
+ await initCanvasComlink(canvasOpts);
637
+
638
+ // Store refs for downscale/upscale
639
+ _workerRefs.canvasWorkerRaw = canvasRaw;
640
+ _workerRefs.canvasWorkerProxy = canvas;
641
+ _workerRefs.canvasWorkerOpts = canvasOpts;
642
+
643
+ const { api, isFirstConnection: dbFirst } =
644
+ await initStorageComlink({
645
+ workerURL:
646
+ typeof opts?.workerURLs?.['dbWorker'] === 'string'
647
+ ? opts.workerURLs['dbWorker']
648
+ : undefined,
649
+ workerRef: opts?.workerRefs?.dbWorker,
650
+ i18nPath: opts?.i18nPath,
651
+ dataPath: opts?.dataPath,
652
+ });
514
653
 
515
- console.log('[DROID] Main<T> => Worker => ModManager');
516
- const mod = await getModManager(
517
- (url) => opts?.workerURLs?.[url] ?? url,
518
- );
519
- const events = DroidEvents;
654
+ const { ws, isFirstConnection: wsFirst } = await initWsComlink({
655
+ workerRef: opts?.workerRefs?.wsWorker,
656
+ workerURL: opts?.workerURLs?.['wsWorker'],
657
+ });
520
658
 
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,
659
+ const isLeaderTab = dbFirst && wsFirst;
660
+
661
+ console.log('[DROID] Initializing mod manager...');
662
+ const modResolver = (url: string) =>
663
+ opts?.workerURLs?.[url] ?? url;
664
+ _workerRefs.modInitOpts = { resolver: modResolver };
665
+ const mod = await getModManager(modResolver);
666
+ const events = DroidEvents;
667
+
668
+ for (const handle of Object.values(mod.registry)) {
669
+ if (typeof handle.instance.init === 'function') {
670
+ await handle.instance.init({
671
+ emitFromWorker: (msg: unknown) =>
672
+ uiux.emitFromWorker(msg as WorkerUIMessage),
673
+ });
674
+ }
675
+ events.emit('droid-mod-ready', {
676
+ meta: handle.meta,
677
+ timestamp: Date.now(),
525
678
  });
526
679
  }
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
680
 
536
- const data = scopeData;
537
- i18n.api = api;
538
- i18n.ready = i18n.hydrateLocale('en');
681
+ // ws-worker db-worker storage now happens directly via
682
+ // BroadcastChannel ('kbve_ws_data'), no main-thread bridge needed.
539
683
 
540
- window.kbve = {
541
- ...(window.kbve || {}),
542
- api,
543
- i18n,
544
- uiux: { ...uiux, worker: canvas },
545
- ws,
546
- data,
547
- mod,
548
- events,
549
- };
684
+ const data = scopeData;
685
+ i18n.api = api;
686
+ i18n.ready = i18n.hydrateLocale('en');
550
687
 
551
- await i18n.ready;
688
+ // Initialize SupabaseGateway if config is provided
689
+ let gateway: SupabaseGateway | undefined;
690
+ if (opts?.gateway) {
691
+ console.log('[DROID] Initializing SupabaseGateway...');
692
+ _workerRefs.gatewayConfig = opts.gateway;
693
+ gateway = new SupabaseGateway(opts.gateway);
694
+ }
552
695
 
553
- window.kbve.events.emit('droid-ready', {
554
- timestamp: Date.now(),
555
- });
696
+ const overlay = new OverlayManager({
697
+ preferredPath: 'auto',
698
+ canvasWorker: canvas,
699
+ });
556
700
 
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', {
701
+ window.kbve = {
702
+ ...(window.kbve || {}),
703
+ api,
704
+ i18n,
705
+ uiux: { ...uiux, worker: canvas },
706
+ ws,
707
+ data,
708
+ mod,
709
+ events,
710
+ overlay,
711
+ downscale,
712
+ upscale,
713
+ scaleLevel: getScaleLevel,
714
+ ...(gateway ? { gateway } : {}),
715
+ };
716
+
717
+ // Sync theme CSS vars to Dexie for worker access
718
+ observeThemeChanges(api);
719
+
720
+ await i18n.ready;
721
+
722
+ window.kbve.events.emit('droid-ready', {
562
723
  timestamp: Date.now(),
563
724
  });
564
- });
565
725
 
566
- console.log('[KBVE] Global API ready');
567
- } catch (err) {
568
- console.error('[DROID] Initialization error:', err);
569
- throw err;
570
- }
726
+ document.addEventListener('astro:page-load', () => {
727
+ console.debug(
728
+ '[KBVE] Re-dispatched droid-ready after astro:page-load',
729
+ );
730
+ window.kbve?.events.emit('droid-ready', {
731
+ timestamp: Date.now(),
732
+ });
733
+ });
734
+
735
+ if (isLeaderTab) {
736
+ events.emit('droid-first-connect', {
737
+ timestamp: Date.now(),
738
+ workersFirst: { db: dbFirst, ws: wsFirst },
739
+ });
740
+ }
741
+
742
+ console.log('[KBVE] Global API ready');
743
+ } catch (err) {
744
+ _initPromise = null;
745
+ console.error('[DROID] Initialization error:', err);
746
+ throw err;
747
+ }
748
+ })();
749
+ await _initPromise;
571
750
  } else {
572
751
  console.log('[KBVE] Already initialized');
573
752
  }