@intrig/core 0.0.15-5 → 0.0.15-6

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 (2) hide show
  1. package/main.js +869 -173
  2. package/package.json +2 -1
package/main.js CHANGED
@@ -981,6 +981,8 @@ SourcesCommand = sources_command_ts_decorate([
981
981
  })
982
982
  ], SourcesCommand);
983
983
 
984
+ ;// external "proper-lockfile"
985
+ const external_proper_lockfile_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("proper-lockfile");
984
986
  ;// external "lodash"
985
987
  const external_lodash_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("lodash");
986
988
  var external_lodash_default = /*#__PURE__*/__webpack_require__.n(external_lodash_namespaceObject);
@@ -1288,30 +1290,110 @@ function spec_management_service_ts_metadata(k, v) {
1288
1290
 
1289
1291
 
1290
1292
 
1293
+
1291
1294
  class SpecManagementService {
1292
1295
  constructor(config){
1293
1296
  this.config = config;
1297
+ this.logger = new common_namespaceObject.Logger(SpecManagementService.name);
1294
1298
  this.specsDir = this.config.get('specsDir') ?? external_path_namespaceObject.resolve(external_node_process_namespaceObject.cwd(), '.intrig', 'specs');
1295
1299
  }
1296
1300
  async save(apiName, content) {
1297
- const currentContent = await this.read(apiName);
1298
- if (currentContent) {
1299
- const differences = openapi3_diff(currentContent, JSON.parse(content));
1300
- if (!Object.keys(differences).length) {
1301
- return;
1301
+ const filePath = external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json`);
1302
+ const tempPath = external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json.tmp`);
1303
+ let release;
1304
+ try {
1305
+ // Ensure directory exists
1306
+ external_fs_namespaceObject.mkdirSync(this.specsDir, {
1307
+ recursive: true
1308
+ });
1309
+ // Acquire lock for the file
1310
+ release = await external_proper_lockfile_namespaceObject.lock(filePath, {
1311
+ retries: {
1312
+ retries: 5,
1313
+ factor: 2,
1314
+ minTimeout: 100,
1315
+ maxTimeout: 2000
1316
+ }
1317
+ });
1318
+ // Read current content with lock protection
1319
+ const currentContent = await this.readWithoutLock(apiName);
1320
+ if (currentContent) {
1321
+ const differences = openapi3_diff(currentContent, JSON.parse(content));
1322
+ if (!Object.keys(differences).length) {
1323
+ this.logger.debug(`No changes detected for ${apiName}, skipping save`);
1324
+ return;
1325
+ }
1326
+ }
1327
+ // Atomic write: write to temp file first, then rename
1328
+ external_fs_namespaceObject.writeFileSync(tempPath, content, 'utf-8');
1329
+ external_fs_namespaceObject.renameSync(tempPath, filePath);
1330
+ this.logger.debug(`Successfully saved spec for ${apiName}`);
1331
+ } catch (error) {
1332
+ // Clean up temp file if it exists
1333
+ if (external_fs_namespaceObject.existsSync(tempPath)) {
1334
+ try {
1335
+ external_fs_namespaceObject.unlinkSync(tempPath);
1336
+ } catch (cleanupError) {
1337
+ this.logger.warn(`Failed to cleanup temp file ${tempPath}: ${cleanupError}`);
1338
+ }
1339
+ }
1340
+ this.logger.error(`Failed to save spec for ${apiName}: ${error.message}`, error);
1341
+ throw new Error(`Failed to save spec for ${apiName}: ${error.message}`);
1342
+ } finally{
1343
+ // Always release the lock
1344
+ if (release) {
1345
+ try {
1346
+ await release();
1347
+ } catch (releaseError) {
1348
+ this.logger.warn(`Failed to release lock for ${filePath}: ${releaseError}`);
1349
+ }
1302
1350
  }
1303
1351
  }
1304
- external_fs_namespaceObject.mkdirSync(this.specsDir, {
1305
- recursive: true
1306
- });
1307
- external_fs_namespaceObject.writeFileSync(external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json`), content, 'utf-8');
1308
1352
  }
1309
1353
  async read(apiName) {
1354
+ const filePath = external_path_namespaceObject.join(this.specsDir, `${apiName}-latest.json`);
1355
+ let release;
1356
+ try {
1357
+ // Acquire lock for reading
1358
+ release = await external_proper_lockfile_namespaceObject.lock(filePath, {
1359
+ retries: {
1360
+ retries: 3,
1361
+ factor: 2,
1362
+ minTimeout: 50,
1363
+ maxTimeout: 1000
1364
+ }
1365
+ });
1366
+ return await this.readWithoutLock(apiName);
1367
+ } catch (error) {
1368
+ if (error.code === 'ENOENT' || !external_fs_namespaceObject.existsSync(filePath)) {
1369
+ // File doesn't exist, this is normal for first-time sync
1370
+ this.logger.debug(`Spec file not found for ${apiName}, returning undefined`);
1371
+ return undefined;
1372
+ }
1373
+ this.logger.error(`Failed to read spec for ${apiName}: ${error.message}`, error);
1374
+ throw new Error(`Failed to read spec for ${apiName}: ${error.message}`);
1375
+ } finally{
1376
+ // Always release the lock
1377
+ if (release) {
1378
+ try {
1379
+ await release();
1380
+ } catch (releaseError) {
1381
+ this.logger.warn(`Failed to release lock for ${filePath}: ${releaseError}`);
1382
+ }
1383
+ }
1384
+ }
1385
+ }
1386
+ async readWithoutLock(apiName) {
1310
1387
  const fileName = `${apiName}-latest.json`;
1311
1388
  const specPath = external_path_namespaceObject.join(this.specsDir, fileName);
1312
1389
  if (external_fs_namespaceObject.existsSync(specPath)) {
1313
- const content = external_fs_namespaceObject.readFileSync(specPath, 'utf-8');
1314
- return JSON.parse(content);
1390
+ try {
1391
+ const content = external_fs_namespaceObject.readFileSync(specPath, 'utf-8');
1392
+ return JSON.parse(content);
1393
+ } catch (error) {
1394
+ this.logger.error(`Failed to parse JSON for ${apiName}: ${error.message}`);
1395
+ throw new Error(`Invalid JSON in spec file for ${apiName}: ${error.message}`);
1396
+ }
1315
1397
  }
1316
1398
  return undefined;
1317
1399
  }
@@ -5535,77 +5617,526 @@ if (isPending(${camelCase(descriptor.name)}Resp)) {
5535
5617
  ${"```"}
5536
5618
  </details>
5537
5619
 
5538
- <details>
5539
- <summary>Inline rendering</summary>
5540
- If you need to show a non-blocking/parallel loading indicator, or change the component behavior based on the loading state, you can inline the loading condition.
5541
-
5542
- ${"```tsx"}
5543
- <>{isPending(${camelCase(descriptor.name)}Resp) ? <LoadingIndicator /> : null}<MyComponent /></>
5544
- ${"```"}
5545
- </details>
5620
+ <details>
5621
+ <summary>Inline rendering</summary>
5622
+ If you need to show a non-blocking/parallel loading indicator, or change the component behavior based on the loading state, you can inline the loading condition.
5623
+
5624
+ ${"```tsx"}
5625
+ <>{isPending(${camelCase(descriptor.name)}Resp) ? <LoadingIndicator /> : null}<MyComponent /></>
5626
+ ${"```"}
5627
+ </details>
5628
+
5629
+ #### Error state
5630
+ The error state extraction is done using the \`isError\` utility method. Make sure to import the method into your component.
5631
+
5632
+ ${"```tsx"}
5633
+ import { isError } from '@intrig/next';
5634
+ ${"```"}
5635
+
5636
+ <details>
5637
+ <summary>Early return</summary>
5638
+ In most cases, in an error state, you may want to show the error message to the user instead of real component. In that case, you can add a conditional early return.
5639
+
5640
+ ${"```tsx"}
5641
+ if (isError(${camelCase(descriptor.name)}Resp)) {
5642
+ return <ErrorMessage />
5643
+ }
5644
+ ${"```"}
5645
+ </details>
5646
+
5647
+ <details>
5648
+ <summary>Inline rendering</summary>
5649
+ If you need to show a non-blocking/parallel error indicator, or change the component behavior based on the error state, you can inline the error condition.
5650
+
5651
+ ${"```tsx"}
5652
+ <>{isError(${camelCase(descriptor.name)}Resp) ? <ErrorMessage /> : null}<MyComponent /></>
5653
+ ${"```"}
5654
+ </details>
5655
+
5656
+ <details>
5657
+ <summary>Side effect upon error</summary>
5658
+
5659
+ Usually if the operation is an update, you may need to show an error message to the user. In such cases, you can add a side effect with useEffect, upon error.
5660
+
5661
+ First import \`useEffect\` to your component.
5662
+ ${"```tsx"}
5663
+ import { useEffect } from 'react'
5664
+ ${"```"}
5665
+
5666
+ Then add the side effect.
5667
+ ${"```tsx"}
5668
+ useEffect(() => {
5669
+ if (isError(${camelCase(descriptor.name)}Resp)) {
5670
+ //TODO execute error related operation.
5671
+ }
5672
+ }, [${camelCase(descriptor.name)}Resp])
5673
+ ${"```"}
5674
+ </details>
5675
+
5676
+ ### Replications
5677
+
5678
+ <details>
5679
+ <summary>Multiple instances for same hook.</summary>
5680
+
5681
+ In some case you may need to instanciate multiple instances of the same request. For an example comparision of the products.
5682
+ In that cases, you can use \`key\` property to distinguish between these Network states.
5683
+
5684
+ ${'```tsx'}
5685
+ const [${camelCase(descriptor.name)}Resp, ${camelCase(descriptor.name)}, clear${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}({ key: 'id1' });
5686
+ ${'```'}
5687
+ </details>
5688
+
5689
+
5690
+ `;
5691
+ }
5692
+
5693
+ ;// ../../lib/next-binding/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts
5694
+
5695
+
5696
+ function extractAsyncHookShape(response, requestBody, imports) {
5697
+ if (response) {
5698
+ if (requestBody) {
5699
+ imports.add(`import { BinaryFunctionAsyncHook } from "@intrig/next"`);
5700
+ return `BinaryFunctionAsyncHook<Params, RequestBody, Response, _ErrorType>`;
5701
+ } else {
5702
+ imports.add(`import { UnaryFunctionAsyncHook } from "@intrig/next"`);
5703
+ return `UnaryFunctionAsyncHook<Params, Response, _ErrorType>`;
5704
+ }
5705
+ } else {
5706
+ if (requestBody) {
5707
+ imports.add(`import { BinaryProduceAsyncHook } from "@intrig/next"`);
5708
+ return `BinaryProduceAsyncHook<Params, RequestBody, _ErrorType>`;
5709
+ } else {
5710
+ imports.add(`import { UnaryProduceAsyncHook } from "@intrig/next"`);
5711
+ return `UnaryProduceAsyncHook<Params, _ErrorType>`;
5712
+ }
5713
+ }
5714
+ }
5715
+ function asyncFunctionHook_template_extractErrorParams(errorTypes) {
5716
+ switch(errorTypes.length){
5717
+ case 0:
5718
+ return `
5719
+ export type _ErrorType = any
5720
+ const errorSchema = z.any()`;
5721
+ case 1:
5722
+ return `
5723
+ export type _ErrorType = ${errorTypes[0]}
5724
+ const errorSchema = ${errorTypes[0]}Schema`;
5725
+ default:
5726
+ return `
5727
+ export type _ErrorType = ${errorTypes.join(' | ')}
5728
+ const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
5729
+ }
5730
+ }
5731
+ function asyncFunctionHook_template_extractParamDeconstruction(variables, requestBody) {
5732
+ const isParamMandatory = variables?.some((a)=>a.in === 'path') || false;
5733
+ if (requestBody) {
5734
+ if (isParamMandatory) {
5735
+ return {
5736
+ paramExpression: 'data, p',
5737
+ paramType: 'data: RequestBody, params: Params'
5738
+ };
5739
+ } else {
5740
+ return {
5741
+ paramExpression: 'data, p = {}',
5742
+ paramType: 'data: RequestBody, params?: Params'
5743
+ };
5744
+ }
5745
+ } else {
5746
+ if (isParamMandatory) {
5747
+ return {
5748
+ paramExpression: 'p',
5749
+ paramType: 'params: Params'
5750
+ };
5751
+ } else {
5752
+ return {
5753
+ paramExpression: 'p = {}',
5754
+ paramType: 'params?: Params'
5755
+ };
5756
+ }
5757
+ }
5758
+ }
5759
+ async function nextAsyncFunctionHookTemplate({ source, data: { paths, operationId, response, requestUrl, variables, requestBody, contentType, responseType, errorResponses, method } }, _path, ctx) {
5760
+ const postfix = ctx.potentiallyConflictingDescriptors.includes(operationId) ? generatePostfix(contentType, responseType) : '';
5761
+ const ts = typescript(external_path_namespaceObject.resolve(_path, 'src', source, ...paths, camelCase(operationId), `use${pascalCase(operationId)}Async${postfix}.ts`));
5762
+ const modifiedRequestUrl = `${requestUrl?.replace(/\{/g, "${")}`;
5763
+ const imports = new Set();
5764
+ // Basic imports
5765
+ imports.add(`import { z } from 'zod'`);
5766
+ imports.add(`import { useCallback } from 'react'`);
5767
+ imports.add(`import { useTransientCall, encode, isError, isSuccess } from '@intrig/next'`);
5768
+ // Hook signature type
5769
+ const hookShape = extractAsyncHookShape(response, requestBody, imports);
5770
+ // Add body/response param imports
5771
+ if (requestBody) {
5772
+ imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/next/${source}/components/schemas/${requestBody}"`);
5773
+ }
5774
+ if (response) {
5775
+ imports.add(`import { ${response} as Response, ${response}Schema as schema } from "@intrig/next/${source}/components/schemas/${response}"`);
5776
+ }
5777
+ imports.add(`import { ${pascalCase(operationId)}Params as Params } from './${pascalCase(operationId)}.params'`);
5778
+ // Error types
5779
+ const errorTypes = [
5780
+ ...new Set(Object.values(errorResponses ?? {}).map((a)=>a.response))
5781
+ ];
5782
+ errorTypes.forEach((ref)=>imports.add(`import { ${ref}, ${ref}Schema } from "@intrig/next/${source}/components/schemas/${ref}"`));
5783
+ // Error schema block
5784
+ const errorSchemaBlock = asyncFunctionHook_template_extractErrorParams(errorTypes.map((a)=>a));
5785
+ // Param deconstruction
5786
+ const { paramExpression, paramType } = asyncFunctionHook_template_extractParamDeconstruction(variables ?? [], requestBody);
5787
+ const paramExplode = [
5788
+ ...variables?.filter((a)=>a.in === 'path').map((a)=>a.name) ?? [],
5789
+ '...params'
5790
+ ].join(',');
5791
+ const finalRequestBodyBlock = requestBody ? `, data: encode(data, "${contentType}", requestBodySchema)` : '';
5792
+ function responseTypePart() {
5793
+ switch(responseType){
5794
+ case "application/octet-stream":
5795
+ return `responseType: 'blob', adapter: 'fetch',`;
5796
+ case "text/event-stream":
5797
+ return `responseType: 'stream', adapter: 'fetch',`;
5798
+ }
5799
+ return '';
5800
+ }
5801
+ return ts`
5802
+ ${[
5803
+ ...imports
5804
+ ].join('\n')}
5805
+
5806
+ ${!response ? `
5807
+ type Response = any;
5808
+ const schema = z.any();
5809
+ ` : ''}
5810
+
5811
+ ${errorSchemaBlock}
5812
+
5813
+ const operation = "${method.toUpperCase()} ${requestUrl}| ${contentType} -> ${responseType}";
5814
+ const source = "${source}";
5815
+
5816
+ function use${pascalCase(operationId)}AsyncHook(): [(${paramType}) => Promise<Response>, () => void] {
5817
+ const [call, abort] = useTransientCall<Response, _ErrorType>({
5818
+ schema,
5819
+ errorSchema
5820
+ });
5821
+
5822
+ const doExecute = useCallback<(${paramType}) => Promise<Response>>(async (${paramExpression}) => {
5823
+ let { ${paramExplode} } = p;
5824
+
5825
+ ${requestBody ? `
5826
+ const validationResult = requestBodySchema.safeParse(data);
5827
+ if (!validationResult.success) {
5828
+ return Promise.reject(validationResult.error);
5829
+ }
5830
+ ` : ''}
5831
+
5832
+ return await call({
5833
+ method: '${method}',
5834
+ url: \`${modifiedRequestUrl}\`,
5835
+ headers: {
5836
+ ${contentType ? `"Content-Type": "${contentType}",` : ''}
5837
+ },
5838
+ params,
5839
+ key: \`${"${source}: ${operation}"}\`,
5840
+ source: '${source}'
5841
+ ${requestBody ? finalRequestBodyBlock : ''},
5842
+ ${responseTypePart()}
5843
+ });
5844
+ }, [call]);
5845
+
5846
+ return [doExecute, abort];
5847
+ }
5848
+
5849
+ use${pascalCase(operationId)}AsyncHook.key = \`${"${source}: ${operation}"}\`;
5850
+
5851
+ export const use${pascalCase(operationId)}Async: ${hookShape} = use${pascalCase(operationId)}AsyncHook;
5852
+ `;
5853
+ }
5854
+
5855
+ ;// ../../lib/next-binding/src/lib/templates/type-utils.template.ts
5856
+
5857
+
5858
+ function typeUtilsTemplate(_path) {
5859
+ const ts = typescript(external_path_default().resolve(_path, 'src', 'type-utils.ts'));
5860
+ return ts`import { z } from 'zod';
5861
+
5862
+ export type BinaryData = Blob;
5863
+ export const BinaryDataSchema: z.ZodType<BinaryData> = z.instanceof(Blob);
5864
+
5865
+ // Base64 helpers (browser + Node compatible; no Buffer required)
5866
+ export function base64ToUint8Array(b64: string): Uint8Array {
5867
+ if (typeof atob === 'function') {
5868
+ // Browser
5869
+ const bin = atob(b64);
5870
+ const bytes = new Uint8Array(bin.length);
5871
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
5872
+ return bytes;
5873
+ } else {
5874
+ // Node
5875
+ const buf = Buffer.from(b64, 'base64');
5876
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
5877
+ }
5878
+ }
5879
+
5880
+ `;
5881
+ }
5882
+
5883
+ ;// ../../lib/next-binding/src/lib/templates/docs/sse-hook.ts
5884
+
5885
+ function nextSseHookDocs(descriptor) {
5886
+ const md = mdLiteral('sse-hook.md');
5887
+ const requestBody = descriptor.data.requestBody ? camelCase(descriptor.data.requestBody) : undefined;
5888
+ const params = descriptor.data.variables?.filter((a)=>a.in.toUpperCase() === 'PATH')?.length ? 'params' : undefined;
5889
+ return md`
5890
+ > Intrig-generated hooks are intended for backend data integration. They follow the same tuple-based API pattern as React's built-in state hooks (e.g. useState).
5891
+
5892
+ ## Imports
5893
+
5894
+ #### Import the hook to the component.
5895
+ ${"```ts"}
5896
+ import { use${pascalCase(descriptor.name)} } from '@intrig/next/${descriptor.path}/client';
5897
+ ${"```"}
5898
+
5899
+ #### Import the utility methods.
5900
+ ${"```ts"}
5901
+ import { isSuccess } from '@intrig/next';
5902
+ ${"```"}
5903
+
5904
+ #### Use hook inside the component.
5905
+
5906
+ ${"```tsx"}
5907
+ const [${camelCase(descriptor.name)}Resp, ${camelCase(descriptor.name)}, clear${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}();
5908
+ ${"```"}
5909
+
5910
+ ### Usage
5911
+
5912
+ #### Execute data fetching.
5913
+
5914
+ ${"```tsx"}
5915
+ useEffect(() => {
5916
+ ${camelCase(descriptor.name)}(${[
5917
+ requestBody,
5918
+ params ?? '{}'
5919
+ ].filter(Boolean).join(', ')});
5920
+ }, [${[
5921
+ requestBody,
5922
+ params
5923
+ ].filter(Boolean).join(', ')}/* Dependencies */])
5924
+ ${"```"}
5925
+
5926
+ #### Extract data from the response.
5927
+ \`SSE\` hooks are a special kind of hook that delivers intermediate data updates as they are received.
5928
+
5929
+ ##### Minimal example.
5930
+ ${"```tsx"}
5931
+ const ${camelCase(descriptor.name)} = isPending(${camelCase(descriptor.name)}Resp) ? ${camelCase(descriptor.name)}Resp.data : null;
5932
+ ${"```"}
5933
+
5934
+ ### Full example.
5935
+
5936
+ #### Short format.
5937
+ You can pass the request body and params as props to the hook for initial data binding. This will create the data to be tightly coupled
5938
+ with the component lifecycle.
5939
+
5940
+ > Usually the SSE-hooks are combined with a collector mechanism to get the complete message.
5941
+
5942
+ <details>
5943
+ <summary>Implementation</summary>
5944
+
5945
+ ${"```tsx"}
5946
+ import { use${pascalCase(descriptor.name)} } from '@intrig/next/${descriptor.path}/client';
5947
+ import { isSuccess, isPending, isError } from '@intrig/next';
5948
+ ${requestBody ? `import { ${pascalCase(requestBody)} } from '@intrig/next/${descriptor.source}/components/schemas/${pascalCase(requestBody)}';` : ''}
5949
+ ${params ? `import { ${pascalCase(params)}Params } from '@intrig/next/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
5950
+ import { useState, useEffect } from 'react';
5951
+ import { LoadingIndicator, ErrorDisplay } from '@/components/ui'; //import from your project.
5952
+ import {flushSync} from "react-dom";
5953
+
5954
+ ${requestBody || params ? `
5955
+ interface MyComponentProps {
5956
+ ${requestBody ? `${camelCase(requestBody)}: ${pascalCase(requestBody)};` : ''}
5957
+ ${params ? `${camelCase(params)}Params: ${pascalCase(params)}Params;` : ''}
5958
+ }
5959
+ ` : ''}
5960
+
5961
+ function MyComponent(${requestBody || params ? 'props: MyComponentProps' : ''}) {
5962
+ const [${camelCase(descriptor.name)}Resp] = use${pascalCase(descriptor.name)}({
5963
+ fetchOnMount: true,
5964
+ clearOnUnmount: true,
5965
+ ${requestBody ? `body: props.${camelCase(requestBody)},` : ''}
5966
+ ${params ? `params: props.${camelCase(params)},` : 'params: {}'}
5967
+ });
5968
+
5969
+ let [data, setData] = useState<${pascalCase(descriptor.name)}[]>([]);
5970
+
5971
+ useEffect(() => {
5972
+ if (isPending(${camelCase(descriptor.name)}Resp)) {
5973
+ flushSync(() => { //Sometimes react tends to skip intermediate renders for the performance. Use flushSync if you need to keep track of messages.
5974
+ setData(data => [...data, ${camelCase(descriptor.name)}Resp.data]);
5975
+ })
5976
+ }
5977
+ }, [${camelCase(descriptor.name)}Resp])
5978
+
5979
+ if (isPending(${camelCase(descriptor.name)}Resp)) {
5980
+ return <>{data.map(a => <>{JSON.stringify(a)}</>)}</>
5981
+ }
5982
+
5983
+ if (isError(${camelCase(descriptor.name)}Resp)) {
5984
+ return <ErrorDisplay error={${camelCase(descriptor.name)}Resp.error}/> //TODO add your error view here.
5985
+ }
5986
+
5987
+ return <>
5988
+ Completed: {JSON.stringify(data)}
5989
+ </>
5990
+ }
5991
+ ${"```"}
5992
+ </details>
5993
+
5994
+ #### Controlled format.
5995
+ If you need more control over the data binding, where the data control is outside of the component lifecycle,
5996
+ you can use the actions provided by the hook.
5997
+
5998
+ <details>
5999
+ <summary>Implementation</summary>
6000
+
6001
+ ${"```tsx"}
6002
+ import { use${pascalCase(descriptor.name)} } from '@intrig/next/${descriptor.path}/client';
6003
+ import { isSuccess } from '@intrig/next';
6004
+ ${requestBody ? `import { ${pascalCase(requestBody)} } from '@intrig/next/${descriptor.source}/components/schemas/${pascalCase(requestBody)}';` : ''}
6005
+ ${params ? `import { ${pascalCase(params)}Params } from '@intrig/next/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
6006
+ import {flushSync} from "react-dom";
6007
+
6008
+ ${requestBody || params ? `
6009
+ interface MyComponentProps {
6010
+ ${requestBody ? `${camelCase(requestBody)}: ${pascalCase(requestBody)};` : ''}
6011
+ ${params ? `${camelCase(params)}Params: ${pascalCase(params)}Params;` : ''}
6012
+ }
6013
+ ` : ''}
6014
+
6015
+ function MyComponent() {
6016
+ const [${camelCase(descriptor.name)}Resp, ${camelCase(descriptor.name)}, clear${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}();
6017
+
6018
+ useEffect(() => {
6019
+ ${camelCase(descriptor.name)}(${requestBody ? `${camelCase(requestBody)},` : ''} ${params ? `${camelCase(params)},` : '{}'}) //Call the fetch function.
6020
+ return clear${pascalCase(descriptor.name)}; //Clear the data on unmount.
6021
+ }, [])
6022
+
6023
+ let [data, setData] = useState<${pascalCase(descriptor.name)}[]>([]);
6024
+
6025
+ useEffect(() => {
6026
+ if (isPending(${camelCase(descriptor.name)}Resp)) {
6027
+ flushSync(() => { //Sometimes react tends to skip intermediate renders for the performance. Use flushSync if you need to keep track of messages.
6028
+ setData(data => [...data, ${camelCase(descriptor.name)}Resp.data]);
6029
+ })
6030
+ }
6031
+ }, [${camelCase(descriptor.name)}Resp])
6032
+
6033
+ if (isPending(${camelCase(descriptor.name)}Resp)) {
6034
+ return <>{data.map(a => <>{JSON.stringify(a)}</>)}</>
6035
+ }
6036
+
6037
+ if (isError(${camelCase(descriptor.name)}Resp)) {
6038
+ return <ErrorDisplay error={${camelCase(descriptor.name)}Resp.error}/> //TODO add your error view here.
6039
+ }
6040
+
6041
+ return <>
6042
+ Completed: {JSON.stringify(data)}
6043
+ </>
6044
+ }
6045
+ ${"```"}
6046
+ </details>
6047
+
6048
+ `;
6049
+ }
5546
6050
 
5547
- #### Error state
5548
- The error state extraction is done using the \`isError\` utility method. Make sure to import the method into your component.
6051
+ ;// ../../lib/next-binding/src/lib/templates/docs/async-hook.ts
5549
6052
 
5550
- ${"```tsx"}
5551
- import { isError } from '@intrig/next';
5552
- ${"```"}
6053
+ function nextAsyncFunctionHookDocs(descriptor) {
6054
+ const md = mdLiteral('async-hook.md');
6055
+ const requestBody = descriptor.data.requestBody ? camelCase(descriptor.data.requestBody) : undefined;
6056
+ const params = descriptor.data.variables?.filter((a)=>a.in.toUpperCase() === 'PATH')?.length ? 'params' : undefined;
6057
+ return md`
6058
+
6059
+ > Intrig generated async hooks are intended to use for the rapid usecases like validations. This effectively bypasses the network-state management.
6060
+ > The async hooks follow the tuple-based API pattern as React's built-in state hooks (e.g. useState).
5553
6061
 
5554
- <details>
5555
- <summary>Early return</summary>
5556
- In most cases, in an error state, you may want to show the error message to the user instead of real component. In that case, you can add a conditional early return.
6062
+ ## Imports
5557
6063
 
5558
- ${"```tsx"}
5559
- if (isError(${camelCase(descriptor.name)}Resp)) {
5560
- return <ErrorMessage />
5561
- }
6064
+ #### Import the hook to the component.
6065
+ ${"```ts"}
6066
+ import { use${pascalCase(descriptor.name)}Async } from '@intrig/next/${descriptor.path}/client';
5562
6067
  ${"```"}
5563
- </details>
5564
6068
 
5565
- <details>
5566
- <summary>Inline rendering</summary>
5567
- If you need to show a non-blocking/parallel error indicator, or change the component behavior based on the error state, you can inline the error condition.
6069
+ #### Use hook inside the component.
5568
6070
 
5569
6071
  ${"```tsx"}
5570
- <>{isError(${camelCase(descriptor.name)}Resp) ? <ErrorMessage /> : null}<MyComponent /></>
6072
+ const [${camelCase(descriptor.name)}, abort${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}Async();
5571
6073
  ${"```"}
5572
- </details>
5573
6074
 
5574
- <details>
5575
- <summary>Side effect upon error</summary>
5576
-
5577
- Usually if the operation is an update, you may need to show an error message to the user. In such cases, you can add a side effect with useEffect, upon error.
6075
+ #### Execute data fetching / calling.
5578
6076
 
5579
- First import \`useEffect\` to your component.
5580
6077
  ${"```tsx"}
5581
- import { useEffect } from 'react'
5582
- ${"```"}
6078
+ const fn = useCallback(async () => {
6079
+ let ${camelCase(descriptor.name)}Data = await ${camelCase(descriptor.name)}(${[
6080
+ requestBody,
6081
+ params ?? '{}'
6082
+ ].filter(Boolean).join(', ')});
6083
+ //TODO do something with the ${camelCase(descriptor.name)}Data.
6084
+ return ${camelCase(descriptor.name)}Data;
6085
+ }, [${[
6086
+ requestBody,
6087
+ params
6088
+ ].filter(Boolean).join(', ')}/* Dependencies */])
5583
6089
 
5584
- Then add the side effect.
5585
- ${"```tsx"}
5586
- useEffect(() => {
5587
- if (isError(${camelCase(descriptor.name)}Resp)) {
5588
- //TODO execute error related operation.
5589
- }
5590
- }, [${camelCase(descriptor.name)}Resp])
5591
6090
  ${"```"}
5592
- </details>
5593
6091
 
5594
- ### Replications
6092
+ ## Full example
5595
6093
 
5596
6094
  <details>
5597
- <summary>Multiple instances for same hook.</summary>
6095
+ <summary>Implementation</summary>
5598
6096
 
5599
- In some case you may need to instanciate multiple instances of the same request. For an example comparision of the products.
5600
- In that cases, you can use \`key\` property to distinguish between these Network states.
6097
+ ${"```tsx"}
6098
+ import { use${pascalCase(descriptor.name)}Async } from '@intrig/next/${descriptor.path}/client';
6099
+ ${requestBody ? `import { ${pascalCase(requestBody)} } from '@intrig/next/${descriptor.source}/components/schemas/${pascalCase(requestBody)}';` : ''}
6100
+ ${params ? `import { ${pascalCase(params)}Params } from '@intrig/next/${descriptor.path}/${pascalCase(descriptor.name)}.params';` : ''}
6101
+ import { useCallback } from 'react';
5601
6102
 
5602
- ${'```tsx'}
5603
- const [${camelCase(descriptor.name)}Resp, ${camelCase(descriptor.name)}, clear${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}({ key: 'id1' });
5604
- ${'```'}
5605
- </details>
6103
+ ${requestBody || params ? `
6104
+ interface MyComponentProps {
6105
+ ${requestBody ? `${camelCase(requestBody)}: ${pascalCase(requestBody)};` : ''}
6106
+ ${params ? `${camelCase(params)}Params: ${pascalCase(params)}Params;` : ''}
6107
+ }
6108
+ ` : ''}
5606
6109
 
6110
+ function MyComponent(${requestBody || params ? 'props: MyComponentProps' : ''}) {
6111
+ const [${camelCase(descriptor.name)}, abort${pascalCase(descriptor.name)}] = use${pascalCase(descriptor.name)}Async();
5607
6112
 
5608
- `;
6113
+ const fn = useCallback(async (${[
6114
+ requestBody,
6115
+ params
6116
+ ].filter(Boolean).join(', ')}) => {
6117
+ return await ${camelCase(descriptor.name)}(${[
6118
+ requestBody,
6119
+ params
6120
+ ].filter(Boolean).join(', ')});
6121
+ }, [/* Dependencies */])
6122
+
6123
+ //use fn where remote callback is needed.
6124
+ return <>
6125
+ <button onClick={() => fn(${[
6126
+ requestBody,
6127
+ params
6128
+ ].filter(Boolean).map((a)=>`props.${a}`).join(', ')})}>Call remote</button>
6129
+ </>
6130
+ }
6131
+
6132
+ ${"```"}
6133
+ </details>
6134
+
6135
+ <hint>
6136
+ //TODO improve with usecases
6137
+ </hint>
6138
+
6139
+ `;
5609
6140
  }
5610
6141
 
5611
6142
  ;// ../../lib/next-binding/src/lib/next-binding.service.ts
@@ -5641,6 +6172,10 @@ function next_binding_service_ts_metadata(k, v) {
5641
6172
 
5642
6173
 
5643
6174
 
6175
+
6176
+
6177
+
6178
+
5644
6179
 
5645
6180
 
5646
6181
 
@@ -5650,11 +6185,12 @@ const nonDownloadMimePatterns = external_picomatch_default()([
5650
6185
  "application/json",
5651
6186
  "application/xml",
5652
6187
  "application/x-www-form-urlencoded",
6188
+ "application/event-stream",
5653
6189
  "text/*"
5654
6190
  ]);
5655
6191
  class IntrigNextBindingService extends GeneratorBinding {
5656
6192
  constructor(sourceManagementService, config){
5657
- super(), this.sourceManagementService = sourceManagementService, this.config = config, this.dump = this.sourceManagementService.dump, this._path = this.config.get("generatedDir") ?? external_path_default().resolve(external_node_process_namespaceObject.cwd(), '.intrig', 'generated');
6193
+ super(), this.sourceManagementService = sourceManagementService, this.config = config, this.logger = new common_namespaceObject.Logger(IntrigNextBindingService.name), this.dump = this.sourceManagementService.dump, this._path = this.config.get("generatedDir") ?? external_path_default().resolve(external_node_process_namespaceObject.cwd(), '.intrig', 'generated');
5658
6194
  }
5659
6195
  getRestOptions() {
5660
6196
  return {
@@ -5686,12 +6222,19 @@ class IntrigNextBindingService extends GeneratorBinding {
5686
6222
  await this.dump(nextExtraTemplate(this._path));
5687
6223
  await this.dump(nextLoggerTemplate(this._path));
5688
6224
  await this.dump(nextSwcrcTemplate(this._path));
6225
+ await this.dump(typeUtilsTemplate(this._path));
5689
6226
  }
5690
6227
  async generateSource(descriptors, source) {
6228
+ //TODO improve this logic to catch potential conflicts.
6229
+ const potentiallyConflictingDescriptors = descriptors.filter(isRestDescriptor).sort((a, b)=>(a.data.contentType === "application/json" ? -1 : 0) - (b.data.contentType === "application/json" ? -1 : 0)).filter((descriptor, index, array)=>array.findIndex((other)=>other.data.operationId === descriptor.data.operationId) !== index).map((descriptor)=>descriptor.id);
6230
+ const ctx = {
6231
+ potentiallyConflictingDescriptors
6232
+ };
5691
6233
  const groupedByPath = {};
5692
6234
  for (const descriptor of descriptors){
6235
+ this.logger.log(`Generating source: ${descriptor.name}`);
5693
6236
  if (isRestDescriptor(descriptor)) {
5694
- await this.generateRestSource(source, descriptor);
6237
+ await this.generateRestSource(source, descriptor, ctx);
5695
6238
  groupedByPath[descriptor.data.requestUrl] = groupedByPath[descriptor.data.requestUrl] || [];
5696
6239
  groupedByPath[descriptor.data.requestUrl].push(descriptor);
5697
6240
  } else if (isSchemaDescriptor(descriptor)) {
@@ -5702,14 +6245,14 @@ class IntrigNextBindingService extends GeneratorBinding {
5702
6245
  await this.dump(nextRequestRouteTemplate(requestUrl, matchingPaths, this._path));
5703
6246
  }
5704
6247
  }
5705
- async generateRestSource(source, descriptor) {
6248
+ async generateRestSource(source, descriptor, ctx) {
5706
6249
  const clientExports = [];
5707
6250
  const serverExports = [];
5708
6251
  await this.dump(nextParamsTemplate(descriptor, clientExports, serverExports, this._path));
5709
6252
  await this.dump(nextRequestHookTemplate(descriptor, clientExports, serverExports, this._path));
5710
6253
  await this.dump(nextRequestMethodTemplate(descriptor, clientExports, serverExports, this._path));
5711
- //TODO incorporate rest options.
5712
- if (descriptor.data.method.toUpperCase() === 'GET' && !nonDownloadMimePatterns(descriptor.data.responseType)) {
6254
+ await this.dump(nextAsyncFunctionHookTemplate(descriptor, this._path, ctx));
6255
+ if (descriptor.data.method.toUpperCase() === 'GET' && (!nonDownloadMimePatterns(descriptor.data.responseType) || descriptor.data.responseType !== '*/*') || descriptor.data.responseHeaders?.['content-disposition']) {
5713
6256
  await this.dump(nextDownloadHookTemplate(descriptor, clientExports, serverExports, this._path));
5714
6257
  }
5715
6258
  await this.dump(nextClientIndexTemplate([
@@ -5796,16 +6339,20 @@ ${"```"}
5796
6339
  ]));
5797
6340
  const tabs = [];
5798
6341
  if (result.data.responseType === 'text/event-stream') {
5799
- // tabs.push({
5800
- // name: 'SSE Hook',
5801
- // content: (await sseHookDocs(result)).content
5802
- // })
6342
+ tabs.push({
6343
+ name: 'SSE Hook',
6344
+ content: (await nextSseHookDocs(result)).content
6345
+ });
5803
6346
  } else {
5804
6347
  tabs.push({
5805
6348
  name: 'React Hook',
5806
6349
  content: (await nextReactHookDocs(result)).content
5807
6350
  });
5808
6351
  }
6352
+ tabs.push({
6353
+ name: 'Async Hook',
6354
+ content: (await nextAsyncFunctionHookDocs(result)).content
6355
+ });
5809
6356
  return RestDocumentation.from({
5810
6357
  id: result.id,
5811
6358
  name: result.name,
@@ -7114,6 +7661,7 @@ function reactPackageJsonTemplate(_path) {
7114
7661
  "@swc/cli": "^0.7.7",
7115
7662
  "@swc/core": "^1.12.6",
7116
7663
  "@types/node": "^24.0.4",
7664
+ "typescript": "${packageJson.devDependencies.typescript ?? packageJson.dependencies.typescript}",
7117
7665
  "react": "${packageJson.dependencies.react}",
7118
7666
  "react-dom": "${packageJson.dependencies['react-dom']}"
7119
7667
  },
@@ -7913,6 +8461,15 @@ async function reactRequestHookTemplate({ source, data: { paths, operationId, re
7913
8461
  "...params"
7914
8462
  ].join(",");
7915
8463
  const finalRequestBodyBlock = requestBody ? `,data: encode(data, "${contentType}", requestBodySchema)` : '';
8464
+ function responseTypePart() {
8465
+ switch(responseType){
8466
+ case "application/octet-stream":
8467
+ return `responseType: 'blob', adapter: 'fetch',`;
8468
+ case "text/event-stream":
8469
+ return `responseType: 'stream', adapter: 'fetch',`;
8470
+ }
8471
+ return '';
8472
+ }
7916
8473
  return ts`
7917
8474
  ${[
7918
8475
  ...imports
@@ -7957,7 +8514,7 @@ async function reactRequestHookTemplate({ source, data: { paths, operationId, re
7957
8514
  key: \`${"${source}: ${operation}"}\`,
7958
8515
  source: '${source}'
7959
8516
  ${requestBody ? finalRequestBodyBlock : ''},
7960
- ${responseType === "text/event-stream" ? `responseType: 'stream', adapter: 'fetch',` : ''}
8517
+ ${responseTypePart()}
7961
8518
  })
7962
8519
  return successfulDispatch();
7963
8520
  }, [dispatch])
@@ -9077,7 +9634,7 @@ ${"```"}
9077
9634
  ;// ../../lib/react-binding/src/lib/templates/source/controller/method/asyncFunctionHook.template.ts
9078
9635
 
9079
9636
 
9080
- function extractAsyncHookShape(response, requestBody, imports) {
9637
+ function asyncFunctionHook_template_extractAsyncHookShape(response, requestBody, imports) {
9081
9638
  if (response) {
9082
9639
  if (requestBody) {
9083
9640
  imports.add(`import { BinaryFunctionAsyncHook } from "@intrig/react"`);
@@ -9096,7 +9653,7 @@ function extractAsyncHookShape(response, requestBody, imports) {
9096
9653
  }
9097
9654
  }
9098
9655
  }
9099
- function asyncFunctionHook_template_extractErrorParams(errorTypes) {
9656
+ function method_asyncFunctionHook_template_extractErrorParams(errorTypes) {
9100
9657
  switch(errorTypes.length){
9101
9658
  case 0:
9102
9659
  return `
@@ -9112,7 +9669,7 @@ function asyncFunctionHook_template_extractErrorParams(errorTypes) {
9112
9669
  const errorSchema = z.union([${errorTypes.map((a)=>`${a}Schema`).join(', ')}])`;
9113
9670
  }
9114
9671
  }
9115
- function asyncFunctionHook_template_extractParamDeconstruction(variables, requestBody) {
9672
+ function method_asyncFunctionHook_template_extractParamDeconstruction(variables, requestBody) {
9116
9673
  const isParamMandatory = variables?.some((a)=>a.in === 'path') || false;
9117
9674
  if (requestBody) {
9118
9675
  if (isParamMandatory) {
@@ -9150,7 +9707,7 @@ async function reactAsyncFunctionHookTemplate({ source, data: { paths, operation
9150
9707
  imports.add(`import { useCallback } from 'react'`);
9151
9708
  imports.add(`import { useTransientCall, encode, isError, isSuccess } from '@intrig/react'`);
9152
9709
  // Hook signature type
9153
- const hookShape = extractAsyncHookShape(response, requestBody, imports);
9710
+ const hookShape = asyncFunctionHook_template_extractAsyncHookShape(response, requestBody, imports);
9154
9711
  // Add body/response param imports
9155
9712
  if (requestBody) {
9156
9713
  imports.add(`import { ${requestBody} as RequestBody, ${requestBody}Schema as requestBodySchema } from "@intrig/react/${source}/components/schemas/${requestBody}"`);
@@ -9165,14 +9722,23 @@ async function reactAsyncFunctionHookTemplate({ source, data: { paths, operation
9165
9722
  ];
9166
9723
  errorTypes.forEach((ref)=>imports.add(`import { ${ref}, ${ref}Schema } from "@intrig/react/${source}/components/schemas/${ref}"`));
9167
9724
  // Error schema block
9168
- const errorSchemaBlock = asyncFunctionHook_template_extractErrorParams(errorTypes.map((a)=>a));
9725
+ const errorSchemaBlock = method_asyncFunctionHook_template_extractErrorParams(errorTypes.map((a)=>a));
9169
9726
  // Param deconstruction
9170
- const { paramExpression, paramType } = asyncFunctionHook_template_extractParamDeconstruction(variables ?? [], requestBody);
9727
+ const { paramExpression, paramType } = method_asyncFunctionHook_template_extractParamDeconstruction(variables ?? [], requestBody);
9171
9728
  const paramExplode = [
9172
9729
  ...variables?.filter((a)=>a.in === 'path').map((a)=>a.name) ?? [],
9173
9730
  '...params'
9174
9731
  ].join(',');
9175
9732
  const finalRequestBodyBlock = requestBody ? `, data: encode(data, "${contentType}", requestBodySchema)` : '';
9733
+ function responseTypePart() {
9734
+ switch(responseType){
9735
+ case "application/octet-stream":
9736
+ return `responseType: 'blob', adapter: 'fetch',`;
9737
+ case "text/event-stream":
9738
+ return `responseType: 'stream', adapter: 'fetch',`;
9739
+ }
9740
+ return '';
9741
+ }
9176
9742
  return ts`
9177
9743
  ${[
9178
9744
  ...imports
@@ -9214,7 +9780,7 @@ function use${pascalCase(operationId)}AsyncHook(): [(${paramType}) => Promise<Re
9214
9780
  key: \`${"${source}: ${operation}"}\`,
9215
9781
  source: '${source}'
9216
9782
  ${requestBody ? finalRequestBodyBlock : ''},
9217
- ${responseType === "text/event-stream" ? `responseType: 'stream', adapter: 'fetch',` : ''}
9783
+ ${responseTypePart()}
9218
9784
  });
9219
9785
  }, [call]);
9220
9786
 
@@ -9321,7 +9887,7 @@ ${"```"}
9321
9887
  ;// ../../lib/react-binding/src/lib/templates/type-utils.template.ts
9322
9888
 
9323
9889
 
9324
- function typeUtilsTemplate(_path) {
9890
+ function type_utils_template_typeUtilsTemplate(_path) {
9325
9891
  const ts = typescript(external_path_default().resolve(_path, 'src', 'type-utils.ts'));
9326
9892
  return ts`import { z } from 'zod';
9327
9893
 
@@ -9406,7 +9972,7 @@ class ReactBindingService extends GeneratorBinding {
9406
9972
  await this.dump(reactPackageJsonTemplate(this._path));
9407
9973
  await this.dump(reactProviderTemplate(this._path, apisToSync));
9408
9974
  await this.dump(reactTsConfigTemplate(this._path));
9409
- await this.dump(typeUtilsTemplate(this._path));
9975
+ await this.dump(type_utils_template_typeUtilsTemplate(this._path));
9410
9976
  }
9411
9977
  async generateSource(descriptors, source) {
9412
9978
  //TODO improve this logic to catch potential conflicts.
@@ -10736,10 +11302,13 @@ function deref(spec) {
10736
11302
  };
10737
11303
  }
10738
11304
 
11305
+ ;// external "node:crypto"
11306
+ const external_node_crypto_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:crypto");
10739
11307
  ;// ../../lib/openapi-source/src/lib/util/normalize.ts
10740
11308
 
10741
11309
 
10742
11310
 
11311
+
10743
11312
  function generateTypeName(operationOb, postFix) {
10744
11313
  return [
10745
11314
  operationOb.tags?.[0],
@@ -10749,7 +11318,7 @@ function generateTypeName(operationOb, postFix) {
10749
11318
  }
10750
11319
  function normalize(spec) {
10751
11320
  const doDeref = deref(spec);
10752
- return (0,external_immer_namespaceObject.produce)(spec, (draft)=>{
11321
+ const normalized = (0,external_immer_namespaceObject.produce)(spec, (draft)=>{
10753
11322
  const paths = draft.paths;
10754
11323
  for (const [path, pathItem] of Object.entries(paths)){
10755
11324
  const pathItemObject = pathItem;
@@ -10854,6 +11423,9 @@ function normalize(spec) {
10854
11423
  }
10855
11424
  //TODO implement fix schema types.
10856
11425
  });
11426
+ return (0,external_immer_namespaceObject.produce)(normalized, (draft)=>{
11427
+ draft.info['x-intrig-hash'] = (0,external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify(draft)).digest('hex');
11428
+ });
10857
11429
  }
10858
11430
 
10859
11431
  ;// ../../lib/openapi-source/src/lib/util/extractRequestsFromSpec.ts
@@ -10997,26 +11569,47 @@ class IntrigOpenapiService {
10997
11569
  }
10998
11570
  async sync(config, id, ctx) {
10999
11571
  this.logger.log('Starting OpenAPI sync process');
11572
+ const errors = [];
11000
11573
  if (!id) {
11574
+ // Process all sources with better error isolation
11001
11575
  for (const source of config.sources){
11002
11576
  this.logger.log(`Processing source: ${source.id}`);
11003
11577
  try {
11004
11578
  await this.doSync(source, config, ctx);
11579
+ this.logger.log(`Successfully synced source: ${source.id}`);
11005
11580
  } catch (e) {
11006
- this.logger.error(`Failed to sync source ${source.id}: ${e.message}`, e);
11581
+ const errorMsg = `Failed to sync source ${source.id}: ${e.message}`;
11582
+ this.logger.error(errorMsg, e);
11583
+ errors.push({
11584
+ sourceId: source.id,
11585
+ error: errorMsg
11586
+ });
11587
+ // Continue with other sources instead of failing completely
11007
11588
  }
11008
11589
  }
11009
11590
  } else {
11010
11591
  const source = config.sources.find((s)=>s.id === id);
11011
11592
  if (!source) {
11012
- this.logger.error(`Source ${id} not found`);
11013
- throw new Error(`Source ${id} not found`);
11593
+ const error = `Source ${id} not found`;
11594
+ this.logger.error(error);
11595
+ throw new Error(error);
11014
11596
  }
11015
11597
  this.logger.log(`Processing specific source: ${id}`);
11016
11598
  try {
11017
11599
  await this.doSync(source, config, ctx);
11600
+ this.logger.log(`Successfully synced source: ${id}`);
11018
11601
  } catch (e) {
11019
- this.logger.error(`Failed to sync source ${id}: ${e.message}`, e);
11602
+ const errorMsg = `Failed to sync source ${id}: ${e.message}`;
11603
+ this.logger.error(errorMsg, e);
11604
+ throw new Error(errorMsg);
11605
+ }
11606
+ }
11607
+ if (errors.length > 0) {
11608
+ const summary = `Sync completed with ${errors.length} errors: ${errors.map((e)=>e.sourceId).join(', ')}`;
11609
+ this.logger.warn(summary);
11610
+ // If all sources failed, throw an error
11611
+ if (errors.length === config.sources.length) {
11612
+ throw new Error(`All sources failed to sync: ${errors.map((e)=>e.error).join('; ')}`);
11020
11613
  }
11021
11614
  }
11022
11615
  this.logger.log('OpenAPI sync process completed');
@@ -11062,7 +11655,8 @@ class IntrigOpenapiService {
11062
11655
  const restData = extractRequestsFromSpec(document);
11063
11656
  const schemas = extractSchemas(document);
11064
11657
  const sha1 = (str)=>external_crypto_namespaceObject.createHash('sha1').update(str).digest('hex');
11065
- return [
11658
+ const hash = document.info['x-intrig-hash'];
11659
+ const descriptors = [
11066
11660
  ...restData.map((restData)=>ResourceDescriptor.from({
11067
11661
  id: sha1(`${id}_${restData.method}_${restData.paths.join('_')}_${restData.operationId}_${restData.contentType}_${restData.responseType}`),
11068
11662
  name: camelCase(restData.operationId),
@@ -11080,6 +11674,17 @@ class IntrigOpenapiService {
11080
11674
  data: schema
11081
11675
  }))
11082
11676
  ];
11677
+ return {
11678
+ descriptors,
11679
+ hash
11680
+ };
11681
+ }
11682
+ async getHash(id) {
11683
+ const document = await this.specManagementService.read(id);
11684
+ if (!document) {
11685
+ throw new Error(`Spec ${id} not found`);
11686
+ }
11687
+ return document.info['x-intrig-hash'];
11083
11688
  }
11084
11689
  validate(normalized, restOptions) {
11085
11690
  function validateForConflictingVariables() {
@@ -11670,7 +12275,7 @@ class SearchService {
11670
12275
  async onModuleInit() {
11671
12276
  try {
11672
12277
  for (const source of this.configService.get().sources){
11673
- const descriptors = await this.openApiService.getResourceDescriptors(source.id);
12278
+ const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
11674
12279
  descriptors.forEach((descriptor)=>this.addDescriptor(descriptor));
11675
12280
  }
11676
12281
  // Update the CodeAnalyzer with all descriptors
@@ -11962,24 +12567,50 @@ class OperationsService {
11962
12567
  this.config = config;
11963
12568
  this.searchService = searchService;
11964
12569
  this.logger = new common_namespaceObject.Logger(OperationsService.name);
12570
+ // Add sync coordination
12571
+ this.syncInProgress = new Set();
12572
+ this.SYNC_TIMEOUT = 300000; // 5 minutes timeout
11965
12573
  this.generateDir = this.config.get("generatedDir") ?? external_path_namespaceObject.resolve(process.cwd(), ".intrig", "generated");
11966
12574
  }
11967
12575
  async getConfig(ctx) {
11968
12576
  return this.configService.get();
11969
12577
  }
11970
12578
  async sync(ctx, id) {
11971
- const config = await this.getConfig(ctx);
11972
- const prevDescriptors = await this.getPreviousState(ctx, config);
11973
- const restOptions = this.generatorBinding.getRestOptions();
11974
- await this.openApiService.sync({
11975
- ...config,
11976
- restOptions: {
11977
- ...config.restOptions,
11978
- ...restOptions
11979
- }
11980
- }, id, ctx);
11981
- const newDescriptors = await this.getNewState(ctx, config);
11982
- await this.indexDiff(ctx, prevDescriptors, newDescriptors);
12579
+ const syncKey = id || 'all';
12580
+ // Check if sync is already in progress
12581
+ if (this.syncInProgress.has(syncKey)) {
12582
+ const error = `Sync already in progress for: ${syncKey}`;
12583
+ this.logger.warn(error);
12584
+ throw new Error(error);
12585
+ }
12586
+ // Add timeout protection
12587
+ const timeoutId = setTimeout(()=>{
12588
+ this.syncInProgress.delete(syncKey);
12589
+ this.logger.error(`Sync timeout for ${syncKey} after ${this.SYNC_TIMEOUT}ms`);
12590
+ }, this.SYNC_TIMEOUT);
12591
+ this.syncInProgress.add(syncKey);
12592
+ this.logger.log(`Starting sync operation for: ${syncKey}`);
12593
+ try {
12594
+ const config = await this.getConfig(ctx);
12595
+ const prevDescriptors = await this.getPreviousState(ctx, config);
12596
+ const restOptions = this.generatorBinding.getRestOptions();
12597
+ await this.openApiService.sync({
12598
+ ...config,
12599
+ restOptions: {
12600
+ ...config.restOptions,
12601
+ ...restOptions
12602
+ }
12603
+ }, id, ctx);
12604
+ const newDescriptors = await this.getNewState(ctx, config);
12605
+ await this.indexDiff(ctx, prevDescriptors, newDescriptors);
12606
+ this.logger.log(`Sync operation completed successfully for: ${syncKey}`);
12607
+ } catch (error) {
12608
+ this.logger.error(`Sync operation failed for ${syncKey}: ${error.message}`, error);
12609
+ throw error;
12610
+ } finally{
12611
+ clearTimeout(timeoutId);
12612
+ this.syncInProgress.delete(syncKey);
12613
+ }
11983
12614
  }
11984
12615
  async indexDiff(ctx, prevDescriptors, newDescriptors) {
11985
12616
  this.searchService.clearAll();
@@ -11987,21 +12618,36 @@ class OperationsService {
11987
12618
  }
11988
12619
  async getPreviousState(ctx, config) {
11989
12620
  let prevDescriptors = [];
12621
+ const errors = [];
11990
12622
  for (const source of config.sources){
11991
12623
  try {
11992
- const descriptors = await this.openApiService.getResourceDescriptors(source.id);
12624
+ const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
11993
12625
  prevDescriptors = [
11994
12626
  ...prevDescriptors,
11995
12627
  ...descriptors
11996
12628
  ];
11997
- } catch (e) {}
12629
+ this.logger.debug(`Loaded ${descriptors.length} descriptors from source ${source.id}`);
12630
+ } catch (e) {
12631
+ const errorMsg = `Failed to load previous state for source ${source.id}: ${e.message}`;
12632
+ this.logger.warn(errorMsg);
12633
+ errors.push(errorMsg);
12634
+ // If it's a critical error (not just missing file), we might want to fail
12635
+ if (e.message.includes('Invalid JSON') || e.message.includes('corrupted')) {
12636
+ throw new Error(`Critical error in getPreviousState: ${errorMsg}`);
12637
+ }
12638
+ }
12639
+ }
12640
+ if (errors.length > 0) {
12641
+ this.logger.warn(`getPreviousState completed with ${errors.length} errors: ${errors.join('; ')}`);
11998
12642
  }
12643
+ this.logger.log(`Loaded total of ${prevDescriptors.length} previous descriptors`);
11999
12644
  return prevDescriptors;
12000
12645
  }
12001
12646
  async getNewState(ctx, config) {
12647
+ this.logger.log('Loading new state');
12002
12648
  let prevDescriptors = [];
12003
12649
  for (const source of config.sources){
12004
- const descriptors = await this.openApiService.getResourceDescriptors(source.id);
12650
+ const { descriptors } = await this.openApiService.getResourceDescriptors(source.id);
12005
12651
  prevDescriptors = [
12006
12652
  ...prevDescriptors,
12007
12653
  ...descriptors
@@ -12033,8 +12679,10 @@ class OperationsService {
12033
12679
  async generate(ctx) {
12034
12680
  const config = await this.getConfig(ctx);
12035
12681
  await this.clearGenerateDir(ctx);
12682
+ const hashes = {};
12036
12683
  for (const source of config.sources){
12037
- const descriptors = await this.getDescriptors(ctx, source);
12684
+ const { descriptors, hash } = await this.getDescriptors(ctx, source);
12685
+ hashes[source.id] = hash;
12038
12686
  await this.generateSourceContent(ctx, descriptors, source);
12039
12687
  }
12040
12688
  await this.generateGlobalContent(ctx, config.sources);
@@ -12042,9 +12690,9 @@ class OperationsService {
12042
12690
  await this.buildContent(ctx);
12043
12691
  const tempBuildContext = config.__dangorouslyOverrideBuild;
12044
12692
  if (tempBuildContext) {
12045
- this.copyContentToSource(ctx, tempBuildContext);
12693
+ await this.copyContentToSource(ctx, tempBuildContext);
12046
12694
  } else {
12047
- await this.copyContentToNodeModules(ctx);
12695
+ await this.copyContentToNodeModules(ctx, hashes);
12048
12696
  }
12049
12697
  await this.executePostBuild(ctx);
12050
12698
  }
@@ -12076,7 +12724,7 @@ class OperationsService {
12076
12724
  throw error;
12077
12725
  }
12078
12726
  }
12079
- async copyContentToNodeModules(ctx) {
12727
+ async copyContentToNodeModules(ctx, hashes) {
12080
12728
  const targetLibDir = external_path_namespaceObject.join(this.config.get('rootDir') ?? process.cwd(), 'node_modules', '@intrig', this.generatorBinding.getLibName());
12081
12729
  try {
12082
12730
  if (await external_fs_extra_namespaceObject.pathExists(external_path_namespaceObject.join(targetLibDir, 'src'))) {
@@ -12088,6 +12736,10 @@ class OperationsService {
12088
12736
  this.logger.log(`Copied ${targetLibDir}`);
12089
12737
  await this.mergePackageJson(external_path_namespaceObject.join(this.generateDir, 'package.json'), external_path_namespaceObject.join(targetLibDir, 'package.json'));
12090
12738
  this.logger.log(`Merged package.json files`);
12739
+ await external_fs_extra_namespaceObject.writeJson(external_path_namespaceObject.join(targetLibDir, 'hashes.json'), hashes, {
12740
+ spaces: 2
12741
+ });
12742
+ this.logger.log(`Wrote hashes.json to ${targetLibDir}`);
12091
12743
  } catch (error) {
12092
12744
  this.logger.error(`Failed to copy content to node_modules: ${error}`);
12093
12745
  throw error;
@@ -12117,7 +12769,9 @@ class OperationsService {
12117
12769
  await this.generatorBinding.generateSource(descriptors, source);
12118
12770
  }
12119
12771
  async getDescriptors(ctx, source) {
12120
- return await this.openApiService.getResourceDescriptors(source.id);
12772
+ const descriptors = await this.openApiService.getResourceDescriptors(source.id);
12773
+ // Hash is now available for operations.service to use
12774
+ return descriptors;
12121
12775
  }
12122
12776
  async clearGenerateDir(ctx) {
12123
12777
  if (external_fs_extra_namespaceObject.pathExistsSync(this.generateDir)) {
@@ -12136,6 +12790,74 @@ class OperationsService {
12136
12790
  external_fs_extra_namespaceObject.ensureDirSync(this.generateDir);
12137
12791
  return this.generateDir;
12138
12792
  }
12793
+ async verify(hashes) {
12794
+ this.logger.log('Verifying hashes');
12795
+ try {
12796
+ // 1. Load intrigConfig
12797
+ const intrigConfig = this.configService.get();
12798
+ const sources = intrigConfig.sources || [];
12799
+ // 2. Check that each key in hashes has a matching source id
12800
+ const sourceIds = new Set(sources.map((source)=>source.id));
12801
+ const hashKeys = Object.keys(hashes);
12802
+ for (const hashKey of hashKeys){
12803
+ if (!sourceIds.has(hashKey)) {
12804
+ this.logger.error(`Hash key '${hashKey}' does not have a matching source id`);
12805
+ return {
12806
+ status: 'conflict',
12807
+ message: `Hash key '${hashKey}' does not have a matching source id`,
12808
+ statusCode: 409
12809
+ };
12810
+ }
12811
+ }
12812
+ // 3. Check that there are no more sources than hashes
12813
+ if (sources.length > hashKeys.length) {
12814
+ const extraSources = sources.map((s)=>s.id).filter((id)=>!hashKeys.includes(id));
12815
+ this.logger.error(`There are more sources than hashes. Extra sources: ${extraSources.join(', ')}`);
12816
+ return {
12817
+ status: 'conflict',
12818
+ message: `There are more sources than hashes. Extra sources: ${extraSources.join(', ')}`,
12819
+ statusCode: 409
12820
+ };
12821
+ }
12822
+ // 4. Get hash for each source and validate they match
12823
+ for (const source of sources){
12824
+ try {
12825
+ const actualHash = await this.openApiService.getHash(source.id);
12826
+ const expectedHash = hashes[source.id];
12827
+ if (actualHash !== expectedHash) {
12828
+ this.logger.error(`Hash mismatch for source '${source.id}'. Expected: ${expectedHash}, Actual: ${actualHash}`);
12829
+ return {
12830
+ status: 'conflict',
12831
+ message: `Hash mismatch for source '${source.id}'. Expected: ${expectedHash}, Actual: ${actualHash}`,
12832
+ statusCode: 409
12833
+ };
12834
+ }
12835
+ } catch (error) {
12836
+ this.logger.error(`Failed to get hash for source '${source.id}': ${error.message}`);
12837
+ return {
12838
+ status: 'conflict',
12839
+ message: `Failed to get hash for source '${source.id}': ${error.message}`,
12840
+ statusCode: 409
12841
+ };
12842
+ }
12843
+ }
12844
+ // 5. All validations passed
12845
+ this.logger.log('All hash validations passed successfully');
12846
+ return {
12847
+ status: 'verified',
12848
+ message: 'All hashes match successfully',
12849
+ hashCount: Object.keys(hashes).length,
12850
+ statusCode: 200
12851
+ };
12852
+ } catch (error) {
12853
+ this.logger.error(`Verification failed with error: ${error.message}`);
12854
+ return {
12855
+ status: 'conflict',
12856
+ message: `Verification failed: ${error.message}`,
12857
+ statusCode: 409
12858
+ };
12859
+ }
12860
+ }
12139
12861
  }
12140
12862
  operations_service_ts_decorate([
12141
12863
  WithStatus((event)=>({
@@ -12203,7 +12925,8 @@ operations_service_ts_decorate([
12203
12925
  })),
12204
12926
  operations_service_ts_metadata("design:type", Function),
12205
12927
  operations_service_ts_metadata("design:paramtypes", [
12206
- typeof GenerateEventContext === "undefined" ? Object : GenerateEventContext
12928
+ typeof GenerateEventContext === "undefined" ? Object : GenerateEventContext,
12929
+ typeof Record === "undefined" ? Object : Record
12207
12930
  ]),
12208
12931
  operations_service_ts_metadata("design:returntype", Promise)
12209
12932
  ], OperationsService.prototype, "copyContentToNodeModules", null);
@@ -12322,6 +13045,7 @@ function operations_controller_ts_param(paramIndex, decorator) {
12322
13045
 
12323
13046
 
12324
13047
 
13048
+
12325
13049
  class OperationsController {
12326
13050
  constructor(operationsService){
12327
13051
  this.operationsService = operationsService;
@@ -12346,9 +13070,16 @@ class OperationsController {
12346
13070
  await this.operationsService.generate(generateEventContext);
12347
13071
  generateEventContext.done(new GenerateDoneEventDto(true));
12348
13072
  this.logger.log('Generate operation completed');
12349
- });
13073
+ }, 0);
12350
13074
  return events$;
12351
13075
  }
13076
+ async verify(hashes, res) {
13077
+ this.logger.log('Starting verify operation');
13078
+ const result = await this.operationsService.verify(hashes);
13079
+ this.logger.log('Verify operation completed');
13080
+ const statusCode = result.statusCode || 200;
13081
+ return res.status(statusCode).json(result);
13082
+ }
12352
13083
  }
12353
13084
  operations_controller_ts_decorate([
12354
13085
  (0,common_namespaceObject.Sse)('sync'),
@@ -12401,6 +13132,25 @@ operations_controller_ts_decorate([
12401
13132
  operations_controller_ts_metadata("design:paramtypes", []),
12402
13133
  operations_controller_ts_metadata("design:returntype", Promise)
12403
13134
  ], OperationsController.prototype, "generate", null);
13135
+ operations_controller_ts_decorate([
13136
+ (0,common_namespaceObject.Post)('verify'),
13137
+ (0,swagger_namespaceObject.ApiResponse)({
13138
+ status: 200,
13139
+ description: 'Verify hashes - successful verification'
13140
+ }),
13141
+ (0,swagger_namespaceObject.ApiResponse)({
13142
+ status: 409,
13143
+ description: 'Verify hashes - validation conflicts found'
13144
+ }),
13145
+ operations_controller_ts_param(0, (0,common_namespaceObject.Body)()),
13146
+ operations_controller_ts_param(1, (0,common_namespaceObject.Res)()),
13147
+ operations_controller_ts_metadata("design:type", Function),
13148
+ operations_controller_ts_metadata("design:paramtypes", [
13149
+ typeof Record === "undefined" ? Object : Record,
13150
+ typeof external_express_namespaceObject.Response === "undefined" ? Object : external_express_namespaceObject.Response
13151
+ ]),
13152
+ operations_controller_ts_metadata("design:returntype", Promise)
13153
+ ], OperationsController.prototype, "verify", null);
12404
13154
  OperationsController = operations_controller_ts_decorate([
12405
13155
  (0,swagger_namespaceObject.ApiTags)('Operations'),
12406
13156
  (0,swagger_namespaceObject.ApiExtraModels)(SyncStatusEventDto, SyncDoneEventDto, GenerateStatusEventDto, GenerateDoneEventDto),
@@ -13609,62 +14359,6 @@ DeamonModule = deamon_module_ts_decorate([
13609
14359
  };
13610
14360
  });
13611
14361
 
13612
- ;// external "url"
13613
- const external_url_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("url");
13614
- ;// external "process"
13615
- const external_process_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("process");
13616
- ;// ./src/app/debug/debug.controller.ts
13617
- function debug_controller_ts_decorate(decorators, target, key, desc) {
13618
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
13619
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
13620
- else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
13621
- return c > 3 && r && Object.defineProperty(target, key, r), r;
13622
- }
13623
- function debug_controller_ts_metadata(k, v) {
13624
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
13625
- }
13626
-
13627
-
13628
-
13629
-
13630
- // Get the directory name in ES modules
13631
- const debug_controller_filename = (0,external_url_namespaceObject.fileURLToPath)("file:///home/tiran-intrigsoft/IdeaProjects/intrig-core/app/intrig/src/app/debug/debug.controller.ts");
13632
- const debug_controller_dirname = (0,external_path_namespaceObject.dirname)(debug_controller_filename);
13633
- class DebugController {
13634
- getDirname() {
13635
- // Calculate paths that might be relevant for debugging
13636
- const assetsPath = (0,external_path_namespaceObject.join)(external_process_namespaceObject.cwd(), 'dist', 'app', 'intrig', 'assets', 'insight');
13637
- const appDirname = (0,external_path_namespaceObject.dirname)(debug_controller_dirname);
13638
- return {
13639
- // ES Module specific values
13640
- dirname: debug_controller_dirname,
13641
- filename: debug_controller_filename,
13642
- meta_url: "file:///home/tiran-intrigsoft/IdeaProjects/intrig-core/app/intrig/src/app/debug/debug.controller.ts",
13643
- // Calculated paths
13644
- assetsPath,
13645
- appDirname,
13646
- // Process information
13647
- cwd: external_process_namespaceObject.cwd(),
13648
- execPath: external_process_namespaceObject.execPath,
13649
- // Environment
13650
- nodeEnv: external_process_namespaceObject.env.NODE_ENV,
13651
- // Module type
13652
- isESM: "function" === 'undefined',
13653
- // Timestamp for cache busting
13654
- timestamp: new Date().toISOString()
13655
- };
13656
- }
13657
- }
13658
- debug_controller_ts_decorate([
13659
- (0,common_namespaceObject.Get)('dirname'),
13660
- debug_controller_ts_metadata("design:type", Function),
13661
- debug_controller_ts_metadata("design:paramtypes", []),
13662
- debug_controller_ts_metadata("design:returntype", void 0)
13663
- ], DebugController.prototype, "getDirname", null);
13664
- DebugController = debug_controller_ts_decorate([
13665
- (0,common_namespaceObject.Controller)('debug')
13666
- ], DebugController);
13667
-
13668
14362
  ;// ./src/app/app.module.ts
13669
14363
  function app_module_ts_decorate(decorators, target, key, desc) {
13670
14364
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -13679,8 +14373,6 @@ function app_module_ts_decorate(decorators, target, key, desc) {
13679
14373
 
13680
14374
 
13681
14375
 
13682
- // import {McpModule} from "./mcp/mcp.module";
13683
-
13684
14376
  class AppModule {
13685
14377
  }
13686
14378
  AppModule = app_module_ts_decorate([
@@ -13697,9 +14389,7 @@ AppModule = app_module_ts_decorate([
13697
14389
  DiscoveryModule,
13698
14390
  DeamonModule
13699
14391
  ],
13700
- controllers: [
13701
- DebugController
13702
- ],
14392
+ controllers: [],
13703
14393
  providers: []
13704
14394
  })
13705
14395
  ], AppModule);
@@ -13717,12 +14407,10 @@ const core_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("
13717
14407
 
13718
14408
 
13719
14409
 
14410
+
13720
14411
  const logger = new common_namespaceObject.Logger('Main');
13721
14412
  async function bootstrapDeamon() {
13722
14413
  const app = await core_namespaceObject.NestFactory.create(AppModule, {
13723
- logger: [
13724
- 'error'
13725
- ]
13726
14414
  });
13727
14415
  app.enableShutdownHooks();
13728
14416
  app.enableCors();
@@ -13760,6 +14448,14 @@ async function bootstrap() {
13760
14448
  await app.init();
13761
14449
  process.stdin.resume();
13762
14450
  } else {
14451
+ if (cmd === 'init') {
14452
+ const configPath = './intrig.config.json';
14453
+ if (external_fs_namespaceObject.existsSync(configPath)) {
14454
+ throw new Error('Intrig is already initialized. Configuration file exists.');
14455
+ }
14456
+ external_fs_namespaceObject.writeFileSync(configPath, JSON.stringify({}, null, 2));
14457
+ logger?.log('Initialized new Intrig project');
14458
+ }
13763
14459
  try {
13764
14460
  await external_nest_commander_namespaceObject.CommandFactory.run(AppModule, {
13765
14461
  logger: false,