@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.
- package/droid.mjs +2312 -74
- package/index.d.ts +32 -0
- package/lib/agents/api-types.d.ts +121 -0
- package/lib/agents/api-types.d.ts.map +1 -0
- package/lib/agents/auth/init.d.ts +8 -0
- package/lib/agents/auth/init.d.ts.map +1 -0
- package/lib/agents/cache.d.ts +9 -0
- package/lib/agents/cache.d.ts.map +1 -0
- package/lib/agents/client/callGuildVault.d.ts +3 -0
- package/lib/agents/client/callGuildVault.d.ts.map +1 -0
- package/lib/agents/client/callJson.d.ts +3 -0
- package/lib/agents/client/callJson.d.ts.map +1 -0
- package/lib/agents/config.d.ts +36 -0
- package/lib/agents/config.d.ts.map +1 -0
- package/lib/agents/constants.d.ts +11 -0
- package/lib/agents/constants.d.ts.map +1 -0
- package/lib/agents/createAgents.d.ts +4 -0
- package/lib/agents/createAgents.d.ts.map +1 -0
- package/lib/agents/ctx.d.ts +46 -0
- package/lib/agents/ctx.d.ts.map +1 -0
- package/lib/agents/discord/bot.d.ts +21 -0
- package/lib/agents/discord/bot.d.ts.map +1 -0
- package/lib/agents/discord/guilds.d.ts +8 -0
- package/lib/agents/discord/guilds.d.ts.map +1 -0
- package/lib/agents/formDrafts.d.ts +5 -0
- package/lib/agents/formDrafts.d.ts.map +1 -0
- package/lib/agents/generated/agents-schema.d.ts +73 -0
- package/lib/agents/generated/agents-schema.d.ts.map +1 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts +71 -0
- package/lib/agents/generated/discordsh-agents-schema.d.ts.map +1 -0
- package/lib/agents/github/backfill.d.ts +23 -0
- package/lib/agents/github/backfill.d.ts.map +1 -0
- package/lib/agents/github/events.d.ts +10 -0
- package/lib/agents/github/events.d.ts.map +1 -0
- package/lib/agents/github/pat.d.ts +13 -0
- package/lib/agents/github/pat.d.ts.map +1 -0
- package/lib/agents/github/repoConfig.d.ts +25 -0
- package/lib/agents/github/repoConfig.d.ts.map +1 -0
- package/lib/agents/github/tokens.d.ts +22 -0
- package/lib/agents/github/tokens.d.ts.map +1 -0
- package/lib/agents/github/webhook.d.ts +37 -0
- package/lib/agents/github/webhook.d.ts.map +1 -0
- package/lib/agents/index.d.ts +8 -0
- package/lib/agents/index.d.ts.map +1 -0
- package/lib/agents/state/atoms.d.ts +122 -0
- package/lib/agents/state/atoms.d.ts.map +1 -0
- package/lib/agents/types.d.ts +65 -0
- package/lib/agents/types.d.ts.map +1 -0
- 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 +5 -302
- 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 +13 -3636
- 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 +105 -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 +23 -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 +25 -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 +10 -0
- package/lib/workers/ws-worker.d.ts.map +1 -0
- package/lib/workers/ws-worker.js +5 -273
- package/main.js +5711 -0
- package/package.json +18 -6
- package/src/index.ts +41 -13
- package/src/lib/agents/api-types.ts +170 -0
- package/src/lib/agents/auth/init.ts +200 -0
- package/src/lib/agents/cache.ts +127 -0
- package/src/lib/agents/client/callGuildVault.ts +71 -0
- package/src/lib/agents/client/callJson.ts +64 -0
- package/src/lib/agents/config.ts +83 -0
- package/src/lib/agents/constants.ts +11 -0
- package/src/lib/agents/createAgents.ts +62 -0
- package/src/lib/agents/ctx.ts +63 -0
- package/src/lib/agents/discord/bot.ts +169 -0
- package/src/lib/agents/discord/guilds.ts +106 -0
- package/src/lib/agents/formDrafts.ts +55 -0
- package/src/lib/agents/generated/agents-schema.ts +155 -0
- package/src/lib/agents/generated/discordsh-agents-schema.ts +96 -0
- package/src/lib/agents/github/backfill.ts +126 -0
- package/src/lib/agents/github/events.ts +131 -0
- package/src/lib/agents/github/pat.ts +150 -0
- package/src/lib/agents/github/repoConfig.ts +240 -0
- package/src/lib/agents/github/tokens.ts +222 -0
- package/src/lib/agents/github/webhook.ts +349 -0
- package/src/lib/agents/index.ts +35 -0
- package/src/lib/agents/state/atoms.ts +122 -0
- package/src/lib/agents/types.ts +83 -0
- package/src/lib/api.ts +71 -0
- package/src/lib/droid.spec.ts +14 -19
- package/src/lib/droid.ts +48 -4
- package/src/lib/gateway/SupabaseGateway.ts +236 -0
- package/src/lib/gateway/WorkerCommunication.ts +132 -0
- package/src/lib/gateway/WorkerPool.ts +304 -0
- package/src/lib/gateway/capabilities.spec.ts +144 -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 +139 -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 +73 -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 +148 -0
- package/src/lib/state/bento.spec.ts +76 -0
- package/src/lib/state/bento.ts +51 -0
- package/src/lib/state/index.ts +98 -0
- package/src/lib/state/overlay-manager.ts +125 -0
- package/src/lib/state/profile-sync.ts +214 -0
- package/src/lib/state/profile.ts +155 -0
- package/src/lib/state/router.ts +24 -0
- package/src/lib/state/staff.ts +128 -0
- package/src/lib/state/sync-bus.ts +109 -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.spec.ts +101 -0
- package/src/lib/state/welcome-toast.ts +76 -0
- package/src/lib/sw-recovery.spec.ts +165 -0
- package/src/lib/sw-recovery.ts +141 -0
- package/src/lib/types/bento.spec.ts +116 -0
- package/src/lib/types/bento.ts +94 -55
- package/src/lib/types/event-schemas.spec.ts +120 -0
- package/src/lib/types/event-types.spec.ts +96 -0
- package/src/lib/types/event-types.ts +113 -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 +64 -37
- package/src/lib/workers/db-worker.ts +105 -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 +506 -327
- package/src/lib/workers/supabase-db-worker.ts +243 -0
- package/src/lib/workers/supabase-shared-worker.ts +607 -0
- package/src/lib/workers/validate.ts +64 -0
- package/src/lib/workers/ws-worker.spec.ts +41 -0
- package/src/lib/workers/ws-worker.ts +255 -31
- 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 +2 -1125
- package/comlink.js +0 -253
- package/index.js +0 -2972
- package/reference.js +0 -259
- package/src/types.d.ts +0 -31
|
@@ -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
|
+
}
|
|
@@ -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,139 @@
|
|
|
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(
|
|
31
|
+
url: string,
|
|
32
|
+
anonKey: string,
|
|
33
|
+
options?: Record<string, unknown>,
|
|
34
|
+
): Promise<SessionResponse>;
|
|
35
|
+
|
|
36
|
+
// Event handling
|
|
37
|
+
on(event: string, callback: (payload: unknown) => void): () => void;
|
|
38
|
+
|
|
39
|
+
// Auth
|
|
40
|
+
getSession(): Promise<SessionResponse>;
|
|
41
|
+
signInWithPassword(
|
|
42
|
+
email: string,
|
|
43
|
+
password: string,
|
|
44
|
+
): Promise<SessionResponse>;
|
|
45
|
+
signOut(): Promise<void>;
|
|
46
|
+
|
|
47
|
+
// Database
|
|
48
|
+
select(table: string, opts?: SelectOptions): Promise<unknown[]>;
|
|
49
|
+
insert(
|
|
50
|
+
table: string,
|
|
51
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
52
|
+
): Promise<unknown[]>;
|
|
53
|
+
update(
|
|
54
|
+
table: string,
|
|
55
|
+
data: Record<string, unknown>,
|
|
56
|
+
match: Record<string, unknown>,
|
|
57
|
+
): Promise<unknown[]>;
|
|
58
|
+
upsert(
|
|
59
|
+
table: string,
|
|
60
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
61
|
+
): Promise<unknown[]>;
|
|
62
|
+
delete(table: string, match: Record<string, unknown>): Promise<unknown[]>;
|
|
63
|
+
rpc(fn: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
64
|
+
|
|
65
|
+
// Realtime
|
|
66
|
+
subscribePostgres(
|
|
67
|
+
key: string,
|
|
68
|
+
params: Record<string, unknown>,
|
|
69
|
+
callback: (payload: unknown) => void,
|
|
70
|
+
): () => void;
|
|
71
|
+
|
|
72
|
+
// WebSocket
|
|
73
|
+
connectWebSocket(wsUrl?: string): Promise<WebSocketStatus>;
|
|
74
|
+
disconnectWebSocket(): Promise<void>;
|
|
75
|
+
sendWebSocketMessage(data: unknown): Promise<void>;
|
|
76
|
+
getWebSocketStatus(): Promise<WebSocketStatus>;
|
|
77
|
+
onWebSocketMessage(callback: (message: unknown) => void): () => void;
|
|
78
|
+
onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void;
|
|
79
|
+
|
|
80
|
+
// Cleanup
|
|
81
|
+
terminate(): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SelectOptions {
|
|
85
|
+
columns?: string;
|
|
86
|
+
match?: Record<string, unknown>;
|
|
87
|
+
limit?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Worker communication messages
|
|
91
|
+
export interface WorkerMessage {
|
|
92
|
+
id: string;
|
|
93
|
+
type: string;
|
|
94
|
+
payload?: unknown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface WorkerResponse {
|
|
98
|
+
id: string;
|
|
99
|
+
ok: boolean;
|
|
100
|
+
data?: unknown;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// BroadcastChannel event types
|
|
105
|
+
export interface BroadcastEvent {
|
|
106
|
+
type:
|
|
107
|
+
| 'auth'
|
|
108
|
+
| 'ws.message'
|
|
109
|
+
| 'ws.status'
|
|
110
|
+
| 'realtime'
|
|
111
|
+
| 'ready'
|
|
112
|
+
| 'worker-error';
|
|
113
|
+
data?: unknown;
|
|
114
|
+
key?: string;
|
|
115
|
+
payload?: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Strategy types
|
|
119
|
+
export type StrategyType = 'shared-worker' | 'web-worker' | 'direct';
|
|
120
|
+
|
|
121
|
+
// Browser capabilities
|
|
122
|
+
export interface BrowserCapabilities {
|
|
123
|
+
hasSharedWorker: boolean;
|
|
124
|
+
hasWorker: boolean;
|
|
125
|
+
hasBroadcastChannel: boolean;
|
|
126
|
+
isAndroid: boolean;
|
|
127
|
+
isSafari: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Gateway configuration
|
|
131
|
+
export interface GatewayConfig {
|
|
132
|
+
workerUrls?: {
|
|
133
|
+
sharedWorker?: string | URL;
|
|
134
|
+
dbWorker?: string | URL;
|
|
135
|
+
webSocketWorker?: string | URL;
|
|
136
|
+
};
|
|
137
|
+
forceStrategy?: StrategyType;
|
|
138
|
+
poolSize?: number;
|
|
139
|
+
}
|