@kbve/droid 0.0.1 → 0.0.3
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/LICENSE +1 -0
- package/README.md +5 -1
- package/assets/canvas-worker-DbsoY2Xv.js +5 -0
- package/assets/db-worker-CjXS52DQ.js +14 -0
- package/assets/ws-worker-FssAQgaD.js +5 -0
- package/comlink-CC72iIUO.js +253 -0
- package/droid.es.js +74 -12
- package/package.json +13 -3
- package/reference-Dk_1njEH.js +259 -0
- package/src/index.ts +21 -0
- package/src/lib/droid.spec.ts +26 -0
- package/src/lib/droid.ts +7 -0
- package/src/lib/mod/mod-manager.ts +73 -0
- package/src/lib/mod/mod-urls.ts +3 -0
- package/src/lib/mod/module/bento/mod-bento.worker.ts +128 -0
- package/src/lib/mod/module/phaser/mod-phaser.ts +0 -0
- package/src/lib/mod/module/supabase/mod-supabase.spec.ts +30 -0
- package/src/lib/mod/module/supabase/mod-supabase.worker.ts +59 -0
- package/src/lib/types/bento.ts +80 -0
- package/src/lib/types/discord.ts +40 -0
- package/src/lib/types/event-types.ts +35 -0
- package/src/lib/types/jedi.ts +66 -0
- package/src/lib/types/modules.ts +46 -0
- package/src/lib/types/panel-types.ts +18 -0
- package/src/lib/types/supabase-esm.d.ts +4 -0
- package/src/lib/workers/canvas-worker.ts +107 -0
- package/src/lib/workers/data.ts +215 -0
- package/src/lib/workers/db-worker.ts +203 -0
- package/src/lib/workers/events.ts +52 -0
- package/src/lib/workers/flexbuilder.ts +6 -0
- package/src/lib/workers/init.ts +40 -0
- package/src/lib/workers/main.ts +531 -0
- package/src/lib/workers/tools.ts +60 -0
- package/src/lib/workers/ws-worker.ts +73 -0
- package/src/setup-vitest.ts +2 -0
- package/src/types.d.ts +31 -0
- package/workers/main.js +1091 -0
- package/droid.cjs.js +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { PayloadFormat, JediEnvelopeFlex, MessageKind } from '../types/jedi';
|
|
2
|
+
import { builder, toReference } from './flexbuilder';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function buildFlexPayload<T extends Record<string, any>>(payload: T): Uint8Array {
|
|
6
|
+
const b = builder();
|
|
7
|
+
b.startMap();
|
|
8
|
+
|
|
9
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
10
|
+
b.addKey(key);
|
|
11
|
+
b.add(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
b.end();
|
|
15
|
+
return b.finish();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function wrapEnvelope<T extends Record<string, any>>(
|
|
19
|
+
payload: T,
|
|
20
|
+
kind: number,
|
|
21
|
+
format: PayloadFormat,
|
|
22
|
+
metadata?: Uint8Array,
|
|
23
|
+
version = 1,
|
|
24
|
+
): Uint8Array {
|
|
25
|
+
let serializedPayload: Uint8Array;
|
|
26
|
+
|
|
27
|
+
if (format === PayloadFormat.FLEX) {
|
|
28
|
+
serializedPayload = buildFlexPayload(payload);
|
|
29
|
+
} else if (format === PayloadFormat.JSON) {
|
|
30
|
+
serializedPayload = new TextEncoder().encode(JSON.stringify(payload));
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error('Unsupported format for wrapEnvelope');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const b = builder();
|
|
36
|
+
b.startMap();
|
|
37
|
+
b.addKey('version'); b.add(version);
|
|
38
|
+
b.addKey('kind'); b.add(kind);
|
|
39
|
+
b.addKey('format'); b.add(format);
|
|
40
|
+
b.addKey('payload'); b.add(serializedPayload);
|
|
41
|
+
b.addKey('metadata'); b.add(metadata ?? new Uint8Array());
|
|
42
|
+
b.end();
|
|
43
|
+
|
|
44
|
+
return b.finish();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function unwrapEnvelope<T = unknown>(
|
|
48
|
+
buffer: Uint8Array | ArrayBuffer
|
|
49
|
+
): { envelope: JediEnvelopeFlex; payload: T } {
|
|
50
|
+
const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
51
|
+
|
|
52
|
+
const root = toReference(view.buffer as ArrayBuffer).toObject() as Record<string, any>;
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
typeof root['version'] !== 'number' ||
|
|
56
|
+
typeof root['kind'] !== 'number' ||
|
|
57
|
+
typeof root['format'] !== 'number' ||
|
|
58
|
+
!root['payload']
|
|
59
|
+
) {
|
|
60
|
+
console.error('[unwrapEnvelope] Bad root:', root);
|
|
61
|
+
throw new Error('[unwrapEnvelope] Invalid envelope structure');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const envelope: JediEnvelopeFlex = {
|
|
65
|
+
version: root['version'],
|
|
66
|
+
kind: root['kind'],
|
|
67
|
+
format: root['format'],
|
|
68
|
+
payload: new Uint8Array(root['payload']),
|
|
69
|
+
metadata: root['metadata'] ? new Uint8Array(root['metadata']) : undefined,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let parsedPayload: T;
|
|
73
|
+
|
|
74
|
+
if (envelope.format === PayloadFormat.FLEX) {
|
|
75
|
+
const ref = toReference(envelope.payload.buffer as ArrayBuffer);
|
|
76
|
+
parsedPayload = ref.toObject() as T;
|
|
77
|
+
} else if (envelope.format === PayloadFormat.JSON) {
|
|
78
|
+
parsedPayload = JSON.parse(new TextDecoder().decode(envelope.payload)) as T;
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error(`[unwrapEnvelope] Unsupported format: ${envelope.format}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { envelope, payload: parsedPayload };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function unwrapFlexToJson(buffer: Uint8Array) {
|
|
87
|
+
return toReference(buffer.buffer as ArrayBuffer).toObject();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function inspectFlex(buffer: Uint8Array | ArrayBuffer): void {
|
|
91
|
+
try {
|
|
92
|
+
const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
93
|
+
const obj = unwrapFlexToJson(view);
|
|
94
|
+
console.log('[FlexObject]', JSON.stringify(obj, null, 2));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('[Flex Decode Error]', err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function hasKind(kind: number, flag: number): boolean {
|
|
101
|
+
return (kind & flag) === flag;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
// Redis Helpers
|
|
106
|
+
|
|
107
|
+
export function wrapRedisSet(key: string, value: string): Uint8Array {
|
|
108
|
+
return wrapEnvelope(
|
|
109
|
+
{ key, value },
|
|
110
|
+
MessageKind.SET | MessageKind.REDIS,
|
|
111
|
+
PayloadFormat.FLEX
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function wrapRedisGet(key: string): Uint8Array {
|
|
116
|
+
return wrapEnvelope(
|
|
117
|
+
{ key },
|
|
118
|
+
MessageKind.GET | MessageKind.REDIS,
|
|
119
|
+
PayloadFormat.FLEX
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function wrapRedisDel(key: string): Uint8Array {
|
|
124
|
+
return wrapEnvelope(
|
|
125
|
+
{ key },
|
|
126
|
+
MessageKind.DEL | MessageKind.REDIS,
|
|
127
|
+
PayloadFormat.FLEX
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function parseRedisPayload<T = unknown>(envelopeBytes: Uint8Array): {
|
|
132
|
+
envelope: JediEnvelopeFlex;
|
|
133
|
+
payload: T;
|
|
134
|
+
} {
|
|
135
|
+
const result = unwrapEnvelope<T>(envelopeBytes);
|
|
136
|
+
if (!hasKind(result.envelope.kind, MessageKind.REDIS)) {
|
|
137
|
+
throw new Error('[Redis] Not a Redis envelope');
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function wrapRedisXAdd(
|
|
143
|
+
stream: string,
|
|
144
|
+
fields: Record<string, string>,
|
|
145
|
+
id = '*',
|
|
146
|
+
): Uint8Array {
|
|
147
|
+
return wrapEnvelope(
|
|
148
|
+
{ stream, id, fields },
|
|
149
|
+
MessageKind.ADD | MessageKind.STREAM | MessageKind.REDIS,
|
|
150
|
+
PayloadFormat.FLEX,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function wrapRedisXRead(
|
|
155
|
+
streams: { stream: string; id: string }[],
|
|
156
|
+
count?: number,
|
|
157
|
+
block?: number,
|
|
158
|
+
): Uint8Array {
|
|
159
|
+
|
|
160
|
+
const inner = builder();
|
|
161
|
+
inner.startMap();
|
|
162
|
+
|
|
163
|
+
inner.addKey('streams');
|
|
164
|
+
inner.startVector();
|
|
165
|
+
for (const { stream, id } of streams) {
|
|
166
|
+
inner.startMap();
|
|
167
|
+
inner.addKey('stream'); inner.add(stream);
|
|
168
|
+
inner.addKey('id'); inner.add(id);
|
|
169
|
+
inner.end();
|
|
170
|
+
}
|
|
171
|
+
inner.end();
|
|
172
|
+
|
|
173
|
+
if (count !== undefined) {
|
|
174
|
+
inner.addKey('count'); inner.add(count);
|
|
175
|
+
}
|
|
176
|
+
if (block !== undefined) {
|
|
177
|
+
inner.addKey('block'); inner.add(block);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
inner.end();
|
|
181
|
+
const serializedPayload = inner.finish();
|
|
182
|
+
|
|
183
|
+
const b = builder();
|
|
184
|
+
b.startMap();
|
|
185
|
+
b.addKey('version'); b.add(1);
|
|
186
|
+
b.addKey('kind'); b.add(MessageKind.READ | MessageKind.STREAM | MessageKind.REDIS);
|
|
187
|
+
b.addKey('format'); b.add(PayloadFormat.FLEX);
|
|
188
|
+
b.addKey('payload'); b.add(serializedPayload);
|
|
189
|
+
b.addKey('metadata'); b.add(new Uint8Array());
|
|
190
|
+
b.end();
|
|
191
|
+
|
|
192
|
+
return b.finish();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
export const scopeData = {
|
|
198
|
+
wrapEnvelope,
|
|
199
|
+
unwrapEnvelope,
|
|
200
|
+
MessageKind,
|
|
201
|
+
PayloadFormat,
|
|
202
|
+
unwrapFlexToJson,
|
|
203
|
+
inspectFlex,
|
|
204
|
+
hasKind,
|
|
205
|
+
redis: {
|
|
206
|
+
wrapRedisSet,
|
|
207
|
+
wrapRedisGet,
|
|
208
|
+
wrapRedisDel,
|
|
209
|
+
wrapRedisXAdd,
|
|
210
|
+
wrapRedisXRead,
|
|
211
|
+
parseRedisPayload,
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export type FlexDataAPI = typeof scopeData;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { expose } from 'comlink';
|
|
2
|
+
import Dexie, { type Table } from 'dexie';
|
|
3
|
+
import type { DiscordServer, DiscordTag, Profile } from '../types/discord';
|
|
4
|
+
import { toReference } from './flexbuilder';
|
|
5
|
+
|
|
6
|
+
interface SharedWorkerGlobalScope extends Worker {
|
|
7
|
+
onconnect: (event: MessageEvent) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const self: SharedWorkerGlobalScope;
|
|
10
|
+
|
|
11
|
+
class AppDexie extends Dexie {
|
|
12
|
+
settings!: Table<{ id: string; value: any }, string>;
|
|
13
|
+
meta!: Table<{ key: string; value: any }, string>;
|
|
14
|
+
i18n!: Table<{ key: string; value: string }, string>;
|
|
15
|
+
htmlservers!: Table<{ key: string; value: string }, string>;
|
|
16
|
+
servers!: Table<DiscordServer, string>;
|
|
17
|
+
tags!: Table<DiscordTag, string>;
|
|
18
|
+
profiles!: Table<Profile, string>;
|
|
19
|
+
ws_messages!: Table<{ key: string; message: any }, string>;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super('AppStorage');
|
|
23
|
+
this.version(3).stores({
|
|
24
|
+
settings: '&id',
|
|
25
|
+
meta: '&key',
|
|
26
|
+
i18n: '&key',
|
|
27
|
+
htmlservers: '&key',
|
|
28
|
+
servers: '&server_id',
|
|
29
|
+
tags: '&tag_id',
|
|
30
|
+
profiles: '&profile_id',
|
|
31
|
+
ws_messages: '&key',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const db = new AppDexie();
|
|
36
|
+
|
|
37
|
+
function delay(ms: number) {
|
|
38
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderHtmlForServer(server: DiscordServer): string {
|
|
42
|
+
return `
|
|
43
|
+
<div class="flex flex-col gap-2 p-2">
|
|
44
|
+
<img src="${server.logo}" alt="${server.name}" class="w-12 h-12 rounded-full" />
|
|
45
|
+
<h3 class="text-lg font-bold">${server.name}</h3>
|
|
46
|
+
<p class="text-sm opacity-70">${server.summary}</p>
|
|
47
|
+
<a href="${server.invite}" class="text-purple-400 underline text-xs">Join Join Join</a>
|
|
48
|
+
</div>
|
|
49
|
+
`.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Comlink-Safe API ---
|
|
53
|
+
const storageAPI = {
|
|
54
|
+
|
|
55
|
+
// WebSocket
|
|
56
|
+
|
|
57
|
+
async storeWsMessage(key: string, buffer: ArrayBuffer) {
|
|
58
|
+
const decoded = toReference(buffer).toObject();
|
|
59
|
+
await db.ws_messages.put({ key, message: decoded });
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async getWsMessage(key: string) {
|
|
63
|
+
return (await db.ws_messages.get(key))?.message ?? null;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async getAllWsMessages(): Promise<{ key: string; message: any }[]> {
|
|
67
|
+
const raw = await db.ws_messages.toArray();
|
|
68
|
+
return raw.sort((a, b) => {
|
|
69
|
+
const at = Number(a.key.split(':')[1]);
|
|
70
|
+
const bt = Number(b.key.split(':')[1]);
|
|
71
|
+
return bt - at;
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async clearWsMessages() {
|
|
76
|
+
await db.ws_messages.clear();
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// I18n
|
|
80
|
+
async getTranslation(key: string) {
|
|
81
|
+
return (await db.i18n.get(key))?.value ?? null;
|
|
82
|
+
},
|
|
83
|
+
async getTranslations(keys: string[]) {
|
|
84
|
+
const result: Record<string, string> = {};
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const val = await db.i18n.get(key);
|
|
87
|
+
if (val) result[key] = val.value;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
},
|
|
91
|
+
async putI18nBatch(data: Record<string, string>) {
|
|
92
|
+
const entries = Object.entries(data).map(([key, value]) => ({
|
|
93
|
+
key,
|
|
94
|
+
value,
|
|
95
|
+
}));
|
|
96
|
+
await db.i18n.bulkPut(entries);
|
|
97
|
+
},
|
|
98
|
+
async loadI18nFromJSON(path = 'https://discord.sh/i18n/db.json') {
|
|
99
|
+
const res = await fetch(path);
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
await storageAPI.putI18nBatch(data);
|
|
102
|
+
},
|
|
103
|
+
async getAllI18nKeys(): Promise<string[]> {
|
|
104
|
+
const all = await db.i18n.toCollection().primaryKeys();
|
|
105
|
+
return all as string[];
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async loadServersFromJSON(path = 'https://discord.sh/data/servers.json') {
|
|
109
|
+
const res = await fetch(path);
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
|
|
112
|
+
// The JSON should be a Record<string, DiscordServer>
|
|
113
|
+
const servers: DiscordServer[] = Object.values(data);
|
|
114
|
+
await storageAPI.putServers(servers);
|
|
115
|
+
await storageAPI.syncHtmlFromServers();
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// HTML PreRender
|
|
119
|
+
async syncHtmlFromServers() {
|
|
120
|
+
const allServers = await db.servers.toArray();
|
|
121
|
+
const existingHtmlKeys = await db.htmlservers.toCollection().primaryKeys();
|
|
122
|
+
const missing: { key: string; value: string }[] = [];
|
|
123
|
+
|
|
124
|
+
for (const server of allServers) {
|
|
125
|
+
if (!existingHtmlKeys.includes(server.server_id)) {
|
|
126
|
+
const html = renderHtmlForServer(server);
|
|
127
|
+
missing.push({ key: server.server_id, value: html });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (missing.length > 0) {
|
|
132
|
+
await db.htmlservers.bulkPut(missing);
|
|
133
|
+
console.info(`[syncHtmlFromServers] Generated ${missing.length} HTML cards.`);
|
|
134
|
+
} else {
|
|
135
|
+
console.info('[syncHtmlFromServers] All servers already have HTML cards.');
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Settings
|
|
140
|
+
async dbSet(id: string, value: any) {
|
|
141
|
+
await db.settings.put({ id, value });
|
|
142
|
+
},
|
|
143
|
+
async dbGet(id: string) {
|
|
144
|
+
return (await db.settings.get(id))?.value ?? null;
|
|
145
|
+
},
|
|
146
|
+
async dbClear() {
|
|
147
|
+
await db.settings.clear();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// Meta
|
|
151
|
+
async setVersion(version: string) {
|
|
152
|
+
await db.meta.put({ key: 'version', value: version });
|
|
153
|
+
},
|
|
154
|
+
async getVersion() {
|
|
155
|
+
return (await db.meta.get('version'))?.value ?? null;
|
|
156
|
+
},
|
|
157
|
+
async markSeeded() {
|
|
158
|
+
await db.meta.put({ key: 'db_seeded', value: true });
|
|
159
|
+
},
|
|
160
|
+
async checkSeeded() {
|
|
161
|
+
return (await db.meta.get('db_seeded'))?.value === true;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Servers
|
|
165
|
+
async putServers(servers: DiscordServer[]) {
|
|
166
|
+
await db.servers.bulkPut(servers);
|
|
167
|
+
},
|
|
168
|
+
async getAllServers() {
|
|
169
|
+
return await db.servers.toArray();
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// HTML servers
|
|
173
|
+
async putHtmlCards(data: { key: string; value: string }[]) {
|
|
174
|
+
await db.htmlservers.bulkPut(data);
|
|
175
|
+
},
|
|
176
|
+
async getHtmlCard(key: string) {
|
|
177
|
+
return (await db.htmlservers.get(key))?.value ?? null;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Tags
|
|
181
|
+
async putTags(tags: DiscordTag[]) {
|
|
182
|
+
await db.tags.bulkPut(tags);
|
|
183
|
+
},
|
|
184
|
+
async getAllTags() {
|
|
185
|
+
return await db.tags.toArray();
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// Profiles
|
|
189
|
+
async putProfiles(profiles: Profile[]) {
|
|
190
|
+
await db.profiles.bulkPut(profiles);
|
|
191
|
+
},
|
|
192
|
+
async getAllProfiles() {
|
|
193
|
+
return await db.profiles.toArray();
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type LocalStorageAPI = typeof storageAPI;
|
|
198
|
+
|
|
199
|
+
self.onconnect = (event: MessageEvent) => {
|
|
200
|
+
const port = event.ports[0];
|
|
201
|
+
port.start();
|
|
202
|
+
expose(storageAPI, port);
|
|
203
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DroidEventSchemas } from '../types/event-types';
|
|
2
|
+
import type { DroidEventMap, EventKey, EventHandler } from '../types/event-types';
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DroidEventBus {
|
|
8
|
+
private listeners = new Map<EventKey, Set<(payload: unknown) => void>>();
|
|
9
|
+
|
|
10
|
+
on<K extends EventKey>(event: K, handler: EventHandler<K>) {
|
|
11
|
+
let handlers = this.listeners.get(event);
|
|
12
|
+
if (!handlers) {
|
|
13
|
+
handlers = new Set();
|
|
14
|
+
this.listeners.set(event, handlers);
|
|
15
|
+
}
|
|
16
|
+
handlers.add(handler as (payload: unknown) => void);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
off<K extends EventKey>(event: K, handler: EventHandler<K>) {
|
|
20
|
+
this.listeners.get(event)?.delete(handler as (payload: unknown) => void);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
emit<K extends EventKey>(event: K, payload: DroidEventMap[K]) {
|
|
24
|
+
const schema = DroidEventSchemas[event];
|
|
25
|
+
if (schema) {
|
|
26
|
+
try {
|
|
27
|
+
schema.parse(payload);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(`[DroidEventBus] Invalid payload for ${event}:`, err);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
window.dispatchEvent(new CustomEvent(event, { detail: payload }));
|
|
35
|
+
|
|
36
|
+
for (const handler of this.listeners.get(event) ?? []) {
|
|
37
|
+
(handler as EventHandler<K>)(payload);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
wait<K extends EventKey>(event: K): Promise<DroidEventMap[K]> {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const fn: EventHandler<K> = (payload) => {
|
|
44
|
+
this.off(event, fn);
|
|
45
|
+
resolve(payload);
|
|
46
|
+
};
|
|
47
|
+
this.on(event, fn);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const DroidEvents = new DroidEventBus();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Remote } from 'comlink'
|
|
2
|
+
import type { LocalStorageAPI } from './db-worker'
|
|
3
|
+
|
|
4
|
+
export interface InitWorkerOptions {
|
|
5
|
+
version: string
|
|
6
|
+
i18nPath?: string
|
|
7
|
+
locale?: string
|
|
8
|
+
defaults?: Record<string, any>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function initializeWorkerDatabase(
|
|
12
|
+
worker: Remote<LocalStorageAPI>,
|
|
13
|
+
{
|
|
14
|
+
version,
|
|
15
|
+
i18nPath = 'https://discord.sh/i18n/db.json',
|
|
16
|
+
locale = 'en',
|
|
17
|
+
defaults = {
|
|
18
|
+
welcome: 'Welcome to the app!',
|
|
19
|
+
},
|
|
20
|
+
}: InitWorkerOptions
|
|
21
|
+
) {
|
|
22
|
+
console.log('[init-worker] Initializing database...')
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await worker.loadI18nFromJSON(i18nPath)
|
|
26
|
+
console.log(`[init-worker] i18n loaded from ${i18nPath}`)
|
|
27
|
+
|
|
28
|
+
await worker.dbSet('locale', locale)
|
|
29
|
+
|
|
30
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
31
|
+
await worker.dbSet(key, value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await worker.setVersion(version)
|
|
35
|
+
void worker.loadServersFromJSON(); // await worker.loadServersFromJSON();
|
|
36
|
+
console.log(`[init-worker] DB initialized to version ${version}`)
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('[init-worker] Initialization failed:', err)
|
|
39
|
+
}
|
|
40
|
+
}
|