@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,278 @@
1
+ // Strategy for browsers without SharedWorker (Android, Safari)
2
+
3
+ import type {
4
+ BroadcastEvent,
5
+ ISupabaseStrategy,
6
+ SelectOptions,
7
+ SessionResponse,
8
+ WebSocketStatus,
9
+ } from '../types';
10
+ import { WorkerPool } from '../WorkerPool';
11
+ import { getWorkerCommunication } from '../WorkerCommunication';
12
+
13
+ /**
14
+ * WebWorkerStrategy: Uses dedicated WebSocket worker + DB Worker Pool
15
+ *
16
+ * Architecture:
17
+ * - Dedicated WebSocket worker handles WebSocket, auth, and realtime (no cross-tab)
18
+ * - Worker pool (3 workers) handles database operations (parallel execution)
19
+ * - BroadcastChannel connects WebSocket worker to worker pool
20
+ *
21
+ * Best for: Android, Safari, browsers without SharedWorker
22
+ */
23
+ export class WebWorkerStrategy implements ISupabaseStrategy {
24
+ private wsWorker?: Worker;
25
+ private workerPool?: WorkerPool;
26
+ private pending = new Map<
27
+ string,
28
+ {
29
+ resolve: (value: unknown) => void;
30
+ reject: (reason?: unknown) => void;
31
+ }
32
+ >();
33
+ private listeners = new Map<string, Set<(p: unknown) => void>>();
34
+ private comm = getWorkerCommunication();
35
+ private wsWorkerUrl: string | URL;
36
+ private dbWorkerUrl: string | URL;
37
+ private poolSize: number;
38
+
39
+ constructor(
40
+ wsWorkerUrl: string | URL,
41
+ dbWorkerUrl: string | URL,
42
+ poolSize = 3,
43
+ ) {
44
+ this.wsWorkerUrl = wsWorkerUrl;
45
+ this.dbWorkerUrl = dbWorkerUrl;
46
+ this.poolSize = poolSize;
47
+
48
+ if (typeof window === 'undefined') return;
49
+ if (!('Worker' in window)) {
50
+ throw new Error('Worker not supported');
51
+ }
52
+ }
53
+
54
+ async init(
55
+ url: string,
56
+ anonKey: string,
57
+ options?: Record<string, unknown>,
58
+ ): Promise<SessionResponse> {
59
+ console.log('[WebWorkerStrategy] Initializing...');
60
+
61
+ this.wsWorker = new Worker(this.wsWorkerUrl, {
62
+ type: 'module',
63
+ name: 'websocket-worker',
64
+ });
65
+ this.wsWorker.onmessage = (e) => this.onMessage(e.data);
66
+ this.wsWorker.onerror = (err) =>
67
+ console.error('[WebWorkerStrategy] WebSocket worker error:', err);
68
+
69
+ this.workerPool = new WorkerPool({
70
+ size: this.poolSize,
71
+ workerUrl: this.dbWorkerUrl,
72
+ });
73
+ await this.workerPool.init();
74
+
75
+ const wsWorkerInit = this.send<SessionResponse>('init', {
76
+ url,
77
+ anonKey,
78
+ options,
79
+ });
80
+
81
+ const poolInitPromises = Array.from({ length: this.poolSize }, () =>
82
+ this.workerPool?.send('init', { url, anonKey, options }),
83
+ );
84
+
85
+ await Promise.all([wsWorkerInit, ...poolInitPromises]);
86
+
87
+ console.log(
88
+ `[WebWorkerStrategy] Initialized (WebSocket worker + ${this.poolSize} DB workers)`,
89
+ );
90
+
91
+ return wsWorkerInit;
92
+ }
93
+
94
+ private onMessage(msg: unknown) {
95
+ const message = msg as
96
+ | (BroadcastEvent & {
97
+ id?: string;
98
+ ok?: boolean;
99
+ data?: unknown;
100
+ error?: string;
101
+ key?: string;
102
+ })
103
+ | null
104
+ | undefined;
105
+ if (message?.type === 'ready' || message?.type === 'auth') {
106
+ this.emit(message.type, message);
107
+ return;
108
+ }
109
+ if (message?.type === 'realtime' && message.key) {
110
+ this.emit(`realtime:${message.key}`, message.payload);
111
+ return;
112
+ }
113
+ if (message?.type === 'ws.message') {
114
+ this.emit('ws.message', message.data);
115
+ return;
116
+ }
117
+ if (message?.type === 'ws.status') {
118
+ this.emit('ws.status', message);
119
+ return;
120
+ }
121
+
122
+ const id = message?.id;
123
+ const ok = message?.ok;
124
+ const data = message?.data;
125
+ const error = message?.error;
126
+ if (id && this.pending.has(id)) {
127
+ const pending = this.pending.get(id);
128
+ if (!pending) return;
129
+ const { resolve, reject } = pending;
130
+ this.pending.delete(id);
131
+ ok ? resolve(data) : reject(new Error(error));
132
+ }
133
+ }
134
+
135
+ private send<T>(type: string, payload?: unknown): Promise<T> {
136
+ const id = crypto.randomUUID();
137
+ return new Promise<T>((resolve, reject) => {
138
+ if (!this.wsWorker)
139
+ return reject(new Error('WebSocket worker unavailable'));
140
+ this.pending.set(id, {
141
+ resolve: resolve as (value: unknown) => void,
142
+ reject,
143
+ });
144
+ this.wsWorker.postMessage({ id, type, payload });
145
+ });
146
+ }
147
+
148
+ on(event: string, callback: (payload: unknown) => void): () => void {
149
+ if (!this.listeners.has(event)) {
150
+ this.listeners.set(event, new Set());
151
+ }
152
+ this.listeners.get(event)?.add(callback);
153
+
154
+ return () => {
155
+ const set = this.listeners.get(event);
156
+ if (set) {
157
+ set.delete(callback);
158
+ if (set.size === 0) this.listeners.delete(event);
159
+ }
160
+ };
161
+ }
162
+
163
+ private emit(event: string, payload: unknown) {
164
+ this.listeners.get(event)?.forEach((cb) => cb(payload));
165
+ }
166
+
167
+ // Auth (via WebSocket worker)
168
+ getSession(): Promise<SessionResponse> {
169
+ return this.send<SessionResponse>('getSession');
170
+ }
171
+
172
+ signInWithPassword(
173
+ email: string,
174
+ password: string,
175
+ ): Promise<SessionResponse> {
176
+ return this.send<SessionResponse>('signInWithPassword', {
177
+ email,
178
+ password,
179
+ });
180
+ }
181
+
182
+ signOut(): Promise<void> {
183
+ return this.send('signOut');
184
+ }
185
+
186
+ // Database (via worker pool)
187
+ select(table: string, opts?: SelectOptions): Promise<unknown[]> {
188
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
189
+ return this.workerPool.send('from.select', { table, ...(opts ?? {}) });
190
+ }
191
+
192
+ insert(
193
+ table: string,
194
+ data: Record<string, unknown> | Record<string, unknown>[],
195
+ ): Promise<unknown[]> {
196
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
197
+ return this.workerPool.send('from.insert', { table, data });
198
+ }
199
+
200
+ update(
201
+ table: string,
202
+ data: Record<string, unknown>,
203
+ match: Record<string, unknown>,
204
+ ): Promise<unknown[]> {
205
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
206
+ return this.workerPool.send('from.update', { table, data, match });
207
+ }
208
+
209
+ upsert(
210
+ table: string,
211
+ data: Record<string, unknown> | Record<string, unknown>[],
212
+ ): Promise<unknown[]> {
213
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
214
+ return this.workerPool.send('from.upsert', { table, data });
215
+ }
216
+
217
+ delete(table: string, match: Record<string, unknown>): Promise<unknown[]> {
218
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
219
+ return this.workerPool.send('from.delete', { table, match });
220
+ }
221
+
222
+ rpc(fn: string, args?: Record<string, unknown>): Promise<unknown> {
223
+ if (!this.workerPool) throw new Error('Worker pool not initialized');
224
+ return this.workerPool.send('rpc', { fn, args });
225
+ }
226
+
227
+ // Realtime (via WebSocket worker)
228
+ subscribePostgres(
229
+ key: string,
230
+ params: Record<string, unknown>,
231
+ callback: (payload: unknown) => void,
232
+ ): () => void {
233
+ const off = this.on(`realtime:${key}`, callback);
234
+ this.send('realtime.subscribe', { key, params }).catch((e) => {
235
+ off();
236
+ console.error(e);
237
+ });
238
+ return () => {
239
+ off();
240
+ this.send('realtime.unsubscribe', { key }).catch(() => {
241
+ /* best-effort */
242
+ });
243
+ };
244
+ }
245
+
246
+ // WebSocket (via dedicated worker)
247
+ connectWebSocket(wsUrl?: string): Promise<WebSocketStatus> {
248
+ return this.send<WebSocketStatus>('ws.connect', { wsUrl });
249
+ }
250
+
251
+ disconnectWebSocket(): Promise<void> {
252
+ return this.send('ws.disconnect');
253
+ }
254
+
255
+ sendWebSocketMessage(data: unknown): Promise<void> {
256
+ return this.send('ws.send', { data });
257
+ }
258
+
259
+ getWebSocketStatus(): Promise<WebSocketStatus> {
260
+ return this.send<WebSocketStatus>('ws.status');
261
+ }
262
+
263
+ onWebSocketMessage(callback: (message: unknown) => void): () => void {
264
+ return this.on('ws.message', callback);
265
+ }
266
+
267
+ onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void {
268
+ return this.on('ws.status', callback as (payload: unknown) => void);
269
+ }
270
+
271
+ terminate() {
272
+ this.wsWorker?.terminate();
273
+ this.workerPool?.terminate();
274
+ this.comm.close();
275
+ this.listeners.clear();
276
+ this.pending.clear();
277
+ }
278
+ }
@@ -0,0 +1,112 @@
1
+ // Shared types for all gateway strategies
2
+
3
+ export interface SupabaseConfig {
4
+ url: string;
5
+ anonKey: string;
6
+ options?: Record<string, unknown>;
7
+ }
8
+
9
+ export interface Session {
10
+ access_token: string;
11
+ refresh_token: string;
12
+ user: Record<string, unknown>;
13
+ expires_at?: number;
14
+ }
15
+
16
+ export interface SessionResponse {
17
+ session: Session | null;
18
+ user?: Record<string, unknown>;
19
+ }
20
+
21
+ export interface WebSocketStatus {
22
+ status: 'connected' | 'disconnected' | 'connecting' | 'error';
23
+ readyState: number | null;
24
+ error?: string;
25
+ }
26
+
27
+ // Unified API that all strategies must implement
28
+ export interface ISupabaseStrategy {
29
+ // Lifecycle
30
+ init(url: string, anonKey: string, options?: Record<string, unknown>): Promise<SessionResponse>;
31
+
32
+ // Event handling
33
+ on(event: string, callback: (payload: unknown) => void): () => void;
34
+
35
+ // Auth
36
+ getSession(): Promise<SessionResponse>;
37
+ signInWithPassword(email: string, password: string): Promise<SessionResponse>;
38
+ signOut(): Promise<void>;
39
+
40
+ // Database
41
+ select(table: string, opts?: SelectOptions): Promise<unknown[]>;
42
+ insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<unknown[]>;
43
+ update(table: string, data: Record<string, unknown>, match: Record<string, unknown>): Promise<unknown[]>;
44
+ upsert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<unknown[]>;
45
+ delete(table: string, match: Record<string, unknown>): Promise<unknown[]>;
46
+ rpc(fn: string, args?: Record<string, unknown>): Promise<unknown>;
47
+
48
+ // Realtime
49
+ subscribePostgres(key: string, params: Record<string, unknown>, callback: (payload: unknown) => void): () => void;
50
+
51
+ // WebSocket
52
+ connectWebSocket(wsUrl?: string): Promise<WebSocketStatus>;
53
+ disconnectWebSocket(): Promise<void>;
54
+ sendWebSocketMessage(data: unknown): Promise<void>;
55
+ getWebSocketStatus(): Promise<WebSocketStatus>;
56
+ onWebSocketMessage(callback: (message: unknown) => void): () => void;
57
+ onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void;
58
+
59
+ // Cleanup
60
+ terminate(): void;
61
+ }
62
+
63
+ export interface SelectOptions {
64
+ columns?: string;
65
+ match?: Record<string, unknown>;
66
+ limit?: number;
67
+ }
68
+
69
+ // Worker communication messages
70
+ export interface WorkerMessage {
71
+ id: string;
72
+ type: string;
73
+ payload?: unknown;
74
+ }
75
+
76
+ export interface WorkerResponse {
77
+ id: string;
78
+ ok: boolean;
79
+ data?: unknown;
80
+ error?: string;
81
+ }
82
+
83
+ // BroadcastChannel event types
84
+ export interface BroadcastEvent {
85
+ type: 'auth' | 'ws.message' | 'ws.status' | 'realtime' | 'ready';
86
+ data?: unknown;
87
+ key?: string;
88
+ payload?: unknown;
89
+ }
90
+
91
+ // Strategy types
92
+ export type StrategyType = 'shared-worker' | 'web-worker' | 'direct';
93
+
94
+ // Browser capabilities
95
+ export interface BrowserCapabilities {
96
+ hasSharedWorker: boolean;
97
+ hasWorker: boolean;
98
+ hasBroadcastChannel: boolean;
99
+ isAndroid: boolean;
100
+ isSafari: boolean;
101
+ }
102
+
103
+ // Gateway configuration
104
+ export interface GatewayConfig {
105
+ workerUrls?: {
106
+ sharedWorker?: string | URL;
107
+ dbWorker?: string | URL;
108
+ webSocketWorker?: string | URL;
109
+ };
110
+ forceStrategy?: StrategyType;
111
+ poolSize?: number;
112
+ }
@@ -1,11 +1,16 @@
1
- import { wrap, proxy } from 'comlink';
2
- import type { Remote } from 'comlink';
3
- import type { ModManager, ModHandle, ModMeta, BaseModAPI } from '../types/modules';
4
- import type { KBVEGlobal } from '../../types';
1
+ import { wrap } from 'comlink';
2
+ import type {
3
+ ModManager,
4
+ ModHandle,
5
+ ModMeta,
6
+ BaseModAPI,
7
+ } from '../types/modules';
5
8
 
