@kbve/droid 0.0.6 → 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.
Files changed (108) hide show
  1. package/comlink.js +1 -7
  2. package/droid.mjs +191 -61
  3. package/index.d.ts +29 -0
  4. package/index.js +9054 -2763
  5. package/lib/gateway/SupabaseGateway.d.ts +38 -0
  6. package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
  7. package/lib/gateway/WorkerCommunication.d.ts +39 -0
  8. package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
  9. package/lib/gateway/WorkerPool.d.ts +55 -0
  10. package/lib/gateway/WorkerPool.d.ts.map +1 -0
  11. package/lib/gateway/capabilities.d.ts +23 -0
  12. package/lib/gateway/capabilities.d.ts.map +1 -0
  13. package/lib/gateway/index.d.ts +9 -0
  14. package/lib/gateway/index.d.ts.map +1 -0
  15. package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
  16. package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
  17. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
  18. package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
  19. package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
  20. package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
  21. package/lib/gateway/types.d.ts +81 -0
  22. package/lib/gateway/types.d.ts.map +1 -0
  23. package/lib/workers/canvas-ui-renderer.d.ts +25 -0
  24. package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
  25. package/lib/workers/canvas-worker.d.ts +14 -0
  26. package/lib/workers/canvas-worker.d.ts.map +1 -0
  27. package/lib/workers/canvas-worker.js +291 -145
  28. package/lib/workers/data.d.ts +40 -0
  29. package/lib/workers/data.d.ts.map +1 -0
  30. package/lib/workers/db-worker.d.ts +69 -0
  31. package/lib/workers/db-worker.d.ts.map +1 -0
  32. package/lib/workers/db-worker.js +2680 -2147
  33. package/lib/workers/events.d.ts +11 -0
  34. package/lib/workers/events.d.ts.map +1 -0
  35. package/lib/workers/flexbuilder.d.ts +4 -0
  36. package/lib/workers/flexbuilder.d.ts.map +1 -0
  37. package/lib/workers/init.d.ts +11 -0
  38. package/lib/workers/init.d.ts.map +1 -0
  39. package/lib/workers/main.d.ts +107 -0
  40. package/lib/workers/main.d.ts.map +1 -0
  41. package/lib/workers/supabase-db-worker.d.ts +2 -0
  42. package/lib/workers/supabase-db-worker.d.ts.map +1 -0
  43. package/lib/workers/supabase-db-worker.js +9337 -0
  44. package/lib/workers/supabase-shared-worker.d.ts +2 -0
  45. package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
  46. package/lib/workers/supabase-shared-worker.js +12635 -0
  47. package/lib/workers/tools.d.ts +4 -0
  48. package/lib/workers/tools.d.ts.map +1 -0
  49. package/lib/workers/validate.d.ts +15 -0
  50. package/lib/workers/validate.d.ts.map +1 -0
  51. package/lib/workers/ws-worker.d.ts +11 -0
  52. package/lib/workers/ws-worker.d.ts.map +1 -0
  53. package/lib/workers/ws-worker.js +174 -115
  54. package/main.js +4875 -0
  55. package/package.json +17 -6
  56. package/reference.js +688 -228
  57. package/src/index.ts +38 -13
  58. package/src/lib/droid.spec.ts +14 -19
  59. package/src/lib/droid.ts +48 -4
  60. package/src/lib/gateway/SupabaseGateway.ts +183 -0
  61. package/src/lib/gateway/WorkerCommunication.ts +132 -0
  62. package/src/lib/gateway/WorkerPool.ts +304 -0
  63. package/src/lib/gateway/capabilities.ts +80 -0
  64. package/src/lib/gateway/index.ts +15 -0
  65. package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
  66. package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
  67. package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
  68. package/src/lib/gateway/types.ts +112 -0
  69. package/src/lib/mod/mod-manager.ts +13 -8
  70. package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
  71. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
  72. package/src/lib/mod/module/supabase/mod-supabase.ts +68 -0
  73. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
  74. package/src/lib/state/auth.spec.ts +58 -0
  75. package/src/lib/state/auth.ts +30 -0
  76. package/src/lib/state/index.ts +34 -0
  77. package/src/lib/state/overlay-manager.ts +125 -0
  78. package/src/lib/state/router.ts +24 -0
  79. package/src/lib/state/theme-sync.ts +75 -0
  80. package/src/lib/state/toasts.spec.ts +106 -0
  81. package/src/lib/state/toasts.ts +34 -0
  82. package/src/lib/state/ui.spec.ts +148 -0
  83. package/src/lib/state/ui.ts +45 -0
  84. package/src/lib/state/welcome-toast.ts +76 -0
  85. package/src/lib/types/bento.spec.ts +116 -0
  86. package/src/lib/types/bento.ts +94 -55
  87. package/src/lib/types/event-types.spec.ts +93 -0
  88. package/src/lib/types/event-types.ts +39 -3
  89. package/src/lib/types/modules.ts +12 -5
  90. package/src/lib/types/ui-event-types.ts +66 -0
  91. package/src/lib/workers/canvas-ui-renderer.ts +266 -0
  92. package/src/lib/workers/canvas-worker.ts +94 -9
  93. package/src/lib/workers/data.ts +59 -36
  94. package/src/lib/workers/db-worker.ts +79 -32
  95. package/src/lib/workers/events.spec.ts +208 -0
  96. package/src/lib/workers/events.ts +27 -14
  97. package/src/lib/workers/flexbuilder.ts +2 -2
  98. package/src/lib/workers/init.ts +25 -17
  99. package/src/lib/workers/main.ts +510 -316
  100. package/src/lib/workers/supabase-db-worker.ts +221 -0
  101. package/src/lib/workers/supabase-shared-worker.ts +572 -0
  102. package/src/lib/workers/validate.ts +64 -0
  103. package/src/lib/workers/ws-worker.ts +156 -25
  104. package/src/setup-vitest.ts +4 -2
  105. package/src/types.ts +30 -0
  106. package/src/vite-env.d.ts +1 -0
  107. package/workers/main.js +10 -1123
  108. package/src/types.d.ts +0 -31
