izteamslots 1.1.0

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,162 @@
1
+ import { spawn, type ChildProcessByStdio } from "node:child_process"
2
+ import type { Readable, Writable } from "node:stream"
3
+ import { randomUUID } from "node:crypto"
4
+ import { dirname, resolve } from "node:path"
5
+ import { createInterface } from "node:readline"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ type RpcEvent = {
9
+ type: "event"
10
+ event: string
11
+ data: Record<string, unknown>
12
+ }
13
+
14
+ type RpcResponse = {
15
+ type: "response"
16
+ id: string
17
+ ok: boolean
18
+ result?: unknown
19
+ error?: { code: number; message: string; data?: unknown }
20
+ }
21
+
22
+ export type EventHandler = (event: RpcEvent) => void
23
+ export type ErrorOutputHandler = (line: string) => void
24
+
25
+ function projectRootFromCurrentFile() {
26
+ const here = dirname(fileURLToPath(import.meta.url))
27
+ return resolve(here, "../../..")
28
+ }
29
+
30
+ export class StdioRpcClient {
31
+ private proc: ChildProcessByStdio<Writable, Readable, Readable> | null = null
32
+ private readonly pending = new Map<
33
+ string,
34
+ {
35
+ resolve: (value: unknown) => void
36
+ reject: (reason?: unknown) => void
37
+ }
38
+ >()
39
+ private readonly eventHandlers = new Set<EventHandler>()
40
+ private readonly errorHandlers = new Set<ErrorOutputHandler>()
41
+
42
+ constructor(
43
+ private readonly pythonCmd: string =
44
+ process.env.PYTHON_BIN ?? process.env.PYTHON ?? "python3",
45
+ private readonly projectRoot: string = process.env.IZTEAMSLOTS_ROOT ?? projectRootFromCurrentFile(),
46
+ ) {}
47
+
48
+ start() {
49
+ if (this.proc) return
50
+
51
+ this.proc = spawn(this.pythonCmd, ["-m", "backend"], {
52
+ stdio: ["pipe", "pipe", "pipe"],
53
+ cwd: this.projectRoot,
54
+ env: {
55
+ ...process.env,
56
+ PYTHONPATH: process.env.PYTHONPATH
57
+ ? `${this.projectRoot}:${process.env.PYTHONPATH}`
58
+ : this.projectRoot,
59
+ },
60
+ })
61
+
62
+ const proc = this.proc
63
+ if (!proc) {
64
+ throw new Error("RPC process did not start")
65
+ }
66
+
67
+ proc.on("error", (err) => {
68
+ this.proc = null
69
+ for (const [, p] of this.pending) {
70
+ p.reject(new Error(`RPC spawn error: ${err.message}`))
71
+ }
72
+ this.pending.clear()
73
+ })
74
+
75
+ const rl = createInterface({ input: proc.stdout })
76
+ const stderr = createInterface({ input: proc.stderr })
77
+
78
+ stderr.on("line", (line) => {
79
+ const message = line.trim()
80
+ if (!message) return
81
+ for (const handler of this.errorHandlers) handler(message)
82
+ })
83
+
84
+ rl.on("line", (line) => {
85
+ if (!line.trim()) return
86
+ let payload: RpcEvent | RpcResponse
87
+ try {
88
+ payload = JSON.parse(line) as RpcEvent | RpcResponse
89
+ } catch {
90
+ return
91
+ }
92
+
93
+ if (payload.type === "event") {
94
+ for (const handler of this.eventHandlers) handler(payload)
95
+ return
96
+ }
97
+
98
+ const entry = this.pending.get(payload.id)
99
+ if (!entry) return
100
+ this.pending.delete(payload.id)
101
+ if (payload.ok) {
102
+ entry.resolve(payload.result)
103
+ } else {
104
+ entry.reject(new Error(payload.error?.message ?? "RPC error"))
105
+ }
106
+ })
107
+
108
+ proc.on("exit", (code, signal) => {
109
+ this.proc = null
110
+ const suffix = signal ? `signal ${signal}` : `code ${String(code ?? "unknown")}`
111
+ for (const handler of this.errorHandlers) handler(`RPC backend остановлен (${suffix})`)
112
+ for (const [id, p] of this.pending) {
113
+ p.reject(new Error(`RPC process exited before response: ${id}`))
114
+ }
115
+ this.pending.clear()
116
+ })
117
+ }
118
+
119
+ onEvent(handler: EventHandler): () => void {
120
+ this.eventHandlers.add(handler)
121
+ return () => this.eventHandlers.delete(handler)
122
+ }
123
+
124
+ onErrorOutput(handler: ErrorOutputHandler): () => void {
125
+ this.errorHandlers.add(handler)
126
+ return () => this.errorHandlers.delete(handler)
127
+ }
128
+
129
+ async request<T>(method: string, params: Record<string, unknown> = {}): Promise<T> {
130
+ this.start()
131
+ const id = randomUUID()
132
+ const message = JSON.stringify({ id, method, params })
133
+
134
+ if (!this.proc) {
135
+ throw new Error("RPC process is not running")
136
+ }
137
+
138
+ return await new Promise<T>((resolve, reject) => {
139
+ this.pending.set(id, {
140
+ resolve: (value) => resolve(value as T),
141
+ reject,
142
+ })
143
+ if (!this.proc) {
144
+ reject(new Error("RPC process is not running"))
145
+ return
146
+ }
147
+ this.proc.stdin.write(message + "\n")
148
+ })
149
+ }
150
+
151
+ async shutdown() {
152
+ try {
153
+ await this.request("shutdown")
154
+ } catch {
155
+ // no-op
156
+ }
157
+ if (this.proc) {
158
+ this.proc.kill()
159
+ this.proc = null
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "types": ["node", "bun"]
11
+ },
12
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
13
+ }