@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.
Files changed (190) hide show
  1. package/droid.mjs +2312 -74
  2. package/index.d.ts +32 -0
  3. package/lib/agents/api-types.d.ts +121 -0
  4. package/lib/agents/api-types.d.ts.map +1 -0
  5. package/lib/agents/auth/init.d.ts +8 -0
  6. package/lib/agents/auth/init.d.ts.map +1 -0
  7. package/lib/agents/cache.d.ts +9 -0
  8. package/lib/agents/cache.d.ts.map +1 -0
  9. package/lib/agents/client/callGuildVault.d.ts +3 -0
  10. package/lib/agents/client/callGuildVault.d.ts.map +1 -0
  11. package/lib/agents/client/callJson.d.ts +3 -0
  12. package/lib/agents/client/callJson.d.ts.map +1 -0
  13. package/lib/agents/config.d.ts +36 -0
  14. package/lib/agents/config.d.ts.map +1 -0
  15. package/lib/agents/constants.d.ts +11 -0
  16. package/lib/agents/constants.d.ts.map +1 -0
  17. package/lib/agents/createAgents.d.ts +4 -0
  18. package/lib/agents/createAgents.d.ts.map +1 -0
  19. package/lib/agents/ctx.d.ts +46 -0
  20. package/lib/agents/ctx.d.ts.map +1 -0
  21. package/lib/agents/discord/bot.d.ts +21 -0
  22. package/lib/agents/discord/bot.d.ts.map +1 -0
  23. package/lib/agents/discord/guilds.d.ts +8 -0
  24. package/lib/agents/discord/guilds.d.ts.map +1 -0
  25. package/lib/agents/formDrafts.d.ts +5 -0
  26. package/lib/agents/formDrafts.d.ts.map +1 -0
  27. package/lib/agents/generated/agents-schema.d.ts +73 -0
  28. package/lib/agents/generated/agents-schema.d.ts.map +1 -0
  29. package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
  30. package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
  31. package/lib/agents/github/backfill.d.ts +23 -0
  32. package/lib/agents/github/backfill.d.ts.map +1 -0
  33. package/lib/agents/github/events.d.ts +10 -0
  34. package/lib/agents/github/events.d.ts.map +1 -0
  35. package/lib/agents/github/pat.d.ts +13 -0
  36. package/lib/agents/github/pat.d.ts.map +1 -0
  37. package/lib/agents/github/repoConfig.d.ts +25 -0
  38. package/lib/agents/github/repoConfig.d.ts.map +1 -0
  39. package/lib/agents/github/tokens.d.ts +22 -0
  40. package/lib/agents/github/tokens.d.ts.map +1 -0
  41. package/lib/agents/github/webhook.d.ts +37 -0
  42. package/lib/agents/github/webhook.d.ts.map +1 -0
  43. package/lib/agents/index.d.ts +8 -0
  44. package/lib/agents/index.d.ts.map +1 -0
  45. package/lib/agents/state/atoms.d.ts +122 -0
  46. package/lib/agents/state/atoms.d.ts.map +1 -0
  47. package/lib/agents/types.d.ts +65 -0
  48. package/lib/agents/types.d.ts.map +1 -0
  49. package/lib/gateway/SupabaseGateway.d.ts +38 -0
  50. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  51. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  52. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  53. package/lib/gateway/WorkerPool.d.ts +55 -0
  54. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  55. package/lib/gateway/capabilities.d.ts +23 -0
  56. package/lib/gateway/capabilities.d.ts.map +1 -0
  57. package/lib/gateway/index.d.ts +9 -0
  58. package/lib/gateway/index.d.ts.map +1 -0
  59. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  60. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  61. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  62. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  63. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  64. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  65. package/lib/gateway/types.d.ts +81 -0
  66. package/lib/gateway/types.d.ts.map +1 -0
  67. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  68. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  69. package/lib/workers/canvas-worker.d.ts +14 -0
  70. package/lib/workers/canvas-worker.d.ts.map +1 -0
  71. package/lib/workers/canvas-worker.js +5 -302
  72. package/lib/workers/data.d.ts +40 -0
  73. package/lib/workers/data.d.ts.map +1 -0
  74. package/lib/workers/db-worker.d.ts +69 -0
  75. package/lib/workers/db-worker.d.ts.map +1 -0
  76. package/lib/workers/db-worker.js +13 -3636
  77. package/lib/workers/events.d.ts +11 -0
  78. package/lib/workers/events.d.ts.map +1 -0
  79. package/lib/workers/flexbuilder.d.ts +4 -0
  80. package/lib/workers/flexbuilder.d.ts.map +1 -0
  81. package/lib/workers/init.d.ts +11 -0
  82. package/lib/workers/init.d.ts.map +1 -0
  83. package/lib/workers/main.d.ts +105 -0
  84. package/lib/workers/main.d.ts.map +1 -0
  85. package/lib/workers/supabase-db-worker.d.ts +2 -0
  86. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  87. package/lib/workers/supabase-db-worker.js +23 -0
  88. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  89. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  90. package/lib/workers/supabase-shared-worker.js +25 -0
  91. package/lib/workers/tools.d.ts +4 -0
  92. package/lib/workers/tools.d.ts.map +1 -0
  93. package/lib/workers/validate.d.ts +15 -0
  94. package/lib/workers/validate.d.ts.map +1 -0
  95. package/lib/workers/ws-worker.d.ts +10 -0
  96. package/lib/workers/ws-worker.d.ts.map +1 -0
  97. package/lib/workers/ws-worker.js +5 -273
  98. package/main.js +5711 -0
  99. package/package.json +18 -6
  100. package/src/index.ts +41 -13
  101. package/src/lib/agents/api-types.ts +170 -0
  102. package/src/lib/agents/auth/init.ts +200 -0
  103. package/src/lib/agents/cache.ts +127 -0
  104. package/src/lib/agents/client/callGuildVault.ts +71 -0
  105. package/src/lib/agents/client/callJson.ts +64 -0
  106. package/src/lib/agents/config.ts +83 -0
  107. package/src/lib/agents/constants.ts +11 -0
  108. package/src/lib/agents/createAgents.ts +62 -0
  109. package/src/lib/agents/ctx.ts +63 -0
  110. package/src/lib/agents/discord/bot.ts +169 -0
  111. package/src/lib/agents/discord/guilds.ts +106 -0
  112. package/src/lib/agents/formDrafts.ts +55 -0
  113. package/src/lib/agents/generated/agents-schema.ts +155 -0
  114. package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
  115. package/src/lib/agents/github/backfill.ts +126 -0
  116. package/src/lib/agents/github/events.ts +131 -0
  117. package/src/lib/agents/github/pat.ts +150 -0
  118. package/src/lib/agents/github/repoConfig.ts +240 -0
  119. package/src/lib/agents/github/tokens.ts +222 -0
  120. package/src/lib/agents/github/webhook.ts +349 -0
  121. package/src/lib/agents/index.ts +35 -0
  122. package/src/lib/agents/state/atoms.ts +122 -0
  123. package/src/lib/agents/types.ts +83 -0
  124. package/src/lib/api.ts +71 -0
  125. package/src/lib/droid.spec.ts +14 -19
  126. package/src/lib/droid.ts +48 -4
  127. package/src/lib/gateway/SupabaseGateway.ts +236 -0
  128. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  129. package/src/lib/gateway/WorkerPool.ts +304 -0
  130. package/src/lib/gateway/capabilities.spec.ts +144 -0
  131. package/src/lib/gateway/capabilities.ts +80 -0
  132. package/src/lib/gateway/index.ts +15 -0
  133. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  134. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  135. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  136. package/src/lib/gateway/types.ts +139 -0
  137. package/src/lib/mod/mod-manager.ts +13 -8
  138. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  139. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  140. package/src/lib/mod/module/supabase/mod-supabase.ts +73 -0
  141. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  142. package/src/lib/state/auth.spec.ts +58 -0
  143. package/src/lib/state/auth.ts +148 -0
  144. package/src/lib/state/bento.spec.ts +76 -0
  145. package/src/lib/state/bento.ts +51 -0
  146. package/src/lib/state/index.ts +98 -0
  147. package/src/lib/state/overlay-manager.ts +125 -0
  148. package/src/lib/state/profile-sync.ts +214 -0
  149. package/src/lib/state/profile.ts +155 -0
  150. package/src/lib/state/router.ts +24 -0
  151. package/src/lib/state/staff.ts +128 -0
  152. package/src/lib/state/sync-bus.ts +109 -0
  153. package/src/lib/state/theme-sync.ts +75 -0
  154. package/src/lib/state/toasts.spec.ts +106 -0
  155. package/src/lib/state/toasts.ts +34 -0
  156. package/src/lib/state/ui.spec.ts +148 -0
  157. package/src/lib/state/ui.ts +45 -0
  158. package/src/lib/state/welcome-toast.spec.ts +101 -0
  159. package/src/lib/state/welcome-toast.ts +76 -0
  160. package/src/lib/sw-recovery.spec.ts +165 -0
  161. package/src/lib/sw-recovery.ts +141 -0
  162. package/src/lib/types/bento.spec.ts +116 -0
  163. package/src/lib/types/bento.ts +94 -55
  164. package/src/lib/types/event-schemas.spec.ts +120 -0
  165. package/src/lib/types/event-types.spec.ts +96 -0
  166. package/src/lib/types/event-types.ts +113 -3
  167. package/src/lib/types/modules.ts +12 -5
  168. package/src/lib/types/ui-event-types.ts +66 -0
  169. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  170. package/src/lib/workers/canvas-worker.ts +94 -9
  171. package/src/lib/workers/data.ts +64 -37
  172. package/src/lib/workers/db-worker.ts +105 -32
  173. package/src/lib/workers/events.spec.ts +208 -0
  174. package/src/lib/workers/events.ts +27 -14
  175. package/src/lib/workers/flexbuilder.ts +2 -2
  176. package/src/lib/workers/init.ts +25 -17
  177. package/src/lib/workers/main.ts +506 -327
  178. package/src/lib/workers/supabase-db-worker.ts +243 -0
  179. package/src/lib/workers/supabase-shared-worker.ts +607 -0
  180. package/src/lib/workers/validate.ts +64 -0
  181. package/src/lib/workers/ws-worker.spec.ts +41 -0
  182. package/src/lib/workers/ws-worker.ts +255 -31
  183. package/src/setup-vitest.ts +4 -2
  184. package/src/types.ts +30 -0
  185. package/src/vite-env.d.ts +1 -0
  186. package/workers/main.js +2 -1125
  187. package/comlink.js +0 -253
  188. package/index.js +0 -2972
  189. package/reference.js +0 -259
  190. 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: any }, string>;
