@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
package/src/lib/droid.ts
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
|
-
export
|
|
1
|
+
export {
|
|
2
|
+
downscale,
|
|
3
|
+
upscale,
|
|
4
|
+
getScaleLevel,
|
|
5
|
+
type DroidScaleLevel,
|
|
6
|
+
} from './workers/main.js';
|
|
7
|
+
|
|
8
|
+
export async function droid(opts?: {
|
|
9
|
+
workerURLs?: Record<string, string>;
|
|
10
|
+
workerRefs?: {
|
|
11
|
+
canvasWorker?: Worker;
|
|
12
|
+
dbWorker?: SharedWorker;
|
|
13
|
+
wsWorker?: SharedWorker;
|
|
14
|
+
};
|
|
15
|
+
i18nPath?: string;
|
|
16
|
+
dataPath?: string;
|
|
17
|
+
/** Maximum time (ms) to wait for worker initialization before rejecting. Default: no timeout. */
|
|
18
|
+
initTimeout?: number;
|
|
19
|
+
}): Promise<{ initialized: boolean }> {
|
|
2
20
|
console.log('[DROID]: droid<T>');
|
|
21
|
+
|
|
3
22
|
const { main } = await import('./workers/main.js');
|
|
4
|
-
|
|
5
|
-
|
|
23
|
+
|
|
24
|
+
const initPromise = main({
|
|
25
|
+
workerURLs: opts?.workerURLs,
|
|
26
|
+
workerRefs: opts?.workerRefs,
|
|
27
|
+
i18nPath: opts?.i18nPath,
|
|
28
|
+
dataPath: opts?.dataPath,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (opts?.initTimeout && opts.initTimeout > 0) {
|
|
32
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
33
|
+
setTimeout(
|
|
34
|
+
() =>
|
|
35
|
+
reject(
|
|
36
|
+
new Error(
|
|
37
|
+
`[DROID] Worker init timed out after ${opts.initTimeout}ms`,
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
opts.initTimeout,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
await Promise.race([initPromise, timeout]);
|
|
44
|
+
} else {
|
|
45
|
+
await initPromise;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('[DROID]: droid<T> => await WorkerRefs + URLs');
|
|
49
|
+
|
|
6
50
|
return { initialized: true };
|
|
7
|
-
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Unified gateway for Supabase operations with automatic strategy selection
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ISupabaseStrategy,
|
|
5
|
+
SelectOptions,
|
|
6
|
+
SessionResponse,
|
|
7
|
+
WebSocketStatus,
|
|
8
|
+
StrategyType,
|
|
9
|
+
GatewayConfig,
|
|
10
|
+
} from './types';
|
|
11
|
+
import {
|
|
12
|
+
detectCapabilities,
|
|
13
|
+
selectStrategy,
|
|
14
|
+
logCapabilities,
|
|
15
|
+
getStrategyDescription,
|
|
16
|
+
} from './capabilities';
|
|
17
|
+
import { SharedWorkerStrategy } from './strategies/SharedWorkerStrategy';
|
|
18
|
+
import { WebWorkerStrategy } from './strategies/WebWorkerStrategy';
|
|
19
|
+
import { DirectStrategy } from './strategies/DirectStrategy';
|
|
20
|
+
import { getWorkerCommunication } from './WorkerCommunication';
|
|
21
|
+
import { DroidEvents } from '../workers/events';
|
|
22
|
+
|
|
23
|
+
// Vite ?worker&url imports — bundles workers as JS and returns their URL
|
|
24
|
+
import defaultSharedWorkerUrl from '../workers/supabase-shared-worker?worker&url';
|
|
25
|
+
import defaultDbWorkerUrl from '../workers/supabase-db-worker?worker&url';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SupabaseGateway: Unified interface for all Supabase operations
|
|
29
|
+
*
|
|
30
|
+
* Automatically selects the best strategy based on browser capabilities:
|
|
31
|
+
* 1. SharedWorker + DB Worker Pool (desktop: Chrome, Firefox, Edge)
|
|
32
|
+
* 2. WebSocket Worker + DB Worker Pool (Android, Safari)
|
|
33
|
+
* 3. Direct/Main thread (fallback)
|
|
34
|
+
*
|
|
35
|
+
* All strategies implement the same interface, so application code is unchanged.
|
|
36
|
+
*/
|
|
37
|
+
export class SupabaseGateway implements ISupabaseStrategy {
|
|
38
|
+
private strategy: ISupabaseStrategy;
|
|
39
|
+
private strategyType: StrategyType;
|
|
40
|
+
|
|
41
|
+
constructor(config: GatewayConfig = {}) {
|
|
42
|
+
const capabilities = detectCapabilities();
|
|
43
|
+
logCapabilities(capabilities);
|
|
44
|
+
|
|
45
|
+
this.strategyType =
|
|
46
|
+
config.forceStrategy || selectStrategy(capabilities);
|
|
47
|
+
console.log(
|
|
48
|
+
`[SupabaseGateway] Selected strategy: ${getStrategyDescription(this.strategyType)}`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const poolSize = config.poolSize ?? 3;
|
|
52
|
+
const workerUrls = config.workerUrls ?? {};
|
|
53
|
+
|
|
54
|
+
switch (this.strategyType) {
|
|
55
|
+
case 'shared-worker': {
|
|
56
|
+
const sharedUrl =
|
|
57
|
+
workerUrls.sharedWorker ?? defaultSharedWorkerUrl;
|
|
58
|
+
const dbUrl = workerUrls.dbWorker ?? defaultDbWorkerUrl;
|
|
59
|
+
this.strategy = new SharedWorkerStrategy(
|
|
60
|
+
sharedUrl,
|
|
61
|
+
dbUrl,
|
|
62
|
+
poolSize,
|
|
63
|
+
);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case 'web-worker': {
|
|
67
|
+
const wsUrl =
|
|
68
|
+
workerUrls.webSocketWorker ?? defaultSharedWorkerUrl;
|
|
69
|
+
const dbUrl = workerUrls.dbWorker ?? defaultDbWorkerUrl;
|
|
70
|
+
this.strategy = new WebWorkerStrategy(wsUrl, dbUrl, poolSize);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'direct':
|
|
74
|
+
this.strategy = new DirectStrategy();
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Forward worker-error events from BroadcastChannel to DroidEvents
|
|
79
|
+
if (typeof window !== 'undefined') {
|
|
80
|
+
const comm = getWorkerCommunication();
|
|
81
|
+
comm.on('worker-error', (payload) => {
|
|
82
|
+
DroidEvents.emit(
|
|
83
|
+
'worker-error',
|
|
84
|
+
payload as {
|
|
85
|
+
timestamp: number;
|
|
86
|
+
worker: 'shared' | 'db' | 'ws';
|
|
87
|
+
operation: string;
|
|
88
|
+
message: string;
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getStrategyType(): StrategyType {
|
|
96
|
+
return this.strategyType;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getStrategyDescription(): string {
|
|
100
|
+
return getStrategyDescription(this.strategyType);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Delegate all methods to the selected strategy
|
|
104
|
+
|
|
105
|
+
async init(
|
|
106
|
+
url: string,
|
|
107
|
+
anonKey: string,
|
|
108
|
+
options?: Record<string, unknown>,
|
|
109
|
+
): Promise<SessionResponse> {
|
|
110
|
+
if (this.strategyType === 'direct') {
|
|
111
|
+
return this.strategy.init(url, anonKey, options);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const WORKER_INIT_FALLBACK_MS = 3500;
|
|
115
|
+
const primary = this.strategy.init(url, anonKey, options);
|
|
116
|
+
|
|
117
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
118
|
+
const fallback = new Promise<never>((_, reject) => {
|
|
119
|
+
timer = setTimeout(
|
|
120
|
+
() => reject(new Error('worker init stalled')),
|
|
121
|
+
WORKER_INIT_FALLBACK_MS,
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = await Promise.race([primary, fallback]);
|
|
127
|
+
if (timer) clearTimeout(timer);
|
|
128
|
+
return result;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (timer) clearTimeout(timer);
|
|
131
|
+
const previousType = this.strategyType;
|
|
132
|
+
console.warn(
|
|
133
|
+
`[SupabaseGateway] ${previousType} init did not respond in ${WORKER_INIT_FALLBACK_MS}ms — switching to direct strategy.`,
|
|
134
|
+
err,
|
|
135
|
+
);
|
|
136
|
+
this.strategy = new DirectStrategy();
|
|
137
|
+
this.strategyType = 'direct';
|
|
138
|
+
DroidEvents.emit('gateway-strategy-fallback', {
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
from: previousType,
|
|
141
|
+
to: 'direct',
|
|
142
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
143
|
+
});
|
|
144
|
+
return this.strategy.init(url, anonKey, options);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
on(event: string, callback: (payload: unknown) => void): () => void {
|
|
149
|
+
return this.strategy.on(event, callback);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getSession(): Promise<SessionResponse> {
|
|
153
|
+
return this.strategy.getSession();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
signInWithPassword(
|
|
157
|
+
email: string,
|
|
158
|
+
password: string,
|
|
159
|
+
): Promise<SessionResponse> {
|
|
160
|
+
return this.strategy.signInWithPassword(email, password);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
signOut(): Promise<void> {
|
|
164
|
+
return this.strategy.signOut();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
select(table: string, opts?: SelectOptions): Promise<unknown[]> {
|
|
168
|
+
return this.strategy.select(table, opts);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
insert(
|
|
172
|
+
table: string,
|
|
173
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
174
|
+
): Promise<unknown[]> {
|
|
175
|
+
return this.strategy.insert(table, data);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
update(
|
|
179
|
+
table: string,
|
|
180
|
+
data: Record<string, unknown>,
|
|
181
|
+
match: Record<string, unknown>,
|
|
182
|
+
): Promise<unknown[]> {
|
|
183
|
+
return this.strategy.update(table, data, match);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
upsert(
|
|
187
|
+
table: string,
|
|
188
|
+
data: Record<string, unknown> | Record<string, unknown>[],
|
|
189
|
+
): Promise<unknown[]> {
|
|
190
|
+
return this.strategy.upsert(table, data);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
delete(table: string, match: Record<string, unknown>): Promise<unknown[]> {
|
|
194
|
+
return this.strategy.delete(table, match);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rpc(fn: string, args?: Record<string, unknown>): Promise<unknown> {
|
|
198
|
+
return this.strategy.rpc(fn, args);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
subscribePostgres(
|
|
202
|
+
key: string,
|
|
203
|
+
params: Record<string, unknown>,
|
|
204
|
+
callback: (payload: unknown) => void,
|
|
205
|
+
): () => void {
|
|
206
|
+
return this.strategy.subscribePostgres(key, params, callback);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
connectWebSocket(wsUrl?: string): Promise<WebSocketStatus> {
|
|
210
|
+
return this.strategy.connectWebSocket(wsUrl);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
disconnectWebSocket(): Promise<void> {
|
|
214
|
+
return this.strategy.disconnectWebSocket();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
sendWebSocketMessage(data: unknown): Promise<void> {
|
|
218
|
+
return this.strategy.sendWebSocketMessage(data);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getWebSocketStatus(): Promise<WebSocketStatus> {
|
|
222
|
+
return this.strategy.getWebSocketStatus();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
onWebSocketMessage(callback: (message: unknown) => void): () => void {
|
|
226
|
+
return this.strategy.onWebSocketMessage(callback);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
onWebSocketStatus(callback: (status: WebSocketStatus) => void): () => void {
|
|
230
|
+
return this.strategy.onWebSocketStatus(callback);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
terminate() {
|
|
234
|
+
this.strategy.terminate();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Unified communication layer for all workers (SharedWorker, WebWorker, main thread)
|
|
2
|
+
// Uses BroadcastChannel for real-time events
|
|
3
|
+
|
|
4
|
+
import type { BroadcastEvent } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WorkerCommunication: Unified communication layer for workers
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - BroadcastChannel for real-time events
|
|
11
|
+
* - Works in SharedWorker, WebWorker, and main thread
|
|
12
|
+
*
|
|
13
|
+
* Note: This layer does NOT open IndexedDB. The SharedWorker is the single
|
|
14
|
+
* writer to the auth database (sb-auth-v2). All other contexts receive
|
|
15
|
+
* auth state via BroadcastChannel or postMessage.
|
|
16
|
+
*/
|
|
17
|
+
export class WorkerCommunication {
|
|
18
|
+
private bc: BroadcastChannel | null = null;
|
|
19
|
+
private listeners = new Map<string, Set<(payload: unknown) => void>>();
|
|
20
|
+
private channelName: string;
|
|
21
|
+
|
|
22
|
+
constructor(channelName = 'supabase_events') {
|
|
23
|
+
this.channelName = channelName;
|
|
24
|
+
this.initBroadcastChannel();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private initBroadcastChannel() {
|
|
28
|
+
try {
|
|
29
|
+
if (typeof BroadcastChannel !== 'undefined') {
|
|
30
|
+
this.bc = new BroadcastChannel(this.channelName);
|
|
31
|
+
this.bc.onmessage = (e: MessageEvent) =>
|
|
32
|
+
this.handleBroadcast(e.data);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(
|
|
36
|
+
'[WorkerCommunication] Failed to initialize BroadcastChannel:',
|
|
37
|
+
err,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Broadcast an event to all workers and tabs
|
|
44
|
+
*/
|
|
45
|
+
broadcast(event: BroadcastEvent) {
|
|
46
|
+
if (this.bc) {
|
|
47
|
+
try {
|
|
48
|
+
this.bc.postMessage(event);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('[WorkerCommunication] Broadcast failed:', err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private handleBroadcast(event: BroadcastEvent) {
|
|
56
|
+
const { type, key } = event;
|
|
57
|
+
|
|
58
|
+
if (type === 'realtime' && key) {
|
|
59
|
+
this.emit(`realtime:${key}`, event.payload);
|
|
60
|
+
} else {
|
|
61
|
+
this.emit(type, event.data ?? event);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Subscribe to events (local and broadcast)
|
|
67
|
+
*/
|
|
68
|
+
on(event: string, callback: (payload: unknown) => void): () => void {
|
|
69
|
+
if (!this.listeners.has(event)) {
|
|
70
|
+
this.listeners.set(event, new Set());
|
|
71
|
+
}
|
|
72
|
+
this.listeners.get(event)?.add(callback);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
const set = this.listeners.get(event);
|
|
76
|
+
if (set) {
|
|
77
|
+
set.delete(callback);
|
|
78
|
+
if (set.size === 0) {
|
|
79
|
+
this.listeners.delete(event);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private emit(event: string, payload: unknown) {
|
|
86
|
+
const listeners = this.listeners.get(event);
|
|
87
|
+
if (listeners) {
|
|
88
|
+
listeners.forEach((cb) => {
|
|
89
|
+
try {
|
|
90
|
+
cb(payload);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(
|
|
93
|
+
`[WorkerCommunication] Listener error for ${event}:`,
|
|
94
|
+
err,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if BroadcastChannel is available
|
|
103
|
+
*/
|
|
104
|
+
hasBroadcastChannel(): boolean {
|
|
105
|
+
return this.bc !== null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Close connections
|
|
110
|
+
*/
|
|
111
|
+
close() {
|
|
112
|
+
if (this.bc) {
|
|
113
|
+
this.bc.close();
|
|
114
|
+
this.bc = null;
|
|
115
|
+
}
|
|
116
|
+
this.listeners.clear();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a singleton instance for use across workers
|
|
122
|
+
*/
|
|
123
|
+
let _instance: WorkerCommunication | null = null;
|
|
124
|
+
|
|
125
|
+
export function getWorkerCommunication(
|
|
126
|
+
channelName?: string,
|
|
127
|
+
): WorkerCommunication {
|
|
128
|
+
if (!_instance) {
|
|
129
|
+
_instance = new WorkerCommunication(channelName);
|
|
130
|
+
}
|
|
131
|
+
return _instance;
|
|
132
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Worker pool manager with round-robin routing for DB operations
|
|
2
|
+
|
|
3
|
+
import type { WorkerMessage, WorkerResponse } from './types';
|
|
4
|
+
|
|
5
|
+
export interface WorkerPoolConfig {
|
|
6
|
+
size: number;
|
|
7
|
+
workerUrl: string | URL;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* WorkerPool: Manages a pool of workers with round-robin load balancing
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Fixed-size pool (default: 3 workers)
|
|
15
|
+
* - Round-robin routing for database operations
|
|
16
|
+
* - Request/response tracking with UUIDs
|
|
17
|
+
* - Automatic error handling with exponential backoff retries
|
|
18
|
+
*/
|
|
19
|
+
export class WorkerPool {
|
|
20
|
+
private workers: Worker[] = [];
|
|
21
|
+
private poolSize: number;
|
|
22
|
+
private roundRobinIndex = 0;
|
|
23
|
+
private pending = new Map<
|
|
24
|
+
string,
|
|
25
|
+
{
|
|
26
|
+
resolve: (value: unknown) => void;
|
|
27
|
+
reject: (reason?: unknown) => void;
|
|
28
|
+
workerId: number;
|
|
29
|
+
}
|
|
30
|
+
>();
|
|
31
|
+
private workerUrl: string | URL;
|
|
32
|
+
private workerRetryCount = new Map<number, number>();
|
|
33
|
+
private workerRetryTimers = new Map<
|
|
34
|
+
number,
|
|
35
|
+
ReturnType<typeof setTimeout>
|
|
36
|
+
>();
|
|
37
|
+
|
|
38
|
+
private readonly MAX_RETRY_ATTEMPTS = 3;
|
|
39
|
+
private readonly BASE_RETRY_DELAY_MS = 1000;
|
|
40
|
+
private readonly MAX_RETRY_DELAY_MS = 8000;
|
|
41
|
+
private readonly REQUEST_TIMEOUT_MS = 30000;
|
|
42
|
+
|
|
43
|
+
constructor(config: WorkerPoolConfig) {
|
|
44
|
+
this.poolSize = config.size;
|
|
45
|
+
this.workerUrl = config.workerUrl;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the worker pool
|
|
50
|
+
*/
|
|
51
|
+
async init(): Promise<void> {
|
|
52
|
+
console.log(
|
|
53
|
+
`[WorkerPool] Initializing pool with ${this.poolSize} workers`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < this.poolSize; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const worker = new Worker(this.workerUrl, {
|
|
59
|
+
type: 'module',
|
|
60
|
+
name: `db-worker-${i}`,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
worker.onmessage = (e: MessageEvent) =>
|
|
64
|
+
this.handleMessage(e.data, i);
|
|
65
|
+
worker.onerror = (err) => this.handleError(err, i);
|
|
66
|
+
|
|
67
|
+
this.workers.push(worker);
|
|
68
|
+
console.log(`[WorkerPool] Worker ${i} initialized`);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(
|
|
71
|
+
`[WorkerPool] Failed to create worker ${i}:`,
|
|
72
|
+
err,
|
|
73
|
+
);
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Wait for all workers to be ready
|
|
79
|
+
await Promise.all(
|
|
80
|
+
this.workers.map((_worker, i) =>
|
|
81
|
+
this.sendToWorker(i, {
|
|
82
|
+
id: crypto.randomUUID(),
|
|
83
|
+
type: 'ping',
|
|
84
|
+
payload: {},
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
console.log('[WorkerPool] All workers ready');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private getNextWorker(): number {
|
|
93
|
+
const workerId = this.roundRobinIndex;
|
|
94
|
+
this.roundRobinIndex = (this.roundRobinIndex + 1) % this.poolSize;
|
|
95
|
+
return workerId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private sendToWorker<T>(
|
|
99
|
+
workerId: number,
|
|
100
|
+
message: WorkerMessage,
|
|
101
|
+
): Promise<T> {
|
|
102
|
+
return new Promise<T>((resolve, reject) => {
|
|
103
|
+
const worker = this.workers[workerId];
|
|
104
|
+
if (!worker) {
|
|
105
|
+
return reject(new Error(`Worker ${workerId} not available`));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
if (this.pending.has(message.id)) {
|
|
110
|
+
this.pending.delete(message.id);
|
|
111
|
+
reject(
|
|
112
|
+
new Error(
|
|
113
|
+
`Worker ${workerId} request ${message.type} timed out after ${this.REQUEST_TIMEOUT_MS}ms`,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}, this.REQUEST_TIMEOUT_MS);
|
|
118
|
+
|
|
119
|
+
this.pending.set(message.id, {
|
|
120
|
+
resolve: (value: unknown) => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolve(value as T);
|
|
123
|
+
},
|
|
124
|
+
reject: (err: unknown) => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
reject(err);
|
|
127
|
+
},
|
|
128
|
+
workerId,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
worker.postMessage(message);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
this.pending.delete(message.id);
|
|
136
|
+
reject(err);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Send message to next available worker (round-robin)
|
|
143
|
+
*/
|
|
144
|
+
send<T>(type: string, payload?: unknown): Promise<T> {
|
|
145
|
+
const workerId = this.getNextWorker();
|
|
146
|
+
const id = crypto.randomUUID();
|
|
147
|
+
const message: WorkerMessage = { id, type, payload };
|
|
148
|
+
|
|
149
|
+
return this.sendToWorker<T>(workerId, message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handleMessage(response: WorkerResponse, workerId: number) {
|
|
153
|
+
const { id, ok, data, error } = response;
|
|
154
|
+
|
|
155
|
+
if (!id) {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[WorkerPool] Worker ${workerId} sent message without ID:`,
|
|
158
|
+
response,
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const pending = this.pending.get(id);
|
|
164
|
+
if (!pending) {
|
|
165
|
+
console.warn(
|
|
166
|
+
`[WorkerPool] Received response for unknown request ${id}`,
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.pending.delete(id);
|
|
172
|
+
|
|
173
|
+
if (ok) {
|
|
174
|
+
pending.resolve(data);
|
|
175
|
+
} else {
|
|
176
|
+
pending.reject(new Error(error || 'Worker request failed'));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private handleError(error: ErrorEvent, workerId: number) {
|
|
181
|
+
console.error(`[WorkerPool] Worker ${workerId} error:`, error);
|
|
182
|
+
|
|
183
|
+
const failedRequests: string[] = [];
|
|
184
|
+
for (const [id, pending] of this.pending.entries()) {
|
|
185
|
+
if (pending.workerId === workerId) {
|
|
186
|
+
pending.reject(
|
|
187
|
+
new Error(`Worker ${workerId} crashed: ${error.message}`),
|
|
188
|
+
);
|
|
189
|
+
failedRequests.push(id);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
failedRequests.forEach((id) => this.pending.delete(id));
|
|
194
|
+
|
|
195
|
+
this.scheduleWorkerRecreation(workerId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private scheduleWorkerRecreation(workerId: number) {
|
|
199
|
+
const retryCount = this.workerRetryCount.get(workerId) || 0;
|
|
200
|
+
|
|
201
|
+
if (retryCount >= this.MAX_RETRY_ATTEMPTS) {
|
|
202
|
+
console.error(
|
|
203
|
+
`[WorkerPool] Worker ${workerId} has failed ${retryCount} times. Giving up.`,
|
|
204
|
+
);
|
|
205
|
+
this.workerRetryCount.delete(workerId);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const delay = Math.min(
|
|
210
|
+
this.BASE_RETRY_DELAY_MS * Math.pow(2, retryCount),
|
|
211
|
+
this.MAX_RETRY_DELAY_MS,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
console.log(
|
|
215
|
+
`[WorkerPool] Scheduling worker ${workerId} recreation in ${delay}ms (attempt ${retryCount + 1}/${this.MAX_RETRY_ATTEMPTS})`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const existingTimer = this.workerRetryTimers.get(workerId);
|
|
219
|
+
if (existingTimer) {
|
|
220
|
+
clearTimeout(existingTimer);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
this.workerRetryTimers.delete(workerId);
|
|
225
|
+
this.recreateWorker(workerId);
|
|
226
|
+
}, delay);
|
|
227
|
+
|
|
228
|
+
this.workerRetryTimers.set(workerId, timer);
|
|
229
|
+
this.workerRetryCount.set(workerId, retryCount + 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async recreateWorker(workerId: number) {
|
|
233
|
+
const retryCount = this.workerRetryCount.get(workerId) || 0;
|
|
234
|
+
console.log(
|
|
235
|
+
`[WorkerPool] Attempting to recreate worker ${workerId} (attempt ${retryCount}/${this.MAX_RETRY_ATTEMPTS})`,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const worker = new Worker(this.workerUrl, {
|
|
240
|
+
type: 'module',
|
|
241
|
+
name: `db-worker-${workerId}`,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
worker.onmessage = (e: MessageEvent) =>
|
|
245
|
+
this.handleMessage(e.data, workerId);
|
|
246
|
+
worker.onerror = (err) => this.handleError(err, workerId);
|
|
247
|
+
|
|
248
|
+
this.workers[workerId] = worker;
|
|
249
|
+
|
|
250
|
+
await this.sendToWorker(workerId, {
|
|
251
|
+
id: crypto.randomUUID(),
|
|
252
|
+
type: 'ping',
|
|
253
|
+
payload: {},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.log(
|
|
257
|
+
`[WorkerPool] Worker ${workerId} recreated successfully`,
|
|
258
|
+
);
|
|
259
|
+
this.workerRetryCount.delete(workerId);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(
|
|
262
|
+
`[WorkerPool] Failed to recreate worker ${workerId}:`,
|
|
263
|
+
err,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Terminate all workers
|
|
270
|
+
*/
|
|
271
|
+
terminate() {
|
|
272
|
+
console.log('[WorkerPool] Terminating all workers');
|
|
273
|
+
|
|
274
|
+
this.workerRetryTimers.forEach((timer) => clearTimeout(timer));
|
|
275
|
+
this.workerRetryTimers.clear();
|
|
276
|
+
this.workerRetryCount.clear();
|
|
277
|
+
|
|
278
|
+
this.workers.forEach((worker, i) => {
|
|
279
|
+
try {
|
|
280
|
+
worker.terminate();
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.error(
|
|
283
|
+
`[WorkerPool] Error terminating worker ${i}:`,
|
|
284
|
+
err,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
this.workers = [];
|
|
290
|
+
this.pending.clear();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get pool statistics
|
|
295
|
+
*/
|
|
296
|
+
getStats() {
|
|
297
|
+
return {
|
|
298
|
+
size: this.poolSize,
|
|
299
|
+
activeWorkers: this.workers.length,
|
|
300
|
+
pendingRequests: this.pending.size,
|
|
301
|
+
nextWorker: this.roundRobinIndex,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|