@@ -0,0 +1,443 @@
1
+ // Fallback strategy for browsers without worker support
2
+ // Runs everything on the main thread
3
+
4
+ import type {
5
+ SupabaseClient,
6
+ RealtimePostgresChangesFilter,
7
+ } from '@supabase/supabase-js';
8
+ import { REALTIME_POSTGRES_CHANGES_LISTEN_EVENT } from '@supabase/supabase-js';
9
+
10
+ import type {
11
+ ISupabaseStrategy,
12
+ SelectOptions,
13
+ SessionResponse,
14
+ WebSocketStatus,
15
+ } from '../types';
16
+
17
+ /**
18
+ * DirectStrategy: Everything runs in main thread
19
+ *
20
+ * Architecture:
21
+ * - No workers, all operations on main thread
22
+ * - Uses Supabase client directly
23
+ * - Implements same interface as worker strategies
24
+ *
25
+ * Best for: Browsers without Worker/BroadcastChannel support, or emergency fallback
26
+ *
27
+ * Note: Requires `@supabase/supabase-js` to be available at runtime.
28
+ * This strategy dynamically imports it to avoid bundling Supabase in the main droid build.
29
+ */
30
+ export class DirectStrategy implements ISupabaseStrategy {
31
+ private client: SupabaseClient | null = null;
32
+ private ws: WebSocket | null = null;
33
+ private wsReconnectTimer: ReturnType<typeof setTimeout> | null = null;
34
+ private wsReconnectAttempts = 0;
35
+ private wsHeartbeatTimer: ReturnType<typeof setInterval> | null = null;
36
+ private wsLastPongTime = 0;
37
+ private subscriptions = new Map<
38
+ string,
39
+ { unsubscribe: () => Promise<void> | void }
40
+ >();
41
+ private listeners = new Map<string, Set<(payload: unknown) => void>>();
42
+
43
+ private readonly WS_MAX_RECONNECT_ATTEMPTS = 5;
44
+ private readonly WS_RECONNECT_DELAY_MS = 3000;
45
+ private readonly WS_HEARTBEAT_INTERVAL_MS = 30000;
46
+ private readonly WS_HEARTBEAT_TIMEOUT_MS = 60000;
47
+
48
+ async init(
49
+ url: string,
50
+ anonKey: string,
51
+ options?: Record<string, unknown>,
52
+ ): Promise<SessionResponse> {
53
+ console.log('[DirectStrategy] Initializing...');
54
+
55
+ // Dynamic import to avoid bundling Supabase when not using DirectStrategy
56
+ const { createClient } = await import('@supabase/supabase-js');
57
+
58
+ const authOptions =
59
+ options?.['auth'] && typeof options['auth'] === 'object'
60
+ ? (options['auth'] as Record<string, unknown>)
61
+ : {};
62
+
63
+ this.client = createClient(url, anonKey, {
64
+ ...options,
65
+ auth: {
66
+ ...authOptions,
67
+ autoRefreshToken: true,
68
+ persistSession: true,
69
+ detectSessionInUrl: false,
70
+ },
71
+ });
72
+
73
+ this.client.auth.onAuthStateChange((_event, session) => {
74
+ this.emit('auth', { session });
75
+ });
76
+
77
+ const {
78
+ data: { session },
79
+ error,
80
+ } = await this.client.auth.getSession();
81
+
82
+ if (error) {
83
+ console.error('[DirectStrategy] Session error:', error);
84
+ }
85
+
86
+ console.log('[DirectStrategy] Initialized');
87
+
88
+ return {
89
+ session: session as SessionResponse['session'],
90
+ user: (session?.user as unknown as Record<string, unknown>) || null,
91
+ };
92
+ }
93
+
94
+ on(event: string, callback: (payload: unknown) => void): () => void {
95
+ if (!this.listeners.has(event)) {
96
+ this.listeners.set(event, new Set());
97
+ }
98
+ this.listeners.get(event)?.add(callback);
99
+
100
+ return () => {
101
+ const set = this.listeners.get(event);
102
+ if (set) {
103
+ set.delete(callback);
104
+ if (set.size === 0) this.listeners.delete(event);
105
+ }
106
+ };
107
+ }
108
+
109
+ private emit(event: string, payload: unknown) {
110
+ this.listeners.get(event)?.forEach((cb) => {
111
+ try {
112
+ cb(payload);
113
+ } catch (err) {
114
+ console.error(
115
+ `[DirectStrategy] Listener error for ${event}:`,
116
+ err,
117
+ );
118
+ }
119
+ });
120
+ }
121
+
122
+ // Auth
123
+ async getSession(): Promise<SessionResponse> {
124
+ if (!this.client) throw new Error('Client not initialized');
125
+ const { data, error } = await this.client.auth.getSession();
126
+ if (error) throw error;
127
+ return data as unknown as SessionResponse;
128
+ }
129
+
130
+ async signInWithPassword(
131
+ email: string,
132
+ password: string,
133
+ ): Promise<SessionResponse> {
134
+ if (!this.client) throw new Error('Client not initialized');
135
+ const { data, error } = await this.client.auth.signInWithPassword({
136
+ email,
137
+ password,
138
+ });
139
+ if (error) throw error;
140
+ return data as unknown as SessionResponse;
141
+ }
142
+
143
+ async signOut(): Promise<void> {
144
+ if (!this.client) throw new Error('Client not initialized');
145
+ const { error } = await this.client.auth.signOut();
146
+ if (error) throw error;
147
+ }
148
+
149
+ // Database
150
+ async select(table: string, opts?: SelectOptions): Promise<unknown[]> {
151
+ if (!this.client) throw new Error('Client not initialized');
152
+ let query = this.client.from(table).select(opts?.columns || '*');
153
+ if (opts?.match) {
154
+ for (const [key, value] of Object.entries(opts.match)) {
155
+ query = query.eq(key, value);
156
+ }
157
+ }
158
+ if (opts?.limit) query = query.limit(opts.limit);
159
+ const { data, error } = await query;
160
+ if (error) throw error;
161
+ return data;
162
+ }
163
+
164
+ async insert(
165
+ table: string,
166
+ data: Record<string, unknown> | Record<string, unknown>[],
167
+ ): Promise<unknown[]> {
168
+ if (!this.client) throw new Error('Client not initialized');
169
+ const { data: result, error } = await this.client
170
+ .from(table)
171
+ .insert(data)
172
+ .select();
173
+ if (error) throw error;
174
+ return result;
175
+ }
176
+
177
+ async update(
178
+ table: string,
179
+ data: Record<string, unknown>,
180
+ match: Record<string, unknown>,
181
+ ): Promise<unknown[]> {
182
+ if (!this.client) throw new Error('Client not initialized');
183
+ let query = this.client.from(table).update(data);
184
+ for (const [key, value] of Object.entries(match)) {
185
+ query = query.eq(key, value);
186
+ }
187
+ const { data: result, error } = await query.select();
188
+ if (error) throw error;
189
+ return result;
190
+ }
191
+
192
+ async upsert(
193
+ table: string,
194
+ data: Record<string, unknown> | Record<string, unknown>[],
195
+ ): Promise<unknown[]> {
196
+ if (!this.client) throw new Error('Client not initialized');
197
+ const { data: result, error } = await this.client
198
+ .from(table)
199
+ .upsert(data)
200
+ .select();
201
+ if (error) throw error;
202
+ return result;
203
+ }
204
+
205
+ async delete(
206
+ table: string,
207
+ match: Record<string, unknown>,
208
+ ): Promise<unknown[]> {
209
+ if (!this.client) throw new Error('Client not initialized');
210
+ let query = this.client.from(table).delete();
211
+ for (const [key, value] of Object.entries(match)) {
212
+ query = query.eq(key, value);
213
+ }
214
+ const { data: result, error } = await query.select();
215
+ if (error) throw error;
216
+ return result;
217
+ }
218
+
219
+ async rpc(fn: string, args?: Record<string, unknown>): Promise<unknown> {
220
+ if (!this.client) throw new Error('Client not initialized');
221
+ const { data, error } = await this.client.rpc(fn, args || {});
222
+ if (error) throw error;
223
+ return data;
224
+ }
225
+
226
+ // Realtime
227
+ subscribePostgres(
228
+ key: string,
229
+ params: Record<string, unknown>,
230
+ callback: (payload: unknown) => void,
231
+ ): () => void {
232
+ if (!this.client) throw new Error('Client not initialized');
233
+
234
+ const channel = this.client
235
+ .channel(key)
236
+ .on(
237
+ 'postgres_changes',
238
+ params as RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT}`>,
239
+ (payload: unknown) => {
240
+ this.emit(`realtime:${key}`, payload);
241
+ },
242
+ );
243
+
244
+ channel.subscribe();
245
+ this.subscriptions.set(key, {
246
+ unsubscribe: async () => {
247
+ await channel.unsubscribe();
248
+ },
249
+ });
250
+
251
+ const off = this.on(`realtime:${key}`, callback);
252
+
253
+ return () => {
254
+ off();
255
+ const sub = this.subscriptions.get(key);
256
+ if (sub) {
257
+ sub.unsubscribe();
258
+ this.subscriptions.delete(key);
259
+ }
260
+ };
261
+ }
262
+
263
+ // WebSocket
264
+ async connectWebSocket(wsUrl?: string): Promise<WebSocketStatus> {
265
+ if (
266
+ this.ws &&
267
+ (this.ws.readyState === WebSocket.CONNECTING ||
268
+ this.ws.readyState === WebSocket.OPEN)
269
+ ) {
270
+ return this.getWebSocketStatus();
271
+ }
272
+
273
+ if (!this.client) throw new Error('Client not initialized');
274
+
275
+ const {
276
+ data: { session },
277
+ error,
278
+ } = await this.client.auth.getSession();
279
+ if (error || !session?.access_token) {
280
+ throw new Error('No active session');
281
+ }
282
+
283
+ const url = wsUrl || this.getDefaultWebSocketUrl();
284
+ const authenticatedUrl = `${url}?token=${encodeURIComponent(session.access_token)}`;
285
+
286
+ console.log('[DirectStrategy] Connecting to WebSocket:', url);
287
+ this.ws = new WebSocket(authenticatedUrl);
288
+
289
+ this.ws.onopen = () => {
290
+ this.wsReconnectAttempts = 0;
291
+ this.wsLastPongTime = Date.now();
292
+ this.startHeartbeat();
293
+ this.emit('ws.status', { status: 'connected', url });
294
+ };
295
+
296
+ this.ws.onmessage = (event) => {
297
+ try {
298
+ const message = JSON.parse(event.data as string) as Record<
299
+ string,
300
+ unknown
301
+ >;
302
+ if (message['type'] === 'pong') {
303
+ this.wsLastPongTime = Date.now();
304
+ return;
305
+ }
306
+ this.emit('ws.message', message);
307
+ } catch (error) {
308
+ console.error(
309
+ '[DirectStrategy] Failed to parse message:',
310
+ error,
311
+ );
312
+ }
313
+ };
314
+
315
+ this.ws.onerror = () => {
316
+ this.emit('ws.status', {
317
+ status: 'error',
318
+ error: 'Connection error',
319
+ });
320
+ };
321
+
322
+ this.ws.onclose = (event) => {
323
+ this.ws = null;
324
+ this.stopHeartbeat();
325
+ this.emit('ws.status', {
326
+ status: 'disconnected',
327
+ code: event.code,
328
+ reason: event.reason,
329
+ });
330
+
331
+ if (
332
+ event.code !== 1000 &&
333
+ this.wsReconnectAttempts < this.WS_MAX_RECONNECT_ATTEMPTS
334
+ ) {
335
+ this.wsReconnectAttempts++;
336
+ this.wsReconnectTimer = setTimeout(() => {
337
+ this.connectWebSocket(wsUrl).catch(() => {
338
+ /* reconnect best-effort */
339
+ });
340
+ }, this.WS_RECONNECT_DELAY_MS);
341
+ }
342
+ };
343
+
344
+ return this.getWebSocketStatus();
345
+ }
346
+
347
+ async disconnectWebSocket(): Promise<void> {
348
+ if (this.wsReconnectTimer) {
349
+ clearTimeout(this.wsReconnectTimer);
350
+ this.wsReconnectTimer = null;
351
+ }
352
+ this.stopHeartbeat();
353
+ if (this.ws) {
354
+ this.ws.close(1000, 'Client request');
355
+ this.ws = null;
356
+ this.wsReconnectAttempts = 0;
357
+ this.emit('ws.status', { status: 'disconnected' });
358
+ }
359
+ }
360
+
361
+ async sendWebSocketMessage(data: unknown): Promise<void> {
362
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
363
+ throw new Error('WebSocket not connected');
364
+ }
365
+ const message = typeof data === 'string' ? data : JSON.stringify(data);
366
+ this.ws.send(message);
367
+ }
368
+
369
+ async getWebSocketStatus(): Promise<WebSocketStatus> {
370
+ if (!this.ws) return { status: 'disconnected', readyState: null };
371
+ const readyStateMap: Record<number, WebSocketStatus['status']> = {
372
+ [WebSocket.CONNECTING]: 'connecting',
373
+ [WebSocket.OPEN]: 'connected',
374
+ [WebSocket.CLOSING]: 'disconnected',
375
+ [WebSocket.CLOSED]: 'disconnected',
376
+ };
377
+ return {
378
+ status: readyStateMap[this.ws.readyState] || 'disconnected',
379
+ readyState: this.ws.readyState,
380
+ };
381
+ }
382
+
383
+ onWebSocketMessage(callback: (message: unknown) => void): () => void {
384
+ return this.on('ws.message', callback);
385
+ }
386
+
387
+ onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void {
388
+ return this.on('ws.status', callback as (payload: unknown) => void);
389
+ }
390
+
391
+ // Helpers
392
+ private getDefaultWebSocketUrl(): string {
393
+ if (typeof window === 'undefined') return 'ws://localhost:4321/ws';
394
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
395
+ const isLocalhost =
396
+ window.location.hostname === 'localhost' ||
397
+ window.location.hostname === '127.0.0.1';
398
+ const port = isLocalhost ? '4321' : window.location.port;
399
+ const host = window.location.hostname;
400
+ return `${protocol}//${host}${port ? ':' + port : ''}/ws`;
401
+ }
402
+
403
+ private startHeartbeat() {
404
+ this.stopHeartbeat();
405
+ this.wsHeartbeatTimer = setInterval(() => {
406
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
407
+ this.stopHeartbeat();
408
+ return;
409
+ }
410
+ if (
411
+ this.wsLastPongTime > 0 &&
412
+ Date.now() - this.wsLastPongTime > this.WS_HEARTBEAT_TIMEOUT_MS
413
+ ) {
414
+ console.warn('[DirectStrategy] Heartbeat timeout');
415
+ this.stopHeartbeat();
416
+ this.ws.close(1001, 'Heartbeat timeout');
417
+ return;
418
+ }
419
+ try {
420
+ this.ws.send(JSON.stringify({ type: 'ping' }));
421
+ } catch (error) {
422
+ console.error(
423
+ '[DirectStrategy] Failed to send heartbeat:',
424
+ error,
425
+ );
426
+ }
427
+ }, this.WS_HEARTBEAT_INTERVAL_MS);
428
+ }
429
+
430
+ private stopHeartbeat() {
431
+ if (this.wsHeartbeatTimer) {
432
+ clearInterval(this.wsHeartbeatTimer);
433
+ this.wsHeartbeatTimer = null;
434
+ }
435
+ }
436
+
437
+ terminate() {
438
+ this.disconnectWebSocket();
439
+ this.subscriptions.forEach((sub) => sub.unsubscribe());
440
+ this.subscriptions.clear();
441
+ this.listeners.clear();
442
+ }
443
+ }
@@ -0,0 +1,278 @@
1
+ // Strategy for desktop browsers with SharedWorker support
2
+
3
+ import type {
4
+ ISupabaseStrategy,
5
+ SelectOptions,
6
+ SessionResponse,
7
+ WebSocketStatus,
8
+ } from '../types';
9
+ import { WorkerPool } from '../WorkerPool';
10
+ import { getWorkerCommunication } from '../WorkerCommunication';
11
+
12
+ /** Union of all possible messages arriving on the SharedWorker port. */
13
+ interface SharedWorkerEvent {
14
+ id?: string;
15
+ type?: string;
16
+ ok?: boolean;
17
+ data?: unknown;
18
+ error?: string;
19
+ key?: string;
20
+ payload?: unknown;
21
+ }
22
+
23
+ /**
24
+ * SharedWorkerStrategy: Uses SharedWorker for WebSocket + DB Worker Pool
25
+ *
26
+ * Architecture:
27
+ * - SharedWorker handles WebSocket, auth, and realtime (cross-tab sharing)
28
+ * - Worker pool (3 workers) handles database operations (parallel execution)
29
+ * - BroadcastChannel connects SharedWorker to worker pool
30
+ *
31
+ * Best for: Desktop Chrome, Firefox, Edge
32
+ */
33
+ export class SharedWorkerStrategy implements ISupabaseStrategy {
34
+ private worker?: SharedWorker;
35
+ private port?: MessagePort;
36
+ private workerPool?: WorkerPool;
37
+ private pending = new Map<
38
+ string,
39
+ {
40
+ resolve: (value: unknown) => void;
41
+ reject: (reason?: unknown) => void;
42
+ }
43
+ >();
44
+ private listeners = new Map<string, Set<(p: unknown) => void>>();
45
+ private comm = getWorkerCommunication();
46
+ private sharedWorkerUrl: string | URL;
47
+ private dbWorkerUrl: string | URL;
48
+ private poolSize: number;
49
+
50
+ constructor(
51
+ sharedWorkerUrl: string | URL,
52
+ dbWorkerUrl: string | URL,
53
+ poolSize = 3,
54
+ ) {
55
+ this.sharedWorkerUrl = sharedWorkerUrl;
56
+ this.dbWorkerUrl = dbWorkerUrl;
57
+ this.poolSize = poolSize;
58
+
59
+ if (typeof window === 'undefined') return;
60
+ if (!('SharedWorker' in window)) {
61
+ throw new Error('SharedWorker not supported');
62
+ }
63
+ }
64
+
65
+ async init(
66
+ url: string,
67
+ anonKey: string,
68
+ options?: Record<string, unknown>,
69
+ ): Promise<SessionResponse> {
70
+ console.log('[SharedWorkerStrategy] Initializing...');
71
+
72
+ this.worker = new SharedWorker(this.sharedWorkerUrl, {
73
+ type: 'module',
74
+ name: 'supabase-shared',
75
+ });
76
+ this.port = this.worker.port;
77
+ this.port.onmessage = (e: MessageEvent<SharedWorkerEvent>) =>
78
+ this.onMessage(e.data);
79
+ this.port.start();
80
+
81
+ this.workerPool = new WorkerPool({
82
+ size: this.poolSize,
83
+ workerUrl: this.dbWorkerUrl,
84
+ });
85
+ await this.workerPool.init();
86
+
87
+ const sharedWorkerInit = this.send<SessionResponse>('init', {
88
+ url,
89
+ anonKey,
90
+ options,
91
+ });
92
+
93
+ const poolInitPromises = Array.from({ length: this.poolSize }, () =>
94
+ this.workerPool?.send('init', { url, anonKey, options }),
95
+ );
96
+
97
+ await Promise.all([sharedWorkerInit, ...poolInitPromises]);
98
+
99
+ console.log(
100
+ `[SharedWorkerStrategy] Initialized (SharedWorker + ${this.poolSize} DB workers)`,
101
+ );
102
+
103
+ return sharedWorkerInit;
104
+ }
105
+
106
+ private onMessage(msg: SharedWorkerEvent) {
107
+ if (msg?.type === 'ready' || msg?.type === 'auth') {
108
+ this.emit(msg.type, msg);
109
+ return;
110
+ }
111
+ if (msg?.type === 'realtime' && msg.key) {
112
+ this.emit(`realtime:${msg.key}`, msg.payload);
113
+ return;
114
+ }
115
+ if (msg?.type === 'ws.message') {
116
+ this.emit('ws.message', msg.data);
117
+ return;
118
+ }
119
+ if (msg?.type === 'ws.status') {
120
+ this.emit('ws.status', msg);
121
+ return;
122
+ }
123
+
124
+ const { id, ok, data, error } = msg ?? {};
125
+ if (id && this.pending.has(id)) {
126
+ const pending = this.pending.get(id);
127
+ if (!pending) return;
128
+ const { resolve, reject } = pending;
129
+ this.pending.delete(id);
130
+ ok ? resolve(data) : reject(new Error(error));
131
+ }
132
+ }
133
+
134
+ private send<T>(type: string, payload?: unknown): Promise<T> {
135
+ const id = crypto.randomUUID();
136
+ return new Promise<T>((resolve, reject) => {
137
+ if (!this.port)
138
+ return reject(new Error('SharedWorker unavailable'));
139
+ this.pending.set(id, {
140
+ resolve: resolve as (value: unknown) => void,
141
+ reject,
142
+ });
143
+ this.port.postMessage({ id, type, payload });
144
+ });
145
+ }
146
+
147
+ on(event: string, callback: (payload: unknown) => void): () => void {
148
+ if (!this.listeners.has(event)) {
149
+ this.listeners.set(event, new Set());
150
+ }
151
+ this.listeners.get(event)?.add(callback);
152
+
153
+ return () => {
154
+ const set = this.listeners.get(event);
155
+ if (set) {
156
+ set.delete(callback);
157
+ if (set.size === 0) this.listeners.delete(event);
158
+ }
159
+ };
160
+ }
161
+
162
+ private emit(event: string, payload: unknown) {
163
+ this.listeners.get(event)?.forEach((cb) => cb(payload));
164
+ }
165
+
166
+ // Auth (via SharedWorker)
167
+ getSession(): Promise<SessionResponse> {
168
+ return this.send<SessionResponse>('getSession');
169
+ }
170
+
171
+ signInWithPassword(
172
+ email: string,
173
+ password: string,
174
+ ): Promise<SessionResponse> {
175
+ return this.send<SessionResponse>('signInWithPassword', {
176
+ email,
177
+ password,
178
+ });
179
+ }
180
+
181
+ signOut(): Promise<void> {
182
+ return this.send('signOut');
183
+ }
184
+
185
+ // Database (via worker pool)
186
+ select(table: string, opts?: SelectOptions): Promise<unknown[]> {
187
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
188
+ return this.workerPool.send('from.select', { table, ...(opts ?? {}) });
189
+ }
190
+
191
+ insert(
192
+ table: string,
193
+ data: Record<string, unknown> | Record<string, unknown>[],
194
+ ): Promise<unknown[]> {
195
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
196
+ return this.workerPool.send('from.insert', { table, data });
197
+ }
198
+
199
+ update(
200
+ table: string,
201
+ data: Record<string, unknown>,
202
+ match: Record<string, unknown>,
203
+ ): Promise<unknown[]> {
204
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
205
+ return this.workerPool.send('from.update', { table, data, match });
206
+ }
207
+
208
+ upsert(
209
+ table: string,
210
+ data: Record<string, unknown> | Record<string, unknown>[],
211
+ ): Promise<unknown[]> {
212
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
213
+ return this.workerPool.send('from.upsert', { table, data });
214
+ }
215
+
216
+ delete(table: string, match: Record<string, unknown>): Promise<unknown[]> {
217
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
218
+ return this.workerPool.send('from.delete', { table, match });
219
+ }
220
+
221
+ rpc(fn: string, args?: Record<string, unknown>): Promise<unknown> {
222
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
223
+ return this.workerPool.send('rpc', { fn, args });
224
+ }
225
+
226
+ // Realtime (via SharedWorker)
227
+ subscribePostgres(
228
+ key: string,
229
+ params: Record<string, unknown>,
230
+ callback: (payload: unknown) => void,
231
+ ): () => void {
232
+ const off = this.on(`realtime:${key}`, callback);
233
+ this.send('realtime.subscribe', { key, params }).catch((e) => {
234
+ off();
235
+ console.error(e);
236
+ });
237
+ return () => {
238
+ off();
239
+ this.send('realtime.unsubscribe', { key }).catch(() => {
240
+ /* best-effort */
241
+ });
242
+ };
243
+ }
244
+
245
+ // WebSocket (via SharedWorker)
246
+ connectWebSocket(wsUrl?: string): Promise<WebSocketStatus> {
247
+ return this.send<WebSocketStatus>('ws.connect', { wsUrl });
248
+ }
249
+
250
+ disconnectWebSocket(): Promise<void> {
251
+ return this.send('ws.disconnect');
252
+ }
253
+
254
+ sendWebSocketMessage(data: unknown): Promise<void> {
255
+ return this.send('ws.send', { data });
256
+ }
257
+
258
+ getWebSocketStatus(): Promise<WebSocketStatus> {
259
+ return this.send<WebSocketStatus>('ws.status');
260
+ }
261
+
262
+ onWebSocketMessage(callback: (message: unknown) => void): () => void {
263
+ return this.on('ws.message', callback);
264
+ }
265
+
266
+ onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void {
267
+ return this.on('ws.status', (payload) =>
268
+ callback(payload as WebSocketStatus),
269
+ );
270
+ }
271
+
272
+ terminate() {
273
+ this.workerPool?.terminate();
274
+ this.comm.close();
275
+ this.listeners.clear();
276
+ this.pending.clear();
277
+ }
278
+ }