@kbve/droid 0.0.5 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/comlink.js +1 -7
- package/droid.mjs +191 -61
- package/index.d.ts +29 -0
- package/index.js +9054 -2763
- package/lib/gateway/SupabaseGateway.d.ts +38 -0
- package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
- package/lib/gateway/WorkerCommunication.d.ts +39 -0
- package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
- package/lib/gateway/WorkerPool.d.ts +55 -0
- package/lib/gateway/WorkerPool.d.ts.map +1 -0
- package/lib/gateway/capabilities.d.ts +23 -0
- package/lib/gateway/capabilities.d.ts.map +1 -0
- package/lib/gateway/index.d.ts +9 -0
- package/lib/gateway/index.d.ts.map +1 -0
- package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
- package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
- package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
- package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
- package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
- package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
- package/lib/gateway/types.d.ts +81 -0
- package/lib/gateway/types.d.ts.map +1 -0
- package/lib/workers/canvas-ui-renderer.d.ts +25 -0
- package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
- package/lib/workers/canvas-worker.d.ts +14 -0
- package/lib/workers/canvas-worker.d.ts.map +1 -0
- package/lib/workers/canvas-worker.js +291 -145
- package/lib/workers/data.d.ts +40 -0
- package/lib/workers/data.d.ts.map +1 -0
- package/lib/workers/db-worker.d.ts +69 -0
- package/lib/workers/db-worker.d.ts.map +1 -0
- package/lib/workers/db-worker.js +2680 -2147
- package/lib/workers/events.d.ts +11 -0
- package/lib/workers/events.d.ts.map +1 -0
- package/lib/workers/flexbuilder.d.ts +4 -0
- package/lib/workers/flexbuilder.d.ts.map +1 -0
- package/lib/workers/init.d.ts +11 -0
- package/lib/workers/init.d.ts.map +1 -0
- package/lib/workers/main.d.ts +107 -0
- package/lib/workers/main.d.ts.map +1 -0
- package/lib/workers/supabase-db-worker.d.ts +2 -0
- package/lib/workers/supabase-db-worker.d.ts.map +1 -0
- package/lib/workers/supabase-db-worker.js +9337 -0
- package/lib/workers/supabase-shared-worker.d.ts +2 -0
- package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
- package/lib/workers/supabase-shared-worker.js +12635 -0
- package/lib/workers/tools.d.ts +4 -0
- package/lib/workers/tools.d.ts.map +1 -0
- package/lib/workers/validate.d.ts +15 -0
- package/lib/workers/validate.d.ts.map +1 -0
- package/lib/workers/ws-worker.d.ts +11 -0
- package/lib/workers/ws-worker.d.ts.map +1 -0
- package/lib/workers/ws-worker.js +174 -115
- package/main.js +4875 -0
- package/package.json +17 -6
- package/reference.js +688 -228
- package/src/index.ts +38 -13
- package/src/lib/droid.spec.ts +14 -19
- package/src/lib/droid.ts +48 -4
- package/src/lib/gateway/SupabaseGateway.ts +183 -0
- package/src/lib/gateway/WorkerCommunication.ts +132 -0
- package/src/lib/gateway/WorkerPool.ts +304 -0
- package/src/lib/gateway/capabilities.ts +80 -0
- package/src/lib/gateway/index.ts +15 -0
- package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
- package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
- package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
- package/src/lib/gateway/types.ts +112 -0
- package/src/lib/mod/mod-manager.ts +13 -8
- package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
- package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
- package/src/lib/mod/module/supabase/mod-supabase.ts +68 -0
- package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
- package/src/lib/state/auth.spec.ts +58 -0
- package/src/lib/state/auth.ts +30 -0
- package/src/lib/state/index.ts +34 -0
- package/src/lib/state/overlay-manager.ts +125 -0
- package/src/lib/state/router.ts +24 -0
- package/src/lib/state/theme-sync.ts +75 -0
- package/src/lib/state/toasts.spec.ts +106 -0
- package/src/lib/state/toasts.ts +34 -0
- package/src/lib/state/ui.spec.ts +148 -0
- package/src/lib/state/ui.ts +45 -0
- package/src/lib/state/welcome-toast.ts +76 -0
- package/src/lib/types/bento.spec.ts +116 -0
- package/src/lib/types/bento.ts +94 -55
- package/src/lib/types/event-types.spec.ts +93 -0
- package/src/lib/types/event-types.ts +39 -3
- package/src/lib/types/modules.ts +12 -5
- package/src/lib/types/ui-event-types.ts +66 -0
- package/src/lib/workers/canvas-ui-renderer.ts +266 -0
- package/src/lib/workers/canvas-worker.ts +94 -9
- package/src/lib/workers/data.ts +59 -36
- package/src/lib/workers/db-worker.ts +79 -32
- package/src/lib/workers/events.spec.ts +208 -0
- package/src/lib/workers/events.ts +27 -14
- package/src/lib/workers/flexbuilder.ts +2 -2
- package/src/lib/workers/init.ts +25 -17
- package/src/lib/workers/main.ts +509 -272
- package/src/lib/workers/supabase-db-worker.ts +221 -0
- package/src/lib/workers/supabase-shared-worker.ts +572 -0
- package/src/lib/workers/validate.ts +64 -0
- package/src/lib/workers/ws-worker.ts +156 -25
- package/src/setup-vitest.ts +4 -2
- package/src/types.ts +30 -0
- package/src/vite-env.d.ts +1 -0
- package/workers/main.js +10 -1089
- package/src/types.d.ts +0 -31
|
@@ -9,18 +9,19 @@ interface SharedWorkerGlobalScope extends Worker {
|
|
|
9
9
|
declare const self: SharedWorkerGlobalScope;
|
|
10
10
|
|
|
11
11
|
class AppDexie extends Dexie {
|
|
12
|
-
settings!: Table<{ id: string; value:
|
|
13
|
-
meta!: Table<{ key: string; value:
|
|
14
|
-
i18n!: Table<{ key: string; value: string }
|
|
15
|
-
htmlservers!: Table<{ key: string; value: string }
|
|
16
|
-
servers!: Table<DiscordServer
|
|
17
|
-
tags!: Table<DiscordTag
|
|
18
|
-
profiles!: Table<Profile
|
|
19
|
-
ws_messages!: Table<{ key: string; message:
|
|
12
|
+
settings!: Table<{ id: string; value: unknown }>;
|
|
13
|
+
meta!: Table<{ key: string; value: unknown }>;
|
|
14
|
+
i18n!: Table<{ key: string; value: string }>;
|
|
15
|
+
htmlservers!: Table<{ key: string; value: string }>;
|
|
16
|
+
servers!: Table<DiscordServer>;
|
|
17
|
+
tags!: Table<DiscordTag>;
|
|
18
|
+
profiles!: Table<Profile>;
|
|
19
|
+
ws_messages!: Table<{ key: string; message: Record<string, unknown> }>;
|
|
20
|
+
auth_tokens!: Table<{ key: string; value: string; expires_at?: number }>;
|
|
20
21
|
|
|
21
22
|
constructor() {
|
|
22
23
|
super('AppStorage');
|
|
23
|
-
this.version(
|
|
24
|
+
this.version(4).stores({
|
|
24
25
|
settings: '&id',
|
|
25
26
|
meta: '&key',
|
|
26
27
|
i18n: '&key',
|
|
@@ -29,15 +30,12 @@ class AppDexie extends Dexie {
|
|
|
29
30
|
tags: '&tag_id',
|
|
30
31
|
profiles: '&profile_id',
|
|
31
32
|
ws_messages: '&key',
|
|
33
|
+
auth_tokens: '&key',
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
const db = new AppDexie();
|
|
36
38
|
|
|
37
|
-
function delay(ms: number) {
|
|
38
|
-
return new Promise((res) => setTimeout(res, ms));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
39
|
function renderHtmlForServer(server: DiscordServer): string {
|
|
42
40
|
return `
|
|
43
41
|
<div class="flex flex-col gap-2 p-2">
|
|
@@ -51,19 +49,23 @@ function renderHtmlForServer(server: DiscordServer): string {
|
|
|
51
49
|
|
|
52
50
|
// --- Comlink-Safe API ---
|
|
53
51
|
const storageAPI = {
|
|
54
|
-
|
|
55
52
|
// WebSocket
|
|
56
53
|
|
|
57
54
|
async storeWsMessage(key: string, buffer: ArrayBuffer) {
|
|
58
|
-
const decoded = toReference(buffer).toObject()
|
|
55
|
+
const decoded = toReference(buffer).toObject() as Record<
|
|
56
|
+
string,
|
|
57
|
+
unknown
|
|
58
|
+
>;
|
|
59
59
|
await db.ws_messages.put({ key, message: decoded });
|
|
60
60
|
},
|
|
61
61
|
|
|
62
62
|
async getWsMessage(key: string) {
|
|
63
63
|
return (await db.ws_messages.get(key))?.message ?? null;
|
|
64
64
|
},
|
|
65
|
-
|
|
66
|
-
async getAllWsMessages(): Promise<
|
|
65
|
+
|
|
66
|
+
async getAllWsMessages(): Promise<
|
|
67
|
+
{ key: string; message: Record<string, unknown> }[]
|
|
68
|
+
> {
|
|
67
69
|
const raw = await db.ws_messages.toArray();
|
|
68
70
|
return raw.sort((a, b) => {
|
|
69
71
|
const at = Number(a.key.split(':')[1]);
|
|
@@ -71,7 +73,7 @@ const storageAPI = {
|
|
|
71
73
|
return bt - at;
|
|
72
74
|
});
|
|
73
75
|
},
|
|
74
|
-
|
|
76
|
+
|
|
75
77
|
async clearWsMessages() {
|
|
76
78
|
await db.ws_messages.clear();
|
|
77
79
|
},
|
|
@@ -95,20 +97,31 @@ const storageAPI = {
|
|
|
95
97
|
}));
|
|
96
98
|
await db.i18n.bulkPut(entries);
|
|
97
99
|
},
|
|
98
|
-
async loadI18nFromJSON(path
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
async loadI18nFromJSON(path: string) {
|
|
101
|
+
const res = await fetch(path);
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`[db-worker] i18n fetch failed: ${res.status} ${res.statusText}`,
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
await storageAPI.putI18nBatch(data);
|
|
102
110
|
},
|
|
103
111
|
async getAllI18nKeys(): Promise<string[]> {
|
|
104
|
-
|
|
105
|
-
return all as string[];
|
|
112
|
+
return (await db.i18n.toCollection().primaryKeys()) as string[];
|
|
106
113
|
},
|
|
107
114
|
|
|
108
|
-
async loadServersFromJSON(path
|
|
115
|
+
async loadServersFromJSON(path: string) {
|
|
109
116
|
const res = await fetch(path);
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
console.warn(
|
|
119
|
+
`[db-worker] servers fetch failed: ${res.status} ${res.statusText}`,
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
110
123
|
const data = await res.json();
|
|
111
|
-
|
|
124
|
+
|
|
112
125
|
// The JSON should be a Record<string, DiscordServer>
|
|
113
126
|
const servers: DiscordServer[] = Object.values(data);
|
|
114
127
|
await storageAPI.putServers(servers);
|
|
@@ -118,26 +131,32 @@ const storageAPI = {
|
|
|
118
131
|
// HTML PreRender
|
|
119
132
|
async syncHtmlFromServers() {
|
|
120
133
|
const allServers = await db.servers.toArray();
|
|
121
|
-
const existingHtmlKeys = await db.htmlservers
|
|
134
|
+
const existingHtmlKeys = await db.htmlservers
|
|
135
|
+
.toCollection()
|
|
136
|
+
.primaryKeys();
|
|
122
137
|
const missing: { key: string; value: string }[] = [];
|
|
123
|
-
|
|
138
|
+
|
|
124
139
|
for (const server of allServers) {
|
|
125
140
|
if (!existingHtmlKeys.includes(server.server_id)) {
|
|
126
141
|
const html = renderHtmlForServer(server);
|
|
127
142
|
missing.push({ key: server.server_id, value: html });
|
|
128
143
|
}
|
|
129
144
|
}
|
|
130
|
-
|
|
145
|
+
|
|
131
146
|
if (missing.length > 0) {
|
|
132
147
|
await db.htmlservers.bulkPut(missing);
|
|
133
|
-
console.info(
|
|
148
|
+
console.info(
|
|
149
|
+
`[syncHtmlFromServers] Generated ${missing.length} HTML cards.`,
|
|
150
|
+
);
|
|
134
151
|
} else {
|
|
135
|
-
console.info(
|
|
152
|
+
console.info(
|
|
153
|
+
'[syncHtmlFromServers] All servers already have HTML cards.',
|
|
154
|
+
);
|
|
136
155
|
}
|
|
137
156
|
},
|
|
138
157
|
|
|
139
158
|
// Settings
|
|
140
|
-
async dbSet(id: string, value:
|
|
159
|
+
async dbSet(id: string, value: unknown) {
|
|
141
160
|
await db.settings.put({ id, value });
|
|
142
161
|
},
|
|
143
162
|
async dbGet(id: string) {
|
|
@@ -192,12 +211,40 @@ const storageAPI = {
|
|
|
192
211
|
async getAllProfiles() {
|
|
193
212
|
return await db.profiles.toArray();
|
|
194
213
|
},
|
|
214
|
+
|
|
215
|
+
// Auth Tokens (IDB-backed auth storage)
|
|
216
|
+
async setAuthToken(key: string, value: string, expiresAt?: number) {
|
|
217
|
+
await db.auth_tokens.put({ key, value, expires_at: expiresAt });
|
|
218
|
+
},
|
|
219
|
+
async getAuthToken(key: string): Promise<string | null> {
|
|
220
|
+
const record = await db.auth_tokens.get(key);
|
|
221
|
+
if (!record) return null;
|
|
222
|
+
if (record.expires_at && Date.now() > record.expires_at) {
|
|
223
|
+
await db.auth_tokens.delete(key);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return record.value;
|
|
227
|
+
},
|
|
228
|
+
async removeAuthToken(key: string) {
|
|
229
|
+
await db.auth_tokens.delete(key);
|
|
230
|
+
},
|
|
231
|
+
async clearAuthTokens() {
|
|
232
|
+
await db.auth_tokens.clear();
|
|
233
|
+
},
|
|
195
234
|
};
|
|
196
235
|
|
|
197
236
|
export type LocalStorageAPI = typeof storageAPI;
|
|
198
237
|
|
|
238
|
+
const ports = new Set<MessagePort>();
|
|
239
|
+
|
|
199
240
|
self.onconnect = (event: MessageEvent) => {
|
|
200
241
|
const port = event.ports[0];
|
|
242
|
+
const isFirst = ports.size === 0;
|
|
243
|
+
ports.add(port);
|
|
201
244
|
port.start();
|
|
245
|
+
port.postMessage({ type: isFirst ? 'first-connect' : 'reconnect' });
|
|
246
|
+
port.addEventListener('message', (e: MessageEvent) => {
|
|
247
|
+
if (e.data?.type === 'close') ports.delete(port);
|
|
248
|
+
});
|
|
202
249
|
expose(storageAPI, port);
|
|
203
250
|
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// We need a fresh DroidEventBus for each test, so import the class-creating module
|
|
4
|
+
// and re-instantiate. Since the module exports a singleton, we'll test via dynamic import.
|
|
5
|
+
// Actually, let's just test the singleton and reset between tests.
|
|
6
|
+
|
|
7
|
+
// The module exports a singleton `DroidEvents`. We can still test it
|
|
8
|
+
// by carefully managing listener cleanup.
|
|
9
|
+
|
|
10
|
+
import { DroidEvents } from './events';
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Remove all listeners by re-creating internal state isn't possible
|
|
14
|
+
// with the singleton, so we rely on off() in each test.
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('DroidEventBus', () => {
|
|
18
|
+
it('should register and fire a listener via on/emit', () => {
|
|
19
|
+
const handler = vi.fn();
|
|
20
|
+
DroidEvents.on('droid-ready', handler);
|
|
21
|
+
|
|
22
|
+
DroidEvents.emit('droid-ready', { timestamp: 123 });
|
|
23
|
+
|
|
24
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
25
|
+
expect(handler).toHaveBeenCalledWith({ timestamp: 123 });
|
|
26
|
+
|
|
27
|
+
DroidEvents.off('droid-ready', handler);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should unregister a listener via off', () => {
|
|
31
|
+
const handler = vi.fn();
|
|
32
|
+
DroidEvents.on('droid-ready', handler);
|
|
33
|
+
DroidEvents.off('droid-ready', handler);
|
|
34
|
+
|
|
35
|
+
DroidEvents.emit('droid-ready', { timestamp: 1 });
|
|
36
|
+
|
|
37
|
+
expect(handler).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should support multiple listeners on the same event', () => {
|
|
41
|
+
const h1 = vi.fn();
|
|
42
|
+
const h2 = vi.fn();
|
|
43
|
+
DroidEvents.on('droid-ready', h1);
|
|
44
|
+
DroidEvents.on('droid-ready', h2);
|
|
45
|
+
|
|
46
|
+
DroidEvents.emit('droid-ready', { timestamp: 1 });
|
|
47
|
+
|
|
48
|
+
expect(h1).toHaveBeenCalledOnce();
|
|
49
|
+
expect(h2).toHaveBeenCalledOnce();
|
|
50
|
+
|
|
51
|
+
DroidEvents.off('droid-ready', h1);
|
|
52
|
+
DroidEvents.off('droid-ready', h2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should not fail when removing a listener that was never added', () => {
|
|
56
|
+
const handler = vi.fn();
|
|
57
|
+
expect(() => DroidEvents.off('droid-ready', handler)).not.toThrow();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should validate payload via Zod and reject invalid payloads', () => {
|
|
61
|
+
const handler = vi.fn();
|
|
62
|
+
DroidEvents.on('droid-ready', handler);
|
|
63
|
+
|
|
64
|
+
const consoleSpy = vi
|
|
65
|
+
.spyOn(console, 'error')
|
|
66
|
+
.mockImplementation(() => undefined);
|
|
67
|
+
|
|
68
|
+
// @ts-expect-error intentionally passing invalid payload
|
|
69
|
+
DroidEvents.emit('droid-ready', { timestamp: 'not-a-number' });
|
|
70
|
+
|
|
71
|
+
expect(handler).not.toHaveBeenCalled();
|
|
72
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
73
|
+
expect.stringContaining('[DroidEventBus] Invalid payload'),
|
|
74
|
+
expect.anything(),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
consoleSpy.mockRestore();
|
|
78
|
+
DroidEvents.off('droid-ready', handler);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should dispatch a CustomEvent on window', () => {
|
|
82
|
+
const windowHandler = vi.fn();
|
|
83
|
+
window.addEventListener('droid-ready', windowHandler);
|
|
84
|
+
|
|
85
|
+
DroidEvents.emit('droid-ready', { timestamp: 42 });
|
|
86
|
+
|
|
87
|
+
expect(windowHandler).toHaveBeenCalledOnce();
|
|
88
|
+
const event = windowHandler.mock.calls[0][0] as CustomEvent;
|
|
89
|
+
expect(event.detail).toEqual({ timestamp: 42 });
|
|
90
|
+
|
|
91
|
+
window.removeEventListener('droid-ready', windowHandler);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should catch and log listener errors without breaking other listeners', () => {
|
|
95
|
+
const badHandler = vi.fn(() => {
|
|
96
|
+
throw new Error('boom');
|
|
97
|
+
});
|
|
98
|
+
const goodHandler = vi.fn();
|
|
99
|
+
|
|
100
|
+
DroidEvents.on('droid-ready', badHandler);
|
|
101
|
+
DroidEvents.on('droid-ready', goodHandler);
|
|
102
|
+
|
|
103
|
+
const consoleSpy = vi
|
|
104
|
+
.spyOn(console, 'error')
|
|
105
|
+
.mockImplementation(() => undefined);
|
|
106
|
+
|
|
107
|
+
DroidEvents.emit('droid-ready', { timestamp: 1 });
|
|
108
|
+
|
|
109
|
+
expect(badHandler).toHaveBeenCalledOnce();
|
|
110
|
+
expect(goodHandler).toHaveBeenCalledOnce();
|
|
111
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
112
|
+
expect.stringContaining('[DroidEventBus] Listener error'),
|
|
113
|
+
expect.anything(),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
consoleSpy.mockRestore();
|
|
117
|
+
DroidEvents.off('droid-ready', badHandler);
|
|
118
|
+
DroidEvents.off('droid-ready', goodHandler);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should resolve wait() when the event fires', async () => {
|
|
122
|
+
const promise = DroidEvents.wait('droid-ready');
|
|
123
|
+
|
|
124
|
+
DroidEvents.emit('droid-ready', { timestamp: 999 });
|
|
125
|
+
|
|
126
|
+
const result = await promise;
|
|
127
|
+
expect(result).toEqual({ timestamp: 999 });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('wait() should auto-unregister after resolving', async () => {
|
|
131
|
+
const promise = DroidEvents.wait('droid-ready');
|
|
132
|
+
DroidEvents.emit('droid-ready', { timestamp: 1 });
|
|
133
|
+
await promise;
|
|
134
|
+
|
|
135
|
+
// Emit again — the wait handler should not fire again
|
|
136
|
+
// (no way to directly test, but ensures no memory leak)
|
|
137
|
+
const handler = vi.fn();
|
|
138
|
+
DroidEvents.on('droid-ready', handler);
|
|
139
|
+
DroidEvents.emit('droid-ready', { timestamp: 2 });
|
|
140
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
141
|
+
DroidEvents.off('droid-ready', handler);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should emit droid-downscale event with level payload', () => {
|
|
145
|
+
const handler = vi.fn();
|
|
146
|
+
DroidEvents.on('droid-downscale', handler);
|
|
147
|
+
|
|
148
|
+
DroidEvents.emit('droid-downscale', {
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
level: 'minimal',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
154
|
+
expect(handler.mock.calls[0][0].level).toBe('minimal');
|
|
155
|
+
|
|
156
|
+
DroidEvents.off('droid-downscale', handler);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should emit droid-upscale event with level payload', () => {
|
|
160
|
+
const handler = vi.fn();
|
|
161
|
+
DroidEvents.on('droid-upscale', handler);
|
|
162
|
+
|
|
163
|
+
DroidEvents.emit('droid-upscale', {
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
level: 'full',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
169
|
+
expect(handler.mock.calls[0][0].level).toBe('full');
|
|
170
|
+
|
|
171
|
+
DroidEvents.off('droid-upscale', handler);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should emit panel-open and panel-close events', () => {
|
|
175
|
+
const openHandler = vi.fn();
|
|
176
|
+
const closeHandler = vi.fn();
|
|
177
|
+
DroidEvents.on('panel-open', openHandler);
|
|
178
|
+
DroidEvents.on('panel-close', closeHandler);
|
|
179
|
+
|
|
180
|
+
DroidEvents.emit('panel-open', { id: 'left' });
|
|
181
|
+
DroidEvents.emit('panel-close', { id: 'right' });
|
|
182
|
+
|
|
183
|
+
expect(openHandler).toHaveBeenCalledWith({ id: 'left' });
|
|
184
|
+
expect(closeHandler).toHaveBeenCalledWith({ id: 'right' });
|
|
185
|
+
|
|
186
|
+
DroidEvents.off('panel-open', openHandler);
|
|
187
|
+
DroidEvents.off('panel-close', closeHandler);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should emit toast events', () => {
|
|
191
|
+
const handler = vi.fn();
|
|
192
|
+
DroidEvents.on('toast-added', handler);
|
|
193
|
+
|
|
194
|
+
DroidEvents.emit('toast-added', {
|
|
195
|
+
id: 'toast-1',
|
|
196
|
+
message: 'Hello',
|
|
197
|
+
severity: 'info',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(handler).toHaveBeenCalledWith({
|
|
201
|
+
id: 'toast-1',
|
|
202
|
+
message: 'Hello',
|
|
203
|
+
severity: 'info',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
DroidEvents.off('toast-added', handler);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { DroidEventSchemas } from '../types/event-types';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import type {
|
|
3
|
+
DroidEventMap,
|
|
4
|
+
EventKey,
|
|
5
|
+
EventHandler,
|
|
6
|
+
} from '../types/event-types';
|
|
6
7
|
|
|
7
8
|
class DroidEventBus {
|
|
8
|
-
|
|
9
|
+
private listeners = new Map<EventKey, Set<(payload: unknown) => void>>();
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
on<K extends EventKey>(event: K, handler: EventHandler<K>) {
|
|
11
12
|
let handlers = this.listeners.get(event);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
if (!handlers) {
|
|
14
|
+
handlers = new Set();
|
|
15
|
+
this.listeners.set(event, handlers);
|
|
16
|
+
}
|
|
17
|
+
handlers.add(handler as (payload: unknown) => void);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
off<K extends EventKey>(event: K, handler: EventHandler<K>) {
|
|
20
|
-
this.listeners
|
|
21
|
+
this.listeners
|
|
22
|
+
.get(event)
|
|
23
|
+
?.delete(handler as (payload: unknown) => void);
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
emit<K extends EventKey>(event: K, payload: DroidEventMap[K]) {
|
|
@@ -26,7 +29,10 @@ class DroidEventBus {
|
|
|
26
29
|
try {
|
|
27
30
|
schema.parse(payload);
|
|
28
31
|
} catch (err) {
|
|
29
|
-
console.error(
|
|
32
|
+
console.error(
|
|
33
|
+
`[DroidEventBus] Invalid payload for ${event}:`,
|
|
34
|
+
err,
|
|
35
|
+
);
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
32
38
|
}
|
|
@@ -34,7 +40,14 @@ class DroidEventBus {
|
|
|
34
40
|
window.dispatchEvent(new CustomEvent(event, { detail: payload }));
|
|
35
41
|
|
|
36
42
|
for (const handler of this.listeners.get(event) ?? []) {
|
|
37
|
-
|
|
43
|
+
try {
|
|
44
|
+
(handler as EventHandler<K>)(payload);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(
|
|
47
|
+
`[DroidEventBus] Listener error for ${event}:`,
|
|
48
|
+
err,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
38
51
|
}
|
|
39
52
|
}
|
|
40
53
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Builder as FlexBuilder } from 'flatbuffers/
|
|
2
|
-
export { toReference } from 'flatbuffers/
|
|
1
|
+
import { Builder as FlexBuilder } from 'flatbuffers/js/flexbuffers/builder';
|
|
2
|
+
export { toReference } from 'flatbuffers/js/flexbuffers/reference';
|
|
3
3
|
|
|
4
4
|
export function builder(): FlexBuilder {
|
|
5
5
|
return new FlexBuilder();
|
package/src/lib/workers/init.ts
CHANGED
|
@@ -1,40 +1,48 @@
|
|
|
1
|
-
import type { Remote } from 'comlink'
|
|
2
|
-
import type { LocalStorageAPI } from './db-worker'
|
|
1
|
+
import type { Remote } from 'comlink';
|
|
2
|
+
import type { LocalStorageAPI } from './db-worker';
|
|
3
3
|
|
|
4
4
|
export interface InitWorkerOptions {
|
|
5
|
-
version: string
|
|
6
|
-
i18nPath?: string
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
version: string;
|
|
6
|
+
i18nPath?: string;
|
|
7
|
+
dataPath?: string;
|
|
8
|
+
locale?: string;
|
|
9
|
+
defaults?: Record<string, string>;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export async function initializeWorkerDatabase(
|
|
12
13
|
worker: Remote<LocalStorageAPI>,
|
|
13
14
|
{
|
|
14
15
|
version,
|
|
15
|
-
i18nPath
|
|
16
|
+
i18nPath,
|
|
17
|
+
dataPath,
|
|
16
18
|
locale = 'en',
|
|
17
19
|
defaults = {
|
|
18
20
|
welcome: 'Welcome to the app!',
|
|
19
21
|
},
|
|
20
|
-
}: InitWorkerOptions
|
|
22
|
+
}: InitWorkerOptions,
|
|
21
23
|
) {
|
|
22
|
-
console.log('[init-worker] Initializing database...')
|
|
24
|
+
console.log('[init-worker] Initializing database...');
|
|
23
25
|
|
|
24
26
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if (i18nPath) {
|
|
28
|
+
await worker.loadI18nFromJSON(i18nPath);
|
|
29
|
+
console.log(`[init-worker] i18n loaded from ${i18nPath}`);
|
|
30
|
+
}
|
|
27
31
|
|
|
28
|
-
await worker.dbSet('locale', locale)
|
|
32
|
+
await worker.dbSet('locale', locale);
|
|
29
33
|
|
|
30
34
|
for (const [key, value] of Object.entries(defaults)) {
|
|
31
|
-
await worker.dbSet(key, value)
|
|
35
|
+
await worker.dbSet(key, value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await worker.setVersion(version);
|
|
39
|
+
|
|
40
|
+
if (dataPath) {
|
|
41
|
+
void worker.loadServersFromJSON(dataPath);
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
void worker.loadServersFromJSON(); // await worker.loadServersFromJSON();
|
|
36
|
-
console.log(`[init-worker] DB initialized to version ${version}`)
|
|
44
|
+
console.log(`[init-worker] DB initialized to version ${version}`);
|
|
37
45
|
} catch (err) {
|
|
38
|
-
console.error('[init-worker] Initialization failed:', err)
|
|
46
|
+
console.error('[init-worker] Initialization failed:', err);
|
|
39
47
|
}
|
|
40
48
|
}
|