@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
@@ -0,0 +1,144 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ detectCapabilities,
4
+ getStrategyDescription,
5
+ logCapabilities,
6
+ selectStrategy,
7
+ } from './capabilities';
8
+
9
+ const realUA = navigator.userAgent;
10
+
11
+ afterEach(() => {
12
+ Object.defineProperty(navigator, 'userAgent', {
13
+ value: realUA,
14
+ configurable: true,
15
+ });
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ function setUA(ua: string) {
20
+ Object.defineProperty(navigator, 'userAgent', {
21
+ value: ua,
22
+ configurable: true,
23
+ });
24
+ }
25
+
26
+ describe('detectCapabilities', () => {
27
+ it('flags Safari when Safari is in UA but Chrome is not', () => {
28
+ setUA(
29
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
30
+ );
31
+ const caps = detectCapabilities();
32
+ expect(caps.isSafari).toBe(true);
33
+ expect(caps.isAndroid).toBe(false);
34
+ });
35
+
36
+ it('does not flag Safari for Chrome on macOS', () => {
37
+ setUA(
38
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
39
+ );
40
+ const caps = detectCapabilities();
41
+ expect(caps.isSafari).toBe(false);
42
+ });
43
+
44
+ it('flags Android UA', () => {
45
+ setUA(
46
+ 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36',
47
+ );
48
+ const caps = detectCapabilities();
49
+ expect(caps.isAndroid).toBe(true);
50
+ });
51
+ });
52
+
53
+ describe('selectStrategy', () => {
54
+ it('picks shared-worker on capable desktop', () => {
55
+ expect(
56
+ selectStrategy({
57
+ hasSharedWorker: true,
58
+ hasWorker: true,
59
+ hasBroadcastChannel: true,
60
+ isAndroid: false,
61
+ isSafari: false,
62
+ }),
63
+ ).toBe('shared-worker');
64
+ });
65
+
66
+ it('falls back to web-worker on Android even with SharedWorker support', () => {
67
+ expect(
68
+ selectStrategy({
69
+ hasSharedWorker: true,
70
+ hasWorker: true,
71
+ hasBroadcastChannel: true,
72
+ isAndroid: true,
73
+ isSafari: false,
74
+ }),
75
+ ).toBe('web-worker');
76
+ });
77
+
78
+ it('falls back to web-worker without SharedWorker', () => {
79
+ expect(
80
+ selectStrategy({
81
+ hasSharedWorker: false,
82
+ hasWorker: true,
83
+ hasBroadcastChannel: true,
84
+ isAndroid: false,
85
+ isSafari: true,
86
+ }),
87
+ ).toBe('web-worker');
88
+ });
89
+
90
+ it('falls back to direct without BroadcastChannel', () => {
91
+ expect(
92
+ selectStrategy({
93
+ hasSharedWorker: true,
94
+ hasWorker: true,
95
+ hasBroadcastChannel: false,
96
+ isAndroid: false,
97
+ isSafari: false,
98
+ }),
99
+ ).toBe('direct');
100
+ });
101
+
102
+ it('returns direct when Worker is unavailable', () => {
103
+ expect(
104
+ selectStrategy({
105
+ hasSharedWorker: false,
106
+ hasWorker: false,
107
+ hasBroadcastChannel: false,
108
+ isAndroid: false,
109
+ isSafari: false,
110
+ }),
111
+ ).toBe('direct');
112
+ });
113
+ });
114
+
115
+ describe('getStrategyDescription', () => {
116
+ it('returns a description for every strategy variant', () => {
117
+ expect(getStrategyDescription('shared-worker')).toMatch(/SharedWorker/);
118
+ expect(getStrategyDescription('web-worker')).toMatch(/WebSocket/);
119
+ expect(getStrategyDescription('direct')).toMatch(/Direct/);
120
+ });
121
+ });
122
+
123
+ describe('logCapabilities', () => {
124
+ it('writes a single console.log with the capability snapshot', () => {
125
+ const spy = vi
126
+ .spyOn(console, 'log')
127
+ .mockImplementation(() => undefined);
128
+ logCapabilities({
129
+ hasSharedWorker: true,
130
+ hasWorker: true,
131
+ hasBroadcastChannel: true,
132
+ isAndroid: false,
133
+ isSafari: false,
134
+ });
135
+ expect(spy).toHaveBeenCalledTimes(1);
136
+ const [, payload] = spy.mock.calls[0];
137
+ expect(payload).toMatchObject({
138
+ SharedWorker: 'yes',
139
+ Worker: 'yes',
140
+ BroadcastChannel: 'yes',
141
+ Platform: 'Desktop',
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,80 @@
1
+ // Browser capability detection for strategy selection
2
+
3
+ import type { BrowserCapabilities, StrategyType } from './types';
4
+
5
+ /**
6
+ * Detect browser capabilities for worker and communication support
7
+ */
8
+ export function detectCapabilities(): BrowserCapabilities {
9
+ // SSR guard
10
+ if (typeof window === 'undefined') {
11
+ return {
12
+ hasSharedWorker: false,
13
+ hasWorker: false,
14
+ hasBroadcastChannel: false,
15
+ isAndroid: false,
16
+ isSafari: false,
17
+ };
18
+ }
19
+
20
+ const userAgent = navigator.userAgent || '';
21
+
22
+ return {
23
+ hasSharedWorker: typeof SharedWorker !== 'undefined',
24
+ hasWorker: typeof Worker !== 'undefined',
25
+ hasBroadcastChannel: typeof BroadcastChannel !== 'undefined',
26
+ isAndroid: /Android/i.test(userAgent),
27
+ isSafari: /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent),
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Select the best strategy based on browser capabilities
33
+ *
34
+ * Priority:
35
+ * 1. SharedWorker + Worker Pool (desktop Chrome, Firefox, Edge)
36
+ * 2. WebWorker Pool only (Android, Safari, or no SharedWorker)
37
+ * 3. Direct/Main thread (emergency fallback)
38
+ */
39
+ export function selectStrategy(capabilities?: BrowserCapabilities): StrategyType {
40
+ const caps = capabilities || detectCapabilities();
41
+
42
+ // Desktop with SharedWorker support (best performance)
43
+ if (caps.hasSharedWorker && caps.hasBroadcastChannel && !caps.isAndroid) {
44
+ return 'shared-worker';
45
+ }
46
+
47
+ // Android or browsers without SharedWorker (still use worker pool)
48
+ if (caps.hasWorker && caps.hasBroadcastChannel) {
49
+ return 'web-worker';
50
+ }
51
+
52
+ // Fallback to main thread (no workers or BroadcastChannel)
53
+ return 'direct';
54
+ }
55
+
56
+ /**
57
+ * Log detected capabilities for debugging
58
+ */
59
+ export function logCapabilities(capabilities: BrowserCapabilities) {
60
+ console.log('[SupabaseGateway] Browser capabilities:', {
61
+ SharedWorker: capabilities.hasSharedWorker ? 'yes' : 'no',
62
+ Worker: capabilities.hasWorker ? 'yes' : 'no',
63
+ BroadcastChannel: capabilities.hasBroadcastChannel ? 'yes' : 'no',
64
+ Platform: capabilities.isAndroid ? 'Android' : capabilities.isSafari ? 'Safari' : 'Desktop',
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Get a human-readable description of the selected strategy
70
+ */
71
+ export function getStrategyDescription(strategy: StrategyType): string {
72
+ switch (strategy) {
73
+ case 'shared-worker':
74
+ return 'SharedWorker + DB Worker Pool (optimal for desktop)';
75
+ case 'web-worker':
76
+ return 'WebSocket Worker + DB Worker Pool (optimized for mobile)';
77
+ case 'direct':
78
+ return 'Direct/Main thread (compatibility fallback)';
79
+ }
80
+ }
@@ -0,0 +1,15 @@
1
+ // Main exports for Supabase Gateway
2
+
3
+ export { SupabaseGateway } from './SupabaseGateway';
4
+ export { WorkerPool } from './WorkerPool';
5
+ export { getWorkerCommunication, WorkerCommunication } from './WorkerCommunication';
6
+ export {
7
+ detectCapabilities,
8
+ selectStrategy,
9
+ logCapabilities,
10
+ getStrategyDescription
11
+ } from './capabilities';
12
+ export * from './types';
13
+ export { SharedWorkerStrategy } from './strategies/SharedWorkerStrategy';
14
+ export { WebWorkerStrategy } from './strategies/WebWorkerStrategy';
15
+ export { DirectStrategy } from './strategies/DirectStrategy';
@@ -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
+ }