@backstage/backend-defaults 0.9.0 → 0.10.0-next.1

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 (31) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/config.d.ts +71 -0
  3. package/dist/cache.d.ts +2 -1
  4. package/dist/entrypoints/auth/DefaultAuthService.cjs.js.map +1 -1
  5. package/dist/entrypoints/cache/CacheManager.cjs.js +33 -3
  6. package/dist/entrypoints/cache/CacheManager.cjs.js.map +1 -1
  7. package/dist/entrypoints/scheduler/database/migrateBackendTasks.cjs.js +5 -4
  8. package/dist/entrypoints/scheduler/database/migrateBackendTasks.cjs.js.map +1 -1
  9. package/dist/entrypoints/scheduler/database/tables.cjs.js.map +1 -1
  10. package/dist/entrypoints/scheduler/lib/DefaultSchedulerService.cjs.js +5 -2
  11. package/dist/entrypoints/scheduler/lib/DefaultSchedulerService.cjs.js.map +1 -1
  12. package/dist/entrypoints/scheduler/lib/LocalTaskWorker.cjs.js +58 -10
  13. package/dist/entrypoints/scheduler/lib/LocalTaskWorker.cjs.js.map +1 -1
  14. package/dist/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.cjs.js +42 -5
  15. package/dist/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.cjs.js.map +1 -1
  16. package/dist/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.cjs.js +6 -2
  17. package/dist/entrypoints/scheduler/lib/PluginTaskSchedulerJanitor.cjs.js.map +1 -1
  18. package/dist/entrypoints/scheduler/lib/TaskWorker.cjs.js +60 -10
  19. package/dist/entrypoints/scheduler/lib/TaskWorker.cjs.js.map +1 -1
  20. package/dist/entrypoints/scheduler/lib/types.cjs.js.map +1 -1
  21. package/dist/entrypoints/scheduler/lib/util.cjs.js +16 -1
  22. package/dist/entrypoints/scheduler/lib/util.cjs.js.map +1 -1
  23. package/dist/entrypoints/scheduler/schedulerServiceFactory.cjs.js +17 -3
  24. package/dist/entrypoints/scheduler/schedulerServiceFactory.cjs.js.map +1 -1
  25. package/dist/entrypoints/urlReader/lib/GitlabUrlReader.cjs.js +11 -8
  26. package/dist/entrypoints/urlReader/lib/GitlabUrlReader.cjs.js.map +1 -1
  27. package/dist/package.json.cjs.js +2 -1
  28. package/dist/package.json.cjs.js.map +1 -1
  29. package/dist/scheduler.d.ts +4 -2
  30. package/migrations/scheduler/20250411000000_last_run.js +47 -0
  31. package/package.json +18 -17
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @backstage/backend-defaults
2
2
 
3
+ ## 0.10.0-next.1
4
+
5
+ ### Minor Changes
6
+
7
+ - d385854: **BREAKING**: The `DefaultSchedulerService` constructor options now requires `RootLifecycleService`, `HttpRouterService`, and `PluginMetadataService` fields.
8
+
9
+ The scheduler will register a REST API for listing and triggering tasks. Please see [the scheduler documentation](https://backstage.io/docs/backend-system/core-services/scheduler) for more details about this API.
10
+
11
+ ### Patch Changes
12
+
13
+ - acea1d4: update documentation
14
+ - 72d019d: Removed various typos
15
+ - c6bc67d: Added Valkey support alongside Redis in backend-defaults cache clients, using the new Keyv Valkey package. Also extended backend-test-utils to support Valkey in tests.
16
+ - 36f77e9: Bug fix: Pass user provided token through to gitlab url resolvers
17
+ - Updated dependencies
18
+ - @backstage/backend-app-api@1.2.3-next.1
19
+ - @backstage/plugin-auth-node@0.6.3-next.1
20
+ - @backstage/backend-plugin-api@1.3.1-next.1
21
+ - @backstage/integration@1.16.4-next.1
22
+ - @backstage/plugin-permission-node@0.10.0-next.1
23
+ - @backstage/config-loader@1.10.1-next.0
24
+ - @backstage/backend-dev-utils@0.1.5
25
+ - @backstage/cli-node@0.2.13
26
+ - @backstage/config@1.3.2
27
+ - @backstage/errors@1.2.7
28
+ - @backstage/integration-aws-node@0.1.15
29
+ - @backstage/types@1.2.1
30
+ - @backstage/plugin-events-node@0.4.11-next.1
31
+
32
+ ## 0.9.1-next.0
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies
37
+ - @backstage/integration@1.16.4-next.0
38
+ - @backstage/backend-app-api@1.2.3-next.0
39
+ - @backstage/backend-plugin-api@1.3.1-next.0
40
+ - @backstage/cli-node@0.2.13
41
+ - @backstage/config-loader@1.10.0
42
+ - @backstage/plugin-auth-node@0.6.3-next.0
43
+ - @backstage/plugin-events-node@0.4.11-next.0
44
+ - @backstage/plugin-permission-node@0.9.2-next.0
45
+ - @backstage/backend-dev-utils@0.1.5
46
+ - @backstage/config@1.3.2
47
+ - @backstage/errors@1.2.7
48
+ - @backstage/integration-aws-node@0.1.15
49
+ - @backstage/types@1.2.1
50
+
3
51
  ## 0.9.0
4
52
 
5
53
  ### Minor Changes
package/config.d.ts CHANGED
@@ -660,6 +660,77 @@ export interface Config {
660
660
  };
661
661
  };
662
662
  }