13
- meta!: Table<{ key: string; value: any }, string>;
14
- i18n!: Table<{ key: string; value: string }, string>;
15
- htmlservers!: Table<{ key: string; value: string }, string>;
16
- servers!: Table<DiscordServer, string>;
17
- tags!: Table<DiscordTag, string>;
18
- profiles!: Table<Profile, string>;
19
- ws_messages!: Table<{ key: string; message: any }, string>;
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(3).stores({
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
- function delay(ms: number) {
38
- return new Promise((res) => setTimeout(res, ms));
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
- const decoded = toReference(buffer).toObject();
59
- await db.ws_messages.put({ key, message: decoded });
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<{ key: string; message: any }[]> {
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 = 'https://discord.sh/i18n/db.json') {
99
- const res = await fetch(path);
100
- const data = await res.json();
101
- await storageAPI.putI18nBatch(data);
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
- const all = await db.i18n.toCollection().primaryKeys();
105
- return all as string[];
138
+ return (await db.i18n.toCollection().primaryKeys()) as string[];
106
139
  },
107
140
 
108
- async loadServersFromJSON(path = 'https://discord.sh/data/servers.json') {
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.toCollection().primaryKeys();
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(`[syncHtmlFromServers] Generated ${missing.length} HTML cards.`);
174
+ console.info(
175
+ `[syncHtmlFromServers] Generated ${missing.length} HTML cards.`,
176
+ );
134
177
  } else {
135
- console.info('[syncHtmlFromServers] All servers already have HTML cards.');
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: any) {
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 { DroidEventMap, EventKey, EventHandler } from '../types/event-types';
3
-
4
- import { z } from 'zod';
5
-
2
+ import type {
3
+ DroidEventMap,
4
+ EventKey,
5
+ EventHandler,
6
+ } from '../types/event-types';
6
7
 
7
8
  class DroidEventBus {
8
- private listeners = new Map<EventKey, Set<(payload: unknown) => void>>();
9
+ private listeners = new Map<EventKey, Set<(payload: unknown) => void>>();
9
10
 
10
- on<K extends EventKey>(event: K, handler: EventHandler<K>) {
11
+ on<K extends EventKey>(event: K, handler: EventHandler<K>) {
11
12
  let handlers = this.listeners.get(event);
12
- if (!handlers) {
13
- handlers = new Set();
14
- this.listeners.set(event, handlers);
15
- }
16
- handlers.add(handler as (payload: unknown) => void);
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.get(event)?.delete(handler as (payload: unknown) => void);
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(`[DroidEventBus] Invalid payload for ${event}:`, err);
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
- (handler as EventHandler<K>)(payload);
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/ts/flexbuffers/builder';
2
- export { toReference } from 'flatbuffers/ts/flexbuffers/reference';
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();
@@ -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
- locale?: string
8
- defaults?: Record<string, any>
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 = 'https://discord.sh/i18n/db.json',
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
- await worker.loadI18nFromJSON(i18nPath)
26
- console.log(`[init-worker] i18n loaded from ${i18nPath}`)
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
- await worker.setVersion(version)
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
  }