@pikku/inspector 0.12.27 → 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 CHANGED
@@ -1,3 +1,37 @@
1
+ ## 0.12.28
2
+
3
+ ### Patch Changes
4
+
5
+ - 66d43d1: Add `deploy.defaultTarget` to `pikku.config.json` to override the default deploy target ('serverless') for functions without an explicit `deploy` flag.
6
+ - a8c9e6d: feat(inspector): add PKU940 — block type casts on rpc.invoke() calls
7
+
8
+ The inspector now emits a critical PKU940 error when `rpc.invoke()` is called
9
+ with an `as` cast on an argument (`rpc.invoke('fn', data as any)`) or when its
10
+ result is cast (`rpc.invoke('fn', data) as any`). Both patterns defeat Pikku's
11
+ generated type safety and are rejected at build time.
12
+
13
+ - ba1ab08: refactor(workflow): replace `inline: false` with `workflowQueued: true` on function meta
14
+
15
+ The per-function workflow dispatch flag has been renamed from the confusing
16
+ negative `inline: false` to the explicit positive `workflowQueued: true`.
17
+ Two companion fields are also added: `workflowRetries` and `workflowTimeout`
18
+ as function-level equivalents of the per-call-site `NodeOptions` fields.
19
+
20
+ **Breaking change (patch — flag was undocumented):** rename `inline: false`
21
+ to `workflowQueued: true` on any `pikkuSessionlessFunc` / `pikkuFunc` that
22
+ dispatches its workflow steps via the queue.
23
+
24
+ **Behaviour change:** a step marked `workflowQueued: true` now throws if no
25
+ queue service is configured, instead of silently falling back to inline
26
+ execution.
27
+
28
+ **Bug fix:** `post-process.ts` was registering `wf-step-*` queues for every
29
+ workflow step node; it now only registers them for steps that are actually
30
+ `workflowQueued: true`, avoiding spurious queue resource allocation.
31
+
32
+ - Updated dependencies [ba1ab08]
33
+ - @pikku/core@0.12.40
34
+
1
35
  ## 0.12.27
2
36
 
3
37
  ### Patch Changes
@@ -277,7 +277,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
277
277
  let deploy;
278
278
  let approvalRequired;
279
279
  let approvalDescription;
280
- let inline;
280
+ let workflowQueued;
281
+ let workflowRetries;
282
+ let workflowTimeout;
281
283
  let version;
282
284
  let objectNode;
283
285
  let nodeDisplayName = null;
@@ -345,7 +347,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
345
347
  readonly_ = getPropertyValue(firstArg, 'readonly');
346
348
  deploy = getPropertyValue(firstArg, 'deploy');
347
349
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired');
348
- inline = getPropertyValue(firstArg, 'inline');
350
+ workflowQueued = getPropertyValue(firstArg, 'workflowQueued');
351
+ workflowRetries = getPropertyValue(firstArg, 'workflowRetries');
352
+ workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout');
349
353
  // Extract approvalDescription identifier reference