6
9
  let _modManager: ModManager | null = null;
7
10
 
8
- export async function getModManager(modWorkerResolver?: (url: string) => string): Promise<ModManager> {
11
+ export async function getModManager(
12
+ modWorkerResolver?: (url: string) => string,
13
+ ): Promise<ModManager> {
9
14
  if (_modManager) return _modManager;
10
15
 
11
16
  const registry: Record<string, ModHandle> = {};
@@ -15,7 +20,7 @@ export async function getModManager(modWorkerResolver?: (url: string) => string)
15
20
  const worker = new Worker(resolvedURL, { type: 'module' });
16
21
  const instance = wrap<BaseModAPI>(worker);
17
22
 
18
- const meta: ModMeta = await instance.getMeta?.() ?? {
23
+ const meta: ModMeta = (await instance.getMeta?.()) ?? {
19
24
  name: 'unknown',
20
25
  version: '0.0.1',
21
26
  };
@@ -51,7 +56,7 @@ export async function getModManager(modWorkerResolver?: (url: string) => string)
51
56
  }
52
57
 
53
58
  function list() {
54
- return Object.values(registry).map(m => m.meta);
59
+ return Object.values(registry).map((m) => m.meta);
55
60
  }
56
61
 
57
62
  async function reload(id: string): Promise<ModHandle> {
@@ -70,4 +75,4 @@ export async function getModManager(modWorkerResolver?: (url: string) => string)
70
75
  };
71
76
 
72
77
  return _modManager;
73
- }
78
+ }
@@ -8,7 +8,7 @@ import {
8
8
  BENTO_VARIANT_CLASS_MAP,
9
9
  } from '../../../types/bento';
