@pikku/core 0.10.1 → 0.10.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 (37) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/function/function-runner.js +2 -2
  3. package/dist/services/file-channel-store.d.ts +19 -0
  4. package/dist/services/file-channel-store.js +69 -0
  5. package/dist/services/file-eventhub-store.d.ts +17 -0
  6. package/dist/services/file-eventhub-store.js +71 -0
  7. package/dist/wirings/channel/channel-common.d.ts +28 -0
  8. package/dist/wirings/channel/channel-common.js +42 -0
  9. package/dist/wirings/channel/channel-handler.js +37 -11
  10. package/dist/wirings/channel/channel-runner.js +9 -4
  11. package/dist/wirings/channel/channel.types.d.ts +8 -2
  12. package/dist/wirings/channel/local/local-channel-handler.js +3 -0
  13. package/dist/wirings/channel/local/local-channel-runner.js +47 -14
  14. package/dist/wirings/channel/serverless/serverless-channel-runner.js +90 -41
  15. package/dist/wirings/cli/channel/cli-channel-runner.js +9 -0
  16. package/dist/wirings/cli/cli-runner.d.ts +1 -1
  17. package/dist/wirings/http/http-runner.js +0 -1
  18. package/dist/wirings/mcp/mcp-runner.js +3 -2
  19. package/dist/wirings/rpc/rpc-runner.d.ts +11 -0
  20. package/dist/wirings/rpc/rpc-runner.js +1 -1
  21. package/package.json +3 -3
  22. package/src/function/function-runner.ts +2 -2
  23. package/src/services/file-channel-store.ts +86 -0
  24. package/src/services/file-eventhub-store.ts +90 -0
  25. package/src/wirings/channel/channel-common.ts +80 -0
  26. package/src/wirings/channel/channel-handler.ts +48 -18
  27. package/src/wirings/channel/channel-runner.ts +15 -4
  28. package/src/wirings/channel/channel.types.ts +12 -2
  29. package/src/wirings/channel/local/local-channel-handler.ts +3 -0
  30. package/src/wirings/channel/local/local-channel-runner.ts +48 -36
  31. package/src/wirings/channel/serverless/serverless-channel-runner.ts +134 -66
  32. package/src/wirings/cli/channel/cli-channel-runner.ts +12 -0
  33. package/src/wirings/cli/cli-runner.ts +1 -1
  34. package/src/wirings/http/http-runner.ts +0 -2
  35. package/src/wirings/mcp/mcp-runner.ts +5 -2
  36. package/src/wirings/rpc/rpc-runner.ts +1 -1
  37. package/tsconfig.tsbuildinfo +1 -1
@@ -56,7 +56,12 @@ export const wireChannel = <
56
56
 
57
57
  // Register onMessage function if provided
58
58
  if (channel.onMessage && channelMeta.message?.pikkuFuncName) {
59
- addFunction(channelMeta.message.pikkuFuncName, channel.onMessage as any)
59
+ addFunction(
60
+ channelMeta.message.pikkuFuncName,
61
+ (channel.onMessage as any).func instanceof Function
62
+ ? channel.onMessage
63
+ : (channel.onMessage as any).func
64
+ )
60
65
  }
61
66
 
62
67
  // Register functions in onMessageWiring
@@ -72,7 +77,13 @@ export const wireChannel = <
72
77
  if (!wiringMeta) return
73
78
 
74
79
  // Register the function using the pikku name from metadata
75
- addFunction(wiringMeta.pikkuFuncName, handler as any)
80
+ // It could be a FuncConfig or a wiring override with a funcConfig
81
+ addFunction(
82
+ wiringMeta.pikkuFuncName,
83
+ (handler as any).func instanceof Function
84
+ ? handler
85
+ : (handler as any).func
86
+ )
76
87
  })
77
88
  })
78
89
  }
@@ -81,8 +92,8 @@ export const wireChannel = <
81
92
  pikkuState('channel', 'channels').set(channel.name, channel as any)
