@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
package/src/lib/droid.ts CHANGED
@@ -1,7 +1,51 @@
1
- export async function droid(opts?: { workerURLs?: Record<string, string> }): Promise<{ initialized: boolean }> {
1
+ export {
2
+ downscale,
3
+ upscale,
4
+ getScaleLevel,
5
+ type DroidScaleLevel,
6
+ } from './workers/main.js';
7
+
8
+ export async function droid(opts?: {
9
+ workerURLs?: Record<string, string>;
10
+ workerRefs?: {
11
+ canvasWorker?: Worker;
12
+ dbWorker?: SharedWorker;
13
+ wsWorker?: SharedWorker;
14
+ };
15
+ i18nPath?: string;
16
+ dataPath?: string;
17
+ /** Maximum time (ms) to wait for worker initialization before rejecting. Default: no timeout. */
18
+ initTimeout?: number;
19
+ }): Promise<{ initialized: boolean }> {
2
20
  console.log('[DROID]: droid<T>');
21
+
3
22
  const { main } = await import('./workers/main.js');
4
- await main({ workerURLs: opts?.workerURLs });
5
- console.log('[DROID]: droid<T> => await WorkerURLs');
23
+
24
+ const initPromise = main({
25
+ workerURLs: opts?.workerURLs,
26
+ workerRefs: opts?.workerRefs,
27
+ i18nPath: opts?.i18nPath,
28
+ dataPath: opts?.dataPath,
29
+ });
30
+
31
+ if (opts?.initTimeout && opts.initTimeout > 0) {
32
+ const timeout = new Promise<never>((_, reject) =>
33
+ setTimeout(
34
+ () =>
35
+ reject(
36
+ new Error(
37
+ `[DROID] Worker init timed out after ${opts.initTimeout}ms`,
38
+ ),
39
+ ),
40
+ opts.initTimeout,
41
+ ),
42
+ );
43
+ await Promise.race([initPromise, timeout]);
44
+ } else {
45
+ await initPromise;
46
+ }
47
+
48
+ console.log('[DROID]: droid<T> => await WorkerRefs + URLs');
49
+
6
50
  return { initialized: true };
7
- }
51
+ }
@@ -0,0 +1,236 @@
1
+ // Unified gateway for Supabase operations with automatic strategy selection
2
+
3
+ import type {
4
+ ISupabaseStrategy,
5
+ SelectOptions,
6
+ SessionResponse,
7
+ WebSocketStatus,
8
+ StrategyType,
9
+ GatewayConfig,
10
+ } from './types';
11
+ import {
12
+ detectCapabilities,
13
+ selectStrategy,
14
+ logCapabilities,
15
+ getStrategyDescription,
16
+ } from './capabilities';
17
+ import { SharedWorkerStrategy } from './strategies/SharedWorkerStrategy';
18
+ import { WebWorkerStrategy } from './strategies/WebWorkerStrategy';
19
+ import { DirectStrategy } from './strategies/DirectStrategy';
20
+ import { getWorkerCommunication } from './WorkerCommunication';
21
+ import { DroidEvents } from '../workers/events';
22
+
23
+ // Vite ?worker&url imports — bundles workers as JS and returns their URL
24
+ import defaultSharedWorkerUrl from '../workers/supabase-shared-worker?worker&url';
25
+ import defaultDbWorkerUrl from '../workers/supabase-db-worker?worker&url';
26
+
27
+ /**
28
+ * SupabaseGateway: Unified interface for all Supabase operations
29
+ *
30
+ * Automatically selects the best strategy based on browser capabilities:
31
+ * 1. SharedWorker + DB Worker Pool (desktop: Chrome, Firefox, Edge)
32
+ * 2. WebSocket Worker + DB Worker Pool (Android, Safari)
33
+ * 3. Direct/Main thread (fallback)
34
+ *
35
+ * All strategies implement the same interface, so application code is unchanged.
36
+ */
37
+ export class SupabaseGateway implements ISupabaseStrategy {
38
+ private strategy: ISupabaseStrategy;
39
+ private strategyType: StrategyType;
40
+
41
+ constructor(config: GatewayConfig = {}) {
42
+ const capabilities = detectCapabilities();
43
+ logCapabilities(capabilities);
44
+
45
+ this.strategyType =
46
+ config.forceStrategy || selectStrategy(capabilities);
47
+ console.log(
48
+ `[SupabaseGateway] Selected strategy: ${getStrategyDescription(this.strategyType)}`,
49
+ );
50
+
51
+ const poolSize = config.poolSize ?? 3;
52
+ const workerUrls = config.workerUrls ?? {};
53
+
54
+ switch (this.strategyType) {
55
+ case 'shared-worker': {
56
+ const sharedUrl =
57
+ workerUrls.sharedWorker ?? defaultSharedWorkerUrl;
58
+ const dbUrl = workerUrls.dbWorker ?? defaultDbWorkerUrl;
59
+ this.strategy = new SharedWorkerStrategy(
60
+ sharedUrl,
61
+ dbUrl,
62
+ poolSize,
63
+ );
64
+ break;
65
+ }
66
+ case 'web-worker': {
67
+ const wsUrl =
68
+ workerUrls.webSocketWorker ?? defaultSharedWorkerUrl;
69
+ const dbUrl = workerUrls.dbWorker ?? defaultDbWorkerUrl;
70
+ this.strategy = new WebWorkerStrategy(wsUrl, dbUrl, poolSize);
71
+ break;
72
+ }
73
+ case 'direct':
74
+ this.strategy = new DirectStrategy();
75
+ break;
76
+ }
77
+
78
+ // Forward worker-error events from BroadcastChannel to DroidEvents
79
+ if (typeof window !== 'undefined') {
80
+ const comm = getWorkerCommunication();
81
+ comm.on('worker-error', (payload) => {
82
+ DroidEvents.emit(
83
+ 'worker-error',
84
+ payload as {
85
+ timestamp: number;
86
+ worker: 'shared' | 'db' | 'ws';
87
+ operation: string;
88
+ message: string;
89
+ },
90
+ );
91
+ });
92
+ }
93
+ }
94
+
95
+ getStrategyType(): StrategyType {
96
+ return this.strategyType;
97
+ }
98
+
99
+ getStrategyDescription(): string {
100
+ return getStrategyDescription(this.strategyType);
101
+ }
102
+
103
+ // Delegate all methods to the selected strategy
104
+
105
+ async init(
106
+ url: string,
107
+ anonKey: string,
108
+ options?: Record<string, unknown>,
109
+ ): Promise<SessionResponse> {
110
+ if (this.strategyType === 'direct') {
111
+ return this.strategy.init(url, anonKey, options);
112
+ }
113
+
114
+ const WORKER_INIT_FALLBACK_MS = 3500;
115
+ const primary = this.strategy.init(url, anonKey, options);
116
+
117
+ let timer: ReturnType<typeof setTimeout> | null = null;
118
+ const fallback = new Promise<never>((_, reject) => {
119
+ timer = setTimeout(
120
+ () => reject(new Error('worker init stalled')),
121
+ WORKER_INIT_FALLBACK_MS,
122
+ );
123
+ });
124
+
125
+ try {
126
+ const result = await Promise.race([primary, fallback]);
127
+ if (timer) clearTimeout(timer);
128
+ return result;
129
+ } catch (err) {
130
+ if (timer) clearTimeout(timer);
131
+ const previousType = this.strategyType;
132
+ console.warn(
133
+ `[SupabaseGateway] ${previousType} init did not respond in ${WORKER_INIT_FALLBACK_MS}ms — switching to direct strategy.`,
134
+ err,
135
+ );
136
+ this.strategy = new DirectStrategy();
137
+ this.strategyType = 'direct';
138
+ DroidEvents.emit('gateway-strategy-fallback', {
139
+ timestamp: Date.now(),
140
+ from: previousType,
141
+ to: 'direct',
142
+ reason: err instanceof Error ? err.message : String(err),
143
+ });
144
+ return this.strategy.init(url, anonKey, options);
145
+ }
146
+ }
147
+
148
+ on(event: string, callback: (payload: unknown) => void): () => void {
149
+ return this.strategy.on(event, callback);
150
+ }
151
+
152
+ getSession(): Promise<SessionResponse> {
153
+ return this.strategy.getSession();
154
+ }
155
+
156
+ signInWithPassword(
157
+ email: string,
158
+ password: string,
159
+ ): Promise<SessionResponse> {
160
+ return this.strategy.signInWithPassword(email, password);
161
+ }
162
+
163
+ signOut(): Promise<void> {
164
+ return this.strategy.signOut();
165
+ }
166
+
167
+ select(table: string, opts?: SelectOptions): Promise<unknown[]> {
168
+ return this.strategy.select(table, opts);
169
+ }
170
+
171
+ insert(
172
+ table: string,
173
+ data: Record<string, unknown> | Record<string, unknown>[],
174
+ ): Promise<unknown[]> {
175
+ return this.strategy.insert(table, data);
176
+ }
177
+
178
+ update(
179
+ table: string,
180
+ data: Record<string, unknown>,
181
+ match: Record<string, unknown>,
182
+ ): Promise<unknown[]> {
183
+ return this.strategy.update(table, data, match);
184
+ }
185
+
186
+ upsert(
187
+ table: string,
188
+ data: Record<string, unknown> | Record<string, unknown>[],
189
+ ): Promise<unknown[]> {
190
+ return this.strategy.upsert(table, data);
191
+ }
192
+
193
+ delete(table: string, match: Record<string, unknown>): Promise<unknown[]> {
194
+ return this.strategy.delete(table, match);
195
+ }
196
+
197
+ rpc(fn: string, args?: Record<string, unknown>): Promise<unknown> {
198
+ return this.strategy.rpc(fn, args);
199
+ }
200
+
201
+ subscribePostgres(
202
+ key: string,
203
+ params: Record<string, unknown>,
204
+ callback: (payload: unknown) => void,
205
+ ): () => void {
206
+ return this.strategy.subscribePostgres(key, params, callback);
207
+ }
208
+
209
+ connectWebSocket(wsUrl?: string): Promise<WebSocketStatus> {
210
+ return this.strategy.connectWebSocket(wsUrl);
211
+ }
212
+
213
+ disconnectWebSocket(): Promise<void> {
214
+ return this.strategy.disconnectWebSocket();
215
+ }
216
+
217
+ sendWebSocketMessage(data: unknown): Promise<void> {
218
+ return this.strategy.sendWebSocketMessage(data);
219
+ }
220
+
221
+ getWebSocketStatus(): Promise<WebSocketStatus> {
222
+ return this.strategy.getWebSocketStatus();
223
+ }
224
+
225
+ onWebSocketMessage(callback: (message: unknown) => void): () => void {
226
+ return this.strategy.onWebSocketMessage(callback);
227
+ }
228
+
229
+ onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void {
230
+ return this.strategy.onWebSocketStatus(callback);
231
+ }
232
+
233
+ terminate() {
234
+ this.strategy.terminate();
235
+ }
236
+ }
@@ -0,0 +1,132 @@
1
+ // Unified communication layer for all workers (SharedWorker, WebWorker, main thread)
2
+ // Uses BroadcastChannel for real-time events
3
+
4
+ import type { BroadcastEvent } from './types';
5
+
6
+ /**
7
+ * WorkerCommunication: Unified communication layer for workers
8
+ *
9
+ * Features:
10
+ * - BroadcastChannel for real-time events
11
+ * - Works in SharedWorker, WebWorker, and main thread
12
+ *
13
+ * Note: This layer does NOT open IndexedDB. The SharedWorker is the single
14
+ * writer to the auth database (sb-auth-v2). All other contexts receive
15
+ * auth state via BroadcastChannel or postMessage.
16
+ */
17
+ export class WorkerCommunication {
18
+ private bc: BroadcastChannel | null = null;
19
+ private listeners = new Map<string, Set<(payload: unknown) => void>>();
20
+ private channelName: string;
21
+
22
+ constructor(channelName = 'supabase_events') {
23
+ this.channelName = channelName;
24
+ this.initBroadcastChannel();
25
+ }
26
+
27
+ private initBroadcastChannel() {
28
+ try {
29
+ if (typeof BroadcastChannel !== 'undefined') {
30
+ this.bc = new BroadcastChannel(this.channelName);
31
+ this.bc.onmessage = (e: MessageEvent) =>
32
+ this.handleBroadcast(e.data);
33
+ }
34
+ } catch (err) {
35
+ console.error(
36
+ '[WorkerCommunication] Failed to initialize BroadcastChannel:',
37
+ err,
38
+ );
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Broadcast an event to all workers and tabs
44
+ */
45
+ broadcast(event: BroadcastEvent) {
46
+ if (this.bc) {
47
+ try {
48
+ this.bc.postMessage(event);
49
+ } catch (err) {
50
+ console.error('[WorkerCommunication] Broadcast failed:', err);
51
+ }
52
+ }
53
+ }
54
+
55
+ private handleBroadcast(event: BroadcastEvent) {
56
+ const { type, key } = event;
57
+
58
+ if (type === 'realtime' && key) {
59
+ this.emit(`realtime:${key}`, event.payload);
60
+ } else {
61
+ this.emit(type, event.data ?? event);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Subscribe to events (local and broadcast)
67
+ */
68
+ on(event: string, callback: (payload: unknown) => void): () => void {
69
+ if (!this.listeners.has(event)) {
70
+ this.listeners.set(event, new Set());
71
+ }
72
+ this.listeners.get(event)?.add(callback);
73
+
74
+ return () => {
75
+ const set = this.listeners.get(event);
76
+ if (set) {
77
+ set.delete(callback);
78
+ if (set.size === 0) {
79
+ this.listeners.delete(event);
80
+ }
81
+ }
82
+ };
83
+ }
84
+
85
+ private emit(event: string, payload: unknown) {
86
+ const listeners = this.listeners.get(event);
87
+ if (listeners) {
88
+ listeners.forEach((cb) => {
89
+ try {
90
+ cb(payload);
91
+ } catch (err) {
92
+ console.error(
93
+ `[WorkerCommunication] Listener error for ${event}:`,
94
+ err,
95
+ );
96
+ }
97
+ });
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Check if BroadcastChannel is available
103
+ */
104
+ hasBroadcastChannel(): boolean {
105
+ return this.bc !== null;
106
+ }
107
+
108
+ /**
109
+ * Close connections
110
+ */
111
+ close() {
112
+ if (this.bc) {
113
+ this.bc.close();
114
+ this.bc = null;
115
+ }
116
+ this.listeners.clear();
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Create a singleton instance for use across workers
122
+ */
123
+ let _instance: WorkerCommunication | null = null;
124
+
125
+ export function getWorkerCommunication(
126
+ channelName?: string,
127
+ ): WorkerCommunication {
128
+ if (!_instance) {
129
+ _instance = new WorkerCommunication(channelName);
130
+ }
131
+ return _instance;
132
+ }
@@ -0,0 +1,304 @@
1
+ // Worker pool manager with round-robin routing for DB operations
2
+
3
+ import type { WorkerMessage, WorkerResponse } from './types';
4
+
5
+ export interface WorkerPoolConfig {
6
+ size: number;
7
+ workerUrl: string | URL;
8
+ }
9
+
10
+ /**
11
+ * WorkerPool: Manages a pool of workers with round-robin load balancing
12
+ *
13
+ * Features:
14
+ * - Fixed-size pool (default: 3 workers)
15
+ * - Round-robin routing for database operations
16
+ * - Request/response tracking with UUIDs
17
+ * - Automatic error handling with exponential backoff retries
18
+ */
19
+ export class WorkerPool {
20
+ private workers: Worker[] = [];
21
+ private poolSize: number;
22
+ private roundRobinIndex = 0;
23
+ private pending = new Map<
24
+ string,
25
+ {
26
+ resolve: (value: unknown) => void;
27
+ reject: (reason?: unknown) => void;
28
+ workerId: number;
29
+ }
30
+ >();
31
+ private workerUrl: string | URL;
32
+ private workerRetryCount = new Map<number, number>();
33
+ private workerRetryTimers = new Map<
34
+ number,
35
+ ReturnType<typeof setTimeout>
36
+ >();
37
+
38
+ private readonly MAX_RETRY_ATTEMPTS = 3;
39
+ private readonly BASE_RETRY_DELAY_MS = 1000;
40
+ private readonly MAX_RETRY_DELAY_MS = 8000;
41
+ private readonly REQUEST_TIMEOUT_MS = 30000;
42
+
43
+ constructor(config: WorkerPoolConfig) {
44
+ this.poolSize = config.size;
45
+ this.workerUrl = config.workerUrl;
46
+ }
47
+
48
+ /**
49
+ * Initialize the worker pool
50
+ */
51
+ async init(): Promise<void> {
52
+ console.log(
53
+ `[WorkerPool] Initializing pool with ${this.poolSize} workers`,
54
+ );
55
+
56
+ for (let i = 0; i < this.poolSize; i++) {
57
+ try {
58
+ const worker = new Worker(this.workerUrl, {
59
+ type: 'module',
60
+ name: `db-worker-${i}`,
61
+ });
62
+
63
+ worker.onmessage = (e: MessageEvent) =>
64
+ this.handleMessage(e.data, i);
65
+ worker.onerror = (err) => this.handleError(err, i);
66
+
67
+ this.workers.push(worker);
68
+ console.log(`[WorkerPool] Worker ${i} initialized`);
69
+ } catch (err) {
70
+ console.error(
71
+ `[WorkerPool] Failed to create worker ${i}:`,
72
+ err,
73
+ );
74
+ throw err;
75
+ }
76
+ }
77
+
78
+ // Wait for all workers to be ready
79
+ await Promise.all(
80
+ this.workers.map((_worker, i) =>
81
+ this.sendToWorker(i, {
82
+ id: crypto.randomUUID(),
83
+ type: 'ping',
84
+ payload: {},
85
+ }),
86
+ ),
87
+ );
88
+
89
+ console.log('[WorkerPool] All workers ready');
90
+ }
91
+
92
+ private getNextWorker(): number {
93
+ const workerId = this.roundRobinIndex;
94
+ this.roundRobinIndex = (this.roundRobinIndex + 1) % this.poolSize;
95
+ return workerId;
96
+ }
97
+
98
+ private sendToWorker<T>(
99
+ workerId: number,
100
+ message: WorkerMessage,
101
+ ): Promise<T> {
102
+ return new Promise<T>((resolve, reject) => {
103
+ const worker = this.workers[workerId];
104
+ if (!worker) {
105
+ return reject(new Error(`Worker ${workerId} not available`));
106
+ }
107
+
108
+ const timer = setTimeout(() => {
109
+ if (this.pending.has(message.id)) {
110
+ this.pending.delete(message.id);
111
+ reject(
112
+ new Error(
113
+ `Worker ${workerId} request ${message.type} timed out after ${this.REQUEST_TIMEOUT_MS}ms`,
114
+ ),
115
+ );
116
+ }
117
+ }, this.REQUEST_TIMEOUT_MS);
118
+
119
+ this.pending.set(message.id, {
120
+ resolve: (value: unknown) => {
121
+ clearTimeout(timer);
122
+ resolve(value as T);
123
+ },
124
+ reject: (err: unknown) => {
125
+ clearTimeout(timer);
126
+ reject(err);
127
+ },
128
+ workerId,
129
+ });
130
+
131
+ try {
132
+ worker.postMessage(message);
133
+ } catch (err) {
134
+ clearTimeout(timer);
135
+ this.pending.delete(message.id);
136
+ reject(err);
137
+ }
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Send message to next available worker (round-robin)
143
+ */
144
+ send<T>(type: string, payload?: unknown): Promise<T> {
145
+ const workerId = this.getNextWorker();
146
+ const id = crypto.randomUUID();
147
+ const message: WorkerMessage = { id, type, payload };
148
+
149
+ return this.sendToWorker<T>(workerId, message);
150
+ }
151
+
152
+ private handleMessage(response: WorkerResponse, workerId: number) {
153
+ const { id, ok, data, error } = response;
154
+
155
+ if (!id) {
156
+ console.warn(
157
+ `[WorkerPool] Worker ${workerId} sent message without ID:`,
158
+ response,
159
+ );
160
+ return;
161
+ }
162
+
163
+ const pending = this.pending.get(id);
164
+ if (!pending) {
165
+ console.warn(
166
+ `[WorkerPool] Received response for unknown request ${id}`,
167
+ );
168
+ return;
169
+ }
170
+
171
+ this.pending.delete(id);
172
+
173
+ if (ok) {
174
+ pending.resolve(data);
175
+ } else {
176
+ pending.reject(new Error(error || 'Worker request failed'));
177
+ }
178
+ }
179
+
180
+ private handleError(error: ErrorEvent, workerId: number) {
181
+ console.error(`[WorkerPool] Worker ${workerId} error:`, error);
182
+
183
+ const failedRequests: string[] = [];
184
+ for (const [id, pending] of this.pending.entries()) {
185
+ if (pending.workerId === workerId) {
186
+ pending.reject(
187
+ new Error(`Worker ${workerId} crashed: ${error.message}`),
188
+ );
189
+ failedRequests.push(id);
190
+ }
191
+ }
192
+
193
+ failedRequests.forEach((id) => this.pending.delete(id));
194
+
195
+ this.scheduleWorkerRecreation(workerId);
196
+ }
197
+
198
+ private scheduleWorkerRecreation(workerId: number) {
199
+ const retryCount = this.workerRetryCount.get(workerId) || 0;
200
+
201
+ if (retryCount >= this.MAX_RETRY_ATTEMPTS) {
202
+ console.error(
203
+ `[WorkerPool] Worker ${workerId} has failed ${retryCount} times. Giving up.`,
204
+ );
205
+ this.workerRetryCount.delete(workerId);
206
+ return;
207
+ }
208
+
209
+ const delay = Math.min(
210
+ this.BASE_RETRY_DELAY_MS * Math.pow(2, retryCount),
211
+ this.MAX_RETRY_DELAY_MS,
212
+ );
213
+
214
+ console.log(
215
+ `[WorkerPool] Scheduling worker ${workerId} recreation in ${delay}ms (attempt ${retryCount + 1}/${this.MAX_RETRY_ATTEMPTS})`,
216
+ );
217
+
218
+ const existingTimer = this.workerRetryTimers.get(workerId);
219
+ if (existingTimer) {
220
+ clearTimeout(existingTimer);
221
+ }
222
+
223
+ const timer = setTimeout(() => {
224
+ this.workerRetryTimers.delete(workerId);
225
+ this.recreateWorker(workerId);
226
+ }, delay);
227
+
228
+ this.workerRetryTimers.set(workerId, timer);
229
+ this.workerRetryCount.set(workerId, retryCount + 1);
230
+ }
231
+
232
+ private async recreateWorker(workerId: number) {
233
+ const retryCount = this.workerRetryCount.get(workerId) || 0;
234
+ console.log(
235
+ `[WorkerPool] Attempting to recreate worker ${workerId} (attempt ${retryCount}/${this.MAX_RETRY_ATTEMPTS})`,
236
+ );
237
+
238
+ try {
239
+ const worker = new Worker(this.workerUrl, {
240
+ type: 'module',
241
+ name: `db-worker-${workerId}`,
242
+ });
243
+
244
+ worker.onmessage = (e: MessageEvent) =>
245
+ this.handleMessage(e.data, workerId);
246
+ worker.onerror = (err) => this.handleError(err, workerId);
247
+
248
+ this.workers[workerId] = worker;
249
+
250
+ await this.sendToWorker(workerId, {
251
+ id: crypto.randomUUID(),
252
+ type: 'ping',
253
+ payload: {},
254
+ });
255
+
256
+ console.log(
257
+ `[WorkerPool] Worker ${workerId} recreated successfully`,
258
+ );
259
+ this.workerRetryCount.delete(workerId);
260
+ } catch (err) {
261
+ console.error(
262
+ `[WorkerPool] Failed to recreate worker ${workerId}:`,
263
+ err,
264
+ );
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Terminate all workers
270
+ */
271
+ terminate() {
272
+ console.log('[WorkerPool] Terminating all workers');
273
+
274
+ this.workerRetryTimers.forEach((timer) => clearTimeout(timer));
275
+ this.workerRetryTimers.clear();
276
+ this.workerRetryCount.clear();
277
+
278
+ this.workers.forEach((worker, i) => {
279
+ try {
280
+ worker.terminate();
281
+ } catch (err) {
282
+ console.error(
283
+ `[WorkerPool] Error terminating worker ${i}:`,
284
+ err,
285
+ );
286
+ }
287
+ });
288
+
289
+ this.workers = [];
290
+ this.pending.clear();
291
+ }
292
+
293
+ /**
294
+ * Get pool statistics
295
+ */
296
+ getStats() {
297
+ return {
298
+ size: this.poolSize,
299
+ activeWorkers: this.workers.length,
300
+ pendingRequests: this.pending.size,
301
+ nextWorker: this.roundRobinIndex,
302
+ };
303
+ }
304
+ }