@kbve/droid 0.0.1 → 0.0.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 (37) 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/droid.cjs.js +1 -1
  7. package/droid.es.js +1 -1
  8. package/index-DSyyyiLP.js +3034 -0
  9. package/main-BYzQVPCs.js +1455 -0
  10. package/package.json +8 -2
  11. package/src/index.ts +3 -0
  12. package/src/lib/droid.spec.ts +26 -0
  13. package/src/lib/droid.ts +5 -0
  14. package/src/lib/mod/mod-manager.ts +72 -0
  15. package/src/lib/mod/mod-urls.ts +3 -0
  16. package/src/lib/mod/module/bento/mod-bento.worker.ts +128 -0
  17. package/src/lib/mod/module/phaser/mod-phaser.ts +0 -0
  18. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +30 -0
  19. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +59 -0
  20. package/src/lib/types/bento.ts +80 -0
  21. package/src/lib/types/discord.ts +40 -0
  22. package/src/lib/types/event-types.ts +35 -0
  23. package/src/lib/types/jedi.ts +66 -0
  24. package/src/lib/types/modules.ts +46 -0
  25. package/src/lib/types/panel-types.ts +18 -0
  26. package/src/lib/types/supabase-esm.d.ts +4 -0
  27. package/src/lib/workers/canvas-worker.ts +107 -0
  28. package/src/lib/workers/data.ts +215 -0
  29. package/src/lib/workers/db-worker.ts +203 -0
  30. package/src/lib/workers/events.ts +52 -0
  31. package/src/lib/workers/flexbuilder.ts +6 -0
  32. package/src/lib/workers/init.ts +40 -0
  33. package/src/lib/workers/main.ts +356 -0
  34. package/src/lib/workers/tools.ts +60 -0
  35. package/src/lib/workers/ws-worker.ts +73 -0
  36. package/src/setup-vitest.ts +2 -0
  37. package/src/types.d.ts +31 -0