10
10
 
11
- let emitToMain: ((msg: any) => void) | undefined;
11
+ let emitToMain: ((msg: unknown) => void) | undefined;
12
12
 
13
13
  function createVNodeFromTile(tile: BentoTile): VirtualNode {
14
14
  const variant = tile.variant ?? 'default';
@@ -1,30 +1,26 @@
1
- import { describe, it, expect, beforeAll } from 'vitest';
2
- import { droid } from '../../../droid';
3
- import type { Remote } from 'comlink';
4
- import type { ModHandle, SupabaseModAPI } from '../../../types/modules';
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mod } from './mod-supabase';
5
3
 
6
- console.log('[spec] Starting mod-supabase test file...');
7
-
8
-
9
- let mod: Remote<SupabaseModAPI>;
10
- let meta: { name: string; version: string };
11
4
  describe('mod-supabase', () => {
12
- it('should have meta info', async () => {
13
- await droid();
14
- if (!window.kbve?.mod) {
15
- throw new Error('Mod manager not available');
16
- }
5
+ it('should have meta info', () => {
6
+ const meta = mod.getMeta();
17
7
 
18
- console.log('[test] Loading Supabase mod...');
19
- const modURL = new URL('./mod-supabase.worker.ts', import.meta.url);
20
- console.log('Resolved worker URL:', modURL.href);
8
+ expect(meta.name).toBe('supabase');
9
+ expect(meta.version).toBe('1.0.0');
10
+ expect(meta.description).toBeDefined();
11
+ expect(meta.author).toBe('kbve');
12
+ });
21
13
 
22
- const handle = await window.kbve.mod.load(modURL.href);
23
- const mod: Remote<SupabaseModAPI> = handle.instance;
24
- const meta = handle.meta;
14
+ it('should accept init context', () => {
15
+ mod.init({ test: true });
16
+ // init stores context internally — no throw = success
17
+ });
25
18
 
26
- console.log('[test] Supabase mod loaded:', meta);
27
- expect(meta.name).toBe('supabase');
28
- expect(meta.version).toBeDefined();
29
- }, 30_000);
30
- });
19
+ it('should reject queries before configure', async () => {
20
+ await expect(mod.queryTestTable()).rejects.toThrow('Supabase not configured');
21
+ });
22
+
23
+ it('should reject inserts before configure', async () => {
24
+ await expect(mod.insertTest({ foo: 'bar' })).rejects.toThrow('Supabase not configured');
25
+ });
26
+ });
@@ -0,0 +1,68 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import type { ModInitContext } from '../../../types/modules';
3
+
4
+ let supabase: SupabaseClient | null = null;
5
+ let initialized = false;
6
+
7
+ const state = {
8
+ ctx: null as ModInitContext | null,
9
+ url: '',
10
+ key: '',
11
+ };
12
+
13
+ export const mod = {
14
+ getMeta: () => ({
15
+ name: 'supabase',
16
+ version: '1.0.0',
17
+ description: 'Supabase mod for async DB access',
18
+ author: 'kbve',
19
+ }),
20
+
21
+ init(ctx: ModInitContext) {
22
+ state.ctx = ctx;
23
+ },
24
+
25
+ async loadSupabaseClient() {
26
+ try {
27
+ const supabaseModule = await import(
28
+ 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm'
29
+ );
30
+ console.log('[mod-supabase] Supabase module loaded');
31
+ return supabaseModule.createClient;
32
+ } catch (err) {
33
+ console.error('[mod-supabase] Failed to load Supabase:', err);
34
+ throw err;
35
+ }
36
+ },
37
+
38
+ async configure(url: string, key: string) {
39
+ if (!initialized) {
40
+ const createClient = await this.loadSupabaseClient();
41
+ supabase = createClient(url, key);
42
+ state.url = url;
43
+ state.key = key;
44
+ initialized = true;
45
+ }
46
+ },
47
+
48
+ async queryTestTable() {
49
+ if (!supabase)
50
+ throw new Error(
51
+ 'Supabase not configured. Call `configure()` first.',
52
+ );
53
+ const { data, error } = await supabase.from('test').select('*');
54
+ return { data, error };
55
+ },
56
+
57
+ async insertTest(payload: Record<string, unknown>) {
58
+ if (!supabase)
59
+ throw new Error(
60
+ 'Supabase not configured. Call `configure()` first.',
61
+ );
62
+ const { data, error } = await supabase
63
+ .from('test')
64
+ .insert(payload)
65
+ .select();
66
+ return { data, error };
67
+ },
68
+ };
@@ -1,59 +1,4 @@
1
1
  import { expose } from 'comlink';
