@pikku/core 0.12.1 → 0.12.2

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 (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/middleware-runner.js +1 -0
  3. package/dist/permissions.js +1 -0
  4. package/dist/pikku-state.js +4 -0
  5. package/dist/services/gateway-service.d.ts +19 -0
  6. package/dist/services/gateway-service.js +1 -0
  7. package/dist/services/index.d.ts +2 -0
  8. package/dist/services/index.js +1 -0
  9. package/dist/services/local-gateway-service.d.ts +22 -0
  10. package/dist/services/local-gateway-service.js +51 -0
  11. package/dist/types/core.types.d.ts +3 -1
  12. package/dist/types/state.types.d.ts +5 -0
  13. package/dist/utils.js +1 -1
  14. package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +27 -5
  15. package/dist/wirings/ai-agent/ai-agent-memory.js +13 -2
  16. package/dist/wirings/ai-agent/ai-agent-prepare.js +14 -1
  17. package/dist/wirings/ai-agent/ai-agent-stream.js +17 -2
  18. package/dist/wirings/ai-agent/ai-agent.types.d.ts +35 -2
  19. package/dist/wirings/ai-agent/index.d.ts +1 -1
  20. package/dist/wirings/channel/channel-handler.d.ts +5 -2
  21. package/dist/wirings/channel/channel-handler.js +11 -1
  22. package/dist/wirings/channel/channel-middleware-runner.js +1 -0
  23. package/dist/wirings/channel/channel.types.d.ts +6 -0
  24. package/dist/wirings/channel/index.d.ts +1 -1
  25. package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -0
  26. package/dist/wirings/channel/local/local-channel-handler.js +17 -0
  27. package/dist/wirings/channel/local/local-channel-runner.js +14 -1
  28. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
  29. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  30. package/dist/wirings/channel/serverless/serverless-channel-runner.js +1 -1
  31. package/dist/wirings/cli/cli-runner.js +3 -0
  32. package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
  33. package/dist/wirings/gateway/gateway-runner.js +325 -0
  34. package/dist/wirings/gateway/gateway.types.d.ts +127 -0
  35. package/dist/wirings/gateway/gateway.types.js +1 -0
  36. package/dist/wirings/gateway/index.d.ts +2 -0
  37. package/dist/wirings/gateway/index.js +1 -0
  38. package/dist/wirings/http/http-runner.js +3 -0
  39. package/package.json +3 -2
  40. package/src/middleware-runner.ts +1 -0
  41. package/src/permissions.ts +1 -0
  42. package/src/pikku-state.ts +5 -0
  43. package/src/services/gateway-service.ts +20 -0
  44. package/src/services/index.ts +2 -0
  45. package/src/services/local-gateway-service.ts +62 -0
  46. package/src/types/core.types.ts +3 -0
  47. package/src/types/state.types.ts +8 -0
  48. package/src/utils.ts +1 -1
  49. package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +33 -9
  50. package/src/wirings/ai-agent/ai-agent-memory.ts +10 -1
  51. package/src/wirings/ai-agent/ai-agent-prepare.ts +19 -1
  52. package/src/wirings/ai-agent/ai-agent-stream.ts +13 -2
  53. package/src/wirings/ai-agent/ai-agent.types.ts +34 -2
  54. package/src/wirings/ai-agent/index.ts +2 -0
  55. package/src/wirings/channel/channel-handler.ts +30 -2
  56. package/src/wirings/channel/channel-middleware-runner.ts +1 -0
  57. package/src/wirings/channel/channel.types.ts +11 -0
  58. package/src/wirings/channel/index.ts +1 -0
  59. package/src/wirings/channel/local/local-channel-handler.ts +26 -0
  60. package/src/wirings/channel/local/local-channel-runner.ts +15 -1
  61. package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
  62. package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
  63. package/src/wirings/channel/serverless/serverless-channel-runner.ts +1 -1
  64. package/src/wirings/cli/cli-runner.ts +3 -0
  65. package/src/wirings/gateway/gateway-runner.test.ts +474 -0
  66. package/src/wirings/gateway/gateway-runner.ts +411 -0
  67. package/src/wirings/gateway/gateway.types.ts +149 -0
  68. package/src/wirings/gateway/index.ts +13 -0
  69. package/src/wirings/http/http-runner.ts +3 -0
  70. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,62 @@
1
+ import type { GatewayService } from './gateway-service.js'
2
+ import type { GatewayAdapter } from '../wirings/gateway/gateway.types.js'
3
+ import { pikkuState, getSingletonServices } from '../pikku-state.js'
4
+ import { createListenerMessageHandler } from '../wirings/gateway/gateway-runner.js'
5
+
6
+ /**
7
+ * Local GatewayService implementation.
8
+ *
9
+ * Starts all registered listener gateways unconditionally (single-process).
10
+ * For distributed deployments, implement `GatewayService` with leader
11
+ * election or similar coordination.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const gatewayService = new LocalGatewayService()
16
+ * await gatewayService.start()
17
+ *
18
+ * // On shutdown
19
+ * await gatewayService.stop()
20
+ * ```
21
+ */
22
+ export class LocalGatewayService implements GatewayService {
23
+ private activeAdapters = new Map<string, GatewayAdapter>()
24
+
25
+ async start(): Promise<void> {
26
+ const singletonServices = getSingletonServices()
27
+ const gateways = pikkuState(null, 'gateway', 'gateways')
28
+
29
+ for (const [name, config] of gateways) {
30
+ if (config.type !== 'listener') continue
31
+ if (this.activeAdapters.has(name)) continue
32
+
33
+ const handleMessage = createListenerMessageHandler(
34
+ name,
35
+ config,
36
+ singletonServices
37
+ )
38
+
39
+ await config.adapter.init(handleMessage)
40
+ this.activeAdapters.set(name, config.adapter)
41
+ singletonServices.logger.info(`Started listener gateway: ${name}`)
42
+ }
43
+ }
44
+
45
+ async stop(): Promise<void> {
46
+ for (const [name, adapter] of this.activeAdapters) {
47
+ try {
48
+ await adapter.close()
49
+ } catch (e: any) {
50
+ try {
51
+ getSingletonServices().logger.error(
52
+ `Error closing listener gateway '${name}':`,
53
+ e
54
+ )
55
+ } catch {
56
+ // logger may not be available during shutdown
57
+ }
58
+ }
59
+ }
60
+ this.activeAdapters.clear()
61
+ }
62
+ }
@@ -22,6 +22,7 @@ import type {
22
22
  } from '../wirings/workflow/workflow.types.js'
23
23
  import type { PikkuGraphWire } from '../wirings/workflow/graph/workflow-graph.types.js'
24
24
  import type { PikkuTrigger } from '../wirings/trigger/trigger.types.js'
25
+ import type { PikkuGateway } from '../wirings/gateway/gateway.types.js'
25
26
  import type { SchedulerService } from '../services/scheduler-service.js'
26
27
  import type { DeploymentService } from '../services/deployment-service.js'
27
28
  import type { AIStorageService } from '../services/ai-storage-service.js'
@@ -42,6 +43,7 @@ export type PikkuWiringTypes =
42
43
  | 'cli'
43
44
  | 'workflow'
44
45
  | 'agent'
46
+ | 'gateway'
45
47
 
46
48
  export interface FunctionServicesMeta {
47
49
  optimized: boolean
@@ -249,6 +251,7 @@ export type PikkuWire<
249
251
  workflowStep: WorkflowStepWire
250
252
  graph: PikkuGraphWire
251
253
  trigger: PikkuTrigger<TriggerOutput>
254
+ gateway: PikkuGateway
252
255
  session: HasInitialSession extends true
253
256
  ? UserSession
254
257
  : UserSession | undefined
@@ -26,6 +26,10 @@ import type {
26
26
  CoreAIAgent,
27
27
  AIAgentMeta,
28
28
  } from '../wirings/ai-agent/ai-agent.types.js'
29
+ import type {
30
+ CoreGateway,
31
+ GatewaysMeta,
32
+ } from '../wirings/gateway/gateway.types.js'
29
33
  import type {
30
34
  CoreQueueWorker,
31
35
  QueueWorkersMeta,
@@ -121,6 +125,10 @@ export interface PikkuPackageState {
121
125
  agents: Map<string, CoreAIAgent>
122
126
  agentsMeta: AIAgentMeta
123
127
  }
128
+ gateway: {
129
+ gateways: Map<string, CoreGateway>
130
+ meta: GatewaysMeta
131
+ }
124
132
  cli: {
125
133
  meta: CLIMeta | Record<string, any> // Backward compatible with old published CLI format
126
134
  programs: Record<string, CLIProgramState>
package/src/utils.ts CHANGED
@@ -103,7 +103,7 @@ export const stopSingletonServices = async (): Promise<void> => {
103
103
  const singletonServices = getSingletonServices()
104
104
  const logger = singletonServices.logger
105
105
 
106
- // First, stop all addon package singleton services
106
+ // Stop all addon package singleton services
107
107
  const stateMap = getAllPackageStates()
108
108
  if (stateMap.size > 0) {
109
109
  for (const [packageName, packageState] of stateMap) {
@@ -75,6 +75,7 @@ export function createAssistantUIChannel(
75
75
  return parent.state
76
76
  },
77
77
  close: () => parent.close(),
78
+ sendBinary: (data) => parent.sendBinary(data),
78
79
  send: (event: AIStreamEvent) => {
79
80
  switch (event.type) {
80
81
  case 'text-delta':
@@ -87,6 +88,16 @@ export function createAssistantUIChannel(
87
88
  sendChunk({ type: 'reasoning-delta', delta: event.text })
88
89
  break
89
90
 
91
+ case 'audio-delta':
92
+ ensureStarted()
93
+ sendChunk({ type: 'audio-delta', data: event } as any)
94
+ break
95
+
96
+ case 'audio-done':
97
+ ensureStarted()
98
+ sendChunk({ type: 'audio-done', data: event } as any)
99
+ break
100
+
90
101
  case 'tool-call':
91
102
  ensureStarted()
92
103
  hasToolCalls = true
@@ -190,18 +201,31 @@ export function parseAssistantUIInput(
190
201
  }
191
202
 
192
203
  let messageText: string
204
+ let attachments: AIAgentInput['attachments']
205
+
206
+ const extractFromParts = (parts: Array<Record<string, unknown>>) => {
207
+ const textParts = parts.filter((p) => p.type === 'text')
208
+ messageText = textParts.map((p) => String(p.text)).join(' ') || ''
209
+ const nonTextParts = parts.filter(
210
+ (p) => p.type === 'image' || p.type === 'file'
211
+ )
212
+ if (nonTextParts.length > 0) {
213
+ attachments = nonTextParts.map((p) => ({
214
+ type: p.type as 'image' | 'file',
215
+ data: p.data as string | undefined,
216
+ url: p.url as string | undefined,
217
+ mediaType: p.mediaType as string | undefined,
218
+ filename: p.filename as string | undefined,
219
+ }))
220
+ }
221
+ }
222
+
193
223
  if (typeof lastUserMessage.content === 'string') {
194
224
  messageText = lastUserMessage.content
195
225
  } else if (Array.isArray(lastUserMessage.parts)) {
196
- const textPart = (
197
- lastUserMessage.parts as Array<Record<string, unknown>>
198
- ).find((p) => p.type === 'text')
199
- messageText = textPart ? String(textPart.text) : ''
226
+ extractFromParts(lastUserMessage.parts as Array<Record<string, unknown>>)
200
227
  } else if (Array.isArray(lastUserMessage.content)) {
201
- const textPart = (
202
- lastUserMessage.content as Array<Record<string, unknown>>
203
- ).find((p) => p.type === 'text')
204
- messageText = textPart ? String(textPart.text) : ''
228
+ extractFromParts(lastUserMessage.content as Array<Record<string, unknown>>)
205
229
  } else {
206
230
  messageText = ''
207
231
  }
@@ -210,5 +234,5 @@ export function parseAssistantUIInput(
210
234
  (input.threadId as string) ?? (input.id as string) ?? randomUUID()
211
235
  const resourceId = defaults?.resourceId ?? 'default'
212
236
 
213
- return { message: messageText, threadId, resourceId }
237
+ return { message: messageText!, threadId, resourceId, attachments }
214
238
  }
@@ -277,7 +277,16 @@ function sanitizeToolMessages(messages: AIMessage[]): AIMessage[] {
277
277
 
278
278
  function estimateTokens(msg: AIMessage): number {
279
279
  let chars = 0
280
- if (msg.content) chars += msg.content.length
280
+ if (msg.content) {
281
+ if (typeof msg.content === 'string') {
282
+ chars += msg.content.length
283
+ } else {
284
+ for (const part of msg.content) {
285
+ if (part.type === 'text') chars += part.text.length
286
+ else chars += 1000
287
+ }
288
+ }
289
+ }
281
290
  if (msg.toolCalls) chars += JSON.stringify(msg.toolCalls).length
282
291
  if (msg.toolResults) chars += JSON.stringify(msg.toolResults).length
283
292
  return Math.ceil(chars / 4)
@@ -3,6 +3,7 @@ import type {
3
3
  CoreAIAgent,
4
4
  AIAgentInput,
5
5
  AIAgentToolDef,
6
+ AIContentPart,
6
7
  AIMessage,
7
8
  AIStreamChannel,
8
9
  AIStreamEvent,
@@ -144,6 +145,7 @@ export function createScopedChannel(
144
145
  return capturedApproval
145
146
  },
146
147
  close: () => {},
148
+ sendBinary: (data) => parent.sendBinary(data),
147
149
  send: (event: AIStreamEvent) => {
148
150
  if (event.type === 'done') return
149
151
  if (event.type === 'approval-request') {
@@ -448,10 +450,26 @@ export async function prepareAgentRun(
448
450
  workingMemoryJsonSchema
449
451
  )
450
452
 
453
+ const userContent: AIMessage['content'] = input.attachments?.length
454
+ ? [
455
+ { type: 'text' as const, text: input.message },
456
+ ...input.attachments.map(
457
+ (a) =>
458
+ ({
459
+ type: a.type,
460
+ data: a.data,
461
+ url: a.url,
462
+ mediaType: a.mediaType,
463
+ ...(a.filename ? { filename: a.filename } : {}),
464
+ }) as AIContentPart
465
+ ),
466
+ ]
467
+ : input.message
468
+
451
469
  const userMessage: AIMessage = {
452
470
  id: randomUUID(),
453
471
  role: 'user',
454
- content: input.message,
472
+ content: userContent,
455
473
  createdAt: new Date(),
456
474
  }
457
475
 
@@ -114,6 +114,7 @@ function createPersistingChannel(
114
114
  },
115
115
  flush: flushStep,
116
116
  close: () => parent.close(),
117
+ sendBinary: (data) => parent.sendBinary(data),
117
118
  send: (event: AIStreamEvent) => {
118
119
  if (storage) {
119
120
  switch (event.type) {
@@ -547,7 +548,12 @@ export async function streamAIAgent(
547
548
  allEvents,
548
549
  state,
549
550
  })
550
- if (result != null) await next(result)
551
+ if (result == null) return
552
+ if (Array.isArray(result)) {
553
+ for (const r of result) await next(r)
554
+ } else {
555
+ await next(result)
556
+ }
551
557
  }
552
558
  })
553
559
 
@@ -928,7 +934,12 @@ async function continueAfterToolResult(
928
934
  allEvents,
929
935
  state,
930
936
  })
931
- if (result != null) await next(result)
937
+ if (result == null) return
938
+ if (Array.isArray(result)) {
939
+ for (const r of result) await next(r)
940
+ } else {
941
+ await next(result)
942
+ }
932
943
  }
933
944
  })
934
945
 
@@ -19,6 +19,17 @@ export interface AIThread {
19
19
  updatedAt: Date
20
20
  }
21
21
 
22
+ export type AIContentPart =
23
+ | { type: 'text'; text: string }
24
+ | { type: 'image'; data?: string; url?: string; mediaType?: string }
25
+ | {
26
+ type: 'file'
27
+ data?: string
28
+ url?: string
29
+ mediaType: string
30
+ filename?: string
31
+ }
32
+
22
33
  export interface AIToolCall {
23
34
  id: string
24
35
  name: string
@@ -34,7 +45,7 @@ export interface AIToolResult {
34
45
  export interface AIMessage {
35
46
  id: string
36
47
  role: 'system' | 'user' | 'assistant' | 'tool'
37
- content?: string
48
+ content?: string | AIContentPart[]
38
49
  toolCalls?: AIToolCall[]
39
50
  toolResults?: AIToolResult[]
40
51
  createdAt: Date
@@ -45,10 +56,19 @@ export interface AIAgentStep {
45
56
  toolCalls?: { name: string; args: Record<string, unknown>; result: string }[]
46
57
  }
47
58
 
59
+ export interface AIAgentInputAttachment {
60
+ type: 'image' | 'file'
61
+ data?: string
62
+ url?: string
63
+ mediaType?: string
64
+ filename?: string
65
+ }
66
+
48
67
  export interface AIAgentInput {
49
68
  message: string
50
69
  threadId: string
51
70
  resourceId: string
71
+ attachments?: AIAgentInputAttachment[]
52
72
  }
53
73
 
54
74
  export interface AIAgentOutput {
@@ -86,7 +106,11 @@ export interface PikkuAIMiddlewareHooks<
86
106
  allEvents: readonly AIStreamEvent[]
87
107
  state: State
88
108
  }
89
- ) => Promise<AIStreamEvent | null> | AIStreamEvent | null
109
+ ) =>
110
+ | Promise<AIStreamEvent | AIStreamEvent[] | null>
111
+ | AIStreamEvent
112
+ | AIStreamEvent[]
113
+ | null
90
114
 
91
115
  modifyOutput?: (
92
116
  services: Services,
@@ -235,6 +259,14 @@ export type AIStreamEvent =
235
259
  session?: string
236
260
  }
237
261
  | { type: 'error'; message: string; agent?: string; session?: string }
262
+ | {
263
+ type: 'audio-delta'
264
+ data: string
265
+ format: string
266
+ agent?: string
267
+ session?: string
268
+ }
269
+ | { type: 'audio-done'; agent?: string; session?: string }
238
270
  | {
239
271
  type: 'suspended'
240
272
  reason: 'rpc-missing'
@@ -23,9 +23,11 @@ export {
23
23
  } from './ai-agent-registry.js'
24
24
  export type {
25
25
  AIAgentInput,
26
+ AIAgentInputAttachment,
26
27
  AIAgentMeta,
27
28
  AIAgentMemoryConfig,
28
29
  AIAgentStep,
30
+ AIContentPart,
29
31
  AgentRunRow,
30
32
  AgentRunService,
31
33
  AgentRunState,
@@ -4,6 +4,7 @@ import type {
4
4
  CoreUserSession,
5
5
  } from '../../types/core.types.js'
6
6
  import type {
7
+ BinaryData,
7
8
  ChannelMessageMeta,
8
9
  CoreChannel,
9
10
  PikkuChannelHandler,
@@ -154,7 +155,7 @@ export const processMessageHandlers = (
154
155
  })
155
156
  }
156
157
 
157
- return async (rawData): Promise<unknown> => {
158
+ const onMessage = async (rawData: unknown): Promise<unknown> => {
158
159
  let result: unknown
159
160
  let processed = false
160
161
 
@@ -198,7 +199,10 @@ export const processMessageHandlers = (
198
199
  // Default handler if no routes matched and json data wasn't parsed
199
200
  if (!processed && channelConfig.onMessage) {
200
201
  processed = true
201
- result = await processMessage(rawData, channelConfig.onMessage)
202
+ result = await processMessage(
203
+ rawData as JSONValue,
204
+ channelConfig.onMessage
205
+ )
202
206
  }
203
207
 
204
208
  if (!processed) {
@@ -210,4 +214,28 @@ export const processMessageHandlers = (
210
214
 
211
215
  return result
212
216
  }
217
+
218
+ const onBinaryMessage = channelConfig.onBinaryMessage
219
+ ? async (data: BinaryData): Promise<BinaryData | void> => {
220
+ if (
221
+ !validateAuth(
222
+ requiresSession,
223
+ userSession?.get() as CoreUserSession | undefined,
224
+ channelConfig
225
+ )
226
+ ) {
227
+ logger.error(
228
+ `Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for binary message`
229
+ )
230
+ return
231
+ }
232
+ return channelConfig.onBinaryMessage!(
233
+ services,
234
+ data,
235
+ channelHandler.getChannel()
236
+ )
237
+ }
238
+ : undefined
239
+
240
+ return { onMessage, onBinaryMessage }
213
241
  }
@@ -114,6 +114,7 @@ export function wrapChannelWithMiddleware<Out>(
114
114
  channel: {
115
115
  ...channel,
116
116
  send: ((data: Out) => dispatch(0, data)) as typeof channel.send,
117
+ sendBinary: channel.sendBinary,
117
118
  },
118
119
  }
119
120
  }
@@ -10,6 +10,8 @@ import type {
10
10
  PermissionMetadata,
11
11
  } from '../../types/core.types.js'
12
12
 
13
+ export type BinaryData = ArrayBuffer | Uint8Array
14
+
13
15
  export type CorePikkuChannelMiddleware<Services = any, Event = unknown> = (
14
16
  services: Services,
15
17
  event: Event,
@@ -64,6 +66,7 @@ export interface ChannelMeta {
64
66
  disconnect: ChannelMessageMeta | null
65
67
  message: ChannelMessageMeta | null
66
68
  messageWirings: Record<string, Record<string, ChannelMessageMeta>>
69
+ binary?: boolean | null
67
70
  summary?: string
68
71
  description?: string
69
72
  errors?: string[]
@@ -132,6 +135,12 @@ export type CoreChannel<
132
135
  >
133
136
  permissions?: CorePermissionGroup<PikkuPermission>
134
137
  auth?: boolean
138
+ binary?: boolean | null
139
+ onBinaryMessage?: (
140
+ services: any,
141
+ data: BinaryData,
142
+ channel: PikkuChannel<ChannelData, any>
143
+ ) => Promise<BinaryData | void> | BinaryData | void
135
144
  docs?: Partial<{
136
145
  description: string
137
146
  response: string
@@ -149,6 +158,7 @@ export interface PikkuChannel<OpeningData, out Out> {
149
158
  openingData: OpeningData
150
159
  // The data to send. This will fail is the stream has been closed.
151
160
  send(data: Out, isBinary?: boolean): Promise<void> | void
161
+ sendBinary(data: BinaryData): Promise<void> | void
152
162
  // This will close the channel.
153
163
  close(): Promise<void> | void
154
164
  // The current state of the channel
@@ -157,6 +167,7 @@ export interface PikkuChannel<OpeningData, out Out> {
157
167
 
158
168
  export interface PikkuChannelHandler<OpeningData = unknown, Out = unknown> {
159
169
  send(message: Out, isBinary?: boolean): Promise<void> | void
170
+ sendBinary(data: BinaryData): Promise<void> | void
160
171
  getChannel(): PikkuChannel<OpeningData, Out>
161
172
  }
162
173
 
@@ -11,6 +11,7 @@ export { ChannelStore } from './channel-store.js'
11
11
  export type { Channel } from './channel-store.js'
12
12
  export { EventHubStore } from './eventhub-store.js'
13
13
  export type {
14
+ BinaryData,
14
15
  ChannelsMeta,
15
16
  CoreChannel,
16
17
  CorePikkuChannelMiddleware,
@@ -1,3 +1,4 @@
1
+ import type { BinaryData } from '../channel.types.js'
1
2
  import { PikkuAbstractChannelHandler } from '../pikku-abstract-channel-handler.js'
2
3
 
3
4
  export class PikkuLocalChannelHandler<
@@ -5,9 +6,11 @@ export class PikkuLocalChannelHandler<
5
6
  Out = unknown,
6
7
  > extends PikkuAbstractChannelHandler<OpeningData, Out> {
7
8
  private onMessageCallback?: (message: unknown) => void
9
+ private onBinaryMessageCallback?: (data: BinaryData) => void
8
10
  private openCallBack?: () => void
9
11
  private closeCallback?: () => void
10
12
  private sendCallback?: (message: Out, isBinary?: boolean) => void
13
+ private sendBinaryCallback?: (data: BinaryData) => void
11
14
 
12
15
  public registerOnOpen(callback: () => void): void {
13
16
  this.openCallBack = callback
@@ -28,6 +31,18 @@ export class PikkuLocalChannelHandler<
28
31
  return this.onMessageCallback?.(data)
29
32
  }
30
33
 
34
+ public registerOnBinaryMessage(
35
+ callback: (
36
+ data: BinaryData
37
+ ) => Promise<BinaryData | void> | BinaryData | void
38
+ ): void {
39
+ this.onBinaryMessageCallback = callback
40
+ }
41
+
42
+ public async binaryMessage(data: BinaryData): Promise<BinaryData | void> {
43
+ return this.onBinaryMessageCallback?.(data)
44
+ }
45
+
31
46
  public registerOnClose(callback: () => void): void {
32
47
  this.closeCallback = callback
33
48
  }
@@ -50,4 +65,15 @@ export class PikkuLocalChannelHandler<
50
65
  }
51
66
  return this.sendCallback?.(message, isBinary)
52
67
  }
68
+
69
+ public registerOnSendBinary(send: (data: BinaryData) => void) {
70
+ this.sendBinaryCallback = send
71
+ }
72
+
73
+ public sendBinary(data: BinaryData): void {
74
+ if (!this.sendBinaryCallback) {
75
+ throw new Error('No sendBinary callback registered')
76
+ }
77
+ this.sendBinaryCallback(data)
78
+ }
53
79
  }
@@ -147,7 +147,7 @@ export const runLocalChannel = async ({
147
147
  await closeServices()
148
148
  })
149
149
 
150
- const onMessage = processMessageHandlers(
150
+ const { onMessage, onBinaryMessage } = processMessageHandlers(
151
151
  services,
152
152
  channelConfig as any,
153
153
  channelHandler,
@@ -162,6 +162,20 @@ export const runLocalChannel = async ({
162
162
  channel.send({ error: e.message || 'Unknown error' })
163
163
  }
164
164
  })
165
+ if (onBinaryMessage) {
166
+ channelHandler.registerOnBinaryMessage(async (data) => {
167
+ try {
168
+ const result = await onBinaryMessage(data)
169
+ if (result) {
170
+ await channel.sendBinary(result)
171
+ }
172
+ } catch (e) {
173
+ singletonServices.logger.error(
174
+ `Error handling binary message: ${e.message}`
175
+ )
176
+ }
177
+ })
178
+ }
165
179
  } catch (e: any) {
166
180
  handleHTTPError(
167
181
  e,
@@ -10,6 +10,8 @@ class TestChannelHandler extends PikkuAbstractChannelHandler<
10
10
  public async send(message: { msg: string }, isBinary = false): Promise<void> {
11
11
  // Mock send implementation
12
12
  }
13
+
14
+ public sendBinary(_data: ArrayBuffer | Uint8Array): void {}
13
15
  }
14
16
 
15
17
  let handler: TestChannelHandler
@@ -1,4 +1,8 @@
1
- import type { PikkuChannel, PikkuChannelHandler } from './channel.types.js'
1
+ import type {
2
+ BinaryData,
3
+ PikkuChannel,
4
+ PikkuChannelHandler,
5
+ } from './channel.types.js'
2
6
 
3
7
  export abstract class PikkuAbstractChannelHandler<
4
8
  OpeningData = unknown,
@@ -15,12 +19,15 @@ export abstract class PikkuAbstractChannelHandler<
15
19
 
16
20
  public abstract send(message: Out, isBinary?: boolean): Promise<void> | void
17
21
 
22
+ public abstract sendBinary(data: BinaryData): Promise<void> | void
23
+
18
24
  public getChannel(): PikkuChannel<OpeningData, Out> {
19
25
  if (!this.channel) {
20
26
  this.channel = {
21
27
  channelId: this.channelId,
22
28
  openingData: this.openingData,
23
29
  send: this.send.bind(this),
30
+ sendBinary: this.sendBinary.bind(this),
24
31
  close: this.close.bind(this),
25
32
  state: 'initial',
26
33
  }
@@ -265,7 +265,7 @@ export const runChannelMessage = async (
265
265
 
266
266
  let response: unknown
267
267
  try {
268
- const onMessage = processMessageHandlers(
268
+ const { onMessage } = processMessageHandlers(
269
269
  services,
270
270
  channelConfig,
271
271
  channelHandler,
@@ -321,6 +321,9 @@ export async function runCLICommand({
321
321
  await Promise.resolve(renderer(singletonServices, data, undefined))
322
322
  }
323
323
  },
324
+ sendBinary: () => {
325
+ throw new Error('Binary data is not supported on CLI channels')
326
+ },
324
327
  close: () => {
325
328
  if (channel) {
326
329
  channel.state = 'closed'