import {createHash, randomUUID} from 'crypto'; import {promises as FS} from 'fs'; import path from 'path'; import type {Source, TokenHash, TokenPK, TokenPair} from '@bitemap/core'; import type { Apis, ClientToServerEvents, ServerToClientEvents, SourceInitializeConfig, } from '@bitemap/server'; import {hc} from 'hono/client'; import type {Socket} from 'socket.io-client'; import {io} from 'socket.io-client'; import type {BitmapSource} from './@bitmap-source.js'; import {_BitmapSource} from './@bitmap-source.js'; import {TOKEN_PATH} from './@constants.js'; import {fileExists} from './utils/index.js'; export type ServerConfig = { host: string; port: number; tokenFileName?: string; }; export class Bitmap { private ready = this.initialize(); fetcher: ReturnType> = hc(this.httpURL); socket!: Socket; tokenPair!: TokenPair; private get baseURL(): string { return `http://${this.config.host}:${this.config.port}`; } private get httpURL(): string { return `${this.baseURL}/api`; } private get tokenFilePath(): string { if (this.config.tokenFileName) { return path.join(process.cwd(), this.config.tokenFileName); } else { return TOKEN_PATH; } } constructor(private config: ServerConfig) {} private async initialize(): Promise { const tokenPair = await this.ensureTokenFile(); await this.ensureTokenInitialized(tokenPair); this.tokenPair = tokenPair; await new Promise((resolve, reject) => { this.socket = io(this.baseURL, { auth: { token: tokenPair!.pk, }, }); this.socket.io.on('close', (reason, desc) => { console.error('[socket]', reason, desc); reject({ reason, desc, }); }); this.socket .on('connect', () => { console.info('[socket] connected', (this.socket as any).nsp); resolve(); }) .on('connect_error', error => { console.error('[socket] connect error: ', error.message); }) .on('disconnect', reason => { console.error('[socket] disconnect: ', reason); }); }); this.fetcher = hc(this.httpURL, { headers: { 'bitmap-token': tokenPair.pk, }, }); console.info('bitmap initialized'); } async createSource(config: SourceInitializeConfig): Promise { await this.ready; const result = await this.fetcher.source.initialize.$post({ json: config, }); if (!result.ok) { throw new Error( `Failed to connect to source, status code ${result.status}, ${await result.text()}`, ); } const source = await result.json(); const bitmapSource = new _BitmapSource(this, { ...config, id: source.id, }); return bitmapSource; } private async writeTokenToFile(token: TokenPair): Promise { await FS.writeFile(this.tokenFilePath, JSON.stringify(token, undefined, 2)); console.info(`Token saved to ${this.tokenFilePath}`); } private async ensureTokenFile(): Promise { if (await fileExists(this.tokenFilePath)) { return JSON.parse(await FS.readFile(this.tokenFilePath, 'utf-8')); } else { const tokenPair = await this.generateTokenPair(); await this.writeTokenToFile(tokenPair); return tokenPair; } } private async ensureTokenInitialized(tokenPair: TokenPair): Promise { try { await this.fetcher.token.initialize.$post({ json: tokenPair, }); } catch (error) { console.error('Failed to create token, retrying in 1s', error); await new Promise(resolve => setTimeout(resolve, 1000)); await this.ensureTokenInitialized(tokenPair); } } private async generateTokenPair(): Promise { const pk = randomUUID() as TokenPK; const hash = (await createHash('sha256') .update(pk) .digest('hex')) as unknown as TokenHash; return { pk, hash, }; } async listSources(): Promise { return (await this.fetcher.source.list.$get()).json(); } }