@tapicker/bridge 1.0.0 → 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.
Files changed (49) hide show
  1. package/README.md +15 -15
  2. package/dist/bridge/bridge-service.d.ts +1 -1
  3. package/dist/bridge/bridge-service.js +7 -7
  4. package/dist/bridge/control.js +1 -1
  5. package/dist/bridge/export-store.d.ts +35 -0
  6. package/dist/bridge/export-store.js +195 -0
  7. package/dist/bridge/extension-session.d.ts +9 -2
  8. package/dist/bridge/extension-session.js +56 -8
  9. package/dist/cli.js +563 -148
  10. package/dist/commands.d.ts +8 -0
  11. package/dist/commands.js +276 -40
  12. package/dist/constants.d.ts +2 -2
  13. package/dist/constants.js +4 -2
  14. package/dist/http-client.d.ts +24 -0
  15. package/dist/http-client.js +101 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +2 -2
  18. package/dist/mcp.d.ts +4 -3
  19. package/dist/mcp.js +221 -134
  20. package/dist/protocol/schema.d.ts +503 -34
  21. package/dist/protocol/schema.js +396 -15
  22. package/dist/server.d.ts +7 -4
  23. package/dist/server.js +70 -26
  24. package/dist/update-notifier-worker.d.ts +2 -0
  25. package/dist/update-notifier-worker.js +3 -0
  26. package/dist/update-notifier.d.ts +9 -0
  27. package/dist/update-notifier.js +178 -0
  28. package/dist/workflow-watch.d.ts +8 -7
  29. package/dist/workflow-watch.js +39 -29
  30. package/package.json +16 -6
  31. package/docs/cli.md +0 -112
  32. package/docs/http-api.md +0 -149
  33. package/docs/mcp.md +0 -90
  34. package/docs/operations.md +0 -85
  35. package/docs/workflow-watch.md +0 -86
  36. package/src/bridge/bridge-service.ts +0 -291
  37. package/src/bridge/control.ts +0 -281
  38. package/src/bridge/daemon.ts +0 -102
  39. package/src/bridge/extension-session.ts +0 -286
  40. package/src/bridge/pairing-state.ts +0 -92
  41. package/src/cli.ts +0 -1019
  42. package/src/commands.ts +0 -158
  43. package/src/constants.ts +0 -10
  44. package/src/errors.ts +0 -72
  45. package/src/index.ts +0 -35
  46. package/src/mcp.ts +0 -386
  47. package/src/protocol/schema.ts +0 -202
  48. package/src/server.ts +0 -623
  49. package/src/workflow-watch.ts +0 -364
