@kbve/droid 0.0.1 → 0.0.3

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 (38) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +5 -1
  3. package/assets/canvas-worker-DbsoY2Xv.js +5 -0
  4. package/assets/db-worker-CjXS52DQ.js +14 -0
  5. package/assets/ws-worker-FssAQgaD.js +5 -0
  6. package/comlink-CC72iIUO.js +253 -0
  7. package/droid.es.js +74 -12
  8. package/package.json +13 -3
  9. package/reference-Dk_1njEH.js +259 -0
  10. package/src/index.ts +21 -0
  11. package/src/lib/droid.spec.ts +26 -0
  12. package/src/lib/droid.ts +7 -0
  13. package/src/lib/mod/mod-manager.ts +73 -0
  14. package/src/lib/mod/mod-urls.ts +3 -0
  15. package/src/lib/mod/module/bento/mod-bento.worker.ts +128 -0
  16. package/src/lib/mod/module/phaser/mod-phaser.ts +0 -0
  17. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +30 -0
  18. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +59 -0
  19. package/src/lib/types/bento.ts +80 -0
  20. package/src/lib/types/discord.ts +40 -0
  21. package/src/lib/types/event-types.ts +35 -0
  22. package/src/lib/types/jedi.ts +66 -0
  23. package/src/lib/types/modules.ts +46 -0
  24. package/src/lib/types/panel-types.ts +18 -0
  25. package/src/lib/types/supabase-esm.d.ts +4 -0
  26. package/src/lib/workers/canvas-worker.ts +107 -0
  27. package/src/lib/workers/data.ts +215 -0
  28. package/src/lib/workers/db-worker.ts +203 -0
  29. package/src/lib/workers/events.ts +52 -0
  30. package/src/lib/workers/flexbuilder.ts +6 -0
  31. package/src/lib/workers/init.ts +40 -0
  32. package/src/lib/workers/main.ts +531 -0
  33. package/src/lib/workers/tools.ts +60 -0
  34. package/src/lib/workers/ws-worker.ts +73 -0
  35. package/src/setup-vitest.ts +2 -0
  36. package/src/types.d.ts +31 -0
  37. package/workers/main.js +1091 -0
  38. package/droid.cjs.js +0 -1
