@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.
- package/CHANGELOG.md +16 -0
- package/dist/middleware-runner.js +1 -0
- package/dist/permissions.js +1 -0
- package/dist/pikku-state.js +4 -0
- package/dist/services/gateway-service.d.ts +19 -0
- package/dist/services/gateway-service.js +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +1 -0
- package/dist/services/local-gateway-service.d.ts +22 -0
- package/dist/services/local-gateway-service.js +51 -0
- package/dist/types/core.types.d.ts +3 -1
- package/dist/types/state.types.d.ts +5 -0
- package/dist/utils.js +1 -1
- package/dist/wirings/ai-agent/ai-agent-assistant-ui.js +27 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +13 -2
- package/dist/wirings/ai-agent/ai-agent-prepare.js +14 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +17 -2
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +35 -2
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/channel/channel-handler.d.ts +5 -2
- package/dist/wirings/channel/channel-handler.js +11 -1
- package/dist/wirings/channel/channel-middleware-runner.js +1 -0
- package/dist/wirings/channel/channel.types.d.ts +6 -0
- package/dist/wirings/channel/index.d.ts +1 -1
- package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -0
- package/dist/wirings/channel/local/local-channel-handler.js +17 -0
- package/dist/wirings/channel/local/local-channel-runner.js +14 -1
- package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -1
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +1 -1
- package/dist/wirings/cli/cli-runner.js +3 -0
- package/dist/wirings/gateway/gateway-runner.d.ts +24 -0
- package/dist/wirings/gateway/gateway-runner.js +325 -0
- package/dist/wirings/gateway/gateway.types.d.ts +127 -0
- package/dist/wirings/gateway/gateway.types.js +1 -0
- package/dist/wirings/gateway/index.d.ts +2 -0
- package/dist/wirings/gateway/index.js +1 -0
- package/dist/wirings/http/http-runner.js +3 -0
- package/package.json +3 -2
- package/src/middleware-runner.ts +1 -0
- package/src/permissions.ts +1 -0
- package/src/pikku-state.ts +5 -0
- package/src/services/gateway-service.ts +20 -0
- package/src/services/index.ts +2 -0
- package/src/services/local-gateway-service.ts +62 -0
- package/src/types/core.types.ts +3 -0
- package/src/types/state.types.ts +8 -0
- package/src/utils.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-assistant-ui.ts +33 -9
- package/src/wirings/ai-agent/ai-agent-memory.ts +10 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +19 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +13 -2
- package/src/wirings/ai-agent/ai-agent.types.ts +34 -2
- package/src/wirings/ai-agent/index.ts +2 -0
- package/src/wirings/channel/channel-handler.ts +30 -2
- package/src/wirings/channel/channel-middleware-runner.ts +1 -0
- package/src/wirings/channel/channel.types.ts +11 -0
- package/src/wirings/channel/index.ts +1 -0
- package/src/wirings/channel/local/local-channel-handler.ts +26 -0
- package/src/wirings/channel/local/local-channel-runner.ts +15 -1
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +2 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +8 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +1 -1
- package/src/wirings/cli/cli-runner.ts +3 -0
- package/src/wirings/gateway/gateway-runner.test.ts +474 -0
- package/src/wirings/gateway/gateway-runner.ts +411 -0
- package/src/wirings/gateway/gateway.types.ts +149 -0
- package/src/wirings/gateway/index.ts +13 -0
- package/src/wirings/http/http-runner.ts +3 -0
- 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
|
+
}
|
package/src/types/core.types.ts
CHANGED
|
@@ -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
|
package/src/types/state.types.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
) =>
|
|
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'
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
}
|
|
@@ -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
|
|
|
@@ -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 {
|
|
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
|
}
|
|
@@ -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'
|