@pikku/core 0.10.1 → 0.11.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 (81) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/function/function-runner.js +2 -2
  3. package/dist/function/functions.types.d.ts +1 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +2 -0
  6. package/dist/middleware/auth-apikey.d.ts +1 -0
  7. package/dist/middleware/auth-bearer.d.ts +1 -0
  8. package/dist/middleware/auth-cookie.d.ts +1 -0
  9. package/dist/middleware/timeout.d.ts +1 -0
  10. package/dist/middleware-runner.js +1 -0
  11. package/dist/permissions.js +1 -0
  12. package/dist/pikku-state.d.ts +7 -2
  13. package/dist/pikku-state.js +4 -0
  14. package/dist/services/index.d.ts +1 -0
  15. package/dist/services/index.js +1 -0
  16. package/dist/services/scheduler-service.d.ts +63 -0
  17. package/dist/services/scheduler-service.js +6 -0
  18. package/dist/time-utils.d.ts +14 -0
  19. package/dist/time-utils.js +62 -0
  20. package/dist/types/core.types.d.ts +24 -2
  21. package/dist/types/core.types.js +1 -0
  22. package/dist/wirings/channel/channel-common.d.ts +28 -0
  23. package/dist/wirings/channel/channel-common.js +42 -0
  24. package/dist/wirings/channel/channel-handler.js +37 -11
  25. package/dist/wirings/channel/channel-runner.js +9 -4
  26. package/dist/wirings/channel/channel.types.d.ts +8 -2
  27. package/dist/wirings/channel/local/local-channel-handler.js +3 -0
  28. package/dist/wirings/channel/local/local-channel-runner.js +47 -14
  29. package/dist/wirings/channel/serverless/serverless-channel-runner.js +90 -41
  30. package/dist/wirings/cli/channel/cli-channel-runner.js +10 -1
  31. package/dist/wirings/cli/cli-runner.d.ts +1 -1
  32. package/dist/wirings/cli/cli-runner.js +1 -1
  33. package/dist/wirings/http/http-runner.js +0 -1
  34. package/dist/wirings/mcp/mcp-runner.js +3 -2
  35. package/dist/wirings/queue/queue-runner.js +6 -3
  36. package/dist/wirings/queue/queue.types.d.ts +1 -3
  37. package/dist/wirings/rpc/index.d.ts +1 -1
  38. package/dist/wirings/rpc/index.js +1 -1
  39. package/dist/wirings/rpc/rpc-runner.d.ts +15 -0
  40. package/dist/wirings/rpc/rpc-runner.js +38 -3
  41. package/dist/wirings/rpc/rpc-types.d.ts +1 -0
  42. package/dist/wirings/workflow/index.d.ts +6 -0
  43. package/dist/wirings/workflow/index.js +6 -0
  44. package/dist/wirings/workflow/pikku-workflow-service.d.ts +152 -0
  45. package/dist/wirings/workflow/pikku-workflow-service.js +448 -0
  46. package/dist/wirings/workflow/workflow-runner.d.ts +35 -0
  47. package/dist/wirings/workflow/workflow-runner.js +68 -0
  48. package/dist/wirings/workflow/workflow.types.d.ts +247 -0
  49. package/dist/wirings/workflow/workflow.types.js +1 -0
  50. package/package.json +2 -3
  51. package/src/function/function-runner.ts +2 -2
  52. package/src/function/functions.types.ts +1 -0
  53. package/src/index.ts +2 -0
  54. package/src/middleware-runner.ts +1 -0
  55. package/src/permissions.ts +1 -0
  56. package/src/pikku-state.ts +14 -2
  57. package/src/services/index.ts +1 -0
  58. package/src/services/scheduler-service.ts +76 -0
  59. package/src/time-utils.ts +75 -0
  60. package/src/types/core.types.ts +30 -1
  61. package/src/wirings/channel/channel-common.ts +80 -0
  62. package/src/wirings/channel/channel-handler.ts +48 -18
  63. package/src/wirings/channel/channel-runner.ts +15 -4
  64. package/src/wirings/channel/channel.types.ts +12 -2
  65. package/src/wirings/channel/local/local-channel-handler.ts +3 -0
  66. package/src/wirings/channel/local/local-channel-runner.ts +48 -36
  67. package/src/wirings/channel/serverless/serverless-channel-runner.ts +134 -66
  68. package/src/wirings/cli/channel/cli-channel-runner.ts +13 -1
  69. package/src/wirings/cli/cli-runner.ts +2 -2
  70. package/src/wirings/http/http-runner.ts +0 -2
  71. package/src/wirings/mcp/mcp-runner.ts +5 -2
  72. package/src/wirings/queue/queue-runner.ts +14 -6
  73. package/src/wirings/queue/queue.types.ts +1 -4
  74. package/src/wirings/rpc/index.ts +1 -1
  75. package/src/wirings/rpc/rpc-runner.ts +57 -4
  76. package/src/wirings/rpc/rpc-types.ts +1 -0
  77. package/src/wirings/workflow/index.ts +22 -0
  78. package/src/wirings/workflow/pikku-workflow-service.ts +756 -0
  79. package/src/wirings/workflow/workflow-runner.ts +85 -0
  80. package/src/wirings/workflow/workflow.types.ts +332 -0
  81. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,80 @@
