@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.
- package/droid.mjs +2312 -74
- package/index.d.ts +32 -0
- package/lib/agents/api-types.d.ts +121 -0
- package/lib/agents/api-types.d.ts.map +1 -0
- package/lib/agents/auth/init.d.ts +8 -0
- package/lib/agents/auth/init.d.ts.map +1 -0
- package/lib/agents/cache.d.ts +9 -0
- package/lib/agents/cache.d.ts.map +1 -0
- package/lib/agents/client/callGuildVault.d.ts +3 -0
- package/lib/agents/client/callGuildVault.d.ts.map +1 -0
- package/lib/agents/client/callJson.d.ts +3 -0
- package/lib/agents/client/callJson.d.ts.map +1 -0
- package/lib/agents/config.d.ts +36 -0
- package/lib/agents/config.d.ts.map +1 -0
- package/lib/agents/constants.d.ts +11 -0
- package/lib/agents/constants.d.ts.map +1 -0
- package/lib/agents/createAgents.d.ts +4 -0
- package/lib/agents/createAgents.d.ts.map +1 -0
- package/lib/agents/ctx.d.ts +46 -0
- package/lib/agents/ctx.d.ts.map +1 -0
- package/lib/agents/discord/bot.d.ts +21 -0
- package/lib/agents/discord/bot.d.ts.map +1 -0
- package/lib/agents/discord/guilds.d.ts +8 -0
- package/lib/agents/discord/guilds.d.ts.map +1 -0
- package/lib/agents/formDrafts.d.ts +5 -0
- package/lib/agents/formDrafts.d.ts.map +1 -0
- package/lib/agents/generated/agents-schema.d.ts +73 -0
- package/lib/agents/generated/agents-schema.d.ts.map +1 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
- package/lib/agents/github/backfill.d.ts +23 -0
- package/lib/agents/github/backfill.d.ts.map +1 -0
- package/lib/agents/github/events.d.ts +10 -0
- package/lib/agents/github/events.d.ts.map +1 -0
- package/lib/agents/github/pat.d.ts +13 -0
- package/lib/agents/github/pat.d.ts.map +1 -0
- package/lib/agents/github/repoConfig.d.ts +25 -0
- package/lib/agents/github/repoConfig.d.ts.map +1 -0
- package/lib/agents/github/tokens.d.ts +22 -0
- package/lib/agents/github/tokens.d.ts.map +1 -0
- package/lib/agents/github/webhook.d.ts +37 -0
- package/lib/agents/github/webhook.d.ts.map +1 -0
- package/lib/agents/index.d.ts +8 -0
- package/lib/agents/index.d.ts.map +1 -0
- package/lib/agents/state/atoms.d.ts +122 -0
- package/lib/agents/state/atoms.d.ts.map +1 -0
- package/lib/agents/types.d.ts +65 -0
- package/lib/agents/types.d.ts.map +1 -0
- 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 +5 -302
- 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 +13 -3636
- 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 +105 -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 +23 -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 +25 -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 +10 -0
- package/lib/workers/ws-worker.d.ts.map +1 -0
- package/lib/workers/ws-worker.js +5 -273
- package/main.js +5711 -0
- package/package.json +18 -6
- package/src/index.ts +41 -13
- package/src/lib/agents/api-types.ts +170 -0
- package/src/lib/agents/auth/init.ts +200 -0
- package/src/lib/agents/cache.ts +127 -0
- package/src/lib/agents/client/callGuildVault.ts +71 -0
- package/src/lib/agents/client/callJson.ts +64 -0
- package/src/lib/agents/config.ts +83 -0
- package/src/lib/agents/constants.ts +11 -0
- package/src/lib/agents/createAgents.ts +62 -0
- package/src/lib/agents/ctx.ts +63 -0
- package/src/lib/agents/discord/bot.ts +169 -0
- package/src/lib/agents/discord/guilds.ts +106 -0
- package/src/lib/agents/formDrafts.ts +55 -0
- package/src/lib/agents/generated/agents-schema.ts +155 -0
- package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
- package/src/lib/agents/github/backfill.ts +126 -0
- package/src/lib/agents/github/events.ts +131 -0
- package/src/lib/agents/github/pat.ts +150 -0
- package/src/lib/agents/github/repoConfig.ts +240 -0
- package/src/lib/agents/github/tokens.ts +222 -0
- package/src/lib/agents/github/webhook.ts +349 -0
- package/src/lib/agents/index.ts +35 -0
- package/src/lib/agents/state/atoms.ts +122 -0
- package/src/lib/agents/types.ts +83 -0
- package/src/lib/api.ts +71 -0
- package/src/lib/droid.spec.ts +14 -19
- package/src/lib/droid.ts +48 -4
- package/src/lib/gateway/SupabaseGateway.ts +236 -0
- package/src/lib/gateway/WorkerCommunication.ts +132 -0
- package/src/lib/gateway/WorkerPool.ts +304 -0
- package/src/lib/gateway/capabilities.spec.ts +144 -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 +139 -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 +73 -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 +148 -0
- package/src/lib/state/bento.spec.ts +76 -0
- package/src/lib/state/bento.ts +51 -0
- package/src/lib/state/index.ts +98 -0
- package/src/lib/state/overlay-manager.ts +125 -0
- package/src/lib/state/profile-sync.ts +214 -0
- package/src/lib/state/profile.ts +155 -0
- package/src/lib/state/router.ts +24 -0
- package/src/lib/state/staff.ts +128 -0
- package/src/lib/state/sync-bus.ts +109 -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.spec.ts +101 -0
- package/src/lib/state/welcome-toast.ts +76 -0
- package/src/lib/sw-recovery.spec.ts +165 -0
- package/src/lib/sw-recovery.ts +141 -0
- package/src/lib/types/bento.spec.ts +116 -0
- package/src/lib/types/bento.ts +94 -55
- package/src/lib/types/event-schemas.spec.ts +120 -0
- package/src/lib/types/event-types.spec.ts +96 -0
- package/src/lib/types/event-types.ts +113 -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 +64 -37
- package/src/lib/workers/db-worker.ts +105 -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 +506 -327
- package/src/lib/workers/supabase-db-worker.ts +243 -0
- package/src/lib/workers/supabase-shared-worker.ts +607 -0
- package/src/lib/workers/validate.ts +64 -0
- package/src/lib/workers/ws-worker.spec.ts +41 -0
- package/src/lib/workers/ws-worker.ts +255 -31
- 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 +2 -1125
- package/comlink.js +0 -253
- package/index.js +0 -2972
- package/reference.js +0 -259
- package/src/types.d.ts +0 -31
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { expose } from 'comlink';
|
|
2
2
|
import Dexie, { type Table } from 'dexie';
|
|
3
3
|
import type { DiscordServer, DiscordTag, Profile } from '../types/discord';
|
|
4
|
-
import { toReference } from './flexbuilder';
|
|
5
4
|
|
|
6
5
|
interface SharedWorkerGlobalScope extends Worker {
|
|
7
6
|
onconnect: (event: MessageEvent) => void;
|
|
@@ -9,18 +8,22 @@ interface SharedWorkerGlobalScope extends Worker {
|
|
|
9
8
|
declare const self: SharedWorkerGlobalScope;
|
|
10
9
|
|
|
11
10
|
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<{
|
|
11
|
+
settings!: Table<{ id: string; value: unknown }>;
|
|
12
|
+
meta!: Table<{ key: string; value: unknown }>;
|
|
13
|
+
i18n!: Table<{ key: string; value: string }>;
|
|
14
|
+
htmlservers!: Table<{ key: string; value: string }>;
|
|
15
|
+
servers!: Table<DiscordServer>;
|
|
16
|
+
tags!: Table<DiscordTag>;
|
|
17
|
+
profiles!: Table<Profile>;
|
|
18
|
+
ws_messages!: Table<{
|
|
19
|
+
key: string;
|
|
20
|
+
message: Uint8Array | Record<string, unknown>;
|
|
21
|
+
}>;
|
|
22
|
+
auth_tokens!: Table<{ key: string; value: string; expires_at?: number }>;
|
|
20
23
|
|
|
21
24
|
constructor() {
|
|
22
25
|
super('AppStorage');
|
|
23
|
-
this.version(
|
|
26
|
+
this.version(4).stores({
|
|
24
27
|
settings: '&id',
|
|
25
28
|
meta: '&key',
|
|
26
29
|
i18n: '&key',
|
|
@@ -29,13 +32,33 @@ class AppDexie extends Dexie {
|
|
|
29
32
|
tags: '&tag_id',
|
|
30
33
|
profiles: '&profile_id',
|
|
31
34
|
ws_messages: '&key',
|
|
35
|
+
auth_tokens: '&key',
|
|
32
36
|
});
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
const db = new AppDexie();
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
// --- ws-worker → db-worker direct pipeline ---
|
|
42
|
+
// Listen for WebSocket messages from the ws-worker via BroadcastChannel.
|
|
43
|
+
// This keeps the main thread out of the data storage path entirely.
|
|
44
|
+
if (typeof BroadcastChannel !== 'undefined') {
|
|
45
|
+
const wsChannel = new BroadcastChannel('kbve_ws_data');
|
|
46
|
+
wsChannel.onmessage = (e: MessageEvent) => {
|
|
47
|
+
const msg = e.data;
|
|
48
|
+
if (msg?.type !== 'ws.store') return;
|
|
49
|
+
|
|
50
|
+
const key = `ws:${msg.ts ?? Date.now()}`;
|
|
51
|
+
const message =
|
|
52
|
+
msg.format === 'text'
|
|
53
|
+
? new TextEncoder().encode(msg.data)
|
|
54
|
+
: msg.data instanceof Uint8Array
|
|
55
|
+
? msg.data
|
|
56
|
+
: new Uint8Array(msg.data ?? []);
|
|
57
|
+
|
|
58
|
+
db.ws_messages.put({ key, message }).catch((err) => {
|
|
59
|
+
console.error('[DB] Failed to store ws message:', err);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
function renderHtmlForServer(server: DiscordServer): string {
|
|
@@ -51,19 +74,24 @@ function renderHtmlForServer(server: DiscordServer): string {
|
|
|
51
74
|
|
|
52
75
|
// --- Comlink-Safe API ---
|
|
53
76
|
const storageAPI = {
|
|
54
|
-
|
|
55
77
|
// WebSocket
|
|
56
78
|
|
|
57
79
|
async storeWsMessage(key: string, buffer: ArrayBuffer) {
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
// Store raw bytes — format-agnostic so the ws-worker can carry
|
|
81
|
+
// any protocol (IRC text, FlatBuffers, Protobuf, etc.).
|
|
82
|
+
// Uint8Array is natively supported by IndexedDB and safe for
|
|
83
|
+
// Dexie's BroadcastChannel multi-tab sync (no DataCloneError).
|
|
84
|
+
// Consumers decode on read using the appropriate deserializer.
|
|
85
|
+
await db.ws_messages.put({ key, message: new Uint8Array(buffer) });
|
|
60
86
|
},
|
|
61
87
|
|
|
62
88
|
async getWsMessage(key: string) {
|
|
63
89
|
return (await db.ws_messages.get(key))?.message ?? null;
|
|
64
90
|
},
|
|
65
|
-
|
|
66
|
-
async getAllWsMessages(): Promise<
|
|
91
|
+
|
|
92
|
+
async getAllWsMessages(): Promise<
|
|
93
|
+
{ key: string; message: Uint8Array | Record<string, unknown> }[]
|
|
94
|
+
> {
|
|
67
95
|
const raw = await db.ws_messages.toArray();
|
|
68
96
|
return raw.sort((a, b) => {
|
|
69
97
|
const at = Number(a.key.split(':')[1]);
|
|
@@ -71,7 +99,7 @@ const storageAPI = {
|
|
|
71
99
|
return bt - at;
|
|
72
100
|
});
|
|
73
101
|
},
|
|
74
|
-
|
|
102
|
+
|
|
75
103
|
async clearWsMessages() {
|
|
76
104
|
await db.ws_messages.clear();
|
|
77
105
|
},
|
|
@@ -95,20 +123,31 @@ const storageAPI = {
|
|
|
95
123
|
}));
|
|
96
124
|
await db.i18n.bulkPut(entries);
|
|
97
125
|
},
|
|
98
|
-
async loadI18nFromJSON(path
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
126
|
+
async loadI18nFromJSON(path: string) {
|
|
127
|
+
const res = await fetch(path);
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`[db-worker] i18n fetch failed: ${res.status} ${res.statusText}`,
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
await storageAPI.putI18nBatch(data);
|
|
102
136
|
},
|
|
103
137
|
async getAllI18nKeys(): Promise<string[]> {
|
|
104
|
-
|
|
105
|
-
return all as string[];
|
|
138
|
+
return (await db.i18n.toCollection().primaryKeys()) as string[];
|
|
106
139
|
},
|
|
107
140
|
|
|
108
|
-
async loadServersFromJSON(path
|
|
141
|
+
async loadServersFromJSON(path: string) {
|
|
109
142
|
const res = await fetch(path);
|
|
143
|
+
if (!res.ok) {
|
|
144
|
+
console.warn(
|
|
145
|
+
`[db-worker] servers fetch failed: ${res.status} ${res.statusText}`,
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
110
149
|
const data = await res.json();
|
|
111
|
-
|
|
150
|
+
|
|
112
151
|
// The JSON should be a Record<string, DiscordServer>
|
|
113
152
|
const servers: DiscordServer[] = Object.values(data);
|
|
114
153
|
await storageAPI.putServers(servers);
|
|
@@ -118,26 +157,32 @@ const storageAPI = {
|
|
|
118
157
|
// HTML PreRender
|
|
119
158
|
async syncHtmlFromServers() {
|
|
120
159
|
const allServers = await db.servers.toArray();
|
|
121
|
-
const existingHtmlKeys = await db.htmlservers
|
|
160
|
+
const existingHtmlKeys = await db.htmlservers
|
|
161
|
+
.toCollection()
|
|
162
|
+
.primaryKeys();
|
|
122
163
|
const missing: { key: string; value: string }[] = [];
|
|
123
|
-
|
|
164
|
+
|
|
124
165
|
for (const server of allServers) {
|
|
125
166
|
if (!existingHtmlKeys.includes(server.server_id)) {
|
|
126
167
|
const html = renderHtmlForServer(server);
|
|
127
168
|
missing.push({ key: server.server_id, value: html });
|
|
128
169
|
}
|
|
129
170
|
}
|
|
130
|
-
|
|
171
|
+
|
|
131
172
|
if (missing.length > 0) {
|
|
132
173
|
await db.htmlservers.bulkPut(missing);
|
|
133
|
-
console.info(
|
|
174
|
+
console.info(
|
|
175
|
+
`[syncHtmlFromServers] Generated ${missing.length} HTML cards.`,
|
|
176
|
+
);
|
|
134
177
|
} else {
|
|
135
|
-
console.info(
|
|
178
|
+
console.info(
|
|
179
|
+
'[syncHtmlFromServers] All servers already have HTML cards.',
|
|
180
|
+
);
|
|
136
181
|
}
|
|
137
182
|
},
|
|
138
183
|
|
|
139
184
|
// Settings
|
|
140
|
-
async dbSet(id: string, value:
|
|
185
|
+
async dbSet(id: string, value: unknown) {
|
|
141
186
|
await db.settings.put({ id, value });
|
|
142
187
|
},
|
|
143
188
|
async dbGet(id: string) {
|
|
@@ -192,12 +237,40 @@ const storageAPI = {
|
|
|
192
237
|
async getAllProfiles() {
|
|
193
238
|
return await db.profiles.toArray();
|
|
194
239
|
},
|
|
240
|
+
|
|
241
|
+
// Auth Tokens (IDB-backed auth storage)
|
|
242
|
+
async setAuthToken(key: string, value: string, expiresAt?: number) {
|
|
243
|
+
await db.auth_tokens.put({ key, value, expires_at: expiresAt });
|
|
244
|
+
},
|
|
245
|
+
async getAuthToken(key: string): Promise<string | null> {
|
|
246
|
+
const record = await db.auth_tokens.get(key);
|
|
247
|
+
if (!record) return null;
|
|
248
|
+
if (record.expires_at && Date.now() > record.expires_at) {
|
|
249
|
+
await db.auth_tokens.delete(key);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return record.value;
|
|
253
|
+
},
|
|
254
|
+
async removeAuthToken(key: string) {
|
|
255
|
+
await db.auth_tokens.delete(key);
|
|
256
|
+
},
|
|
257
|
+
async clearAuthTokens() {
|
|
258
|
+
await db.auth_tokens.clear();
|
|
259
|
+
},
|
|
195
260
|
};
|
|
196
261
|
|
|
197
262
|
export type LocalStorageAPI = typeof storageAPI;
|
|
198
263
|
|
|
264
|
+
const ports = new Set<MessagePort>();
|
|
265
|
+
|
|
199
266
|
self.onconnect = (event: MessageEvent) => {
|
|
200
267
|
const port = event.ports[0];
|
|
268
|
+
const isFirst = ports.size === 0;
|
|
269
|
+
ports.add(port);
|
|
201
270
|
port.start();
|
|
271
|
+
port.postMessage({ type: isFirst ? 'first-connect' : 'reconnect' });
|
|
272
|
+
port.addEventListener('message', (e: MessageEvent) => {
|
|
273
|
+
if (e.data?.type === 'close') ports.delete(port);
|
|
274
|
+
});
|
|
202
275
|
expose(storageAPI, port);
|
|
203
276
|
};
|
|
@@ -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
|
}
|