@pikku/core 0.10.0 → 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 (39) hide show
  1. package/CHANGELOG.md +44 -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/queue/queue-runner.js +1 -0
  20. package/dist/wirings/rpc/rpc-runner.d.ts +11 -0
  21. package/dist/wirings/rpc/rpc-runner.js +1 -1
  22. package/package.json +3 -3
  23. package/src/function/function-runner.ts +2 -2
  24. package/src/services/file-channel-store.ts +86 -0
  25. package/src/services/file-eventhub-store.ts +90 -0
  26. package/src/wirings/channel/channel-common.ts +80 -0
  27. package/src/wirings/channel/channel-handler.ts +48 -18
  28. package/src/wirings/channel/channel-runner.ts +15 -4
  29. package/src/wirings/channel/channel.types.ts +12 -2
  30. package/src/wirings/channel/local/local-channel-handler.ts +3 -0
  31. package/src/wirings/channel/local/local-channel-runner.ts +48 -36
  32. package/src/wirings/channel/serverless/serverless-channel-runner.ts +134 -66
  33. package/src/wirings/cli/channel/cli-channel-runner.ts +12 -0
  34. package/src/wirings/cli/cli-runner.ts +1 -1
  35. package/src/wirings/http/http-runner.ts +0 -2
  36. package/src/wirings/mcp/mcp-runner.ts +5 -2
  37. package/src/wirings/queue/queue-runner.ts +1 -0
  38. package/src/wirings/rpc/rpc-runner.ts +1 -1
  39. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @pikku/core
2
2
 
3
+ ## 0.10.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ea652dc: Refactor channel middleware handling and add lifecycle middleware support
8
+
9
+ **Breaking Changes:**
10
+
11
+ - Improved middleware resolution for channel message handlers to properly combine channel-level and message-level middleware
12
+ - Fixed cache key collisions when multiple message handlers use the same function
13
+
14
+ **New Features:**
15
+
16
+ - Add `runChannelLifecycleWithMiddleware` helper in `channel-common.ts` for consistent lifecycle function execution
17
+ - Support middleware on `onConnect` and `onDisconnect` lifecycle functions
18
+ - Channel-level middleware now properly applies to all messages in the channel
19
+
20
+ **Bug Fixes:**
21
+
22
+ - Fix middleware ordering: channel middleware → message middleware → inherited middleware
23
+ - Fix cache key generation to include routing information (prevents cache collisions)
24
+ - Properly detect wrapper objects vs direct function configs for message handlers
25
+
26
+ - 4349ec5: Add file-based storage implementations for serverless environments
27
+
28
+ **New Services:**
29
+
30
+ - Add `FileChannelStore` for file-based channel storage (suitable for AWS Lambda /tmp)
31
+ - Add `FileEventHubStore` for file-based event hub subscriptions
32
+ - Export new services in package.json for use in serverless runtimes
33
+
34
+ **Bug Fixes:**
35
+
36
+ - Fix serverless channel runner to handle disconnect gracefully when channel is already cleaned up
37
+ - Fix MCP runner to pass `mcp` service to functions and use correct function type
38
+
39
+ - 44d71a8: fix: fixing inspector ensuring pikkuConfig is set
40
+
41
+ ## 0.10.1
42
+
43
+ ### Patch Changes
44
+
45
+ - 778267e: fix: fixing inspector ensuring pikkuConfig is set
46
+
3
47
  ## 0.9.0
4
48
 
5
49
  ## 0.10.0
@@ -29,7 +29,7 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { getAllServices,
29
29
  ];
30
30
  // Helper function to run permissions and execute the function
