@ts-cloud/core 0.5.7 → 0.5.9

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/dist/index.js CHANGED
@@ -45597,6 +45597,28 @@ function composeServerlessAppTemplate(opts) {
45597
45597
  addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900, undefined, app.cliTmpStorage ?? tmpStorage);
45598
45598
  if (hasQueue)
45599
45599
  addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120, undefined, app.queueTmpStorage ?? tmpStorage);
45600
+ const pc = (app.provisionedConcurrency ?? 0) > 0 ? app.provisionedConcurrency : 0;
45601
+ const fnLogicalIds = ["HttpFunction", "CliFunction", ...hasQueue ? ["QueueFunction"] : []];
45602
+ if (pc) {
45603
+ for (const L of fnLogicalIds) {
45604
+ resources[`${L}Version`] = {
45605
+ Type: "AWS::Lambda::Version",
45606
+ DeletionPolicy: "Retain",
45607
+ Properties: { FunctionName: Fn2.ref(L) }
45608
+ };
45609
+ resources[`${L}Alias`] = {
45610
+ Type: "AWS::Lambda::Alias",
45611
+ Properties: {
45612
+ FunctionName: Fn2.ref(L),
45613
+ Name: "live",
45614
+ FunctionVersion: Fn2.getAtt(`${L}Version`, "Version"),
45615
+ ProvisionedConcurrencyConfig: { ProvisionedConcurrentExecutions: pc }
45616
+ }
45617
+ };
45618
+ }
45619
+ }
45620
+ const invokeArn = (L) => pc ? Fn2.ref(`${L}Alias`) : Fn2.getAtt(L, "Arn");
45621
+ const invokeName = (L) => pc ? Fn2.ref(`${L}Alias`) : Fn2.ref(L);
45600
45622
  resources.HttpApi = {
45601
45623
  Type: "AWS::ApiGatewayV2::Api",
45602
45624
  Properties: {
@@ -45609,7 +45631,7 @@ function composeServerlessAppTemplate(opts) {
45609
45631
  Properties: {
45610
45632
  ApiId: Fn2.ref("HttpApi"),
45611
45633
  IntegrationType: "AWS_PROXY",
45612
- IntegrationUri: Fn2.getAtt("HttpFunction", "Arn"),
45634
+ IntegrationUri: invokeArn("HttpFunction"),
45613
45635
  PayloadFormatVersion: "2.0"
45614
45636
  }
45615
45637
  };
@@ -45632,7 +45654,7 @@ function composeServerlessAppTemplate(opts) {
45632
45654
  resources.HttpPermission = {
45633
45655
  Type: "AWS::Lambda::Permission",
45634
45656
  Properties: {
45635
- FunctionName: Fn2.ref("HttpFunction"),
45657
+ FunctionName: invokeName("HttpFunction"),
45636
45658
  Action: "lambda:InvokeFunction",
45637
45659
  Principal: "apigateway.amazonaws.com",
45638
45660
  SourceArn: Fn2.sub("arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*/*")
@@ -45723,7 +45745,7 @@ function composeServerlessAppTemplate(opts) {
45723
45745
  Type: "AWS::Lambda::EventSourceMapping",
45724
45746
  Properties: {
45725
45747
  EventSourceArn: Fn2.getAtt(qId, "Arn"),
45726
- FunctionName: Fn2.ref("QueueFunction"),
45748
+ FunctionName: invokeName("QueueFunction"),
45727
45749
  BatchSize: 1,
45728
45750
  FunctionResponseTypes: ["ReportBatchItemFailures"],
45729
45751
  ...concurrency ? { ScalingConfig: { MaximumConcurrency: Math.max(2, concurrency) } } : {}
@@ -45741,7 +45763,7 @@ function composeServerlessAppTemplate(opts) {
45741
45763
  State: "ENABLED",
45742
45764
  Targets: [{
45743
45765
  Id: "cli",
45744
- Arn: Fn2.getAtt("CliFunction", "Arn"),
45766
+ Arn: invokeArn("CliFunction"),
45745
45767
  Input: JSON.stringify({ command: "schedule:run" })
45746
45768
  }]
45747
45769
  }
@@ -45749,7 +45771,7 @@ function composeServerlessAppTemplate(opts) {
45749
45771
  resources.SchedulerPermission = {
45750
45772
  Type: "AWS::Lambda::Permission",
45751
45773
  Properties: {
45752
- FunctionName: Fn2.ref("CliFunction"),
45774
+ FunctionName: invokeName("CliFunction"),
45753
45775
  Action: "lambda:InvokeFunction",
45754
45776
  Principal: "events.amazonaws.com",
45755
45777
  SourceArn: Fn2.getAtt("SchedulerRule", "Arn")
@@ -45758,34 +45780,39 @@ function composeServerlessAppTemplate(opts) {
45758
45780
  }
45759
45781
  if (app.warm && app.warm > 0) {
45760
45782
  const TARGETS_PER_RULE = 5;
45761
- const ruleCount = Math.ceil(app.warm / TARGETS_PER_RULE);
45762
- let warmed = 0;
45763
- for (let r = 0;r < ruleCount; r++) {
45764
- const targets = Math.min(TARGETS_PER_RULE, app.warm - warmed);
45765
- resources[`WarmerRule${r}`] = {
45766
- Type: "AWS::Events::Rule",
45783
+ const fnResource = { http: "HttpFunction", queue: "QueueFunction", cli: "CliFunction" };
45784
+ const warmModes = (app.warmFunctions ?? ["http"]).filter((m) => m !== "queue" || hasQueue);
45785
+ for (const mode of warmModes) {
45786
+ const cap = mode.charAt(0).toUpperCase() + mode.slice(1);
45787
+ const ruleCount = Math.ceil(app.warm / TARGETS_PER_RULE);
45788
+ let warmed = 0;
45789
+ for (let r = 0;r < ruleCount; r++) {
45790
+ const targets = Math.min(TARGETS_PER_RULE, app.warm - warmed);
45791
+ resources[`Warmer${cap}Rule${r}`] = {
45792
+ Type: "AWS::Events::Rule",
45793
+ Properties: {
45794
+ Name: `${slug}-${environment}-warmer-${mode}-${r}`,
45795
+ ScheduleExpression: "rate(5 minutes)",
45796
+ State: "ENABLED",
45797
+ Targets: Array.from({ length: targets }, (_, i) => ({
45798
+ Id: `warm-${mode}-${r}-${i}`,
45799
+ Arn: Fn2.getAtt(fnResource[mode], "Arn"),
45800
+ Input: JSON.stringify({ warmer: true })
45801
+ }))
45802
+ }
45803
+ };
45804
+ warmed += targets;
45805
+ }
45806
+ resources[`Warmer${cap}Permission`] = {
45807
+ Type: "AWS::Lambda::Permission",
45767
45808
  Properties: {
45768
- Name: `${slug}-${environment}-warmer-${r}`,
45769
- ScheduleExpression: "rate(5 minutes)",
45770
- State: "ENABLED",
45771
- Targets: Array.from({ length: targets }, (_, i) => ({
45772
- Id: `warm-${r}-${i}`,
45773
- Arn: Fn2.getAtt("HttpFunction", "Arn"),
45774
- Input: JSON.stringify({ warmer: true })
45775
- }))
45809
+ FunctionName: Fn2.ref(fnResource[mode]),
45810
+ Action: "lambda:InvokeFunction",
45811
+ Principal: "events.amazonaws.com",
45812
+ SourceArn: Fn2.sub("arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/" + `${slug}-${environment}-warmer-${mode}-*`)
45776
45813
  }
45777
45814
  };
45778
- warmed += targets;
45779
45815
  }
45780
- resources.WarmerPermission = {
45781
- Type: "AWS::Lambda::Permission",
45782
- Properties: {
45783
- FunctionName: Fn2.ref("HttpFunction"),
45784
- Action: "lambda:InvokeFunction",
45785
- Principal: "events.amazonaws.com",
45786
- SourceArn: Fn2.sub("arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/" + `${slug}-${environment}-warmer-*`)
45787
- }
45788
- };
45789
45816
  }
45790
45817
  if (cacheEnabled) {
45791
45818
  resources.CacheTable = {
@@ -46407,6 +46434,8 @@ function parseRecordBody(body) {
46407
46434
  function createQueueHandler(handler8) {
46408
46435
  return async (event) => {
46409
46436
  const batchItemFailures = [];
46437
+ if (event.warmer)
46438
+ return { batchItemFailures };
46410
46439
  if (!handler8)
46411
46440
  return { batchItemFailures };
46412
46441
  for (const record of event.Records ?? []) {
@@ -46421,6 +46450,8 @@ function createQueueHandler(handler8) {
46421
46450
  }
46422
46451
  function createCliHandler(handler8) {
46423
46452
  return async (event) => {
46453
+ if (event.warmer)
46454
+ return { statusCode: 0, output: "warm" };
46424
46455
  if (!handler8)
46425
46456
  return { statusCode: 501, output: "No CLI handler configured" };
46426
46457
  return handler8(event);
@@ -245,6 +245,9 @@ function parseRecordBody(body: string): unknown {
245
245
  export function createQueueHandler(handler: JobHandler | undefined): LambdaQueueHandler {
246
246
  return async (event: SqsEvent): Promise<SqsBatchResponse> => {
247
247
  const batchItemFailures: Array<{ itemIdentifier: string }> = []
248
+ // Warmer pings invoke the function directly (no SQS Records) — keep the
249
+ // container warm without treating the ping as a job.
250
+ if ((event as unknown as { warmer?: boolean }).warmer) return { batchItemFailures }
248
251
  if (!handler) return { batchItemFailures }
249
252
 
250
253
  for (const record of event.Records ?? []) {
@@ -268,6 +271,8 @@ export function createQueueHandler(handler: JobHandler | undefined): LambdaQueue
268
271
  */
269
272
  export function createCliHandler(handler: CommandHandler | undefined): LambdaCliHandler {
270
273
  return async (event: CliEvent): Promise<CliResult> => {
274
+ // Warmer pings keep the container warm without running a command.
275
+ if ((event as unknown as { warmer?: boolean }).warmer) return { statusCode: 0, output: 'warm' }
271
276
  if (!handler) return { statusCode: 501, output: 'No CLI handler configured' }
272
277
  return handler(event)
273
278
  }
@@ -36,6 +36,11 @@ while (true) {
36
36
 
37
37
  function dispatch(array $event, string $artisan, string $queueCommand): array
38
38
  {
39
+ // Warmer pings keep the container warm without running anything.
40
+ if (($event['warmer'] ?? false) === true) {
41
+ return ['warm' => true];
42
+ }
43
+
39
44
  // SQS queue event.
40
45
  if (isset($event['Records']) && is_array($event['Records'])) {
41
46
  $failures = [];
package/dist/types.d.ts CHANGED
@@ -1585,11 +1585,25 @@ export interface ServerlessAppConfig {
1585
1585
  */
1586
1586
  gatewayVersion?: 1 | 2;
1587
1587
  /**
1588
- * Keep-warm count. Drives a scheduled EventBridge warmer rule that pings the
1589
- * HTTP function every few minutes (the runtime short-circuits warmer pings).
1590
- * 0/undefined disables warming.
1588
+ * Cheap keep-warm count. Drives a scheduled EventBridge warmer rule that pings
1589
+ * the function(s) every few minutes (the runtime short-circuits warmer pings).
1590
+ * Reduces (does not eliminate) cold starts. For zero cold starts use
1591
+ * {@link provisionedConcurrency}. 0/undefined disables ping-warming.
1591
1592
  */
1592
1593
  warm?: number;
1594
+ /**
1595
+ * Which functions the warmer keeps warm. @default ['http']
1596
+ * (queue/cli are async + latency-tolerant, so HTTP-only is the sensible default).
1597
+ */
1598
+ warmFunctions?: Array<'http' | 'queue' | 'cli'>;
1599
+ /**
1600
+ * Real provisioned concurrency (zero cold starts for N environments). Opts the
1601
+ * app into the alias/version model: each function gets a `live` alias that
1602
+ * traffic routes through, and every deploy publishes a version + flips the
1603
+ * alias. Costs more than `warm` (you pay for the reserved capacity) but
1604
+ * eliminates cold starts. 0/undefined keeps the default $LATEST model.
1605
+ */
1606
+ provisionedConcurrency?: number;
1593
1607
  /** CloudWatch log retention (days) for all function log groups. @default 14 */
1594
1608
  logRetention?: number;
1595
1609
  /** CLI function memory in MB. @default 1024 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ts-cloud/core",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "type": "module",
5
5
  "description": "Core CloudFormation generation library for ts-cloud",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
@@ -31,7 +31,7 @@
31
31
  "typecheck": "tsc --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@ts-cloud/aws-types": "0.5.7"
34
+ "@ts-cloud/aws-types": "0.5.9"
35
35
  },
36
36
  "devDependencies": {
37
37
  "typescript": "^5.9.3"