@@ -1,281 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
- import { chmod, mkdir, unlink } from 'node:fs/promises'
3
- import { createConnection, createServer } from 'node:net'
4
- import type { Server, Socket } from 'node:net'
5
- import { join, resolve } from 'node:path'
6
- import { BridgeError, toBridgeError } from '../errors.js'
7
-
8
- export type BridgeControlAction = 'code.get' | 'code.refresh' | 'extensions.list' | 'bridge.stop'
9
-
10
- const CONTROL_ACTIONS = new Set<BridgeControlAction>(['code.get', 'code.refresh', 'extensions.list', 'bridge.stop'])
11
- const MAX_CONTROL_MESSAGE_BYTES = 4096
12
-
13
- export function getBridgeControlPath(dataDir: string) {
14
- const normalizedDataDir = resolve(dataDir)
15
- if (process.platform === 'win32') {
16
- const suffix = createHash('sha256').update(normalizedDataDir.toLowerCase()).digest('hex').slice(0, 24)
17
- return `\\\\.\\pipe\\tapicker-bridge-${suffix}`
18
- }
19
- return join(normalizedDataDir, 'bridge.sock')
20
- }
21
-
22
- export async function startBridgeControlServer(
23
- dataDir: string,
24
- handle: (action: BridgeControlAction) => Promise<unknown>,
25
- ) {
26
- await mkdir(dataDir, { recursive: true, mode: 0o700 })
27
- const endpoint = getBridgeControlPath(dataDir)
28
- const server = createServer((socket) => handleControlSocket(socket, handle))
29
-
30
- try {
31
- await listenControlServer(server, endpoint)
32
- if (process.platform !== 'win32') {
33
- await chmod(endpoint, 0o600)
34
- }
35
- } catch (error) {
36
- await closeServer(server)
37
- throw error
38
- }
39
-
40
- return {
41
- async close() {
42
- await closeServer(server)
43
- if (process.platform !== 'win32') {
44
- await unlink(endpoint).catch((error) => {
45
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
46
- })
47
- }
48
- },
49
- }
50
- }
51
-
52
- export async function sendBridgeControlRequest<T>(dataDir: string, action: BridgeControlAction): Promise<T> {
53
- const endpoint = getBridgeControlPath(dataDir)
54
-
55
- return new Promise<T>((resolveResponse, rejectResponse) => {
56
- const socket = createConnection(endpoint)
57
- let buffer = ''
58
- let settled = false
59
-
60
- const finish = (callback: () => void) => {
61
- if (settled) return
62
- settled = true
63
- clearTimeout(timeout)
64
- socket.destroy()
65
- callback()
66
- }
67
-
68
- const timeout = setTimeout(() => {
69
- finish(() =>
70
- rejectResponse(
71
- new BridgeError('BridgeControlTimeout', 'Timed out communicating with Tapicker Bridge', { status: 504 }),
72
- ),
73
- )
74
- }, 3000)
75
-
76
- socket.setEncoding('utf8')
77
- socket.once('connect', () => socket.write(`${JSON.stringify({ action })}\n`))
78
- socket.on('data', (chunk: string) => {
79
- buffer += chunk
80
- if (Buffer.byteLength(buffer) > MAX_CONTROL_MESSAGE_BYTES) {
81
- finish(() =>
82
- rejectResponse(new BridgeError('InvalidBridgeControlResponse', 'Bridge control response is too large')),
83
- )
84
- return
85
- }
86
-
87
- const newline = buffer.indexOf('\n')
88
- if (newline < 0) return
89
-
90
- let response: unknown
91
- try {
92
- response = JSON.parse(buffer.slice(0, newline))
93
- } catch (cause) {
94
- finish(() =>
95
- rejectResponse(
96
- new BridgeError('InvalidBridgeControlResponse', 'Bridge returned an invalid control response', {
97
- status: 502,
98
- cause,
99
- }),
100
- ),
101
- )
102
- return
103
- }
104
-
105
- if (!response || typeof response !== 'object') {
106
- finish(() =>
107
- rejectResponse(
108
- new BridgeError('InvalidBridgeControlResponse', 'Bridge returned an invalid control response', {
109
- status: 502,
110
- }),
111
- ),
112
- )
113
- return
114
- }
115
-
116
- const result = response as {
117
- ok?: unknown
118
- data?: unknown
119
- error?: { code?: unknown; message?: unknown; status?: unknown }
120
- }
121
- if (result.ok === true) {
122
- finish(() => resolveResponse(result.data as T))
123
- return
124
- }
125
-
126
- const error = result.error
127
- finish(() =>
128
- rejectResponse(
129
- new BridgeError(
130
- typeof error?.code === 'string' ? error.code : 'BridgeControlFailed',
131
- typeof error?.message === 'string' ? error.message : 'Bridge control request failed',
132
- { status: typeof error?.status === 'number' ? error.status : 500 },
133
- ),
134
- ),
135
- )
136
- })
137
- socket.once('error', (cause) => {
138
- const code = (cause as NodeJS.ErrnoException).code
139
- const isUnavailable = code === 'ENOENT' || code === 'ECONNREFUSED' || code === 'EINVAL'
140
- finish(() =>
141
- rejectResponse(
142
- new BridgeError(
143
- isUnavailable ? 'BridgeControlUnavailable' : 'BridgeControlFailed',
144
- isUnavailable
145
- ? 'No Tapicker Bridge control service is available'
146
- : 'Could not communicate with Tapicker Bridge',
147
- { status: isUnavailable ? 503 : 500, cause },
148
- ),
149
- ),
150
- )
151
- })
152
- socket.once('end', () => {
153
- if (!settled) {
154
- finish(() =>
155
- rejectResponse(
156
- new BridgeError('InvalidBridgeControlResponse', 'Bridge closed the control connection without a response', {
157
- status: 502,
158
- }),
159
- ),
160
- )
161
- }
162
- })
163
- })
164
- }
165
-
166
- function handleControlSocket(socket: Socket, handle: (action: BridgeControlAction) => Promise<unknown>) {
167
- let buffer = ''
168
- socket.setEncoding('utf8')
169
- socket.setTimeout(5000, () => socket.destroy())
170
- socket.on('data', (chunk: string) => {
171
- buffer += chunk
172
- if (Buffer.byteLength(buffer) > MAX_CONTROL_MESSAGE_BYTES) {
173
- socket.destroy()
174
- return
175
- }
176
-
177
- const newline = buffer.indexOf('\n')
178
- if (newline < 0) return
179
-
180
- socket.removeAllListeners('data')
181
- let action: unknown
182
- try {
183
- const message = JSON.parse(buffer.slice(0, newline)) as { action?: unknown }
184
- action = message?.action
185
- } catch (cause) {
186
- sendControlError(
187
- socket,
188
- new BridgeError('InvalidBridgeControlRequest', 'Control request is not valid JSON', { status: 400, cause }),
189
- )
190
- return
191
- }
192
-
193
- if (typeof action !== 'string' || !CONTROL_ACTIONS.has(action as BridgeControlAction)) {
194
- sendControlError(
195
- socket,
196
- new BridgeError('InvalidBridgeControlRequest', 'Unsupported Bridge control action', { status: 400 }),
197
- )
198
- return
199
- }
200
-
201
- void handle(action as BridgeControlAction)
202
- .then((data) => socket.end(`${JSON.stringify({ ok: true, data })}\n`))
203
- .catch((error) => sendControlError(socket, error))
204
- })
205
- }
206
-
207
- function sendControlError(socket: Socket, error: unknown) {
208
- const bridgeError = toBridgeError(error)
209
- socket.end(
210
- `${JSON.stringify({
211
- ok: false,
212
- error: {
213
- code: bridgeError.code,
214
- message: bridgeError.message,
215
- status: bridgeError.status,
216
- },
217
- })}\n`,
218
- )
219
- }
220
-
221
- function listenControlServer(server: Server, endpoint: string): Promise<void> {
222
- return listen(server, endpoint).catch(async (error) => {
223
- if ((error as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw error
224
- if (await canConnect(endpoint)) {
225
- throw new BridgeError(
226
- 'BridgeControlInUse',
227
- 'A Tapicker Bridge control service is already using this data directory',
228
- {
229
- status: 409,
230
- cause: error,
231
- },
232
- )
233
- }
234
-
235
- if (process.platform === 'win32') throw error
236
- await unlink(endpoint).catch((unlinkError) => {
237
- if ((unlinkError as NodeJS.ErrnoException).code !== 'ENOENT') throw unlinkError
238
- })
239
- await listen(server, endpoint)
240
- })
241
- }
242
-
243
- function listen(server: Server, endpoint: string) {
244
- return new Promise<void>((resolveListen, rejectListen) => {
245
- const onError = (error: Error) => {
246
- server.off('listening', onListening)
247
- rejectListen(error)
248
- }
249
- const onListening = () => {
250
- server.off('error', onError)
251
- resolveListen()
252
- }
253
- server.once('error', onError)
254
- server.once('listening', onListening)
255
- server.listen(endpoint)
256
- })
257
- }
258
-
259
- function canConnect(endpoint: string) {
260
- return new Promise<boolean>((resolveProbe) => {
261
- const socket = createConnection(endpoint)
262
- const timeout = setTimeout(() => {
263
- socket.destroy()
264
- resolveProbe(false)
265
- }, 300)
266
- socket.once('connect', () => {
267
- clearTimeout(timeout)
268
- socket.end()
269
- resolveProbe(true)
270
- })
271
- socket.once('error', () => {
272
- clearTimeout(timeout)
273
- resolveProbe(false)
274
- })
275
- })
276
- }
277
-
278
- function closeServer(server: Server) {
279
- if (!server.listening) return Promise.resolve()
280
- return new Promise<void>((resolveClose) => server.close(() => resolveClose()))
281
- }
@@ -1,102 +0,0 @@
1
- import { randomUUID } from 'node:crypto'
2
- import { mkdir, open, readFile, rename, unlink } from 'node:fs/promises'
3
- import { dirname, join } from 'node:path'
4
- import { z } from 'zod'
5
- import { BridgeError } from '../errors.js'
6
-
7
- const BridgeDaemonStateSchema = z
8
- .object({
9
- pid: z.number().int().positive(),
10
- bridgeId: z.uuid(),
11
- httpUrl: z.url(),
12
- startedAt: z.number().int().positive(),
13
- })
14
- .strict()
15
-
16
- export type BridgeDaemonState = z.infer<typeof BridgeDaemonStateSchema>
17
-
18
- export function getBridgeDaemonStatePath(dataDir: string) {
19
- return join(dataDir, 'bridge.pid')
20
- }
21
-
22
- export function getBridgeDaemonLogPath(dataDir: string) {
23
- return join(dataDir, 'bridge.log')
24
- }
25
-
26
- export async function readBridgeDaemonState(dataDir: string): Promise<BridgeDaemonState | undefined> {
27
- const filePath = getBridgeDaemonStatePath(dataDir)
28
- let content: string
29
-
30
- try {
31
- content = await readFile(filePath, 'utf8')
32
- } catch (error) {
33
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
34
- throw error
35
- }
36
-
37
- let value: unknown
38
- try {
39
- value = JSON.parse(content)
40
- } catch (cause) {
41
- throw new BridgeError('InvalidDaemonState', 'Saved Bridge process state is not valid JSON', {
42
- status: 500,
43
- cause,
44
- })
45
- }
46
-
47
- const parsed = BridgeDaemonStateSchema.safeParse(value)
48
- if (!parsed.success) {
49
- throw new BridgeError('InvalidDaemonState', 'Saved Bridge process state has an unsupported format', {
50
- status: 500,
51
- details: parsed.error.issues,
52
- })
53
- }
54
-
55
- return parsed.data
56
- }
57
-
58
- export async function writeBridgeDaemonState(dataDir: string, state: BridgeDaemonState) {
59
- const filePath = getBridgeDaemonStatePath(dataDir)
60
- const directory = dirname(filePath)
61
- await mkdir(directory, { recursive: true, mode: 0o700 })
62
-
63
- const temporaryPath = join(directory, `.bridge-${process.pid}-${randomUUID()}.tmp`)
64
- const content = JSON.stringify(state, null, 2)
65
- let file: Awaited<ReturnType<typeof open>> | undefined
66
-
67
- try {
68
- file = await open(temporaryPath, 'wx', 0o600)
69
- await file.writeFile(content, 'utf8')
70
- await file.sync()
71
- await file.close()
72
- file = undefined
73
- await rename(temporaryPath, filePath)
74
- } catch (error) {
75
- await file?.close().catch(() => undefined)
76
- await unlink(temporaryPath).catch(() => undefined)
77
- throw error
78
- }
79
- }
80
-
81
- export async function removeBridgeDaemonState(dataDir: string, expectedPid: number) {
82
- const state = await readBridgeDaemonState(dataDir)
83
- if (state?.pid !== expectedPid) return
84
-
85
- try {
86
- await unlink(getBridgeDaemonStatePath(dataDir))
87
- } catch (error) {
88
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
89
- }
90
- }
91
-
92
- export function isProcessRunning(pid: number) {
93
- try {
94
- process.kill(pid, 0)
95
- return true
96
- } catch (error) {
97
- const code = (error as NodeJS.ErrnoException).code
98
- if (code === 'ESRCH') return false
99
- if (code === 'EPERM') return true
100
- throw error
101
- }
102
- }
@@ -1,286 +0,0 @@
1
- import { EventEmitter } from 'node:events'
2
- import type { WebSocket } from 'ws'
3
- import { DEFAULT_REQUEST_TIMEOUT, PROTOCOL_VERSION } from '../constants.js'
4
- import { BridgeError, safeError, safeSuccess } from '../errors.js'
5
- import {
6
- BridgeHelloDataSchema,
7
- BridgePairDataSchema,
8
- ExtensionHelloInputSchema,
9
- ExtensionPairInputSchema,
10
- SafeResultSchema,
11
- createEnvelope,
12
- parseEnvelope,
13
- } from '../protocol/schema.js'
14
- import type { ExtensionHelloInput, ExtensionPairInput, MessageEnvelope } from '../protocol/schema.js'
15
-
16
- export type ExtensionSessionStatus = {
17
- connected: boolean
18
- ready: boolean
19
- pairingRequired?: boolean
20
- clientId?: string
21
- clientName?: string
22
- capabilities: string[]
23
- }
24
-
25
- export type ExtensionSessionOptions = {
26
- bridgeVersion: string
27
- protocolVersion: number
28
- capabilities: string[]
29
- remoteAddress: string
30
- verifyCredential: (clientId: string, credential: string) => boolean
31
- pair: (input: ExtensionPairInput, remoteAddress: string) => Promise<string>
32
- }
33
-
34
- type PendingRequest = {
35
- type: string
36
- timer: ReturnType<typeof setTimeout>
37
- resolve: (value: unknown) => void
38
- reject: (error: unknown) => void
39
- }
40
-
41
- export class ExtensionSession extends EventEmitter {
42
- private ready = false
43
- private pairingRequired = false
44
- private clientId: string | undefined
45
- private clientName: string | undefined
46
- private capabilities: string[] = []
47
- private pendingRequests = new Map<string, PendingRequest>()
48
-
49
- constructor(
50
- private readonly socket: WebSocket,
51
- private readonly options: ExtensionSessionOptions,
52
- ) {
53
- super()
54
- }
55
-
56
- start() {
57
- this.socket.on('message', (data) => void this.handleMessage(data).catch((error) => this.reportError(error)))
58
- this.socket.on('error', (error) => this.reportError(error))
59
- this.socket.on('close', (code, reason) => {
60
- this.rejectPending(new BridgeError('ExtensionDisconnected', 'Tapicker Extension disconnected', { status: 503 }))
61
- this.emit('close', { code, reason: reason.toString() })
62
- })
63
- }
64
-
65
- getStatus(): ExtensionSessionStatus {
66
- return {
67
- connected: this.socket.readyState === this.socket.OPEN,
68
- ready: this.ready,
69
- pairingRequired: this.pairingRequired,
70
- clientId: this.clientId,
71
- clientName: this.clientName,
72
- capabilities: this.ready ? this.capabilities : [],
73
- }
74
- }
75
-
76
- request<T>(type: string, data: unknown, timeout = DEFAULT_REQUEST_TIMEOUT): Promise<T> {
77
- if (!this.ready || this.socket.readyState !== this.socket.OPEN) {
78
- return Promise.reject(
79
- new BridgeError('ExtensionUnavailable', 'No paired Tapicker Extension is connected', { status: 503 }),
80
- )
81
- }
82
-
83
- const envelope = createEnvelope(type, data)
84
- const responseType = `${type}:res`
85
-
86
- return new Promise<T>((resolve, reject) => {
87
- const timer = setTimeout(() => {
88
- this.pendingRequests.delete(envelope.id)
89
- reject(
90
- new BridgeError('ExtensionRequestTimeout', `Extension did not respond to ${type}`, {
91
- status: 504,
92
- details: { timeout },
93
- }),
94
- )
95
- }, timeout)
96
-
97
- this.pendingRequests.set(envelope.id, {
98
- type: responseType,
99
- timer,
100
- resolve: (value) => resolve(value as T),
101
- reject,
102
- })
103
-
104
- try {
105
- this.send(envelope)
106
- } catch (error) {
107
- clearTimeout(timer)
108
- this.pendingRequests.delete(envelope.id)
109
- reject(error)
110
- }
111
- })
112
- }
113
-
114
- close(code = 1000, reason = 'Bridge session closed') {
115
- if (this.socket.readyState === this.socket.OPEN || this.socket.readyState === this.socket.CONNECTING) {
116
- this.socket.close(code, reason)
117
- }
118
- }
119
-
120
- private async handleMessage(raw: unknown) {
121
- const parsed = parseEnvelope(raw)
122
- if (!parsed.success) {
123
- this.reportError(parsed.error)
124
- return
125
- }
126
-
127
- const message = parsed.data
128
- if (message.type === 'pong') return
129
-
130
- const pending = this.pendingRequests.get(message.id)
131
- if (pending && pending.type === message.type) {
132
- this.pendingRequests.delete(message.id)
133
- clearTimeout(pending.timer)
134
- const result = SafeResultSchema.safeParse(message.data)
135
- if (!result.success) {
136
- pending.reject(
137
- new BridgeError('InvalidExtensionResponse', 'Extension returned an invalid response', { status: 502 }),
138
- )
139
- } else if (result.data.ok) {
140
- pending.resolve(result.data.data)
141
- } else {
142
- pending.reject(
143
- new BridgeError(result.data.error.code, result.data.error.message, {
144
- status: 502,
145
- details: result.data.error.details,
146
- }),
147
- )
148
- }
149
- return
150
- }
151
-
152
- if (message.type === 'ping') {
153
- this.respond(message, 'pong', undefined)
154
- return
155
- }
156
-
157
- if (message.type === 'extension.hello') {
158
- await this.handleHello(message)
159
- return
160
- }
161
-
162
- if (message.type === 'extension.pair') {
163
- await this.handlePair(message)
164
- return
165
- }
166
-
167
- this.emit('message', message)
168
- }
169
-
170
- private async handleHello(message: MessageEnvelope) {
171
- const parsed = ExtensionHelloInputSchema.safeParse(message.data)
172
- if (!parsed.success) {
173
- this.respond(
174
- message,
175
- `${message.type}:res`,
176
- safeError(
177
- new BridgeError('InvalidHello', 'Invalid Extension hello', {
178
- status: 400,
179
- details: parsed.error.issues,
180
- }),
181
- ),
182
- )
183
- return
184
- }
185
-
186
- const hello = parsed.data
187
- this.clientId = hello.clientId
188
- this.clientName = hello.clientName
189
- this.capabilities = hello.capabilities
190
-
191
- const accepted = Boolean(hello.credential && this.options.verifyCredential(hello.clientId, hello.credential))
192
- this.pairingRequired = !accepted
193
- const response = BridgeHelloDataSchema.parse({
194
- accepted,
195
- requiresPairing: !accepted,
196
- bridgeVersion: this.options.bridgeVersion,
197
- protocolVersion: this.options.protocolVersion,
198
- capabilities: this.options.capabilities,
199
- })
200
-
201
- this.respond(message, `${message.type}:res`, safeSuccess(response))
202
- if (accepted) this.authorize(hello)
203
- }
204
-
205
- private async handlePair(message: MessageEnvelope) {
206
- const parsed = ExtensionPairInputSchema.safeParse(message.data)
207
- if (!parsed.success) {
208
- this.respond(
209
- message,
210
- `${message.type}:res`,
211
- safeError(
212
- new BridgeError('InvalidPairingRequest', 'Invalid pairing request', {
213
- status: 400,
214
- details: parsed.error.issues,
215
- }),
216
- ),
217
- )
218
- return
219
- }
220
-
221
- const input = parsed.data
222
- if (!this.clientId || input.clientId !== this.clientId || input.clientName !== this.clientName) {
223
- this.respond(
224
- message,
225
- `${message.type}:res`,
226
- safeError(
227
- new BridgeError('PairingSessionMismatch', 'Pairing request does not match the Extension hello', {
228
- status: 400,
229
- }),
230
- ),
231
- )
232
- return
233
- }
234
-
235
- try {
236
- const credential = await this.options.pair(input, this.options.remoteAddress)
237
- const response = BridgePairDataSchema.parse({
238
- accepted: true,
239
- clientId: input.clientId,
240
- credential,
241
- })
242
- this.respond(message, `${message.type}:res`, safeSuccess(response))
243
- this.authorize({ ...input, capabilities: this.capabilities })
244
- } catch (error) {
245
- this.respond(message, `${message.type}:res`, safeError(error))
246
- }
247
- }
248
-
249
- private authorize(hello: Pick<ExtensionHelloInput, 'clientId' | 'clientName' | 'capabilities'>) {
250
- this.ready = true
251
- this.pairingRequired = false
252
- this.clientId = hello.clientId
253
- this.clientName = hello.clientName
254
- this.capabilities = hello.capabilities
255
- this.emit('ready', {
256
- clientId: this.clientId,
257
- clientName: this.clientName,
258
- capabilities: this.capabilities,
259
- })
260
- }
261
-
262
- private respond(request: MessageEnvelope, type: string, data: unknown) {
263
- this.send(createEnvelope(type, data, { id: request.id, callback: false }))
264
- }
265
-
266
- private send(envelope: MessageEnvelope) {
267
- if (this.socket.readyState !== this.socket.OPEN) {
268
- throw new BridgeError('ExtensionUnavailable', 'Tapicker Extension WebSocket is not open', { status: 503 })
269
- }
270
- this.socket.send(JSON.stringify(envelope))
271
- }
272
-
273
- private rejectPending(error: unknown) {
274
- for (const pending of this.pendingRequests.values()) {
275
- clearTimeout(pending.timer)
276
- pending.reject(error)
277
- }
278
- this.pendingRequests.clear()
279
- }
280
-
281
- private reportError(error: unknown) {
282
- if (this.listenerCount('error') > 0) {
283
- this.emit('error', error)
284
- }
285
- }
286
- }