350
354
  for (const prop of firstArg.properties) {
351
355
  if (ts.isPropertyAssignment(prop) &&
@@ -740,7 +744,9 @@ export const addFunctions = (logger, node, checker, state, options) => {
740
744
  deploy: deploy || undefined,
741
745
  approvalRequired: approvalRequired || undefined,
742
746
  approvalDescription: approvalDescription || undefined,
743
- inline: inline === false ? false : undefined,
747
+ workflowQueued: workflowQueued === true ? true : undefined,
748
+ workflowRetries: workflowRetries ?? undefined,
749
+ workflowTimeout: workflowTimeout ?? undefined,
744
750
  implementationHash,
745
751
  version,
746
752
  title,
@@ -1,4 +1,18 @@
1
1
  import * as ts from 'typescript';
2
+ import { ErrorCode } from '../error-codes.js';
3
+ function hasTypeCast(node) {
4
+ return ts.isAsExpression(node) || ts.isTypeAssertionExpression(node);
5
+ }
6
+ function outerParent(node) {
7
+ let p = node.parent;
8
+ while (p && (ts.isAwaitExpression(p) || ts.isParenthesizedExpression(p))) {
9
+ p = p.parent;
10
+ }
11
+ return p;
12
+ }
13
+ function findCastArg(args) {
14
+ return args.find(hasTypeCast);
15
+ }
2
16
  /**
3
17
  * Helper to extract namespace from a namespaced function reference like 'ext:hello'
4
18
  */
@@ -52,6 +66,19 @@ export function addRPCInvocations(node, state, logger) {
52
66
  expression.name.text === 'invoke' &&
53
67
  ts.isIdentifier(expression.expression) &&
54
68
  expression.expression.text === 'rpc') {
69
+ // Skip PKU940 for generated files — they may contain intentional casts
70
+ // (e.g. the paginated useInfiniteQuery hook in pikku-react-query.gen.ts).
71
+ const sourceFileName = node.getSourceFile().fileName;
72
+ const isGenerated = sourceFileName.endsWith('.gen.ts') || sourceFileName.endsWith('.gen.js');
73
+ if (!isGenerated) {
74
+ if (hasTypeCast(outerParent(node))) {
75
+ logger.critical(ErrorCode.RPC_INVOCATION_TYPE_CAST, `rpc.invoke() result is type-cast — remove the 'as' expression and rely on Pikku's generated types`);
76
+ }
77
+ const castArg = findCastArg(args);
78
+ if (castArg) {
79
+ logger.critical(ErrorCode.RPC_INVOCATION_TYPE_CAST, `rpc.invoke() has a type cast on an argument — remove the 'as' expression and rely on Pikku's generated types`);
80
+ }
81
+ }
55
82
  const [firstArg] = args;
56
83
  if (firstArg) {
57
84
  if (ts.isStringLiteral(firstArg)) {
@@ -59,7 +59,8 @@ export declare enum ErrorCode {
59
59
  WORKFLOW_MULTI_QUEUE_NOT_SUPPORTED = "PKU901",
60
60
  PII_IN_OUTPUT = "PKU910",
61
61
  ADDON_WIRING_NOT_ALLOWED = "PKU920",
62
- ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921"
62
+ ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = "PKU921",
63
+ RPC_INVOCATION_TYPE_CAST = "PKU940"
63
64
  }
64
65
  /**
65
66
  * Severity of a tracked, coded diagnostic. `critical` always blocks the build;
@@ -75,4 +75,5 @@ export var ErrorCode;
75
75
  // Addon authoring errors
76
76
  ErrorCode["ADDON_WIRING_NOT_ALLOWED"] = "PKU920";
77
77
  ErrorCode["ADDON_CONTRACT_HANDLERS_NOT_ALLOWED"] = "PKU921";
78
+ ErrorCode["RPC_INVOCATION_TYPE_CAST"] = "PKU940";
78
79
  })(ErrorCode || (ErrorCode = {}));
package/dist/types.d.ts CHANGED
@@ -196,6 +196,7 @@ export type InspectorFilters = {
196
196
  target?: Array<'serverless' | 'server'>;
197
197
  excludeTarget?: Array<'serverless' | 'server'>;
198
198
  serverlessIncompatible?: string[];
199
+ defaultTarget?: 'serverless' | 'server';
199
200
  };
200
201
  export type AddonConfig = {
201
202
  package: string;
@@ -256,9 +256,10 @@ export function filterInspectorState(state, filters, logger) {
256
256
  ? new Set(filters.excludeTarget)
257
257
  : null;
258
258
  const incompatible = new Set(filters.serverlessIncompatible ?? []);
259
+ const defaultTarget = filters.defaultTarget ?? 'serverless';
259
260
  keptByDeploy = new Set();
260
261
  for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
261
- const target = resolveDeployTarget(funcMeta, incompatible, funcId);
262
+ const target = resolveDeployTarget(funcMeta, incompatible, funcId, defaultTarget);
262
263
  if (allowed && !allowed.has(target))
263
264
  continue;
264
265
  if (excluded && excluded.has(target))
@@ -761,10 +762,10 @@ export function filterInspectorState(state, filters, logger) {
761
762
  filteredState.requiredSchemas = prunedSchemas;
762
763
  }
763
764
  // Step dispatch is decided purely per-function: a workflow step runs via the
764
- // queue only when its function opts out of inline execution (inline: false).
765
- // Such a unit needs workflowService + queueService injected even though the
766
- // function itself doesn't reference them. Check the ORIGINAL graph meta
767
- // (before filtering pruned it).
765
+ // queue only when its function is marked `workflowQueued: true`. Such a unit
766
+ // needs workflowService + queueService injected even though the function
767
+ // itself doesn't reference them. Check the ORIGINAL graph meta (before
768
+ // filtering pruned it).
768
769
  const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta));
769
770
  const resolveFuncId = (rpcName) => filteredState.rpc.internalMeta[rpcName] ??
770
771
  filteredState.rpc.exposedMeta[rpcName] ??
@@ -782,7 +783,7 @@ export function filterInspectorState(state, filters, logger) {
782
783
  continue;
783
784
  const funcMeta = (filteredState.functions.meta[funcId] ??
784
785
  filteredState.functions.meta[rpcName]);
785
- if (funcMeta?.inline === false) {
786
+ if (funcMeta?.workflowQueued === true) {
786
787
  filteredState.serviceAggregation.requiredServices.add('workflowService');
787
788
  filteredState.serviceAggregation.requiredServices.add('queueService');
788
789
  }
@@ -220,11 +220,18 @@ export function aggregateRequiredServices(state) {
220
220
  pikkuFuncId: `pikkuWorkflowOrchestrator:${graph.name}`,
221
221
  };
222
222
  }
223
- // Per-step queues
223
+ // Per-step queues — only for steps explicitly marked workflowQueued: true
224
224
  for (const node of Object.values(graph.nodes)) {
225
225
  if (!('rpcName' in node) || !node.rpcName)
226
226
  continue;
227
227
  const rpcName = node.rpcName;
228
+ const funcId = state.rpc?.internalMeta?.[rpcName] ??
229
+ state.rpc?.exposedMeta?.[rpcName] ??
230
+ rpcName;
231
+ const funcMeta = (state.functions.meta[funcId] ??
232
+ state.functions.meta[rpcName]);
233
+ if (funcMeta?.workflowQueued !== true)
234
+ continue;
228
235
  const stepQueueName = `wf-step-${toKebab(rpcName)}`;
229
236
  if (!state.queueWorkers.meta[stepQueueName]) {
230
237
  state.queueWorkers.meta[stepQueueName] = {
@@ -18,11 +18,12 @@ export declare class IncompatibleDeployTargetError extends Error {
18
18
  * - throw if the function explicitly declares `deploy: 'serverless'`
19
19
  * - otherwise target is 'server'
20
20
  * 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
21
- * 3. Default 'serverless'
21
+ * 3. `defaultTarget` (sourced from `pikku.config.json` →
22
+ * `deploy.defaultTarget`, falling back to 'serverless')
22
23
  *
23
24
  * Used both by the per-unit deploy analyzer (when bucketing functions
24
25
  * into deployment units) and by `filterInspectorState` (when
25
26
  * `pikku all --deploy <target>` is used to emit a target-scoped set
26
27
  * of gen files).
27
28
  */
28
- export declare function resolveDeployTarget(funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>, serverlessIncompatible: Set<string>, functionName?: string): 'serverless' | 'server';
29
+ export declare function resolveDeployTarget(funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>, serverlessIncompatible: Set<string>, functionName?: string, defaultTarget?: 'serverless' | 'server'): 'serverless' | 'server';
@@ -25,14 +25,15 @@ export class IncompatibleDeployTargetError extends Error {
25
25
  * - throw if the function explicitly declares `deploy: 'serverless'`
26
26
  * - otherwise target is 'server'
27
27
  * 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
28
- * 3. Default 'serverless'
28
+ * 3. `defaultTarget` (sourced from `pikku.config.json` →
29
+ * `deploy.defaultTarget`, falling back to 'serverless')
29
30
  *
30
31
  * Used both by the per-unit deploy analyzer (when bucketing functions
31
32
  * into deployment units) and by `filterInspectorState` (when
32
33
  * `pikku all --deploy <target>` is used to emit a target-scoped set
33
34
  * of gen files).
34
35
  */
35
- export function resolveDeployTarget(funcMeta, serverlessIncompatible, functionName = '<unknown>') {
36
+ export function resolveDeployTarget(funcMeta, serverlessIncompatible, functionName = '<unknown>', defaultTarget = 'serverless') {
36
37
  // Service compatibility wins over the explicit flag — a serverless
37
38
  // bundle of a function that needs (e.g.) node:fs would crash at runtime.
38
39
  const incompatibleHits = [];
@@ -52,5 +53,5 @@ export function resolveDeployTarget(funcMeta, serverlessIncompatible, functionNa
52
53
  return 'server';
53
54
  if (funcMeta.deploy === 'serverless')
54
55
  return 'serverless';
55
- return 'serverless';
56
+ return defaultTarget;
56
57
  }
@@ -62,8 +62,6 @@ export interface NodeOptions {
62
62
  retryDelay?: string;
63
63
  /** Timeout for node execution (e.g., '30s', '5m') */
64
64
  timeout?: string;
65
- /** If true, execute via queue (async). Default: false (inline) */
66
- async?: boolean;
67
65
  }
68
66
  /**
69
67
  * Flow node types for control flow (no RPC call)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.27",
3
+ "version": "0.12.28",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
38
- "@pikku/core": "^0.12.39",
38
+ "@pikku/core": "^0.12.40",
39
39
  "openapi-types": "^12.1.3",
40
40
  "path-to-regexp": "^8.3.0",
41
41
  "ts-json-schema-generator": "^2.5.0",
@@ -392,7 +392,9 @@ export const addFunctions: AddWiring = (
392
392
  let deploy: 'serverless' | 'server' | 'auto' | undefined
393
393
  let approvalRequired: boolean | undefined
394
394
  let approvalDescription: string | undefined
395
- let inline: boolean | undefined
395
+ let workflowQueued: boolean | undefined
396
+ let workflowRetries: number | undefined
397
+ let workflowTimeout: string | undefined
396
398
  let version: number | undefined
397
399
  let objectNode: ts.ObjectLiteralExpression | undefined
398
400
  let nodeDisplayName: string | null = null
@@ -488,7 +490,9 @@ export const addFunctions: AddWiring = (
488
490
  approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
489
491
  | boolean
490
492
  | undefined
491
- inline = getPropertyValue(firstArg, 'inline') as boolean | undefined
493
+ workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as boolean | undefined
494
+ workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as number | undefined
495
+ workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as string | undefined
492
496
 
493
497
  // Extract approvalDescription identifier reference
494
498
  for (const prop of firstArg.properties) {
@@ -1027,7 +1031,9 @@ export const addFunctions: AddWiring = (
1027
1031
  deploy: deploy || undefined,
1028
1032
  approvalRequired: approvalRequired || undefined,
1029
1033
  approvalDescription: approvalDescription || undefined,
1030
- inline: inline === false ? false : undefined,
1034
+ workflowQueued: workflowQueued === true ? true : undefined,
1035
+ workflowRetries: workflowRetries ?? undefined,
1036
+ workflowTimeout: workflowTimeout ?? undefined,
1031
1037
  implementationHash,
1032
1038
  version,
1033
1039
  title,
@@ -1,5 +1,24 @@
1
1
  import * as ts from 'typescript'
2
2
  import type { InspectorState, InspectorLogger } from '../types.js'
3
+ import { ErrorCode } from '../error-codes.js'
4
+
5
+ function hasTypeCast(node: ts.Node): boolean {
6
+ return ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
7
+ }
8
+
9
+ function outerParent(node: ts.Node): ts.Node {
10
+ let p = node.parent
11
+ while (p && (ts.isAwaitExpression(p) || ts.isParenthesizedExpression(p))) {
12
+ p = p.parent
13
+ }
14
+ return p
15
+ }
16
+
17
+ function findCastArg(
18
+ args: ts.NodeArray<ts.Expression>
19
+ ): ts.Expression | undefined {
20
+ return args.find(hasTypeCast)
21
+ }
3
22
 
4
23
  /**
5
24
  * Helper to extract namespace from a namespaced function reference like 'ext:hello'
@@ -67,6 +86,28 @@ export function addRPCInvocations(
67
86
  ts.isIdentifier(expression.expression) &&
68
87
  expression.expression.text === 'rpc'
69
88
  ) {
89
+ // Skip PKU940 for generated files — they may contain intentional casts
90
+ // (e.g. the paginated useInfiniteQuery hook in pikku-react-query.gen.ts).
91
+ const sourceFileName = node.getSourceFile().fileName
92
+ const isGenerated =
93
+ sourceFileName.endsWith('.gen.ts') || sourceFileName.endsWith('.gen.js')
94
+ if (!isGenerated) {
95
+ if (hasTypeCast(outerParent(node))) {
96
+ logger.critical(
97
+ ErrorCode.RPC_INVOCATION_TYPE_CAST,
98
+ `rpc.invoke() result is type-cast — remove the 'as' expression and rely on Pikku's generated types`
99
+ )
100
+ }
101
+
102
+ const castArg = findCastArg(args)
103
+ if (castArg) {
104
+ logger.critical(
105
+ ErrorCode.RPC_INVOCATION_TYPE_CAST,
106
+ `rpc.invoke() has a type cast on an argument — remove the 'as' expression and rely on Pikku's generated types`
107
+ )
108
+ }
109
+ }
110
+
70
111
  const [firstArg] = args
71
112
  if (firstArg) {
72
113
  if (ts.isStringLiteral(firstArg)) {
@@ -0,0 +1,123 @@
1
+ import { strict as assert } from 'assert'
2
+ import { describe, test } from 'node:test'
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { inspect } from '../inspector.js'
7
+ import { ErrorCode } from '../error-codes.js'
8
+ import type { InspectorLogger } from '../types.js'
9
+
10
+ function makeLogger() {
11
+ const criticals: Array<{ code: ErrorCode; message: string }> = []
12
+ const logger: InspectorLogger = {
13
+ debug: () => {},
14
+ info: () => {},
15
+ warn: () => {},
16
+ error: () => {},
17
+ diagnostic: ({ code, message }) => criticals.push({ code, message }),
18
+ critical: (code, message) => criticals.push({ code, message }),
19
+ hasCriticalErrors: () => criticals.length > 0,
20
+ }
21
+ return { logger, criticals }
22
+ }
23
+
24
+ async function runInspect(sourceCode: string) {
25
+ const tmpDir = await mkdtemp(join(tmpdir(), 'pikku-rpc-cast-test-'))
26
+ const file = join(tmpDir, 'funcs.ts')
27
+ await writeFile(file, sourceCode)
28
+ const { logger, criticals } = makeLogger()
29
+ try {
30
+ await inspect(logger, [file], { rootDir: tmpDir })
31
+ } finally {
32
+ await rm(tmpDir, { recursive: true, force: true })
33
+ }
34
+ return criticals
35
+ }
36
+
37
+ describe('RPC type-cast check — PKU940', () => {
38
+ test('flags rpc.invoke() with an as-cast on an argument', async () => {
39
+ const criticals = await runInspect(`
40
+ declare const rpc: { invoke: (name: string, data: unknown) => Promise<unknown> }
41
+ export async function doWork() {
42
+ return rpc.invoke('someFunction', { id: 1 } as any)
43
+ }
44
+ `)
45
+ const hit = criticals.find(
46
+ (c) => c.code === ErrorCode.RPC_INVOCATION_TYPE_CAST
47
+ )
48
+ assert.ok(hit, `Expected PKU940 but got: ${JSON.stringify(criticals)}`)
49
+ })
50
+
51
+ test('flags rpc.invoke() with an angle-bracket cast on an argument', async () => {
52
+ const criticals = await runInspect(`
53
+ declare const rpc: { invoke: (name: string, data: unknown) => Promise<unknown> }
54
+ export async function doWork() {
55
+ return rpc.invoke('someFunction', <any>{ id: 1 })
56
+ }
57
+ `)
58
+ const hit = criticals.find(
59
+ (c) => c.code === ErrorCode.RPC_INVOCATION_TYPE_CAST
60
+ )
61
+ assert.ok(hit, `Expected PKU940 but got: ${JSON.stringify(criticals)}`)
62
+ })
63
+
64
+ test('flags rpc.invoke() result cast with as any', async () => {
65
+ const criticals = await runInspect(`
66
+ declare const rpc: { invoke: (name: string, data: unknown) => Promise<unknown> }
67
+ export async function doWork() {
68
+ return (rpc.invoke('someFunction', { id: 1 }) as any)
69
+ }
70
+ `)
71
+ const hit = criticals.find(
72
+ (c) => c.code === ErrorCode.RPC_INVOCATION_TYPE_CAST
73
+ )
74
+ assert.ok(hit, `Expected PKU940 but got: ${JSON.stringify(criticals)}`)
75
+ })
76
+
77
+ test('flags rpc.invoke() result cast with as never', async () => {
78
+ const criticals = await runInspect(`
79
+ declare const rpc: { invoke: (name: string, data: unknown) => Promise<unknown> }
80
+ export async function doWork() {
81
+ return (rpc.invoke('someFunction', { id: 1 }) as never)
82
+ }
83
+ `)
84
+ const hit = criticals.find(
85
+ (c) => c.code === ErrorCode.RPC_INVOCATION_TYPE_CAST
86
+ )
87
+ assert.ok(hit, `Expected PKU940 but got: ${JSON.stringify(criticals)}`)
88
+ })
89
+
90
+ test('does not flag a clean rpc.invoke() call', async () => {
91
+ const criticals = await runInspect(`
92
+ declare const rpc: { invoke: (name: string, data: unknown) => Promise<unknown> }
93
+ export async function doWork() {
94
+ return rpc.invoke('someFunction', { id: 1 })
95
+ }
96
+ `)
97
+ const hit = criticals.find(
98
+ (c) => c.code === ErrorCode.RPC_INVOCATION_TYPE_CAST
99
+ )
100
+ assert.equal(
101
+ hit,
102
+ undefined,
103
+ `Expected no PKU940 but got: ${JSON.stringify(hit)}`
104
+ )
105
+ })
106
+
107
+ test('does not flag as-casts on unrelated calls', async () => {
108
+ const criticals = await runInspect(`
109
+ declare function otherFn(data: unknown): Promise<unknown>
110
+ export async function doWork() {
111
+ return otherFn({ id: 1 } as any)
112
+ }
113
+ `)
114
+ const hit = criticals.find(
115
+ (c) => c.code === ErrorCode.RPC_INVOCATION_TYPE_CAST
116
+ )
117
+ assert.equal(
118
+ hit,
119
+ undefined,
120
+ `Expected no PKU940 but got: ${JSON.stringify(hit)}`
121
+ )
122
+ })
123
+ })
@@ -88,6 +88,8 @@ export enum ErrorCode {
88
88
  // Addon authoring errors
89
89
  ADDON_WIRING_NOT_ALLOWED = 'PKU920',
90
90
  ADDON_CONTRACT_HANDLERS_NOT_ALLOWED = 'PKU921',
91
+
92
+ RPC_INVOCATION_TYPE_CAST = 'PKU940',
91
93
  }
92
94
 
93
95
  /**
package/src/types.ts CHANGED
@@ -263,6 +263,10 @@ export type InspectorFilters = {
263
263
  // to 'server'. Sourced from `pikku.config.json` →
264
264
  // `deploy.serverlessIncompatible`. Used only when deploy filters are set.
265
265
  serverlessIncompatible?: string[]
266
+ // Default deploy target for functions without an explicit `deploy` flag.
267
+ // Sourced from `pikku.config.json` → `deploy.defaultTarget`. Used only
268
+ // when deploy filters are set. Defaults to 'serverless'.
269
+ defaultTarget?: 'serverless' | 'server'
266
270
  }
267
271
 
268
272
  export type AddonConfig = {
@@ -340,9 +340,15 @@ export function filterInspectorState(
340
340
  ? new Set(filters.excludeTarget)
341
341
  : null
342
342
  const incompatible = new Set(filters.serverlessIncompatible ?? [])
343
+ const defaultTarget = filters.defaultTarget ?? 'serverless'
343
344
  keptByDeploy = new Set<string>()
344
345
  for (const [funcId, funcMeta] of Object.entries(state.functions.meta)) {
345
- const target = resolveDeployTarget(funcMeta as any, incompatible, funcId)
346
+ const target = resolveDeployTarget(
347
+ funcMeta as any,
348
+ incompatible,
349
+ funcId,
350
+ defaultTarget
351
+ )
346
352
  if (allowed && !allowed.has(target)) continue
347
353
  if (excluded && excluded.has(target)) continue
348
354
  keptByDeploy.add(funcId)
@@ -1062,10 +1068,10 @@ export function filterInspectorState(
1062
1068
  }
1063
1069
 
1064
1070
  // Step dispatch is decided purely per-function: a workflow step runs via the
1065
- // queue only when its function opts out of inline execution (inline: false).
1066
- // Such a unit needs workflowService + queueService injected even though the
1067
- // function itself doesn't reference them. Check the ORIGINAL graph meta
1068
- // (before filtering pruned it).
1071
+ // queue only when its function is marked `workflowQueued: true`. Such a unit
1072
+ // needs workflowService + queueService injected even though the function
1073
+ // itself doesn't reference them. Check the ORIGINAL graph meta (before
1074
+ // filtering pruned it).
1069
1075
  const survivingFuncIds = new Set(Object.keys(filteredState.functions.meta))
1070
1076
  const resolveFuncId = (rpcName: string): string =>
1071
1077
  filteredState.rpc.internalMeta[rpcName] ??
@@ -1081,8 +1087,8 @@ export function filterInspectorState(
1081
1087
  if (!survivingFuncIds.has(funcId) && !survivingFuncIds.has(rpcName))
1082
1088
  continue
1083
1089
  const funcMeta = (filteredState.functions.meta[funcId] ??
1084
- filteredState.functions.meta[rpcName]) as { inline?: boolean }
1085
- if (funcMeta?.inline === false) {
1090
+ filteredState.functions.meta[rpcName]) as { workflowQueued?: boolean }
1091
+ if (funcMeta?.workflowQueued === true) {
1086
1092
  filteredState.serviceAggregation.requiredServices.add('workflowService')
1087
1093
  filteredState.serviceAggregation.requiredServices.add('queueService')
1088
1094
  }
@@ -290,10 +290,17 @@ export function aggregateRequiredServices(
290
290
  }
291
291
  }
292
292
 
293
- // Per-step queues
293
+ // Per-step queues — only for steps explicitly marked workflowQueued: true
294
294
  for (const node of Object.values(graph.nodes)) {
295
295
  if (!('rpcName' in node) || !node.rpcName) continue
296
296
  const rpcName = node.rpcName as string
297
+ const funcId =
298
+ state.rpc?.internalMeta?.[rpcName] ??
299
+ state.rpc?.exposedMeta?.[rpcName] ??
300
+ rpcName
301
+ const funcMeta = (state.functions.meta[funcId] ??
302
+ state.functions.meta[rpcName]) as { workflowQueued?: boolean }
303
+ if (funcMeta?.workflowQueued !== true) continue
297
304
  const stepQueueName = `wf-step-${toKebab(rpcName)}`
298
305
  if (!state.queueWorkers.meta[stepQueueName]) {
299
306
  state.queueWorkers.meta[stepQueueName] = {
@@ -102,4 +102,34 @@ describe('resolveDeployTarget', () => {
102
102
  'server'
103
103
  )
104
104
  })
105
+
106
+ test('defaultTarget: server overrides the serverless default', () => {
107
+ assert.strictEqual(
108
+ resolveDeployTarget({}, new Set(), '<unknown>', 'server'),
109
+ 'server'
110
+ )
111
+ })
112
+
113
+ test('explicit deploy flag wins over defaultTarget', () => {
114
+ assert.strictEqual(
115
+ resolveDeployTarget({ deploy: 'serverless' }, new Set(), 'fn', 'server'),
116
+ 'serverless'
117
+ )
118
+ assert.strictEqual(
119
+ resolveDeployTarget({ deploy: 'server' }, new Set(), 'fn', 'serverless'),
120
+ 'server'
121
+ )
122
+ })
123
+
124
+ test('serverlessIncompatible still forces server regardless of defaultTarget', () => {
125
+ assert.strictEqual(
126
+ resolveDeployTarget(
127
+ { services: { services: ['metaService'] } as any },
128
+ new Set(['metaService']),
129
+ 'fn',
130
+ 'serverless'
131
+ ),
132
+ 'server'
133
+ )
134
+ })
105
135
  })
@@ -29,7 +29,8 @@ export class IncompatibleDeployTargetError extends Error {
29
29
  * - throw if the function explicitly declares `deploy: 'serverless'`
30
30
  * - otherwise target is 'server'
31
31
  * 2. Explicit `funcMeta.deploy: 'serverless' | 'server'`
32
- * 3. Default 'serverless'
32
+ * 3. `defaultTarget` (sourced from `pikku.config.json` →
33
+ * `deploy.defaultTarget`, falling back to 'serverless')
33
34
  *
34
35
  * Used both by the per-unit deploy analyzer (when bucketing functions
35
36
  * into deployment units) and by `filterInspectorState` (when
@@ -39,7 +40,8 @@ export class IncompatibleDeployTargetError extends Error {
39
40
  export function resolveDeployTarget(
40
41
  funcMeta: Pick<FunctionMeta, 'deploy' | 'services'>,
41
42
  serverlessIncompatible: Set<string>,
42
- functionName = '<unknown>'
43
+ functionName = '<unknown>',
44
+ defaultTarget: 'serverless' | 'server' = 'serverless'
43
45
  ): 'serverless' | 'server' {
44
46
  // Service compatibility wins over the explicit flag — a serverless
45
47
  // bundle of a function that needs (e.g.) node:fs would crash at runtime.
@@ -59,5 +61,5 @@ export function resolveDeployTarget(
59
61
 
60
62
  if (funcMeta.deploy === 'server') return 'server'
61
63
  if (funcMeta.deploy === 'serverless') return 'serverless'
62
- return 'serverless'
64
+ return defaultTarget
63
65
  }
@@ -87,8 +87,6 @@ export interface NodeOptions {
87
87
  retryDelay?: string
88
88
  /** Timeout for node execution (e.g., '30s', '5m') */
89
89
  timeout?: string
90
- /** If true, execute via queue (async). Default: false (inline) */
91
- async?: boolean
92
90
  }
93
91
 
94
92
  /**