2
- import type { SupabaseClient } from '@supabase/supabase-js';
3
-
4
- let supabase: SupabaseClient | null = null;
5
- let initialized = false;
6
-
7
- const state = {
8
- ctx: null as any,
9
- url: '',
10
- key: '',
11
- };
12
-
13
- const mod = {
14
- getMeta: () => ({
15
- name: 'supabase',
16
- version: '1.0.0',
17
- description: 'Supabase mod for async DB access',
18
- author: 'kbve',
19
- }),
20
-
21
- init(ctx: any) {
22
- state.ctx = ctx;
23
- },
24
-
25
- async loadSupabaseClient() {
26
- try {
27
- const supabaseModule = await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm');
28
- console.log('[mod-supabase] Supabase module loaded');
29
- return supabaseModule.createClient;
30
- } catch (err) {
31
- console.error('[mod-supabase] Failed to load Supabase:', err);
32
- throw err;
33
- }
34
- },
35
-
36
- async configure(url: string, key: string) {
37
- if (!initialized) {
38
- const createClient = await this.loadSupabaseClient();
39
- supabase = createClient(url, key);
40
- state.url = url;
41
- state.key = key;
42
- initialized = true;
43
- }
44
- },
45
-
46
- async queryTestTable() {
47
- if (!supabase) throw new Error('Supabase not configured. Call `configure()` first.');
48
- const { data, error } = await supabase.from('test').select('*');
49
- return { data, error };
50
- },
51
-
52
- async insertTest(payload: Record<string, any>) {
53
- if (!supabase) throw new Error('Supabase not configured. Call `configure()` first.');
54
- const { data, error } = await supabase.from('test').insert(payload).select();
55
- return { data, error };
56
- }
57
- };
2
+ import { mod } from './mod-supabase';
58
3
 