@@ -0,0 +1,356 @@
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
+
16
+ // * DeepProxy
17
+
18
+ function deepProxy<T>(obj: T): T {
19
+ if (typeof obj === 'function') return proxy(obj) as T;
20
+
21
+ if (obj && typeof obj === 'object') {
22
+ const result: any = Array.isArray(obj) ? [] : {};
23
+ for (const key in obj) {
24
+ result[key] = deepProxy(obj[key]);
25
+ }
26
+ return result;
27
+ }
28
+
29
+ return obj;
30
+ }
31
+
32
+ // * WebSocket
33
+ async function initWsComlink(): Promise<Remote<WSInstance>> {
34
+ const worker = new SharedWorker(new URL('./ws-worker', import.meta.url), {
35
+ type: 'module',
36
+ });
37
+ worker.port.start();
38
+ return wrap<WSInstance>(worker.port);
39
+ }
40
+
41
+ // * Interface -> Moved to the panels.ts
42
+
43
+ // * UIUX
44
+
45
+ const uiuxState = persistentMap<{
46
+ panelManager: Record<PanelId,
47
+ {
48
+ open: boolean;
49
+ payload?: PanelPayload;
50
+ }
51
+ >;
52
+ themeManager: { theme: 'light' | 'dark' | 'auto' };
53
+ toastManager: Record<string, any>;
54
+ scrollY: number;
55
+ }>(
56
+ 'uiux-state',
57
+ {
58
+ panelManager: {
59
+ top: { open: false },
60
+ right: { open: false },
61
+ bottom: { open: false },
62
+ left: { open: false },
63
+ },
64
+ themeManager: { theme: 'auto' },
65
+ toastManager: {},
66
+ scrollY: 0,
67
+ },
68
+ {
69
+ encode: JSON.stringify,
70
+ decode: JSON.parse,
71
+ },
72
+ );
73
+
74
+ const canvasWorker = wrap<CanvasWorkerAPI>(
75
+ new Worker(new URL('./canvas-worker', import.meta.url), { type: 'module' }),
76
+ );
77
+
78
+ export const uiux = {
79
+ state: uiuxState,
80
+ worker: canvasWorker,
81
+ openPanel(id: PanelId, payload?: PanelPayload) {
82
+ const panels = { ...uiuxState.get().panelManager };
83
+ panels[id] = { open: true, payload };
84
+ uiuxState.setKey('panelManager', panels);
85
+ },
86
+
87
+ closePanel(id: PanelId) {
88
+ const panels = { ...uiuxState.get().panelManager };
89
+ panels[id] = { open: false, payload: undefined };
90
+ uiuxState.setKey('panelManager', panels);
91
+ },
92
+
93
+ togglePanel(
94
+ id: PanelId,
95
+ payload?: PanelPayload,
96
+ ) {
97
+ const panels = { ...uiuxState.get().panelManager };
98
+ const isOpen = panels[id]?.open ?? false;
99
+ panels[id] = { open: !isOpen, payload: !isOpen ? payload : undefined };
100
+ uiuxState.setKey('panelManager', panels);
101
+ },
102
+
103
+ setTheme(theme: 'light' | 'dark' | 'auto') {
104
+ uiuxState.setKey('themeManager', { theme });
105
+ },
106
+
107
+ addToast(id: string, data: any) {
108
+ const toasts = { ...uiuxState.get().toastManager, [id]: data };
109
+ uiuxState.setKey('toastManager', toasts);
110
+ },
111
+
112
+ removeToast(id: string) {
113
+ const toasts = { ...uiuxState.get().toastManager };
114
+ delete toasts[id];
115
+ uiuxState.setKey('toastManager', toasts);
116
+ },
117
+
118
+ async dispatchCanvasRequest(
119
+ panelId: PanelId,
120
+ canvasEl: HTMLCanvasElement,
121
+ mode: 'static' | 'animated' | 'dynamic' = 'animated',
122
+ ) {
123
+ const offscreen = canvasEl.transferControlToOffscreen();
124
+ await this.worker.bindCanvas(panelId, offscreen, mode);
125
+ },
126
+
127
+ closeAllPanels() {
128
+ const panels = { ...uiuxState.get().panelManager };
129
+ console.log('error panel is closing');
130
+
131
+ for (const id of Object.keys(panels) as Array<
132
+ PanelId
133
+ >) {
134
+ panels[id] = { open: false, payload: undefined };
135
+ }
136
+
137
+ uiuxState.setKey('panelManager', panels);
138
+ },
139
+
140
+ emitFromWorker(msg: any) {
141
+ if (msg.type === 'injectVNode' && msg.vnode) {
142
+ dispatchAsync(() => {
143
+ const target = document.getElementById('bento-grid-inject');
144
+ if (!target) {
145
+ console.warn(
146
+ '[KBVE] No injection target found: #bento-grid-inject',
147
+ );
148
+ return;
149
+ }
150
+
151
+ const el = renderVNode(msg.vnode);
152
+ el.classList.add('animate-fade-in');
153
+ if (msg.vnode.id) {
154
+ const existing = document.getElementById(msg.vnode.id);
155
+ if (existing) existing.remove();
156
+ }
157
+
158
+ target.appendChild(el);
159
+ });
160
+ }
161
+ },
162
+ };
163
+
164
+ // * i18n
165
+
166
+ const i18nStore = persistentMap<Record<string, string>>(
167
+ 'i18n-cache',
168
+ {},
169
+ {
170
+ encode: JSON.stringify,
171
+ decode: JSON.parse,
172
+ },
173
+ );
174
+
175
+ export const i18n = {
176
+ store: i18nStore,
177
+ api: null as Remote<LocalStorageAPI> | null,
178
+ ready: Promise.resolve(),
179
+
180
+ get(key: string): string {
181
+ return i18nStore.get()[key] ?? `[${key}]`;
182
+ },
183
+
184
+ async getAsync(key: string): Promise<string> {
185
+ const cached = i18nStore.get()[key];
186
+ if (cached !== undefined) return cached;
187
+
188
+ if (!this.api) return `[${key}]`;
189
+
190
+ const value = await this.api.getTranslation(key);
191
+ if (value !== null) {
192
+ i18nStore.setKey(key, value);
193
+ return value;
194
+ }
195
+
196
+ return `[${key}]`;
197
+ },
198
+
199
+ set(key: string, value: string) {
200
+ i18nStore.setKey(key, value);
201
+ },
202
+
203
+ async hydrate(api: Remote<LocalStorageAPI>, keys: string[]) {
204
+ this.api = api;
205
+ for (const key of keys) {
206
+ const value = await api.getTranslation(key);
207
+ if (value !== null) {
208
+ i18nStore.setKey(key, value);
209
+ }
210
+ }
211
+ },
212
+
213
+ async hydrateLocale(locale = 'en') {
214
+ if (!this.api) return;
215
+
216
+ const allKeys = await this.api.getAllI18nKeys();
217
+ const localeKeys = allKeys.filter((key) =>
218
+ key.startsWith(`${locale}:`),
219
+ );
220
+ const translations = await this.api.getTranslations(localeKeys);
221
+
222
+ for (const [key, value] of Object.entries(translations)) {
223
+ console.log(`[i18n.setKey] ${key} = ${value}`);
224
+ this.store.setKey(key, value);
225
+ }
226
+ },
227
+ };
228
+
229
+ function initSWComlink() {
230
+ if (!navigator.serviceWorker?.controller) return;
231
+ const channel = new MessageChannel();
232
+ navigator.serviceWorker.controller.postMessage(channel.port2, [
233
+ channel.port2,
234
+ ]);
235
+ channel.port1.start();
236
+ }
237
+
238
+ async function initStorageComlink(): Promise<Remote<LocalStorageAPI>> {
239
+ const worker = new SharedWorker(new URL('./db-worker', import.meta.url), {
240
+ type: 'module',
241
+ });
242
+ worker.port.start();
243
+ const api = wrap<LocalStorageAPI>(worker.port);
244
+
245
+ const version = await api.getVersion();
246
+ if (version !== EXPECTED_DB_VERSION) {
247
+ await initializeWorkerDatabase(api, {
248
+ version: EXPECTED_DB_VERSION,
249
+ i18nPath: 'https://discord.sh/i18n/db.json',
250
+ locale: 'en',
251
+ defaults: { welcome: 'Welcome!', theme: 'dark' },
252
+ });
253
+ }
254
+
255
+ return api;
256
+ }
257
+
258
+ let initialized = false;
259
+
260
+ // * Bridge
261
+ export function bridgeWsToDb(
262
+ ws: Remote<WSInstance>,
263
+ db: Remote<LocalStorageAPI>,
264
+ ) {
265
+ const handler = proxy(async (buf: ArrayBuffer) => {
266
+ dispatchAsync(() => {
267
+ const key = `ws:${Date.now()}`;
268
+ void db.storeWsMessage(key, buf);
269
+ });
270
+ });
271
+
272
+ ws.onMessage(transfer(handler, [0]));
273
+ }
274
+
275
+ // * MAIN
276
+ export async function main() {
277
+ if (!initialized) {
278
+ initialized = true;
279
+
280
+ // Attach to existing service worker (or wait for one to take control)
281
+ if (navigator.serviceWorker?.controller) {
282
+ initSWComlink();
283
+ } else {
284
+ navigator.serviceWorker?.addEventListener(
285
+ 'controllerchange',
286
+ initSWComlink,
287
+ );
288
+ }
289
+ }
290
+
291
+ const needsInit =
292
+ !window.kbve?.api || !window.kbve?.i18n || !window.kbve?.uiux;
293
+
294
+ if (needsInit) {
295
+ const api = await initStorageComlink();
296
+ const ws = await initWsComlink();
297
+ const mod = await getModManager();
298
+ const events = DroidEvents;
299
+
300
+ for (const handle of Object.values(mod.registry)) {
301
+ if (typeof handle.instance.init === 'function') {
302
+ await handle.instance.init({
303
+ emitFromWorker: uiux.emitFromWorker,
304
+ });
305
+ }
306
+ console.log('[Event] -> Fire Mod Ready');
307
+ events.emit('droid-mod-ready', {
308
+ meta: handle.meta,
309
+ timestamp: Date.now(),
310
+ });
311
+ }
312
+
313
+ bridgeWsToDb(ws, api);
314
+
315
+ const data = scopeData;
316
+ i18n.api = api;
317
+ i18n.ready = i18n.hydrateLocale('en');
318
+
319
+ window.kbve = {
320
+ ...(window.kbve || {}),
321
+ api,
322
+ i18n,
323
+ uiux,
324
+ ws,
325
+ data,
326
+ mod,
327
+ events,
328
+ };
329
+
330
+ //window.kbve = deepProxy(window.kbve);
331
+
332
+ await i18n.ready;
333
+
334
+ window.kbve.events.emit('droid-ready', {
335
+ timestamp: Date.now(),
336
+ });
337
+
338
+ document.addEventListener('astro:page-load', () => {
339
+ console.debug('[KBVE] Re-dispatched droid-ready after astro:page-load');
340
+ window.kbve?.events.emit('droid-ready', {
341
+ timestamp: Date.now(),
342
+ });
343
+ });
344
+
345
+ // document.addEventListener('astro:page-load', () => {
346
+ // console.debug('[KBVE] Re-dispatched droid-ready after DomContentLoaded');
347
+ // window.kbve?.events.emit('droid-ready', {
348
+ // timestamp: Date.now(),
349
+ // });
350
+ // });
351
+
352
+ console.log('[KBVE] Global API ready');
353
+ } else {
354
+ console.log('[KBVE] Already initialized');
355
+ }
356
+ }
@@ -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
+ }