@pikku/inspector 0.12.28 → 0.12.31

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.
@@ -46,12 +46,19 @@ export interface SerializableInspectorState {
46
46
  >
47
47
  wireServicesMeta: Array<[string, string[]]>
48
48
  addonRequiredParentServices: string[]
49
+ addonServerlessIncompatible: Array<[string, string[]]>
49
50
  configFactories: Array<
50
51
  [
51
52
  string,
52
53
  { variable: string; type: string | null; typePath: string | null }[],
53
54
  ]
54
55
  >
56
+ serverLifecycleFactories: Array<
57
+ [
58
+ string,
59
+ { variable: string; type: string | null; typePath: string | null }[],
60
+ ]
61
+ >
55
62
  filesAndMethods: InspectorState['filesAndMethods']
56
63
  filesAndMethodsErrors: Array<
57
64
  [
@@ -312,7 +319,13 @@ export function serializeInspectorState(
312
319
  wireServicesFactories: Array.from(state.wireServicesFactories.entries()),
313
320
  wireServicesMeta: Array.from(state.wireServicesMeta.entries()),
314
321
  addonRequiredParentServices: state.addonRequiredParentServices,
322
+ addonServerlessIncompatible: Array.from(
323
+ state.addonServerlessIncompatible.entries()
324
+ ),
315
325
  configFactories: Array.from(state.configFactories.entries()),
326
+ serverLifecycleFactories: Array.from(
327
+ state.serverLifecycleFactories.entries()
328
+ ),
316
329
  filesAndMethods: state.filesAndMethods,
317
330
  filesAndMethodsErrors: Array.from(
318
331
  state.filesAndMethodsErrors.entries()
@@ -491,7 +504,11 @@ export function deserializeInspectorState(
491
504
  wireServicesFactories: new Map(data.wireServicesFactories),
492
505
  wireServicesMeta: new Map(data.wireServicesMeta),
493
506
  addonRequiredParentServices: data.addonRequiredParentServices || [],
507
+ addonServerlessIncompatible: new Map(
508
+ data.addonServerlessIncompatible || []
509
+ ),
494
510
  configFactories: new Map(data.configFactories),
511
+ serverLifecycleFactories: new Map(data.serverLifecycleFactories),
495
512
  filesAndMethods: data.filesAndMethods,
496
513
  filesAndMethodsErrors: new Map(
497
514
  data.filesAndMethodsErrors.map(([key, entries]) => [
@@ -119,4 +119,22 @@ describe('deriveWorkflowPlan', () => {
119
119
  assert.equal(plan.deterministic, true)
120
120
  assert.deepEqual(plan.plannedSteps, [{ stepName: 'a' }])
121
121
  })
122
+
123
+ test('suspend steps appear in the plan with __workflow_suspend: key and sentence-case displayName', () => {
124
+ const plan = deriveWorkflowPlan([
125
+ rpc('build'),
126
+ { type: 'suspend', reason: 'building' } as WorkflowStepMeta,
127
+ rpc('publish'),
128
+ { type: 'suspend', reason: 'awaiting_approval' } as WorkflowStepMeta,
129
+ { type: 'suspend', reason: 'building-image' } as WorkflowStepMeta,
130
+ ])
131
+ assert.equal(plan.deterministic, true)
132
+ assert.deepEqual(plan.plannedSteps, [
133
+ { stepName: 'build' },
134
+ { stepName: '__workflow_suspend:building', displayName: 'Building' },
135
+ { stepName: 'publish' },
136
+ { stepName: '__workflow_suspend:awaiting_approval', displayName: 'Awaiting approval' },
137
+ { stepName: '__workflow_suspend:building-image', displayName: 'Building image' },
138
+ ])
139
+ })
122
140
  })
@@ -58,6 +58,16 @@ function containsConditional(steps: WorkflowStepMeta[]): boolean {
58
58
  return steps.some((step) => step.type === 'branch' || step.type === 'switch')
59
59
  }
60
60
 
61
+ /**
62
+ * Turn an internal snake/kebab reason key into a sentence-case display label.
63
+ * e.g. "awaiting_approval" → "Awaiting approval", "building-image" → "Building image"
64
+ */
65
+ function reasonToLabel(reason: string): string {
66
+ return reason
67
+ .replace(/[-_]/g, ' ')
68
+ .replace(/^(.)/, (c) => c.toUpperCase())
69
+ }
70
+
61
71
  /** Flatten named steps (rpc/inline/sleep/parallel children) in source order. */
62
72
  function collectNamedSteps(steps: WorkflowStepMeta[]): WorkflowPlannedStep[] {
63
73
  const planned: WorkflowPlannedStep[] = []
@@ -68,6 +78,16 @@ function collectNamedSteps(steps: WorkflowStepMeta[]): WorkflowPlannedStep[] {
68
78
  case 'sleep':
69
79
  planned.push({ stepName: step.stepName })
70
80
  break
81
+ case 'suspend':
82
+ // Runtime stores the suspend step as `__workflow_suspend:${reason}`.
83
+ // We surface it in the planned ladder with a readable displayName so
84
+ // the UI shows it in the right position instead of appending it at the
85
+ // bottom as an unrecognised orphan.
86
+ planned.push({
87
+ stepName: `__workflow_suspend:${step.reason}`,
88
+ displayName: reasonToLabel(step.reason),
89
+ })
90
+ break
71
91
  case 'parallel':
72
92
  for (const child of step.children) {
73
93
  planned.push({ stepName: child.stepName })
@@ -7,6 +7,7 @@ import type {
7
7
  ParallelGroupStepMeta,
8
8
  FanoutStepMeta,
9
9
  CancelStepMeta,
10
+ SuspendStepMeta,
10
11
  SetStepMeta,
11
12
  SwitchStepMeta,
12
13
  SwitchCaseMeta,
@@ -21,6 +22,7 @@ import type {
21
22
  import {
22
23
  isWorkflowDoCall,
23
24
  isWorkflowSleepCall,
25
+ isWorkflowSuspendCall,
24
26
  isThrowCancelException,
25
27
  extractCancelReason,
26
28
  isParallelFanout,
@@ -512,6 +514,10 @@ function extractExpressionStatement(
512
514
  return extractSleepStep(call, context)
513
515
  }
514
516
 
517
+ if (isWorkflowSuspendCall(call, context.checker)) {
518
+ return extractSuspendStep(call, context)
519
+ }
520
+
515
521
  // Check for parallel group or fanout
516
522
  if (isParallelFanout(call)) {
517
523
  return extractParallelFanout(call, context)
@@ -698,6 +704,31 @@ function extractSleepStep(
698
704
  }
699
705
  }
700
706
 
707
+ /**
708
+ * Extract suspend step from workflow.suspend() call
709
+ */
710
+ function extractSuspendStep(
711
+ call: ts.CallExpression,
712
+ context: ExtractionContext
713
+ ): SuspendStepMeta | null {
714
+ const args = call.arguments
715
+ if (args.length < 1) return null
716
+
717
+ try {
718
+ const reason = extractStringLiteral(args[0], context.checker)
719
+ return {
720
+ type: 'suspend',
721
+ reason,
722
+ }
723
+ } catch (error) {
724
+ context.errors.push({
725
+ message: `Failed to extract suspend step: ${error instanceof Error ? error.message : String(error)}`,
726
+ node: call,
727
+ })
728
+ return null
729
+ }
730
+ }
731
+
701
732
  /**
702
733
  * Extract cancel step from throw WorkflowCancelledException statement
703
734
  */
@@ -42,6 +42,25 @@ export function isWorkflowSleepCall(
42
42
  )
43
43
  }
44
44
 
45
+ /**
46
+ * Check if a call expression is workflow.suspend()
47
+ */
48
+ export function isWorkflowSuspendCall(
49
+ node: ts.CallExpression,
50
+ _checker: ts.TypeChecker
51
+ ): boolean {
52
+ if (!ts.isPropertyAccessExpression(node.expression)) {
53
+ return false
54
+ }
55
+
56
+ const propAccess = node.expression
57
+ return (
58
+ propAccess.name.text === 'suspend' &&
59
+ ts.isIdentifier(propAccess.expression) &&
60
+ propAccess.expression.text === 'workflow'
61
+ )
62
+ }
63
+
45
64
  /**
46
65
  * Check if a throw statement throws WorkflowCancelledException
47
66
  * Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
package/src/visit.ts CHANGED
@@ -87,6 +87,12 @@ export const visitSetup = (
87
87
  )
88
88
 
89
89
  addFileWithFactory(node, checker, state.configFactories, 'CreateConfig')
90
+ addFileWithFactory(
91
+ node,
92
+ checker,
93
+ state.serverLifecycleFactories,
94
+ 'ServerLifecycle'
95
+ )
90
96
 
91
97
  addRPCInvocations(node, state, logger)
92
98
  addWireAddon(node, state, logger)