@rvoh/psychic-workers 2.0.2 → 2.1.0

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.
@@ -92,8 +92,8 @@ export default class BaseBackgroundedModel extends Dream {
92
92
  * in psychic-workers.
93
93
  */
94
94
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
- get psychicTypes() {
96
- throw new Error('Must define psychicTypes getter in BackgroundedService class within your application');
95
+ get psychicWorkerTypes() {
96
+ throw new Error('Must define psychicWorkerTypes getter in ApplicationBackgroundedModel class within your application');
97
97
  }
98
98
  /**
99
99
  * runs the specified method in a background queue, driven by BullMQ,
@@ -104,7 +104,7 @@ export default class BaseBackgroundedService {
104
104
  * in psychic-workers.
105
105
  */
106
106
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
- get psychicTypes() {
108
- throw new Error('Must define psychicTypes getter in BackgroundedService class within your application');
107
+ get psychicWorkerTypes() {
108
+ throw new Error('Must define psychicWorkerTypes getter in ApplicationBackgroundedService class within your application');
109
109
  }
110
110
  }
@@ -73,7 +73,7 @@ export default class BaseScheduledService {
73
73
  * in psychic-workers.
74
74
  */
75
75
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- get psychicTypes() {
77
- throw new Error('Must define psychicTypes getter in BackgroundedService class within your application');
76
+ get psychicWorkerTypes() {
77
+ throw new Error('Must define psychicWorkerTypes getter in ApplicationScheduledService class within your application');
78
78
  }
79
79
  }
@@ -526,8 +526,6 @@ export class Background {
526
526
  if (!queueInstance)
527
527
  throw new Error(`missing queue: ${jobConfig?.queue?.toString() || 'N/A'}`);
528
528
  const jobOptions = {};
529
- if (jobId)
530
- jobOptions.jobId = jobId;
531
529
  if (delay)
532
530
  jobOptions.delay = delay;
533
531
  if (delay && jobId) {
@@ -0,0 +1,176 @@
1
+ import { DreamApp } from '@rvoh/dream';
2
+ import * as path from 'node:path';
3
+ import ts from 'typescript';
4
+ import PsychicAppWorkers from '../psychic-app-workers/index.js';
5
+ const f = ts.factory;
6
+ /**
7
+ * @internal
8
+ *
9
+ * This is a base class, which is inherited by the ASTSchemaBuilder,
10
+ * the ASTKyselyCodegenEnhancer, and the ASTGlobalSchemaBuilder,
11
+ * each of which is responsible for building up the output of the various
12
+ * type files consumed by dream internally.
13
+ *
14
+ * This base class is just a container for common methods used by all
15
+ * classes.
16
+ */
17
+ export default class ASTBuilder {
18
+ /**
19
+ * @internal
20
+ *
21
+ * builds a new line, useful for injecting new lines into AST statements
22
+ */
23
+ newLine() {
24
+ return f.createIdentifier('\n');
25
+ }
26
+ /**
27
+ * @internal
28
+ *
29
+ * given an interface declaration, it will extrace the relevant property statement
30
+ * by the given property name.
31
+ */
32
+ getPropertyFromInterface(interfaceNode, propertyName) {
33
+ for (const member of interfaceNode.members) {
34
+ if (ts.isPropertySignature(member)) {
35
+ if (ts.isIdentifier(member.name) && member.name.text === propertyName) {
36
+ return member;
37
+ }
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+ /**
43
+ * @internal
44
+ *
45
+ * returns an array of string type literals which were extracted from
46
+ * either a type or type union, depending on what is provided
47
+ * for the typeAlias. this allows you to safely and easily collect
48
+ * an array of types given an alias
49
+ */
50
+ extractStringLiteralTypeNodesFromTypeOrUnion(typeAlias) {
51
+ const literals = [];
52
+ if (ts.isUnionTypeNode(typeAlias.type)) {
53
+ typeAlias.type.types.forEach(typeNode => {
54
+ if (ts.isLiteralTypeNode(typeNode) && ts.isStringLiteral(typeNode.literal)) {
55
+ literals.push(typeNode);
56
+ }
57
+ });
58
+ }
59
+ else if (ts.isLiteralTypeNode(typeAlias.type) && ts.isStringLiteral(typeAlias.type.literal)) {
60
+ literals.push(typeAlias.type);
61
+ }
62
+ return literals;
63
+ }
64
+ /**
65
+ * @internal
66
+ *
67
+ * returns an array of type literals which were extracted from
68
+ * either a type or type union, depending on what is provided
69
+ * for the typeAlias. this allows you to safely and easily collect
70
+ * an array of types given an alias
71
+ */
72
+ extractTypeNodesFromTypeOrUnion(typeAlias) {
73
+ const literals = [];
74
+ if (typeAlias.type && ts.isUnionTypeNode(typeAlias.type)) {
75
+ typeAlias.type.types.forEach(typeNode => {
76
+ literals.push(typeNode);
77
+ });
78
+ }
79
+ else if (typeAlias.type) {
80
+ literals.push(typeAlias.type);
81
+ }
82
+ return literals;
83
+ }
84
+ /**
85
+ * @internal
86
+ *
87
+ * returns the provided node iff
88
+ * a.) the node is an exported type alias
89
+ * b.) the exported name matches the provided name (or else there was no name provided)
90
+ *
91
+ * otherwise, returns null
92
+ */
93
+ exportedTypeAliasOrNull(node, exportName) {
94
+ if (ts.isTypeAliasDeclaration(node) &&
95
+ node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
96
+ (!exportName ? true : node.name.text === exportName))
97
+ return node;
98
+ return null;
99
+ }
100
+ /**
101
+ * @internal
102
+ *
103
+ * returns the provided node iff
104
+ * a.) the node is an exported interface
105
+ * b.) the exported name matches the provided name (or else there was no name provided)
106
+ *
107
+ * otherwise, returns null
108
+ */
109
+ exportedInterfaceOrNull(node, exportName) {
110
+ if (ts.isInterfaceDeclaration(node) &&
111
+ node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
112
+ (!exportName ? true : node.name.text === exportName))
113
+ return node;
114
+ return null;
115
+ }
116
+ /**
117
+ * @internal
118
+ *
119
+ * returns the path to the dream.globals.ts file
120
+ */
121
+ workersSchemaPath() {
122
+ const workersApp = PsychicAppWorkers.getOrFail();
123
+ return path.join(workersApp.psychicApp.apiRoot, DreamApp.getOrFail().paths.types, 'workers.ts');
124
+ }
125
+ /**
126
+ * @internal
127
+ *
128
+ * safely runs prettier against the provided output. If prettier
129
+ * is not installed, then the original output is returned
130
+ */
131
+ async prettier(output) {
132
+ try {
133
+ // dynamically, safely bring in prettier.
134
+ // ini the event that it fails, we will return the
135
+ // original output, unformatted, since prettier
136
+ // is technically not a real dependency of dream,
137
+ // though psychic and dream apps are provisioned
138
+ // with prettier by default, so this should usually work
139
+ const prettier = (await import('prettier')).default;
140
+ const results = await prettier.format(output, {
141
+ parser: 'typescript',
142
+ semi: false,
143
+ singleQuote: true,
144
+ tabWidth: 2,
145
+ lineWidth: 80,
146
+ });
147
+ return typeof results === 'string' ? results : output;
148
+ }
149
+ catch {
150
+ // intentional noop, we don't want to raise if prettier
151
+ // fails, since it is possible for the end user to not
152
+ // want to use prettier, and it is not a required peer
153
+ // dependency of dream
154
+ return output;
155
+ }
156
+ }
157
+ /**
158
+ * @internal
159
+ *
160
+ * given a type node, it will send back the first found generic
161
+ * provided to that type.
162
+ */
163
+ getFirstGenericType(node) {
164
+ if (ts.isTypeReferenceNode(node)) {
165
+ if (node.typeArguments && node.typeArguments.length > 0) {
166
+ return node.typeArguments[0];
167
+ }
168
+ }
169
+ else if (ts.isCallExpression(node)) {
170
+ if (node.typeArguments && node.typeArguments.length > 0) {
171
+ return node.typeArguments[0];
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ }
@@ -0,0 +1,60 @@
1
+ import { CliFileWriter, DreamCLI } from '@rvoh/dream/system';
2
+ import ts from 'typescript';
3
+ import background from '../background/index.js';
4
+ import ASTBuilder from './ASTBuilder.js';
5
+ const f = ts.factory;
6
+ /**
7
+ * Responsible for building dream globals, which can be found at
8
+ * types/dream.globals.ts.
9
+ *
10
+ * This class leverages internal AST building mechanisms built into
11
+ * typescript to manually build up object literals and interfaces
12
+ * for our app to consume.
13
+ */
14
+ export default class ASTWorkersSchemaBuilder extends ASTBuilder {
15
+ async build() {
16
+ const logger = DreamCLI.logger;
17
+ const sourceFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
18
+ await logger.logProgress('[psychic workers] building workers types', async () => {
19
+ const output = await this.prettier(this.printStatements(this.buildWorkersTypeConfigConst(), sourceFile));
20
+ await CliFileWriter.write(this.workersSchemaPath(), output);
21
+ });
22
+ }
23
+ /**
24
+ * @internal
25
+ *
26
+ * builds up the `export const globalTypeConfig = ...` statement within the types/workers.ts
27
+ * file. It does this by leveraging low-level AST utils built into typescript
28
+ * to manually build up an object literal, cast it as a const, and write it to
29
+ * an exported variable.
30
+ */
31
+ buildWorkersTypeConfigConst() {
32
+ background.connect();
33
+ const globalTypeConfigObjectLiteral = f.createObjectLiteralExpression([
34
+ f.createPropertyAssignment(f.createIdentifier('workstreamNames'), f.createArrayLiteralExpression(background['workstreamNames'].map(key => f.createStringLiteral(key)))),
35
+ f.createPropertyAssignment(f.createIdentifier('queueGroupMap'), f.createObjectLiteralExpression(Object.keys(background['groupNames']).map(key => f.createPropertyAssignment(f.createStringLiteral(key), f.createArrayLiteralExpression(background['groupNames'][key].map(str => f.createStringLiteral(str))))), true)),
36
+ ], true);
37
+ // add "as const" to the end of the schema object we
38
+ // have built before returning it
39
+ const constAssertion = f.createAsExpression(globalTypeConfigObjectLiteral, f.createKeywordTypeNode(ts.SyntaxKind.ConstKeyword));
40
+ const psychicWorkerTypesObjectLiteralConst = f.createVariableStatement(undefined, f.createVariableDeclarationList([
41
+ f.createVariableDeclaration(f.createIdentifier('psychicWorkerTypes'), undefined, undefined, constAssertion),
42
+ ], ts.NodeFlags.Const));
43
+ const defaultExportIdentifier = f.createIdentifier('psychicWorkerTypes');
44
+ const exportDefaultStatement = f.createExportDefault(defaultExportIdentifier);
45
+ return [psychicWorkerTypesObjectLiteralConst, this.newLine(), exportDefaultStatement];
46
+ }
47
+ /**
48
+ * @internal
49
+ *
50
+ * writes the compiled statements to string.
51
+ *
52
+ */
53
+ printStatements(statements, sourceFile) {
54
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, omitTrailingSemicolon: true });
55
+ const result = printer.printList(ts.ListFormat.SourceFileStatements, f.createNodeArray(statements), sourceFile);
56
+ // TODO: add autogenerate disclaimer
57
+ return `\
58
+ ${result}`;
59
+ }
60
+ }
@@ -0,0 +1,67 @@
1
+ import { DreamApp } from '@rvoh/dream';
2
+ import { DreamCLI } from '@rvoh/dream/system';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as path from 'node:path';
5
+ import PsychicAppWorkers from '../psychic-app-workers/index.js';
6
+ /**
7
+ * Originally, psychic-workers tapped into the types produced by psychic,
8
+ * modifying the psychicTypes to include type configurations for workers
9
+ * as well. Since Psychic no longer supports this method of augmenting
10
+ * types, psychic-workers has been refactored to produce its own types
11
+ * file.
12
+ *
13
+ * This service is responsible for identifying applications that are still
14
+ * reliant on the types produced by psychic, and refactoring them so that their
15
+ * imports are now in the correct places.
16
+ */
17
+ export default class PsychicTypesDeprecation {
18
+ async deprecate() {
19
+ const workersApp = PsychicAppWorkers.getOrFail();
20
+ if (workersApp.bypassDeprecationChecks)
21
+ return;
22
+ const files = [
23
+ path.join(process.cwd(), DreamApp.system.dreamPath('models'), 'ApplicationBackgroundedModel.ts'),
24
+ path.join(process.cwd(), DreamApp.system.dreamPath('models'), '..', 'services', 'ApplicationBackgroundedService.ts'),
25
+ path.join(process.cwd(), DreamApp.system.dreamPath('models'), '..', 'services', 'ApplicationScheduledService.ts'),
26
+ ];
27
+ try {
28
+ for (const file of files) {
29
+ const fileContent = (await fs.readFile(file)).toString();
30
+ if (fileContent.includes('psychicTypes')) {
31
+ await DreamCLI.logger.logProgress(`[psychic workers] patching deprecated types for ${file.split(path.sep).at(-1)}`, async () => {
32
+ await fs.writeFile(file, fileContent
33
+ .replace(/psychicTypes/g, 'psychicWorkerTypes')
34
+ .replace(/types\/psychic\.js/, 'types/workers.js'));
35
+ });
36
+ }
37
+ }
38
+ }
39
+ catch (err) {
40
+ console.error(err);
41
+ console.log(`
42
+ ATTENTION:
43
+
44
+ The psychic-workers package now requires a new configuration in order to continue providing types
45
+ in the modern psychic ecosystem. We attempted to automatically fix this for you, but something went
46
+ wrong. Please locate the following files, and ensure that they no longer provide the "psychicTypes" getter.
47
+ they should instead provide a psychicWorkerTypes getter in its place, which brings in types that
48
+ are now located in the newly-generated "types/workers.ts" file.
49
+
50
+ For the ApplicationBackgroundedModel.ts, ApplicationScheduledService.ts, and ApplicationBackgroundedService.ts
51
+ files in your system, ensure that their "psychicTypes" getter is replaced with the "psychicWorkerTypes"
52
+ getter, like so:
53
+
54
+
55
+ import { psychicWorkerTypes } from '@src/types/workers.js'
56
+
57
+ export default class ApplicationBackgroundedModel extends BaseBackgroundedModel {
58
+ ...
59
+
60
+ public override get psychicWorkerTypes() {
61
+ return psychicWorkerTypes
62
+ }
63
+ }
64
+ `);
65
+ }
66
+ }
67
+ }
@@ -1,17 +1,15 @@
1
1
  import { Queue, Worker } from 'bullmq';
2
- import { cachePsychicWorkersApp, getCachedPsychicWorkersAppOrFail } from './cache.js';
3
2
  import background from '../background/index.js';
3
+ import ASTWorkersSchemaBuilder from '../cli/ASTWorkersSchemaBuilder.js';
4
+ import PsychicTypesDeprecation from '../cli/PsychicTypesDeprecation.js';
5
+ import { cachePsychicWorkersApp, getCachedPsychicWorkersAppOrFail } from './cache.js';
4
6
  export default class PsychicAppWorkers {
5
7
  static async init(psychicApp, cb) {
6
8
  const psychicWorkersApp = new PsychicAppWorkers(psychicApp);
7
9
  await cb(psychicWorkersApp);
8
- psychicApp.on('cli:sync', () => {
9
- background.connect();
10
- const output = {
11
- workstreamNames: [...background['workstreamNames']],
12
- queueGroupMap: { ...background['groupNames'] },
13
- };
14
- return output;
10
+ psychicApp.on('cli:sync', async () => {
11
+ await new ASTWorkersSchemaBuilder().build();
12
+ await new PsychicTypesDeprecation().deprecate();
15
13
  });
16
14
  psychicApp.on('server:shutdown', async () => {
17
15
  await background.closeAllRedisConnections();
@@ -56,6 +54,16 @@ export default class PsychicAppWorkers {
56
54
  return this._testInvocation;
57
55
  }
58
56
  _testInvocation = 'automatic';
57
+ /**
58
+ * if set to true, it will bypass deprecation checks that run
59
+ * during the sync hook. Defaults to false, we only recommend
60
+ * overriding this if you are having issues with the deprecation
61
+ * check.
62
+ */
63
+ get bypassDeprecationChecks() {
64
+ return this._bypassDeprecationChecks;
65
+ }
66
+ _bypassDeprecationChecks = false;
59
67
  _hooks = {
60
68
  workerShutdown: [],
61
69
  };
@@ -87,6 +95,9 @@ export default class PsychicAppWorkers {
87
95
  case 'testInvocation':
88
96
  this._testInvocation = value;
89
97
  break;
98
+ case 'bypassDeprecationChecks':
99
+ this._bypassDeprecationChecks = value;
100
+ break;
90
101
  default:
91
102
  throw new Error(`Unhandled option type passed to PsychicWorkersApp#set: ${option}`);
92
103
  }
@@ -92,8 +92,8 @@ export default class BaseBackgroundedModel extends Dream {
92
92
  * in psychic-workers.
93
93
  */
94
94
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
- get psychicTypes() {
96
- throw new Error('Must define psychicTypes getter in BackgroundedService class within your application');
95
+ get psychicWorkerTypes() {
96
+ throw new Error('Must define psychicWorkerTypes getter in ApplicationBackgroundedModel class within your application');
97
97
  }
98
98
  /**
99
99
  * runs the specified method in a background queue, driven by BullMQ,
@@ -104,7 +104,7 @@ export default class BaseBackgroundedService {
104
104
  * in psychic-workers.
105
105
  */
106
106
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
- get psychicTypes() {
108
- throw new Error('Must define psychicTypes getter in BackgroundedService class within your application');
107
+ get psychicWorkerTypes() {
108
+ throw new Error('Must define psychicWorkerTypes getter in ApplicationBackgroundedService class within your application');
109
109
  }
110
110
  }
@@ -73,7 +73,7 @@ export default class BaseScheduledService {
73
73
  * in psychic-workers.
74
74
  */
75
75
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- get psychicTypes() {
77
- throw new Error('Must define psychicTypes getter in BackgroundedService class within your application');
76
+ get psychicWorkerTypes() {
77
+ throw new Error('Must define psychicWorkerTypes getter in ApplicationScheduledService class within your application');
78
78
  }
79
79
  }
@@ -526,8 +526,6 @@ export class Background {
526
526
  if (!queueInstance)
527
527
  throw new Error(`missing queue: ${jobConfig?.queue?.toString() || 'N/A'}`);
528
528
  const jobOptions = {};
529
- if (jobId)
530
- jobOptions.jobId = jobId;
531
529
  if (delay)
532
530
  jobOptions.delay = delay;
533
531
  if (delay && jobId) {
@@ -0,0 +1,176 @@
1
+ import { DreamApp } from '@rvoh/dream';
2
+ import * as path from 'node:path';
3
+ import ts from 'typescript';
4
+ import PsychicAppWorkers from '../psychic-app-workers/index.js';
5
+ const f = ts.factory;
6
+ /**
7
+ * @internal
8
+ *
9
+ * This is a base class, which is inherited by the ASTSchemaBuilder,
10
+ * the ASTKyselyCodegenEnhancer, and the ASTGlobalSchemaBuilder,
11
+ * each of which is responsible for building up the output of the various
12
+ * type files consumed by dream internally.
13
+ *
14
+ * This base class is just a container for common methods used by all
15
+ * classes.
16
+ */
17
+ export default class ASTBuilder {
18
+ /**
19
+ * @internal
20
+ *
21
+ * builds a new line, useful for injecting new lines into AST statements
22
+ */
23
+ newLine() {
24
+ return f.createIdentifier('\n');
25
+ }
26
+ /**
27
+ * @internal
28
+ *
29
+ * given an interface declaration, it will extrace the relevant property statement
30
+ * by the given property name.
31
+ */
32
+ getPropertyFromInterface(interfaceNode, propertyName) {
33
+ for (const member of interfaceNode.members) {
34
+ if (ts.isPropertySignature(member)) {
35
+ if (ts.isIdentifier(member.name) && member.name.text === propertyName) {
36
+ return member;
37
+ }
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+ /**
43
+ * @internal
44
+ *
45
+ * returns an array of string type literals which were extracted from
46
+ * either a type or type union, depending on what is provided
47
+ * for the typeAlias. this allows you to safely and easily collect
48
+ * an array of types given an alias
49
+ */
50
+ extractStringLiteralTypeNodesFromTypeOrUnion(typeAlias) {
51
+ const literals = [];
52
+ if (ts.isUnionTypeNode(typeAlias.type)) {
53
+ typeAlias.type.types.forEach(typeNode => {
54
+ if (ts.isLiteralTypeNode(typeNode) && ts.isStringLiteral(typeNode.literal)) {
55
+ literals.push(typeNode);
56
+ }
57
+ });
58
+ }
59
+ else if (ts.isLiteralTypeNode(typeAlias.type) && ts.isStringLiteral(typeAlias.type.literal)) {
60
+ literals.push(typeAlias.type);
61
+ }
62
+ return literals;
63
+ }
64
+ /**
65
+ * @internal
66
+ *
67
+ * returns an array of type literals which were extracted from
68
+ * either a type or type union, depending on what is provided
69
+ * for the typeAlias. this allows you to safely and easily collect
70
+ * an array of types given an alias
71
+ */
72
+ extractTypeNodesFromTypeOrUnion(typeAlias) {
73
+ const literals = [];
74
+ if (typeAlias.type && ts.isUnionTypeNode(typeAlias.type)) {
75
+ typeAlias.type.types.forEach(typeNode => {
76
+ literals.push(typeNode);
77
+ });
78
+ }
79
+ else if (typeAlias.type) {
80
+ literals.push(typeAlias.type);
81
+ }
82
+ return literals;
83
+ }
84
+ /**
85
+ * @internal
86
+ *
87
+ * returns the provided node iff
88
+ * a.) the node is an exported type alias
89
+ * b.) the exported name matches the provided name (or else there was no name provided)
90
+ *
91
+ * otherwise, returns null
92
+ */
93
+ exportedTypeAliasOrNull(node, exportName) {
94
+ if (ts.isTypeAliasDeclaration(node) &&
95
+ node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
96
+ (!exportName ? true : node.name.text === exportName))
97
+ return node;
98
+ return null;
99
+ }
100
+ /**
101
+ * @internal
102
+ *
103
+ * returns the provided node iff
104
+ * a.) the node is an exported interface
105
+ * b.) the exported name matches the provided name (or else there was no name provided)
106
+ *
107
+ * otherwise, returns null
108
+ */
109
+ exportedInterfaceOrNull(node, exportName) {
110
+ if (ts.isInterfaceDeclaration(node) &&
111
+ node?.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
112
+ (!exportName ? true : node.name.text === exportName))
113
+ return node;
114
+ return null;
115
+ }
116
+ /**
117
+ * @internal
118
+ *
119
+ * returns the path to the dream.globals.ts file
120
+ */
121
+ workersSchemaPath() {
122
+ const workersApp = PsychicAppWorkers.getOrFail();
123
+ return path.join(workersApp.psychicApp.apiRoot, DreamApp.getOrFail().paths.types, 'workers.ts');
124
+ }
125
+ /**
126
+ * @internal
127
+ *
128
+ * safely runs prettier against the provided output. If prettier
129
+ * is not installed, then the original output is returned
130
+ */
131
+ async prettier(output) {
132
+ try {
133
+ // dynamically, safely bring in prettier.
134
+ // ini the event that it fails, we will return the
135
+ // original output, unformatted, since prettier
136
+ // is technically not a real dependency of dream,
137
+ // though psychic and dream apps are provisioned
138
+ // with prettier by default, so this should usually work
139
+ const prettier = (await import('prettier')).default;
140
+ const results = await prettier.format(output, {
141
+ parser: 'typescript',
142
+ semi: false,
143
+ singleQuote: true,
144
+ tabWidth: 2,
145
+ lineWidth: 80,
146
+ });
147
+ return typeof results === 'string' ? results : output;
148
+ }
149
+ catch {
150
+ // intentional noop, we don't want to raise if prettier
151
+ // fails, since it is possible for the end user to not
152
+ // want to use prettier, and it is not a required peer
153
+ // dependency of dream
154
+ return output;
155
+ }
156
+ }
157
+ /**
158
+ * @internal
159
+ *
160
+ * given a type node, it will send back the first found generic
161
+ * provided to that type.
162
+ */
163
+ getFirstGenericType(node) {
164
+ if (ts.isTypeReferenceNode(node)) {
165
+ if (node.typeArguments && node.typeArguments.length > 0) {
166
+ return node.typeArguments[0];
167
+ }
168
+ }
169
+ else if (ts.isCallExpression(node)) {
170
+ if (node.typeArguments && node.typeArguments.length > 0) {
171
+ return node.typeArguments[0];
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ }
@@ -0,0 +1,60 @@
1
+ import { CliFileWriter, DreamCLI } from '@rvoh/dream/system';
2
+ import ts from 'typescript';
3
+ import background from '../background/index.js';
4
+ import ASTBuilder from './ASTBuilder.js';
5
+ const f = ts.factory;
6
+ /**
7
+ * Responsible for building dream globals, which can be found at
8
+ * types/dream.globals.ts.
9
+ *
10
+ * This class leverages internal AST building mechanisms built into
11
+ * typescript to manually build up object literals and interfaces
12
+ * for our app to consume.
13
+ */
14
+ export default class ASTWorkersSchemaBuilder extends ASTBuilder {
15
+ async build() {
16
+ const logger = DreamCLI.logger;
17
+ const sourceFile = ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
18
+ await logger.logProgress('[psychic workers] building workers types', async () => {
19
+ const output = await this.prettier(this.printStatements(this.buildWorkersTypeConfigConst(), sourceFile));
20
+ await CliFileWriter.write(this.workersSchemaPath(), output);
21
+ });
22
+ }
23
+ /**
24
+ * @internal
25
+ *
26
+ * builds up the `export const globalTypeConfig = ...` statement within the types/workers.ts
27
+ * file. It does this by leveraging low-level AST utils built into typescript
28
+ * to manually build up an object literal, cast it as a const, and write it to
29
+ * an exported variable.
30
+ */
31
+ buildWorkersTypeConfigConst() {
32
+ background.connect();
33
+ const globalTypeConfigObjectLiteral = f.createObjectLiteralExpression([
34
+ f.createPropertyAssignment(f.createIdentifier('workstreamNames'), f.createArrayLiteralExpression(background['workstreamNames'].map(key => f.createStringLiteral(key)))),
35
+ f.createPropertyAssignment(f.createIdentifier('queueGroupMap'), f.createObjectLiteralExpression(Object.keys(background['groupNames']).map(key => f.createPropertyAssignment(f.createStringLiteral(key), f.createArrayLiteralExpression(background['groupNames'][key].map(str => f.createStringLiteral(str))))), true)),
36
+ ], true);
37
+ // add "as const" to the end of the schema object we
38
+ // have built before returning it
39
+ const constAssertion = f.createAsExpression(globalTypeConfigObjectLiteral, f.createKeywordTypeNode(ts.SyntaxKind.ConstKeyword));
40
+ const psychicWorkerTypesObjectLiteralConst = f.createVariableStatement(undefined, f.createVariableDeclarationList([
41
+ f.createVariableDeclaration(f.createIdentifier('psychicWorkerTypes'), undefined, undefined, constAssertion),
42
+ ], ts.NodeFlags.Const));
43
+ const defaultExportIdentifier = f.createIdentifier('psychicWorkerTypes');
44
+ const exportDefaultStatement = f.createExportDefault(defaultExportIdentifier);
45
+ return [psychicWorkerTypesObjectLiteralConst, this.newLine(), exportDefaultStatement];
46
+ }
47
+ /**
48
+ * @internal
49
+ *
50
+ * writes the compiled statements to string.
51
+ *
52
+ */
53
+ printStatements(statements, sourceFile) {
54
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, omitTrailingSemicolon: true });
55
+ const result = printer.printList(ts.ListFormat.SourceFileStatements, f.createNodeArray(statements), sourceFile);
56
+ // TODO: add autogenerate disclaimer
57
+ return `\
58
+ ${result}`;
59
+ }
60
+ }
@@ -0,0 +1,67 @@
1
+ import { DreamApp } from '@rvoh/dream';
2
+ import { DreamCLI } from '@rvoh/dream/system';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as path from 'node:path';
5
+ import PsychicAppWorkers from '../psychic-app-workers/index.js';
6
+ /**
7
+ * Originally, psychic-workers tapped into the types produced by psychic,
8
+ * modifying the psychicTypes to include type configurations for workers
9
+ * as well. Since Psychic no longer supports this method of augmenting
10
+ * types, psychic-workers has been refactored to produce its own types
11
+ * file.
12
+ *
13
+ * This service is responsible for identifying applications that are still
14
+ * reliant on the types produced by psychic, and refactoring them so that their
15
+ * imports are now in the correct places.
16
+ */
17
+ export default class PsychicTypesDeprecation {
18
+ async deprecate() {
19
+ const workersApp = PsychicAppWorkers.getOrFail();
20
+ if (workersApp.bypassDeprecationChecks)
21
+ return;
22
+ const files = [
23
+ path.join(process.cwd(), DreamApp.system.dreamPath('models'), 'ApplicationBackgroundedModel.ts'),
24
+ path.join(process.cwd(), DreamApp.system.dreamPath('models'), '..', 'services', 'ApplicationBackgroundedService.ts'),
25
+ path.join(process.cwd(), DreamApp.system.dreamPath('models'), '..', 'services', 'ApplicationScheduledService.ts'),
26
+ ];
27
+ try {
28
+ for (const file of files) {
29
+ const fileContent = (await fs.readFile(file)).toString();
30
+ if (fileContent.includes('psychicTypes')) {
31
+ await DreamCLI.logger.logProgress(`[psychic workers] patching deprecated types for ${file.split(path.sep).at(-1)}`, async () => {
32
+ await fs.writeFile(file, fileContent
33
+ .replace(/psychicTypes/g, 'psychicWorkerTypes')
34
+ .replace(/types\/psychic\.js/, 'types/workers.js'));
35
+ });
36
+ }
37
+ }
38
+ }
39
+ catch (err) {
40
+ console.error(err);
41
+ console.log(`
42
+ ATTENTION:
43
+
44
+ The psychic-workers package now requires a new configuration in order to continue providing types
45
+ in the modern psychic ecosystem. We attempted to automatically fix this for you, but something went
46
+ wrong. Please locate the following files, and ensure that they no longer provide the "psychicTypes" getter.
47
+ they should instead provide a psychicWorkerTypes getter in its place, which brings in types that
48
+ are now located in the newly-generated "types/workers.ts" file.
49
+
50
+ For the ApplicationBackgroundedModel.ts, ApplicationScheduledService.ts, and ApplicationBackgroundedService.ts
51
+ files in your system, ensure that their "psychicTypes" getter is replaced with the "psychicWorkerTypes"
52
+ getter, like so:
53
+
54
+
55
+ import { psychicWorkerTypes } from '@src/types/workers.js'
56
+
57
+ export default class ApplicationBackgroundedModel extends BaseBackgroundedModel {
58
+ ...
59
+
60
+ public override get psychicWorkerTypes() {
61
+ return psychicWorkerTypes
62
+ }
63
+ }
64
+ `);
65
+ }
66
+ }
67
+ }
@@ -1,17 +1,15 @@
1
1
  import { Queue, Worker } from 'bullmq';
2
- import { cachePsychicWorkersApp, getCachedPsychicWorkersAppOrFail } from './cache.js';
3
2
  import background from '../background/index.js';
3
+ import ASTWorkersSchemaBuilder from '../cli/ASTWorkersSchemaBuilder.js';
4
+ import PsychicTypesDeprecation from '../cli/PsychicTypesDeprecation.js';
5
+ import { cachePsychicWorkersApp, getCachedPsychicWorkersAppOrFail } from './cache.js';
4
6
  export default class PsychicAppWorkers {
5
7
  static async init(psychicApp, cb) {
6
8
  const psychicWorkersApp = new PsychicAppWorkers(psychicApp);
7
9
  await cb(psychicWorkersApp);
8
- psychicApp.on('cli:sync', () => {
9
- background.connect();
10
- const output = {
11
- workstreamNames: [...background['workstreamNames']],
12
- queueGroupMap: { ...background['groupNames'] },
13
- };
14
- return output;
10
+ psychicApp.on('cli:sync', async () => {
11
+ await new ASTWorkersSchemaBuilder().build();
12
+ await new PsychicTypesDeprecation().deprecate();
15
13
  });
16
14
  psychicApp.on('server:shutdown', async () => {
17
15
  await background.closeAllRedisConnections();
@@ -56,6 +54,16 @@ export default class PsychicAppWorkers {
56
54
  return this._testInvocation;
57
55
  }
58
56
  _testInvocation = 'automatic';
57
+ /**
58
+ * if set to true, it will bypass deprecation checks that run
59
+ * during the sync hook. Defaults to false, we only recommend
60
+ * overriding this if you are having issues with the deprecation
61
+ * check.
62
+ */
63
+ get bypassDeprecationChecks() {
64
+ return this._bypassDeprecationChecks;
65
+ }
66
+ _bypassDeprecationChecks = false;
59
67
  _hooks = {
60
68
  workerShutdown: [],
61
69
  };
@@ -87,6 +95,9 @@ export default class PsychicAppWorkers {
87
95
  case 'testInvocation':
88
96
  this._testInvocation = value;
89
97
  break;
98
+ case 'bypassDeprecationChecks':
99
+ this._bypassDeprecationChecks = value;
100
+ break;
90
101
  default:
91
102
  throw new Error(`Unhandled option type passed to PsychicWorkersApp#set: ${option}`);
92
103
  }
@@ -71,7 +71,7 @@ export default class BaseBackgroundedModel extends Dream {
71
71
  * metadata, which can be used to help provide types for the underlying methods
72
72
  * in psychic-workers.
73
73
  */
74
- get psychicTypes(): any;
74
+ get psychicWorkerTypes(): any;
75
75
  /**
76
76
  * runs the specified method in a background queue, driven by BullMQ,
77
77
  * sending in the provided args.
@@ -79,7 +79,7 @@ export default class BaseBackgroundedService {
79
79
  * metadata, which can be used to help provide types for the underlying methods
80
80
  * in psychic-workers.
81
81
  */
82
- get psychicTypes(): any;
82
+ get psychicWorkerTypes(): any;
83
83
  }
84
84
  export type PsychicBackgroundedServiceStaticMethods<T extends typeof BaseBackgroundedService> = Exclude<FunctionPropertyNames<Required<T>>, FunctionPropertyNames<typeof BaseBackgroundedService>>;
85
85
  export type PsychicBackgroundedServiceInstanceMethods<T extends BaseBackgroundedService> = Exclude<FunctionPropertyNames<Required<T>>, FunctionPropertyNames<BaseBackgroundedService>>;
@@ -57,6 +57,6 @@ export default class BaseScheduledService {
57
57
  * metadata, which can be used to help provide types for the underlying methods
58
58
  * in psychic-workers.
59
59
  */
60
- get psychicTypes(): any;
60
+ get psychicWorkerTypes(): any;
61
61
  }
62
62
  export type PsychicScheduledServiceStaticMethods<T extends typeof BaseScheduledService> = Exclude<FunctionPropertyNames<Required<T>>, FunctionPropertyNames<typeof BaseScheduledService>>;
@@ -0,0 +1,89 @@
1
+ import ts from 'typescript';
2
+ /**
3
+ * @internal
4
+ *
5
+ * This is a base class, which is inherited by the ASTSchemaBuilder,
6
+ * the ASTKyselyCodegenEnhancer, and the ASTGlobalSchemaBuilder,
7
+ * each of which is responsible for building up the output of the various
8
+ * type files consumed by dream internally.
9
+ *
10
+ * This base class is just a container for common methods used by all
11
+ * classes.
12
+ */
13
+ export default class ASTBuilder {
14
+ /**
15
+ * @internal
16
+ *
17
+ * builds a new line, useful for injecting new lines into AST statements
18
+ */
19
+ protected newLine(): ts.Identifier;
20
+ /**
21
+ * @internal
22
+ *
23
+ * given an interface declaration, it will extrace the relevant property statement
24
+ * by the given property name.
25
+ */
26
+ protected getPropertyFromInterface(interfaceNode: ts.InterfaceDeclaration, propertyName: string): ts.PropertySignature | null;
27
+ /**
28
+ * @internal
29
+ *
30
+ * returns an array of string type literals which were extracted from
31
+ * either a type or type union, depending on what is provided
32
+ * for the typeAlias. this allows you to safely and easily collect
33
+ * an array of types given an alias
34
+ */
35
+ protected extractStringLiteralTypeNodesFromTypeOrUnion(typeAlias: ts.TypeAliasDeclaration): (ts.LiteralTypeNode & {
36
+ literal: {
37
+ text: string;
38
+ };
39
+ })[];
40
+ /**
41
+ * @internal
42
+ *
43
+ * returns an array of type literals which were extracted from
44
+ * either a type or type union, depending on what is provided
45
+ * for the typeAlias. this allows you to safely and easily collect
46
+ * an array of types given an alias
47
+ */
48
+ protected extractTypeNodesFromTypeOrUnion(typeAlias: ts.TypeAliasDeclaration | ts.PropertySignature): ts.TypeNode[];
49
+ /**
50
+ * @internal
51
+ *
52
+ * returns the provided node iff
53
+ * a.) the node is an exported type alias
54
+ * b.) the exported name matches the provided name (or else there was no name provided)
55
+ *
56
+ * otherwise, returns null
57
+ */
58
+ protected exportedTypeAliasOrNull(node: ts.Node, exportName?: string): ts.TypeAliasDeclaration | null;
59
+ /**
60
+ * @internal
61
+ *
62
+ * returns the provided node iff
63
+ * a.) the node is an exported interface
64
+ * b.) the exported name matches the provided name (or else there was no name provided)
65
+ *
66
+ * otherwise, returns null
67
+ */
68
+ protected exportedInterfaceOrNull(node: ts.Node, exportName?: string): ts.InterfaceDeclaration | null;
69
+ /**
70
+ * @internal
71
+ *
72
+ * returns the path to the dream.globals.ts file
73
+ */
74
+ protected workersSchemaPath(): string;
75
+ /**
76
+ * @internal
77
+ *
78
+ * safely runs prettier against the provided output. If prettier
79
+ * is not installed, then the original output is returned
80
+ */
81
+ protected prettier(output: string): Promise<string>;
82
+ /**
83
+ * @internal
84
+ *
85
+ * given a type node, it will send back the first found generic
86
+ * provided to that type.
87
+ */
88
+ protected getFirstGenericType(node: ts.Node): ts.TypeNode | null;
89
+ }
@@ -0,0 +1,28 @@
1
+ import ASTBuilder from './ASTBuilder.js';
2
+ /**
3
+ * Responsible for building dream globals, which can be found at
4
+ * types/dream.globals.ts.
5
+ *
6
+ * This class leverages internal AST building mechanisms built into
7
+ * typescript to manually build up object literals and interfaces
8
+ * for our app to consume.
9
+ */
10
+ export default class ASTWorkersSchemaBuilder extends ASTBuilder {
11
+ build(): Promise<void>;
12
+ /**
13
+ * @internal
14
+ *
15
+ * builds up the `export const globalTypeConfig = ...` statement within the types/workers.ts
16
+ * file. It does this by leveraging low-level AST utils built into typescript
17
+ * to manually build up an object literal, cast it as a const, and write it to
18
+ * an exported variable.
19
+ */
20
+ private buildWorkersTypeConfigConst;
21
+ /**
22
+ * @internal
23
+ *
24
+ * writes the compiled statements to string.
25
+ *
26
+ */
27
+ private printStatements;
28
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Originally, psychic-workers tapped into the types produced by psychic,
3
+ * modifying the psychicTypes to include type configurations for workers
4
+ * as well. Since Psychic no longer supports this method of augmenting
5
+ * types, psychic-workers has been refactored to produce its own types
6
+ * file.
7
+ *
8
+ * This service is responsible for identifying applications that are still
9
+ * reliant on the types produced by psychic, and refactoring them so that their
10
+ * imports are now in the correct places.
11
+ */
12
+ export default class PsychicTypesDeprecation {
13
+ deprecate(): Promise<void>;
14
+ }
@@ -30,16 +30,24 @@ export default class PsychicAppWorkers {
30
30
  */
31
31
  get testInvocation(): PsychicWorkersAppTestInvocationType;
32
32
  private _testInvocation;
33
+ /**
34
+ * if set to true, it will bypass deprecation checks that run
35
+ * during the sync hook. Defaults to false, we only recommend
36
+ * overriding this if you are having issues with the deprecation
37
+ * check.
38
+ */
39
+ get bypassDeprecationChecks(): boolean;
40
+ private _bypassDeprecationChecks;
33
41
  private _hooks;
34
42
  get hooks(): PsychicWorkersAppHooks;
35
43
  on<T extends PsychicWorkersHookEventType>(hookEventType: T, cb: T extends 'workers:shutdown' ? () => void | Promise<void> : never): void;
36
- set<Opt extends PsychicWorkersAppOption>(option: Opt, value: Opt extends 'background' ? PsychicBackgroundOptions : Opt extends 'testInvocation' ? PsychicWorkersAppTestInvocationType : unknown): void;
44
+ set<Opt extends PsychicWorkersAppOption>(option: Opt, value: Opt extends 'background' ? PsychicBackgroundOptions : Opt extends 'testInvocation' ? PsychicWorkersAppTestInvocationType : Opt extends 'bypassDeprecationChecks' ? boolean : unknown): void;
37
45
  }
38
46
  export interface PsychicWorkersTypeSync {
39
47
  workstreamNames: string[];
40
48
  queueGroupMap: Record<string, string[]>;
41
49
  }
42
- export type PsychicWorkersAppOption = 'background' | 'testInvocation';
50
+ export type PsychicWorkersAppOption = 'background' | 'testInvocation' | 'bypassDeprecationChecks';
43
51
  export type PsychicWorkersAppTestInvocationType = 'automatic' | 'manual';
44
52
  export type PsychicWorkersHookEventType = 'workers:shutdown';
45
53
  export interface PsychicWorkersAppHooks {
@@ -49,9 +49,9 @@ interface BaseBackgroundJobConfig {
49
49
  priority?: BackgroundQueuePriority;
50
50
  }
51
51
  export interface WorkstreamBackgroundJobConfig<T extends BaseScheduledService | BaseBackgroundedService> extends BaseBackgroundJobConfig {
52
- workstream?: T['psychicTypes']['workstreamNames'][number];
52
+ workstream?: T['psychicWorkerTypes']['workstreamNames'][number];
53
53
  }
54
- export interface QueueBackgroundJobConfig<T extends BaseScheduledService | BaseBackgroundedService, PsyTypes extends T['psychicTypes'] = T['psychicTypes'], QueueGroupMap = PsyTypes['queueGroupMap'], Queue extends keyof QueueGroupMap & string = keyof QueueGroupMap & string, Groups extends QueueGroupMap[Queue] = QueueGroupMap[Queue], GroupId = Groups[number & keyof Groups]> extends BaseBackgroundJobConfig {
54
+ export interface QueueBackgroundJobConfig<T extends BaseScheduledService | BaseBackgroundedService, WorkerTypes extends T['psychicWorkerTypes'] = T['psychicWorkerTypes'], QueueGroupMap = WorkerTypes['queueGroupMap'], Queue extends keyof QueueGroupMap & string = keyof QueueGroupMap & string, Groups extends QueueGroupMap[Queue] = QueueGroupMap[Queue], GroupId = Groups[number & keyof Groups]> extends BaseBackgroundJobConfig {
55
55
  groupId?: GroupId;
56
56
  queue?: Queue;
57
57
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic-workers",
4
4
  "description": "Background job system for Psychic applications",
5
- "version": "2.0.2",
5
+ "version": "2.1.0",
6
6
  "author": "RVO Health",
7
7
  "repository": {
8
8
  "type": "git",
@@ -84,7 +84,7 @@
84
84
  "supertest": "^7.1.4",
85
85
  "tslib": "^2.7.0",
86
86
  "tsx": "^4.20.6",
87
- "typedoc": "^0.26.6",
87
+ "typedoc": "^0.28.15",
88
88
  "typescript": "^5.8.2",
89
89
  "typescript-eslint": "=7.18.0",
90
90
  "vitest": "^4.0.10"