@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.
Files changed (38) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +5 -1
  3. package/assets/canvas-worker-DbsoY2Xv.js +5 -0
  4. package/assets/db-worker-CjXS52DQ.js +14 -0
  5. package/assets/ws-worker-FssAQgaD.js +5 -0
  6. package/comlink-CC72iIUO.js +253 -0
  7. package/droid.es.js +74 -12
  8. package/package.json +13 -3
  9. package/reference-Dk_1njEH.js +259 -0
  10. package/src/index.ts +21 -0
  11. package/src/lib/droid.spec.ts +26 -0
  12. package/src/lib/droid.ts +7 -0
  13. package/src/lib/mod/mod-manager.ts +73 -0
  14. package/src/lib/mod/mod-urls.ts +3 -0
  15. package/src/lib/mod/module/bento/mod-bento.worker.ts +128 -0
  16. package/src/lib/mod/module/phaser/mod-phaser.ts +0 -0
  17. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +30 -0
  18. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +59 -0
  19. package/src/lib/types/bento.ts +80 -0
  20. package/src/lib/types/discord.ts +40 -0
  21. package/src/lib/types/event-types.ts +35 -0
  22. package/src/lib/types/jedi.ts +66 -0
  23. package/src/lib/types/modules.ts +46 -0
  24. package/src/lib/types/panel-types.ts +18 -0
  25. package/src/lib/types/supabase-esm.d.ts +4 -0
  26. package/src/lib/workers/canvas-worker.ts +107 -0
  27. package/src/lib/workers/data.ts +215 -0
  28. package/src/lib/workers/db-worker.ts +203 -0
  29. package/src/lib/workers/events.ts +52 -0
  30. package/src/lib/workers/flexbuilder.ts +6 -0
  31. package/src/lib/workers/init.ts +40 -0
  32. package/src/lib/workers/main.ts +531 -0
  33. package/src/lib/workers/tools.ts +60 -0
  34. package/src/lib/workers/ws-worker.ts +73 -0
  35. package/src/setup-vitest.ts +2 -0
  36. package/src/types.d.ts +31 -0
  37. package/workers/main.js +1091 -0
  38. 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,6 @@
1
+ import { Builder as FlexBuilder } from 'flatbuffers/ts/flexbuffers/builder';
2
+ export { toReference } from 'flatbuffers/ts/flexbuffers/reference';
3
+
4
+ export function builder(): FlexBuilder {
5
+ return new FlexBuilder();
6
+ }
@@ -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
+ }