59
4
  expose(mod);
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { $auth, setAuth, resetAuth } from './auth';
3
+
4
+ beforeEach(() => {
5
+ resetAuth();
6
+ // resetAuth sets tone to 'anon', restore to default 'loading' for clean tests
7
+ $auth.set({
8
+ tone: 'loading',
9
+ name: '',
10
+ avatar: undefined,
11
+ id: '',
12
+ error: undefined,
13
+ });
14
+ });
15
+
16
+ describe('Auth state', () => {
17
+ it('starts with loading tone', () => {
18
+ expect($auth.get().tone).toBe('loading');
19
+ });
20
+
21
+ it('setAuth merges partial state', () => {
22
+ setAuth({ tone: 'auth', name: 'Alice', id: 'u1' });
23
+
24
+ const state = $auth.get();
25
+ expect(state.tone).toBe('auth');
26
+ expect(state.name).toBe('Alice');
27
+ expect(state.id).toBe('u1');
28
+ expect(state.avatar).toBeUndefined();
29
+ });
30
+
31
+ it('setAuth preserves existing fields', () => {
32
+ setAuth({ tone: 'auth', name: 'Alice', id: 'u1' });
33
+ setAuth({ avatar: 'https://example.com/avatar.png' });
34
+
35
+ const state = $auth.get();
36
+ expect(state.name).toBe('Alice');
37
+ expect(state.avatar).toBe('https://example.com/avatar.png');
38
+ });
39
+
40
+ it('setAuth can set error state', () => {
41
+ setAuth({ tone: 'error', error: 'Token expired' });
42
+
43
+ expect($auth.get().tone).toBe('error');
44
+ expect($auth.get().error).toBe('Token expired');
45
+ });
46
+
47
+ it('resetAuth clears to anon defaults', () => {
48
+ setAuth({ tone: 'auth', name: 'Alice', id: 'u1', avatar: 'img.png' });
49
+ resetAuth();
50
+
51
+ const state = $auth.get();
52
+ expect(state.tone).toBe('anon');
53
+ expect(state.name).toBe('');
54
+ expect(state.id).toBe('');
55
+ expect(state.avatar).toBeUndefined();
56
+ expect(state.error).toBeUndefined();
57
+ });
58
+ });