@@ -0,0 +1,531 @@
1
+ import { wrap, transfer, proxy } from 'comlink';
2
+ import type { Remote } from 'comlink';
3
+ import { persistentMap } from '@nanostores/persistent';
4
+ import type { LocalStorageAPI } from './db-worker';
5
+ import type { WSInstance } from './ws-worker';
6
+ import { initializeWorkerDatabase, type InitWorkerOptions } from './init';
7
+ import type { CanvasWorkerAPI } from './canvas-worker';
8
+ import { getModManager } from '../mod/mod-manager';
9
+ import { scopeData } from './data';
10
+ import { dispatchAsync, renderVNode } from './tools';
11
+ import type { PanelPayload, PanelId } from '../types/panel-types';
12
+ import { DroidEvents } from './events';
13
+
14
+ const EXPECTED_DB_VERSION = '1.0.3';
15
+ let initialized = false;
16
+
17
+ export function resolveWorkerURL(name: string, fallback?: string): string {
18
+ if (!name)
19
+ throw new Error('[resolveWorkerURL] Worker name must be defined');
20
+
21
+ if (typeof window !== 'undefined') {
22
+ const globalMap = (window as any).kbveWorkerURLs;
23
+ if (globalMap?.[name]) return globalMap[name];
24
+ }
25
+
26
+ // No bundler-specific resolution: just return fallback or root-relative
27
+ return fallback ?? `/workers/${name}`;
28
+ }
29
+
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;
40
+ }
41
+
42
+ return obj;
43
+ }
44
+
45
+ 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;
101
+ }
102
+
103
+
104
+ // * UIUX
105
+ const uiuxState = persistentMap<{
106
+ panelManager: Record<
107
+ PanelId,
108
+ {
109
+ open: boolean;
110
+ payload?: PanelPayload;
111
+ }
112
+ >;
113
+ themeManager: { theme: 'light' | 'dark' | 'auto' };
114
+ toastManager: Record<string, any>;
115
+ scrollY: number;
116
+ }>(
117
+ 'uiux-state',
118
+ {
119
+ panelManager: {
120
+ top: { open: false },
121
+ right: { open: false },
122
+ bottom: { open: false },
123
+ left: { open: false },
124
+ },
125
+ themeManager: { theme: 'auto' },
126
+ toastManager: {},
127
+ scrollY: 0,
128
+ },
129
+ {
130
+ encode: JSON.stringify,
131
+ decode: JSON.parse,
132
+ },
133
+ );
134
+
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
+ export const uiux = {
183
+ state: uiuxState,
184
+ openPanel(id: PanelId, payload?: PanelPayload) {
185
+ const panels = { ...uiuxState.get().panelManager };
186
+ panels[id] = { open: true, payload };
187
+ uiuxState.setKey('panelManager', panels);
188
+ },
189
+
190
+ closePanel(id: PanelId) {
191
+ const panels = { ...uiuxState.get().panelManager };
192
+ panels[id] = { open: false, payload: undefined };
193
+ uiuxState.setKey('panelManager', panels);
194
+ },
195
+
196
+ togglePanel(id: PanelId, payload?: PanelPayload) {
197
+ const panels = { ...uiuxState.get().panelManager };
198
+ const isOpen = panels[id]?.open ?? false;
199
+ panels[id] = { open: !isOpen, payload: !isOpen ? payload : undefined };
200
+ uiuxState.setKey('panelManager', panels);
201
+ },
202
+
203
+ setTheme(theme: 'light' | 'dark' | 'auto') {
204
+ uiuxState.setKey('themeManager', { theme });
205
+ },
206
+
207
+ addToast(id: string, data: any) {
208
+ const toasts = { ...uiuxState.get().toastManager, [id]: data };
209
+ uiuxState.setKey('toastManager', toasts);
210
+ },
211
+
212
+ removeToast(id: string) {
213
+ const toasts = { ...uiuxState.get().toastManager };
214
+ delete toasts[id];
215
+ uiuxState.setKey('toastManager', toasts);
216
+ },
217
+ async dispatchCanvasRequest(
218
+ panelId: PanelId,
219
+ canvasEl: HTMLCanvasElement,
220
+ mode: 'static' | 'animated' | 'dynamic' = 'animated',
221
+ ) {
222
+ const offscreen = canvasEl.transferControlToOffscreen();
223
+ await window.kbve?.uiux?.worker?.bindCanvas(panelId, offscreen, mode);
224
+ },
225
+
226
+ closeAllPanels() {
227
+ const panels = { ...uiuxState.get().panelManager };
228
+ console.log('error panel is closing');
229
+
230
+ for (const id of Object.keys(panels) as Array<PanelId>) {
231
+ panels[id] = { open: false, payload: undefined };
232
+ }
233
+
234
+ uiuxState.setKey('panelManager', panels);
235
+ },
236
+
237
+ emitFromWorker(msg: any) {
238
+ if (msg.type === 'injectVNode' && msg.vnode) {
239
+ dispatchAsync(() => {
240
+ const target = document.getElementById('bento-grid-inject');
241
+ if (!target) {
242
+ console.warn(
243
+ '[KBVE] No injection target found: #bento-grid-inject',
244
+ );
245
+ return;
246
+ }
247
+
248
+ const el = renderVNode(msg.vnode);
249
+ el.classList.add('animate-fade-in');
250
+ if (msg.vnode.id) {
251
+ const existing = document.getElementById(msg.vnode.id);
252
+ if (existing) existing.remove();
253
+ }
254
+
255
+ target.appendChild(el);
256
+ });
257
+ }
258
+ },
259
+ };
260
+
261
+ // * i18n
262
+
263
+ const i18nStore = persistentMap<Record<string, string>>(
264
+ 'i18n-cache',
265
+ {},
266
+ {
267
+ encode: JSON.stringify,
268
+ decode: JSON.parse,
269
+ },
270
+ );
271
+
272
+ export const i18n = {
273
+ store: i18nStore,
274
+ api: null as Remote<LocalStorageAPI> | null,
275
+ ready: Promise.resolve(),
276
+
277
+ get(key: string): string {
278
+ return i18nStore.get()[key] ?? `[${key}]`;
279
+ },
280
+
281
+ async getAsync(key: string): Promise<string> {
282
+ const cached = i18nStore.get()[key];
283
+ if (cached !== undefined) return cached;
284
+
285
+ if (!this.api) return `[${key}]`;
286
+
287
+ const value = await this.api.getTranslation(key);
288
+ if (value !== null) {
289
+ i18nStore.setKey(key, value);
290
+ return value;
291
+ }
292
+
293
+ return `[${key}]`;
294
+ },
295
+
296
+ set(key: string, value: string) {
297
+ i18nStore.setKey(key, value);
298
+ },
299
+
300
+ async hydrate(api: Remote<LocalStorageAPI>, keys: string[]) {
301
+ this.api = api;
302
+ for (const key of keys) {
303
+ const value = await api.getTranslation(key);
304
+ if (value !== null) {
305
+ i18nStore.setKey(key, value);
306
+ }
307
+ }
308
+ },
309
+
310
+ async hydrateLocale(locale = 'en') {
311
+ if (!this.api) return;
312
+
313
+ const allKeys = await this.api.getAllI18nKeys();
314
+ const localeKeys = allKeys.filter((key) =>
315
+ key.startsWith(`${locale}:`),
316
+ );
317
+ const translations = await this.api.getTranslations(localeKeys);
318
+
319
+ for (const [key, value] of Object.entries(translations)) {
320
+ console.log(`[i18n.setKey] ${key} = ${value}`);
321
+ this.store.setKey(key, value);
322
+ }
323
+ },
324
+ };
325
+
326
+ function initSWComlink() {
327
+ if (!navigator.serviceWorker?.controller) return;
328
+ const channel = new MessageChannel();
329
+ navigator.serviceWorker.controller.postMessage(channel.port2, [
330
+ channel.port2,
331
+ ]);
332
+ channel.port1.start();
333
+ }
334
+
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
408
+ export function bridgeWsToDb(
409
+ ws: Remote<WSInstance>,
410
+ db: Remote<LocalStorageAPI>,
411
+ ) {
412
+ const handler = proxy(async (buf: ArrayBuffer) => {
413
+ dispatchAsync(() => {
414
+ const key = `ws:${Date.now()}`;
415
+ void db.storeWsMessage(key, buf);
416
+ });
417
+ });
418
+
419
+ ws.onMessage(transfer(handler, [0]));
420
+ }
421
+
422
+ // * MAIN
423
+ export async function main(opts?: {
424
+ workerURLs?: Record<string, string>;
425
+ workerRefs?: {
426
+ canvasWorker?: Worker;
427
+ dbWorker?: SharedWorker;
428
+ wsWorker?: SharedWorker;
429
+ };
430
+ }) {
431
+ console.log('[DROID]: Main<T>');
432
+
433
+ if (!initialized) {
434
+ initialized = true;
435
+
436
+ // Attach to existing service worker (or wait for one to take control)
437
+ if (navigator.serviceWorker?.controller) {
438
+ initSWComlink();
439
+ } else {
440
+ navigator.serviceWorker?.addEventListener(
441
+ 'controllerchange',
442
+ initSWComlink,
443
+ );
444
+ }
445
+ }
446
+
447
+ console.log('[DROID] Main<T> => Worker URLs', opts?.workerURLs);
448
+
449
+ const needsInit =
450
+ !window.kbve?.api || !window.kbve?.i18n || !window.kbve?.uiux;
451
+
452
+ 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
+ });
471
+
472
+ console.log('[DROID] Main<T> => Worker => ModManager');
473
+ const mod = await getModManager(
474
+ (url) => opts?.workerURLs?.[url] ?? url,
475
+ );
476
+ const events = DroidEvents;
477
+
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,
482
+ });
483
+ }
484
+ console.log('[Event] -> Fire Mod Ready');
485
+ events.emit('droid-mod-ready', {
486
+ meta: handle.meta,
487
+ timestamp: Date.now(),
488
+ });
489
+ }
490
+
491
+ bridgeWsToDb(ws, api);
492
+
493
+ const data = scopeData;
494
+ i18n.api = api;
495
+ i18n.ready = i18n.hydrateLocale('en');
496
+
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;
509
+
510
+ window.kbve.events.emit('droid-ready', {
511
+ timestamp: Date.now(),
512
+ });
513
+
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', {
519
+ timestamp: Date.now(),
520
+ });
521
+ });
522
+
523
+ console.log('[KBVE] Global API ready');
524
+ } catch (err) {
525
+ console.error('[DROID] Initialization error:', err);
526
+ throw err;
527
+ }
528
+ } else {
529
+ console.log('[KBVE] Already initialized');
530
+ }
531
+ }
@@ -0,0 +1,60 @@
1
+ import type { VirtualNode } from '../types/modules';
2
+
3
+ export function dispatchAsync(fn: () => void) {
4
+ if (typeof queueMicrotask === 'function') {
5
+ queueMicrotask(fn);
6
+ } else {
7
+ setTimeout(fn, 0);
8
+ }
9
+ }
10
+
11
+ export function renderVNode(vnode: VirtualNode): HTMLElement {
12
+ const el = document.createElement(vnode.tag);
13
+
14
+ if (vnode.class) el.className = vnode.class;
15
+
16
+ if (vnode.attrs) {
17
+ for (const [key, value] of Object.entries(vnode.attrs)) {
18
+ if (typeof value === 'function' && key.startsWith('on')) {
19
+ el.addEventListener(key.slice(2).toLowerCase(), value);
20
+ } else if (key === 'style' && typeof value === 'object') {
21
+ Object.assign(el.style, value);
22
+ } else if (key === 'dataset' && typeof value === 'object') {
23
+ for (const [data_key, data_value] of Object.entries(value)) {
24
+ el.dataset[data_key] = String(data_value);
25
+ }
26
+ } else {
27
+ try {
28
+ el.setAttribute(key, String(value));
29
+ } catch {
30
+ //
31
+ }
32
+ }
33
+ }
34
+ }
35
+
36
+ if (vnode.style) {
37
+ Object.assign(el.style, vnode.style);
38
+ }
39
+
40
+ // if (vnode.children) {
41
+ // for (const child of vnode.children) {
42
+ // el.appendChild(
43
+ // typeof child === 'string'
44
+ // ? document.createTextNode(child)
45
+ // : renderVNode(child),
46
+ // );
47
+ // }
48
+ // }
49
+
50
+ if (vnode.children) {
51
+ for (const child of vnode.children) {
52
+ const node = typeof child === 'string'
53
+ ? document.createTextNode(child)
54
+ : renderVNode(child);
55
+ el.appendChild(node);
56
+ }
57
+ }
58
+
59
+ return el;
60
+ }
@@ -0,0 +1,73 @@
1
+ import { wrap, expose, type Remote } from 'comlink';
2
+ import type { LocalStorageAPI } from './db-worker';
3
+ import { toReference, builder } from './flexbuilder';
4
+
5
+
6
+ interface SharedWorkerGlobalScope extends Worker {
7
+ onconnect: (event: MessageEvent) => void;
8
+ }
9
+ declare const self: SharedWorkerGlobalScope;
10
+
11
+
12
+ let ws: WebSocket | null = null;
13
+ let onMessageCallback: ((data: any) => void) | null = null;
14
+
15
+
16
+ // --- WebSocket API ---
17
+ const wsInstanceAPI = {
18
+ async connect(url: string) {
19
+ if (ws) return;
20
+
21
+ ws = new WebSocket(url);
22
+ ws.binaryType = 'arraybuffer';
23
+
24
+ ws.onopen = () => console.log('[WS] Connected:', url);
25
+
26
+ ws.onmessage = (e) => {
27
+ try {
28
+ console.log('[WS] Received binary message');
29
+
30
+ if (e.data instanceof ArrayBuffer) {
31
+ onMessageCallback?.(e.data);
32
+ }
33
+ } catch (err) {
34
+ console.error('[WS] Failed to forward message', err);
35
+ }
36
+ };
37
+
38
+
39
+ ws.onerror = (e) => console.error('[WS] Error:', e);
40
+ ws.onclose = () => {
41
+ console.log('[WS] Disconnected');
42
+ ws = null;
43
+ };
44
+ },
45
+
46
+ async send(payload: Uint8Array) {
47
+ if (ws?.readyState === WebSocket.OPEN) {
48
+ ws.send(payload);
49
+ } else {
50
+ console.warn('[WS] Tried to send while disconnected');
51
+ }
52
+ },
53
+
54
+ async close() {
55
+ ws?.close();
56
+ ws = null;
57
+ },
58
+
59
+ onMessage(callback: (data: any) => void) {
60
+ onMessageCallback = callback;
61
+ },
62
+
63
+
64
+ };
65
+
66
+ export type WSInstance = typeof wsInstanceAPI;
67
+
68
+ self.onconnect = (event: MessageEvent) => {
69
+ const port = event.ports[0];
70
+ port.start();
71
+ expose(wsInstanceAPI, port);
72
+ };
73
+
@@ -0,0 +1,2 @@
1
+ import '@vitest/web-worker';
2
+ import 'fake-indexeddb/auto';
package/src/types.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /// <reference lib="webworker" />
2
+
3
+ import type { Remote } from 'comlink';
4
+ import type { LocalStorageAPI } from './lib/workers/db-worker';
5
+ import type { WSInstance } from './lib/workers/ws-worker';
6
+ import { FlexDataAPI } from './lib/workers/data';
7
+ import type { ModManager } from './types/modules';
8
+ import type { DroidEventBus } from './lib/workers/events';
9
+
10
+ export interface KBVEGlobal {
11
+ api: Remote<LocalStorageAPI>;
12
+ i18n: typeof I18nInstance;
13
+ uiux: typeof UiUxInstance;
14
+ ws: Remote<WSInstance>;
15
+ data: FlexDataAPI;
16
+ mod: ModManager;
17
+ events: DroidEventBus;
18
+
19
+ // Helper Flags @GPT
20
+ // droidReady?: boolean;
21
+ // waitForDroidReady?: () => Promise<void>;
22
+
23
+ // <T>
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ declare global {
28
+ interface Window {
29
+ kbve?: KBVEGlobal;
30
+ }
31
+ }