@pikku/core 0.12.27 → 0.12.29

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 (32) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/function/function-runner.d.ts +1 -2
  3. package/dist/function/function-runner.js +2 -22
  4. package/dist/function/functions.types.d.ts +2 -0
  5. package/dist/services/meta-service.js +18 -7
  6. package/dist/types/core.types.d.ts +2 -0
  7. package/dist/wirings/channel/channel-common.js +9 -2
  8. package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -4
  9. package/dist/wirings/channel/local/local-channel-handler.js +13 -5
  10. package/dist/wirings/channel/local/local-channel-runner.js +1 -1
  11. package/dist/wirings/http/pikku-fetch-http-request.js +42 -8
  12. package/dist/wirings/workflow/pikku-workflow-service.js +10 -1
  13. package/package.json +1 -1
  14. package/src/dev/hot-reload.test.ts +2 -0
  15. package/src/function/function-runner.test.ts +2 -28
  16. package/src/function/function-runner.ts +2 -35
  17. package/src/function/functions.types.ts +2 -0
  18. package/src/services/meta-service.ts +17 -7
  19. package/src/types/core.types.ts +2 -0
  20. package/src/wirings/ai-agent/ai-agent-prepare.test.ts +2 -0
  21. package/src/wirings/ai-agent/ai-agent-runner.test.ts +28 -4
  22. package/src/wirings/ai-agent/ai-agent-stream.test.ts +3 -0
  23. package/src/wirings/channel/channel-common.ts +9 -2
  24. package/src/wirings/channel/local/local-channel-handler.ts +24 -9
  25. package/src/wirings/channel/local/local-channel-runner.ts +2 -1
  26. package/src/wirings/cli/cli-runner.test.ts +4 -0
  27. package/src/wirings/http/http-runner.test.ts +1 -0
  28. package/src/wirings/http/pikku-fetch-http-request.ts +43 -8
  29. package/src/wirings/queue/queue-runner.test.ts +1 -0
  30. package/src/wirings/scheduler/scheduler-runner.test.ts +10 -0
  31. package/src/wirings/workflow/pikku-workflow-service.ts +10 -1
  32. package/tsconfig.tsbuildinfo +1 -1
