@pikku/core 0.12.26 → 0.12.28
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 +30 -0
- package/dist/data-classification.d.ts +7 -3
- package/dist/errors/error-handler.js +1 -0
- package/dist/function/function-runner.d.ts +1 -2
- package/dist/function/function-runner.js +2 -22
- package/dist/function/functions.types.d.ts +2 -0
- package/dist/handle-error.js +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/pikku-state.d.ts +1 -0
- package/dist/pikku-state.js +3 -0
- package/dist/services/gopass-secrets.d.ts +1 -0
- package/dist/services/gopass-secrets.js +9 -0
- package/dist/services/local-secrets.d.ts +1 -0
- package/dist/services/local-secrets.js +9 -0
- package/dist/services/meta-service.js +18 -7
- package/dist/services/scoped-secret-service.d.ts +1 -0
- package/dist/services/scoped-secret-service.js +4 -0
- package/dist/services/secret-service.d.ts +7 -0
- package/dist/services/typed-secret-service.d.ts +1 -0
- package/dist/services/typed-secret-service.js +3 -0
- package/dist/test-utils.d.ts +18 -0
- package/dist/test-utils.js +33 -0
- package/dist/types/core.types.d.ts +2 -0
- package/dist/wirings/channel/channel-common.js +9 -2
- package/dist/wirings/channel/local/local-channel-handler.d.ts +7 -4
- package/dist/wirings/channel/local/local-channel-handler.js +13 -5
- package/dist/wirings/channel/local/local-channel-runner.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +10 -1
- package/package.json +1 -1
- package/src/data-classification.ts +11 -4
- package/src/dev/hot-reload.test.ts +2 -0
- package/src/errors/error-handler.ts +1 -0
- package/src/function/function-runner.test.ts +2 -28
- package/src/function/function-runner.ts +2 -35
- package/src/function/functions.types.ts +2 -0
- package/src/handle-error.test.ts +1 -0
- package/src/handle-error.ts +1 -0
- package/src/index.ts +7 -1
- package/src/pikku-state.ts +4 -0
- package/src/services/gopass-secrets.ts +10 -0
- package/src/services/local-secrets.ts +10 -0
- package/src/services/meta-service.ts +17 -7
- package/src/services/scoped-secret-service.ts +5 -0
- package/src/services/secret-service.ts +7 -0
- package/src/services/typed-secret-service.ts +4 -0
- package/src/test-utils.ts +38 -0
- package/src/types/core.types.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +28 -4
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +3 -0
- package/src/wirings/channel/channel-common.ts +9 -2
- package/src/wirings/channel/local/local-channel-handler.ts +24 -9
- package/src/wirings/channel/local/local-channel-runner.ts +2 -1
- package/src/wirings/cli/cli-runner.test.ts +4 -0
- package/src/wirings/http/http-runner.test.ts +1 -0
- package/src/wirings/queue/queue-runner.test.ts +1 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +10 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +10 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -7,7 +7,6 @@ import { runPermissions } from '../permissions.js'
|
|
|
7
7
|
import { pikkuState } from '../pikku-state.js'
|
|
8
8
|
import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js'
|
|
9
9
|
import type {
|
|
10
|
-
CoreServices,
|
|
11
10
|
CoreUserSession,
|
|
12
11
|
CorePikkuMiddleware,
|
|
13
12
|
PikkuWiringTypes,
|
|
@@ -26,10 +25,7 @@ import type {
|
|
|
26
25
|
} from './functions.types.js'
|
|
27
26
|
import { parseVersionedId } from '../version.js'
|
|
28
27
|
import type { SessionService } from '../services/user-session-service.js'
|
|
29
|
-
import {
|
|
30
|
-
PikkuSessionService,
|
|
31
|
-
createFunctionSessionWireProps,
|
|
32
|
-
} from '../services/user-session-service.js'
|
|
28
|
+
import { PikkuSessionService } from '../services/user-session-service.js'
|
|
33
29
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
|
|
34
30
|
import {
|
|
35
31
|
PikkuCredentialWireService,
|
|
@@ -200,27 +196,6 @@ export const getAllFunctionNames = (): string[] => {
|
|
|
200
196
|
return functions
|
|
201
197
|
}
|
|
202
198
|
|
|
203
|
-
export const runPikkuFuncDirectly = async <In, Out>(
|
|
204
|
-
funcName: string,
|
|
205
|
-
allServices: CoreServices,
|
|
206
|
-
wire: PikkuWire,
|
|
207
|
-
data: In,
|
|
208
|
-
userSession?: SessionService<CoreUserSession>,
|
|
209
|
-
packageName: string | null = null
|
|
210
|
-
) => {
|
|
211
|
-
const funcConfig = pikkuState(packageName, 'function', 'functions').get(
|
|
212
|
-
funcName
|
|
213
|
-
)
|
|
214
|
-
if (!funcConfig) {
|
|
215
|
-
throw new Error(`Function not found: ${funcName}`)
|
|
216
|
-
}
|
|
217
|
-
const wireWithSession = {
|
|
218
|
-
...wire,
|
|
219
|
-
...(userSession && createFunctionSessionWireProps(userSession)),
|
|
220
|
-
}
|
|
221
|
-
return (await funcConfig.func(allServices, data, wireWithSession)) as Out
|
|
222
|
-
}
|
|
223
|
-
|
|
224
199
|
export const runPikkuFunc = async <In = any, Out = any>(
|
|
225
200
|
wireType: PikkuWiringTypes,
|
|
226
201
|
wireId: string,
|
|
@@ -381,7 +356,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
381
356
|
throw new ForbiddenError('Authentication required')
|
|
382
357
|
}
|
|
383
358
|
}
|
|
384
|
-
} else
|
|
359
|
+
} else {
|
|
385
360
|
if (wiringAuth === false || funcConfig.auth === false) {
|
|
386
361
|
resolvedSingletonServices.logger.warn(
|
|
387
362
|
`Function '${funcName}' requires a session but auth was explicitly disabled — use pikkuSessionlessFunc instead.`
|
|
@@ -390,14 +365,6 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
390
365
|
if (!session) {
|
|
391
366
|
throw new ForbiddenError('Authentication required')
|
|
392
367
|
}
|
|
393
|
-
} else {
|
|
394
|
-
// TODO: Remove after a couple of releases — backward compat for
|
|
395
|
-
// generated metadata that doesn't include the `sessionless` field yet.
|
|
396
|
-
if (wiringAuth === true || funcConfig.auth === true) {
|
|
397
|
-
if (!session) {
|
|
398
|
-
throw new ForbiddenError('Authentication required')
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
368
|
}
|
|
402
369
|
|
|
403
370
|
if ((session as any)?.readonly && !funcMeta.readonly) {
|
|
@@ -295,6 +295,8 @@ export type CorePikkuFunctionConfig<
|
|
|
295
295
|
readonly?: boolean
|
|
296
296
|
deploy?: 'serverless' | 'server' | 'auto'
|
|
297
297
|
approvalRequired?: boolean
|
|
298
|
+
/** When false, workflow steps calling this function are dispatched via the queue instead of running inline. Defaults to true (inline). */
|
|
299
|
+
inline?: boolean
|
|
298
300
|
audit?:
|
|
299
301
|
| boolean
|
|
300
302
|
| {
|
package/src/handle-error.test.ts
CHANGED
|
@@ -89,6 +89,7 @@ describe('handleHTTPError', () => {
|
|
|
89
89
|
|
|
90
90
|
assert.strictEqual(http._state.statusCode, 404)
|
|
91
91
|
assert.deepStrictEqual(http._state.jsonBody, {
|
|
92
|
+
name: 'NotFoundError',
|
|
92
93
|
message: 'The server cannot find the requested resource.',
|
|
93
94
|
payload: undefined,
|
|
94
95
|
errorId: 'tracker-1',
|
package/src/handle-error.ts
CHANGED
|
@@ -36,6 +36,7 @@ export const handleHTTPError = (
|
|
|
36
36
|
// Set status and response body
|
|
37
37
|
http?.response?.status(errorResponse.status)
|
|
38
38
|
http?.response?.json({
|
|
39
|
+
name: e instanceof Error ? e.name : undefined,
|
|
39
40
|
message:
|
|
40
41
|
e instanceof Error && e.message && e.message !== 'An error occurred'
|
|
41
42
|
? e.message
|
package/src/index.ts
CHANGED
|
@@ -171,7 +171,12 @@ export {
|
|
|
171
171
|
checkAuthPermissions,
|
|
172
172
|
} from './permissions.js'
|
|
173
173
|
export { isSerializable, stopSingletonServices } from './utils.js'
|
|
174
|
-
export {
|
|
174
|
+
export {
|
|
175
|
+
getSingletonServices,
|
|
176
|
+
getCreateWireServices,
|
|
177
|
+
setSingletonServices,
|
|
178
|
+
} from './pikku-state.js'
|
|
179
|
+
export { clearPikkuRuntimeState } from './test-utils.js'
|
|
175
180
|
export {
|
|
176
181
|
type ScheduledTaskInfo,
|
|
177
182
|
type ScheduledTaskSummary,
|
|
@@ -180,6 +185,7 @@ export { SchedulerService } from './services/scheduler-service.js'
|
|
|
180
185
|
|
|
181
186
|
export type {
|
|
182
187
|
Private,
|
|
188
|
+
Pii,
|
|
183
189
|
Secret,
|
|
184
190
|
Classification,
|
|
185
191
|
AnonymizeStrategy,
|
package/src/pikku-state.ts
CHANGED
|
@@ -193,6 +193,10 @@ export const getSingletonServices = (): CoreSingletonServices => {
|
|
|
193
193
|
return services
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
export const setSingletonServices = (services: CoreSingletonServices): void => {
|
|
197
|
+
pikkuState(null, 'package', 'singletonServices', services)
|
|
198
|
+
}
|
|
199
|
+
|
|
196
200
|
export const getCreateWireServices = (): CreateWireServices | undefined => {
|
|
197
201
|
return pikkuState(null, 'package', 'factories')?.createWireServices
|
|
198
202
|
}
|
|
@@ -63,4 +63,14 @@ export class GopassSecretService implements SecretService {
|
|
|
63
63
|
// Ignore errors if secret doesn't exist
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
public async getSecrets(keys: string[]): Promise<Record<string, unknown>> {
|
|
68
|
+
const results = await Promise.allSettled(keys.map((k) => this.getSecret(k)))
|
|
69
|
+
const out: Record<string, unknown> = {}
|
|
70
|
+
keys.forEach((key, i) => {
|
|
71
|
+
if (results[i].status === 'fulfilled')
|
|
72
|
+
out[key] = (results[i] as PromiseFulfilledResult<unknown>).value
|
|
73
|
+
})
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
66
76
|
}
|
|
@@ -52,4 +52,14 @@ export class LocalSecretService implements SecretService {
|
|
|
52
52
|
public async deleteSecret(key: string): Promise<void> {
|
|
53
53
|
this.localSecrets.delete(key)
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
public async getSecrets(keys: string[]): Promise<Record<string, unknown>> {
|
|
57
|
+
const results = await Promise.allSettled(keys.map((k) => this.getSecret(k)))
|
|
58
|
+
const out: Record<string, unknown> = {}
|
|
59
|
+
keys.forEach((key, i) => {
|
|
60
|
+
if (results[i].status === 'fulfilled')
|
|
61
|
+
out[key] = (results[i] as PromiseFulfilledResult<unknown>).value
|
|
62
|
+
})
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
55
65
|
}
|
|
@@ -533,7 +533,17 @@ export class LocalMetaService implements MetaService {
|
|
|
533
533
|
)
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
// emailsMeta.src is an absolute path resolved by the CLI at generation time.
|
|
537
|
+
// Read directly rather than through readProjectFile (which prepends the project
|
|
538
|
+
// root and produces a wrong compound path when baseDir is absolute).
|
|
536
539
|
const baseDir = emailsMeta.src
|
|
540
|
+
const readEmailFile = async (rel: string): Promise<string | null> => {
|
|
541
|
+
try {
|
|
542
|
+
return await readFile(join(baseDir, rel), 'utf-8')
|
|
543
|
+
} catch {
|
|
544
|
+
return null
|
|
545
|
+
}
|
|
546
|
+
}
|
|
537
547
|
const [
|
|
538
548
|
themeRaw,
|
|
539
549
|
localeRaw,
|
|
@@ -543,13 +553,13 @@ export class LocalMetaService implements MetaService {
|
|
|
543
553
|
templateSubject,
|
|
544
554
|
templateText,
|
|
545
555
|
] = await Promise.all([
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
556
|
+
readEmailFile('theme.json'),
|
|
557
|
+
readEmailFile(`locales/${locale}.json`),
|
|
558
|
+
readEmailFile('partials/layout.html'),
|
|
559
|
+
readEmailFile('partials/footer.html'),
|
|
560
|
+
readEmailFile(`templates/${templateName}.html`),
|
|
561
|
+
readEmailFile(`templates/${templateName}.subject.txt`),
|
|
562
|
+
readEmailFile(`templates/${templateName}.text.txt`),
|
|
553
563
|
])
|
|
554
564
|
|
|
555
565
|
return {
|
|
@@ -33,4 +33,9 @@ export class ScopedSecretService implements SecretService {
|
|
|
33
33
|
async deleteSecret(_key: string): Promise<void> {
|
|
34
34
|
throw new Error('deleteSecret is not allowed in scoped secret service')
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
async getSecrets(keys: string[]): Promise<Record<string, unknown>> {
|
|
38
|
+
const allowed = keys.filter((k) => this.allowedKeys.has(k))
|
|
39
|
+
return this.secrets.getSecrets(allowed)
|
|
40
|
+
}
|
|
36
41
|
}
|
|
@@ -28,4 +28,11 @@ export interface SecretService {
|
|
|
28
28
|
* @returns A promise that resolves when the secret is deleted.
|
|
29
29
|
*/
|
|
30
30
|
deleteSecret(key: string): Promise<void>
|
|
31
|
+
/**
|
|
32
|
+
* Retrieves multiple secrets in a single batch operation.
|
|
33
|
+
* Returns a map of key → value for successfully fetched secrets; missing
|
|
34
|
+
* keys are omitted rather than throwing.
|
|
35
|
+
* @param keys - The keys of the secrets to retrieve.
|
|
36
|
+
*/
|
|
37
|
+
getSecrets(keys: string[]): Promise<Record<string, unknown>>
|
|
31
38
|
}
|
|
@@ -43,6 +43,10 @@ export class TypedSecretService<
|
|
|
43
43
|
return this.secrets.deleteSecret(key)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
async getSecrets(keys: string[]): Promise<Record<string, unknown>> {
|
|
47
|
+
return this.secrets.getSecrets(keys)
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
async getAllStatus(): Promise<CredentialStatus[]> {
|
|
47
51
|
const results: CredentialStatus[] = []
|
|
48
52
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getAllPackageStates } from './pikku-state.js'
|
|
2
|
+
import { clearMiddlewareCache } from './middleware-runner.js'
|
|
3
|
+
import { clearPermissionsCache } from './permissions.js'
|
|
4
|
+
import { clearChannelMiddlewareCache } from './wirings/channel/channel-middleware-runner.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Clears all runtime caches between test scenarios without touching registered
|
|
8
|
+
* routes, functions, or middleware groups (which are set at module load time
|
|
9
|
+
* and must persist for the whole test run).
|
|
10
|
+
*
|
|
11
|
+
* Call this alongside your DB snapshot restore in your Before hook:
|
|
12
|
+
*
|
|
13
|
+
* ```typescript
|
|
14
|
+
* Before(async function() {
|
|
15
|
+
* await restoreSnapshot()
|
|
16
|
+
* clearPikkuRuntimeState()
|
|
17
|
+
* setSingletonServices(bundle.services)
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Only works when NODE_ENV === 'test'. Logs an error and returns otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export const clearPikkuRuntimeState = (): void => {
|
|
24
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
25
|
+
console.error(
|
|
26
|
+
'[pikku] clearPikkuRuntimeState() must only be called in test environments. Ignoring.'
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
clearMiddlewareCache()
|
|
32
|
+
clearPermissionsCache()
|
|
33
|
+
clearChannelMiddlewareCache()
|
|
34
|
+
|
|
35
|
+
for (const packageState of getAllPackageStates().values()) {
|
|
36
|
+
packageState.package.singletonServices = null
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/types/core.types.ts
CHANGED
|
@@ -118,6 +118,8 @@ export type FunctionRuntimeMeta = {
|
|
|
118
118
|
readonly?: boolean
|
|
119
119
|
deploy?: 'serverless' | 'server' | 'auto'
|
|
120
120
|
sessionless?: boolean
|
|
121
|
+
/** When false, workflow steps calling this function are dispatched via the queue. Defaults to true (inline). */
|
|
122
|
+
inline?: boolean
|
|
121
123
|
version?: number
|
|
122
124
|
approvalRequired?: boolean
|
|
123
125
|
approvalDescription?: string
|
|
@@ -178,6 +178,7 @@ describe('ai-agent-prepare', () => {
|
|
|
178
178
|
description: 'Deploy the service',
|
|
179
179
|
approvalRequired: true,
|
|
180
180
|
inputSchemaName: 'DeployInput',
|
|
181
|
+
sessionless: true,
|
|
181
182
|
}
|
|
182
183
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
183
184
|
type: 'object',
|
|
@@ -254,6 +255,7 @@ describe('ai-agent-prepare', () => {
|
|
|
254
255
|
description: 'Secret tool',
|
|
255
256
|
permissions: ['admin'],
|
|
256
257
|
inputSchemaName: 'SecretInput',
|
|
258
|
+
sessionless: true,
|
|
257
259
|
}
|
|
258
260
|
pikkuState(null, 'misc', 'schemas').set('SecretInput', {
|
|
259
261
|
type: 'object',
|
|
@@ -560,6 +560,7 @@ describe('runAIAgent', () => {
|
|
|
560
560
|
description: 'Deploy',
|
|
561
561
|
approvalRequired: true,
|
|
562
562
|
inputSchemaName: 'DeployInput',
|
|
563
|
+
sessionless: true,
|
|
563
564
|
}
|
|
564
565
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
565
566
|
type: 'object',
|
|
@@ -716,6 +717,7 @@ describe('resumeAIAgentSync', () => {
|
|
|
716
717
|
pikkuState(null, 'function', 'meta').deploy = {
|
|
717
718
|
description: 'Deploy',
|
|
718
719
|
inputSchemaName: 'DeployInput',
|
|
720
|
+
sessionless: true,
|
|
719
721
|
}
|
|
720
722
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
721
723
|
type: 'object',
|
|
@@ -901,6 +903,7 @@ describe('resumeAIAgentSync', () => {
|
|
|
901
903
|
pikkuState(null, 'function', 'meta').deploy = {
|
|
902
904
|
description: 'Deploy',
|
|
903
905
|
inputSchemaName: 'DeployInput',
|
|
906
|
+
sessionless: true,
|
|
904
907
|
}
|
|
905
908
|
pikkuState(null, 'misc', 'schemas').set('DeployInput', {
|
|
906
909
|
type: 'object',
|
|
@@ -986,7 +989,12 @@ describe('getCredential API key override', () => {
|
|
|
986
989
|
}
|
|
987
990
|
|
|
988
991
|
const mockServices = {
|
|
989
|
-
logger: {
|
|
992
|
+
logger: {
|
|
993
|
+
info: () => {},
|
|
994
|
+
warn: () => {},
|
|
995
|
+
error: () => {},
|
|
996
|
+
debug: () => {},
|
|
997
|
+
},
|
|
990
998
|
aiAgentRunner: {
|
|
991
999
|
run: async (): Promise<AIAgentStepResult> => {
|
|
992
1000
|
originalRunCalls.push('original')
|
|
@@ -1019,13 +1027,24 @@ describe('getCredential API key override', () => {
|
|
|
1019
1027
|
const originalRunCalls: string[] = []
|
|
1020
1028
|
|
|
1021
1029
|
const mockServices = {
|
|
1022
|
-
logger: {
|
|
1030
|
+
logger: {
|
|
1031
|
+
info: () => {},
|
|
1032
|
+
warn: () => {},
|
|
1033
|
+
error: () => {},
|
|
1034
|
+
debug: () => {},
|
|
1035
|
+
},
|
|
1023
1036
|
aiAgentRunner: {
|
|
1024
1037
|
run: async (): Promise<AIAgentStepResult> => {
|
|
1025
1038
|
originalRunCalls.push('original')
|
|
1026
1039
|
return makeStepResult({ text: 'from-original', finishReason: 'stop' })
|
|
1027
1040
|
},
|
|
1028
|
-
withApiKey: (_key: string) => ({
|
|
1041
|
+
withApiKey: (_key: string) => ({
|
|
1042
|
+
run: async () =>
|
|
1043
|
+
makeStepResult({
|
|
1044
|
+
text: 'should-not-be-called',
|
|
1045
|
+
finishReason: 'stop',
|
|
1046
|
+
}),
|
|
1047
|
+
}),
|
|
1029
1048
|
},
|
|
1030
1049
|
aiRunState: {
|
|
1031
1050
|
createRun: async () => 'run-no-cred',
|
|
@@ -1051,7 +1070,12 @@ describe('getCredential API key override', () => {
|
|
|
1051
1070
|
const originalRunCalls: string[] = []
|
|
1052
1071
|
|
|
1053
1072
|
const mockServices = {
|
|
1054
|
-
logger: {
|
|
1073
|
+
logger: {
|
|
1074
|
+
info: () => {},
|
|
1075
|
+
warn: () => {},
|
|
1076
|
+
error: () => {},
|
|
1077
|
+
debug: () => {},
|
|
1078
|
+
},
|
|
1055
1079
|
aiAgentRunner: {
|
|
1056
1080
|
run: async (): Promise<AIAgentStepResult> => {
|
|
1057
1081
|
originalRunCalls.push('original')
|
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
|
@@ -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 (
|
|
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
|