1
+ import {
2
+ CoreServices,
3
+ PikkuWiringTypes,
4
+ CorePikkuMiddleware,
5
+ } from '../../types/core.types.js'
6
+ import { CoreChannel, ChannelMessageMeta } from './channel.types.js'
7
+ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js'
8
+ import { runPikkuFuncDirectly } from '../../function/function-runner.js'
9
+
10
+ /**
11
+ * Runs a channel lifecycle function (onConnect or onDisconnect) with proper middleware handling.
12
+ *
13
+ * This function:
14
+ * 1. Extracts inline middleware from the lifecycle config if present
15
+ * 2. Combines metadata middleware with inline middleware using combineMiddleware()
16
+ * 3. Runs the lifecycle function with middleware support
17
+ *
18
+ * @param channelConfig - The channel configuration
19
+ * @param meta - Metadata for the lifecycle function (connect or disconnect)
20
+ * @param lifecycleConfig - The onConnect or onDisconnect config (can be function or object with middleware)
21
+ * @param lifecycleType - Type of lifecycle for cache key ('connect' or 'disconnect')
22
+ * @param services - All services (singleton + session)
23
+ * @param channel - The channel instance
24
+ * @param data - Optional data to pass to the lifecycle function (for onConnect)
25
+ * @returns Promise<unknown> - Result from the lifecycle function (if any)
26
+ */
27
+ export const runChannelLifecycleWithMiddleware = async ({
28
+ channelConfig,
29
+ meta,
30
+ lifecycleConfig,
31
+ lifecycleType,
32
+ services,
33
+ channel,
34
+ data,
35
+ }: {
36
+ channelConfig: CoreChannel<unknown, any>
37
+ meta: ChannelMessageMeta
38
+ lifecycleConfig: any
39
+ lifecycleType: 'connect' | 'disconnect'
40
+ services: CoreServices
41
+ channel: any
42
+ data?: unknown
43
+ }): Promise<unknown> => {
44
+ // Extract middleware if lifecycle config is an object
45
+ const lifecycleMiddleware =
46
+ typeof lifecycleConfig === 'object' && 'middleware' in lifecycleConfig
47
+ ? (lifecycleConfig.middleware as CorePikkuMiddleware[]) || []
48
+ : []
49
+
50
+ // Use combineMiddleware to properly resolve metadata + inline middleware
51
+ const allMiddleware = combineMiddleware(
52
+ PikkuWiringTypes.channel,
53
+ `${channelConfig.name}:${lifecycleType}`,
54
+ {
55
+ wireInheritedMiddleware: meta.middleware,
56
+ wireMiddleware: lifecycleMiddleware,
57
+ }
58
+ )
59
+
60
+ // Run the lifecycle function
61
+ const runLifecycle = async () => {
62
+ return await runPikkuFuncDirectly(
63
+ meta.pikkuFuncName,
64
+ { ...services, channel },
65
+ data
66
+ )
67
+ }
68
+
69
+ // Run with middleware if any
70
+ if (allMiddleware.length > 0) {
71
+ return await runMiddleware(
72
+ services,
73
+ { channel },
74
+ allMiddleware,
75
+ runLifecycle
76
+ )
77
+ } else {
78
+ return await runLifecycle()
79
+ }
80
+ }
@@ -87,19 +87,43 @@ export const processMessageHandlers = (
87
87
 
88
88
  const {
89
89
  pikkuFuncName,
90
- middleware: inheritedMiddleware,
90
+ middleware: routeInheritedMiddleware,
91
91
  permissions: inheritedPermissions,
92
92
  } = getRouteMeta(channelConfig.name, routingProperty, routerValue)
93
93
 
94
- const wirePermissions =
95
- typeof onMessage === 'function' ? undefined : onMessage.permissions
94
+ // Get wire middleware: channel-level middleware + message-specific middleware
95
+ const channelWireMiddleware = channelConfig.middleware || []
96
96
 
97
- const wireMiddleware =
98
- typeof onMessage === 'function' ? [] : onMessage.middleware
97
+ // Check if onMessage is a wrapper object vs direct function config:
98
+ // - Direct config: onMessage.func is a plain Function
99
+ // - Wrapper: onMessage.func is a CorePikkuFunctionConfig (has onMessage.func.func)
100
+ // - Simple wrapper: onMessage has both func (plain Function) and middleware properties
101
+ const isWrapper =
102
+ onMessage &&
103
+ typeof onMessage === 'object' &&
104
+ 'func' in onMessage &&
105
+ ((typeof onMessage.func === 'object' && 'func' in onMessage.func) ||
106
+ 'middleware' in onMessage)
107
+ const messageWireMiddleware = isWrapper ? onMessage.middleware || [] : []
108
+
109
+ // Combine channel middleware with message middleware (actual functions)
110
+ // Channel middleware runs first, then message middleware
111
+ const wireMiddleware = [...channelWireMiddleware, ...messageWireMiddleware]
112
+
113
+ // Inherited middleware comes from metadata (tag groups, non-inline wire)
114
+ const inheritedMiddleware = routeInheritedMiddleware || []
115
+
116
+ const wirePermissions = isWrapper ? onMessage.permissions : undefined
117
+
118
+ // Create unique cache key that includes routing info to avoid cache collisions
119
+ // when multiple message handlers use the same function
120
+ const cacheKey = routingProperty
121
+ ? `${channelConfig.name}:${routingProperty}:${routerValue}`
122
+ : `${channelConfig.name}:default`
99
123
 
100
124
  return await runPikkuFunc(
101
125
  PikkuWiringTypes.channel,
102
- channelConfig.name,
126
+ cacheKey,
103
127
  pikkuFuncName,
104
128
  {
105
129
  singletonServices: services,
@@ -119,25 +143,35 @@ export const processMessageHandlers = (
119
143
  )
120
144
  }
121
145
 
122
- const onMessage = async (rawData): Promise<unknown> => {
146
+ return async (rawData): Promise<unknown> => {
123
147
  let result: unknown
124
148
  let processed = false
125
149
 
126
150
  // Route-specific handling
127
151
  if (typeof rawData === 'string' && channelConfig.onMessageWiring) {
152
+ let messageData: any
128
153
  try {
129
- const messageData = JSON.parse(rawData)
154
+ messageData = JSON.parse(rawData)
155
+ } catch (error) {
156
+ // Most likely a json error.. ignore
157
+ }
158
+
159
+ if (messageData) {
130
160
  const entries = Object.entries(channelConfig.onMessageWiring)
131
161
  for (const [routingProperty, routes] of entries) {
132
162
  const routerValue = messageData[routingProperty]
133
163
  if (routerValue && routes[routerValue]) {
164
+ const { [routingProperty]: _, ...data } = messageData
165
+
134
166
  processed = true
135
- result = await processMessage(
136
- messageData,
137
- routes[routerValue],
138
- routingProperty,
139
- routerValue
140
- )
167
+ result =
168
+ (await processMessage(
169
+ data as any,
170
+ routes[routerValue],
171
+ routingProperty,
172
+ routerValue
173
+ )) || {}
174
+ ;(result as any)[routingProperty] = routerValue
141
175
  break
142
176
  }
143
177
  }
@@ -147,8 +181,6 @@ export const processMessageHandlers = (
147
181
  processed = true
148
182
  result = await processMessage(messageData, channelConfig.onMessage)
149
183
  }
150
- } catch (error) {
151
- // Most likely a json error.. ignore
152
184
  }
153
185
  }
154
186
 
@@ -167,6 +199,4 @@ export const processMessageHandlers = (
167
199
 
168
200
  return result
169
201
  }
170
-
171
- return onMessage
172
202
  }
@@ -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)
@@ -30,7 +30,7 @@ export async function executeCLIViaChannel({
30
30
  const allCLIMeta = pikkuState('cli', 'meta') as unknown as CLIMeta | undefined
31
31
  if (!allCLIMeta) {
32
32
  throw new Error(
33
- '[PKU342] CLI metadata not found. No CLI wirings were registered. See https://pikku.dev/errors/pku342 for more information.'
33
+ '[PKU342] CLI metadata not found. No CLI wirings were registered. See https://pikku.dev/docs/pikku-cli/errors/pku342 for more information.'
34
34
  )
35
35
  }
36
36
  const programMeta = allCLIMeta.programs[programName]
@@ -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
  }