31
31
  const executeFunction = async () => {
32
- const session = userSession?.get();
32
+ const session = await userSession?.get();
33
33
  if (wiringAuth === true || funcConfig.auth === true) {
34
34
  // This means it was explicitly enabled in either wiring or function and has to be respected
35
35
  if (!session) {
@@ -39,7 +39,7 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { getAllServices,
39
39
  if (wiringAuth === undefined && funcConfig.auth === undefined) {
40
40
  // We always default to requiring auth unless explicitly disabled
41
41
  if (!session) {
42
- throw new ForbiddenError('Authentication required');
42
+ // throw new ForbiddenError('Authentication required')
43
43
  }
44
44
  }
45
45
  // Evaluate the data from the lazy function
@@ -0,0 +1,19 @@
1
+ import { CoreUserSession } from '../types/core.types.js';
2
+ import { Channel, ChannelStore } from '../wirings/channel/channel-store.js';
3
+ /**
4
+ * File-based implementation of ChannelStore for serverless/multi-process environments.
5
+ * Stores channels as JSON files in a directory. Suitable for AWS Lambda /tmp storage
6
+ * or other shared filesystem scenarios.
7
+ */
8
+ export declare class FileChannelStore extends ChannelStore {
9
+ private storageDir;
10
+ constructor(storageDir?: string);
11
+ private ensureDir;
12
+ private getChannelPath;
13
+ addChannel({ channelId, channelName, channelObject, openingData, }: Channel): Promise<void>;
14
+ removeChannels(channelIds: string[]): Promise<void>;
15
+ setUserSession(channelId: string, session: CoreUserSession | null): Promise<void>;
16
+ getChannelAndSession(channelId: string): Promise<Channel & {
17
+ session: CoreUserSession;
18
+ }>;
19
+ }
@@ -0,0 +1,69 @@
1
+ import { promises as fs } from 'fs';
2
+ import { join } from 'path';
3
+ import { ChannelStore } from '../wirings/channel/channel-store.js';
4
+ /**
5
+ * File-based implementation of ChannelStore for serverless/multi-process environments.
6
+ * Stores channels as JSON files in a directory. Suitable for AWS Lambda /tmp storage
7
+ * or other shared filesystem scenarios.
8
+ */
9
+ export class FileChannelStore extends ChannelStore {
10
+ storageDir;
11
+ constructor(storageDir = '/tmp/pikku-channels') {
12
+ super();
13
+ this.storageDir = storageDir;
14
+ }
15
+ async ensureDir() {
16
+ try {
17
+ await fs.mkdir(this.storageDir, { recursive: true });
18
+ }
19
+ catch (err) {
20
+ // Directory might already exist, ignore error
21
+ }
22
+ }
23
+ getChannelPath(channelId) {
24
+ return join(this.storageDir, `${channelId}.json`);
25
+ }
26
+ async addChannel({ channelId, channelName, channelObject, openingData, }) {
27
+ await this.ensureDir();
28
+ const data = {
29
+ channelId,
30
+ channelName,
31
+ channelObject,
32
+ openingData,
33
+ session: null,
34
+ };
35
+ await fs.writeFile(this.getChannelPath(channelId), JSON.stringify(data));
36
+ }
37
+ async removeChannels(channelIds) {
38
+ await Promise.all(channelIds.map(async (channelId) => {
39
+ try {
40
+ await fs.unlink(this.getChannelPath(channelId));
41
+ }
42
+ catch (err) {
43
+ // File might not exist, ignore error
44
+ }
45
+ }));
46
+ }
47
+ async setUserSession(channelId, session) {
48
+ const channelPath = this.getChannelPath(channelId);
49
+ try {
50
+ const content = await fs.readFile(channelPath, 'utf-8');
51
+ const channel = JSON.parse(content);
52
+ channel.session = session;
53
+ await fs.writeFile(channelPath, JSON.stringify(channel));
54
+ }
55
+ catch (err) {
56
+ throw new Error(`Channel ${channelId} not found`);
57
+ }
58
+ }
59
+ async getChannelAndSession(channelId) {
60
+ const channelPath = this.getChannelPath(channelId);
61
+ try {
62
+ const content = await fs.readFile(channelPath, 'utf-8');
63
+ return JSON.parse(content);
64
+ }
65
+ catch (err) {
66
+ throw new Error(`Channel ${channelId} not found`);
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,17 @@
1
+ import { EventHubStore } from '../wirings/channel/eventhub-store.js';
2
+ /**
3
+ * File-based implementation of EventHubStore for serverless/multi-process environments.
4
+ * Stores subscriptions as JSON files in a directory. Suitable for AWS Lambda /tmp storage
5
+ * or other shared filesystem scenarios.
6
+ */
7
+ export declare class FileEventHubStore<EventTopics extends Record<string, any> = {}> extends EventHubStore<EventTopics> {
8
+ private storageDir;
9
+ constructor(storageDir?: string);
10
+ private ensureDir;
11
+ private getTopicPath;
12
+ private readSubscriptions;
13
+ private writeSubscriptions;
14
+ getChannelIdsForTopic(topic: string): Promise<string[]>;
15
+ subscribe<T extends keyof EventTopics>(topic: T, channelId: string): Promise<boolean>;
16
+ unsubscribe<T extends keyof EventTopics>(topic: T, channelId: string): Promise<boolean>;
17
+ }
@@ -0,0 +1,71 @@
1
+ import { promises as fs } from 'fs';
2
+ import { join } from 'path';
3
+ import { EventHubStore } from '../wirings/channel/eventhub-store.js';
4
+ /**
5
+ * File-based implementation of EventHubStore for serverless/multi-process environments.
6
+ * Stores subscriptions as JSON files in a directory. Suitable for AWS Lambda /tmp storage
7
+ * or other shared filesystem scenarios.
8
+ */
9
+ export class FileEventHubStore extends EventHubStore {
10
+ storageDir;
11
+ constructor(storageDir = '/tmp/pikku-eventhub') {
12
+ super();
13
+ this.storageDir = storageDir;
14
+ }
15
+ async ensureDir() {
16
+ try {
17
+ await fs.mkdir(this.storageDir, { recursive: true });
18
+ }
19
+ catch (err) {
20
+ // Directory might already exist, ignore error
21
+ }
22
+ }
23
+ getTopicPath(topic) {
24
+ // Sanitize topic name for filesystem
25
+ const safeTopic = topic.replace(/[^a-zA-Z0-9-_]/g, '_');
26
+ return join(this.storageDir, `${safeTopic}.json`);
27
+ }
28
+ async readSubscriptions(topic) {
29
+ try {
30
+ const content = await fs.readFile(this.getTopicPath(topic), 'utf-8');
31
+ const channelIds = JSON.parse(content);
32
+ return new Set(channelIds);
33
+ }
34
+ catch (err) {
35
+ return new Set();
36
+ }
37
+ }
38
+ async writeSubscriptions(topic, channelIds) {
39
+ await this.ensureDir();
40
+ await fs.writeFile(this.getTopicPath(topic), JSON.stringify(Array.from(channelIds)));
41
+ }
42
+ async getChannelIdsForTopic(topic) {
43
+ const channelIds = await this.readSubscriptions(topic);
44
+ return Array.from(channelIds);
45
+ }
46
+ async subscribe(topic, channelId) {
47
+ const topicKey = topic;
48
+ const channelIds = await this.readSubscriptions(topicKey);
49
+ channelIds.add(channelId);
50
+ await this.writeSubscriptions(topicKey, channelIds);
51
+ return true;
52
+ }
53
+ async unsubscribe(topic, channelId) {
54
+ const topicKey = topic;
55
+ const channelIds = await this.readSubscriptions(topicKey);
56
+ channelIds.delete(channelId);
57
+ if (channelIds.size === 0) {
58
+ // Remove the file if no subscriptions left
59
+ try {
60
+ await fs.unlink(this.getTopicPath(topicKey));
61
+ }
62
+ catch (err) {
63
+ // File might not exist, ignore error
64
+ }
65
+ }
66
+ else {
67
+ await this.writeSubscriptions(topicKey, channelIds);
68
+ }
69
+ return true;
70
+ }
71
+ }
@@ -0,0 +1,28 @@
1
+ import { CoreServices } from '../../types/core.types.js';
2
+ import { CoreChannel, ChannelMessageMeta } from './channel.types.js';
3
+ /**
4
+ * Runs a channel lifecycle function (onConnect or onDisconnect) with proper middleware handling.
5
+ *
6
+ * This function:
7
+ * 1. Extracts inline middleware from the lifecycle config if present
8
+ * 2. Combines metadata middleware with inline middleware using combineMiddleware()
9
+ * 3. Runs the lifecycle function with middleware support
10
+ *
11
+ * @param channelConfig - The channel configuration
12
+ * @param meta - Metadata for the lifecycle function (connect or disconnect)
13
+ * @param lifecycleConfig - The onConnect or onDisconnect config (can be function or object with middleware)
14
+ * @param lifecycleType - Type of lifecycle for cache key ('connect' or 'disconnect')
15
+ * @param services - All services (singleton + session)
16
+ * @param channel - The channel instance
17
+ * @param data - Optional data to pass to the lifecycle function (for onConnect)
18
+ * @returns Promise<unknown> - Result from the lifecycle function (if any)
19
+ */
20
+ export declare const runChannelLifecycleWithMiddleware: ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, }: {
21
+ channelConfig: CoreChannel<unknown, any>;
22
+ meta: ChannelMessageMeta;
23
+ lifecycleConfig: any;
24
+ lifecycleType: "connect" | "disconnect";
25
+ services: CoreServices;
26
+ channel: any;
27
+ data?: unknown;
28
+ }) => Promise<unknown>;
@@ -0,0 +1,42 @@
1
+ import { PikkuWiringTypes, } from '../../types/core.types.js';
2
+ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
3
+ import { runPikkuFuncDirectly } from '../../function/function-runner.js';
4
+ /**
5
+ * Runs a channel lifecycle function (onConnect or onDisconnect) with proper middleware handling.
6
+ *
7
+ * This function:
8
+ * 1. Extracts inline middleware from the lifecycle config if present
9
+ * 2. Combines metadata middleware with inline middleware using combineMiddleware()
10
+ * 3. Runs the lifecycle function with middleware support
11
+ *
12
+ * @param channelConfig - The channel configuration
13
+ * @param meta - Metadata for the lifecycle function (connect or disconnect)
14
+ * @param lifecycleConfig - The onConnect or onDisconnect config (can be function or object with middleware)
15
+ * @param lifecycleType - Type of lifecycle for cache key ('connect' or 'disconnect')
16
+ * @param services - All services (singleton + session)
17
+ * @param channel - The channel instance
18
+ * @param data - Optional data to pass to the lifecycle function (for onConnect)
19
+ * @returns Promise<unknown> - Result from the lifecycle function (if any)
20
+ */
21
+ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, }) => {
22
+ // Extract middleware if lifecycle config is an object
23
+ const lifecycleMiddleware = typeof lifecycleConfig === 'object' && 'middleware' in lifecycleConfig
24
+ ? lifecycleConfig.middleware || []
25
+ : [];
26
+ // Use combineMiddleware to properly resolve metadata + inline middleware
27
+ const allMiddleware = combineMiddleware(PikkuWiringTypes.channel, `${channelConfig.name}:${lifecycleType}`, {
28
+ wireInheritedMiddleware: meta.middleware,
29
+ wireMiddleware: lifecycleMiddleware,
30
+ });
31
+ // Run the lifecycle function
32
+ const runLifecycle = async () => {
33
+ return await runPikkuFuncDirectly(meta.pikkuFuncName, { ...services, channel }, data);
34
+ };
35
+ // Run with middleware if any
36
+ if (allMiddleware.length > 0) {
37
+ return await runMiddleware(services, { channel }, allMiddleware, runLifecycle);
38
+ }
39
+ else {
40
+ return await runLifecycle();
41
+ }
42
+ };
@@ -45,10 +45,31 @@ export const processMessageHandlers = (services, channelConfig, channelHandler)
45
45
  channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`);
46
46
  return;
47
47
  }
48
- const { pikkuFuncName, middleware: inheritedMiddleware, permissions: inheritedPermissions, } = getRouteMeta(channelConfig.name, routingProperty, routerValue);
49
- const wirePermissions = typeof onMessage === 'function' ? undefined : onMessage.permissions;
50
- const wireMiddleware = typeof onMessage === 'function' ? [] : onMessage.middleware;
51
- return await runPikkuFunc(PikkuWiringTypes.channel, channelConfig.name, pikkuFuncName, {
48
+ const { pikkuFuncName, middleware: routeInheritedMiddleware, permissions: inheritedPermissions, } = getRouteMeta(channelConfig.name, routingProperty, routerValue);
49
+ // Get wire middleware: channel-level middleware + message-specific middleware
50
+ const channelWireMiddleware = channelConfig.middleware || [];
51
+ // Check if onMessage is a wrapper object vs direct function config:
52
+ // - Direct config: onMessage.func is a plain Function
53
+ // - Wrapper: onMessage.func is a CorePikkuFunctionConfig (has onMessage.func.func)
54
+ // - Simple wrapper: onMessage has both func (plain Function) and middleware properties
55
+ const isWrapper = onMessage &&
56
+ typeof onMessage === 'object' &&
57
+ 'func' in onMessage &&
58
+ ((typeof onMessage.func === 'object' && 'func' in onMessage.func) ||
59
+ 'middleware' in onMessage);
60
+ const messageWireMiddleware = isWrapper ? onMessage.middleware || [] : [];
61
+ // Combine channel middleware with message middleware (actual functions)
62
+ // Channel middleware runs first, then message middleware
63
+ const wireMiddleware = [...channelWireMiddleware, ...messageWireMiddleware];
64
+ // Inherited middleware comes from metadata (tag groups, non-inline wire)
65
+ const inheritedMiddleware = routeInheritedMiddleware || [];
66
+ const wirePermissions = isWrapper ? onMessage.permissions : undefined;
67
+ // Create unique cache key that includes routing info to avoid cache collisions
68
+ // when multiple message handlers use the same function
69
+ const cacheKey = routingProperty
70
+ ? `${channelConfig.name}:${routingProperty}:${routerValue}`
71
+ : `${channelConfig.name}:default`;
72
+ return await runPikkuFunc(PikkuWiringTypes.channel, cacheKey, pikkuFuncName, {
52
73
  singletonServices: services,
53
74
  getAllServices: () => ({
54
75
  ...services,
@@ -64,19 +85,28 @@ export const processMessageHandlers = (services, channelConfig, channelHandler)
64
85
  interaction: { channel: channelHandler.getChannel() },
65
86
  });
66
87
  };
67
- const onMessage = async (rawData) => {
88
+ return async (rawData) => {
68
89
  let result;
69
90
  let processed = false;
70
91
  // Route-specific handling
71
92
  if (typeof rawData === 'string' && channelConfig.onMessageWiring) {
93
+ let messageData;
72
94
  try {
73
- const messageData = JSON.parse(rawData);
95
+ messageData = JSON.parse(rawData);
96
+ }
97
+ catch (error) {
98
+ // Most likely a json error.. ignore
99
+ }
100
+ if (messageData) {
74
101
  const entries = Object.entries(channelConfig.onMessageWiring);
75
102
  for (const [routingProperty, routes] of entries) {
76
103
  const routerValue = messageData[routingProperty];
77
104
  if (routerValue && routes[routerValue]) {
105
+ const { [routingProperty]: _, ...data } = messageData;
78
106
  processed = true;
79
- result = await processMessage(messageData, routes[routerValue], routingProperty, routerValue);
107
+ result =
108
+ (await processMessage(data, routes[routerValue], routingProperty, routerValue)) || {};
109
+ result[routingProperty] = routerValue;
80
110
  break;
81
111
  }
82
112
  }
@@ -86,9 +116,6 @@ export const processMessageHandlers = (services, channelConfig, channelHandler)
86
116
  result = await processMessage(messageData, channelConfig.onMessage);
87
117
  }
88
118
  }
89
- catch (error) {
90
- // Most likely a json error.. ignore
91
- }
92
119
  }
93
120
  // Default handler if no routes matched and json data wasn't parsed
94
121
  if (!processed && channelConfig.onMessage) {
@@ -101,5 +128,4 @@ export const processMessageHandlers = (services, channelConfig, channelHandler)
101
128
  }
102
129
  return result;
103
130
  };
104
- return onMessage;
105
131
  };
@@ -25,7 +25,9 @@ export const wireChannel = (channel) => {
25
25
  }
26
26
  // Register onMessage function if provided
27
27
  if (channel.onMessage && channelMeta.message?.pikkuFuncName) {
28
- addFunction(channelMeta.message.pikkuFuncName, channel.onMessage);
28
+ addFunction(channelMeta.message.pikkuFuncName, channel.onMessage.func instanceof Function
29
+ ? channel.onMessage
30
+ : channel.onMessage.func);
29
31
  }
30
32
  // Register functions in onMessageWiring
31
33
  if (channel.onMessageWiring && channelMeta.messageWirings) {
@@ -40,15 +42,18 @@ export const wireChannel = (channel) => {
40
42
  if (!wiringMeta)
41
43
  return;
42
44
  // Register the function using the pikku name from metadata
43
- addFunction(wiringMeta.pikkuFuncName, handler);
45
+ // It could be a FuncConfig or a wiring override with a funcConfig
46
+ addFunction(wiringMeta.pikkuFuncName, handler.func instanceof Function
47
+ ? handler
48
+ : handler.func);
44
49
  });
45
50
  });
46
51
  }
47
52
  // Store the channel configuration
48
53
  pikkuState('channel', 'channels').set(channel.name, channel);
49
54
  };
50
- const getMatchingChannelConfig = (request) => {
51
- const matchedPath = httpRouter.match('get', request);
55
+ const getMatchingChannelConfig = (path) => {
56
+ const matchedPath = httpRouter.match('get', path);
52
57
  if (!matchedPath) {
53
58
  return null;
54
59
  }
@@ -42,8 +42,14 @@ export type ChannelsMeta = Record<string, ChannelMeta>;
42
42
  export type CoreChannel<ChannelData, Channel extends string, ChannelConnect = CorePikkuFunctionConfig<CorePikkuFunction<void, unknown, ChannelData> | CorePikkuFunctionSessionless<void, unknown, ChannelData>, CorePikkuPermission<void>, CorePikkuMiddleware>, ChannelDisconnect = CorePikkuFunctionConfig<CorePikkuFunction<void, void, ChannelData> | CorePikkuFunctionSessionless<void, void, ChannelData>, CorePikkuPermission<void>, CorePikkuMiddleware>, ChannelFunctionMessage = CorePikkuFunctionConfig<CorePikkuFunction<unknown, unknown, ChannelData> | CorePikkuFunctionSessionless<unknown, unknown, ChannelData>, CorePikkuPermission<unknown>, CorePikkuMiddleware>, PikkuPermission = CorePikkuPermission<ChannelData>, PikkuMiddleware = CorePikkuMiddleware> = {
43
43
  name: string;
44
44
  route: Channel;
45
- onConnect?: ChannelConnect;
46
- onDisconnect?: ChannelDisconnect;
45
+ onConnect?: ChannelConnect | {
46
+ func?: ChannelConnect;
47
+ middleware?: PikkuMiddleware[];
48
+ };
49
+ onDisconnect?: ChannelDisconnect | {
50
+ func?: ChannelDisconnect;
51
+ middleware?: PikkuMiddleware[];
52
+ };
47
53
  onMessage?: ChannelFunctionMessage;
48
54
  onMessageWiring?: Record<string, Record<string, ChannelFunctionMessage | {
49
55
  func: ChannelFunctionMessage;
@@ -23,6 +23,9 @@ export class PikkuLocalChannelHandler extends PikkuAbstractChannelHandler {
23
23
  this.closeCallback = callback;
24
24
  }
25
25
  close() {
26
+ if (this.getChannel().state === 'closed') {
27
+ return;
28
+ }
26
29
  super.close();
27
30
  this.closeCallback?.();
28
31
  }
@@ -3,12 +3,10 @@ import { createHTTPInteraction } from '../../http/http-runner.js';
3
3
  import { closeSessionServices } from '../../../utils.js';
4
4
  import { processMessageHandlers } from '../channel-handler.js';
5
5
  import { PikkuLocalChannelHandler } from './local-channel-handler.js';
6
- import { PikkuWiringTypes, } from '../../../types/core.types.js';
7
6
  import { handleHTTPError } from '../../../handle-error.js';
8
- import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js';
9
7
  import { PikkuUserSessionService } from '../../../services/user-session-service.js';
10
- import { runPikkuFuncDirectly } from '../../../function/function-runner.js';
11
8
  import { rpcService } from '../../rpc/rpc-runner.js';
9
+ import { runChannelLifecycleWithMiddleware } from '../channel-common.js';
12
10
  export const runLocalChannel = async ({ singletonServices, channelId, request, response, route, createSessionServices, skipUserSession = false, respondWith404 = true, coerceDataFromSchema = true, logWarningsForStatusCodes = [], bubbleErrors = false, }) => {
13
11
  let sessionServices;
14
12
  let channelHandler;
@@ -49,23 +47,64 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
49
47
  const getAllServices = (channel, requiresAuth) => rpcService.injectRPCService({
50
48
  ...singletonServices,
51
49
  ...sessionServices,
50
+ channel,
52
51
  userSession,
53
52
  }, interaction, requiresAuth);
54
53
  const interaction = { channel };
55
- channelHandler.registerOnOpen(() => {
54
+ channelHandler.registerOnOpen(async () => {
56
55
  if (channelConfig.onConnect && meta.connect) {
57
- runPikkuFuncDirectly(meta.connect.pikkuFuncName, getAllServices(channel, false), openingData);
56
+ try {
57
+ const result = await runChannelLifecycleWithMiddleware({
58
+ channelConfig,
59
+ meta: meta.connect,
60
+ lifecycleConfig: channelConfig.onConnect,
61
+ lifecycleType: 'connect',
62
+ services: getAllServices(channel, false),
63
+ channel,
64
+ data: openingData,
65
+ });
66
+ if (result !== undefined) {
67
+ await channel.send(result);
68
+ }
69
+ }
70
+ catch (e) {
71
+ singletonServices.logger.error(`Error handling onConnect: ${e}`);
72
+ channel.send({ error: e.message || 'Unknown error' });
73
+ }
58
74
  }
59
75
  });
60
76
  channelHandler.registerOnClose(async () => {
61
77
  if (channelConfig.onDisconnect && meta.disconnect) {
62
- runPikkuFuncDirectly(meta.disconnect.pikkuFuncName, getAllServices(channel, false), openingData);
78
+ try {
79
+ await runChannelLifecycleWithMiddleware({
80
+ channelConfig,
81
+ meta: meta.disconnect,
82
+ lifecycleConfig: channelConfig.onDisconnect,
83
+ lifecycleType: 'disconnect',
84
+ services: getAllServices(channel, false),
85
+ channel,
86
+ });
87
+ }
88
+ catch (e) {
89
+ singletonServices.logger.error(`Error handling onDisconnect: ${e}`);
90
+ channel.send({ error: e.message || 'Unknown error' });
91
+ }
63
92
  }
64
93
  if (sessionServices) {
65
94
  await closeSessionServices(singletonServices.logger, sessionServices);
66
95
  }
67
96
  });
68
- channelHandler.registerOnMessage(processMessageHandlers(getAllServices(channel), channelConfig, channelHandler));
97
+ const onMessage = processMessageHandlers(getAllServices(channel), channelConfig, channelHandler);
98
+ channelHandler.registerOnMessage(async (data) => {
99
+ try {
100
+ const result = await onMessage(data);
101
+ await channel.send(result);
102
+ }
103
+ catch (e) {
104
+ singletonServices.logger.error(`Error handling message: ${e.message}`);
105
+ channel.send({ error: e.message || 'Unknown error' });
106
+ }
107
+ });
69
108
  }
70
109
  catch (e) {
71
110
  handleHTTPError(e, http, channelId, singletonServices.logger, logWarningsForStatusCodes, respondWith404, bubbleErrors);
@@ -76,12 +115,6 @@ export const runLocalChannel = async ({ singletonServices, channelId, request, r
76
115
  }
77
116
  }
78
117
  };
79
- await runMiddleware({
80
- ...singletonServices,
81
- userSession,
82
- }, { http }, combineMiddleware(PikkuWiringTypes.channel, channelConfig.name, {
83
- wireInheritedMiddleware: meta.middleware,
84
- wireMiddleware: channelConfig.middleware,
85
- }), main);
118
+ await main();
86
119
  return channelHandler;
87
120
  };