@jokio/sdk 0.0.1

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.
@@ -0,0 +1,325 @@
1
+ import {
2
+ connect,
3
+ DebugEvents,
4
+ Events,
5
+ JSONCodec,
6
+ type NatsConnection,
7
+ } from 'nats.ws'
8
+
9
+ const jsonCodec = JSONCodec()
10
+
11
+ export class NatsService<TApi> {
12
+ onStatusChange?: (isConnected: boolean) => void
13
+
14
+ private nc?: NatsConnection
15
+ private sessionId: string = ''
16
+ private userId: string = ''
17
+
18
+ private globalPrefix = ''
19
+
20
+ private resolveConnected = (_: unknown) => {}
21
+ isConnected = new Promise(
22
+ resolve => (this.resolveConnected = resolve),
23
+ )
24
+
25
+ async connect(
26
+ natsServerUrls: string[],
27
+ user: IdentityUser,
28
+ prefix?: string,
29
+ ) {
30
+ this.nc = await connect({
31
+ servers: natsServerUrls,
32
+ })
33
+
34
+ this.userId = user.userId
35
+ this.sessionId = user.sessionId
36
+
37
+ this.globalPrefix = prefix ?? ''
38
+
39
+ this.resolveConnected(true)
40
+
41
+ this.onStatusChange?.(true)
42
+ console.log(
43
+ '[nats] connected.',
44
+ this.globalPrefix ? `(prefix: ${this.globalPrefix})` : '',
45
+ )
46
+
47
+ // Monitor connection status
48
+ ;(async () => {
49
+ for await (const status of this.nc!.status()) {
50
+ if (status.type !== DebugEvents.PingTimer) {
51
+ console.log('[nats] connection status:', status.type)
52
+ }
53
+
54
+ switch (status.type) {
55
+ case Events.Reconnect:
56
+ this.onStatusChange?.(true)
57
+ break
58
+
59
+ case Events.Disconnect:
60
+ case Events.Error:
61
+ case DebugEvents.Reconnecting:
62
+ this.onStatusChange?.(false)
63
+ break
64
+ }
65
+ }
66
+ })()
67
+ }
68
+
69
+ async disconnect() {
70
+ await this.nc?.drain()
71
+ await this.nc?.close()
72
+ }
73
+
74
+ async publish<TSubject extends keyof TApi & string>(
75
+ subject: TSubject,
76
+ data: InferReturnType<TApi, TSubject>,
77
+ ) {
78
+ await this.isConnected
79
+
80
+ if (!this.nc) {
81
+ throw new Error('[nats] not connected')
82
+ }
83
+
84
+ try {
85
+ const finalSubject = this.buildSubject(subject)
86
+
87
+ console.debug('[nats]>>', finalSubject)
88
+
89
+ this.nc.publish(finalSubject, jsonCodec.encode(data))
90
+ } catch (error) {
91
+ console.error('[nats] publish failed:', error)
92
+ throw error
93
+ }
94
+ }
95
+
96
+ async call<TSubject extends keyof TApi & string>(
97
+ subject: TSubject,
98
+ data: InferProps<TApi, TSubject>,
99
+ opts?: { timeoutInMs?: number },
100
+ ): Promise<InferReturnType<TApi, TSubject>> {
101
+ await this.isConnected
102
+
103
+ if (!this.nc) {
104
+ throw new Error('[nats] not connected')
105
+ }
106
+
107
+ try {
108
+ const finalSubject = this.buildSubject(subject)
109
+
110
+ console.debug('[nats]>>', finalSubject)
111
+
112
+ const response = await this.nc.request(
113
+ finalSubject,
114
+ jsonCodec.encode(data),
115
+ { timeout: opts?.timeoutInMs ?? 5000 },
116
+ )
117
+ const result:
118
+ | { ok: true; data: any }
119
+ | { ok: false; message: string } = jsonCodec.decode(
120
+ response.data,
121
+ ) as any
122
+
123
+ if (!result.ok) {
124
+ throw new Error(result.message)
125
+ }
126
+
127
+ return result.data
128
+ } catch (error) {
129
+ console.error('[nats] request failed:', error)
130
+ throw new Error('Api call failed.')
131
+ }
132
+ }
133
+
134
+ async on<TSubject extends keyof TApi & string>(
135
+ subject: TSubject,
136
+ cb: (
137
+ data: InferReturnType<TApi, TSubject>,
138
+ ctx: {
139
+ subject: string
140
+ userId: string
141
+ sessionId: string
142
+ },
143
+ ) => void,
144
+ ) {
145
+ await this.isConnected
146
+
147
+ if (!this.nc) {
148
+ throw new Error('[nats] not connected')
149
+ }
150
+
151
+ const prefix = this.globalPrefix ? `${this.globalPrefix}.` : ''
152
+ const finalSubject = prefix + subject + '.>'
153
+
154
+ const subscription = this.nc!.subscribe(finalSubject)
155
+
156
+ ;(async () => {
157
+ for await (const message of subscription) {
158
+ try {
159
+ console.debug('[nats]<<', message.subject)
160
+
161
+ const subjectParts = message.subject.split('.')
162
+
163
+ // -- process prefix --
164
+ if (
165
+ this.globalPrefix &&
166
+ subjectParts[0] !== this.globalPrefix
167
+ ) {
168
+ console.log('[nats]', 'Skipped - ' + message.subject)
169
+ continue
170
+ }
171
+
172
+ if (subjectParts[0] === 'dev') {
173
+ // remove the first element / prefix
174
+ subjectParts.shift()
175
+ }
176
+ // -- process prefix --
177
+
178
+ const subject = subjectParts
179
+ .slice(0, subjectParts.length - 2)
180
+ .join('.')
181
+ const userId = subjectParts[subjectParts.length - 2]
182
+ const sessionId = subjectParts[subjectParts.length - 1]
183
+
184
+ const decodedMessage = jsonCodec.decode(message.data)
185
+
186
+ cb(decodedMessage as any, {
187
+ subject,
188
+ userId,
189
+ sessionId,
190
+ })
191
+ } catch (err) {
192
+ console.error('[nats] subscribe error', err)
193
+ }
194
+ }
195
+ })()
196
+
197
+ return () => subscription.unsubscribe()
198
+ }
199
+
200
+ async handler<TSubject extends keyof TApi & string>(
201
+ subject: TSubject,
202
+ cb: (
203
+ data: InferProps<TApi, TSubject>,
204
+ ctx: {
205
+ subject: string
206
+ userId: string
207
+ sessionId: string
208
+ },
209
+ ) => Promise<InferReturnType<TApi, TSubject>>,
210
+ ) {
211
+ await this.isConnected
212
+
213
+ if (!this.nc) {
214
+ throw new Error('[nats] not connected')
215
+ }
216
+
217
+ const prefix = this.globalPrefix ? `${this.globalPrefix}.` : ''
218
+ const finalSubject = prefix + subject + '.>'
219
+
220
+ const subscription = this.nc!.subscribe(finalSubject)
221
+
222
+ ;(async () => {
223
+ for await (const message of subscription) {
224
+ try {
225
+ console.debug('[nats]<<', message.subject)
226
+
227
+ const subjectParts = message.subject.split('.')
228
+
229
+ // -- process prefix --
230
+ if (
231
+ this.globalPrefix &&
232
+ subjectParts[0] !== this.globalPrefix
233
+ ) {
234
+ console.log('[nats]', 'Skipped - ' + message.subject)
235
+ continue
236
+ }
237
+
238
+ if (subjectParts[0] === 'dev') {
239
+ // remove the first element / prefix
240
+ subjectParts.shift()
241
+ }
242
+ // -- process prefix --
243
+
244
+ const subject = subjectParts
245
+ .slice(0, subjectParts.length - 2)
246
+ .join('.')
247
+ const userId = subjectParts[subjectParts.length - 2]
248
+ const sessionId = subjectParts[subjectParts.length - 1]
249
+
250
+ const decodedMessage = jsonCodec.decode(message.data)
251
+
252
+ cb(decodedMessage as any, {
253
+ subject,
254
+ userId,
255
+ sessionId,
256
+ })
257
+ .then(res => {
258
+ if (message.reply) {
259
+ message.respond(
260
+ jsonCodec.encode({
261
+ ok: true,
262
+ data: res,
263
+ }),
264
+ )
265
+ }
266
+ })
267
+ .catch(err => {
268
+ console.error('[nats] handle', err)
269
+
270
+ if (message.reply) {
271
+ message.respond(
272
+ jsonCodec.encode({
273
+ ok: false,
274
+ message: err.message,
275
+ }),
276
+ )
277
+ }
278
+ })
279
+ } catch (err: any) {
280
+ console.error('[nats] handle', err)
281
+
282
+ if (message.reply) {
283
+ message.respond(
284
+ jsonCodec.encode({
285
+ ok: false,
286
+ message: err.message,
287
+ }),
288
+ )
289
+ }
290
+ }
291
+ }
292
+ })()
293
+
294
+ return () => subscription.unsubscribe()
295
+ }
296
+
297
+ private buildSubject(subject: string) {
298
+ return [this.globalPrefix, subject, this.userId, this.sessionId]
299
+ .filter(x => x)
300
+ .join('.')
301
+ }
302
+ }
303
+
304
+ export const nats = new NatsService()
305
+
306
+ type IdentityUser = {
307
+ jwt: string
308
+ seed: string
309
+ userId: string
310
+ sessionId: string
311
+ }
312
+
313
+ type InferProps<
314
+ TApi,
315
+ TSubject extends keyof TApi,
316
+ > = TApi[TSubject] extends (args: infer TProps) => unknown
317
+ ? TProps
318
+ : {}
319
+
320
+ type InferReturnType<
321
+ TApi,
322
+ TSubject extends keyof TApi,
323
+ > = TApi[TSubject] extends (args: any) => infer TResult
324
+ ? TResult
325
+ : TApi[TSubject]
@@ -0,0 +1,94 @@
1
+ import { playAudioFromBuffer } from './common/playAudioFromBuffer'
2
+
3
+ type Config = {
4
+ voicevoxUrl: string
5
+ }
6
+
7
+ export class VoicevoxTtsService {
8
+ constructor(private config: Config) {}
9
+
10
+ async getAudioBuffer(text: string, voice: string) {
11
+ const audioQueryResponse = await fetch(
12
+ `${
13
+ this.config.voicevoxUrl
14
+ }/audio_query?text=${encodeURIComponent(text)}&speaker=${
15
+ voice || 3
16
+ }`,
17
+ {
18
+ method: 'POST',
19
+ headers: {
20
+ Accept: 'application/json',
21
+ },
22
+ },
23
+ )
24
+
25
+ if (!audioQueryResponse.ok) {
26
+ throw new Error(
27
+ `Audio query failed: ${audioQueryResponse.statusText}`,
28
+ )
29
+ }
30
+
31
+ const audioQuery = await audioQueryResponse.json()
32
+
33
+ // Step 2: Synthesize the speech
34
+ const synthesisResponse = await fetch(
35
+ `${this.config.voicevoxUrl}/synthesis?speaker=${voice}`,
36
+ {
37
+ method: 'POST',
38
+ headers: {
39
+ Accept: 'audio/wav',
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ body: JSON.stringify(audioQuery),
43
+ },
44
+ )
45
+
46
+ if (!synthesisResponse.body) {
47
+ return null
48
+ }
49
+
50
+ const reader = synthesisResponse.body.getReader()
51
+ const chunks = []
52
+
53
+ // Step 1: Read all the chunks
54
+ while (true) {
55
+ const { done, value } = await reader.read()
56
+ if (done) break
57
+ chunks.push(value)
58
+ }
59
+
60
+ // Step 2: Concatenate into one Uint8Array
61
+ const totalLength = chunks.reduce(
62
+ (acc, chunk) => acc + chunk.length,
63
+ 0,
64
+ )
65
+ const audioData = new Uint8Array(totalLength)
66
+ let offset = 0
67
+ for (const chunk of chunks) {
68
+ audioData.set(chunk, offset)
69
+ offset += chunk.length
70
+ }
71
+
72
+ return audioData.buffer
73
+ }
74
+
75
+ async getAudio(text: string, voice: string) {
76
+ return this.getAudioBuffer(text, voice).then(x =>
77
+ x ? playAudioFromBuffer(x) : null,
78
+ )
79
+ }
80
+
81
+ async getVoices(): Promise<any[]> {
82
+ const speakers = await fetch(
83
+ `${this.config.voicevoxUrl}/speakers`,
84
+ ).then(x => x.json())
85
+
86
+ return speakers.flatMap((x: any) =>
87
+ x.styles.map((y: any) => ({
88
+ id: y.id.toString(),
89
+ locale: 'ja-JP',
90
+ description: `${x.name} - ${y.name}`,
91
+ })),
92
+ )
93
+ }
94
+ }
@@ -0,0 +1,63 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <title>MyLibrary Test</title>
7
+ </head>
8
+
9
+ <body>
10
+ <h1>Library Test</h1>
11
+
12
+ <button onclick="playAudio()">Edge TTS</button>
13
+
14
+ <button onclick="playAudio2()">Voicevox TTS</button>
15
+
16
+ <script type="module">
17
+ import { jok } from './dist/jokio.sdk.es.js';
18
+ window.jok = jok
19
+ // jok.voicevoxTts.getVoices().then(console.log)
20
+ // document.body.innerHTML += `<p>${}</p>`;
21
+
22
+ window.playAudio = async function () {
23
+ const audio = await jok.edgeTts.getAudio("Hello there, how are you?", "en-US-AvaNeural")
24
+
25
+ audio.play()
26
+ }
27
+
28
+
29
+ window.playAudio2 = async function () {
30
+ const audio = await jok.voicevoxTts.getAudio("こんにちわ、おげんきですか?あなたはあたまがいいですね。", 3)
31
+
32
+ audio.play()
33
+ }
34
+
35
+
36
+ window.p2pChat = async function () {
37
+ const k1 = await jok.crypto.createSessionKeyPair()
38
+ const k2 = await jok.crypto.createSessionKeyPair()
39
+
40
+ const p1 = await jok.crypto.exportPublicKey(k1.publicKey)
41
+ const p2 = await jok.crypto.exportPublicKey(k2.publicKey)
42
+
43
+ console.log({ p1, p2 })
44
+
45
+ const shared1 = await jok.crypto.deriveAESKey(k1.privateKey, p2)
46
+ const shared2 = await jok.crypto.deriveAESKey(k2.privateKey, p1)
47
+
48
+ const originalText = "Hello World"
49
+
50
+ const encrypted = await jok.crypto.encrypt(originalText, shared1)
51
+ const decrypted = await jok.crypto.decrypt(encrypted, shared2)
52
+
53
+ console.log({
54
+ match: originalText === decrypted,
55
+ originalText,
56
+ decrypted,
57
+ })
58
+ }
59
+ </script>
60
+
61
+ </body>
62
+
63
+ </html>
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "declaration": true,
6
+ "emitDeclarationOnly": true,
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "moduleResolution": "Node",
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,23 @@
1
+ import path from 'path'
2
+ import { defineConfig } from 'vite'
3
+
4
+ export default defineConfig({
5
+ publicDir: "test",
6
+ server: {
7
+ open: '/index.html' // or 'test/index.html' if kept in /test
8
+ },
9
+ build: {
10
+ lib: {
11
+ entry: path.resolve(__dirname, 'src/index.ts'),
12
+ name: '@jokio/sdk',
13
+ fileName: format => `jokio.sdk.${format}.js`,
14
+ },
15
+ // rollupOptions: {
16
+ // output: {
17
+ // globals: {
18
+ // // Example: react: 'React'
19
+ // },
20
+ // },
21
+ // },
22
+ },
23
+ })