@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.
- package/LICENSE +1 -0
- package/README.md +5 -1
- package/assets/canvas-worker-DbsoY2Xv.js +5 -0
- package/assets/db-worker-CjXS52DQ.js +14 -0
- package/assets/ws-worker-FssAQgaD.js +5 -0
- package/comlink-CC72iIUO.js +253 -0
- package/droid.es.js +74 -12
- package/package.json +13 -3
- package/reference-Dk_1njEH.js +259 -0
- package/src/index.ts +21 -0
- package/src/lib/droid.spec.ts +26 -0
- package/src/lib/droid.ts +7 -0
- package/src/lib/mod/mod-manager.ts +73 -0
- package/src/lib/mod/mod-urls.ts +3 -0
- package/src/lib/mod/module/bento/mod-bento.worker.ts +128 -0
- package/src/lib/mod/module/phaser/mod-phaser.ts +0 -0
- package/src/lib/mod/module/supabase/mod-supabase.spec.ts +30 -0
- package/src/lib/mod/module/supabase/mod-supabase.worker.ts +59 -0
- package/src/lib/types/bento.ts +80 -0
- package/src/lib/types/discord.ts +40 -0
- package/src/lib/types/event-types.ts +35 -0
- package/src/lib/types/jedi.ts +66 -0
- package/src/lib/types/modules.ts +46 -0
- package/src/lib/types/panel-types.ts +18 -0
- package/src/lib/types/supabase-esm.d.ts +4 -0
- package/src/lib/workers/canvas-worker.ts +107 -0
- package/src/lib/workers/data.ts +215 -0
- package/src/lib/workers/db-worker.ts +203 -0
- package/src/lib/workers/events.ts +52 -0
- package/src/lib/workers/flexbuilder.ts +6 -0
- package/src/lib/workers/init.ts +40 -0
- package/src/lib/workers/main.ts +531 -0
- package/src/lib/workers/tools.ts +60 -0
- package/src/lib/workers/ws-worker.ts +73 -0
- package/src/setup-vitest.ts +2 -0
- package/src/types.d.ts +31 -0
- package/workers/main.js +1091 -0
- 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
|
+
|
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
|
+
}
|