@kbve/droid 0.0.5 → 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.
- package/README.md +3 -1
- package/comlink.js +1 -7
- package/droid.mjs +191 -61
- package/index.d.ts +29 -0
- package/index.js +9054 -2763
- package/lib/gateway/SupabaseGateway.d.ts +38 -0
- package/lib/gateway/SupabaseGateway.d.ts.map +1 -0
- package/lib/gateway/WorkerCommunication.d.ts +39 -0
- package/lib/gateway/WorkerCommunication.d.ts.map +1 -0
- package/lib/gateway/WorkerPool.d.ts +55 -0
- package/lib/gateway/WorkerPool.d.ts.map +1 -0
- package/lib/gateway/capabilities.d.ts +23 -0
- package/lib/gateway/capabilities.d.ts.map +1 -0
- package/lib/gateway/index.d.ts +9 -0
- package/lib/gateway/index.d.ts.map +1 -0
- package/lib/gateway/strategies/DirectStrategy.d.ts +52 -0
- package/lib/gateway/strategies/DirectStrategy.d.ts.map +1 -0
- package/lib/gateway/strategies/SharedWorkerStrategy.d.ts +46 -0
- package/lib/gateway/strategies/SharedWorkerStrategy.d.ts.map +1 -0
- package/lib/gateway/strategies/WebWorkerStrategy.d.ts +45 -0
- package/lib/gateway/strategies/WebWorkerStrategy.d.ts.map +1 -0
- package/lib/gateway/types.d.ts +81 -0
- package/lib/gateway/types.d.ts.map +1 -0
- package/lib/workers/canvas-ui-renderer.d.ts +25 -0
- package/lib/workers/canvas-ui-renderer.d.ts.map +1 -0
- package/lib/workers/canvas-worker.d.ts +14 -0
- package/lib/workers/canvas-worker.d.ts.map +1 -0
- package/lib/workers/canvas-worker.js +291 -145
- package/lib/workers/data.d.ts +40 -0
- package/lib/workers/data.d.ts.map +1 -0
- package/lib/workers/db-worker.d.ts +69 -0
- package/lib/workers/db-worker.d.ts.map +1 -0
- package/lib/workers/db-worker.js +2680 -2147
- package/lib/workers/events.d.ts +11 -0
- package/lib/workers/events.d.ts.map +1 -0
- package/lib/workers/flexbuilder.d.ts +4 -0
- package/lib/workers/flexbuilder.d.ts.map +1 -0
- package/lib/workers/init.d.ts +11 -0
- package/lib/workers/init.d.ts.map +1 -0
- package/lib/workers/main.d.ts +107 -0
- package/lib/workers/main.d.ts.map +1 -0
- package/lib/workers/supabase-db-worker.d.ts +2 -0
- package/lib/workers/supabase-db-worker.d.ts.map +1 -0
- package/lib/workers/supabase-db-worker.js +9337 -0
- package/lib/workers/supabase-shared-worker.d.ts +2 -0
- package/lib/workers/supabase-shared-worker.d.ts.map +1 -0
- package/lib/workers/supabase-shared-worker.js +12635 -0
- package/lib/workers/tools.d.ts +4 -0
- package/lib/workers/tools.d.ts.map +1 -0
- package/lib/workers/validate.d.ts +15 -0
- package/lib/workers/validate.d.ts.map +1 -0
- package/lib/workers/ws-worker.d.ts +11 -0
- package/lib/workers/ws-worker.d.ts.map +1 -0
- package/lib/workers/ws-worker.js +174 -115
- package/main.js +4875 -0
- package/package.json +17 -6
- package/reference.js +688 -228
- package/src/index.ts +38 -13
- package/src/lib/droid.spec.ts +14 -19
- package/src/lib/droid.ts +48 -4
- package/src/lib/gateway/SupabaseGateway.ts +183 -0
- package/src/lib/gateway/WorkerCommunication.ts +132 -0
- package/src/lib/gateway/WorkerPool.ts +304 -0
- package/src/lib/gateway/capabilities.ts +80 -0
- package/src/lib/gateway/index.ts +15 -0
- package/src/lib/gateway/strategies/DirectStrategy.ts +443 -0
- package/src/lib/gateway/strategies/SharedWorkerStrategy.ts +278 -0
- package/src/lib/gateway/strategies/WebWorkerStrategy.ts +278 -0
- package/src/lib/gateway/types.ts +112 -0
- package/src/lib/mod/mod-manager.ts +13 -8
- package/src/lib/mod/module/bento/mod-bento.worker.ts +1 -1
- package/src/lib/mod/module/supabase/mod-supabase.spec.ts +21 -25
- package/src/lib/mod/module/supabase/mod-supabase.ts +68 -0
- package/src/lib/mod/module/supabase/mod-supabase.worker.ts +1 -56
- package/src/lib/state/auth.spec.ts +58 -0
- package/src/lib/state/auth.ts +30 -0
- package/src/lib/state/index.ts +34 -0
- package/src/lib/state/overlay-manager.ts +125 -0
- package/src/lib/state/router.ts +24 -0
- package/src/lib/state/theme-sync.ts +75 -0
- package/src/lib/state/toasts.spec.ts +106 -0
- package/src/lib/state/toasts.ts +34 -0
- package/src/lib/state/ui.spec.ts +148 -0
- package/src/lib/state/ui.ts +45 -0
- package/src/lib/state/welcome-toast.ts +76 -0
- package/src/lib/types/bento.spec.ts +116 -0
- package/src/lib/types/bento.ts +94 -55
- package/src/lib/types/event-types.spec.ts +93 -0
- package/src/lib/types/event-types.ts +39 -3
- package/src/lib/types/modules.ts +12 -5
- package/src/lib/types/ui-event-types.ts +66 -0
- package/src/lib/workers/canvas-ui-renderer.ts +266 -0
- package/src/lib/workers/canvas-worker.ts +94 -9
- package/src/lib/workers/data.ts +59 -36
- package/src/lib/workers/db-worker.ts +79 -32
- package/src/lib/workers/events.spec.ts +208 -0
- package/src/lib/workers/events.ts +27 -14
- package/src/lib/workers/flexbuilder.ts +2 -2
- package/src/lib/workers/init.ts +25 -17
- package/src/lib/workers/main.ts +509 -272
- package/src/lib/workers/supabase-db-worker.ts +221 -0
- package/src/lib/workers/supabase-shared-worker.ts +572 -0
- package/src/lib/workers/validate.ts +64 -0
- package/src/lib/workers/ws-worker.ts +156 -25
- package/src/setup-vitest.ts +4 -2
- package/src/types.ts +30 -0
- package/src/vite-env.d.ts +1 -0
- package/workers/main.js +10 -1089
- 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
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
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(
|
|
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:
|
|
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
|
|
2
|
-
import {
|
|
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',
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
it('should accept init context', () => {
|
|
15
|
+
mod.init({ test: true });
|
|
16
|
+
// init stores context internally — no throw = success
|
|
17
|
+
});
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
expect(
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
+
});
|