663
+ | {
664
+ store: 'valkey';
665
+ /**
666
+ * A valkey connection string in the form `redis://user:pass@host:port`.
667
+ * @visibility secret
668
+ */
669
+ connection: string;
670
+ /** An optional default TTL (in milliseconds, if given as a number). */
671
+ defaultTtl?: number | HumanDuration | string;
672
+ valkey?: {
673
+ /**
674
+ * An optional Valkey client configuration. These options are passed to the `@keyv/valkey` client.
675
+ */
676
+ client?: {
677
+ /**
678
+ * Namespace for the current instance.
679
+ */
680
+ namespace?: string;
681
+ /**
682
+ * Separator to use between namespace and key.
683
+ */
684
+ keyPrefixSeparator?: string;
685
+ /**
686
+ * Number of keys to delete in a single batch.
687
+ */
688
+ clearBatchSize?: number;
689
+ /**
690
+ * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
691
+ */
692
+ useUnlink?: boolean;
693
+ /**
694
+ * Whether to allow clearing all keys when no namespace is set.
695
+ * If set to true and no namespace is set, iterate() will return all keys.
696
+ * Defaults to `false`.
697
+ */
698
+ noNamespaceAffectsAll?: boolean;
699
+ };
700
+ /**
701
+ * An optional Valkey cluster (redis cluster under the hood) configuration.
702
+ */
703
+ cluster?: {
704
+ /**
705
+ * Cluster configuration options to be passed to the `@keyv/valkey` client (and node-redis under the hood)
706
+ * https://github.com/redis/node-redis/blob/master/docs/clustering.md
707
+ *
708
+ * @visibility secret
709
+ */
710
+ rootNodes: Array<object>;
711
+ /**
712
+ * Cluster node default configuration options to be passed to the `@keyv/redis` client (and node-redis under the hood)
713
+ * https://github.com/redis/node-redis/blob/master/docs/clustering.md
714
+ *
715
+ * @visibility secret
716
+ */
717
+ defaults?: Partial<object>;
718
+ /**
719
+ * When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes.
720
+ * Useful for short-term or PubSub-only connections.
721
+ */
722
+ minimizeConnections?: boolean;
723
+ /**
724
+ * When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes.
725
+ */
726
+ useReplicas?: boolean;
727
+ /**
728
+ * The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors.
729
+ */
730
+ maxCommandRedirections?: number;
731
+ };
732
+ };
733
+ }
663
734
  | {
664
735
  store: 'memcache';
665
736
  /**
package/dist/cache.d.ts CHANGED
@@ -58,7 +58,7 @@ declare class CacheManager {
58
58
  /**
59
59
  * Parse store-specific options from configuration.
60
60
  *
61
- * @param store - The cache store type ('redis', 'memcache', or 'memory')
61
+ * @param store - The cache store type ('redis', 'valkey', 'memcache', or 'memory')
62
62
  * @param config - The configuration service
63
63
  * @param logger - Optional logger for warnings
64
64
  * @returns The parsed store options
@@ -77,6 +77,7 @@ declare class CacheManager {
77
77
  forPlugin(pluginId: string): CacheService;
78
78
  private getClientWithTtl;
79
79
  private createRedisStoreFactory;
80
+ private createValkeyStoreFactory;
80
81
  private createMemcacheStoreFactory;
81
82
  private createMemoryStoreFactory;
82
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultAuthService.cjs.js","sources":["../../../src/entrypoints/auth/DefaultAuthService.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n BackstageNonePrincipal,\n BackstagePrincipalTypes,\n BackstageServicePrincipal,\n BackstageUserPrincipal,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError } from '@backstage/errors';\nimport { JsonObject } from '@backstage/types';\nimport { decodeJwt } from 'jose';\nimport { ExternalTokenHandler } from './external/ExternalTokenHandler';\nimport {\n createCredentialsWithNonePrincipal,\n createCredentialsWithServicePrincipal,\n createCredentialsWithUserPrincipal,\n toInternalBackstageCredentials,\n} from './helpers';\nimport { PluginTokenHandler } from './plugin/PluginTokenHandler';\nimport { PluginKeySource } from './plugin/keys/types';\nimport { UserTokenHandler } from './user/UserTokenHandler';\n\n/** @internal */\nexport class DefaultAuthService implements AuthService {\n constructor(\n private readonly userTokenHandler: UserTokenHandler,\n private readonly pluginTokenHandler: PluginTokenHandler,\n private readonly externalTokenHandler: ExternalTokenHandler,\n private readonly pluginId: string,\n private readonly disableDefaultAuthPolicy: boolean,\n private readonly pluginKeySource: PluginKeySource,\n ) {}\n\n async authenticate(\n token: string,\n options?: {\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials> {\n const pluginResult = await this.pluginTokenHandler.verifyToken(token);\n if (pluginResult) {\n if (pluginResult.limitedUserToken) {\n const userResult = await this.userTokenHandler.verifyToken(\n pluginResult.limitedUserToken,\n );\n if (!userResult) {\n throw new AuthenticationError(\n 'Invalid user token in plugin token obo claim',\n );\n }\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n pluginResult.limitedUserToken,\n this.#getJwtExpiration(pluginResult.limitedUserToken),\n pluginResult.subject,\n );\n }\n return createCredentialsWithServicePrincipal(pluginResult.subject);\n }\n\n const userResult = await this.userTokenHandler.verifyToken(token);\n if (userResult) {\n if (\n !options?.allowLimitedAccess &&\n this.userTokenHandler.isLimitedUserToken(token)\n ) {\n throw new AuthenticationError('Illegal limited user token');\n }\n\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n token,\n this.#getJwtExpiration(token),\n );\n }\n\n const externalResult = await this.externalTokenHandler.verifyToken(token);\n if (externalResult) {\n return createCredentialsWithServicePrincipal(\n externalResult.subject,\n undefined,\n externalResult.accessRestrictions,\n );\n }\n\n throw new AuthenticationError('Illegal token');\n }\n\n isPrincipal<TType extends keyof BackstagePrincipalTypes>(\n credentials: BackstageCredentials,\n type: TType,\n ): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]> {\n const principal = credentials.principal as\n | BackstageUserPrincipal\n | BackstageServicePrincipal;\n\n if (type === 'unknown') {\n return true;\n }\n\n if (principal.type !== type) {\n return false;\n }\n\n return true;\n }\n\n async getNoneCredentials(): Promise<\n BackstageCredentials<BackstageNonePrincipal>\n > {\n return createCredentialsWithNonePrincipal();\n }\n\n async getOwnServiceCredentials(): Promise<\n BackstageCredentials<BackstageServicePrincipal>\n > {\n return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);\n }\n\n async getPluginRequestToken(options: {\n onBehalfOf: BackstageCredentials;\n targetPluginId: string;\n }): Promise<{ token: string }> {\n const { targetPluginId } = options;\n const internalForward = toInternalBackstageCredentials(options.onBehalfOf);\n const { type } = internalForward.principal;\n\n // Since disabling the default policy means we'll be allowing\n // unauthenticated requests through, we might have unauthenticated\n // credentials from service calls that reach this point. If that's the case,\n // we'll want to keep \"forwarding\" the unauthenticated credentials, which we\n // do by returning an empty token.\n if (type === 'none' && this.disableDefaultAuthPolicy) {\n return { token: '' };\n }\n\n // check whether a plugin support the new auth system\n // by checking the public keys endpoint existance.\n switch (type) {\n // TODO: Check whether the principal is ourselves\n case 'service':\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n });\n case 'user': {\n const { token } = internalForward;\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(\n token,\n );\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n onBehalfOf: {\n limitedUserToken: onBehalfOf.token,\n expiresAt: onBehalfOf.expiresAt,\n },\n });\n }\n default:\n throw new AuthenticationError(\n `Refused to issue service token for credential type '${type}'`,\n );\n }\n }\n\n async getLimitedUserToken(\n credentials: BackstageCredentials<BackstageUserPrincipal>,\n ): Promise<{ token: string; expiresAt: Date }> {\n const { token: backstageToken } =\n toInternalBackstageCredentials(credentials);\n if (!backstageToken) {\n throw new AuthenticationError(\n 'User credentials is unexpectedly missing token',\n );\n }\n\n return this.userTokenHandler.createLimitedUserToken(backstageToken);\n }\n\n async listPublicServiceKeys(): Promise<{ keys: JsonObject[] }> {\n const { keys } = await this.pluginKeySource.listKeys();\n return { keys: keys.map(({ key }) => key) };\n }\n\n #getJwtExpiration(token: string) {\n const { exp } = decodeJwt(token);\n if (!exp) {\n throw new AuthenticationError('User token is missing expiration');\n }\n return new Date(exp * 1000);\n }\n}\n"],"names":["userResult","AuthenticationError","createCredentialsWithUserPrincipal","createCredentialsWithServicePrincipal","createCredentialsWithNonePrincipal","toInternalBackstageCredentials","decodeJwt"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultAuthService.cjs.js","sources":["../../../src/entrypoints/auth/DefaultAuthService.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuthService,\n BackstageCredentials,\n BackstageNonePrincipal,\n BackstagePrincipalTypes,\n BackstageServicePrincipal,\n BackstageUserPrincipal,\n} from '@backstage/backend-plugin-api';\nimport { AuthenticationError } from '@backstage/errors';\nimport { JsonObject } from '@backstage/types';\nimport { decodeJwt } from 'jose';\nimport { ExternalTokenHandler } from './external/ExternalTokenHandler';\nimport {\n createCredentialsWithNonePrincipal,\n createCredentialsWithServicePrincipal,\n createCredentialsWithUserPrincipal,\n toInternalBackstageCredentials,\n} from './helpers';\nimport { PluginTokenHandler } from './plugin/PluginTokenHandler';\nimport { PluginKeySource } from './plugin/keys/types';\nimport { UserTokenHandler } from './user/UserTokenHandler';\n\n/** @internal */\nexport class DefaultAuthService implements AuthService {\n constructor(\n private readonly userTokenHandler: UserTokenHandler,\n private readonly pluginTokenHandler: PluginTokenHandler,\n private readonly externalTokenHandler: ExternalTokenHandler,\n private readonly pluginId: string,\n private readonly disableDefaultAuthPolicy: boolean,\n private readonly pluginKeySource: PluginKeySource,\n ) {}\n\n async authenticate(\n token: string,\n options?: {\n allowLimitedAccess?: boolean;\n },\n ): Promise<BackstageCredentials> {\n const pluginResult = await this.pluginTokenHandler.verifyToken(token);\n if (pluginResult) {\n if (pluginResult.limitedUserToken) {\n const userResult = await this.userTokenHandler.verifyToken(\n pluginResult.limitedUserToken,\n );\n if (!userResult) {\n throw new AuthenticationError(\n 'Invalid user token in plugin token obo claim',\n );\n }\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n pluginResult.limitedUserToken,\n this.#getJwtExpiration(pluginResult.limitedUserToken),\n pluginResult.subject,\n );\n }\n return createCredentialsWithServicePrincipal(pluginResult.subject);\n }\n\n const userResult = await this.userTokenHandler.verifyToken(token);\n if (userResult) {\n if (\n !options?.allowLimitedAccess &&\n this.userTokenHandler.isLimitedUserToken(token)\n ) {\n throw new AuthenticationError('Illegal limited user token');\n }\n\n return createCredentialsWithUserPrincipal(\n userResult.userEntityRef,\n token,\n this.#getJwtExpiration(token),\n );\n }\n\n const externalResult = await this.externalTokenHandler.verifyToken(token);\n if (externalResult) {\n return createCredentialsWithServicePrincipal(\n externalResult.subject,\n undefined,\n externalResult.accessRestrictions,\n );\n }\n\n throw new AuthenticationError('Illegal token');\n }\n\n isPrincipal<TType extends keyof BackstagePrincipalTypes>(\n credentials: BackstageCredentials,\n type: TType,\n ): credentials is BackstageCredentials<BackstagePrincipalTypes[TType]> {\n const principal = credentials.principal as\n | BackstageUserPrincipal\n | BackstageServicePrincipal;\n\n if (type === 'unknown') {\n return true;\n }\n\n if (principal.type !== type) {\n return false;\n }\n\n return true;\n }\n\n async getNoneCredentials(): Promise<\n BackstageCredentials<BackstageNonePrincipal>\n > {\n return createCredentialsWithNonePrincipal();\n }\n\n async getOwnServiceCredentials(): Promise<\n BackstageCredentials<BackstageServicePrincipal>\n > {\n return createCredentialsWithServicePrincipal(`plugin:${this.pluginId}`);\n }\n\n async getPluginRequestToken(options: {\n onBehalfOf: BackstageCredentials;\n targetPluginId: string;\n }): Promise<{ token: string }> {\n const { targetPluginId } = options;\n const internalForward = toInternalBackstageCredentials(options.onBehalfOf);\n const { type } = internalForward.principal;\n\n // Since disabling the default policy means we'll be allowing\n // unauthenticated requests through, we might have unauthenticated\n // credentials from service calls that reach this point. If that's the case,\n // we'll want to keep \"forwarding\" the unauthenticated credentials, which we\n // do by returning an empty token.\n if (type === 'none' && this.disableDefaultAuthPolicy) {\n return { token: '' };\n }\n\n // check whether a plugin support the new auth system\n // by checking the public keys endpoint existence.\n switch (type) {\n // TODO: Check whether the principal is ourselves\n case 'service':\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n });\n case 'user': {\n const { token } = internalForward;\n if (!token) {\n throw new Error('User credentials is unexpectedly missing token');\n }\n const onBehalfOf = await this.userTokenHandler.createLimitedUserToken(\n token,\n );\n return this.pluginTokenHandler.issueToken({\n pluginId: this.pluginId,\n targetPluginId,\n onBehalfOf: {\n limitedUserToken: onBehalfOf.token,\n expiresAt: onBehalfOf.expiresAt,\n },\n });\n }\n default:\n throw new AuthenticationError(\n `Refused to issue service token for credential type '${type}'`,\n );\n }\n }\n\n async getLimitedUserToken(\n credentials: BackstageCredentials<BackstageUserPrincipal>,\n ): Promise<{ token: string; expiresAt: Date }> {\n const { token: backstageToken } =\n toInternalBackstageCredentials(credentials);\n if (!backstageToken) {\n throw new AuthenticationError(\n 'User credentials is unexpectedly missing token',\n );\n }\n\n return this.userTokenHandler.createLimitedUserToken(backstageToken);\n }\n\n async listPublicServiceKeys(): Promise<{ keys: JsonObject[] }> {\n const { keys } = await this.pluginKeySource.listKeys();\n return { keys: keys.map(({ key }) => key) };\n }\n\n #getJwtExpiration(token: string) {\n const { exp } = decodeJwt(token);\n if (!exp) {\n throw new AuthenticationError('User token is missing expiration');\n }\n return new Date(exp * 1000);\n }\n}\n"],"names":["userResult","AuthenticationError","createCredentialsWithUserPrincipal","createCredentialsWithServicePrincipal","createCredentialsWithNonePrincipal","toInternalBackstageCredentials","decodeJwt"],"mappings":";;;;;;AAuCO,MAAM,kBAA0C,CAAA;AAAA,EACrD,YACmB,gBACA,EAAA,kBAAA,EACA,oBACA,EAAA,QAAA,EACA,0BACA,eACjB,EAAA;AANiB,IAAA,IAAA,CAAA,gBAAA,GAAA,gBAAA;AACA,IAAA,IAAA,CAAA,kBAAA,GAAA,kBAAA;AACA,IAAA,IAAA,CAAA,oBAAA,GAAA,oBAAA;AACA,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AACA,IAAA,IAAA,CAAA,wBAAA,GAAA,wBAAA;AACA,IAAA,IAAA,CAAA,eAAA,GAAA,eAAA;AAAA;AAChB,EAEH,MAAM,YACJ,CAAA,KAAA,EACA,OAG+B,EAAA;AAC/B,IAAA,MAAM,YAAe,GAAA,MAAM,IAAK,CAAA,kBAAA,CAAmB,YAAY,KAAK,CAAA;AACpE,IAAA,IAAI,YAAc,EAAA;AAChB,MAAA,IAAI,aAAa,gBAAkB,EAAA;AACjC,QAAMA,MAAAA,WAAAA,GAAa,MAAM,IAAA,CAAK,gBAAiB,CAAA,WAAA;AAAA,UAC7C,YAAa,CAAA;AAAA,SACf;AACA,QAAA,IAAI,CAACA,WAAY,EAAA;AACf,UAAA,MAAM,IAAIC,0BAAA;AAAA,YACR;AAAA,WACF;AAAA;AAEF,QAAO,OAAAC,0CAAA;AAAA,UACLF,WAAW,CAAA,aAAA;AAAA,UACX,YAAa,CAAA,gBAAA;AAAA,UACb,IAAA,CAAK,iBAAkB,CAAA,YAAA,CAAa,gBAAgB,CAAA;AAAA,UACpD,YAAa,CAAA;AAAA,SACf;AAAA;AAEF,MAAO,OAAAG,6CAAA,CAAsC,aAAa,OAAO,CAAA;AAAA;AAGnE,IAAA,MAAM,UAAa,GAAA,MAAM,IAAK,CAAA,gBAAA,CAAiB,YAAY,KAAK,CAAA;AAChE,IAAA,IAAI,UAAY,EAAA;AACd,MAAA,IACE,CAAC,OAAS,EAAA,kBAAA,IACV,KAAK,gBAAiB,CAAA,kBAAA,CAAmB,KAAK,CAC9C,EAAA;AACA,QAAM,MAAA,IAAIF,2BAAoB,4BAA4B,CAAA;AAAA;AAG5D,MAAO,OAAAC,0CAAA;AAAA,QACL,UAAW,CAAA,aAAA;AAAA,QACX,KAAA;AAAA,QACA,IAAA,CAAK,kBAAkB,KAAK;AAAA,OAC9B;AAAA;AAGF,IAAA,MAAM,cAAiB,GAAA,MAAM,IAAK,CAAA,oBAAA,CAAqB,YAAY,KAAK,CAAA;AACxE,IAAA,IAAI,cAAgB,EAAA;AAClB,MAAO,OAAAC,6CAAA;AAAA,QACL,cAAe,CAAA,OAAA;AAAA,QACf,KAAA,CAAA;AAAA,QACA,cAAe,CAAA;AAAA,OACjB;AAAA;AAGF,IAAM,MAAA,IAAIF,2BAAoB,eAAe,CAAA;AAAA;AAC/C,EAEA,WAAA,CACE,aACA,IACqE,EAAA;AACrE,IAAA,MAAM,YAAY,WAAY,CAAA,SAAA;AAI9B,IAAA,IAAI,SAAS,SAAW,EAAA;AACtB,MAAO,OAAA,IAAA;AAAA;AAGT,IAAI,IAAA,SAAA,CAAU,SAAS,IAAM,EAAA;AAC3B,MAAO,OAAA,KAAA;AAAA;AAGT,IAAO,OAAA,IAAA;AAAA;AACT,EAEA,MAAM,kBAEJ,GAAA;AACA,IAAA,OAAOG,0CAAmC,EAAA;AAAA;AAC5C,EAEA,MAAM,wBAEJ,GAAA;AACA,IAAA,OAAOD,6CAAsC,CAAA,CAAA,OAAA,EAAU,IAAK,CAAA,QAAQ,CAAE,CAAA,CAAA;AAAA;AACxE,EAEA,MAAM,sBAAsB,OAGG,EAAA;AAC7B,IAAM,MAAA,EAAE,gBAAmB,GAAA,OAAA;AAC3B,IAAM,MAAA,eAAA,GAAkBE,sCAA+B,CAAA,OAAA,CAAQ,UAAU,CAAA;AACzE,IAAM,MAAA,EAAE,IAAK,EAAA,GAAI,eAAgB,CAAA,SAAA;AAOjC,IAAI,IAAA,IAAA,KAAS,MAAU,IAAA,IAAA,CAAK,wBAA0B,EAAA;AACpD,MAAO,OAAA,EAAE,OAAO,EAAG,EAAA;AAAA;AAKrB,IAAA,QAAQ,IAAM;AAAA;AAAA,MAEZ,KAAK,SAAA;AACH,QAAO,OAAA,IAAA,CAAK,mBAAmB,UAAW,CAAA;AAAA,UACxC,UAAU,IAAK,CAAA,QAAA;AAAA,UACf;AAAA,SACD,CAAA;AAAA,MACH,KAAK,MAAQ,EAAA;AACX,QAAM,MAAA,EAAE,OAAU,GAAA,eAAA;AAClB,QAAA,IAAI,CAAC,KAAO,EAAA;AACV,UAAM,MAAA,IAAI,MAAM,gDAAgD,CAAA;AAAA;AAElE,QAAM,MAAA,UAAA,GAAa,MAAM,IAAA,CAAK,gBAAiB,CAAA,sBAAA;AAAA,UAC7C;AAAA,SACF;AACA,QAAO,OAAA,IAAA,CAAK,mBAAmB,UAAW,CAAA;AAAA,UACxC,UAAU,IAAK,CAAA,QAAA;AAAA,UACf,cAAA;AAAA,UACA,UAAY,EAAA;AAAA,YACV,kBAAkB,UAAW,CAAA,KAAA;AAAA,YAC7B,WAAW,UAAW,CAAA;AAAA;AACxB,SACD,CAAA;AAAA;AACH,MACA;AACE,QAAA,MAAM,IAAIJ,0BAAA;AAAA,UACR,uDAAuD,IAAI,CAAA,CAAA;AAAA,SAC7D;AAAA;AACJ;AACF,EAEA,MAAM,oBACJ,WAC6C,EAAA;AAC7C,IAAA,MAAM,EAAE,KAAA,EAAO,cAAe,EAAA,GAC5BI,uCAA+B,WAAW,CAAA;AAC5C,IAAA,IAAI,CAAC,cAAgB,EAAA;AACnB,MAAA,MAAM,IAAIJ,0BAAA;AAAA,QACR;AAAA,OACF;AAAA;AAGF,IAAO,OAAA,IAAA,CAAK,gBAAiB,CAAA,sBAAA,CAAuB,cAAc,CAAA;AAAA;AACpE,EAEA,MAAM,qBAAyD,GAAA;AAC7D,IAAA,MAAM,EAAE,IAAK,EAAA,GAAI,MAAM,IAAA,CAAK,gBAAgB,QAAS,EAAA;AACrD,IAAO,OAAA,EAAE,MAAM,IAAK,CAAA,GAAA,CAAI,CAAC,EAAE,GAAA,EAAU,KAAA,GAAG,CAAE,EAAA;AAAA;AAC5C,EAEA,kBAAkB,KAAe,EAAA;AAC/B,IAAA,MAAM,EAAE,GAAA,EAAQ,GAAAK,cAAA,CAAU,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,GAAK,EAAA;AACR,MAAM,MAAA,IAAIL,2BAAoB,kCAAkC,CAAA;AAAA;AAElE,IAAO,OAAA,IAAI,IAAK,CAAA,GAAA,GAAM,GAAI,CAAA;AAAA;AAE9B;;;;"}
@@ -17,6 +17,7 @@ class CacheManager {
17
17
  */
18
18
  storeFactories = {
19
19
  redis: this.createRedisStoreFactory(),
20
+ valkey: this.createValkeyStoreFactory(),
20
21
  memcache: this.createMemcacheStoreFactory(),
21
22
  memory: this.createMemoryStoreFactory()
22
23
  };
@@ -41,7 +42,7 @@ class CacheManager {
41
42
  });
42
43
  if (config$1.has("backend.cache.useRedisSets")) {
43
44
  logger?.warn(
44
- "The 'backend.cache.useRedisSets' configuration key is deprecated and no longer has any effect. The underlying '@keyv/redis' library no longer supports redis sets."
45
+ "The 'backend.cache.useRedisSets' configuration key is deprecated and no longer has any effect. The underlying '@keyv/redis' and '@keyv/valkey' libraries no longer support redis sets."
45
46
  );
46
47
  }
47
48
  let defaultTtl;
@@ -67,14 +68,14 @@ class CacheManager {
67
68
  /**
68
69
  * Parse store-specific options from configuration.
69
70
  *
70
- * @param store - The cache store type ('redis', 'memcache', or 'memory')
71
+ * @param store - The cache store type ('redis', 'valkey', 'memcache', or 'memory')
71
72
  * @param config - The configuration service
72
73
  * @param logger - Optional logger for warnings
73
74
  * @returns The parsed store options
74
75
  */
75
76
  static parseStoreOptions(store, config, logger) {
76
77
  const storeConfigPath = `backend.cache.${store}`;
77
- if (store === "redis" && config.has(storeConfigPath)) {
78
+ if ((store === "redis" || store === "valkey") && config.has(storeConfigPath)) {
78
79
  return CacheManager.parseRedisOptions(storeConfigPath, config, logger);
79
80
  }
80
81
  return void 0;
@@ -176,6 +177,35 @@ class CacheManager {
176
177
  });
177
178
  };
178
179
  }
180
+ createValkeyStoreFactory() {
181
+ const KeyvValkey = require("@keyv/valkey").default;
182
+ const { createCluster } = require("@keyv/valkey");
183
+ const stores = {};
184
+ return (pluginId, defaultTtl) => {
185
+ if (!stores[pluginId]) {
186
+ const valkeyOptions = this.storeOptions?.client || {
187
+ keyPrefixSeparator: ":"
188
+ };
189
+ if (this.storeOptions?.cluster) {
190
+ const cluster = createCluster(this.storeOptions?.cluster);
191
+ stores[pluginId] = new KeyvValkey(cluster, valkeyOptions);
192
+ } else {
193
+ stores[pluginId] = new KeyvValkey(this.connection, valkeyOptions);
194
+ }
195
+ stores[pluginId].on("error", (err) => {
196
+ this.logger?.error("Failed to create valkey cache client", err);
197
+ this.errorHandler?.(err);
198
+ });
199
+ }
200
+ return new Keyv__default.default({
201
+ namespace: pluginId,
202
+ ttl: defaultTtl,
203
+ store: stores[pluginId],
204
+ emitErrors: false,
205
+ useKeyPrefix: false
206
+ });
207
+ };
208
+ }
179
209
  createMemcacheStoreFactory() {
180
210
  const KeyvMemcache = require("@keyv/memcache").default;
181
211
  const stores = {};
@@ -1 +1 @@
1
- {"version":3,"file":"CacheManager.cjs.js","sources":["../../../src/entrypoints/cache/CacheManager.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CacheService,\n CacheServiceOptions,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport Keyv from 'keyv';\nimport { DefaultCacheClient } from './CacheClient';\nimport {\n CacheManagerOptions,\n ttlToMilliseconds,\n CacheStoreOptions,\n RedisCacheStoreOptions,\n} from './types';\nimport { durationToMilliseconds } from '@backstage/types';\nimport { readDurationFromConfig } from '@backstage/config';\n\ntype StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv;\n\n/**\n * Implements a Cache Manager which will automatically create new cache clients\n * for plugins when requested. All requested cache clients are created with the\n * connection details provided.\n *\n * @public\n */\nexport class CacheManager {\n /**\n * Keys represent supported `backend.cache.store` values, mapped to factories\n * that return Keyv instances appropriate to the store.\n */\n private readonly storeFactories = {\n redis: this.createRedisStoreFactory(),\n memcache: this.createMemcacheStoreFactory(),\n memory: this.createMemoryStoreFactory(),\n };\n\n private readonly logger?: LoggerService;\n private readonly store: keyof CacheManager['storeFactories'];\n private readonly connection: string;\n private readonly errorHandler: CacheManagerOptions['onError'];\n private readonly defaultTtl?: number;\n private readonly storeOptions?: CacheStoreOptions;\n\n /**\n * Creates a new {@link CacheManager} instance by reading from the `backend`\n * config section, specifically the `.cache` key.\n *\n * @param config - The loaded application configuration.\n */\n static fromConfig(\n config: RootConfigService,\n options: CacheManagerOptions = {},\n ): CacheManager {\n // If no `backend.cache` config is provided, instantiate the CacheManager\n // with an in-memory cache client.\n const store = config.getOptionalString('backend.cache.store') || 'memory';\n const defaultTtlConfig = config.getOptional('backend.cache.defaultTtl');\n const connectionString =\n config.getOptionalString('backend.cache.connection') || '';\n const logger = options.logger?.child({\n type: 'cacheManager',\n });\n\n if (config.has('backend.cache.useRedisSets')) {\n logger?.warn(\n \"The 'backend.cache.useRedisSets' configuration key is deprecated and no longer has any effect. The underlying '@keyv/redis' library no longer supports redis sets.\",\n );\n }\n\n let defaultTtl: number | undefined;\n if (defaultTtlConfig !== undefined) {\n if (typeof defaultTtlConfig === 'number') {\n defaultTtl = defaultTtlConfig;\n } else {\n defaultTtl = durationToMilliseconds(\n readDurationFromConfig(config, { key: 'backend.cache.defaultTtl' }),\n );\n }\n }\n\n // Read store-specific options from config\n const storeOptions = CacheManager.parseStoreOptions(store, config, logger);\n\n return new CacheManager(\n store,\n connectionString,\n options.onError,\n logger,\n defaultTtl,\n storeOptions,\n );\n }\n\n /**\n * Parse store-specific options from configuration.\n *\n * @param store - The cache store type ('redis', 'memcache', or 'memory')\n * @param config - The configuration service\n * @param logger - Optional logger for warnings\n * @returns The parsed store options\n */\n private static parseStoreOptions(\n store: string,\n config: RootConfigService,\n logger?: LoggerService,\n ): CacheStoreOptions | undefined {\n const storeConfigPath = `backend.cache.${store}`;\n\n if (store === 'redis' && config.has(storeConfigPath)) {\n return CacheManager.parseRedisOptions(storeConfigPath, config, logger);\n }\n\n return undefined;\n }\n\n /**\n * Parse Redis-specific options from configuration.\n */\n private static parseRedisOptions(\n storeConfigPath: string,\n config: RootConfigService,\n logger?: LoggerService,\n ): RedisCacheStoreOptions {\n const redisOptions: RedisCacheStoreOptions = {};\n const redisConfig = config.getConfig(storeConfigPath);\n\n redisOptions.client = {\n namespace: redisConfig.getOptionalString('client.namespace'),\n keyPrefixSeparator:\n redisConfig.getOptionalString('client.keyPrefixSeparator') || ':',\n clearBatchSize: redisConfig.getOptionalNumber('client.clearBatchSize'),\n useUnlink: redisConfig.getOptionalBoolean('client.useUnlink'),\n noNamespaceAffectsAll: redisConfig.getOptionalBoolean(\n 'client.noNamespaceAffectsAll',\n ),\n };\n\n if (redisConfig.has('cluster')) {\n const clusterConfig = redisConfig.getConfig('cluster');\n\n if (!clusterConfig.has('rootNodes')) {\n logger?.warn(\n `Redis cluster config has no 'rootNodes' key, defaulting to non-clustered mode`,\n );\n return redisOptions;\n }\n\n redisOptions.cluster = {\n rootNodes: clusterConfig.get('rootNodes'),\n defaults: clusterConfig.getOptional('defaults'),\n minimizeConnections: clusterConfig.getOptionalBoolean(\n 'minimizeConnections',\n ),\n useReplicas: clusterConfig.getOptionalBoolean('useReplicas'),\n maxCommandRedirections: clusterConfig.getOptionalNumber(\n 'maxCommandRedirections',\n ),\n };\n }\n\n return redisOptions;\n }\n\n /** @internal */\n constructor(\n store: string,\n connectionString: string,\n errorHandler: CacheManagerOptions['onError'],\n logger?: LoggerService,\n defaultTtl?: number,\n storeOptions?: CacheStoreOptions,\n ) {\n if (!this.storeFactories.hasOwnProperty(store)) {\n throw new Error(`Unknown cache store: ${store}`);\n }\n this.logger = logger;\n this.store = store as keyof CacheManager['storeFactories'];\n this.connection = connectionString;\n this.errorHandler = errorHandler;\n this.defaultTtl = defaultTtl;\n this.storeOptions = storeOptions;\n }\n\n /**\n * Generates a PluginCacheManager for consumption by plugins.\n *\n * @param pluginId - The plugin that the cache manager should be created for.\n * Plugin names should be unique.\n */\n forPlugin(pluginId: string): CacheService {\n const clientFactory = (options: CacheServiceOptions) => {\n const ttl = options.defaultTtl ?? this.defaultTtl;\n return this.getClientWithTtl(\n pluginId,\n ttl !== undefined ? ttlToMilliseconds(ttl) : undefined,\n );\n };\n\n return new DefaultCacheClient(clientFactory({}), clientFactory, {});\n }\n\n private getClientWithTtl(pluginId: string, ttl: number | undefined): Keyv {\n return this.storeFactories[this.store](pluginId, ttl);\n }\n\n private createRedisStoreFactory(): StoreFactory {\n const KeyvRedis = require('@keyv/redis').default;\n const { createCluster } = require('@keyv/redis');\n const stores: Record<string, typeof KeyvRedis> = {};\n\n return (pluginId, defaultTtl) => {\n if (!stores[pluginId]) {\n const redisOptions = this.storeOptions?.client || {\n keyPrefixSeparator: ':',\n };\n if (this.storeOptions?.cluster) {\n // Create a Redis cluster\n const cluster = createCluster(this.storeOptions?.cluster);\n stores[pluginId] = new KeyvRedis(cluster, redisOptions);\n } else {\n // Create a regular Redis connection\n stores[pluginId] = new KeyvRedis(this.connection, redisOptions);\n }\n\n // Always provide an error handler to avoid stopping the process\n stores[pluginId].on('error', (err: Error) => {\n this.logger?.error('Failed to create redis cache client', err);\n this.errorHandler?.(err);\n });\n }\n return new Keyv({\n namespace: pluginId,\n ttl: defaultTtl,\n store: stores[pluginId],\n emitErrors: false,\n useKeyPrefix: false,\n });\n };\n }\n\n private createMemcacheStoreFactory(): StoreFactory {\n const KeyvMemcache = require('@keyv/memcache').default;\n const stores: Record<string, typeof KeyvMemcache> = {};\n\n return (pluginId, defaultTtl) => {\n if (!stores[pluginId]) {\n stores[pluginId] = new KeyvMemcache(this.connection);\n // Always provide an error handler to avoid stopping the process\n stores[pluginId].on('error', (err: Error) => {\n this.logger?.error('Failed to create memcache cache client', err);\n this.errorHandler?.(err);\n });\n }\n return new Keyv({\n namespace: pluginId,\n ttl: defaultTtl,\n emitErrors: false,\n store: stores[pluginId],\n });\n };\n }\n\n private createMemoryStoreFactory(): StoreFactory {\n const store = new Map();\n return (pluginId, defaultTtl) =>\n new Keyv({\n namespace: pluginId,\n ttl: defaultTtl,\n emitErrors: false,\n store,\n });\n }\n}\n"],"names":["config","durationToMilliseconds","readDurationFromConfig","ttlToMilliseconds","DefaultCacheClient","Keyv"],"mappings":";;;;;;;;;;;;AA0CO,MAAM,YAAa,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKP,cAAiB,GAAA;AAAA,IAChC,KAAA,EAAO,KAAK,uBAAwB,EAAA;AAAA,IACpC,QAAA,EAAU,KAAK,0BAA2B,EAAA;AAAA,IAC1C,MAAA,EAAQ,KAAK,wBAAyB;AAAA,GACxC;AAAA,EAEiB,MAAA;AAAA,EACA,KAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQjB,OAAO,UAAA,CACLA,QACA,EAAA,OAAA,GAA+B,EACjB,EAAA;AAGd,IAAA,MAAM,KAAQ,GAAAA,QAAA,CAAO,iBAAkB,CAAA,qBAAqB,CAAK,IAAA,QAAA;AACjE,IAAM,MAAA,gBAAA,GAAmBA,QAAO,CAAA,WAAA,CAAY,0BAA0B,CAAA;AACtE,IAAA,MAAM,gBACJ,GAAAA,QAAA,CAAO,iBAAkB,CAAA,0BAA0B,CAAK,IAAA,EAAA;AAC1D,IAAM,MAAA,MAAA,GAAS,OAAQ,CAAA,MAAA,EAAQ,KAAM,CAAA;AAAA,MACnC,IAAM,EAAA;AAAA,KACP,CAAA;AAED,IAAI,IAAAA,QAAA,CAAO,GAAI,CAAA,4BAA4B,CAAG,EAAA;AAC5C,MAAQ,MAAA,EAAA,IAAA;AAAA,QACN;AAAA,OACF;AAAA;AAGF,IAAI,IAAA,UAAA;AACJ,IAAA,IAAI,qBAAqB,KAAW,CAAA,EAAA;AAClC,MAAI,IAAA,OAAO,qBAAqB,QAAU,EAAA;AACxC,QAAa,UAAA,GAAA,gBAAA;AAAA,OACR,MAAA;AACL,QAAa,UAAA,GAAAC,4BAAA;AAAA,UACXC,6BAAuB,CAAAF,QAAA,EAAQ,EAAE,GAAA,EAAK,4BAA4B;AAAA,SACpE;AAAA;AACF;AAIF,IAAA,MAAM,YAAe,GAAA,YAAA,CAAa,iBAAkB,CAAA,KAAA,EAAOA,UAAQ,MAAM,CAAA;AAEzE,IAAA,OAAO,IAAI,YAAA;AAAA,MACT,KAAA;AAAA,MACA,gBAAA;AAAA,MACA,OAAQ,CAAA,OAAA;AAAA,MACR,MAAA;AAAA,MACA,UAAA;AAAA,MACA;AAAA,KACF;AAAA;AACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,OAAe,iBAAA,CACb,KACA,EAAA,MAAA,EACA,MAC+B,EAAA;AAC/B,IAAM,MAAA,eAAA,GAAkB,iBAAiB,KAAK,CAAA,CAAA;AAE9C,IAAA,IAAI,KAAU,KAAA,OAAA,IAAW,MAAO,CAAA,GAAA,CAAI,eAAe,CAAG,EAAA;AACpD,MAAA,OAAO,YAAa,CAAA,iBAAA,CAAkB,eAAiB,EAAA,MAAA,EAAQ,MAAM,CAAA;AAAA;AAGvE,IAAO,OAAA,KAAA,CAAA;AAAA;AACT;AAAA;AAAA;AAAA,EAKA,OAAe,iBAAA,CACb,eACA,EAAA,MAAA,EACA,MACwB,EAAA;AACxB,IAAA,MAAM,eAAuC,EAAC;AAC9C,IAAM,MAAA,WAAA,GAAc,MAAO,CAAA,SAAA,CAAU,eAAe,CAAA;AAEpD,IAAA,YAAA,CAAa,MAAS,GAAA;AAAA,MACpB,SAAA,EAAW,WAAY,CAAA,iBAAA,CAAkB,kBAAkB,CAAA;AAAA,MAC3D,kBACE,EAAA,WAAA,CAAY,iBAAkB,CAAA,2BAA2B,CAAK,IAAA,GAAA;AAAA,MAChE,cAAA,EAAgB,WAAY,CAAA,iBAAA,CAAkB,uBAAuB,CAAA;AAAA,MACrE,SAAA,EAAW,WAAY,CAAA,kBAAA,CAAmB,kBAAkB,CAAA;AAAA,MAC5D,uBAAuB,WAAY,CAAA,kBAAA;AAAA,QACjC;AAAA;AACF,KACF;AAEA,IAAI,IAAA,WAAA,CAAY,GAAI,CAAA,SAAS,CAAG,EAAA;AAC9B,MAAM,MAAA,aAAA,GAAgB,WAAY,CAAA,SAAA,CAAU,SAAS,CAAA;AAErD,MAAA,IAAI,CAAC,aAAA,CAAc,GAAI,CAAA,WAAW,CAAG,EAAA;AACnC,QAAQ,MAAA,EAAA,IAAA;AAAA,UACN,CAAA,6EAAA;AAAA,SACF;AACA,QAAO,OAAA,YAAA;AAAA;AAGT,MAAA,YAAA,CAAa,OAAU,GAAA;AAAA,QACrB,SAAA,EAAW,aAAc,CAAA,GAAA,CAAI,WAAW,CAAA;AAAA,QACxC,QAAA,EAAU,aAAc,CAAA,WAAA,CAAY,UAAU,CAAA;AAAA,QAC9C,qBAAqB,aAAc,CAAA,kBAAA;AAAA,UACjC;AAAA,SACF;AAAA,QACA,WAAA,EAAa,aAAc,CAAA,kBAAA,CAAmB,aAAa,CAAA;AAAA,QAC3D,wBAAwB,aAAc,CAAA,iBAAA;AAAA,UACpC;AAAA;AACF,OACF;AAAA;AAGF,IAAO,OAAA,YAAA;AAAA;AACT;AAAA,EAGA,YACE,KACA,EAAA,gBAAA,EACA,YACA,EAAA,MAAA,EACA,YACA,YACA,EAAA;AACA,IAAA,IAAI,CAAC,IAAA,CAAK,cAAe,CAAA,cAAA,CAAe,KAAK,CAAG,EAAA;AAC9C,MAAA,MAAM,IAAI,KAAA,CAAM,CAAwB,qBAAA,EAAA,KAAK,CAAE,CAAA,CAAA;AAAA;AAEjD,IAAA,IAAA,CAAK,MAAS,GAAA,MAAA;AACd,IAAA,IAAA,CAAK,KAAQ,GAAA,KAAA;AACb,IAAA,IAAA,CAAK,UAAa,GAAA,gBAAA;AAClB,IAAA,IAAA,CAAK,YAAe,GAAA,YAAA;AACpB,IAAA,IAAA,CAAK,UAAa,GAAA,UAAA;AAClB,IAAA,IAAA,CAAK,YAAe,GAAA,YAAA;AAAA;AACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAU,QAAgC,EAAA;AACxC,IAAM,MAAA,aAAA,GAAgB,CAAC,OAAiC,KAAA;AACtD,MAAM,MAAA,GAAA,GAAM,OAAQ,CAAA,UAAA,IAAc,IAAK,CAAA,UAAA;AACvC,MAAA,OAAO,IAAK,CAAA,gBAAA;AAAA,QACV,QAAA;AAAA,QACA,GAAQ,KAAA,KAAA,CAAA,GAAYG,yBAAkB,CAAA,GAAG,CAAI,GAAA,KAAA;AAAA,OAC/C;AAAA,KACF;AAEA,IAAO,OAAA,IAAIC,+BAAmB,aAAc,CAAA,EAAE,CAAG,EAAA,aAAA,EAAe,EAAE,CAAA;AAAA;AACpE,EAEQ,gBAAA,CAAiB,UAAkB,GAA+B,EAAA;AACxE,IAAA,OAAO,KAAK,cAAe,CAAA,IAAA,CAAK,KAAK,CAAA,CAAE,UAAU,GAAG,CAAA;AAAA;AACtD,EAEQ,uBAAwC,GAAA;AAC9C,IAAM,MAAA,SAAA,GAAY,OAAQ,CAAA,aAAa,CAAE,CAAA,OAAA;AACzC,IAAA,MAAM,EAAE,aAAA,EAAkB,GAAA,OAAA,CAAQ,aAAa,CAAA;AAC/C,IAAA,MAAM,SAA2C,EAAC;AAElD,IAAO,OAAA,CAAC,UAAU,UAAe,KAAA;AAC/B,MAAI,IAAA,CAAC,MAAO,CAAA,QAAQ,CAAG,EAAA;AACrB,QAAM,MAAA,YAAA,GAAe,IAAK,CAAA,YAAA,EAAc,MAAU,IAAA;AAAA,UAChD,kBAAoB,EAAA;AAAA,SACtB;AACA,QAAI,IAAA,IAAA,CAAK,cAAc,OAAS,EAAA;AAE9B,UAAA,MAAM,OAAU,GAAA,aAAA,CAAc,IAAK,CAAA,YAAA,EAAc,OAAO,CAAA;AACxD,UAAA,MAAA,CAAO,QAAQ,CAAA,GAAI,IAAI,SAAA,CAAU,SAAS,YAAY,CAAA;AAAA,SACjD,MAAA;AAEL,UAAA,MAAA,CAAO,QAAQ,CAAI,GAAA,IAAI,SAAU,CAAA,IAAA,CAAK,YAAY,YAAY,CAAA;AAAA;AAIhE,QAAA,MAAA,CAAO,QAAQ,CAAA,CAAE,EAAG,CAAA,OAAA,EAAS,CAAC,GAAe,KAAA;AAC3C,UAAK,IAAA,CAAA,MAAA,EAAQ,KAAM,CAAA,qCAAA,EAAuC,GAAG,CAAA;AAC7D,UAAA,IAAA,CAAK,eAAe,GAAG,CAAA;AAAA,SACxB,CAAA;AAAA;AAEH,MAAA,OAAO,IAAIC,qBAAK,CAAA;AAAA,QACd,SAAW,EAAA,QAAA;AAAA,QACX,GAAK,EAAA,UAAA;AAAA,QACL,KAAA,EAAO,OAAO,QAAQ,CAAA;AAAA,QACtB,UAAY,EAAA,KAAA;AAAA,QACZ,YAAc,EAAA;AAAA,OACf,CAAA;AAAA,KACH;AAAA;AACF,EAEQ,0BAA2C,GAAA;AACjD,IAAM,MAAA,YAAA,GAAe,OAAQ,CAAA,gBAAgB,CAAE,CAAA,OAAA;AAC/C,IAAA,MAAM,SAA8C,EAAC;AAErD,IAAO,OAAA,CAAC,UAAU,UAAe,KAAA;AAC/B,MAAI,IAAA,CAAC,MAAO,CAAA,QAAQ,CAAG,EAAA;AACrB,QAAA,MAAA,CAAO,QAAQ,CAAA,GAAI,IAAI,YAAA,CAAa,KAAK,UAAU,CAAA;AAEnD,QAAA,MAAA,CAAO,QAAQ,CAAA,CAAE,EAAG,CAAA,OAAA,EAAS,CAAC,GAAe,KAAA;AAC3C,UAAK,IAAA,CAAA,MAAA,EAAQ,KAAM,CAAA,wCAAA,EAA0C,GAAG,CAAA;AAChE,UAAA,IAAA,CAAK,eAAe,GAAG,CAAA;AAAA,SACxB,CAAA;AAAA;AAEH,MAAA,OAAO,IAAIA,qBAAK,CAAA;AAAA,QACd,SAAW,EAAA,QAAA;AAAA,QACX,GAAK,EAAA,UAAA;AAAA,QACL,UAAY,EAAA,KAAA;AAAA,QACZ,KAAA,EAAO,OAAO,QAAQ;AAAA,OACvB,CAAA;AAAA,KACH;AAAA;AACF,EAEQ,wBAAyC,GAAA;AAC/C,IAAM,MAAA,KAAA,uBAAY,GAAI,EAAA;AACtB,IAAA,OAAO,CAAC,QAAA,EAAU,UAChB,KAAA,IAAIA,qBAAK,CAAA;AAAA,MACP,SAAW,EAAA,QAAA;AAAA,MACX,GAAK,EAAA,UAAA;AAAA,MACL,UAAY,EAAA,KAAA;AAAA,MACZ;AAAA,KACD,CAAA;AAAA;AAEP;;;;"}
1
+ {"version":3,"file":"CacheManager.cjs.js","sources":["../../../src/entrypoints/cache/CacheManager.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CacheService,\n CacheServiceOptions,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport Keyv from 'keyv';\nimport { DefaultCacheClient } from './CacheClient';\nimport {\n CacheManagerOptions,\n ttlToMilliseconds,\n CacheStoreOptions,\n RedisCacheStoreOptions,\n} from './types';\nimport { durationToMilliseconds } from '@backstage/types';\nimport { readDurationFromConfig } from '@backstage/config';\n\ntype StoreFactory = (pluginId: string, defaultTtl: number | undefined) => Keyv;\n\n/**\n * Implements a Cache Manager which will automatically create new cache clients\n * for plugins when requested. All requested cache clients are created with the\n * connection details provided.\n *\n * @public\n */\nexport class CacheManager {\n /**\n * Keys represent supported `backend.cache.store` values, mapped to factories\n * that return Keyv instances appropriate to the store.\n */\n private readonly storeFactories = {\n redis: this.createRedisStoreFactory(),\n valkey: this.createValkeyStoreFactory(),\n memcache: this.createMemcacheStoreFactory(),\n memory: this.createMemoryStoreFactory(),\n };\n\n private readonly logger?: LoggerService;\n private readonly store: keyof CacheManager['storeFactories'];\n private readonly connection: string;\n private readonly errorHandler: CacheManagerOptions['onError'];\n private readonly defaultTtl?: number;\n private readonly storeOptions?: CacheStoreOptions;\n\n /**\n * Creates a new {@link CacheManager} instance by reading from the `backend`\n * config section, specifically the `.cache` key.\n *\n * @param config - The loaded application configuration.\n */\n static fromConfig(\n config: RootConfigService,\n options: CacheManagerOptions = {},\n ): CacheManager {\n // If no `backend.cache` config is provided, instantiate the CacheManager\n // with an in-memory cache client.\n const store = config.getOptionalString('backend.cache.store') || 'memory';\n const defaultTtlConfig = config.getOptional('backend.cache.defaultTtl');\n const connectionString =\n config.getOptionalString('backend.cache.connection') || '';\n const logger = options.logger?.child({\n type: 'cacheManager',\n });\n\n if (config.has('backend.cache.useRedisSets')) {\n logger?.warn(\n \"The 'backend.cache.useRedisSets' configuration key is deprecated and no longer has any effect. The underlying '@keyv/redis' and '@keyv/valkey' libraries no longer support redis sets.\",\n );\n }\n\n let defaultTtl: number | undefined;\n if (defaultTtlConfig !== undefined) {\n if (typeof defaultTtlConfig === 'number') {\n defaultTtl = defaultTtlConfig;\n } else {\n defaultTtl = durationToMilliseconds(\n readDurationFromConfig(config, { key: 'backend.cache.defaultTtl' }),\n );\n }\n }\n\n // Read store-specific options from config\n const storeOptions = CacheManager.parseStoreOptions(store, config, logger);\n\n return new CacheManager(\n store,\n connectionString,\n options.onError,\n logger,\n defaultTtl,\n storeOptions,\n );\n }\n\n /**\n * Parse store-specific options from configuration.\n *\n * @param store - The cache store type ('redis', 'valkey', 'memcache', or 'memory')\n * @param config - The configuration service\n * @param logger - Optional logger for warnings\n * @returns The parsed store options\n */\n private static parseStoreOptions(\n store: string,\n config: RootConfigService,\n logger?: LoggerService,\n ): CacheStoreOptions | undefined {\n const storeConfigPath = `backend.cache.${store}`;\n\n if (\n (store === 'redis' || store === 'valkey') &&\n config.has(storeConfigPath)\n ) {\n return CacheManager.parseRedisOptions(storeConfigPath, config, logger);\n }\n\n return undefined;\n }\n\n /**\n * Parse Redis-specific options from configuration.\n */\n private static parseRedisOptions(\n storeConfigPath: string,\n config: RootConfigService,\n logger?: LoggerService,\n ): RedisCacheStoreOptions {\n const redisOptions: RedisCacheStoreOptions = {};\n const redisConfig = config.getConfig(storeConfigPath);\n\n redisOptions.client = {\n namespace: redisConfig.getOptionalString('client.namespace'),\n keyPrefixSeparator:\n redisConfig.getOptionalString('client.keyPrefixSeparator') || ':',\n clearBatchSize: redisConfig.getOptionalNumber('client.clearBatchSize'),\n useUnlink: redisConfig.getOptionalBoolean('client.useUnlink'),\n noNamespaceAffectsAll: redisConfig.getOptionalBoolean(\n 'client.noNamespaceAffectsAll',\n ),\n };\n\n if (redisConfig.has('cluster')) {\n const clusterConfig = redisConfig.getConfig('cluster');\n\n if (!clusterConfig.has('rootNodes')) {\n logger?.warn(\n `Redis cluster config has no 'rootNodes' key, defaulting to non-clustered mode`,\n );\n return redisOptions;\n }\n\n redisOptions.cluster = {\n rootNodes: clusterConfig.get('rootNodes'),\n defaults: clusterConfig.getOptional('defaults'),\n minimizeConnections: clusterConfig.getOptionalBoolean(\n 'minimizeConnections',\n ),\n useReplicas: clusterConfig.getOptionalBoolean('useReplicas'),\n maxCommandRedirections: clusterConfig.getOptionalNumber(\n 'maxCommandRedirections',\n ),\n };\n }\n\n return redisOptions;\n }\n\n /** @internal */\n constructor(\n store: string,\n connectionString: string,\n errorHandler: CacheManagerOptions['onError'],\n logger?: LoggerService,\n defaultTtl?: number,\n storeOptions?: CacheStoreOptions,\n ) {\n if (!this.storeFactories.hasOwnProperty(store)) {\n throw new Error(`Unknown cache store: ${store}`);\n }\n this.logger = logger;\n this.store = store as keyof CacheManager['storeFactories'];\n this.connection = connectionString;\n this.errorHandler = errorHandler;\n this.defaultTtl = defaultTtl;\n this.storeOptions = storeOptions;\n }\n\n /**\n * Generates a PluginCacheManager for consumption by plugins.\n *\n * @param pluginId - The plugin that the cache manager should be created for.\n * Plugin names should be unique.\n */\n forPlugin(pluginId: string): CacheService {\n const clientFactory = (options: CacheServiceOptions) => {\n const ttl = options.defaultTtl ?? this.defaultTtl;\n return this.getClientWithTtl(\n pluginId,\n ttl !== undefined ? ttlToMilliseconds(ttl) : undefined,\n );\n };\n\n return new DefaultCacheClient(clientFactory({}), clientFactory, {});\n }\n\n private getClientWithTtl(pluginId: string, ttl: number | undefined): Keyv {\n return this.storeFactories[this.store](pluginId, ttl);\n }\n\n private createRedisStoreFactory(): StoreFactory {\n const KeyvRedis = require('@keyv/redis').default;\n const { createCluster } = require('@keyv/redis');\n const stores: Record<string, typeof KeyvRedis> = {};\n\n return (pluginId, defaultTtl) => {\n if (!stores[pluginId]) {\n const redisOptions = this.storeOptions?.client || {\n keyPrefixSeparator: ':',\n };\n if (this.storeOptions?.cluster) {\n // Create a Redis cluster\n const cluster = createCluster(this.storeOptions?.cluster);\n stores[pluginId] = new KeyvRedis(cluster, redisOptions);\n } else {\n // Create a regular Redis connection\n stores[pluginId] = new KeyvRedis(this.connection, redisOptions);\n }\n\n // Always provide an error handler to avoid stopping the process\n stores[pluginId].on('error', (err: Error) => {\n this.logger?.error('Failed to create redis cache client', err);\n this.errorHandler?.(err);\n });\n }\n return new Keyv({\n namespace: pluginId,\n ttl: defaultTtl,\n store: stores[pluginId],\n emitErrors: false,\n useKeyPrefix: false,\n });\n };\n }\n\n private createValkeyStoreFactory(): StoreFactory {\n const KeyvValkey = require('@keyv/valkey').default;\n const { createCluster } = require('@keyv/valkey');\n const stores: Record<string, typeof KeyvValkey> = {};\n\n return (pluginId, defaultTtl) => {\n if (!stores[pluginId]) {\n const valkeyOptions = this.storeOptions?.client || {\n keyPrefixSeparator: ':',\n };\n if (this.storeOptions?.cluster) {\n // Create a Valkey cluster (Redis cluster under the hood)\n const cluster = createCluster(this.storeOptions?.cluster);\n stores[pluginId] = new KeyvValkey(cluster, valkeyOptions);\n } else {\n // Create a regular Valkey connection\n stores[pluginId] = new KeyvValkey(this.connection, valkeyOptions);\n }\n\n // Always provide an error handler to avoid stopping the process\n stores[pluginId].on('error', (err: Error) => {\n this.logger?.error('Failed to create valkey cache client', err);\n this.errorHandler?.(err);\n });\n }\n return new Keyv({\n namespace: pluginId,\n ttl: defaultTtl,\n store: stores[pluginId],\n emitErrors: false,\n useKeyPrefix: false,\n });\n };\n }\n\n private createMemcacheStoreFactory(): StoreFactory {\n const KeyvMemcache = require('@keyv/memcache').default;\n const stores: Record<string, typeof KeyvMemcache> = {};\n\n return (pluginId, defaultTtl) => {\n if (!stores[pluginId]) {\n stores[pluginId] = new KeyvMemcache(this.connection);\n // Always provide an error handler to avoid stopping the process\n stores[pluginId].on('error', (err: Error) => {\n this.logger?.error('Failed to create memcache cache client', err);\n this.errorHandler?.(err);\n });\n }\n return new Keyv({\n namespace: pluginId,\n ttl: defaultTtl,\n emitErrors: false,\n store: stores[pluginId],\n });\n };\n }\n\n private createMemoryStoreFactory(): StoreFactory {\n const store = new Map();\n return (pluginId, defaultTtl) =>\n new Keyv({\n namespace: pluginId,\n ttl: defaultTtl,\n emitErrors: false,\n store,\n });\n }\n}\n"],"names":["config","durationToMilliseconds","readDurationFromConfig","ttlToMilliseconds","DefaultCacheClient","Keyv"],"mappings":"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"}
@@ -3,11 +3,11 @@
3
3
  var backendPluginApi = require('@backstage/backend-plugin-api');
4
4
  var tables = require('./tables.cjs.js');
5
5
 
6
+ const migrationsDir = backendPluginApi.resolvePackagePath(
7
+ "@backstage/backend-defaults",
8
+ "migrations/scheduler"
9
+ );
6
10
  async function migrateBackendTasks(knex) {
7
- const migrationsDir = backendPluginApi.resolvePackagePath(
8
- "@backstage/backend-defaults",
9
- "migrations/scheduler"
10
- );
11
11
  await knex.migrate.latest({
12
12
  directory: migrationsDir,
13
13
  tableName: tables.DB_MIGRATIONS_TABLE
@@ -15,4 +15,5 @@ async function migrateBackendTasks(knex) {
15
15
  }
16
16
 
17
17
  exports.migrateBackendTasks = migrateBackendTasks;
18
+ exports.migrationsDir = migrationsDir;
18
19
  //# sourceMappingURL=migrateBackendTasks.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrateBackendTasks.cjs.js","sources":["../../../../src/entrypoints/scheduler/database/migrateBackendTasks.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolvePackagePath } from '@backstage/backend-plugin-api';\nimport { Knex } from 'knex';\nimport { DB_MIGRATIONS_TABLE } from './tables';\n\nexport async function migrateBackendTasks(knex: Knex): Promise<void> {\n const migrationsDir = resolvePackagePath(\n '@backstage/backend-defaults',\n 'migrations/scheduler',\n );\n\n await knex.migrate.latest({\n directory: migrationsDir,\n tableName: DB_MIGRATIONS_TABLE,\n });\n}\n"],"names":["resolvePackagePath","DB_MIGRATIONS_TABLE"],"mappings":";;;;;AAoBA,eAAsB,oBAAoB,IAA2B,EAAA;AACnE,EAAA,MAAM,aAAgB,GAAAA,mCAAA;AAAA,IACpB,6BAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAM,MAAA,IAAA,CAAK,QAAQ,MAAO,CAAA;AAAA,IACxB,SAAW,EAAA,aAAA;AAAA,IACX,SAAW,EAAAC;AAAA,GACZ,CAAA;AACH;;;;"}
1
+ {"version":3,"file":"migrateBackendTasks.cjs.js","sources":["../../../../src/entrypoints/scheduler/database/migrateBackendTasks.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolvePackagePath } from '@backstage/backend-plugin-api';\nimport { Knex } from 'knex';\nimport { DB_MIGRATIONS_TABLE } from './tables';\n\nexport const migrationsDir = resolvePackagePath(\n '@backstage/backend-defaults',\n 'migrations/scheduler',\n);\n\nexport async function migrateBackendTasks(knex: Knex): Promise<void> {\n await knex.migrate.latest({\n directory: migrationsDir,\n tableName: DB_MIGRATIONS_TABLE,\n });\n}\n"],"names":["resolvePackagePath","DB_MIGRATIONS_TABLE"],"mappings":";;;;;AAoBO,MAAM,aAAgB,GAAAA,mCAAA;AAAA,EAC3B,6BAAA;AAAA,EACA;AACF;AAEA,eAAsB,oBAAoB,IAA2B,EAAA;AACnE,EAAM,MAAA,IAAA,CAAK,QAAQ,MAAO,CAAA;AAAA,IACxB,SAAW,EAAA,aAAA;AAAA,IACX,SAAW,EAAAC;AAAA,GACZ,CAAA;AACH;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"tables.cjs.js","sources":["../../../../src/entrypoints/scheduler/database/tables.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations';\nexport const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks';\n\nexport type DbTasksRow = {\n id: string;\n settings_json: string;\n next_run_start_at: Date;\n current_run_ticket?: string;\n current_run_started_at?: Date | string;\n current_run_expires_at?: Date | string;\n};\n"],"names":[],"mappings":";;AAgBO,MAAM,mBAAsB,GAAA;AAC5B,MAAM,cAAiB,GAAA;;;;;"}
1
+ {"version":3,"file":"tables.cjs.js","sources":["../../../../src/entrypoints/scheduler/database/tables.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const DB_MIGRATIONS_TABLE = 'backstage_backend_tasks__knex_migrations';\nexport const DB_TASKS_TABLE = 'backstage_backend_tasks__tasks';\n\nexport type DbTasksRow = {\n id: string;\n settings_json: string;\n next_run_start_at?: Date | string; // This can be null when in manual trigger mode\n current_run_ticket?: string;\n current_run_started_at?: Date | string;\n current_run_expires_at?: Date | string;\n last_run_error_json?: string;\n last_run_ended_at?: Date | string;\n};\n"],"names":[],"mappings":";;AAgBO,MAAM,mBAAsB,GAAA;AAC5B,MAAM,cAAiB,GAAA;;;;;"}
@@ -20,16 +20,19 @@ class DefaultSchedulerService {
20
20
  waitBetweenRuns: luxon.Duration.fromObject({ minutes: 1 }),
21
21
  logger: options.logger
22
22
  });
23
- options.rootLifecycle?.addShutdownHook(() => abortController.abort());
23
+ options.rootLifecycle.addShutdownHook(() => abortController.abort());
24
24
  janitor.start(abortController.signal);
25
25
  }
26
26
  return knex;
27
27
  });
28
- return new PluginTaskSchedulerImpl.PluginTaskSchedulerImpl(
28
+ const scheduler = new PluginTaskSchedulerImpl.PluginTaskSchedulerImpl(
29
+ options.pluginMetadata.getId(),
29
30
  databaseFactory,
30
31
  options.logger,
31
32
  options.rootLifecycle
32
33
  );
34
+ options.httpRouter.use(scheduler.getRouter());
35
+ return scheduler;
33
36
  }
34
37
  }
35
38
 
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultSchedulerService.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/DefaultSchedulerService.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DatabaseService,\n LoggerService,\n RootLifecycleService,\n SchedulerService,\n} from '@backstage/backend-plugin-api';\nimport { once } from 'lodash';\nimport { Duration } from 'luxon';\nimport { migrateBackendTasks } from '../database/migrateBackendTasks';\nimport { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl';\nimport { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor';\n\n/**\n * Default implementation of the task scheduler service.\n *\n * @public\n */\nexport class DefaultSchedulerService {\n static create(options: {\n database: DatabaseService;\n logger: LoggerService;\n rootLifecycle?: RootLifecycleService;\n }): SchedulerService {\n const databaseFactory = once(async () => {\n const knex = await options.database.getClient();\n\n if (!options.database.migrations?.skip) {\n await migrateBackendTasks(knex);\n }\n\n if (process.env.NODE_ENV !== 'test') {\n const abortController = new AbortController();\n const janitor = new PluginTaskSchedulerJanitor({\n knex,\n waitBetweenRuns: Duration.fromObject({ minutes: 1 }),\n logger: options.logger,\n });\n\n options.rootLifecycle?.addShutdownHook(() => abortController.abort());\n janitor.start(abortController.signal);\n }\n\n return knex;\n });\n\n return new PluginTaskSchedulerImpl(\n databaseFactory,\n options.logger,\n options.rootLifecycle,\n );\n }\n}\n"],"names":["once","migrateBackendTasks","PluginTaskSchedulerJanitor","Duration","PluginTaskSchedulerImpl"],"mappings":";;;;;;;;AAiCO,MAAM,uBAAwB,CAAA;AAAA,EACnC,OAAO,OAAO,OAIO,EAAA;AACnB,IAAM,MAAA,eAAA,GAAkBA,YAAK,YAAY;AACvC,MAAA,MAAM,IAAO,GAAA,MAAM,OAAQ,CAAA,QAAA,CAAS,SAAU,EAAA;AAE9C,MAAA,IAAI,CAAC,OAAA,CAAQ,QAAS,CAAA,UAAA,EAAY,IAAM,EAAA;AACtC,QAAA,MAAMC,wCAAoB,IAAI,CAAA;AAAA;AAGhC,MAAI,IAAA,OAAA,CAAQ,GAAI,CAAA,QAAA,KAAa,MAAQ,EAAA;AACnC,QAAM,MAAA,eAAA,GAAkB,IAAI,eAAgB,EAAA;AAC5C,QAAM,MAAA,OAAA,GAAU,IAAIC,qDAA2B,CAAA;AAAA,UAC7C,IAAA;AAAA,UACA,iBAAiBC,cAAS,CAAA,UAAA,CAAW,EAAE,OAAA,EAAS,GAAG,CAAA;AAAA,UACnD,QAAQ,OAAQ,CAAA;AAAA,SACjB,CAAA;AAED,QAAA,OAAA,CAAQ,aAAe,EAAA,eAAA,CAAgB,MAAM,eAAA,CAAgB,OAAO,CAAA;AACpE,QAAQ,OAAA,CAAA,KAAA,CAAM,gBAAgB,MAAM,CAAA;AAAA;AAGtC,MAAO,OAAA,IAAA;AAAA,KACR,CAAA;AAED,IAAA,OAAO,IAAIC,+CAAA;AAAA,MACT,eAAA;AAAA,MACA,OAAQ,CAAA,MAAA;AAAA,MACR,OAAQ,CAAA;AAAA,KACV;AAAA;AAEJ;;;;"}
1
+ {"version":3,"file":"DefaultSchedulerService.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/DefaultSchedulerService.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DatabaseService,\n HttpRouterService,\n LoggerService,\n PluginMetadataService,\n RootLifecycleService,\n SchedulerService,\n} from '@backstage/backend-plugin-api';\nimport { once } from 'lodash';\nimport { Duration } from 'luxon';\nimport { migrateBackendTasks } from '../database/migrateBackendTasks';\nimport { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl';\nimport { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor';\n\n/**\n * Default implementation of the task scheduler service.\n *\n * @public\n */\nexport class DefaultSchedulerService {\n static create(options: {\n database: DatabaseService;\n logger: LoggerService;\n rootLifecycle: RootLifecycleService;\n httpRouter: HttpRouterService;\n pluginMetadata: PluginMetadataService;\n }): SchedulerService {\n const databaseFactory = once(async () => {\n const knex = await options.database.getClient();\n\n if (!options.database.migrations?.skip) {\n await migrateBackendTasks(knex);\n }\n\n if (process.env.NODE_ENV !== 'test') {\n const abortController = new AbortController();\n const janitor = new PluginTaskSchedulerJanitor({\n knex,\n waitBetweenRuns: Duration.fromObject({ minutes: 1 }),\n logger: options.logger,\n });\n\n options.rootLifecycle.addShutdownHook(() => abortController.abort());\n janitor.start(abortController.signal);\n }\n\n return knex;\n });\n\n const scheduler = new PluginTaskSchedulerImpl(\n options.pluginMetadata.getId(),\n databaseFactory,\n options.logger,\n options.rootLifecycle,\n );\n\n options.httpRouter.use(scheduler.getRouter());\n\n return scheduler;\n }\n}\n"],"names":["once","migrateBackendTasks","PluginTaskSchedulerJanitor","Duration","PluginTaskSchedulerImpl"],"mappings":";;;;;;;;AAmCO,MAAM,uBAAwB,CAAA;AAAA,EACnC,OAAO,OAAO,OAMO,EAAA;AACnB,IAAM,MAAA,eAAA,GAAkBA,YAAK,YAAY;AACvC,MAAA,MAAM,IAAO,GAAA,MAAM,OAAQ,CAAA,QAAA,CAAS,SAAU,EAAA;AAE9C,MAAA,IAAI,CAAC,OAAA,CAAQ,QAAS,CAAA,UAAA,EAAY,IAAM,EAAA;AACtC,QAAA,MAAMC,wCAAoB,IAAI,CAAA;AAAA;AAGhC,MAAI,IAAA,OAAA,CAAQ,GAAI,CAAA,QAAA,KAAa,MAAQ,EAAA;AACnC,QAAM,MAAA,eAAA,GAAkB,IAAI,eAAgB,EAAA;AAC5C,QAAM,MAAA,OAAA,GAAU,IAAIC,qDAA2B,CAAA;AAAA,UAC7C,IAAA;AAAA,UACA,iBAAiBC,cAAS,CAAA,UAAA,CAAW,EAAE,OAAA,EAAS,GAAG,CAAA;AAAA,UACnD,QAAQ,OAAQ,CAAA;AAAA,SACjB,CAAA;AAED,QAAA,OAAA,CAAQ,aAAc,CAAA,eAAA,CAAgB,MAAM,eAAA,CAAgB,OAAO,CAAA;AACnE,QAAQ,OAAA,CAAA,KAAA,CAAM,gBAAgB,MAAM,CAAA;AAAA;AAGtC,MAAO,OAAA,IAAA;AAAA,KACR,CAAA;AAED,IAAA,MAAM,YAAY,IAAIC,+CAAA;AAAA,MACpB,OAAA,CAAQ,eAAe,KAAM,EAAA;AAAA,MAC7B,eAAA;AAAA,MACA,OAAQ,CAAA,MAAA;AAAA,MACR,OAAQ,CAAA;AAAA,KACV;AAEA,IAAA,OAAA,CAAQ,UAAW,CAAA,GAAA,CAAI,SAAU,CAAA,SAAA,EAAW,CAAA;AAE5C,IAAO,OAAA,SAAA;AAAA;AAEX;;;;"}
@@ -12,6 +12,12 @@ class LocalTaskWorker {
12
12
  this.logger = logger;
13
13
  }
14
14
  abortWait;
15
+ #taskState = {
16
+ status: "idle"
17
+ };
18
+ #workerState = {
19
+ status: "idle"
20
+ };
15
21
  start(settings, options) {
16
22
  this.logger.info(
17
23
  `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`
@@ -20,12 +26,7 @@ class LocalTaskWorker {
20
26
  let attemptNum = 1;
21
27
  for (; ; ) {
22
28
  try {
23
- if (settings.initialDelayDuration) {
24
- await this.sleep(
25
- luxon.Duration.fromISO(settings.initialDelayDuration),
26
- options.signal
27
- );
28
- }
29
+ await this.performInitialWait(settings, options.signal);
29
30
  while (!options.signal.aborted) {
30
31
  const startTime = process.hrtime();
31
32
  await this.runOnce(settings, options.signal);
@@ -55,17 +56,56 @@ class LocalTaskWorker {
55
56
  }
56
57
  this.abortWait.abort();
57
58
  }
59
+ taskState() {
60
+ return this.#taskState;
61
+ }
62
+ workerState() {
63
+ return this.#workerState;
64
+ }
65
+ /**
66
+ * Does the once-at-startup initial wait, if configured.
67
+ */
68
+ async performInitialWait(settings, signal) {
69
+ if (settings.initialDelayDuration) {
70
+ const parsedDuration = luxon.Duration.fromISO(settings.initialDelayDuration);
71
+ this.#taskState = {
72
+ status: "idle",
73
+ startsAt: luxon.DateTime.utc().plus(parsedDuration).toISO(),
74
+ lastRunEndedAt: this.#taskState.lastRunEndedAt,
75
+ lastRunError: this.#taskState.lastRunError
76
+ };
77
+ this.#workerState = {
78
+ status: "initial-wait"
79
+ };
80
+ await this.sleep(parsedDuration, signal);
81
+ }
82
+ }
58
83
  /**
59
84
  * Makes a single attempt at running the task to completion.
60
85
  */
61
86
  async runOnce(settings, signal) {
62
87
  const taskAbortController = util.delegateAbortController(signal);
88
+ const timeoutDuration = luxon.Duration.fromISO(settings.timeoutAfterDuration);
63
89
  const timeoutHandle = setTimeout(() => {
64
90
  taskAbortController.abort();
65
- }, luxon.Duration.fromISO(settings.timeoutAfterDuration).as("milliseconds"));
91
+ }, timeoutDuration.as("milliseconds"));
92
+ this.#taskState = {
93
+ status: "running",
94
+ startedAt: luxon.DateTime.utc().toISO(),
95
+ timesOutAt: luxon.DateTime.utc().plus(timeoutDuration).toISO(),
96
+ lastRunEndedAt: this.#taskState.lastRunEndedAt,
97
+ lastRunError: this.#taskState.lastRunError
98
+ };
99
+ this.#workerState = {
100
+ status: "running"
101
+ };
66
102
  try {
67
103
  await this.fn(taskAbortController.signal);
104
+ this.#taskState.lastRunEndedAt = luxon.DateTime.utc().toISO();
105
+ this.#taskState.lastRunError = void 0;
68
106
  } catch (e) {
107
+ this.#taskState.lastRunEndedAt = luxon.DateTime.utc().toISO();
108
+ this.#taskState.lastRunError = util.serializeError(e);
69
109
  }
70
110
  clearTimeout(timeoutHandle);
71
111
  taskAbortController.abort();
@@ -86,10 +126,18 @@ class LocalTaskWorker {
86
126
  dt = luxon.Duration.fromISO(settings.cadence).as("milliseconds") - lastRunMillis;
87
127
  }
88
128
  dt = Math.max(dt, 0);
129
+ const startsAt = luxon.DateTime.now().plus(luxon.Duration.fromMillis(dt));
130
+ this.#taskState = {
131
+ status: "idle",
132
+ startsAt: startsAt.toISO(),
133
+ lastRunEndedAt: this.#taskState.lastRunEndedAt,
134
+ lastRunError: this.#taskState.lastRunError
135
+ };
136
+ this.#workerState = {
137
+ status: "idle"
138
+ };
89
139
  this.logger.debug(
90
- `task: ${this.taskId} will next occur around ${luxon.DateTime.now().plus(
91
- luxon.Duration.fromMillis(dt)
92
- )}`
140
+ `task: ${this.taskId} will next occur around ${startsAt}`
93
141
  );
94
142
  await this.sleep(luxon.Duration.fromMillis(dt), signal);
95
143
  }
@@ -1 +1 @@
1
- {"version":3,"file":"LocalTaskWorker.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/LocalTaskWorker.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api';\nimport { ConflictError } from '@backstage/errors';\nimport { CronTime } from 'cron';\nimport { DateTime, Duration } from 'luxon';\nimport { TaskSettingsV2 } from './types';\nimport { delegateAbortController, sleep } from './util';\n\n/**\n * Implements tasks that run locally without cross-host collaboration.\n *\n * @private\n */\nexport class LocalTaskWorker {\n private abortWait: AbortController | undefined;\n\n constructor(\n private readonly taskId: string,\n private readonly fn: SchedulerServiceTaskFunction,\n private readonly logger: LoggerService,\n ) {}\n\n start(settings: TaskSettingsV2, options: { signal: AbortSignal }) {\n this.logger.info(\n `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`,\n );\n\n (async () => {\n let attemptNum = 1;\n for (;;) {\n try {\n if (settings.initialDelayDuration) {\n await this.sleep(\n Duration.fromISO(settings.initialDelayDuration),\n options.signal,\n );\n }\n\n while (!options.signal.aborted) {\n const startTime = process.hrtime();\n await this.runOnce(settings, options.signal);\n const timeTaken = process.hrtime(startTime);\n await this.waitUntilNext(\n settings,\n (timeTaken[0] + timeTaken[1] / 1e9) * 1000,\n options.signal,\n );\n }\n\n this.logger.info(`Task worker finished: ${this.taskId}`);\n attemptNum = 0;\n break;\n } catch (e) {\n attemptNum += 1;\n this.logger.warn(\n `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`,\n );\n await sleep(Duration.fromObject({ seconds: 1 }));\n }\n }\n })();\n }\n\n trigger(): void {\n if (!this.abortWait) {\n throw new ConflictError(`Task ${this.taskId} is currently running`);\n }\n this.abortWait.abort();\n }\n\n /**\n * Makes a single attempt at running the task to completion.\n */\n private async runOnce(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n // Abort the task execution either if the worker is stopped, or if the\n // task timeout is hit\n const taskAbortController = delegateAbortController(signal);\n const timeoutHandle = setTimeout(() => {\n taskAbortController.abort();\n }, Duration.fromISO(settings.timeoutAfterDuration).as('milliseconds'));\n\n try {\n await this.fn(taskAbortController.signal);\n } catch (e) {\n // ignore intentionally\n }\n\n // release resources\n clearTimeout(timeoutHandle);\n taskAbortController.abort();\n }\n\n /**\n * Sleeps until it's time to run the task again.\n */\n private async waitUntilNext(\n settings: TaskSettingsV2,\n lastRunMillis: number,\n signal: AbortSignal,\n ) {\n if (signal.aborted) {\n return;\n }\n\n const isCron = !settings.cadence.startsWith('P');\n let dt: number;\n\n if (isCron) {\n const nextRun = +new CronTime(settings.cadence).sendAt().toJSDate();\n dt = nextRun - Date.now();\n } else {\n dt =\n Duration.fromISO(settings.cadence).as('milliseconds') - lastRunMillis;\n }\n\n dt = Math.max(dt, 0);\n\n this.logger.debug(\n `task: ${this.taskId} will next occur around ${DateTime.now().plus(\n Duration.fromMillis(dt),\n )}`,\n );\n\n await this.sleep(Duration.fromMillis(dt), signal);\n }\n\n private async sleep(\n duration: Duration,\n abortSignal: AbortSignal,\n ): Promise<void> {\n this.abortWait = delegateAbortController(abortSignal);\n await sleep(duration, this.abortWait.signal);\n this.abortWait.abort(); // cleans up resources\n this.abortWait = undefined;\n }\n}\n"],"names":["Duration","sleep","ConflictError","delegateAbortController","CronTime","DateTime"],"mappings":"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"}
1
+ {"version":3,"file":"LocalTaskWorker.cjs.js","sources":["../../../../src/entrypoints/scheduler/lib/LocalTaskWorker.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { SchedulerServiceTaskFunction } from '@backstage/backend-plugin-api';\nimport { ConflictError } from '@backstage/errors';\nimport { CronTime } from 'cron';\nimport { DateTime, Duration } from 'luxon';\nimport { TaskSettingsV2, TaskApiTasksResponse } from './types';\nimport { delegateAbortController, serializeError, sleep } from './util';\n\n/**\n * Implements tasks that run locally without cross-host collaboration.\n *\n * @private\n */\nexport class LocalTaskWorker {\n private abortWait: AbortController | undefined;\n #taskState: Exclude<TaskApiTasksResponse['taskState'], null> = {\n status: 'idle',\n };\n #workerState: TaskApiTasksResponse['workerState'] = {\n status: 'idle',\n };\n\n constructor(\n private readonly taskId: string,\n private readonly fn: SchedulerServiceTaskFunction,\n private readonly logger: LoggerService,\n ) {}\n\n start(settings: TaskSettingsV2, options: { signal: AbortSignal }) {\n this.logger.info(\n `Task worker starting: ${this.taskId}, ${JSON.stringify(settings)}`,\n );\n\n (async () => {\n let attemptNum = 1;\n for (;;) {\n try {\n await this.performInitialWait(settings, options.signal);\n\n while (!options.signal.aborted) {\n const startTime = process.hrtime();\n await this.runOnce(settings, options.signal);\n const timeTaken = process.hrtime(startTime);\n await this.waitUntilNext(\n settings,\n (timeTaken[0] + timeTaken[1] / 1e9) * 1000,\n options.signal,\n );\n }\n\n this.logger.info(`Task worker finished: ${this.taskId}`);\n attemptNum = 0;\n break;\n } catch (e) {\n attemptNum += 1;\n this.logger.warn(\n `Task worker failed unexpectedly, attempt number ${attemptNum}, ${e}`,\n );\n await sleep(Duration.fromObject({ seconds: 1 }));\n }\n }\n })();\n }\n\n trigger(): void {\n if (!this.abortWait) {\n throw new ConflictError(`Task ${this.taskId} is currently running`);\n }\n this.abortWait.abort();\n }\n\n taskState(): TaskApiTasksResponse['taskState'] {\n return this.#taskState;\n }\n\n workerState(): TaskApiTasksResponse['workerState'] {\n return this.#workerState;\n }\n\n /**\n * Does the once-at-startup initial wait, if configured.\n */\n private async performInitialWait(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n if (settings.initialDelayDuration) {\n const parsedDuration = Duration.fromISO(settings.initialDelayDuration);\n\n this.#taskState = {\n status: 'idle',\n startsAt: DateTime.utc().plus(parsedDuration).toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'initial-wait',\n };\n\n await this.sleep(parsedDuration, signal);\n }\n }\n\n /**\n * Makes a single attempt at running the task to completion.\n */\n private async runOnce(\n settings: TaskSettingsV2,\n signal: AbortSignal,\n ): Promise<void> {\n // Abort the task execution either if the worker is stopped, or if the\n // task timeout is hit\n const taskAbortController = delegateAbortController(signal);\n const timeoutDuration = Duration.fromISO(settings.timeoutAfterDuration);\n const timeoutHandle = setTimeout(() => {\n taskAbortController.abort();\n }, timeoutDuration.as('milliseconds'));\n\n this.#taskState = {\n status: 'running',\n startedAt: DateTime.utc().toISO()!,\n timesOutAt: DateTime.utc().plus(timeoutDuration).toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'running',\n };\n\n try {\n await this.fn(taskAbortController.signal);\n this.#taskState.lastRunEndedAt = DateTime.utc().toISO()!;\n this.#taskState.lastRunError = undefined;\n } catch (e) {\n this.#taskState.lastRunEndedAt = DateTime.utc().toISO()!;\n this.#taskState.lastRunError = serializeError(e);\n }\n\n // release resources\n clearTimeout(timeoutHandle);\n taskAbortController.abort();\n }\n\n /**\n * Sleeps until it's time to run the task again.\n */\n private async waitUntilNext(\n settings: TaskSettingsV2,\n lastRunMillis: number,\n signal: AbortSignal,\n ) {\n if (signal.aborted) {\n return;\n }\n\n const isCron = !settings.cadence.startsWith('P');\n let dt: number;\n\n if (isCron) {\n const nextRun = +new CronTime(settings.cadence).sendAt().toJSDate();\n dt = nextRun - Date.now();\n } else {\n dt =\n Duration.fromISO(settings.cadence).as('milliseconds') - lastRunMillis;\n }\n\n dt = Math.max(dt, 0);\n const startsAt = DateTime.now().plus(Duration.fromMillis(dt));\n\n this.#taskState = {\n status: 'idle',\n startsAt: startsAt.toISO()!,\n lastRunEndedAt: this.#taskState.lastRunEndedAt,\n lastRunError: this.#taskState.lastRunError,\n };\n this.#workerState = {\n status: 'idle',\n };\n\n this.logger.debug(\n `task: ${this.taskId} will next occur around ${startsAt}`,\n );\n\n await this.sleep(Duration.fromMillis(dt), signal);\n }\n\n private async sleep(\n duration: Duration,\n abortSignal: AbortSignal,\n ): Promise<void> {\n this.abortWait = delegateAbortController(abortSignal);\n await sleep(duration, this.abortWait.signal);\n this.abortWait.abort(); // cleans up resources\n this.abortWait = undefined;\n }\n}\n"],"names":["sleep","Duration","ConflictError","DateTime","delegateAbortController","serializeError","CronTime"],"mappings":"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"}