82
93
  }
83
94
 
84
- const getMatchingChannelConfig = (request: string) => {
85
- const matchedPath = httpRouter.match('get', request)
95
+ const getMatchingChannelConfig = (path: string) => {
96
+ const matchedPath = httpRouter.match('get', path)
86
97
  if (!matchedPath) {
87
98
  return null
88
99
  }
@@ -88,8 +88,18 @@ export type CoreChannel<
88
88
  > = {
89
89
  name: string
90
90
  route: Channel
91
- onConnect?: ChannelConnect
92
- onDisconnect?: ChannelDisconnect
91
+ onConnect?:
92
+ | ChannelConnect
93
+ | {
94
+ func?: ChannelConnect
95
+ middleware?: PikkuMiddleware[]
96
+ }
97
+ onDisconnect?:
98
+ | ChannelDisconnect
99
+ | {
100
+ func?: ChannelDisconnect
101
+ middleware?: PikkuMiddleware[]
102
+ }
93
103
  onMessage?: ChannelFunctionMessage
94
104
  onMessageWiring?: Record<
95
105
  string,
@@ -33,6 +33,9 @@ export class PikkuLocalChannelHandler<
33
33
  }
34
34
 
35
35
  public close() {
36
+ if (this.getChannel().state === 'closed') {
37
+ return
38
+ }
36
39
  super.close()
37
40
  this.closeCallback?.()
38
41
  }
@@ -8,17 +8,12 @@ import {
8
8
  RunChannelParams,
9
9
  } from '../channel.types.js'
10
10
  import { PikkuLocalChannelHandler } from './local-channel-handler.js'
11
- import {
12
- PikkuInteraction,
13
- PikkuWiringTypes,
14
- SessionServices,
15
- } from '../../../types/core.types.js'
11
+ import { PikkuInteraction, SessionServices } from '../../../types/core.types.js'
16
12
  import { handleHTTPError } from '../../../handle-error.js'
17
- import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js'
18
13
  import { PikkuUserSessionService } from '../../../services/user-session-service.js'
19
14
  import { PikkuHTTP } from '../../http/http.types.js'
20
- import { runPikkuFuncDirectly } from '../../../function/function-runner.js'
21
15
  import { rpcService } from '../../rpc/rpc-runner.js'
16
+ import { runChannelLifecycleWithMiddleware } from '../channel-common.js'
22
17
 
23
18
  export const runLocalChannel = async ({
24
19
  singletonServices,
@@ -95,6 +90,7 @@ export const runLocalChannel = async ({
95
90
  {
96
91
  ...singletonServices,
97
92
  ...sessionServices,
93
+ channel,
98
94
  userSession,
99
95
  },
100
96
  interaction,
@@ -103,23 +99,43 @@ export const runLocalChannel = async ({
103
99
 
104
100
  const interaction: PikkuInteraction = { channel }
105
101
 
106
- channelHandler.registerOnOpen(() => {
102
+ channelHandler.registerOnOpen(async () => {
107
103
  if (channelConfig.onConnect && meta.connect) {
108
- runPikkuFuncDirectly(
109
- meta.connect.pikkuFuncName,
110
- getAllServices(channel, false),
111
- openingData
112
- )
104
+ try {
105
+ const result = await runChannelLifecycleWithMiddleware({
106
+ channelConfig,
107
+ meta: meta.connect,
108
+ lifecycleConfig: channelConfig.onConnect,
109
+ lifecycleType: 'connect',
110
+ services: getAllServices(channel, false),
111
+ channel,
112
+ data: openingData,
113
+ })
114
+ if (result !== undefined) {
115
+ await channel.send(result)
116
+ }
117
+ } catch (e) {
118
+ singletonServices.logger.error(`Error handling onConnect: ${e}`)
119
+ channel.send({ error: e.message || 'Unknown error' })
120
+ }
113
121
  }
114
122
  })
115
123
 
116
124
  channelHandler.registerOnClose(async () => {
117
125
  if (channelConfig.onDisconnect && meta.disconnect) {
118
- runPikkuFuncDirectly(
119
- meta.disconnect.pikkuFuncName,
120
- getAllServices(channel, false),
121
- openingData
122
- )
126
+ try {
127
+ await runChannelLifecycleWithMiddleware({
128
+ channelConfig,
129
+ meta: meta.disconnect,
130
+ lifecycleConfig: channelConfig.onDisconnect,
131
+ lifecycleType: 'disconnect',
132
+ services: getAllServices(channel, false),
133
+ channel,
134
+ })
135
+ } catch (e) {
136
+ singletonServices.logger.error(`Error handling onDisconnect: ${e}`)
137
+ channel.send({ error: e.message || 'Unknown error' })
138
+ }
123
139
  }
124
140
 
125
141
  if (sessionServices) {
@@ -127,13 +143,20 @@ export const runLocalChannel = async ({
127
143
  }
128
144
  })
129
145
 
130
- channelHandler.registerOnMessage(
131
- processMessageHandlers(
132
- getAllServices(channel),
133
- channelConfig as any,
134
- channelHandler
135
- )
146
+ const onMessage = processMessageHandlers(
147
+ getAllServices(channel),
148
+ channelConfig as any,
149
+ channelHandler
136
150
  )
151
+ channelHandler.registerOnMessage(async (data) => {
152
+ try {
153
+ const result = await onMessage(data)
154
+ await channel.send(result)
155
+ } catch (e) {
156
+ singletonServices.logger.error(`Error handling message: ${e.message}`)
157
+ channel.send({ error: e.message || 'Unknown error' })
158
+ }
159
+ })
137
160
  } catch (e: any) {
138
161
  handleHTTPError(
139
162
  e,
@@ -151,18 +174,7 @@ export const runLocalChannel = async ({
151
174
  }
152
175
  }
153
176
 
154
- await runMiddleware(
155
- {
156
- ...singletonServices,
157
- userSession,
158
- },
159
- { http },
160
- combineMiddleware(PikkuWiringTypes.channel, channelConfig.name, {
161
- wireInheritedMiddleware: meta.middleware,
162
- wireMiddleware: channelConfig.middleware,
163
- }),
164
- main
165
- )
177
+ await main()
166
178
 
167
179
  return channelHandler
168
180
  }
@@ -1,4 +1,4 @@
1
- import { PikkuWiringTypes, SessionServices } from '../../../types/core.types.js'
1
+ import { PikkuInteraction, SessionServices } from '../../../types/core.types.js'
2
2
  import { closeSessionServices } from '../../../utils.js'
3
3
  import { processMessageHandlers } from '../channel-handler.js'
4
4
  import { openChannel } from '../channel-runner.js'
@@ -12,11 +12,11 @@ import { createHTTPInteraction } from '../../http/http-runner.js'
12
12
  import { ChannelStore } from '../channel-store.js'
13
13
  import { handleHTTPError } from '../../../handle-error.js'
14
14
  import { PikkuUserSessionService } from '../../../services/user-session-service.js'
15
- import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js'
16
15
  import { pikkuState } from '../../../pikku-state.js'
17
16
  import { PikkuFetchHTTPRequest } from '../../http/pikku-fetch-http-request.js'
18
17
  import { PikkuHTTP } from '../../http/http.types.js'
19
- import { runPikkuFuncDirectly } from '../../../function/function-runner.js'
18
+ import { runChannelLifecycleWithMiddleware } from '../channel-common.js'
19
+ import { rpcService } from '../../rpc/rpc-runner.js'
20
20
 
21
21
  export interface RunServerlessChannelParams<ChannelData>
22
22
  extends RunChannelParams<ChannelData> {
@@ -37,7 +37,7 @@ const getVariablesForChannel = ({
37
37
  openingData?: unknown
38
38
  }) => {
39
39
  const channels = pikkuState('channel', 'channels')
40
- const channelConfig = channels[channelName]
40
+ const channelConfig = channels.get(channelName)
41
41
  const channelsMeta = pikkuState('channel', 'meta')
42
42
  const meta = channelsMeta[channelName]
43
43
  if (!channelConfig) {
@@ -95,63 +95,66 @@ export const runChannelConnect = async ({
95
95
  userSession,
96
96
  })
97
97
 
98
- const main = async () => {
99
- try {
100
- await channelStore.addChannel({
101
- channelId,
102
- channelName: channelConfig.name,
103
- openingData,
104
- channelObject,
105
- })
106
- const { channel } = getVariablesForChannel({
107
- channelId,
108
- channelHandlerFactory,
109
- channelName: channelConfig.name,
110
- })
111
- if (createSessionServices) {
112
- sessionServices = await createSessionServices(
113
- singletonServices,
114
- { http },
115
- await userSession.get()
116
- )
117
- }
118
- if (channelConfig.onConnect && meta.connect) {
119
- await runPikkuFuncDirectly(
120
- meta.connect.pikkuFuncName,
121
- { ...singletonServices, ...sessionServices, channel },
122
- openingData
123
- )
124
- }
125
- http?.response?.status(101)
126
- } catch (e: any) {
127
- handleHTTPError(
128
- e,
129
- http,
130
- channelId,
131
- singletonServices.logger,
132
- logWarningsForStatusCodes,
133
- respondWith404,
134
- bubbleErrors
98
+ try {
99
+ await channelStore.addChannel({
100
+ channelId,
101
+ channelName: channelConfig.name,
102
+ openingData,
103
+ channelObject,
104
+ })
105
+ const { channel } = getVariablesForChannel({
106
+ channelId,
107
+ channelHandlerFactory,
108
+ channelName: channelConfig.name,
109
+ })
110
+ if (createSessionServices) {
111
+ sessionServices = await createSessionServices(
112
+ singletonServices,
113
+ { http },
114
+ await userSession.get()
135
115
  )
136
- } finally {
137
- if (sessionServices) {
138
- await closeSessionServices(singletonServices.logger, sessionServices)
139
- }
140
116
  }
141
- }
142
117
 
143
- await runMiddleware(
144
- {
145
- ...singletonServices,
146
- userSession,
147
- },
148
- { http },
149
- combineMiddleware(PikkuWiringTypes.channel, channelConfig.name, {
150
- wireInheritedMiddleware: meta.middleware,
151
- wireMiddleware: channelConfig.middleware,
152
- }),
153
- main
154
- )
118
+ const interaction: PikkuInteraction = { channel }
119
+ const getAllServices = (requiresAuth?: boolean) =>
120
+ rpcService.injectRPCService(
121
+ {
122
+ ...singletonServices,
123
+ ...sessionServices,
124
+ channel,
125
+ userSession,
126
+ },
127
+ interaction,
128
+ requiresAuth
129
+ )
130
+
131
+ if (channelConfig.onConnect && meta.connect) {
132
+ await runChannelLifecycleWithMiddleware({
133
+ channelConfig,
134
+ meta: meta.connect,
135
+ lifecycleConfig: channelConfig.onConnect,
136
+ lifecycleType: 'connect',
137
+ services: getAllServices(false),
138
+ channel,
139
+ data: openingData,
140
+ })
141
+ }
142
+ http?.response?.status(101)
143
+ } catch (e: any) {
144
+ handleHTTPError(
145
+ e,
146
+ http,
147
+ channelId,
148
+ singletonServices.logger,
149
+ logWarningsForStatusCodes,
150
+ respondWith404,
151
+ bubbleErrors
152
+ )
153
+ } finally {
154
+ if (sessionServices) {
155
+ await closeSessionServices(singletonServices.logger, sessionServices)
156
+ }
157
+ }
155
158
  }
156
159
 
157
160
  export const runChannelDisconnect = async ({
@@ -159,13 +162,33 @@ export const runChannelDisconnect = async ({
159
162
  ...params
160
163
  }: RunServerlessChannelParams<unknown>): Promise<void> => {
161
164
  let sessionServices: SessionServices | undefined
162
- const { openingData, channelName, session } =
163
- await params.channelStore.getChannelAndSession(params.channelId)
165
+
166
+ // Try to get channel from store. In serverless environments (especially with
167
+ // serverless-offline or worker threads), disconnect can be called multiple times
168
+ // or after the channel has already been cleaned up. If channel doesn't exist,
169
+ // there's nothing to disconnect, so we can return early.
170
+ let channelData
171
+ try {
172
+ channelData = await params.channelStore.getChannelAndSession(
173
+ params.channelId
174
+ )
175
+ } catch (error) {
176
+ singletonServices.logger.info(
177
+ `Channel ${params.channelId} not found during disconnect - already cleaned up`
178
+ )
179
+ return
180
+ }
181
+
182
+ const { openingData, channelName, session } = channelData
164
183
  const { channel, channelConfig, meta } = getVariablesForChannel({
165
184
  ...params,
166
185
  openingData,
167
186
  channelName,
168
187
  })
188
+ const userSession = new PikkuUserSessionService(
189
+ params.channelStore,
190
+ params.channelId
191
+ )
169
192
  if (!sessionServices && params.createSessionServices) {
170
193
  sessionServices = await params.createSessionServices(
171
194
  singletonServices,
@@ -173,12 +196,35 @@ export const runChannelDisconnect = async ({
173
196
  session
174
197
  )
175
198
  }
176
- if (channelConfig.onDisconnect && meta.disconnect) {
177
- await runPikkuFuncDirectly(
178
- meta.disconnect.pikkuFuncName,
179
- { ...singletonServices, ...sessionServices, channel },
180
- undefined
199
+
200
+ const interaction: PikkuInteraction = { channel }
201
+ const getAllServices = (requiresAuth?: boolean) =>
202
+ rpcService.injectRPCService(
203
+ {
204
+ ...singletonServices,
205
+ ...sessionServices,
206
+ channel,
207
+ userSession,
208
+ },
209
+ interaction,
210
+ requiresAuth
181
211
  )
212
+
213
+ if (channelConfig.onDisconnect && meta.disconnect) {
214
+ try {
215
+ await runChannelLifecycleWithMiddleware({
216
+ channelConfig,
217
+ meta: meta.disconnect,
218
+ lifecycleConfig: channelConfig.onDisconnect,
219
+ lifecycleType: 'disconnect',
220
+ services: getAllServices(false),
221
+ channel,
222
+ })
223
+ } catch (e: any) {
224
+ singletonServices.logger.error(
225
+ `Error handling onDisconnect: ${e.message || e}`
226
+ )
227
+ }
182
228
  }
183
229
  await params.channelStore.removeChannels([channel.channelId])
184
230
  if (sessionServices) {
@@ -199,6 +245,10 @@ export const runChannelMessage = async (
199
245
  openingData,
200
246
  channelName,
201
247
  })
248
+ const userSession = new PikkuUserSessionService(
249
+ params.channelStore,
250
+ params.channelId
251
+ )
202
252
  if (params.createSessionServices) {
203
253
  sessionServices = await params.createSessionServices(
204
254
  singletonServices,
@@ -206,14 +256,32 @@ export const runChannelMessage = async (
206
256
  session
207
257
  )
208
258
  }
259
+
260
+ const interaction: PikkuInteraction = { channel }
261
+ const getAllServices = () =>
262
+ rpcService.injectRPCService(
263
+ {
264
+ ...singletonServices,
265
+ ...sessionServices,
266
+ channel,
267
+ userSession,
268
+ },
269
+ interaction
270
+ )
271
+
209
272
  let response: unknown
210
273
  try {
211
274
  const onMessage = processMessageHandlers(
212
- { ...singletonServices, ...sessionServices },
275
+ getAllServices(),
213
276
  channelConfig,
214
277
  channelHandler
215
278
  )
216
279
  response = await onMessage(data)
280
+ } catch (e: any) {
281
+ singletonServices.logger.error(
282
+ `Error processing message: ${e.message || e}`
283
+ )
284
+ return { error: e.message || 'Unknown error' }
217
285
  } finally {
218
286
  if (sessionServices) {
219
287
  await closeSessionServices(singletonServices.logger, sessionServices)
@@ -92,6 +92,18 @@ export async function executeCLIViaChannel({
92
92
  const commandRoute = pikkuWS.getRoute('command')
93
93
 
94
94
  const responseHandler = (response: any) => {
95
+ // Check if this is a control message
96
+ if (
97
+ response?.action === 'cli-control' &&
98
+ response?.event === 'complete'
99
+ ) {
100
+ // Server signals command is complete, close the connection client-side
101
+ commandRoute.unsubscribe(commandId, responseHandler)
102
+ pikkuWS.ws.close()
103
+ resolve(undefined)
104
+ return
105
+ }
106
+
95
107
  // Call renderer for any output
96
108
  renderer(null as any, response, undefined)
97
109
  }
@@ -434,7 +434,7 @@ export async function executeCLI({
434
434
  args?: string[]
435
435
  createConfig: CreateConfig<any, any>
436
436
  createSingletonServices: CreateSingletonServices<any, any>
437
- createSessionServices?: CreateSessionServices<any, any>
437
+ createSessionServices?: CreateSessionServices<any, any, any>
438
438
  }): Promise<void> {
439
439
  if (!args) {
440
440
  throw new Error(
@@ -327,8 +327,6 @@ const executeRoute = async (
327
327
  const interaction: PikkuInteraction = { http, channel }
328
328
 
329
329
  const getAllServices = async (session?: CoreUserSession) => {
330
- let channel: PikkuChannel<unknown, unknown> | undefined
331
-
332
330
  // Create session-specific services for handling the request
333
331
  sessionServices = await createSessionServices(
334
332
  { ...singletonServices, userSession, channel },
@@ -62,7 +62,7 @@ export const wireMCPResource = <
62
62
  if (!mcpResourceMeta) {
63
63
  throw new Error(`MCP resource metadata not found for '${mcpResource.uri}'`)
64
64
  }
65
- addFunction(mcpResourceMeta.pikkuFuncName, mcpResource)
65
+ addFunction(mcpResourceMeta.pikkuFuncName, mcpResource.func as any)
66
66
  const resources = pikkuState('mcp', 'resources')
67
67
  if (resources.has(mcpResource.uri)) {
68
68
  throw new Error(`MCP resource already exists: ${mcpResource.uri}`)
@@ -160,7 +160,9 @@ export async function runMCPResource(
160
160
  uri,
161
161
  endpoint,
162
162
  pikkuFuncName,
163
- { ...params, mcp: { uri } } as RunMCPEndpointParams<keyof CoreMCPResource>
163
+ { ...params, mcp: { ...params.mcp, uri } } as RunMCPEndpointParams<
164
+ keyof CoreMCPResource
165
+ >
164
166
  )
165
167
  }
166
168
 
@@ -250,6 +252,7 @@ async function runMCPPikkuFunc(
250
252
  {
251
253
  ...singletonServices,
252
254
  ...sessionServices,
255
+ mcp: mcpInteraction,
253
256
  },
254
257
  interaction
255
258
  )
@@ -23,7 +23,7 @@ const getPikkuFunctionName = (rpcName: string): string => {
23
23
  }
24
24
 
25
25
  // Context-aware RPC client for use within services
26
- class ContextAwareRPCService {
26
+ export class ContextAwareRPCService {
27
27
  constructor(
28
28
  private services: CoreServices,
29
29
  private interaction: PikkuInteraction,