@@ -327,6 +327,7 @@ describe('streamAIAgent', () => {
327
327
  description: 'Add a todo',
328
328
  approvalRequired: true,
329
329
  inputSchemaName: 'AddTodoInput',
330
+ sessionless: true,
330
331
  }
331
332
  pikkuState(null, 'misc', 'schemas').set('AddTodoInput', {
332
333
  type: 'object',
@@ -428,6 +429,7 @@ describe('streamAIAgent', () => {
428
429
  description: 'Add a todo',
429
430
  approvalRequired: true,
430
431
  inputSchemaName: 'AddTodoInput',
432
+ sessionless: true,
431
433
  }
432
434
  pikkuState('@test/addon-todos', 'misc', 'schemas').set('AddTodoInput', {
433
435
  type: 'object',
@@ -1287,6 +1289,7 @@ describe('resumeAIAgent', () => {
1287
1289
  pikkuState(null, 'function', 'meta').deploy = {
1288
1290
  description: 'Deploy',
1289
1291
  inputSchemaName: 'DeployInput',
1292
+ sessionless: true,
1290
1293
  }
1291
1294
  pikkuState(null, 'misc', 'schemas').set('DeployInput', {
1292
1295
  type: 'object',
@@ -6,7 +6,7 @@ import type {
6
6
  } from '../../types/core.types.js'
7
7
  import type { CoreChannel, ChannelMessageMeta } from './channel.types.js'
8
8
  import { combineMiddleware, runMiddleware } from '../../middleware-runner.js'
9
- import { runPikkuFuncDirectly } from '../../function/function-runner.js'
9
+ import { runPikkuFunc } from '../../function/function-runner.js'
10
10
  import {
11
11
  combineChannelMiddleware,
12
12
  wrapChannelWithMiddleware,
@@ -79,7 +79,14 @@ export const runChannelLifecycleWithMiddleware = async ({
79
79
  }
80
80
 
81
81
  const runLifecycle = async () => {
82
- return await runPikkuFuncDirectly(meta.pikkuFuncId, services, wire, data)
82
+ return await runPikkuFunc('channel', channelConfig.name, meta.pikkuFuncId, {
83
+ singletonServices: services,
84
+ data: () => data as any,
85
+ wire,
86
+ tags: meta.tags ?? [],
87
+ inheritedPermissions: meta.permissions,
88
+ packageName: meta.packageName ?? null,
89
+ })
83
90
  }
84
91
 
85
92
  if (allMiddleware.length > 0) {
@@ -1,3 +1,4 @@
1
+ import type { Logger } from '../../../services/logger.js'
1
2
  import type { BinaryData } from '../channel.types.js'
2
3
  import { PikkuAbstractChannelHandler } from '../pikku-abstract-channel-handler.js'
3
4
 
@@ -7,19 +8,30 @@ export class PikkuLocalChannelHandler<
7
8
  > extends PikkuAbstractChannelHandler<OpeningData, Out> {
8
9
  private onMessageCallback?: (message: unknown) => void
9
10
  private onBinaryMessageCallback?: (data: BinaryData) => void
10
- private openCallBack?: () => void
11
- private closeCallbacks: (() => void)[] = []
11
+ private openCallBack?: () => Promise<void> | void
12
+ private closeCallbacks: (() => Promise<void> | void)[] = []
12
13
  private sendCallback?: (message: Out, isBinary?: boolean) => void
13
14
  private sendBinaryCallback?: (data: BinaryData) => void
15
+ private logger?: Logger
14
16
 
15
- public registerOnOpen(callback: () => void): void {
17
+ constructor(
18
+ channelId: string,
19
+ channelName: string,
20
+ openingData: OpeningData,
21
+ logger?: Logger
22
+ ) {
23
+ super(channelId, channelName, openingData)
24
+ this.logger = logger
25
+ }
26
+
27
+ public registerOnOpen(callback: () => Promise<void> | void): void {
16
28
  this.openCallBack = callback
17
29
  }
18
30
 
19
- public open() {
31
+ public async open(): Promise<void> {
20
32
  this.getChannel().state = 'open'
21
33
  if (this.openCallBack) {
22
- this.openCallBack()
34
+ await this.openCallBack()
23
35
  }
24
36
  }
25
37
 
@@ -43,17 +55,20 @@ export class PikkuLocalChannelHandler<
43
55
  return this.onBinaryMessageCallback?.(data)
44
56
  }
45
57
 
46
- public registerOnClose(callback: () => void): void {
58
+ public registerOnClose(callback: () => Promise<void> | void): void {
47
59
  this.closeCallbacks.push(callback)
48
60
  }
49
61
 
50
- public close() {
62
+ public async close(): Promise<void> {
51
63
  if (this.getChannel().state === 'closed') {
52
64
  return
53
65
  }
54
66
  super.close()
55
- for (const cb of this.closeCallbacks) {
56
- cb()
67
+ const results = await Promise.allSettled(this.closeCallbacks.map((cb) => cb()))
68
+ for (const result of results) {
69
+ if (result.status === 'rejected') {
70
+ this.logger?.error('Error in channel close callback:', result.reason)
71
+ }
57
72
  }
58
73
  }
59
74
 
@@ -119,7 +119,8 @@ export const runLocalChannel = async ({
119
119
  channelHandler = new PikkuLocalChannelHandler(
120
120
  channelId,
121
121
  channelConfig.name,
122
- openingData
122
+ openingData,
123
+ singletonServices.logger
123
124
  )
124
125
  const channel = channelHandler.getChannel()
125
126
  const wire: PikkuWire = {
@@ -128,6 +128,7 @@ describe('CLI Runner', () => {
128
128
  pikkuFuncId: 'greetFunc',
129
129
  inputSchemaName: null,
130
130
  outputSchemaName: null,
131
+ sessionless: true,
131
132
  },
132
133
  })
133
134
 
@@ -192,6 +193,7 @@ describe('CLI Runner', () => {
192
193
  pikkuFuncId: 'testFunc',
193
194
  inputSchemaName: null,
194
195
  outputSchemaName: null,
196
+ sessionless: true,
195
197
  },
196
198
  })
197
199
 
@@ -252,6 +254,7 @@ describe('CLI Runner', () => {
252
254
  pikkuFuncId: 'greetFunc',
253
255
  inputSchemaName: null,
254
256
  outputSchemaName: null,
257
+ sessionless: true,
255
258
  },
256
259
  })
257
260
 
@@ -296,6 +299,7 @@ describe('CLI Runner', () => {
296
299
  pikkuFuncId: 'secureFunc',
297
300
  inputSchemaName: null,
298
301
  outputSchemaName: null,
302
+ sessionless: true,
299
303
  },
300
304
  })
301
305
 
@@ -129,6 +129,7 @@ const setRouteMeta = (
129
129
  inputSchemaName: null,
130
130
  outputSchemaName: null,
131
131
  sessionless: true,
132
+ sessionless: true,
132
133
  permissions: undefined,
133
134
  middleware: undefined,
134
135
  } as any
@@ -14,6 +14,9 @@ export class PikkuFetchHTTPRequest<
14
14
  #cookies: Partial<Record<string, string>> | undefined
15
15
  #params: Partial<Record<string, string | string[]>> = {}
16
16
  #url: URL
17
+ #rawBodyText: string | undefined
18
+ #rawBodyBuffer: ArrayBuffer | undefined
19
+ #bodyRead = false
17
20
 
18
21
  constructor(private request: Request) {
19
22
  this.#url = new URL(request.url)
@@ -31,16 +34,47 @@ export class PikkuFetchHTTPRequest<
31
34
  * Retrieves the request body.
32
35
  * @returns A promise that resolves to the request body.
33
36
  */
34
- public json(): Promise<In> {
35
- return this.request.json()
37
+ public async json(): Promise<In> {
38
+ const text = await this.#readRawText()
39
+ return JSON.parse(text) as In
36
40
  }
37
41
 
38
42
  /**
39
43
  * Retrieves the raw request body as a Buffer.
40
44
  * @returns A promise that resolves to the raw request body.
41
45
  */
42
- public arrayBuffer(): Promise<ArrayBuffer> {
43
- return this.request.arrayBuffer()
46
+ public async arrayBuffer(): Promise<ArrayBuffer> {
47
+ if (this.#rawBodyBuffer !== undefined) {
48
+ return this.#rawBodyBuffer
49
+ }
50
+ if (this.#bodyRead) {
51
+ if (this.#rawBodyText !== undefined) {
52
+ const buf = new TextEncoder().encode(this.#rawBodyText)
53
+ .buffer as ArrayBuffer
54
+ this.#rawBodyBuffer = buf
55
+ return buf
56
+ }
57
+ return new ArrayBuffer(0)
58
+ }
59
+ const buf = await this.request.arrayBuffer()
60
+ this.#rawBodyBuffer = buf
61
+ this.#bodyRead = true
62
+ return buf
63
+ }
64
+
65
+ async #readRawText(): Promise<string> {
66
+ if (this.#rawBodyText !== undefined) {
67
+ return this.#rawBodyText
68
+ }
69
+ if (this.#rawBodyBuffer !== undefined) {
70
+ const text = new TextDecoder().decode(this.#rawBodyBuffer)
71
+ this.#rawBodyText = text
72
+ return text
73
+ }
74
+ const text = await this.request.text()
75
+ this.#rawBodyText = text
76
+ this.#bodyRead = true
77
+ return text
44
78
  }
45
79
 
46
80
  public headers(): Record<string, string> {
@@ -133,7 +167,8 @@ export class PikkuFetchHTTPRequest<
133
167
  const contentType = this.header('content-type') || ''
134
168
  try {
135
169
  if (contentType.includes('application/json')) {
136
- const parsed = await this.json()
170
+ const text = await this.#readRawText()
171
+ const parsed = text ? JSON.parse(text) : null
137
172
  body =
138
173
  typeof parsed === 'object' &&
139
174
  parsed !== null &&
@@ -141,13 +176,13 @@ export class PikkuFetchHTTPRequest<
141
176
  ? parsed
142
177
  : { data: parsed }
143
178
  } else if (contentType.includes('text/')) {
144
- const text = await this.request.text()
179
+ const text = await this.#readRawText()
145
180
  body = { data: text }
146
181
  } else if (contentType.includes('application/octet-stream')) {
147
- const buffer = await this.request.arrayBuffer()
182
+ const buffer = await this.arrayBuffer()
148
183
  body = { data: buffer }
149
184
  } else if (contentType === 'application/x-www-form-urlencoded') {
150
- const text = await this.request.text()
185
+ const text = await this.#readRawText()
151
186
  const params = new URLSearchParams(text)
152
187
  let count = 0
153
188
  for (const _ of params) {
@@ -43,6 +43,7 @@ const addTestQueueFunction = (pikkuFuncId: string) => {
43
43
  pikkuFuncId,
44
44
  inputSchemaName: null,
45
45
  outputSchemaName: null,
46
+ sessionless: true,
46
47
  middleware: undefined,
47
48
  permissions: undefined,
48
49
  }
@@ -157,6 +157,7 @@ describe('runScheduledTask', () => {
157
157
  pikkuFuncId: 'scheduler_simple-task',
158
158
  inputSchemaName: null,
159
159
  outputSchemaName: null,
160
+ sessionless: true,
160
161
  }
161
162
  wireScheduler(mockTask)
162
163
 
@@ -199,6 +200,7 @@ describe('runScheduledTask', () => {
199
200
  pikkuFuncId: 'scheduler_task-with-session',
200
201
  inputSchemaName: null,
201
202
  outputSchemaName: null,
203
+ sessionless: true,
202
204
  }
203
205
  wireScheduler(mockTask)
204
206
 
@@ -287,6 +289,7 @@ describe('runScheduledTask', () => {
287
289
  pikkuFuncId: 'scheduler_skipped-task',
288
290
  inputSchemaName: null,
289
291
  outputSchemaName: null,
292
+ sessionless: true,
290
293
  }
291
294
  wireScheduler(mockTask)
292
295
 
@@ -332,6 +335,7 @@ describe('runScheduledTask', () => {
332
335
  pikkuFuncId: 'scheduler_skipped-task-no-reason',
333
336
  inputSchemaName: null,
334
337
  outputSchemaName: null,
338
+ sessionless: true,
335
339
  }
336
340
  wireScheduler(mockTask)
337
341
 
@@ -380,6 +384,7 @@ describe('runScheduledTask', () => {
380
384
  pikkuFuncId: 'scheduler_wire-task',
381
385
  inputSchemaName: null,
382
386
  outputSchemaName: null,
387
+ sessionless: true,
383
388
  }
384
389
  wireScheduler(mockTask)
385
390
 
@@ -420,6 +425,7 @@ describe('runScheduledTask', () => {
420
425
  pikkuFuncId: 'scheduler_session-services-task',
421
426
  inputSchemaName: null,
422
427
  outputSchemaName: null,
428
+ sessionless: true,
423
429
  }
424
430
  wireScheduler(mockTask)
425
431
 
@@ -469,6 +475,7 @@ describe('runScheduledTask', () => {
469
475
  pikkuFuncId: 'scheduler_cleanup-task',
470
476
  inputSchemaName: null,
471
477
  outputSchemaName: null,
478
+ sessionless: true,
472
479
  }
473
480
  wireScheduler(mockTask)
474
481
 
@@ -517,6 +524,7 @@ describe('runScheduledTask', () => {
517
524
  pikkuFuncId: 'scheduler_error-cleanup-task',
518
525
  inputSchemaName: null,
519
526
  outputSchemaName: null,
527
+ sessionless: true,
520
528
  }
521
529
  wireScheduler(mockTask)
522
530
 
@@ -558,6 +566,7 @@ describe('runScheduledTask', () => {
558
566
  pikkuFuncId: 'scheduler_error-task',
559
567
  inputSchemaName: null,
560
568
  outputSchemaName: null,
569
+ sessionless: true,
561
570
  }
562
571
  wireScheduler(mockTask)
563
572
 
@@ -607,6 +616,7 @@ describe('runScheduledTask', () => {
607
616
  pikkuFuncId: 'scheduler_middleware-task',
608
617
  inputSchemaName: null,
609
618
  outputSchemaName: null,
619
+ sessionless: true,
610
620
  }
611
621
  wireScheduler(mockTask)
612
622
 
@@ -879,7 +879,16 @@ export abstract class PikkuWorkflowService implements WorkflowService {
879
879
  data: unknown,
880
880
  stepOptions?: WorkflowStepOptions
881
881
  ): Promise<boolean> {
882
- if (this.isInline(runId) || !getSingletonServices()?.queueService) {
882
+ if (!getSingletonServices()?.queueService) {
883
+ return false
884
+ }
885
+ // Functions default to inline execution. Only dispatch via queue when the
886
+ // function explicitly sets inline: false.
887
+ const functionsMeta = pikkuState(null, 'function', 'meta')
888
+ const rpcFuncId = pikkuState(null, 'rpc', 'meta')[rpcName]
889
+ const rpcMeta = typeof rpcFuncId === 'string' ? functionsMeta[rpcFuncId] : undefined
890
+ const forceQueue = rpcMeta?.inline === false
891
+ if (!forceQueue && this.isInline(runId)) {
883
892
  return false
884
893
  }
885
894
  const retries = stepOptions?.retries ?? 0