diff --git a/.eslintrc.yml b/.eslintrc.yml index 46446eff..acfca4f1 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,5 +1,5 @@ --- -parser: '@babel/eslint-parser' +parser: '@typescript-eslint/parser' extends: - es/2015/server plugins: @@ -24,7 +24,25 @@ parserOptions: requireConfigFile: false sourceType: 'module' ecmaVersion: 2017 +overrides: + - files: ['**/tests/**/*.{js,cjs,mjs}'] + rules: + no-console: 0 + - files: ['*.ts', '*.tsx'] + extends: ['plugin:@typescript-eslint/recommended'] + rules: + '@typescript-eslint/no-non-null-assertion': 0 + '@typescript-eslint/no-explicit-any': 0 + '@typescript-eslint/no-empty-interface': 0 + '@typescript-eslint/explicit-module-boundary-types': 0 + '@typescript-eslint/no-unused-vars': 0 + '@typescript-eslint/no-this-alias': 0 + '@typescript-eslint/ban-ts-comment': 0 + '@typescript-eslint/no-namespace': 0 + '@typescript-eslint/no-empty-function': 0 + '@typescript-eslint/no-extra-semi': 0 rules: + arrow-body-style: 0 arrow-parens: 0 prefer-arrow-callback: 0 newline-before-return: 0 @@ -277,7 +295,3 @@ rules: prefer-rest-params: 0 symbol-description: 1 callback-return: 0 -overrides: - - files: ['**/tests/**/*.{js,cjs,mjs}'] - rules: - no-console: 0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4ecaefb2..493d590a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,9 +8,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 14 + node-version: 22 - run: npm install - run: npm run lint diff --git a/.github/workflows/test-redis-oplog.yml b/.github/workflows/test-redis-oplog.yml index e31ef1a2..8002a6b5 100644 --- a/.github/workflows/test-redis-oplog.yml +++ b/.github/workflows/test-redis-oplog.yml @@ -12,24 +12,27 @@ jobs: fail-fast: false matrix: meteorRelease: - - '--release 1.11' - - '--release 2.16' + - '--release 3.0.3' env: REDIS_OPLOG_SETTINGS: '{"debug":true}' steps: - uses: supercharge/redis-github-action@1.4.0 - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 with: - node-version: '12.x' + cache: 'npm' + node-version: 18 - name: Install Dependencies run: | curl https://install.meteor.com | /bin/sh npm i -g @zodern/mtest + npm ci - name: Run Tests run: | - # Fix using old versions of Meteor - export NODE_TLS_REJECT_UNAUTHORIZED=0 + echo "Env: $NODE_ENV" + export FORCE_COLOR=true + export NODE_ENV=test mtest --package ./ --once ${{ matrix.meteorRelease }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d1492ae..ac8d98d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,49 +12,30 @@ jobs: fail-fast: false matrix: meteorRelease: - - '--release 1.4.4.6' - - '--release 1.5.4.1' - - '--release 1.6.0.1' - - '--release 1.7.0.5' - - '--release 1.8.1' - - '--release 1.8.3' - - '--release 1.9.1' - - '--release 1.10.2' - - '--release 1.11' - - '--release 1.12.1' - - '--release 2.1.1' - - '--release 2.2' - - '--release 2.3.2' - - '--release 2.4.1' - - '--release 2.5.6' - - '--release 2.6.1' - - '--release 2.7.3' - - '--release 2.8.2' - - '--release 2.9.0' - - '--release 2.10.0' - - '--release 2.11.0' - - '--release 2.12' - - '--release 2.13.3' - - '--release 2.14' - - '--release 2.15' - - '--release 2.16' + - '--release 3.0' + - '--release 3.0.4' + - '--release 3.1.1' + # latest release + - steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: + cache: 'npm' node-version: '22.x' - name: Install Dependencies run: | curl https://install.meteor.com | /bin/sh npm i -g @zodern/mtest + npm ci - name: Run Tests run: | - # Fix using old versions of Meteor - export NODE_TLS_REJECT_UNAUTHORIZED=0 - + echo "Env: $NODE_ENV" + export FORCE_COLOR=true + export NODE_ENV=test mtest --package ./ --once ${{ matrix.meteorRelease }} diff --git a/.gitignore b/.gitignore index a3a16e79..0bbd8bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ smart.lock versions.json .idea .eslintcache +packages/ .envrc /packages/* diff --git a/.npm/package/npm-shrinkwrap.json b/.npm/package/npm-shrinkwrap.json index f8a412f4..438fed45 100644 --- a/.npm/package/npm-shrinkwrap.json +++ b/.npm/package/npm-shrinkwrap.json @@ -1,6 +1,18 @@ { - "lockfileVersion": 1, + "lockfileVersion": 4, "dependencies": { + "@monti-apm/core": { + "version": "2.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@monti-apm/core/-/core-2.0.0-beta.11.tgz", + "integrity": "sha512-L7lFGaRGazdJBHFTI6JTsxbxTE2hnXiELVJBviU4BwUokxrBAU57blIbrA66ARJ9RM6Tbzy4L0wUaWXSYhDXEw==", + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==" + } + } + }, "@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -35,21 +47,41 @@ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==" }, + "agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==" + }, "array-from": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==" + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==" }, "debug": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz", "integrity": "sha512-HlXEJm99YsRjLJ8xmuz0Lq8YUwrv7hAJkTEr6/Em3sUlSUNl0UdFA+1SrY4fnykeq1FVkUEUtwRGHs9VvlYbGA==" }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -70,15 +102,32 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" }, + "form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==" + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==" + } + } }, "isarray": { "version": "0.0.1", @@ -115,17 +164,15 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==" }, - "monti-apm-core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/monti-apm-core/-/monti-apm-core-1.8.0.tgz", - "integrity": "sha512-s0A6ZUXRKY7EBwi9Nxqe+HfT6SqH4EuIv1SF3WBs0FRvKN8rBE62frTvE/WtLg8a27lVJS8D7j5lgTbF/DHN9Q==", - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==" - } - } + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==" }, "monti-apm-sketches-js": { "version": "0.0.3", diff --git a/CHANGELOG.md b/CHANGELOG.md index b868ce42..639f1d02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog -## v2.50.0 +## Next +* Support Meteor 3 +* Add `Monti.event` +* Tracer supports parallel events +* Improve support for proxies +## v2.50.0 December 30, 2024 * Create custom traces with `Monti.traceJob` diff --git a/README.md b/README.md index 01a3d7b8..97ff95df 100644 --- a/README.md +++ b/README.md @@ -90,20 +90,20 @@ You should use the same method that you used to give the agent the app id and se #### List of Options -| Name | Environment Variable | Default | Description | -|---------------------|-------------------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| appId | APP_ID | none | | -| appSecret | APP_SECRET | none | | -| enableErrorTracking | OPTIONS_ENABLE_ERROR_TRACKING | true | Enable sending errors to Monti APM | -| disableClientErrorTracking | OPTIONS_DISABLE_CLIENT_ERROR_TRACKING | false | Disable sending client errors to Monti APM | -| endpoint | OPTIONS_ENDPOINT | https://engine.montiapm.com | Monti / Kadira engine url | -| hostname | OPTIONS_HOSTNAME | Server's hostname | What the instance is named in Monti APM | -| uploadSourceMaps | UPLOAD_SOURCE_MAPS | true | Enables sending source maps to Monti APM to improve error stack traces | -| recordIPAddress | RECORD_IP_ADDRESS | 'full' | Set to 'full' to record IP Address, 'anonymized' to anonymize last octet of address, or 'none' to not record an IP Address for client errors | -| eventStackTrace | EVENT_STACK_TRACE | false | If true, records a stack trace when an event starts. Slightly decreases server performance. | -| disableNtp | OPTIONS_DISABLE_NTP | false | Disable NTP time synchronization used to get the accurate time in case the server or client's clock is wrong | -| stalledTimeout | STALLED_TIMEOUT | 1800000 (30m) | Timeout used to detect when methods and subscriptions might be stalled (have been running for a long time and might never return). The value is in milliseconds, and can be disabled by setting it to 0 | -| proxy | OPTIONS_PROXY | none | Allows you to connect to Monti APM using a proxy | +| Name | Environment Variable | Default | Description | +|----------------------------|---------------------------------------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| appId | APP_ID | none | | +| appSecret | APP_SECRET | none | | +| enableErrorTracking | OPTIONS_ENABLE_ERROR_TRACKING | true | Enable sending errors to Monti APM | +| disableClientErrorTracking | OPTIONS_DISABLE_CLIENT_ERROR_TRACKING | false | Disable sending client errors to Monti APM | +| endpoint | OPTIONS_ENDPOINT | https://engine.montiapm.com | Monti / Kadira engine url | +| hostname | OPTIONS_HOSTNAME | Server's hostname | What the instance is named in Monti APM | +| uploadSourceMaps | UPLOAD_SOURCE_MAPS | true | Enables sending source maps to Monti APM to improve error stack traces | +| recordIPAddress | RECORD_IP_ADDRESS | 'full' | Set to 'full' to record IP Address, 'anonymized' to anonymize last octet of address, or 'none' to not record an IP Address for client errors | +| eventStackTrace | EVENT_STACK_TRACE | false | If true, records a stack trace when an event starts. Slightly decreases server performance. | +| disableNtp | OPTIONS_DISABLE_NTP | false | Disable NTP time synchronization used to get the accurate time in case the server or client's clock is wrong | +| stalledTimeout | STALLED_TIMEOUT | 1800000 (30m) | Timeout used to detect when methods and subscriptions might be stalled (have been running for a long time and might never return). The value is in milliseconds, and can be disabled by setting it to 0 | +| proxy | MONTI_OPTIONS_PROXY | none | Allows you to connect to Monti APM using a proxy | | disableInstrumentation | DISABLE_INSTRUMENTATION | false | Disables recording most metrics and traces. Can be configured using Meteor.settings, or by env variable | @@ -129,13 +129,26 @@ The agent records up to one level of nested events. You can add custom events to the traces to time specific code or events, or to provide more details to the trace. ```js +Monti.event('event name', { + // This object can have any details you want to store + // The object is optional + userId: userId +}, async () => { + // do the work inside the event + // the event will automatically end when this function returns +}); + +// Older api. Not recommended since it doesn't nest child events in Meteor 3 const event = Monti.startEvent('event name', { - details: true + // This object can have any details you want + organization: organizationId }); // After code runs or event happens Monti.endEvent(event, { - additionalDetails: true + // This object can have any details you want + // It is merged with the object created when starting the event + organizationMembers: 10 }); ``` diff --git a/lib/.meteor-package-versions b/lib/.meteor-package-versions index e4114c9d..52251852 100644 --- a/lib/.meteor-package-versions +++ b/lib/.meteor-package-versions @@ -1,2 +1,2 @@ -// the zodern:meteor-package-versions compiler replaces this file with -// the list of packages and their versions +// the zodern:meteor-package-versions compiler replaces this file with +// the list of packages and their versions \ No newline at end of file diff --git a/lib/async/README.md b/lib/async/README.md new file mode 100644 index 00000000..b9ca9753 --- /dev/null +++ b/lib/async/README.md @@ -0,0 +1,288 @@ +## How Async Hooks work + +Async Hooks is a module in Node.js that provides an API to track asynchronous resources in your application. This is crucial when building and maintaining large-scale applications, as it allows developers to understand what's happening under the hood of their asynchronous operations, providing a deeper level of insight into the event loop, promises, and other asynchronous behaviors. + +Here's a basic rundown of how Async Hooks works: + +- **Initialization** (`init`): This is the first step in the life cycle of an async operation. When an async operation is initialized, the init hook is called. This hook function receives several arguments: the unique id of the async operation, the type of the operation (e.g., 'TIMERWRAP', 'PROMISE', etc.), the id of the parent async operation (which caused the current operation), and the resource object. + +- **Before** (`before`): Just before the async operation's callback is about to be called, the before hook is invoked. The callback receives the id of the async operation. + +- **After** (`after`): Just after the async operation's callback has finished, the after hook is invoked. Like the before hook, it also receives the id of the async operation. + +- **Destruction** (`destroy`): When the async operation is about to be garbage collected, the destroy hook is invoked. This hook receives the id of the async operation. + +- **Promise Resolve** (`promiseResolve`): This is a special hook that is invoked when a Promise-based operation is resolved. It receives the id of the async operation. + +To use Async Hooks, you need to create a hooks object with the desired lifecycle hooks, and then call `async_hooks.createHook(hooks)` to create a new AsyncHooks instance. This instance has `enable()` and `disable()` methods to start and stop the tracking of async operations. + +## Clarification on the `destroy` hook + +The `destroy` hook in Node.js's Async Hooks API is called when an asynchronous resource is about to be garbage collected, not necessarily immediately after the resource's work has completed. + +It's important to understand that in JavaScript, and Node.js in particular, the timing of garbage collection is not guaranteed. The JavaScript engine decides when to perform garbage collection based on a variety of factors, including memory pressure and CPU usage. This means that there can be a delay between when an async resource's work is finished and when the `destroy` hook is called. + +So while the `destroy` hook can give you an indication that an async resource is no longer in use and is about to be cleaned up, it's not a reliable way to determine exactly when the resource's work has finished. For that, you would typically use the `after` hook, which is called immediately after the callback associated with an async operation has completed. + +In the case of Promise-based async operations, like those created with `then` or `catch`, the `destroy` hook would typically be called sometime after the Promise has settled (i.e., either resolved or rejected) and the associated callbacks have run. But the exact timing would depend on when the JavaScript engine decides to perform garbage collection. + +## The Promise executor function is called synchronously + +When you create a new Promise, you provide a function as an argument. This function is often referred to as the "executor function". The executor function takes two arguments: a `resolve` function and a `reject` function. + +Here's an example: + +```javascript +new Promise((resolve, reject) => { + // This is the executor function +}); +``` + +The executor function is called immediately, synchronously when the Promise is created. This is by design, as it allows you to start any asynchronous operations needed to eventually resolve or reject the Promise. However, the Promise itself doesn't resolve or reject until you call the provided `resolve` or `reject` function, and that can happen synchronously or asynchronously depending on your code. + +Because the executor function is called synchronously, if you call `async_hooks.executionAsyncId()` inside the executor function, it will return the async ID of the current synchronous execution context, not the async ID of the Promise itself. This can be misleading if you're trying to track the async ID of the Promise. + +Here's an example that illustrates this: + +```javascript +const async_hooks = require('async_hooks'); + +new Promise((resolve, reject) => { + console.log(async_hooks.executionAsyncId()); // Logs the async ID of the current synchronous execution context + setTimeout(resolve, 1000); // Resolve the Promise asynchronously after 1 second +}); +``` + +In this example, the `console.log` statement is executed synchronously when the Promise is created, so it logs the async ID of the current synchronous execution context. The Promise is then resolved asynchronously after 1 second by the `setTimeout` call. + +To get the async ID of the Promise itself, you would need to use the `init` hook in the Async Hooks API, which is called when an async resource is created: + +```javascript +const async_hooks = require('async_hooks'); + +async_hooks.createHook({ + init: (asyncId, type, triggerAsyncId) => { + if (type === 'PROMISE') { + console.log(asyncId); // Logs the async ID of the Promise + } + } +}).enable(); + +new Promise((resolve, reject) => { + setTimeout(resolve, 1000); // Resolve the Promise asynchronously after 1 second +}); +``` + +In this modified example, the `init` hook logs the async ID of the Promise when the Promise is created. + +The `init` hook in Node.js's Async Hooks API is called whenever an asynchronous resource is being initialized. This means that it's called at the point when the system sets up the internal state for the resource but before the resource's asynchronous operation has actually started. + +For example, in the case of a Promise, the `init` hook is called when the Promise is created but before the executor function has started running. Here's a simplified illustration: + +```javascript +const async_hooks = require('async_hooks'); + +async_hooks.createHook({ + init: (asyncId, type, triggerAsyncId) => { + // You need this check as `console.log` also spawns an async resource and will cause an infinite loop + if (type === 'PROMISE') { + console.log(`init: ${asyncId}`); + } + } +}).enable(); + +console.log('before promise'); +new Promise((resolve, reject) => { + console.log('inside promise'); + resolve(); +}); +console.log('after promise'); +``` + +In this code, the `init` hook logs a message whenever an async resource is initialized. The output would look something like this: + +``` +before promise +init: 5 +inside promise +after promise +``` + +This shows that the `init` hook is called in between the 'before promise' and 'inside promise' log messages, i.e., after the Promise is created but before the executor function runs. Note that the specific async ID (5 in this example) may vary between runs. + +It's important to understand that while the `init` hook is called at the initialization stage of an async operation, the associated async resource may not have completed its construction when the hook runs. Therefore, you shouldn't rely on all properties of the async resource being available in the `init` hook. + +## Some promises do not call `before` or `after` + +Async Hooks is a powerful feature in Node.js that allows developers to track the lifetime of asynchronous resources in a Node.js application. However, not all asynchronous operations are guaranteed to trigger Async Hooks events, including the before and after events. + +Promises in particular can behave somewhat differently than other asynchronous resources in regards to Async Hooks. + +The before and after events of async hooks correspond to the execution of a JavaScript callback function. If a Promise is resolved (or rejected) with a value that is not another Promise, and there are no `.then()` or `.catch()` handlers registered on it by the end of the current "tick" of the event loop, then no JavaScript callback is ever executed for that Promise. As a result, no before or after events would be emitted by Async Hooks for that Promise. + +This is because the Promise implementation in JavaScript engines is optimized to avoid unnecessary operations when possible. If a Promise is resolved and no handlers are registered on it, the engine may decide not to queue any microtask for that Promise, since doing so would have no observable effect. Since Async Hooks hooks into the Node.js event loop and not the JavaScript engine itself, it would not see any activity for such Promises. + +> So in short, if you're seeing some Promises not triggering before or after events in Async Hooks, it could be because those Promises are being resolved without any handlers registered on them. + +Keep in mind that the behavior of Async Hooks and Promises can be complex and may vary between different versions of Node.js and different JavaScript engines, so it's possible there may be other factors at play as well. + +## All promises trigger `promiseResolve`, except when they are rejected + +The `promiseResolve` hook in Node.js's Async Hooks API is called when a Promise has been resolved, that is when the Promise has completed its work and has a result ready. This happens regardless of how the Promise was created or used. + +Here's a simple example: + +```javascript +const async_hooks = require('async_hooks'); + +async_hooks.createHook({ + promiseResolve: (asyncId) => { + console.log(`Promise resolved with id ${asyncId}`); + }, +}).enable(); + +Promise.resolve(42); +``` + +In this code, a Promise is created and immediately resolved with the value `42`. The `promiseResolve` hook is called when this Promise is resolved, and logs a message with the async ID of the Promise. + +> It's important to note that the `promiseResolve` hook is not called when a Promise is rejected. If you want to track when Promises are rejected, you would typically need to attach a `catch` callback to the Promise and handle the rejection there. + +## The `then` and `catch` callbacks create new async operations which trigger `before` and `after` + +Promises in JavaScript provide `then` and `catch` methods for handling the resolution and rejection of the promise, respectively. These methods take callbacks that are called when the promise is resolved or rejected. + +In the context of Async Hooks in Node.js, when you attach a `then` or `catch` callback to a promise, a new asynchronous operation is created. This means that the `init` hook is called when you attach the `then` or `catch` callback. + +When the promise is resolved (for `then`) or rejected (for `catch`), and it's time to call the callback, the `before` hook is called. After the callback has been executed, the `after` hook is called. + +Here's a simplified example: + +```javascript +doSomethingAsync() // `init` hook called here + .then(result => { // `init` hook called here for the `then` callback + console.log(result); // `before` hook called here, then `after` hook after console.log + }) // `destroy` hook called here after the `then` callback is finished + .catch(error => { // `init` hook called here for the `catch` callback + console.error(error); // `before` hook called here, then `after` hook after console.error + }); // `destroy` hook called here after the `catch` callback is finished +``` + +In this example, the `init`, `before`, and `after` hooks are called for the `doSomethingAsync` operation, as well as for the `then` and `catch` callbacks. The `destroy` hook is called after each operation is completed and the resource is about to be garbage collected. + +Remember that the `then` and `catch` callbacks are associated with new async operations, not with the original promise. This means that they have their own async IDs and can be tracked independently in Async Hooks. + +It's also worth noting that if the `then` or `catch` callbacks themselves return a promise or include an `await` expression, this would create additional async operations and trigger additional hook calls. + +## The `before` and `after` hooks are called for `await` operations + +When an `await` operation is encountered in an async function, the function is paused, and control is returned to the event loop, which can then process other tasks. This pausing of the function creates an asynchronous boundary. When the awaited promise is resolved, the function is resumed. + +In the context of Async Hooks, when a new async operation is created (such as when a promise is awaited), the `init` hook is called. Then, just before the async operation's callback is called (i.e., just before the async function is resumed after an `await`), the `before` hook is called. Similarly, just after the callback has completed, the `after` hook is called. + +If you have multiple `await` expressions in a row in an async function, each `await` creates a new async operation. So for each `await`, the `before` and `after` hooks are called when the function is paused and resumed. Here's a simplified view: + +```javascript +async function example() { + // Before any awaits - function is running synchronously + await doSomething(); // `before` hook called here, then `after` hook when promise is resolved + await doSomethingElse(); // `before` hook called here, then `after` hook when promise is resolved + // After all awaits - function resumes running synchronously +} +``` + +In this example, the `before` and `after` hooks are called twice: once for each `await`. + +Remember that the `before` and `after` hooks are associated with the async operations, not directly with the `await` keyword. Any operation that creates a new async operation (which includes any operation that returns a promise) will trigger the `before` and `after` hooks. The `await` keyword just makes it easy to create async operations by pausing and resuming async functions. + +It's important to note that while the `before` and `after` hooks give you visibility into when async operations are starting and finishing, they don't necessarily provide information about what's happening inside those operations. For that, you would need to look at the code of the operations themselves or use other debugging tools. + +## The last `await` also triggers `before` or `after` even though there is nothing else to do + +The `before` and `after` hooks in Async Hooks are triggered around the execution of the callback associated with an asynchronous operation. In the case of `await`, the "callback" could be considered as the remaining part of the async function that follows the `await` expression. + +When you use `await` on a promise, the part of the function that follows the `await` expression is effectively a callback that gets executed when the promise is resolved. So, even if an `await` expression is the last statement in an async function, it would still trigger the `before` and `after` hooks because there's still a "callback" to execute: the rest of the function (even if it's just the function's implicit `return` statement). + +Here's a simplified example: + +```javascript +async function example() { + await doSomething(); // `before` hook called here, then `after` hook when promise is resolved + await doSomethingElse(); // `before` hook called here, then `after` hook when promise is resolved + await doOneLastThing(); // `before` hook called here, then `after` hook when promise is resolved + // Implicit return statement here +} +``` + +In this example, the `before` and `after` hooks are called for each `await`, including the last one. Even though there's no explicit code after the last `await`, there's still an implicit `return` statement that ends the function, and the `after` hook would be called before this `return` statement. + +That said, it's important to remember that the `before` and `after` hooks are a somewhat low-level feature that provide visibility into the execution of asynchronous operations. They can be useful for debugging and understanding the flow of execution in complex applications, but they don't necessarily provide a complete picture of what your code is doing. + +## After each `await` there is a new Async Resource + +When you use `await` in JavaScript (and thus in Node.js), you're essentially pausing the execution of async function and waiting for a Promise to be resolved or rejected. The function execution is resumed with the resolved value of the Promise, or throws an error if the Promise was rejected. + +During the pause in execution, control is given back to the JavaScript runtime, which can go on to handle other tasks. This is what allows JavaScript to handle multiple tasks "at the same time" despite being single-threaded; while one async function is paused, waiting for a Promise, another function can run. + +When you use `await`, a new async operation is created, which is why you see a new async resource for each `await` in Async Hooks. The operation is completed when the Promise that you're waiting for is resolved or rejected. The init hook in Async Hooks is called when the operation is created (i.e., when `await` is encountered), and the destroy hook is called when the operation is completed (i.e., when the Promise is resolved or rejected). + +It's also worth noting that each `await` creates a new microtask. The JavaScript event loop handles tasks and microtasks differently: a new task can't start until the current task and all of its associated microtasks have completed. By creating a new microtask with each `await`, JavaScript ensures that async functions behave predictably: the function will always resume where it left off, even if other tasks or functions have run in the meantime. + +> To summarize, each `await` results in a new async operation (and thus a new async resource in Async Hooks) because `await` pauses the execution of the current async function and allows the JavaScript runtime to handle other tasks. This new async operation is completed when the Promise that's being awaited is resolved or rejected. This mechanism allows JavaScript to handle multiple tasks "at the same time" despite being single-threaded. + +## In an Async Function, the first section of the code up to the first `await` is actually synchronous + +The execution of an Async Function (up to the first `await` expression) is indeed synchronous. When an async function is called, it runs synchronously until it encounters an `await` expression. At that point, it yields execution back to the caller and waits for the Promise to either resolve or reject. + +Here's a simple example: + +```js +async function example() { + console.log('1. This is synchronous'); + await new Promise(resolve => setTimeout(resolve, 1000)); + console.log('2. This is asynchronous'); +} + +console.log('Start'); +example(); +console.log('End'); +``` + +When you run this code, you will see the following output: + +``` +Start +1. This is synchronous +End +2. This is asynchronous +``` + +The call to `example()` runs synchronously until it encounters the `await` expression, at which point it pauses and allows the rest of the synchronous code to run. Then, after the Promise resolves (after one second in this case), the async function resumes and runs the rest of its code. + +## The first part of an Async Function has the same Async ID as the caller + +The initial **synchronous** part of an async function (up to the first `await`) is executed in the same turn of the event loop as the calling code. This means it shares the same async context, so it has the same Async ID as the parent in the context of Node.js async hooks. + +When the async function hits an `await` statement, a new asynchronous operation is created, and it will be given a new async id. This is why you see a different Async ID after an `await` statement. + +To put it simply, everything in an async function before the first `await` is executed in the same context (and thus has the same Async ID) as the calling code. After an `await`, a new context (and a new Async ID) is created. + +> In Node.js, when an async resource has the same triggerAsyncId as the executionAsyncId, what are the assumptions I can make of it? + +Async Hooks is a Node.js API that allows developers to monitor and manage asynchronous operations in their applications. There are two important concepts here: + +1. `executionAsyncId`: This ID represents the unique identifier of the asynchronous resource that is currently being executed by the JavaScript engine. This could be a Promise, a setTimeout, etc. + +2. `triggerAsyncId`: This ID represents the unique identifier of the asynchronous resource that caused (or "triggered") the current execution context. This could be the Promise that initiated a .then callback, the setTimeout that initiated its callback, etc. + +If an async resource has the same `triggerAsyncId` as the `executionAsyncId`, it means that the async operation was triggered within the same execution context where it is running. Essentially, it implies that the async operation and the execution context it is part of belong to the same chain of asynchronous operations. + +Here are some assumptions you can make when the `triggerAsyncId` and `executionAsyncId` are the same: + +1. The async operation was not triggered in a different execution context. In other words, it wasn't triggered by an unrelated async operation. For example, if a timer was set within a callback function that was itself triggered by another async operation, then the `triggerAsyncId` of the timer would be different from the `executionAsyncId`. + +2. The async operation was not triggered by a nested async operation. If a Promise is resolved within another Promise, the `triggerAsyncId` for the inner Promise would be different from the `executionAsyncId`. + +3. The async operation was not triggered by an async operation that has completed its execution. If an async operation is triggered by another one that has already completed, the `triggerAsyncId` would be different from the `executionAsyncId`. + +Please note that the Async Hooks API can be quite complex and difficult to work with, and the above assumptions may not hold in every situation. This is due to the nature of the JavaScript event loop and the way asynchronous operations are scheduled and executed. So while the above assumptions can generally be made, they should be used as guidelines rather than strict rules. diff --git a/lib/async/als.js b/lib/async/als.js new file mode 100644 index 00000000..f7fedafb --- /dev/null +++ b/lib/async/als.js @@ -0,0 +1,38 @@ +import fs from 'fs'; +import util from 'util'; +import { AsyncLocalStorage } from 'node:async_hooks'; + +let id = 0; +export const getId = () => id++; + +export const MontiAsyncStorage = new AsyncLocalStorage(); + +export function createStore () { + return { + id: getId(), + info: null, + activeEvent: null + }; +} + +export const runWithALS = ( + fn, +) => function (...args) { + return MontiAsyncStorage.run(createStore(), () => fn.apply(this, args)); +}; + +export const MontiEnvironmentVariable = new AsyncLocalStorage(); + +export const runWithEnvironment = (fn, store = new Map([['env', {}]])) => function (...args) { + return MontiEnvironmentVariable.run(store, () => fn.apply(this, args)); +}; + +export const getStore = () => MontiAsyncStorage.getStore() ?? {}; + +export const getInfo = () => MontiAsyncStorage.getStore()?.info; + +export const getActiveEvent = (store = MontiAsyncStorage.getStore()) => store?.activeEvent; + +export const debug = label => (...args) => { + fs.writeFileSync(1, `${util.format(label, ...args)}\n`, { flag: 'a' }); +}; diff --git a/lib/async/async-hook.js b/lib/async/async-hook.js new file mode 100644 index 00000000..24b4cc41 --- /dev/null +++ b/lib/async/async-hook.js @@ -0,0 +1,42 @@ +import { EventType } from '../constants'; +import { AwaitDetector } from '@monti-apm/core/dist/await-detector'; + +const EventSymbol = Symbol('monti-agent-event'); + +export const awaitDetector = new AwaitDetector({ + onAwaitStart (promise, info) { + if (!info.trace || info.trace.isEventsProcessed) return; + + let currentInfo = Kadira._getInfo(); + if (currentInfo && currentInfo !== info) return; + + let event = Kadira.tracer.event(info.trace, EventType.Async); + promise[EventSymbol] = event; + }, + onAwaitEnd (promise, info) { + let event = promise[EventSymbol]; + + if (event && !info.trace.isEventsProcessed) { + Kadira.tracer.eventEnd(info.trace, event); + } + }, +}); + +if (Package.promise?.Promise) { + // Forcing new Promise to create a non-native promise makes the promise + // events for awaits consistent and possible to track. + // This will only make packages loaded later use the wrapped promise, other + // packages will unfortunately use the native promise and will not be tracked + // reliably. + Package.promise.Promise = awaitDetector.createWrappedPromiseConstructor( + Package.promise.Promise + ); + + // Make sure we use the wrapped promise too. Needed for tests to work + // This isn't changing the global Promise, and instead is changing the value + // of the Promise variable exported from the promise package. + if (Promise !== global.Promise) { + // eslint-disable-next-line no-native-reassign, no-global-assign + Promise = Package.promise.Promise; + } +} diff --git a/lib/auto_connect.js b/lib/auto_connect.js index be7f20ec..0e5bc389 100644 --- a/lib/auto_connect.js +++ b/lib/auto_connect.js @@ -1,10 +1,13 @@ import { Meteor } from 'meteor/meteor'; +import { connectLogger } from './logger'; const envOptions = Kadira._parseEnv(process.env); const montiSettings = Meteor.settings.monti || Meteor.settings.kadira; Kadira._connectWithEnv = function (env) { + connectLogger(`Checking env: id: "${env.appId}", secret: "${env.appSecret}"`); if (env.appId && env.appSecret) { + connectLogger(`Auto-connecting with env`); Kadira.connect( env.appId, env.appSecret, @@ -14,16 +17,21 @@ Kadira._connectWithEnv = function (env) { Kadira.connect = function () { throw new Error('Monti APM: already connected using credentials from Environment Variables'); }; + } else { + connectLogger(`Not auto-connecting with env`); } }; Kadira._connectWithSettings = function (settings) { + connectLogger(`Checking settings: id: "${settings?.appId}", secret: "${settings?.appSecret}"`); + if ( settings && settings.appId && settings.appSecret ) { + connectLogger('Auto-connecting with settings'); Kadira.connect( settings.appId, settings.appSecret, @@ -33,6 +41,8 @@ Kadira._connectWithSettings = function (settings) { Kadira.connect = function () { throw new Error('Monti APM: already connected using credentials from Meteor.settings'); }; + } else { + connectLogger(`Not auto-connecting with settings`); } }; diff --git a/lib/client/utils.js b/lib/client/utils.js index 58711412..1e0d2121 100644 --- a/lib/client/utils.js +++ b/lib/client/utils.js @@ -92,7 +92,7 @@ export function getClientArch () { export function checkSizeAndPickFields (maxFieldSize) { return function (obj) { maxFieldSize = maxFieldSize || 100; - for (let key in obj) { + for (let key of Object.keys(obj)) { let value = obj[key]; try { let valueStringified = JSON.stringify(value); diff --git a/lib/common/unify.js b/lib/common/unify.js index b50ef3e1..9cb28bfc 100644 --- a/lib/common/unify.js +++ b/lib/common/unify.js @@ -5,11 +5,7 @@ Kadira.options = {}; Monti = Kadira; -if (Meteor.wrapAsync) { - Kadira._wrapAsync = Meteor.wrapAsync; -} else { - Kadira._wrapAsync = Meteor._wrapAsync; -} +Kadira.wrapAsync = Meteor.wrapAsync; if (Meteor.isServer) { const EventEmitter = Npm.require('events').EventEmitter; diff --git a/lib/common/utils.js b/lib/common/utils.js index 4c6717a4..b9ba53a1 100644 --- a/lib/common/utils.js +++ b/lib/common/utils.js @@ -93,6 +93,8 @@ export const getErrorParameters = function (args) { * @returns {boolean} True if the object has set any data which is not `null`, `undefined` or an empty string. */ export const objectHasData = function (obj) { + if (!obj) return false; + return Object.values(obj).some(val => ![null, undefined, ''].includes(val)); }; @@ -131,3 +133,8 @@ export const millisToHuman = function (milliseconds) { return builder.join(' '); }; + + +export const isNumber = n => typeof n === 'number' && !isNaN(n); + +export const isString = s => typeof s === 'string'; diff --git a/lib/constants.js b/lib/constants.js index b45db6fa..fb04ca6a 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,3 +1,21 @@ +export const MaxAsyncLevel = 2; + +export const DEBUG_ASYNC_HOOKS = false; + +export const EventType = { + Async: 'async', + Complete: 'complete', + Compute: 'compute', + Custom: 'custom', + DB: 'db', + Error: 'error', + FS: 'fs', + HTTP: 'http', + Start: 'start', + Wait: 'wait', + Email: 'email', +}; + export const JobType = { CPU_PROFILE: 'cpuProfile', HEAP_SNAPSHOT: 'heapSnapshot', diff --git a/lib/docsize_cache.js b/lib/docsize_cache.js index bbade40a..62dcbe42 100644 --- a/lib/docsize_cache.js +++ b/lib/docsize_cache.js @@ -1,5 +1,5 @@ -let LRU = Npm.require('lru-cache'); -let jsonStringify = Npm.require('json-stringify-safe'); +let LRU = require('lru-cache'); +let jsonStringify = require('json-stringify-safe'); export const DocSzCache = function (maxItems, maxValues) { this.items = new LRU({max: maxItems}); diff --git a/lib/environment_variables.js b/lib/environment_variables.js index 020a0eb1..f608c714 100644 --- a/lib/environment_variables.js +++ b/lib/environment_variables.js @@ -4,7 +4,8 @@ function normalizedPrefix (name) { Kadira._parseEnv = function (env) { let options = {}; - for (let name in env) { + + for (let name of Object.keys(env)) { let value = env[name]; let normalizedName = normalizedPrefix(name); let info = Kadira._parseEnv._options[normalizedName]; diff --git a/lib/event_loop_monitor.js b/lib/event_loop_monitor.js index 31aa94e4..709d9e36 100644 --- a/lib/event_loop_monitor.js +++ b/lib/event_loop_monitor.js @@ -92,7 +92,7 @@ export class EventLoopMonitor extends EventEmitter { performance // eslint-disable-next-line global-require } = require('perf_hooks'); - this._now = performance.now; + this._now = performance.now.bind(performance); return; } diff --git a/lib/hijack/async.js b/lib/hijack/async.js deleted file mode 100644 index 44beb6cd..00000000 --- a/lib/hijack/async.js +++ /dev/null @@ -1,122 +0,0 @@ -let Fibers = Npm.require('fibers'); -let EventSymbol = Symbol('MontiEventSymbol'); -let StartTracked = Symbol('MontiStartTracked'); - -let activeFibers = 0; -let wrapped = false; - -function endAsyncEvent (fiber) { - if (!fiber[EventSymbol]) return; - - const kadiraInfo = Kadira._getInfo(fiber); - - if (!kadiraInfo) return; - - Kadira.tracer.eventEnd(kadiraInfo.trace, fiber[EventSymbol]); - - fiber[EventSymbol] = null; -} - -export function wrapFibers () { - if (wrapped) { - return; - } - wrapped = true; - - let originalYield = Fibers.yield; - - Fibers.yield = function () { - let kadiraInfo = Kadira._getInfo(); - - if (kadiraInfo) { - let eventId = Kadira.tracer.event(kadiraInfo.trace, 'async'); - - if (eventId) { - // The event unique to this fiber - // Using a symbol since Meteor doesn't copy symbols to new fibers created - // for promises. This is needed so the correct event is ended when a fiber runs after being yielded. - Fibers.current[EventSymbol] = eventId; - } - } - - return originalYield(); - }; - - let originalRun = Fibers.prototype.run; - let originalThrowInto = Fibers.prototype.throwInto; - - function ensureFiberCounted (fiber) { - // If fiber.started is true, and StartTracked is false - // then the fiber was probably initially ran before we wrapped Fibers.run - if (!fiber.started || !fiber[StartTracked]) { - activeFibers += 1; - fiber[StartTracked] = true; - } - } - - Fibers.prototype.run = function (val) { - ensureFiberCounted(this); - - if (this[EventSymbol]) { - endAsyncEvent(this); - } else if (!this.__kadiraInfo && Fibers.current && Fibers.current.__kadiraInfo) { - // Copy kadiraInfo when packages or user code creates a new fiber - // Done by many apps and packages in connect middleware since older - // versions of Meteor did not do it automatically - this.__kadiraInfo = Fibers.current.__kadiraInfo; - } - - let result; - try { - result = originalRun.call(this, val); - } finally { - if (!this.started) { - activeFibers -= 1; - this[StartTracked] = false; - } - } - - return result; - }; - - Fibers.prototype.throwInto = function (val) { - ensureFiberCounted(this); - endAsyncEvent(this); - - let result; - - try { - result = originalThrowInto.call(this, val); - } finally { - if (!this.started) { - activeFibers -= 1; - this[StartTracked] = false; - } - } - - return result; - }; -} - -let activeFiberTotal = 0; -let activeFiberCount = 0; -let previousTotalCreated = 0; - -setInterval(() => { - activeFiberTotal += activeFibers; - activeFiberCount += 1; -}, 1000); - -export function getFiberMetrics () { - return { - created: Fibers.fibersCreated - previousTotalCreated, - active: activeFiberTotal / activeFiberCount, - poolSize: Fibers.poolSize - }; -} - -export function resetFiberMetrics () { - activeFiberTotal = 0; - activeFiberCount = 0; - previousTotalCreated = Fibers.fibersCreated; -} diff --git a/lib/hijack/db.js b/lib/hijack/db.js deleted file mode 100644 index 1b3d43b2..00000000 --- a/lib/hijack/db.js +++ /dev/null @@ -1,280 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; -import { Mongo, MongoInternals } from 'meteor/mongo'; -import { _ } from 'meteor/underscore'; -import { haveAsyncCallback, OptimizedApply } from '../utils'; - - -// This hijack is important to make sure, collections created before -// we hijack dbOps, even gets tracked. -// Meteor does not simply expose MongoConnection object to the client -// It picks methods which are necessary and make a binded object and -// assigned to the Mongo.Collection -// so, even we updated prototype, we can't track those collections -// but, this will fix it. - -let originalOpen = MongoInternals.RemoteCollectionDriver.prototype.open; -MongoInternals.RemoteCollectionDriver.prototype.open = function open (name) { - let self = this; - let ret = originalOpen.call(self, name); - - _.each(ret, function (fn, m) { - // make sure, it's in the actual mongo connection object - // meteorhacks:mongo-collection-utils package add some arbitary methods - // which does not exist in the mongo connection - if (self.mongo[m]) { - ret[m] = function () { - Array.prototype.unshift.call(arguments, name); - return OptimizedApply(self.mongo, self.mongo[m], arguments); - }; - } - }); - - return ret; -}; - -// TODO: this should be added to Meteorx -function getSyncronousCursor () { - const MongoColl = typeof Mongo !== 'undefined' ? Mongo.Collection : Meteor.Collection; - const coll = new MongoColl(`__dummy_coll_${Random.id()}`); - // we need to wait until the db is connected with meteor. findOne does that - coll.findOne(); - - const cursor = coll.find(); - cursor.fetch(); - return cursor._synchronousCursor.constructor; -} - -export function hijackDBOps () { - let mongoConnectionProto = MongoInternals.Connection.prototype; - - // findOne is handled by find - so no need to track it - // upsert is handles by update - // 2.4 replaced _ensureIndex with createIndex - [ - 'update', 'remove', 'insert', '_ensureIndex', '_dropIndex', 'createIndex' - ].forEach(function (func) { - let originalFunc = mongoConnectionProto[func]; - - if (!originalFunc) { - return; - } - - mongoConnectionProto[func] = function (collName, selector, mod, options) { - let payload = { - coll: collName, - func, - }; - - if (func === 'insert') { - // add nothing more to the payload - } else if (func === '_ensureIndex' || func === '_dropIndex' || func === 'createIndex') { - // add index - payload.index = JSON.stringify(selector); - } else if (func === 'update' && options && options.upsert) { - payload.func = 'upsert'; - payload.selector = JSON.stringify(selector); - } else { - // all the other functions have selectors - payload.selector = JSON.stringify(selector); - } - - let kadiraInfo = Kadira._getInfo(); - - let eventId; - - if (kadiraInfo) { - eventId = Kadira.tracer.event(kadiraInfo.trace, 'db', payload); - } - - // this cause V8 to avoid any performance optimizations, but this is must use - // otherwise, if the error adds try catch block our logs get messy and didn't work - // see: issue #6 - - let ret; - - try { - ret = originalFunc.apply(this, arguments); - // handling functions which can be triggered with an asyncCallback - let endOptions = {}; - - if (haveAsyncCallback(arguments)) { - endOptions.async = true; - } - - if (func === 'update') { - // upsert only returns an object when called `upsert` directly - // otherwise it only act an update command - if (options && options.upsert && typeof ret === 'object') { - endOptions.updatedDocs = ret.numberAffected; - endOptions.insertedId = ret.insertedId; - } else { - endOptions.updatedDocs = ret; - } - } else if (func === 'remove') { - endOptions.removedDocs = ret; - } - - if (eventId) { - Kadira.tracer.eventEnd(kadiraInfo.trace, eventId, endOptions); - } - } catch (ex) { - if (eventId) { - Kadira.tracer.eventEnd(kadiraInfo.trace, eventId, {err: ex.message}); - } - throw ex; - } - - return ret; - }; - }); - - let cursorProto = MeteorX.MongoCursor.prototype; - ['forEach', 'map', 'fetch', 'count', 'observeChanges', 'observe'].forEach(function (type) { - let originalFunc = cursorProto[type]; - cursorProto[type] = function () { - let cursorDescription = this._cursorDescription; - let payload = Object.assign(Object.create(null), { - coll: cursorDescription.collectionName, - selector: JSON.stringify(cursorDescription.selector), - func: type, - cursor: true - }); - - if (cursorDescription.options) { - const opts = cursorDescription.options; - - const projection = opts.fields || opts.projection; - - let cursorOptions = _.pick(cursorDescription.options, ['sort', 'limit']); - - if (projection) { - cursorOptions.projection = projection; - } - - for (let field in cursorOptions) { - let value = cursorOptions[field]; - if (typeof value === 'object') { - value = JSON.stringify(value); - } - payload[field] = value; - } - } - - let kadiraInfo = Kadira._getInfo(); - let previousTrackNextObject; - let eventId; - - if (kadiraInfo) { - eventId = Kadira.tracer.event(kadiraInfo.trace, 'db', payload); - - previousTrackNextObject = kadiraInfo.trackNextObject; - if (type === 'forEach' || type === 'map') { - kadiraInfo.trackNextObject = true; - } - } - - try { - let ret = originalFunc.apply(this, arguments); - - let endData = {}; - - if (type === 'observeChanges' || type === 'observe') { - let observerDriver; - endData.oplog = false; - // get data written by the multiplexer - endData.wasMultiplexerReady = ret._wasMultiplexerReady; - endData.queueLength = ret._queueLength; - endData.elapsedPollingTime = ret._elapsedPollingTime; - - if (ret._multiplexer) { - // older meteor versions done not have an _multiplexer value - observerDriver = ret._multiplexer._observeDriver; - if (observerDriver) { - observerDriver = ret._multiplexer._observeDriver; - let observerDriverClass = observerDriver.constructor; - endData.oplog = typeof observerDriverClass.cursorSupported === 'function'; - - let size = 0; - ret._multiplexer._cache.docs.forEach(function () { - size++; - }); - endData.noOfCachedDocs = size; - - // if multiplexerWasNotReady, we need to get the time spend for the polling - if (!ret._wasMultiplexerReady) { - endData.initialPollingTime = observerDriver._lastPollTime; - } - } - } - - if (!endData.oplog) { - // let's try to find the reason - let reasonInfo = Kadira.checkWhyNoOplog(cursorDescription, observerDriver); - endData.noOplogCode = reasonInfo.code; - endData.noOplogReason = reasonInfo.reason; - endData.noOplogSolution = reasonInfo.solution; - } - } else if (type === 'fetch' || type === 'map') { - // for other cursor operation - - endData.docsFetched = ret.length; - - if (type === 'fetch') { - let coll = cursorDescription.collectionName; - let query = cursorDescription.selector; - let opts = cursorDescription.options; - let docSize = Kadira.docSzCache.getSize(coll, query, opts, ret) * ret.length; - endData.docSize = docSize; - - if (kadiraInfo) { - if (kadiraInfo.trace.type === 'method') { - Kadira.models.methods.trackDocSize(kadiraInfo.trace.name, docSize); - } else if (kadiraInfo.trace.type === 'sub') { - Kadira.models.pubsub.trackDocSize(kadiraInfo.trace.name, 'cursorFetches', docSize); - } - - kadiraInfo.trackNextObject = previousTrackNextObject; - } else { - // Fetch with no kadira info are tracked as from a null method - Kadira.models.methods.trackDocSize('', docSize); - } - - // TODO: Add doc size tracking to `map` as well. - } - } - - if (eventId) { - Kadira.tracer.eventEnd(kadiraInfo.trace, eventId, endData); - } - return ret; - } catch (ex) { - if (eventId) { - Kadira.tracer.eventEnd(kadiraInfo.trace, eventId, {err: ex.message}); - } - throw ex; - } - }; - }); - - const SyncronousCursor = getSyncronousCursor(); - let origNextObject = SyncronousCursor.prototype._nextObject; - SyncronousCursor.prototype._nextObject = function () { - let kadiraInfo = Kadira._getInfo(); - let shouldTrack = kadiraInfo && kadiraInfo.trackNextObject; - let event; - if (shouldTrack ) { - event = Kadira.tracer.event(kadiraInfo.trace, 'db', { - func: '_nextObject', - coll: this._cursorDescription.collectionName - }); - } - - let result = origNextObject.call(this); - - if (shouldTrack) { - Kadira.tracer.eventEnd(kadiraInfo.trace, event); - } - return result; - }; -} diff --git a/lib/hijack/db/calculate-metrics.js b/lib/hijack/db/calculate-metrics.js new file mode 100644 index 00000000..133b92e4 --- /dev/null +++ b/lib/hijack/db/calculate-metrics.js @@ -0,0 +1,20 @@ +export function calculateMetrics (cursorDescription, result, endData, kadiraInfo, previousTrackNextObject) { + let coll = cursorDescription.collectionName; + let query = cursorDescription.selector; + let opts = cursorDescription.options; + let docSize = Kadira.docSzCache.getSize(coll, query, opts, result) * result.length; + endData.docSize = docSize; + + if (kadiraInfo) { + if (kadiraInfo.trace.type === 'method') { + Kadira.models.methods.trackDocSize(kadiraInfo.trace.name, docSize); + } else if (kadiraInfo.trace.type === 'sub') { + Kadira.models.pubsub.trackDocSize(kadiraInfo.trace.name, 'cursorFetches', docSize); + } + + kadiraInfo.trackNextObject = previousTrackNextObject; + } else { + // Fetch with no kadira info are tracked as from a null method + Kadira.models.methods.trackDocSize('', docSize); + } +} diff --git a/lib/hijack/db/get-cursor-data.js b/lib/hijack/db/get-cursor-data.js new file mode 100644 index 00000000..19a8e1a5 --- /dev/null +++ b/lib/hijack/db/get-cursor-data.js @@ -0,0 +1,26 @@ +export function getCursorData ({ type, cursor }) { + const cursorDescription = cursor._cursorDescription; + + const payload = Object.assign(Object.create(null), { + coll: cursorDescription.collectionName, + selector: JSON.stringify(cursorDescription.selector), + func: type, + cursor: true + }); + + if (cursorDescription.options) { + const fields = ['fields', 'projection', 'sort', 'limit']; + + for (let field of fields) { + let value = cursorDescription.options[field]; + + if (typeof value === 'object') { + value = JSON.stringify(value); + } + + payload[field] = value; + } + } + + return { payload, cursorDescription }; +} diff --git a/lib/hijack/db/hijack-async-methods.js b/lib/hijack/db/hijack-async-methods.js new file mode 100644 index 00000000..3707ee77 --- /dev/null +++ b/lib/hijack/db/hijack-async-methods.js @@ -0,0 +1,66 @@ +import { EventType } from '../../constants'; + +export function hijackAsyncMethods () { + let mongoConnectionProto = MeteorX.MongoConnection.prototype; + + const ASYNC_METHODS = [ + 'insertAsync', + 'removeAsync', + 'updateAsync', + 'createIndexAsync', + 'dropIndexAsync', + 'ensureIndex', + ]; + + // findOne is handled by find - so no need to track it + // upsert is handles by update + ASYNC_METHODS.forEach(function (func) { + let originalFunc = mongoConnectionProto[func]; + + if (!originalFunc) { + return; + } + + mongoConnectionProto[func] = function (collName, selector, mod, options) { + let payload = { + coll: collName, + func, + }; + + if (func === 'insertAsync') { + // add nothing more to the payload + } else if (['dropIndexAsync', 'createIndexAsync'].includes(func)) { + // add index + payload.index = JSON.stringify(selector); + } else if (func === 'updateAsync' && options && options.upsert) { + payload.func = 'upsert'; + payload.selector = JSON.stringify(selector); + } else { + // all the other functions have selectors + payload.selector = JSON.stringify(selector); + } + + return Kadira.tracer.asyncEvent(EventType.DB, payload, async (event) => { + const result = await originalFunc.apply(this, arguments); + let endData = {}; + + if (func === 'updateAsync') { + // upsert only returns an object when called `upsert` directly + // otherwise it only act an update command + if (options && options.upsert && typeof result === 'object') { + endData.updatedDocs = result.numberAffected; + endData.insertedId = result.insertedId; + } else { + endData.updatedDocs = result; + } + } else if (func === 'removeAsync') { + endData.removedDocs = result; + } + + Kadira.tracer.asyncEventEnd(event, endData); + + return result; + }); + }; + }); +} diff --git a/lib/hijack/db/hijack-cursor-methods.js b/lib/hijack/db/hijack-cursor-methods.js new file mode 100644 index 00000000..e740c0cc --- /dev/null +++ b/lib/hijack/db/hijack-cursor-methods.js @@ -0,0 +1,57 @@ +import { EventType } from '../../constants'; +import { getCursorData } from './get-cursor-data'; +import { calculateMetrics } from './calculate-metrics'; + +export function hijackCursorMethods () { + const cursorProto = MeteorX.MongoCursor.prototype; + + // `fetchAsync` calls `fetch` behind the scenes, so we only need to intercept `fetch`, which is also called by `findOneAsync`. + const ASYNC_CURSOR_METHODS = ['fetch', 'forEach', 'map', 'countAsync']; + + ASYNC_CURSOR_METHODS.forEach(function (type) { + let originalFunc = cursorProto[type]; + + if (!originalFunc) { + return; + } + + cursorProto[type] = function () { + const { payload, cursorDescription } = getCursorData({ + type, + cursor: this, + }); + + return Kadira.tracer.asyncEvent(EventType.DB, payload, async (event) => { + let kadiraInfo = Kadira._getInfo(); + + let previousTrackNextObject; + + if (kadiraInfo) { + previousTrackNextObject = kadiraInfo.trackNextObject; + + if (['forEach', 'map'].includes(type)) { + kadiraInfo.trackNextObject = true; + } + } + + const result = await originalFunc.apply(this, arguments); + + let endData = {}; + + if (['fetch', 'map'].includes(type)) { + // for other cursor operation + endData.docsFetched = result.length; + + if (type === 'fetch') { + calculateMetrics(cursorDescription, result, endData, kadiraInfo, previousTrackNextObject); + + // TODO: Add doc size tracking to `map` as well. + } + } + + Kadira.tracer.asyncEventEnd(event, endData); + return result; + }); + }; + }); +} diff --git a/lib/hijack/db/hijack-next-object.js b/lib/hijack/db/hijack-next-object.js new file mode 100644 index 00000000..e56d9311 --- /dev/null +++ b/lib/hijack/db/hijack-next-object.js @@ -0,0 +1,28 @@ +import { EventType } from '../../constants'; + +export function hijackNextObject () { + const SynchronousCursor = MeteorX.SynchronousCursor; + + let origNextObject = SynchronousCursor.prototype._nextObjectPromise; + + SynchronousCursor.prototype._nextObjectPromise = async function () { + let kadiraInfo = Kadira._getInfo(); + let shouldTrack = kadiraInfo && kadiraInfo.trackNextObject; + let event; + + if (shouldTrack) { + event = Kadira.tracer.event(kadiraInfo.trace, EventType.DB, { + func: '_nextObjectPromise', + coll: this._cursorDescription.collectionName + }); + } + + let result = await origNextObject.call(this); + + if (shouldTrack) { + Kadira.tracer.eventEnd(kadiraInfo.trace, event); + } + + return result; + }; +} diff --git a/lib/hijack/db/hijack-observe-methods.js b/lib/hijack/db/hijack-observe-methods.js new file mode 100644 index 00000000..e16407b7 --- /dev/null +++ b/lib/hijack/db/hijack-observe-methods.js @@ -0,0 +1,63 @@ +import { EventType } from '../../constants'; +import { getCursorData } from './get-cursor-data'; + +export function hijackObserveMethods () { + const cursorProto = MeteorX.MongoCursor.prototype; + + // Right now they are async in the server and sync in the client, but it might change. + const OBSERVE_METHODS = ['observeChanges', 'observe']; + + OBSERVE_METHODS.forEach(function (type) { + let originalFunc = cursorProto[type]; + + cursorProto[type] = function () { + const { payload, cursorDescription } = getCursorData({ + type, + cursor: this, + }); + + return Kadira.tracer.asyncEvent(EventType.DB, payload, async (event) => { + let ret = await originalFunc.apply(this, arguments); + + let endData = {}; + + let observerDriver; + + endData.oplog = false; + // get data written by the multiplexer + endData.wasMultiplexerReady = ret._wasMultiplexerReady; + endData.queueLength = ret._queueLength; + endData.elapsedPollingTime = ret._elapsedPollingTime; + + if (ret._multiplexer) { + // older meteor versions done not have an _multiplexer value + observerDriver = ret._multiplexer._observeDriver; + if (observerDriver) { + observerDriver = ret._multiplexer._observeDriver; + let observerDriverClass = observerDriver.constructor; + endData.oplog = typeof observerDriverClass.cursorSupported === 'function'; + + endData.noOfCachedDocs = ret._multiplexer._cache.docs._map.size; + + // if multiplexerWasNotReady, we need to get the time spend for the polling + if (!ret._wasMultiplexerReady) { + endData.initialPollingTime = observerDriver._lastPollTime; + } + } + } + + if (!endData.oplog) { + // let's try to find the reason + let reasonInfo = Kadira.checkWhyNoOplog(cursorDescription, observerDriver); + endData.noOplogCode = reasonInfo.code; + endData.noOplogReason = reasonInfo.reason; + endData.noOplogSolution = reasonInfo.solution; + } + + Kadira.tracer.asyncEventEnd(event, endData); + + return ret; + }); + }; + }); +} diff --git a/lib/hijack/db/index.js b/lib/hijack/db/index.js new file mode 100644 index 00000000..9845c3d7 --- /dev/null +++ b/lib/hijack/db/index.js @@ -0,0 +1,47 @@ +import { MongoInternals } from 'meteor/mongo'; +import { _ } from 'meteor/underscore'; +import { optimizedApply } from '../../utils'; +import { hijackAsyncMethods } from './hijack-async-methods'; +import { hijackObserveMethods } from './hijack-observe-methods'; +import { hijackCursorMethods } from './hijack-cursor-methods'; +import { hijackNextObject } from './hijack-next-object'; + + +// This hijack is important to make sure, collections created before +// we hijack dbOps, even gets tracked. +// Meteor does not simply expose MongoConnection object to the client +// It picks methods which are necessary and make a binded object and +// assigned to the Mongo.Collection +// so, even we updated prototype, we can't track those collections +// but, this will fix it. + +let originalOpen = MongoInternals.RemoteCollectionDriver.prototype.open; + +MongoInternals.RemoteCollectionDriver.prototype.open = function open (name) { + let self = this; + let ret = originalOpen.call(self, name); + + _.each(ret, function (fn, m) { + // make sure, it's in the actual mongo connection object + // meteorhacks:mongo-collection-utils package add some arbitary methods + // which does not exist in the mongo connection + if (self.mongo[m]) { + ret[m] = function () { + Array.prototype.unshift.call(arguments, name); + return optimizedApply(self.mongo, self.mongo[m], arguments); + }; + } + }); + + return ret; +}; + +export function hijackDBOps () { + hijackAsyncMethods(); + + hijackObserveMethods(); + + hijackCursorMethods(); + + hijackNextObject(); +} diff --git a/lib/hijack/email.js b/lib/hijack/email.js index 53b6eb76..04a6acf0 100644 --- a/lib/hijack/email.js +++ b/lib/hijack/email.js @@ -1,30 +1,12 @@ import { pick } from '../utils'; +import { EventType } from '../constants'; const CAPTURED_OPTIONS = ['from', 'to', 'cc', 'bcc', 'replyTo', 'messageId']; const getWrapper = (originalSend, func) => function wrapper (options) { - let eventId; - const kadiraInfo = Kadira._getInfo(); + const data = { ...pick(options, CAPTURED_OPTIONS), func }; - if (kadiraInfo) { - const data = pick(options, CAPTURED_OPTIONS); - data.func = func; - - eventId = Kadira.tracer.event(kadiraInfo.trace, 'email', data); - } - - try { - const ret = originalSend.call(this, options); - if (eventId) { - Kadira.tracer.eventEnd(kadiraInfo.trace, eventId); - } - return ret; - } catch (ex) { - if (eventId) { - Kadira.tracer.eventEnd(kadiraInfo.trace, eventId, {err: ex.message}); - } - throw ex; - } + return Kadira.tracer.asyncEvent(EventType.Email, data, () => originalSend.call(this, options)); }; if (Package['email']) { diff --git a/lib/hijack/fs.js b/lib/hijack/fs.js index d250f110..0e852df4 100644 --- a/lib/hijack/fs.js +++ b/lib/hijack/fs.js @@ -1,5 +1,5 @@ import fs from 'fs'; -const Fibers = require('fibers'); +import { EventType } from '../constants'; function wrapCallback (args, createWrapper) { if (typeof args[args.length - 1] === 'function') { @@ -27,18 +27,12 @@ export function handleErrorEvent (eventEmitter, trace, event) { } export function wrapFs () { - // Some npm packages will do fs calls in the - // callback of another fs call. - // This variable is set with the kadiraInfo while - // a callback is run so we can track other fs calls - let fsKadiraInfo = null; - let originalStat = fs.stat; fs.stat = function () { - const kadiraInfo = Kadira._getInfo() || fsKadiraInfo; + const kadiraInfo = Kadira._getInfo(); if (kadiraInfo) { - let event = Kadira.tracer.event(kadiraInfo.trace, 'fs', { + let event = Kadira.tracer.event(kadiraInfo.trace, EventType.FS, { func: 'stat', path: arguments[0], options: typeof arguments[1] === 'object' ? arguments[1] : undefined @@ -47,15 +41,7 @@ export function wrapFs () { wrapCallback(arguments, (cb) => function () { Kadira.tracer.eventEnd(kadiraInfo.trace, event); - if (!Fibers.current) { - fsKadiraInfo = kadiraInfo; - } - - try { - cb(...arguments); - } finally { - fsKadiraInfo = null; - } + cb(...arguments); }); } @@ -64,11 +50,11 @@ export function wrapFs () { let originalCreateReadStream = fs.createReadStream; fs.createReadStream = function () { - const kadiraInfo = Kadira._getInfo() || fsKadiraInfo; + const kadiraInfo = Kadira._getInfo(); let stream = originalCreateReadStream.apply(this, arguments); if (kadiraInfo) { - const event = Kadira.tracer.event(kadiraInfo.trace, 'fs', { + const event = Kadira.tracer.event(kadiraInfo.trace, EventType.FS, { func: 'createReadStream', path: arguments[0], options: JSON.stringify(arguments[1]) diff --git a/lib/hijack/gc.js b/lib/hijack/gc.js index e1920840..3068d7ae 100644 --- a/lib/hijack/gc.js +++ b/lib/hijack/gc.js @@ -35,7 +35,8 @@ export default class GCMetrics { this.observer = new PerformanceObserver(list => { list.getEntries().forEach(entry => { - let metric = this._mapKindToMetric(entry.kind); + let metric = this._mapKindToMetric(entry.detail?.kind); + this.metrics[metric] += entry.duration; }); diff --git a/lib/hijack/http.js b/lib/hijack/http.js index d249161b..0fe82411 100644 --- a/lib/hijack/http.js +++ b/lib/hijack/http.js @@ -1,7 +1,8 @@ /* eslint-disable prefer-rest-params */ -import { haveAsyncCallback } from '../utils'; +import { haveAsyncCallback, waitForPackage } from '../utils'; +import { EventType } from '../constants'; -if (Package['http']) { +waitForPackage('http', () => { const HTTP = Package['http'].HTTP; const library = 'meteor/http'; const originalCall = HTTP.call; @@ -10,7 +11,7 @@ if (Package['http']) { const tracer = Kadira.tracer; const kadiraInfo = Kadira._getInfo(); - const event = kadiraInfo ? tracer.event(kadiraInfo.trace, 'http', { + const event = kadiraInfo ? tracer.event(kadiraInfo.trace, EventType.HTTP, { method, url, library, @@ -21,12 +22,25 @@ if (Package['http']) { } try { + if (haveAsyncCallback(arguments)) { + const originalCallback = arguments[arguments.length - 1]; + + arguments[arguments.length - 1] = function (err, response) { + if (err) { + tracer.eventEnd(kadiraInfo.trace, event, { err: err.message }); + } else { + tracer.eventEnd(kadiraInfo.trace, event, { statusCode: response.statusCode, async: true }); + } + + originalCallback.apply(this, arguments); + }; + + return originalCall.apply(this, arguments); + } + const response = originalCall.apply(this, arguments); - // If the user supplied an asyncCallback, - // we don't have a response object and it handled asynchronously. - // We need to track it down to prevent issues like: #3 - const endOptions = haveAsyncCallback(arguments) ? { async: true } : { statusCode: response.statusCode }; + const endOptions = { statusCode: response.statusCode }; tracer.eventEnd(kadiraInfo.trace, event, endOptions); @@ -37,7 +51,7 @@ if (Package['http']) { throw ex; } }; -} +}); if (Package['fetch']) { const library = 'meteor/fetch'; @@ -49,7 +63,7 @@ if (Package['fetch']) { const tracer = Kadira.tracer; const kadiraInfo = Kadira._getInfo(); - const event = kadiraInfo ? tracer.event(kadiraInfo.trace, 'http', { + const event = kadiraInfo ? tracer.event(kadiraInfo.trace, EventType.HTTP, { method: request.method, url: request.url, library, diff --git a/lib/hijack/instrument.js b/lib/hijack/instrument.js index ffe60cf7..5cd9f7c4 100644 --- a/lib/hijack/instrument.js +++ b/lib/hijack/instrument.js @@ -3,7 +3,6 @@ import { wrapFastRender } from './fast_render.js'; import { wrapFs } from './fs.js'; import { wrapPicker } from './picker.js'; import { wrapRouters } from './wrap_routers.js'; -import { wrapFibers } from './async.js'; import { wrapSubscription } from './wrap_subscription'; import { wrapServer } from './wrap_server'; import { wrapSession } from './wrap_session'; @@ -29,7 +28,6 @@ Kadira._startInstrumenting = function (callback = () => {}) { } instrumented = true; - wrapFibers(); wrapStringifyDDP(); wrapWebApp(); wrapFastRender(); diff --git a/lib/hijack/mongo_driver_events.js b/lib/hijack/mongo_driver_events.js index c40e73cb..9173f56e 100644 --- a/lib/hijack/mongo_driver_events.js +++ b/lib/hijack/mongo_driver_events.js @@ -26,7 +26,7 @@ setInterval(() => { }, 1000); // Version 4 of the driver defaults to 100. Older versions used 10. -let DEFAULT_MAX_POOL_SIZE = 100; +const DEFAULT_MAX_POOL_SIZE = 100; function getPoolSize () { if (client && client.topology && client.topology.s && client.topology.s.options) { diff --git a/lib/hijack/picker.js b/lib/hijack/picker.js index 307d5164..a5db601b 100644 --- a/lib/hijack/picker.js +++ b/lib/hijack/picker.js @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import Fiber from 'fibers'; +import { runWithALS } from '../async/als'; export function wrapPicker () { Meteor.startup(() => { @@ -13,21 +13,14 @@ export function wrapPicker () { // handler in a Fiber with __kadiraInfo set // Needed if any previous middleware called `next` outside of a fiber. const origProcessRoute = Picker.constructor.prototype._processRoute; - Picker.constructor.prototype._processRoute = function (callback, params, req) { + Picker.constructor.prototype._processRoute = runWithALS(function (callback, params, req) { const args = arguments; - if (!Fiber.current) { - return new Fiber(() => { - Kadira._setInfo(req.__kadiraInfo); - return origProcessRoute.apply(this, args); - }).run(); - } - if (req.__kadiraInfo) { Kadira._setInfo(req.__kadiraInfo); } return origProcessRoute.apply(this, args); - }; + }); }); } diff --git a/lib/hijack/redis_oplog.js b/lib/hijack/redis_oplog.js index 205ea60d..22018201 100644 --- a/lib/hijack/redis_oplog.js +++ b/lib/hijack/redis_oplog.js @@ -1,8 +1,8 @@ import { Random } from 'meteor/random'; -export function getDummyCollectionName () { +export async function getDummyCollectionName () { const collection = new Meteor.Collection(`__dummy_coll_${Random.id()}`); - const handler = collection.find({}).observeChanges({ added: Function.prototype }); + const handler = await collection.find({}).observeChanges({ added: Function.prototype }); handler.stop(); return collection._name; } @@ -40,8 +40,8 @@ export function rewriteReloadRequeryFuncs (observableCollection) { }; } -export function wrapRedisOplogObserveDriver (driver) { - const collectionName = getDummyCollectionName(); +export async function wrapRedisOplogObserveDriver (driver) { + const collectionName = await getDummyCollectionName(); const dummyDriver = new driver({ cursorDescription: { @@ -52,6 +52,7 @@ export function wrapRedisOplogObserveDriver (driver) { multiplexer: { ready () {} }, matcher: { combineIntoProjection: () => ({}) }, }); + await dummyDriver.init(); const observableCollectionProto = dummyDriver.observableCollection.constructor.prototype; const redisSubscriberProto = dummyDriver.redisSubscriber.constructor.prototype; @@ -142,7 +143,7 @@ export function wrapRedisOplogObserveDriver (driver) { if (op !== 'r' && protectAgainstRaceConditions(collection)) { Kadira.models.pubsub.trackPolledDocuments(this.observableCollection._ownerInfo, 1); } - originalRedisProcess.call(this, op, doc, fields); + return originalRedisProcess.call(this, op, doc, fields); }; // @todo check this diff --git a/lib/hijack/synced-cron.js b/lib/hijack/synced-cron.js index 8cbb2e03..f80231a2 100644 --- a/lib/hijack/synced-cron.js +++ b/lib/hijack/synced-cron.js @@ -1,6 +1,6 @@ export function wrapSyncedCron () { Meteor.startup(() => { - let cronPackage = Package['littledata:synced-cron'] || Package['percolate:synced-cron']; + let cronPackage = Package['quave:synced-cron'] || Package['percolate:synced-cron']; if (!cronPackage) { return; diff --git a/lib/hijack/timeout_manager.js b/lib/hijack/timeout_manager.js index 3b8b4597..3e766353 100644 --- a/lib/hijack/timeout_manager.js +++ b/lib/hijack/timeout_manager.js @@ -16,11 +16,11 @@ export const TimeoutManager = { const id = ++this.id; - this.map.set(id, setTimeout(Meteor.bindEnvironment(() => { + this.map.set(id, setTimeout(() => { fn(); this.map.delete(id); - }), timeout)); + }, timeout)); return id; }, @@ -50,7 +50,9 @@ export const TimeoutManager = { const { timeoutId } = kadiraInfo; - if (timeoutId && this.map.has(timeoutId)) { + if (!timeoutId) return; + + if (this.map.has(timeoutId)) { clearTimeout(this.map.get(timeoutId)); this.map.delete(timeoutId); delete kadiraInfo.timeoutId; diff --git a/lib/hijack/wrap_ddp_stringify.js b/lib/hijack/wrap_ddp_stringify.js index 4fe6cdcb..6623336c 100644 --- a/lib/hijack/wrap_ddp_stringify.js +++ b/lib/hijack/wrap_ddp_stringify.js @@ -7,24 +7,25 @@ export function wrapStringifyDDP () { let msgString = originalStringifyDDP(msg); let msgSize = Buffer.byteLength(msgString, 'utf8'); - let kadiraInfo = Kadira._getInfo(null, true); + let kadiraInfo = Kadira._getInfo(); - if (kadiraInfo && !Kadira.env.currentSub) { - if (kadiraInfo.trace.type === 'method') { - Kadira.models.methods.trackMsgSize(kadiraInfo.trace.name, msgSize); - } - - return msgString; + if (kadiraInfo?.trace?.type === 'method') { + Kadira.models.methods.trackMsgSize(kadiraInfo.trace.name, msgSize); } - // 'currentSub' is set when we wrap Subscription object and override - // handlers for 'added', 'changed', 'removed' events. (see lib/hijack/wrap_subscription.js) - if (Kadira.env.currentSub) { - if (Kadira.env.currentSub.__kadiraInfo) { + if (Kadira.env?.currentSub) { + if (msg?.msg === 'ready' && msg?.subs?.includes(Kadira.env.currentSub._subscriptionId)) { + Kadira.env.currentSub._initialSentFinished = true; + return msgString; + } + + if (!Kadira.env.currentSub._initialSentFinished) { Kadira.models.pubsub.trackMsgSize(Kadira.env.currentSub._name, 'initialSent', msgSize); return msgString; } + Kadira.models.pubsub.trackMsgSize(Kadira.env.currentSub._name, 'liveSent', msgSize); + return msgString; } diff --git a/lib/hijack/wrap_observers.js b/lib/hijack/wrap_observers.js index 8d755990..5d7dcfb2 100644 --- a/lib/hijack/wrap_observers.js +++ b/lib/hijack/wrap_observers.js @@ -5,12 +5,15 @@ export function wrapOplogObserveDriver (proto) { // Track the polled documents. This is reflected to the RAM size and // for the CPU usage directly let originalPublishNewResults = proto._publishNewResults; + proto._publishNewResults = function (newResults, newBuffer) { let coll = this._cursorDescription.collectionName; let query = this._cursorDescription.selector; let opts = this._cursorDescription.options; const docSize = Kadira.docSzCache.getSize(coll, query, opts, newBuffer); + let count = newResults.size() + newBuffer.size(); + if (this._ownerInfo) { Kadira.models.pubsub.trackPolledDocuments(this._ownerInfo, count); Kadira.models.pubsub.trackDocSize(this._ownerInfo.name, 'polledFetches', docSize * count); @@ -97,8 +100,8 @@ export function wrapOplogObserveDriver (proto) { export function wrapPollingObserveDriver (proto) { let originalPollMongo = proto._pollMongo; - proto._pollMongo = function () { - originalPollMongo.call(this); + proto._pollMongo = async function () { + const result = await originalPollMongo.call(this); // Current result is stored in the following variable. // So, we can use that @@ -125,6 +128,8 @@ export function wrapPollingObserveDriver (proto) { this._polledDocuments = count; this._polledDocSize = docSize; } + + return result; }; let originalStop = proto.stop; @@ -158,12 +163,13 @@ export function wrapMultiplexer (proto) { export function wrapForCountingObservers () { // to count observers let mongoConnectionProto = MeteorX.MongoConnection.prototype; + let originalObserveChanges = mongoConnectionProto._observeChanges; - mongoConnectionProto._observeChanges = function (cursorDescription, ordered, callbacks) { - let ret = originalObserveChanges.call(this, cursorDescription, ordered, callbacks); - // get the Kadira Info via the Meteor.EnvironmentalVariable - let kadiraInfo = Kadira._getInfo(null, true); + mongoConnectionProto._observeChanges = async function (cursorDescription, ordered, callbacks) { + let ret = await originalObserveChanges.call(this, cursorDescription, ordered, callbacks); + + let kadiraInfo = Kadira._getInfo(); if (kadiraInfo && ret._multiplexer) { if (!ret._multiplexer.__kadiraTracked) { @@ -171,6 +177,7 @@ export function wrapForCountingObservers () { ret._multiplexer.__kadiraTracked = true; Kadira.EventBus.emit('pubsub', 'newSubHandleCreated', kadiraInfo.trace); Kadira.models.pubsub.incrementHandleCount(kadiraInfo.trace, false); + if (kadiraInfo.trace.type === 'sub') { let ownerInfo = { type: kadiraInfo.trace.type, @@ -189,6 +196,7 @@ export function wrapForCountingObservers () { } Kadira.EventBus.emit('pubsub', 'observerCreated', ownerInfo); + Kadira.models.pubsub.trackCreatedObserver(ownerInfo); // We need to send initially polled documents if there are diff --git a/lib/hijack/wrap_session.js b/lib/hijack/wrap_session.js index de7222db..85610eb6 100644 --- a/lib/hijack/wrap_session.js +++ b/lib/hijack/wrap_session.js @@ -2,10 +2,14 @@ import { Meteor } from 'meteor/meteor'; import { _ } from 'meteor/underscore'; import { MeteorDebugIgnore } from './error'; import { TimeoutManager } from './timeout_manager'; +import { runWithALS } from '../async/als'; +import { EventType } from '../constants'; +import { awaitDetector } from '../async/async-hook'; const MAX_PARAMS_LENGTH = 4000; + export function wrapSession (sessionProto) { let originalProcessMessage = sessionProto.processMessage; sessionProto.processMessage = function (msg) { @@ -29,8 +33,8 @@ export function wrapSession (sessionProto) { } let startData = { userId: this.userId, params: stringifiedParams }; - Kadira.tracer.event(kadiraInfo.trace, 'start', startData); - msg._waitEventId = Kadira.tracer.event(kadiraInfo.trace, 'wait', {}, kadiraInfo); + Kadira.tracer.event(kadiraInfo.trace, EventType.Start, startData); + msg._waitEventId = Kadira.tracer.event(kadiraInfo.trace, EventType.Wait, {}, kadiraInfo); msg.__kadiraInfo = kadiraInfo; if (msg.msg === 'sub') { @@ -46,9 +50,9 @@ export function wrapSession (sessionProto) { return originalProcessMessage.call(this, msg); }; - // adding the method context to the current fiber let originalMethodHandler = sessionProto.protocol_handlers.method; - sessionProto.protocol_handlers.method = function (msg, unblock) { + + sessionProto.protocol_handlers.method = runWithALS(async function (msg, unblock) { let self = this; // add context let kadiraInfo = msg.__kadiraInfo; @@ -65,23 +69,24 @@ export function wrapSession (sessionProto) { // end wait event let waitList = Kadira.waitTimeBuilder.build(this, msg.id); + Kadira.tracer.eventEnd(kadiraInfo.trace, msg._waitEventId, {waitOn: waitList}); unblock = Kadira.waitTimeBuilder.trackWaitTime(this, msg, unblock); - response = Kadira.env.kadiraInfo.withValue(kadiraInfo, function () { - return originalMethodHandler.call(self, msg, unblock); - }); + + response = await originalMethodHandler.call(self, msg, unblock); + unblock(); } else { - response = originalMethodHandler.call(self, msg, unblock); + response = await originalMethodHandler.call(self, msg, unblock); } return response; - }; + }); // to capture the currently processing message let orginalSubHandler = sessionProto.protocol_handlers.sub; - sessionProto.protocol_handlers.sub = function (msg, unblock) { + sessionProto.protocol_handlers.sub = runWithALS(async function (msg, unblock) { let self = this; // add context let kadiraInfo = msg.__kadiraInfo; @@ -97,20 +102,19 @@ export function wrapSession (sessionProto) { // end wait event Kadira.models.pubsub.trackWaitTime(msg); let waitList = Kadira.waitTimeBuilder.build(this, msg.id); + Kadira.tracer.eventEnd(kadiraInfo.trace, msg._waitEventId, {waitOn: waitList}); unblock = Kadira.waitTimeBuilder.trackWaitTime(this, msg, unblock); - response = Kadira.env.kadiraInfo.withValue(kadiraInfo, function () { - return orginalSubHandler.call(self, msg, unblock); - }); + response = await orginalSubHandler.call(self, msg, unblock); unblock(); } else { - response = orginalSubHandler.call(self, msg, unblock); + response = await orginalSubHandler.call(self, msg, unblock); } return response; - }; + }); // to capture the currently processing message let orginalUnSubHandler = sessionProto.protocol_handlers.unsub; @@ -139,17 +143,17 @@ export function wrapSession (sessionProto) { // the error stack is wrapped so Meteor._debug can identify // this as a method error. error = _.pick(kadiraInfo.currentError, ['message', 'stack', 'details']); - // see wrapMethodHanderForErrors() method def for more info + // see wrapMethodHandlerForErrors() method def for more info if (error.stack && error.stack.stack) { error.stack = error.stack.stack; } } Kadira.tracer.endLastEvent(kadiraInfo.trace); - Kadira.tracer.event(kadiraInfo.trace, 'error', {error}); + Kadira.tracer.event(kadiraInfo.trace, EventType.Error, {error}); } else { Kadira.tracer.endLastEvent(kadiraInfo.trace); - Kadira.tracer.event(kadiraInfo.trace, 'complete'); + Kadira.tracer.event(kadiraInfo.trace, EventType.Complete); } // processing the message @@ -161,10 +165,6 @@ export function wrapSession (sessionProto) { if (error && Kadira.options.enableErrorTracking) { Kadira.models.error.trackError(error, trace); } - - // clean and make sure, fiber is clean - // not sure we need to do this, but a preventive measure - Kadira._setInfo(null); } } @@ -172,25 +172,40 @@ export function wrapSession (sessionProto) { }; } +// eslint-disable-next-line camelcase +Meteor.server.publish_handlers = new Proxy(Meteor.server.publish_handlers, { + get (target, propKey) { + const origMethod = target[propKey]; + + if (typeof origMethod !== 'function') { + return origMethod; + } + + return function (...args) { + return awaitDetector.detect(() => origMethod.apply(this, args), Kadira._getInfo()); + }; + } +}); + // wrap existing method handlers for capturing errors _.each(Meteor.server.method_handlers, function (handler, name) { - wrapMethodHanderForErrors(name, handler, Meteor.server.method_handlers); + wrapMethodHandlerForErrors(name, handler, Meteor.server.method_handlers); }); // wrap future method handlers for capturing errors let originalMeteorMethods = Meteor.methods; Meteor.methods = function (methodMap) { _.each(methodMap, function (handler, name) { - wrapMethodHanderForErrors(name, handler, methodMap); + wrapMethodHandlerForErrors(name, handler, methodMap); }); originalMeteorMethods(methodMap); }; -function wrapMethodHanderForErrors (name, originalHandler, methodMap) { +function wrapMethodHandlerForErrors (name, originalHandler, methodMap) { methodMap[name] = function () { try { - return originalHandler.apply(this, arguments); + return awaitDetector.detect(() => originalHandler.apply(this, arguments), Kadira._getInfo()); } catch (ex) { if (ex && Kadira._getInfo()) { // sometimes error may be just a string or a primitive diff --git a/lib/hijack/wrap_subscription.js b/lib/hijack/wrap_subscription.js index 6e809820..96dd33e3 100644 --- a/lib/hijack/wrap_subscription.js +++ b/lib/hijack/wrap_subscription.js @@ -1,20 +1,28 @@ import { MeteorDebugIgnore } from './error'; import { _ } from 'meteor/underscore'; +import { runWithEnvironment } from '../async/als'; +import { EventType } from '../constants'; export function wrapSubscription (subscriptionProto) { - // If the ready event runs outside the Fiber, Kadira._getInfo() doesn't work. - // we need some other way to store kadiraInfo so we can use it at ready hijack. let originalRunHandler = subscriptionProto._runHandler; - subscriptionProto._runHandler = function () { + + subscriptionProto._runHandler = runWithEnvironment(function () { + Kadira.env.currentSub = this; + let kadiraInfo = Kadira._getInfo(); + if (kadiraInfo) { this.__kadiraInfo = kadiraInfo; } - originalRunHandler.call(this); - }; + + return originalRunHandler.call(this); + }); let originalReady = subscriptionProto.ready; - subscriptionProto.ready = function () { + + subscriptionProto.ready = runWithEnvironment(function () { + Kadira.env.currentSub = this; + // meteor has a field called `_ready` which tracks this, // but we need to make it future-proof if (!this._apmReadyTracked) { @@ -30,7 +38,7 @@ export function wrapSubscription (subscriptionProto) { // the trace.id are both undefined, but we don't want to complete the HTTP trace here if (kadiraInfo && this._subscriptionId && this._subscriptionId === kadiraInfo.trace.id) { Kadira.tracer.endLastEvent(kadiraInfo.trace); - Kadira.tracer.event(kadiraInfo.trace, 'complete'); + Kadira.tracer.event(kadiraInfo.trace, EventType.Complete); trace = Kadira.tracer.buildTrace(kadiraInfo.trace); } @@ -42,9 +50,10 @@ export function wrapSubscription (subscriptionProto) { // we still pass the control to the original implementation // since multiple ready calls are handled by itself originalReady.call(this); - }; + }); let originalError = subscriptionProto.error; + subscriptionProto.error = function (err) { if (typeof err === 'string') { err = { message: err }; @@ -56,7 +65,7 @@ export function wrapSubscription (subscriptionProto) { Kadira.tracer.endLastEvent(kadiraInfo.trace); let errorForApm = _.pick(err, 'message', 'stack'); - Kadira.tracer.event(kadiraInfo.trace, 'error', {error: errorForApm}); + Kadira.tracer.event(kadiraInfo.trace, EventType.Error, {error: errorForApm}); let trace = Kadira.tracer.buildTrace(kadiraInfo.trace); Kadira.models.pubsub._trackError(this._session, this, trace); @@ -79,6 +88,7 @@ export function wrapSubscription (subscriptionProto) { }; let originalDeactivate = subscriptionProto._deactivate; + subscriptionProto._deactivate = function () { Kadira.EventBus.emit('pubsub', 'subDeactivated', this._session, this); Kadira.models.pubsub._trackUnsub(this._session, this); @@ -88,18 +98,11 @@ export function wrapSubscription (subscriptionProto) { // adding the currenSub env variable ['added', 'changed', 'removed'].forEach(function (funcName) { let originalFunc = subscriptionProto[funcName]; - subscriptionProto[funcName] = function (collectionName, id, fields) { - let self = this; - - // we need to run this code in a fiber and that's how we track - // subscription info. Maybe we can figure out, some other way to do this - // We use this currently to get the publication info when tracking message - // sizes at wrap_ddp_stringify.js - Kadira.env.currentSub = self; - let res = originalFunc.call(self, collectionName, id, fields); - Kadira.env.currentSub = null; - - return res; - }; + + subscriptionProto[funcName] = runWithEnvironment(function (collectionName, id, fields) { + Kadira.env.currentSub = this; + + return originalFunc.call(this, collectionName, id, fields); + }); }); } diff --git a/lib/hijack/wrap_webapp.js b/lib/hijack/wrap_webapp.js index baac2d05..43cf4e13 100644 --- a/lib/hijack/wrap_webapp.js +++ b/lib/hijack/wrap_webapp.js @@ -1,5 +1,7 @@ -import { WebAppInternals, WebApp } from 'meteor/webapp'; -import Fibers from 'fibers'; +import { WebApp, WebAppInternals } from 'meteor/webapp'; +import { runWithALS } from '../async/als'; +import { EventType } from '../constants'; +import { awaitDetector } from '../async/async-hook'; // Maximum content-length size const MAX_BODY_SIZE = 8000; @@ -8,62 +10,38 @@ const MAX_STRINGIFIED_BODY_SIZE = 4000; const canWrapStaticHandler = !!WebAppInternals.staticFilesByArch; -// This checks if running on a version of Meteor that -// wraps connect handlers in a fiber. -// This check is dependant on Meteor's implementation of `use`, -// which wraps every handler in a new fiber. -// This will need to be updated if Meteor starts reusing -// fibers when they exist. -export function checkHandlersInFiber () { - const handlersLength = WebApp.rawConnectHandlers.stack.length; - let inFiber = false; - let outsideFiber = Fibers.current; - - WebApp.rawConnectHandlers.use((_req, _res, next) => { - inFiber = Fibers.current && Fibers.current !== outsideFiber; - - // in case we didn't successfully remove this handler - // and it is a real request - next(); - }); - - if (WebApp.rawConnectHandlers.stack[handlersLength]) { - let handler = WebApp.rawConnectHandlers.stack[handlersLength].handle; - - // remove the newly added handler - // We remove it immediately so there is no opportunity for - // other code to add handlers first if the current fiber is yielded - // while running the handler - while (WebApp.rawConnectHandlers.stack.length > handlersLength) { - WebApp.rawConnectHandlers.stack.pop(); - } - - handler({}, {}, () => {}); - } +const InfoSymbol = Symbol('MontiInfoSymbol'); - return inFiber; +export function getRouter (app) { + // Express 5 removed the _ prefix to router. + return app.parent._router || app.parent.router; } -const InfoSymbol = Symbol('MontiInfoSymbol'); - -export async function wrapWebApp () { - if (!checkHandlersInFiber() || !canWrapStaticHandler) { +/** + * https://github.com/meteor/meteor/pull/12442 + */ +export function wrapWebApp () { + if (!canWrapStaticHandler) { return; } + // eslint-disable-next-line global-require const parseUrl = require('parseurl'); - WebAppInternals.registerBoilerplateDataCallback('__montiApmRouteName', function (request) { - // TODO: record in trace which arch is used + WebAppInternals.registerBoilerplateDataCallback( + '__montiApmRouteName', + function (request) { + // TODO: record in trace which arch is used - if (request[InfoSymbol]) { - request[InfoSymbol].isAppRoute = true; - } + if (request[InfoSymbol]) { + request[InfoSymbol].isAppRoute = true; + } - // Let WebApp know we didn't make changes - // so it can use a cache - return false; - }); + // Let WebApp know we didn't make changes + // so it can use a cache + return false; + } + ); // We want the request object returned by categorizeRequest to have // __kadiraInfo @@ -78,78 +56,95 @@ export async function wrapWebApp () { return result; }; - // Adding the handler directly to the stack - // to force it to be the first one to run - WebApp.rawConnectHandlers.stack.unshift({ - route: '', - handle: (req, res, next) => { - const name = parseUrl(req).pathname; - const trace = Kadira.tracer.start(`${req.method}-${name}`, 'http'); - - const headers = Kadira.tracer._applyObjectFilters(req.headers); - Kadira.tracer.event(trace, 'start', { - url: req.url, - method: req.method, - headers: JSON.stringify(headers), - }); - req.__kadiraInfo = { trace }; + const Layer = getRouter(WebApp.rawHandlers).stack[0].constructor; - res.on('finish', () => { - if (req.__kadiraInfo.asyncEvent) { - Kadira.tracer.eventEnd(trace, req.__kadiraInfo.asyncEvent); - } + const middleware = (req, res, next) => { + const name = parseUrl(req).pathname; + const trace = Kadira.tracer.start(`${req.method}-${name}`, 'http'); - Kadira.tracer.endLastEvent(trace); + const headers = Kadira.tracer._applyObjectFilters(req.headers); - if (req.__kadiraInfo.isStatic) { - trace.name = `${req.method}-`; - } else if (req.__kadiraInfo.suggestedRouteName) { - trace.name = `${req.method}-${req.__kadiraInfo.suggestedRouteName}`; - } else if (req.__kadiraInfo.isAppRoute) { - trace.name = `${req.method}-`; - } + Kadira.tracer.event(trace, EventType.Start, { + url: req.url, + method: req.method, + headers: JSON.stringify(headers), + }); - const isJson = req.headers['content-type'] === 'application/json'; - const hasSmallBody = req.headers['content-length'] > 0 && req.headers['content-length'] < MAX_BODY_SIZE; - - // Check after all middleware have run to see if any of them - // set req.body - // Technically bodies can be used with any method, but since many load balancers and - // other software only support bodies for POST requests, we are - // not recording the body for other methods. - if (req.method === 'POST' && req.body && isJson && hasSmallBody) { - try { - let filteredBody = Kadira.tracer._applyObjectFilters(req.body); - let body = JSON.stringify(filteredBody); - - // Check the body size again in case it is much - // larger than what was in the content-length header - if (body.length < MAX_STRINGIFIED_BODY_SIZE) { - trace.events[0].data.body = body; - } - } catch (e) { - // It is okay if this fails + req.__kadiraInfo = { trace }; + + res.on('finish', () => { + Kadira.tracer.endLastEvent(trace); + + if (req.__kadiraInfo.isStatic) { + trace.name = `${req.method}-`; + } else if (req.__kadiraInfo.suggestedRouteName) { + trace.name = `${req.method}-${req.__kadiraInfo.suggestedRouteName}`; + } else if (req.__kadiraInfo.isAppRoute) { + trace.name = `${req.method}-`; + } + + const isJson = req.headers['content-type'] === 'application/json'; + const hasSmallBody = + req.headers['content-length'] > 0 && + req.headers['content-length'] < MAX_BODY_SIZE; + + // Check after all middleware have run to see if any of them + // set req.body + // Technically bodies can be used with any method, but since many load balancers and + // other software only support bodies for POST requests, we are + // not recording the body for other methods. + if (req.method === 'POST' && req.body && isJson && hasSmallBody) { + try { + let filteredBody = Kadira.tracer._applyObjectFilters(req.body); + let body = JSON.stringify(filteredBody); + + // Check the body size again in case it is much + // larger than what was in the content-length header + if (body.length < MAX_STRINGIFIED_BODY_SIZE) { + trace.events[0].data.body = body; } + } catch (e) { + // It is okay if this fails } + } - // TODO: record status code - Kadira.tracer.event(trace, 'complete'); - let built = Kadira.tracer.buildTrace(trace); - Kadira.models.http.processRequest(built, req, res); - }); + // TODO: record status code + Kadira.tracer.event(trace, EventType.Complete); + let built = Kadira.tracer.buildTrace(trace); + Kadira.models.http.processRequest(built, req, res); + }); - next(); - } - }); + runWithALS(next)(); + }; + const layer = new Layer( + '', + { + sensitive: false, + strict: false, + end: false, + }, + middleware + ); + + // Adding the handler directly to the stack + // to force it to be the first one to run + getRouter(WebApp.rawHandlers).stack.unshift(layer); - function wrapHandler (handler) { - // connect identifies error handles by them accepting + function wrapHandler (handler, possibleRouteName) { + if (Array.isArray(handler)) { + return handler.map((singleHandler) => { + return wrapHandler(singleHandler, possibleRouteName); + }); + } + + // identifies error handles by them accepting // four arguments let errorHandler = handler.length === 4; function wrapper (req, res, next) { let error; + if (errorHandler) { error = req; req = res; @@ -158,41 +153,26 @@ export async function wrapWebApp () { } const kadiraInfo = req.__kadiraInfo; + if (possibleRouteName && typeof possibleRouteName === 'string') { + req.__kadiraInfo.suggestedRouteName = possibleRouteName; + } + Kadira._setInfo(kadiraInfo); - let nextCalled = false; // TODO: track errors passed to next or thrown - function wrappedNext (...args) { - if (kadiraInfo && kadiraInfo.asyncEvent) { - Kadira.tracer.eventEnd(req.__kadiraInfo.trace, req.__kadiraInfo.asyncEvent); - req.__kadiraInfo.asyncEvent = null; - } - - nextCalled = true; - next(...args); - } let potentialPromise; if (errorHandler) { - potentialPromise = handler.call(this, error, req, res, wrappedNext); + potentialPromise = awaitDetector.detect(() => + handler.call(this, error, req, res, next), + kadiraInfo + ); } else { - potentialPromise = handler.call(this, req, res, wrappedNext); - } - - if (potentialPromise && typeof potentialPromise.then === 'function') { - potentialPromise.then(() => { - // res.finished is depreciated in Node 13, but it is the only option - // for Node 12.9 and older. - if (kadiraInfo && !res.finished && !nextCalled) { - const lastEvent = Kadira.tracer.getLastEvent(kadiraInfo.trace); - if (lastEvent.endAt) { - // req is not done, and next has not been called - // create an async event that will end when either of those happens - kadiraInfo.asyncEvent = Kadira.tracer.event(kadiraInfo.trace, 'async'); - } - } - }); + potentialPromise = awaitDetector.detect(() => + handler.call(this, req, res, next), + kadiraInfo + ); } return potentialPromise; @@ -208,66 +188,73 @@ export async function wrapWebApp () { }; } - function wrapConnect (app, wrapStack) { + function wrapExpress (app, wrapStack) { let oldUse = app.use; if (wrapStack) { - // We need to set kadiraInfo on the Fiber the handler will run in. - // Meteor has already wrapped the handler to run it in a new Fiber - // by using Promise.asyncApply so we are not able to directly set it - // on that Fiber. - // Meteor's promise library copies properties from the current fiber to - // the new fiber, so we can wrap it in another Fiber with kadiraInfo set - // and Meteor will copy kadiraInfo to the new Fiber. - // It will only create the additional Fiber if it isn't already running in a Fiber - app.stack.forEach(entry => { + getRouter(app).stack.forEach((entry) => { let wrappedHandler = wrapHandler(entry.handle); if (entry.handle.length >= 4) { // eslint-disable-next-line no-unused-vars,handle-callback-err entry.handle = function (error, req, res, next) { - return Promise.asyncApply( - wrappedHandler, - this, - arguments, - true - ); + return wrappedHandler.apply(this, arguments); }; } else { // eslint-disable-next-line no-unused-vars entry.handle = function (req, res, next) { - return Promise.asyncApply( - wrappedHandler, - this, - arguments, - true - ); + return wrappedHandler.apply(this, arguments); }; } }); } + app.use = function (...args) { - args[args.length - 1] = wrapHandler(args[args.length - 1]); + let fn = args[args.length - 1] = args[args.length - 1]; + + // Make sure this isn't an instance of an express app, since our wrapping + // would prevent express from detecting if it is + // https://github.com/monti-apm/monti-apm-agent/issues/133 + // TODO: instrument child express app instances + if (!fn.handle) { + const possibleRouteName = args[0]; + args[args.length - 1] = wrapHandler( + args[args.length - 1], + possibleRouteName + ); + } + return oldUse.apply(app, args); }; } - wrapConnect(WebApp.rawConnectHandlers, false); - wrapConnect(WebAppInternals.meteorInternalHandlers, false); + wrapExpress(WebApp.rawHandlers, false); + wrapExpress(WebAppInternals.meteorInternalHandlers, false); // The oauth package and other core packages might have already added their middleware, // so we need to wrap the existing middleware - wrapConnect(WebApp.connectHandlers, true); + wrapExpress(WebApp.handlers, true); - wrapConnect(WebApp.connectApp, false); + wrapExpress(WebApp.expressApp, false); let oldStaticFilesMiddleware = WebAppInternals.staticFilesMiddleware; - WebAppInternals.staticFilesMiddleware = function (staticFiles, req, res, next) { + wrapHandler( + oldStaticFilesMiddleware.bind( + WebAppInternals, + WebAppInternals.staticFilesByArch + ) + ); + WebAppInternals.staticFilesMiddleware = function ( + _staticFiles, + req, + res, + next + ) { if (req.__kadiraInfo) { req.__kadiraInfo.isStatic = true; } return oldStaticFilesMiddleware.call( this, - staticFiles, + _staticFiles, req, res, function () { diff --git a/lib/kadira.js b/lib/kadira.js index 9c29af6a..d59bb879 100644 --- a/lib/kadira.js +++ b/lib/kadira.js @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ /* global MontiProfiler */ import { Meteor } from 'meteor/meteor'; @@ -9,7 +8,6 @@ import { HttpModel } from './models/http'; import { MethodsModel } from './models/methods'; import { PubsubModel } from './models/pubsub'; import { SystemModel } from './models/system'; -import packageMap from './.meteor-package-versions'; import { getErrorParameters } from './common/utils'; import { WaitTimeBuilder } from './wait_time_builder'; import { DocSzCache } from './docsize_cache'; @@ -20,22 +18,27 @@ import { TrackMeteorDebug, TrackUncaughtExceptions, TrackUnhandledRejections } f import { queueJob } from './jobs'; import { JobsModel } from './models/jobs'; -const hostname = Npm.require('os').hostname(); -const logger = Npm.require('debug')('kadira:apm'); -const Fibers = Npm.require('fibers'); -const { CoreEvent, Kadira: KadiraCore, Feature } = Npm.require('monti-apm-core'); +import { createStore, getInfo, MontiAsyncStorage, MontiEnvironmentVariable } from './async/als'; +import { EventType } from './constants'; +import packageMap from './.meteor-package-versions'; +import { awaitDetector } from './async/async-hook'; +import { connectLogger } from './logger'; -const DEBUG_PAYLOAD_SIZE = process.env.MONTI_DEBUG_PAYLOAD_SIZE === 'true'; +const hostname = require('os').hostname(); +const logger = require('debug')('kadira:apm'); +const { CoreEvent, Monti: KadiraCore, Feature } = Npm.require('@monti-apm/core'); -// The Meteor versions with Node 4 did not support promise.finally -const WEBSOCKETS_SUPPORTED = parseInt(process.versions.node.split('.')[0], 10) > 4; +const DEBUG_PAYLOAD_SIZE = process.env.MONTI_DEBUG_PAYLOAD_SIZE === 'true'; Kadira.models = {}; Kadira.options = {}; -Kadira.env = { - currentSub: null, // keep current subscription inside ddp - kadiraInfo: new Meteor.EnvironmentVariable(), -}; + +Object.defineProperty(Kadira, 'env', { + get () { + return MontiEnvironmentVariable.getStore()?.get('env'); + } +}); + Kadira.waitTimeBuilder = new WaitTimeBuilder(); Kadira.errors = []; Kadira.errors.addFilter = Kadira.errors.push.bind(Kadira.errors); @@ -57,7 +60,11 @@ let buildInterval = Meteor.setInterval(() => { Kadira.connect = function (appId, appSecret, options) { + connectLogger(`Starting connection: id: "${appId}", secret: "${appSecret}", options: ${JSON.stringify(options)}`); + if (Kadira.connected) { + connectLogger('Already connected - skipping connection'); + // eslint-disable-next-line no-console console.log('Monti APM: Already Connected'); return; } @@ -96,13 +103,18 @@ Kadira.connect = function (appId, appSecret, options) { options.uploadSourceMaps = true; } + connectLogger(`Options with defaults: ${JSON.stringify(options)}`); + Kadira.options = options; Kadira.options.authHeaders = { 'KADIRA-APP-ID': Kadira.options.appId, 'KADIRA-APP-SECRET': Kadira.options.appSecret }; + + connectLogger(`Auth options: ${JSON.stringify(Kadira.options.authHeaders)}`); if (appId && appSecret) { + connectLogger(`Opening connection to ${options.endpoint} from ${options.hostname}`); options.appId = options.appId.trim(); options.appSecret = options.appSecret.trim(); @@ -111,22 +123,26 @@ Kadira.connect = function (appId, appSecret, options) { appSecret: options.appSecret, endpoint: options.endpoint, hostname: options.hostname, - agentVersion: packageMap['montiapm:agent'] || '' + agentVersion: packageMap['montiapm:agent'] || '', + proxy: options.proxy }); Kadira.coreApi._headers['METEOR-RELEASE'] = Meteor.release.replace('METEOR@', ''); + connectLogger(`Validating auth...`); Kadira.coreApi._checkAuth() .then(function () { + connectLogger(`Validated auth - connected to ${appId}`); logger('connected to app: ', appId); + // eslint-disable-next-line no-console console.log('Monti APM: Connected'); + + connectLogger(`Scheduling payloads`); Kadira._sendAppStats(); Kadira._schedulePayloadSend(); - if ( - WEBSOCKETS_SUPPORTED && - Kadira.coreApi.featureSupported(Feature.WEBSOCKETS) - ) { + if (Kadira.coreApi.featureSupported(Feature.WEBSOCKETS)) { + connectLogger(`Connecting websocket...`); logger('websockets supported'); Kadira.coreApi._initWebSocket(); @@ -139,12 +155,15 @@ Kadira.connect = function (appId, appSecret, options) { }) .catch(function (err) { if (err.message === 'Unauthorized') { + // eslint-disable-next-line no-console console.log('Monti APM: Authentication failed, check your "appId" & "appSecret"'); } else { + // eslint-disable-next-line no-console console.log(`Monti APM: Unable to connect. ${err.message}`); } }); } else { + connectLogger(`Missing app id and secret`); throw new Error('Monti APM: required appId and appSecret'); } @@ -233,6 +252,7 @@ Kadira._sendAppStats = function () { }).then(function (body) { handleApiResponse(body); }).catch(function (err) { + // eslint-disable-next-line no-console console.error('Monti APM Error on sending appStats:', err.message); }); }; @@ -247,6 +267,10 @@ Kadira._schedulePayloadSend = function () { }; function logPayload (payload) { + if (!DEBUG_PAYLOAD_SIZE) { + return; + } + let traceCount = payload.methodRequests.length + payload.pubRequests.length + payload.httpRequests.length + payload.errors.length; @@ -288,6 +312,7 @@ function logPayload (payload) { return Object.entries(histogram).map(([k, v]) => `${k}: ${v}`).join(', '); } + /* eslint-disable no-console */ console.log('------- APM Payload Metrics -------'); console.log(`methods: ${countBreakdowns(payload.methodMetrics, 'methods')}`); console.log(`pubs: ${countBreakdowns(payload.pubMetrics, 'pubs')}`); @@ -300,43 +325,44 @@ function logPayload (payload) { console.log('Error trace sizes:', sizeTraces(payload.errors)); console.log('Largest trace:', largestTrace); console.log('------- ------------------- -------'); + /* eslint-enable no-console */ } Kadira._sendPayload = function () { - new Fibers(function () { - let payload = Kadira._buildPayload(); + let payload = Kadira._buildPayload(); - if (DEBUG_PAYLOAD_SIZE) { - logPayload(payload); - } + if (DEBUG_PAYLOAD_SIZE) { + logPayload(payload); + } - function send () { - return Kadira.coreApi.sendData(payload) - .then(function (body) { - handleApiResponse(body); - }); - } + function send () { + return Kadira.coreApi.sendData(payload) + .then(function (body) { + handleApiResponse(body); + }); + } - function logErr (err) { - console.log('Monti APM Error:', 'while sending payload to Monti APM:', err.message); - } + function logErr (err) { + // eslint-disable-next-line no-console + console.log('Monti APM Error:', 'while sending payload to Monti APM:', err.message); + } - send() - .catch(function (err) { - // Likely is RangeError: Invalid string length - // This probably means we are close to running out of memory. - if (err instanceof RangeError) { - console.log('Monti APM: payload was too large to send to Monti APM. Resending without traces'); - payload.methodRequests = undefined; - payload.httpRequests = undefined; - payload.pubRequests = undefined; - send() - .catch(logErr); - } else { - logErr(err); - } - }); - }).run(); + send() + .catch(function (err) { + // Likely is RangeError: Invalid string length + // This probably means we are close to running out of memory. + if (err instanceof RangeError) { + // eslint-disable-next-line no-console + console.log('Monti APM: payload was too large to send to Monti APM. Resending without traces'); + payload.methodRequests = undefined; + payload.httpRequests = undefined; + payload.pubRequests = undefined; + send() + .catch(logErr); + } else { + logErr(err); + } + }); }; // this return the __kadiraInfo from the current Fiber by default @@ -345,19 +371,19 @@ Kadira._sendPayload = function () { // // WARNNING: returned info object is the reference object. // Changing it might cause issues when building traces. So use with care -Kadira._getInfo = function (currentFiber, useEnvironmentVariable) { - currentFiber = currentFiber || Fibers.current; - if (currentFiber) { - if (useEnvironmentVariable) { - return Kadira.env.kadiraInfo.get(); - } - return currentFiber.__kadiraInfo; - } +Kadira._getInfo = function () { + return MontiAsyncStorage.getStore()?.info; }; // this does not clone the info object. So, use with care Kadira._setInfo = function (info) { - Fibers.current.__kadiraInfo = info; + const store = MontiAsyncStorage.getStore(); + + if (!store) { + return; + } + + store.info = info; }; Kadira.startContinuousProfiling = function () { @@ -366,7 +392,8 @@ Kadira.startContinuousProfiling = function () { return; } - Kadira.coreApi.sendData({ profiles: [{ profile, startTime, endTime }] }) + Kadira.coreApi.sendData({ profiles: [{ profile, startTime, endTime }]}) + // eslint-disable-next-line no-console .catch(e => console.log('Monti: err sending cpu profile', e)); }); }; @@ -409,8 +436,12 @@ Kadira.trackError = function () { const now = Ntp._now(); + if (kadiraInfo?.trace) { + kadiraInfo.trace.endAt = now; + } + const previousEvents = - kadiraInfo && kadiraInfo.trace ? + kadiraInfo?.trace ? kadiraInfo.trace.events : [{ type: 'start', at: now, endAt: now }]; @@ -438,9 +469,10 @@ Kadira.ignoreErrorTracking = function (err) { }; Kadira.startEvent = function (name, data = {}) { - let kadiraInfo = Kadira._getInfo(); - if (kadiraInfo) { - return Kadira.tracer.event(kadiraInfo.trace, 'custom', data, { name }); + let info = getInfo(); + + if (info) { + return Kadira.tracer.event(info.trace, EventType.Custom, data, { name }); } return false; @@ -452,7 +484,7 @@ Kadira.event = function (name, data, fn) { data = undefined; } - let info = Kadira._getInfo(); + let info = getInfo(); if (typeof name !== 'string') { throw new Error('Monti APM: Monti.event requires an event name'); @@ -463,13 +495,15 @@ Kadira.event = function (name, data, fn) { } if (info) { - const event = Kadira.tracer.event(info.trace, 'custom', data, { name }); - + const event = Kadira.tracer.event(info.trace, EventType.Custom, data, { name }); + let reset = Kadira.tracer.setActiveEvent(event); + let result; try { - let result = fn(); + result = fn(); return result; } finally { - Kadira.tracer.eventEnd(info.trace, event); + reset(); + Kadira.tracer.endAfterResult(event, result); } } @@ -477,7 +511,7 @@ Kadira.event = function (name, data, fn) { }; Kadira.endEvent = function (event, data) { - let kadiraInfo = Kadira._getInfo(); + let kadiraInfo = getInfo(); // The event could be false if it could not be started. // Handle it here instead of requiring the app to. @@ -487,17 +521,10 @@ Kadira.endEvent = function (event, data) { }; Kadira.traceJob = function (details, processor) { - let info = Kadira._getInfo(); - if (info || !details) { + if (Kadira._getInfo() || !details) { return processor.apply(this, arguments); } - if (!Fibers.current) { - return Fibers(function () { - return Kadira.traceJob(details, processor); - }).run(); - } - let wait = details.waitTime || 0; let data = details.data; @@ -520,7 +547,6 @@ Kadira.traceJob = function (details, processor) { Kadira.tracer.event(trace, 'start', startData); - Kadira._setInfo({ trace }); Kadira.models.jobs.trackActiveJobs(details.name, 1); let ended = false; @@ -551,9 +577,16 @@ Kadira.traceJob = function (details, processor) { } } + let prevStore = MontiAsyncStorage.getStore(); + MontiAsyncStorage.enterWith(createStore()); + let info = { trace }; + Kadira._setInfo(info); + let result; try { - result = processor.apply(this, arguments); + let self = this; + let args = arguments; + result = awaitDetector.detect(() => processor.apply(self, args), info); if (result && typeof result.then === 'function') { result.then(() => end(), err => end(err)); @@ -564,9 +597,7 @@ Kadira.traceJob = function (details, processor) { end(err); throw err; } finally { - // Synchronously reset info so outside code doesn't see it. - // It'll still be set in any child fibers if the processor uses promises - Kadira._setInfo(null); + MontiAsyncStorage.enterWith(prevStore); } return result; @@ -579,3 +610,7 @@ Kadira.recordNewJob = function (jobName) { Kadira.recordPendingJobs = function (jobName, count) { Kadira.models.jobs.trackPendingJobs(jobName, count); }; + +Kadira._disableAwaitDetector = function () { + awaitDetector.destroy(); +}; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 00000000..e10db2a2 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,3 @@ +const debug = require('debug'); + +export const connectLogger = debug('monti:connect'); diff --git a/lib/models/http.js b/lib/models/http.js index 291142c6..9b32734a 100644 --- a/lib/models/http.js +++ b/lib/models/http.js @@ -101,13 +101,13 @@ HttpModel.prototype.buildPayload = function () { let metricsByMinute = this.metricsByMinute; this.metricsByMinute = Object.create(null); - for (let key in metricsByMinute) { + for (let key of Object.keys(metricsByMinute)) { const metrics = metricsByMinute[key]; // convert startTime into the actual serverTime let startTime = metrics.startTime; metrics.startTime = Kadira.syncedDate.syncTime(startTime); - for (let requestName in metrics.routes) { + for (let requestName of Object.keys(metrics.routes)) { METHOD_METRICS_FIELDS.forEach(function (field) { metrics.routes[requestName][field] /= metrics.routes[requestName].count; }); diff --git a/lib/models/jobs.js b/lib/models/jobs.js index f673d18b..dc8e2f30 100644 --- a/lib/models/jobs.js +++ b/lib/models/jobs.js @@ -159,13 +159,13 @@ JobsModel.prototype.buildPayload = function () { this.jobMetricsByMinute = Object.create(null); // create final payload for jobMetrics - for (let key in jobMetricsByMinute) { + for (let key of Object.keys(jobMetricsByMinute)) { const jobMetrics = jobMetricsByMinute[key]; // converting startTime into the actual serverTime let startTime = jobMetrics.startTime; jobMetrics.startTime = Kadira.syncedDate.syncTime(startTime); - for (let jobName in jobMetrics.jobs) { + for (let jobName of Object.keys(jobMetrics.jobs)) { JOB_METRICS_FIELDS.forEach(function (field) { if (jobMetrics.jobs[jobName][field] === 0) { return; diff --git a/lib/models/methods.js b/lib/models/methods.js index 93d5fbf4..b848269d 100644 --- a/lib/models/methods.js +++ b/lib/models/methods.js @@ -6,6 +6,7 @@ import { calculateWaitedOnTime } from '../utils'; const { DDSketch } = require('monti-apm-sketches-js'); + const METHOD_METRICS_FIELDS = ['wait', 'db', 'http', 'email', 'async', 'compute', 'total']; export function MethodsModel (metricsThreshold) { @@ -117,6 +118,7 @@ MethodsModel.prototype.trackMsgSize = function (method, size) { const dateId = this._getDateId(timestamp); let methodMetrics = this._getMetrics(dateId, method); + methodMetrics.sentMsgSize += size; }; @@ -145,13 +147,13 @@ MethodsModel.prototype.buildPayload = function () { this.methodMetricsByMinute = Object.create(null); // create final payload for methodMetrics - for (let key in methodMetricsByMinute) { + for (let key of Object.keys(methodMetricsByMinute)) { const methodMetrics = methodMetricsByMinute[key]; // converting startTime into the actual serverTime let startTime = methodMetrics.startTime; methodMetrics.startTime = Kadira.syncedDate.syncTime(startTime); - for (let methodName in methodMetrics.methods) { + for (let methodName of Object.keys(methodMetrics.methods)) { METHOD_METRICS_FIELDS.forEach(function (field) { methodMetrics.methods[methodName][field] /= methodMetrics.methods[methodName].count; diff --git a/lib/models/pubsub.js b/lib/models/pubsub.js index 805c1844..0fc35ee3 100644 --- a/lib/models/pubsub.js +++ b/lib/models/pubsub.js @@ -10,7 +10,7 @@ import { iterate, } from '../utils'; -const logger = Npm.require('debug')('kadira:pubsub'); +const logger = require('debug')('kadira:pubsub'); const { DDSketch } = require('monti-apm-sketches-js'); export function PubsubModel () { diff --git a/lib/models/system.js b/lib/models/system.js index 357f550d..7fe8c3cc 100644 --- a/lib/models/system.js +++ b/lib/models/system.js @@ -2,7 +2,6 @@ import { _ } from 'meteor/underscore'; import { Meteor } from 'meteor/meteor'; import { countKeys, createHistogram } from '../utils.js'; import GCMetrics from '../hijack/gc.js'; -import { getFiberMetrics, resetFiberMetrics } from '../hijack/async.js'; import { getMongoDriverStats, resetMongoDriverStats } from '../hijack/mongo_driver_events.js'; import { KadiraModel } from './0model'; import { EventLoopMonitor } from '../event_loop_monitor.js'; @@ -157,6 +156,7 @@ SystemModel.prototype.buildPayload = function () { metrics.activeHandles = process._getActiveHandles().length; // track eventloop metrics + metrics.pctEvloopBlock = this.evloopMonitor.status().pctBlock; metrics.evloopHistogram = this.evloopHistogram; this.evloopHistogram = createHistogram(); @@ -178,12 +178,6 @@ SystemModel.prototype.buildPayload = function () { metrics.mongoPoolCheckedOutConnections = driverMetrics.checkedOut; metrics.mongoPoolCreatedConnections = driverMetrics.created; - const fiberMetrics = getFiberMetrics(); - resetFiberMetrics(); - metrics.createdFibers = fiberMetrics.created; - metrics.activeFibers = fiberMetrics.active; - metrics.fiberPoolSize = fiberMetrics.poolSize; - metrics.pcpu = 0; metrics.pcpuUser = 0; metrics.pcpuSystem = 0; diff --git a/lib/ntp.js b/lib/ntp.js index 7ab97b15..50cf138f 100644 --- a/lib/ntp.js +++ b/lib/ntp.js @@ -145,6 +145,7 @@ export class Ntp { function getLogger () { if (Meteor.isServer) { + // `Npm.require` prevents it from being included in the client bundle, unlike `require` return Npm.require('debug')('kadira:ntp'); } diff --git a/lib/sourcemaps.js b/lib/sourcemaps.js index a2c59abc..ee74c85c 100644 --- a/lib/sourcemaps.js +++ b/lib/sourcemaps.js @@ -1,7 +1,8 @@ import { WebApp } from 'meteor/webapp'; -let path = Npm.require('path'); -let fs = Npm.require('fs'); -let logger = Npm.require('debug')('kadira:apm:sourcemaps'); + +let path = require('path'); +let fs = require('fs'); +let logger = require('debug')('kadira:apm:sourcemaps'); // Meteor 1.7 and older used clientPaths let clientPaths = __meteor_bootstrap__.configJson.clientPaths; diff --git a/lib/tracer/tracer.js b/lib/tracer/tracer.js index 64c72f95..164869d2 100644 --- a/lib/tracer/tracer.js +++ b/lib/tracer/tracer.js @@ -1,15 +1,18 @@ import { _ } from 'meteor/underscore'; import { objectHasData } from '../common/utils'; -import { CreateUserStack, DefaultUniqueId } from '../utils'; +import { CreateUserStack, DefaultUniqueId, isPromise, last } from '../utils'; import { Ntp } from '../ntp'; +import { EventType, MaxAsyncLevel } from '../constants'; +import { getActiveEvent, getInfo, getStore, MontiAsyncStorage } from '../async/als'; +import assert from 'assert'; - -let eventLogger = Npm.require('debug')('kadira:tracer'); +let eventLogger = require('debug')('kadira:tracer'); let REPETITIVE_EVENTS = {db: true, http: true, email: true, wait: true, async: true, custom: true, fs: true}; let TRACE_TYPES = ['sub', 'method', 'http', 'job']; let MAX_TRACE_EVENTS = 1500; +const EMPTY_OBJECT = Object.freeze({}); export const Tracer = function () { this._filters = []; @@ -50,7 +53,7 @@ Tracer.prototype.start = function (name, type, { return null; } - const traceInfo = { + return { _id: `${sessionId}::${msgId || DefaultUniqueId.get()}`, type, name, @@ -59,20 +62,28 @@ Tracer.prototype.start = function (name, type, { events: [], userId, }; - - return traceInfo; }; -Tracer.prototype.event = function (traceInfo, type, data, metaData) { - // do not allow to proceed, if already completed or errored - let lastEvent = this.getLastEvent(traceInfo); +Tracer.prototype.event = function (trace, type, data, meta) { + // We should never nest start and complete events. + let activeEvent = [EventType.Start, EventType.Complete, EventType.Error].includes(type) ? null : getActiveEvent(); + + const level = activeEvent ? activeEvent.level + 1 : 0; + + if (level > MaxAsyncLevel) { + return false; + } + + // We should not nest based on the last event, + // but based on the active event of the current context, due to parallel execution. + const lastEvent = last(trace.events); + // Do not allow proceeding, if already completed or errored if ( - // trace completed but has not been processed - lastEvent && - ['complete', 'error'].indexOf(lastEvent.type) >= 0 || - // trace completed and processed. - traceInfo.isEventsProcessed + // Trace completed but has not been processed + [EventType.Complete, EventType.Error].includes(lastEvent?.type) || + // Trace completed and processed. + trace.isEventsProcessed ) { return false; } @@ -80,75 +91,143 @@ Tracer.prototype.event = function (traceInfo, type, data, metaData) { let event = { type, at: Ntp._now(), - endAt: null, - nested: [], + level, }; + if (meta && meta.name) { + event.name = meta.name; + } + // special handling for events that are not repetitive if (!REPETITIVE_EVENTS[type]) { event.endAt = event.at; } if (data) { - let info = _.pick(traceInfo, 'type', 'name'); + let info = { type: trace.type, name: trace.name }; event.data = this._applyFilters(type, data, info, 'start'); } - if (metaData && metaData.name) { - event.name = metaData.name; - } - if (Kadira.options.eventStackTrace) { event.stack = CreateUserStack(); } - eventLogger('%s %s', type, traceInfo._id); + event.nested = []; - if (lastEvent && !lastEvent.endAt) { - if (!lastEvent.nested) { - // eslint-disable-next-line no-console - console.error('Monti: invalid trace. Please share the trace below at'); - // eslint-disable-next-line no-console - console.error('Monti: https://github.com/monti-apm/monti-apm-agent/issues/14'); - // eslint-disable-next-line no-console - console.dir(traceInfo, { depth: 10 }); - } - let lastNested = lastEvent.nested[lastEvent.nested.length - 1]; + eventLogger('%s %s', type, trace._id); - // Only nest one level - if (!lastNested || lastNested.endAt) { - lastEvent.nested.push(event); - return event; - } - - return false; + if (activeEvent) { + activeEvent.nested.push(event); + return event; } - traceInfo.events.push(event); + trace.events.push(event); return event; }; -Tracer.prototype.eventEnd = function (traceInfo, event, data) { +// Sets the current event for nesting events +// The returned function resets the active event to the previous value +// It must be synchronously called - the value will still be preserved +// for any async resources created before it was reset. +Tracer.prototype.setActiveEvent = function (event) { + let prev = MontiAsyncStorage.getStore(); + + if (!prev || !event) { + return () => {}; + } + + let newValue = Object.assign({}, prev, { activeEvent: event }); + + MontiAsyncStorage.enterWith(newValue); + + return function reset () { + MontiAsyncStorage.enterWith(prev); + }; +}; + +// Pass the returned value from a function as result +// If the function returned a promise, ends the event once the promise +// is settled. Otherwise, the function was sync so the event is ended +// immediately +Tracer.prototype.endAfterResult = function (event, result) { + const { info } = getStore(); + + if (!event || !info) { + return; + } + + if (isPromise(result)) { + result.then(() => { + this.eventEnd(info.trace, event); + }, (err) => { + this.eventEnd(info.trace, event, { err: err.message }); + }); + } else { + this.eventEnd(info.trace, event); + } +}; + +Tracer.prototype.asyncEvent = function (type, data, fn) { + const { info } = getStore(); + + if (!info) { + return fn(false); + } + + const event = this.event(info.trace, type, data); + + let reset = this.setActiveEvent(event); + let result; + + try { + result = fn(event); + } catch (err) { + this.eventEnd(info.trace, event, { err: err.message }); + throw err; + } finally { + this.endAfterResult(event, result); + reset(); + } + + return result; +}; + +Tracer.prototype.eventEnd = function (trace, event, data) { + if (!event) { + return false; + } + if (event.endAt) { - // Event already ended or is not a repititive event + // Event already ended or is not a repetitive event return false; } event.endAt = Ntp._now(); if (data) { - let info = _.pick(traceInfo, 'type', 'name'); + let info = { type: trace.type, name: trace.name }; event.data = Object.assign( event.data || {}, this._applyFilters(`${event.type}end`, data, info, 'end') ); } - eventLogger('%s %s', `${event.type}end`, traceInfo._id); + + eventLogger('%s %s', `${event.type}end`, trace._id); return true; }; +Tracer.prototype.asyncEventEnd = function (event, data) { + const info = getInfo(); + + if (!info) { + return; + } + + this.eventEnd(info.trace, event, data); +}; + Tracer.prototype.getLastEvent = function (traceInfo) { return traceInfo.events[traceInfo.events.length - 1]; }; @@ -168,199 +247,228 @@ Tracer.prototype.endLastEvent = function (traceInfo) { // which is not helpful. This returns true if // there are nested events other than async. Tracer.prototype._hasUsefulNested = function (event) { - return event.nested && - event.nested.length && - !event.nested.every(e => e.type === 'async'); -}; - -Tracer.prototype.buildEvent = function (event, depth = 0, trace) { - let elapsedTimeForEvent = event.endAt - event.at; - let builtEvent = [event.type]; - let nested = []; - - builtEvent.push(elapsedTimeForEvent); - builtEvent.push(event.data || {}); - - if (this._hasUsefulNested(event)) { - let prevEnd = event.at; - for (let i = 0; i < event.nested.length; i++) { - let nestedEvent = event.nested[i]; - if (!nestedEvent.endAt) { - this.eventEnd(trace, nestedEvent); - nestedEvent.forcedEnd = true; - } - - let computeTime = nestedEvent.at - prevEnd; - if (computeTime > 0) { - nested.push(['compute', computeTime]); - } - - nested.push(this.buildEvent(nestedEvent, depth + 1, trace)); - prevEnd = nestedEvent.endAt; - } - } - - - if ( - nested.length || - event.stack || - event.forcedEnd || - event.name - ) { - builtEvent.push({ - stack: event.stack, - nested: nested.length ? nested : undefined, - forcedEnd: event.forcedEnd, - name: event.name - }); + if (!event.nested || event.nested.length === 0) { + return false; } - return builtEvent; + return event.type === Event.Custom || + event.nested.some(e => e.type !== 'async'); }; Tracer.prototype.buildTrace = function (traceInfo) { + let metrics = { + compute: 0, + async: 0, + }; + let firstEvent = traceInfo.events[0]; - let lastEvent = traceInfo.events[traceInfo.events.length - 1]; - let processedEvents = []; + let lastEvent = last(traceInfo.events); - if (firstEvent.type !== 'start') { + if (firstEvent.type !== EventType.Start) { // eslint-disable-next-line no-console console.warn('Monti APM: trace has not started yet'); return null; - } else if (lastEvent.type !== 'complete' && lastEvent.type !== 'error') { - // trace is not completed or errored yet + } else if (lastEvent.type !== EventType.Complete && lastEvent.type !== EventType.Error) { // eslint-disable-next-line no-console console.warn('Monti APM: trace has not completed or errored yet'); return null; } + // build the metrics - traceInfo.errored = lastEvent.type === 'error'; + traceInfo.errored = lastEvent.type === EventType.Error; traceInfo.at = firstEvent.at; + traceInfo.endAt = lastEvent.endAt || lastEvent.at; - let metrics = { - total: lastEvent.at - firstEvent.at, - }; - - let totalNonCompute = 0; - - firstEvent = ['start', 0]; - if (traceInfo.events[0].data) { - firstEvent.push(traceInfo.events[0].data); - } - processedEvents.push(firstEvent); - - let computeTime; - - for (let lc = 1; lc < traceInfo.events.length - 1; lc += 1) { - let prevEvent = traceInfo.events[lc - 1]; - let event = traceInfo.events[lc]; - - if (!event.endAt) { - // eslint-disable-next-line no-console - console.error('Monti APM: no end event for type: ', event.type); - return null; - } + const processedEvents = this.optimizeEvents(traceInfo.events, metrics); - computeTime = event.at - prevEvent.endAt; - if (computeTime > 0) { - processedEvents.push(['compute', computeTime]); - } - let builtEvent = this.buildEvent(event, 0, traceInfo); - processedEvents.push(builtEvent); + metrics.total = traceInfo.endAt - firstEvent.at; - metrics[event.type] = metrics[event.type] || 0; - metrics[event.type] += builtEvent[1]; - totalNonCompute += builtEvent[1]; - } - - - computeTime = lastEvent.at - traceInfo.events[traceInfo.events.length - 2].endAt; - - if (computeTime > 0) { - processedEvents.push(['compute', computeTime]); - } - - let lastEventData = [lastEvent.type, 0]; - if (lastEvent.data) { - lastEventData.push(lastEvent.data); - } - processedEvents.push(lastEventData); - - if (processedEvents.length > MAX_TRACE_EVENTS) { - const removeCount = processedEvents.length - MAX_TRACE_EVENTS; - processedEvents.splice(MAX_TRACE_EVENTS, removeCount); - } - - metrics.compute = metrics.total - totalNonCompute; traceInfo.metrics = metrics; traceInfo.events = processedEvents; traceInfo.isEventsProcessed = true; + traceInfo.parallel = true; + return traceInfo; }; +function incrementMetric (metrics, name, value) { + if (!metrics || value === 0) { + return; + } + + metrics[name] = metrics[name] || 0; + metrics[name] += value; +} + /** - * There are two formats for traces. While the method/publication is running, the trace is in the object format. - * This is easier to work with, but takes more space to store. After the trace is complete (either finished or errored), + * There are two formats for traces. While the method/publication is running, + * the trace is in the object format. This is easier to work with, but takes more + * space to store. After the trace is complete (either finished or errored), * it is built which among other things converts the events to the array format. * - * The key difference of `optimizeEvent` and `optimizeEvents` is that they do not mutate the original events. + * optimizeEvent does not mutate the events, so it can safely be called + * multiple times for the same trace * * @param {Object} objectEvent Expanded object event. * - * @returns {Array} Array notation of the event optimized for transport + * @param {Number} prevEnded The timestamp the previous event ended + * @param {Object} nestedMetrics A metrics object to be updated for nested events + * Only include if the nested events should affect metrics + * @returns {Array} Array notation of the event optimized for transport. */ -Tracer.prototype.optimizeEvent = function (objectEvent) { - let {at, endAt, stack, nested = [], forcedEnd, name, type, data} = objectEvent; - - if (!endAt) { - endAt = Ntp._now(); - forcedEnd = true; +Tracer.prototype.optimizeEvent = function (objectEvent, prevEnded, nestedMetrics) { + if (Array.isArray(objectEvent)) { + // If it is an array, it is already optimized + return objectEvent; } - let duration = at && endAt ? endAt - at : 0; + let { at, endAt, stack, nested = [], name, forcedEnd, type, data } = objectEvent; + let isCustom = type === EventType.Custom; - const optimizedNestedEvents = this._hasUsefulNested(objectEvent) ? this.optimizeEvents(nested) : undefined; - - const optimizedEvent = [type, duration, data || {}]; + // Unused data is removed at the end + const optimizedEvent = [type, 0, EMPTY_OBJECT, null]; const extraInfo = { stack, - forcedEnd, name, - nested: optimizedNestedEvents + forcedEnd, }; + // Start and end events do not have duration or nested events. + if (![EventType.Complete, EventType.Start].includes(type)) { + if (!endAt) { + const info = getInfo(); + // In some tests, we don't have a trace + endAt = info?.trace?.endAt || Ntp._now(); + extraInfo.forcedEnd = true; + } + + let duration = endAt - at; + optimizedEvent[1] = duration; + + let offset = prevEnded - at; + if (offset < 0) { + throw new Error('Monti APM: unexpected gap between events'); + } else if (offset > 0) { + extraInfo.offset = offset; + } + + if (this._hasUsefulNested(objectEvent)) { + let nestedStartedAt = nested[0].at; + let beginningCompute = nestedStartedAt - at; + extraInfo.nested = this.optimizeEvents(nested, nestedMetrics); + + if (beginningCompute > 0) { + extraInfo.nested.unshift(['compute', beginningCompute]); + incrementMetric(nestedMetrics, 'compute', beginningCompute); + } + + let lastEvent = last(nested); + let lastEndedAt = lastEvent.at + (last(extraInfo.nested)[1] || 0); + let endComputeTime = endAt - lastEndedAt; + if (endComputeTime > 0) { + extraInfo.nested.push(['compute', endComputeTime]); + incrementMetric(nestedMetrics, 'compute', endComputeTime); + } + } else if (isCustom) { + // If there are no nested events, record everything as compute time + extraInfo.nested = [['compute', duration]]; + incrementMetric(nestedMetrics, 'compute', duration); + } + } + + if (data) { + optimizedEvent[2] = data; + } + if (objectHasData(extraInfo)) { - optimizedEvent.push(extraInfo); + optimizedEvent[3] = extraInfo; + } + + // remove unneeded values from end of array + if (optimizedEvent[3]) { + // do nothing, everything is needed + } else if (optimizedEvent[2] !== EMPTY_OBJECT) { + optimizedEvent.length = 3; + } else if (optimizedEvent[1] !== 0) { + optimizedEvent.length = 2; + } else { + optimizedEvent.length = 1; } return optimizedEvent; }; -Tracer.prototype.optimizeEvents = function (events) { - if (!events) { +Tracer.prototype.optimizeEvents = function (events, metrics) { + if (!events || events.length === 0) { return []; } - const optimizedEvents = []; + let processedEvents = []; + + let computeTime; + + processedEvents.push(this.optimizeEvent(events[0], events[0].at, metrics)); + + let previousEnd = events[0].at + (processedEvents[0][1] || 0); + let countedUntil = previousEnd; - let prevEvent = {}; + for (let i = 1; i < events.length; i += 1) { + let event = events[i]; + let isCustomEvent = event.type === EventType.Custom; - events.forEach((event) => { - if (prevEvent.endAt && event.at) { - const computeTime = event.at - prevEvent.endAt; + // There are sometimes extra async events related to the previous event. + // Ignore new async events that end before last event. + // These events don't contribute anything to the metrics or compute events. + if ( + event.type === EventType.Async && + event.endAt && + event.endAt <= previousEnd + ) { + continue; + } - if (computeTime > 0) { - optimizedEvents.push(['compute', computeTime]); + computeTime = event.at - previousEnd; + + if (computeTime > 0) { + processedEvents.push([EventType.Compute, computeTime]); + previousEnd += computeTime; + + if (previousEnd > countedUntil) { + let diff = Math.min(computeTime, previousEnd - countedUntil); + incrementMetric(metrics, 'compute', diff); + countedUntil = previousEnd; } } - optimizedEvents.push(this.optimizeEvent(event)); + let processedEvent = this.optimizeEvent( + event, + previousEnd, + isCustomEvent ? metrics : undefined + ); + processedEvents.push(processedEvent); + + let duration = processedEvent[1] || 0; + previousEnd = event.at + duration; - prevEvent = event; - }); + if (duration > 0 && previousEnd > countedUntil) { + let count = previousEnd - countedUntil; + assert(count <= duration, 'unexpected gap in countedUntil'); + + // The nested events are used instead + if (!isCustomEvent) { + incrementMetric(metrics, processedEvent[0], count); + } + + countedUntil = previousEnd; + } + } + + if (processedEvents.length > MAX_TRACE_EVENTS) { + processedEvents.length = MAX_TRACE_EVENTS; + } - return optimizedEvents; + return processedEvents; }; Tracer.prototype.addFilter = function (filterFn) { diff --git a/lib/tracer/tracer_store.js b/lib/tracer/tracer_store.js index 25bf87d5..ed30a44e 100644 --- a/lib/tracer/tracer_store.js +++ b/lib/tracer/tracer_store.js @@ -1,6 +1,7 @@ import { _ } from 'meteor/underscore'; import { EJSON } from 'meteor/ejson'; -let logger = Npm.require('debug')('kadira:ts'); + +let logger = require('debug')('kadira:ts'); export function TracerStore (options) { options = options || {}; diff --git a/lib/utils.js b/lib/utils.js index 2c1b5ffc..0bbbb713 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -40,7 +40,7 @@ export function CreateUserStack (error) { // Optimized version of apply which tries to call as possible as it can // Then fall back to apply // This is because, v8 is very slow to invoke apply. -export function OptimizedApply (context, fn, args) { +export function optimizedApply (context, fn, args) { let a = args; switch (a.length) { case 0: @@ -107,14 +107,92 @@ export function createHistogram () { } export function pick (obj, keys) { - return keys.reduce((result, key) => { + if (!obj) { + return obj; + } + + const result = {}; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; if (obj[key] !== undefined) { result[key] = obj[key]; } - return result; - }, {}); + } + + return result; +} + +export function omit (obj, keys) { + const keySet = new Set(keys); + const result = {}; + for (let key in obj) { + if (!keySet.has(key)) { + result[key] = obj[key]; + } + } + return result; +} + +export function first (arr) { + return arr[0]; +} + +export function last (arr) { + return arr[arr.length - 1]; +} + +export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const deferrable = () => { + let resolve; + let reject; + + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { + promise, + resolve, + reject + }; +}; + +export const defer = fn => setTimeout(fn, 0); + +export const isPromise = obj => obj && typeof obj.then === 'function'; + +const ignoredStackFrames = [/\(internal\/async_hooks.js/, /^\s+at AsyncHook\.init/]; + +export function stackTrace () { + const restoreLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 10; + + const obj = {}; + Error.captureStackTrace(obj, stackTrace); + const structuredStackTrace = obj.stack; + + Error.stackTraceLimit = restoreLimit; + + return deleteLinesWhichMatchAnyOf(structuredStackTrace, ignoredStackFrames); } +export function deleteLinesWhichMatchAnyOf (string, regexes) { + return string.split('\n').filter(line => !regexes.some(regex => regex.test(line))).join('\n'); +} + +export const isPlainObject = obj => obj && typeof obj === 'object' && !Array.isArray(obj); + +export const cloneDeep = obj => JSON.parse(JSON.stringify(obj)); + +export const cleanTrailingNilValues = arr => { + while (arr.length > 0 && last(arr) === null || last(arr) === undefined) { + arr.pop(); + } +}; + export function calculateWaitedOnTime (messageQueue, startedTime) { let waitedOnTime = 0; @@ -132,3 +210,17 @@ export function calculateWaitedOnTime (messageQueue, startedTime) { return waitedOnTime; } + +export function waitForPackage(name, fn) { + if (Package[name]) { + fn(Package[name]); + } else { + Package._promise(name) + .catch(() => { /* it rejects if the package is not used */ }) + .finally(() => { + if (Package[name]) { + fn(Package[name]); + } + }); + } +} diff --git a/lib/wait_time_builder.js b/lib/wait_time_builder.js index a421babb..04fb46f8 100644 --- a/lib/wait_time_builder.js +++ b/lib/wait_time_builder.js @@ -90,7 +90,8 @@ export class WaitTimeBuilder { let unblocked = false; const self = this; - const wrappedUnblock = function () { + + return function () { if (!unblocked) { const waitTime = Date.now() - started; const key = self._getMessageKey(session.id, msg.id); @@ -112,7 +113,5 @@ export class WaitTimeBuilder { TimeoutManager.clearTimeout(); } }; - - return wrappedUnblock; } } diff --git a/meteor.d.ts b/meteor.d.ts new file mode 100644 index 00000000..0f472a7c --- /dev/null +++ b/meteor.d.ts @@ -0,0 +1,11 @@ +declare module Meteor { + let _isFibersEnabled: boolean + + function _getAslStore(): Record + + function _runAsync(func: Function, ctx: object, store: Record): void + + function _getValueFromAslStore(key: string): any + + function _updateAslStore(key: string, value: any): void +} diff --git a/package-lock.json b/package-lock.json index 54f8be5f..6cfb6c41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,379 +1,2543 @@ { "name": "monti-apm-agent", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "packages": { + "": { + "name": "monti-apm-agent", + "version": "0.0.0", + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "chalk": "^4.1.2", + "cli-highlight": "^2.1.11", + "debug": "^4.3.4", + "diff": "^5.1.0", + "eslint": "^8.52.0", + "eslint-config-es": "^0.8.12", + "eslint-plugin-babel": "^5.3.1", + "typescript": "^5.2.2", + "yaml": "^2.3.2" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.14", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.11.2", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.22.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.12", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.52.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-es": { + "version": "0.8.12", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "3.0.3" + } + }, + "node_modules/eslint-plugin-babel": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": ">=4.0.0" + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.15.0", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" } }, - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "node_modules/thenify-all": { + "version": "1.6.0", "dev": true, - "requires": { - "@babel/highlight": "^7.18.6" + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" } }, - "@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", - "dev": true + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } }, - "@babel/core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", - "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", + "node_modules/tslib": { + "version": "1.14.1", "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.2", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-module-transforms": "^7.20.2", - "@babel/helpers": "^7.20.1", - "@babel/parser": "^7.20.2", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "@babel/eslint-parser": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.5.tgz", - "integrity": "sha512-mUqYa46lgWqHKQ33Q6LNCGp/wPR3eqOYTUixHFsfrSQqRxH0+WOzca75iEjFr5RDGH1dDz622LaHhLOzOuQRUA==", + "node_modules/type-fest": { + "version": "0.20.2", "dev": true, - "requires": { - "eslint-scope": "^5.1.1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.0" + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" }, - "dependencies": { - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@babel/generator": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz", - "integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==", + "node_modules/typed-array-buffer": { + "version": "1.0.0", "dev": true, - "requires": { - "@babel/types": "^7.20.2", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", "dev": true, - "requires": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "dev": true, + "license": "MIT", "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true + "node_modules/typescript": { + "version": "5.2.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, - "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "node_modules/unbox-primitive": { + "version": "1.0.2", "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "node_modules/uri-js": { + "version": "4.4.1", "dev": true, - "requires": { - "@babel/types": "^7.18.6" + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, - "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "node_modules/which": { + "version": "2.0.2", "dev": true, - "requires": { - "@babel/types": "^7.18.6" + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "@babel/helper-module-transforms": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", - "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", + "node_modules/which-boxed-primitive": { + "version": "1.0.2", "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2" + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "node_modules/which-typed-array": { + "version": "1.1.13", "dev": true, - "requires": { - "@babel/types": "^7.20.2" + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "node_modules/wrap-ansi": { + "version": "7.0.0", "dev": true, - "requires": { - "@babel/types": "^7.18.6" + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } }, - "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "node_modules/yaml": { + "version": "2.3.2", "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" + "license": "ISC", + "engines": { + "node": ">= 14" } }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "node_modules/yargs": { + "version": "16.2.0", "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, + "license": "MIT", "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" } }, - "@babel/parser": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz", - "integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==", - "dev": true - }, - "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "node_modules/yargs-parser": { + "version": "20.2.9", "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "license": "ISC", + "engines": { + "node": ">=10" } }, - "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", + "node_modules/yocto-queue": { + "version": "0.1.0", "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", - "debug": "^4.1.0", - "globals": "^11.1.0" + "license": "MIT", + "engines": { + "node": ">=10" }, - "dependencies": { - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "dev": true }, - "@babel/types": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz", - "integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==", + "@eslint-community/eslint-utils": { + "version": "4.4.0", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" + "eslint-visitor-keys": "^3.3.0" } }, + "@eslint-community/regexpp": { + "version": "4.10.0", + "dev": true + }, "@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.1.2", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -382,71 +2546,29 @@ "strip-json-comments": "^3.1.1" } }, + "@eslint/js": { + "version": "8.52.0", + "dev": true + }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.13", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" } }, "@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "2.0.1", "dev": true }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, "@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", @@ -455,127 +2577,136 @@ }, "@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true }, "@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, - "@types/bson": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.2.0.tgz", - "integrity": "sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg==", - "dev": true, - "requires": { - "bson": "*" - } + "@types/json-schema": { + "version": "7.0.14", + "dev": true }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "@types/semver": { + "version": "7.5.4", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.62.0", "dev": true, "requires": { - "@types/node": "*" + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" } }, - "@types/jquery": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", - "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", + "@typescript-eslint/parser": { + "version": "5.62.0", "dev": true, "requires": { - "@types/sizzle": "*" + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" } }, - "@types/meteor": { - "version": "1.4.87", - "resolved": "https://registry.npmjs.org/@types/meteor/-/meteor-1.4.87.tgz", - "integrity": "sha512-UU2rJvq3jkCYc//OXL482OLfwYOf8WQTiTRfYiCHJnzvhUHsg0Cx6iSzzThPuFGupp6FgmSeenJBwmt854hh7w==", + "@typescript-eslint/scope-manager": { + "version": "5.62.0", "dev": true, "requires": { - "@types/connect": "*", - "@types/jquery": "*", - "@types/mongodb": "^3.6.20", - "@types/react": "*", - "@types/underscore": "*" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" } }, - "@types/mongodb": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", - "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", + "@typescript-eslint/type-utils": { + "version": "5.62.0", "dev": true, "requires": { - "@types/bson": "*", - "@types/node": "*" + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" } }, - "@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", - "dev": true - }, - "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "@typescript-eslint/types": { + "version": "5.62.0", "dev": true }, - "@types/react": { - "version": "18.0.26", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", - "integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", + "@typescript-eslint/typescript-estree": { + "version": "5.62.0", "dev": true, "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" } }, - "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "@typescript-eslint/utils": { + "version": "5.62.0", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "dev": true + } + } }, - "@types/sizzle": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", - "dev": true + "@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + } }, - "@types/underscore": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.4.tgz", - "integrity": "sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg==", + "@ungap/structured-clone": { + "version": "1.2.0", "dev": true }, "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.11.2", "dev": true }, "acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv": { "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -586,14 +2717,10 @@ }, "ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { "color-convert": "^2.0.1" @@ -601,111 +2728,83 @@ }, "any-promise": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true }, "argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "array-buffer-byte-length": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, "array-includes": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", "dev": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.7.0" } }, + "array-union": { + "version": "2.1.0", + "dev": true + }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, "available-typed-arrays": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, "balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, "brace-expansion": { "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - } - }, - "bson": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", - "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", - "dev": true, - "requires": { - "buffer": "^5.6.0" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "braces": { + "version": "3.0.2", "dev": true, "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "fill-range": "^7.0.1" } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", "dev": true, "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "callsites": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001431", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz", - "integrity": "sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ==", "dev": true }, "chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -714,8 +2813,6 @@ }, "cli-highlight": { "version": "2.1.11", - "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", - "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", "dev": true, "requires": { "chalk": "^4.0.0", @@ -724,24 +2821,10 @@ "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } } }, "cliui": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { "string-width": "^4.2.0", @@ -751,8 +2834,6 @@ }, "color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { "color-name": "~1.1.4" @@ -760,26 +2841,14 @@ }, "color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, "cross-spawn": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -787,12 +2856,6 @@ "which": "^2.0.1" } }, - "csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -804,101 +2867,106 @@ }, "deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "define-data-property": { + "version": "1.1.1", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.1", "dev": true, "requires": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { "esutils": "^2.0.2" } }, - "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", - "dev": true - }, "emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, "es-abstract": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.0.tgz", - "integrity": "sha512-GUGtW7eXQay0c+PRq0sGIKSdaBorfVqsCMhGHo4elP7YVqZu9nCZS4UkK4gv71gOWNMra/PaSKD3ao1oWExO0g==", + "version": "1.22.3", "dev": true, "requires": { - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.0", + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.5", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.4", - "is-array-buffer": "^3.0.0", + "hasown": "^2.0.0", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.13" } }, "es-set-tostringtag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.0.tgz", - "integrity": "sha512-vZVAIWss0FcR/+a08s6e2/GjGjjYBCZJXDrOnj6l5kJCKhQvJs4cnVqUxkVepIhqHbKHm3uwOvPb8lRcqA3DSg==", + "version": "2.0.2", "dev": true, "requires": { - "get-intrinsic": "^1.1.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" } }, "es-to-primitive": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -908,79 +2976,67 @@ }, "escalade": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, "escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, "eslint": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz", - "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==", + "version": "8.52.0", "dev": true, "requires": { - "@eslint/eslintrc": "^1.4.1", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "glob-parent": { + "version": "6.0.2", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "is-glob": "^4.0.3" } } } }, "eslint-config-es": { "version": "0.8.12", - "resolved": "https://registry.npmjs.org/eslint-config-es/-/eslint-config-es-0.8.12.tgz", - "integrity": "sha512-r7nN8/NzRZTuLhWoa6t+HSo1rdfvkVcHYs5QRBQov33AJi5RNXWAvKhr80iuPWwluCv3oDQX4X+ooYPrVvvbXw==", "dev": true, "requires": { "array-includes": "3.0.3" @@ -988,8 +3044,6 @@ }, "eslint-plugin-babel": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz", - "integrity": "sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==", "dev": true, "requires": { "eslint-rule-composer": "^0.3.0" @@ -997,58 +3051,31 @@ }, "eslint-rule-composer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", - "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", "dev": true }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", "dev": true }, "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.6.1", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" } }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -1056,8 +3083,6 @@ }, "esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { "estraverse": "^5.2.0" @@ -1065,38 +3090,37 @@ }, "estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, "esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, "fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.3.1", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, "fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, "fastq": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -1104,17 +3128,20 @@ }, "file-entry-cache": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { "flat-cache": "^3.0.4" } }, + "fill-range": { + "version": "7.0.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { "locate-path": "^6.0.0", @@ -1122,25 +3149,20 @@ } }, "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", "dev": true, "requires": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", "dev": true }, "for-each": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, "requires": { "is-callable": "^1.1.3" @@ -1148,61 +3170,42 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", "dev": true }, "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, "functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.2", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, "get-symbol-description": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1211,8 +3214,6 @@ }, "glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -1224,18 +3225,14 @@ } }, "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "version": "5.1.2", "dev": true, "requires": { - "is-glob": "^4.0.3" + "is-glob": "^4.0.1" } }, "globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "version": "13.23.0", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -1243,41 +3240,36 @@ }, "globalthis": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", "dev": true, "requires": { "define-properties": "^1.1.3" } }, + "globby": { + "version": "11.1.0", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "gopd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, "requires": { "get-intrinsic": "^1.1.3" } }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "graphemer": { + "version": "1.4.0", "dev": true }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, "has-bigints": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, "has-flag": { @@ -1287,57 +3279,44 @@ "dev": true }, "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.1", "dev": true, "requires": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" } }, "has-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "dev": true }, "has-symbols": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true }, "has-tostringtag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dev": true, "requires": { "has-symbols": "^1.0.2" } }, + "hasown": { + "version": "2.0.0", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, "highlight.js": { "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "dev": true - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true }, "ignore": { "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, "import-fresh": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -1346,14 +3325,10 @@ }, "imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { "once": "^1.3.0", @@ -1362,35 +3337,28 @@ }, "inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, "internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.0.6", "dev": true, "requires": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" } }, "is-array-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.0.tgz", - "integrity": "sha512-TI2hnvT6dPUnn/jARFCJBKL1eeabAfLnKZ2lmW5Uh317s1Ii2IMroL1yMciEk/G+OETykVzlsH6x/L4q/avhgw==", + "version": "3.0.2", "dev": true, "requires": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" } }, "is-bigint": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, "requires": { "has-bigints": "^1.0.1" @@ -1398,8 +3366,6 @@ }, "is-boolean-object": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1408,14 +3374,10 @@ }, "is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true }, "is-date-object": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "requires": { "has-tostringtag": "^1.0.0" @@ -1423,20 +3385,14 @@ }, "is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -1444,14 +3400,14 @@ }, "is-negative-zero": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", "dev": true }, "is-number-object": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dev": true, "requires": { "has-tostringtag": "^1.0.0" @@ -1459,14 +3415,10 @@ }, "is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, "is-regex": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1475,8 +3427,6 @@ }, "is-shared-array-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", "dev": true, "requires": { "call-bind": "^1.0.2" @@ -1484,8 +3434,6 @@ }, "is-string": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, "requires": { "has-tostringtag": "^1.0.0" @@ -1493,90 +3441,63 @@ }, "is-symbol": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dev": true, "requires": { "has-symbols": "^1.0.2" } }, "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" } }, "is-weakref": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "requires": { "call-bind": "^1.0.2" } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "isexe": { + "version": "2.0.0", "dev": true }, "js-yaml": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { "argparse": "^2.0.1" } }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "json-buffer": { + "version": "3.0.1", "dev": true }, "json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true + "keyv": { + "version": "4.5.4", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } }, "levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { "prelude-ls": "^1.2.1", @@ -1585,8 +3506,6 @@ }, "locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { "p-locate": "^5.0.0" @@ -1594,14 +3513,31 @@ }, "lodash.merge": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "merge2": { + "version": "1.4.1", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, "minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -1615,8 +3551,6 @@ }, "mz": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "requires": { "any-promise": "^1.0.0", @@ -1626,38 +3560,26 @@ }, "natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "natural-compare-lite": { + "version": "1.4.0", "dev": true }, "object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.13.1", "dev": true }, "object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, "object.assign": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1668,31 +3590,25 @@ }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { "wrappy": "1" } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { "yocto-queue": "^0.1.0" @@ -1700,8 +3616,6 @@ }, "p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { "p-limit": "^3.0.2" @@ -1709,8 +3623,6 @@ }, "parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "requires": { "callsites": "^3.0.0" @@ -1718,14 +3630,10 @@ }, "parse5": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", "dev": true }, "parse5-htmlparser2-tree-adapter": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", "dev": true, "requires": { "parse5": "^6.0.1" @@ -1733,93 +3641,65 @@ "dependencies": { "parse5": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true } } }, "path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, "path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "path-type": { + "version": "4.0.0", + "dev": true + }, + "picomatch": { + "version": "2.3.1", "dev": true }, "prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", "dev": true }, "queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.1", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "set-function-name": "^2.0.0" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, "require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, "resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, "reusify": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, "rimraf": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -1827,17 +3707,23 @@ }, "run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "requires": { "queue-microtask": "^1.2.2" } }, + "safe-array-concat": { + "version": "1.0.1", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, "safe-regex-test": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1845,10 +3731,34 @@ "is-regex": "^1.1.4" } }, + "semver": { + "version": "7.5.4", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "set-function-length": { + "version": "1.1.1", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "set-function-name": { + "version": "2.0.1", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, "shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { "shebang-regex": "^3.0.0" @@ -1856,14 +3766,10 @@ }, "shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "side-channel": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, "requires": { "call-bind": "^1.0.0", @@ -1871,10 +3777,12 @@ "object-inspect": "^1.9.0" } }, + "slash": { + "version": "3.0.0", + "dev": true + }, "string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -1882,32 +3790,35 @@ "strip-ansi": "^6.0.1" } }, + "string.prototype.trim": { + "version": "1.2.8", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + } + }, "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { "ansi-regex": "^5.0.1" @@ -1915,8 +3826,6 @@ }, "strip-json-comments": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "supports-color": { @@ -1930,14 +3839,10 @@ }, "text-table": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, "thenify": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { "any-promise": "^1.0.0" @@ -1945,23 +3850,31 @@ }, "thenify-all": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, "requires": { "thenify": ">= 3.1.0 < 4" } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "1.14.1", "dev": true }, + "tsutils": { + "version": "3.21.0", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { "prelude-ls": "^1.2.1" @@ -1969,14 +3882,40 @@ }, "type-fest": { "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typed-array-buffer": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, "typed-array-length": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1984,10 +3923,12 @@ "is-typed-array": "^1.1.9" } }, + "typescript": { + "version": "5.2.2", + "dev": true + }, "unbox-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -1996,20 +3937,8 @@ "which-boxed-primitive": "^1.0.2" } }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, "uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -2017,8 +3946,6 @@ }, "which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -2026,8 +3953,6 @@ }, "which-boxed-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, "requires": { "is-bigint": "^1.0.1", @@ -2038,29 +3963,18 @@ } }, "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.13", "dev": true, "requires": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.4", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" } }, - "word-wrap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", - "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { "ansi-styles": "^4.0.0", @@ -2070,26 +3984,24 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, "y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, "yaml": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", "dev": true }, "yargs": { "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { "cliui": "^7.0.2", @@ -2103,14 +4015,10 @@ }, "yargs-parser": { "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, "yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true } } diff --git a/package.js b/package.js index 067c388a..873b3728 100644 --- a/package.js +++ b/package.js @@ -2,14 +2,14 @@ Package.describe({ summary: 'Performance Monitoring for Meteor', - version: '2.50.0', + version: '3.0.0-beta.14', git: 'https://github.com/monti-apm/monti-apm-agent.git', name: 'montiapm:agent' }); let npmModules = { debug: '0.8.1', - 'monti-apm-core': '1.8.0', + '@monti-apm/core': '2.0.0-beta.11', 'lru-cache': '5.1.1', 'json-stringify-safe': '5.0.1', 'monti-apm-sketches-js': '0.0.3', @@ -33,9 +33,7 @@ Package.onTest(function (api) { configurePackage(api, true); api.use([ - 'peerlibrary:reactive-publish', 'tinytest', - 'test-helpers', ], ['client', 'server']); // common before @@ -60,7 +58,6 @@ Package.onTest(function (api) { 'tests/hijack/user.js', 'tests/hijack/email.js', 'tests/hijack/base.js', - 'tests/hijack/async.js', 'tests/hijack/webapp.js', 'tests/hijack/http.js', 'tests/hijack/db.js', @@ -85,9 +82,7 @@ Package.onTest(function (api) { 'tests/event_loop_monitor.js', ], 'server'); - if (canRunTestsWithFetch()) { - api.addFiles(['tests/hijack/http_fetch.js'], 'server'); - } + api.addFiles(['tests/hijack/http_fetch.js'], 'server'); // common client api.addFiles([ @@ -108,32 +103,17 @@ Package.onTest(function (api) { ], ['client', 'server']); }); -// use meteor/fetch in tests only for NodeJS 8.11+ (Meteor 1.7+) -function canRunTestsWithFetch () { - const nums = process.versions.node.split('.').map(Number); - - const major = nums[0]; - const minor = nums[1]; - - if (major < 8) return false; - - if (major > 8) return true; - - // major === 8 and ... - return minor >= 11; -} - function configurePackage (api, isTesting) { - api.versionsFrom('METEOR@1.4'); + api.versionsFrom('METEOR@3.0'); + api.use('montiapm:meteorx@2.3.1', ['server']); if (isTesting && process.env.REDIS_OPLOG_SETTINGS) { api.use([ - 'cultofcoders:redis-oplog@2.2.1', + 'cultofcoders:redis-oplog@3.0.0', 'disable-oplog@1.0.7' ], ['server']); } - api.use('montiapm:meteorx@2.2.0', ['server']); api.use('meteorhacks:zones@1.2.1', { weak: true }); api.use('simple:json-routes@2.1.0', { weak: true }); api.use('zodern:meteor-package-versions@0.2.0'); @@ -143,10 +123,13 @@ function configurePackage (api, isTesting) { 'minimongo', 'mongo', 'ddp', 'ejson', 'ddp-common', 'underscore', 'random', 'webapp', 'ecmascript' ], ['server']); - api.use(['http@1.0.0||2.0.0', 'email@1.0.0||2.0.0'], 'server', { weak: !isTesting }); - api.use('fetch@0.1.0', 'server', { - weak: !(isTesting && canRunTestsWithFetch()), + if (isTesting) { + api.use(['http@3.0.0', 'email'], 'server'); + } + + api.use('fetch', 'server', { + weak: !isTesting, }); api.use(['random', 'ecmascript', 'tracker'], ['client']); @@ -183,16 +166,16 @@ function configurePackage (api, isTesting) { 'lib/hijack/wrap_observers.js', 'lib/hijack/wrap_ddp_stringify.js', 'lib/hijack/instrument.js', - 'lib/hijack/db.js', + 'lib/hijack/db/index.js', 'lib/hijack/http.js', 'lib/hijack/email.js', - 'lib/hijack/async.js', 'lib/hijack/error.js', 'lib/hijack/set_labels.js', 'lib/hijack/redis_oplog.js', 'lib/environment_variables.js', 'lib/auto_connect.js', 'lib/conflicting_agents.js', + 'lib/async/async-hook.js', ], 'server'); if (isTesting && process.env.REDIS_OPLOG_SETTINGS) { diff --git a/package.json b/package.json index 4cd0c766..1f919b10 100644 --- a/package.json +++ b/package.json @@ -3,24 +3,26 @@ "private": true, "version": "0.0.0", "scripts": { - "test": "FORCE_COLOR=true meteor test-packages ./", + "test": "FORCE_COLOR=true meteor --release=3.0 test-packages ./ ", + "test:redis": "REDIS_OPLOG_SETTINGS='{\"debug\":true}' FORCE_COLOR=true meteor --release=3.0 test-packages ./ ", + "test:local": "METEOR_PACKAGE_DIRS=./packages meteor test-packages ./ --release=3.0", "lint": "eslint . --cache . --ext .js", "publish": "npm prune --production && meteor publish" }, "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/eslint-parser": "^7.16.5", - "@types/meteor": "^1.4.87", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "debug": "^4.3.4", "chalk": "^4.1.2", "cli-highlight": "^2.1.11", "diff": "^5.1.0", - "eslint": "^8.31.0", + "eslint": "^8.52.0", "eslint-config-es": "^0.8.12", "eslint-plugin-babel": "^5.3.1", - "yaml": "^2.3.2" + "yaml": "^2.3.2", + "typescript": "^5.2.2" }, - "dependencies": {}, "volta": { - "node": "14.21.2" + "node": "14.21.3" } } diff --git a/settings.json b/settings.json new file mode 100644 index 00000000..4555ff28 --- /dev/null +++ b/settings.json @@ -0,0 +1,3 @@ +{ + "redisOplog": {"debug": true} +} \ No newline at end of file diff --git a/tests/_helpers/helpers.js b/tests/_helpers/helpers.js index 3b858bc9..8eccc131 100644 --- a/tests/_helpers/helpers.js +++ b/tests/_helpers/helpers.js @@ -2,15 +2,24 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { DDP } from 'meteor/ddp'; import { MethodStore, TestData } from './globals'; +import { EJSON } from 'meteor/ejson'; +import { EventType } from '../../lib/constants'; +import { cleanTrailingNilValues, cloneDeep, isPlainObject, last, sleep } from '../../lib/utils'; +import { isNumber } from '../../lib/common/utils'; +import { diffObjects } from './pretty-log'; +import util from 'util'; -const Future = Npm.require('fibers/future'); +const _client = DDP.connect(Meteor.absoluteUrl(), {retry: false}); -export const GetMeteorClient = function (_url) { +export const callAsync = async (method, ...args) => _client.callAsync(method, ...args); +export const clientCallAsync = async (client, method, ...args) => client.callAsync(method, ...args); + +export const getMeteorClient = function (_url) { const url = _url || Meteor.absoluteUrl(); return DDP.connect(url, {retry: false, }); }; -export const waitForConnection = function (client) { +export const waitForConnection = async function (client) { let timeout = Date.now() + 1000; while (Date.now() < timeout) { let status = client.status(); @@ -18,7 +27,7 @@ export const waitForConnection = function (client) { return; } - Meteor._sleepForMs(50); + await sleep(50); } throw new Error('timed out waiting for connection'); @@ -32,9 +41,11 @@ export const RegisterMethod = function (F) { return id; }; -export const RegisterPublication = function (F) { - let id = `test_${Random.id()}`; - Meteor.publish(id, F); +export const registerMethod = RegisterMethod; + +export const registerPublication = function (func) { + const id = `test_${Random.id()}`; + Meteor.publish(id, func); return id; }; @@ -46,31 +57,90 @@ export const EnableTrackingMethods = function () { // }; }; -export const GetLastMethodEvents = function (_indices, ignore = []) { +export const getLastMethodTrace = () => { if (MethodStore.length < 1) { return []; } - let indices = _indices || [0]; - let events = MethodStore[MethodStore.length - 1].events; + return MethodStore[MethodStore.length - 1]; +}; + + +export const getMethodEvents = () => last(MethodStore).events; + +export function getLastMethodEvents (indices = [0], keysToPreserve = []) { + if (MethodStore.length < 1) { + return []; + } + + let events = last(MethodStore).events; + events = Array.prototype.slice.call(events, 0); - events = events.filter(isNotCompute); + events = events.filter(isNotCompute).filter(isNotEmptyAsync); events = events.map(filterFields); + return events; function isNotCompute (event) { - return event[0] !== 'compute' && !ignore.includes(event[0]); + return event[0] !== EventType.Compute; } - function filterFields (event) { - let filteredEvent = []; - indices.forEach(function (index) { - if (event[index]) { - filteredEvent[index] = event[index]; + function isNotEmptyAsync (event) { + return event[0] !== EventType.Async || event[3]?.nested?.length > 0; + } + + function clean (_data) { + if (isNumber(_data)) { + return 0; + } + + if (!isPlainObject(_data)) { + return _data; + } + + const data = cloneDeep(_data); + + const rejectedKeys = [ + 'stack' + ]; + + for ( const [key, value] of Object.entries(data)) { + if (rejectedKeys.includes(key)) { + delete data[key]; + continue; } - }); + + if (key === 'nested' && value?.length) { + data[key] = value.filter(isNotCompute).filter(isNotEmptyAsync).map(filterFields); + } + + if (key === 'err' && value?.startsWith('E11000')) { + data[key] = 'E11000'; + } + + // In tests, we can't use numbers like timestamps, + // but it is still useful to know if a number is positive or negative + // i.e. when an interval subtraction when awry + if (isNumber(value) && !keysToPreserve.includes(key)) { + if (value > 0) { + data[key] = 1; + } else if (value < 0) { + data[key] = -1; + } else { + data[key] = 0; + } + } + } + + return data; + } + + function filterFields (event) { + let filteredEvent = indices.map((index) => clean(event[index])); + cleanTrailingNilValues(filteredEvent); + return filteredEvent; } -}; +} export const GetPubSubMetrics = function () { let metricsArr = []; @@ -94,7 +164,7 @@ export const FindMetricsForPub = function (pubname) { return candidates[candidates.length - 1]; }; -export const GetPubSubPayload = function (detailInfoNeeded) { +export const getPubSubPayload = function (detailInfoNeeded) { return Kadira.models.pubsub.buildPayload(detailInfoNeeded).pubMetrics; }; @@ -113,54 +183,38 @@ export function findMetricsForMethod (name) { } export const Wait = function (time) { - let f = new Future(); - Meteor.setTimeout(function () { - f.return(); - }, time); - f.wait(); + return new Promise((resolve) => { + setTimeout(resolve, time); + }); }; -export const CleanTestData = function () { +export const CleanTestData = async function () { MethodStore.length = 0; - TestData.remove({}); + await TestData.removeAsync({}); Kadira.models.pubsub.metricsByMinute = {}; Kadira.models.pubsub.subscriptions = {}; Kadira.models.jobs.jobMetricsByMinute = {}; Kadira.models.jobs.activeJobCounts.clear(); }; -export const SubscribeAndWait = function (client, name, args) { - let f = new Future(); - args = Array.prototype.splice.call(arguments, 1); - args.push({ - onError (err) { - f.return(err); - }, - onReady () { - f.return(); - } - }); +export const cleanTestData = CleanTestData; - // eslint-disable-next-line prefer-spread - let handler = client.subscribe.apply(client, args); - let error = f.wait(); +export const subscribeAndWait = function (client, name, args) { + return new Promise((resolve, reject) => { + let sub = null; - if (error) { - throw error; - } else { - return handler; - } -}; + args = Array.prototype.splice.call(arguments, 1); -export const callPromise = function (client, ...args) { - return new Promise((resolve, reject) => { - client.call(...args, (err, result) => { - if (err) { - return reject(err); + args.push({ + onError (err) { + reject(err); + }, + onReady () { + resolve(sub); } - - resolve(result); }); + + sub = client.subscribe(...args); }); }; @@ -190,32 +244,39 @@ export function compareNear (v1, v2, maxDifference) { return isNear; } -export const CloseClient = function (client) { - let sessionId = client._lastSessionId; - client.disconnect(); - let f = new Future(); - function checkClientExtence (_sessionId) { - let sessionExists; - if (Meteor.server.sessions instanceof Map) { - // Meteor 1.8.1 and newer - sessionExists = Meteor.server.sessions.has(_sessionId); - } else { - sessionExists = Meteor.server.sessions[_sessionId]; - } +export const closeClient = function (client) { + return new Promise((resolve) => { + let sessionId = client._lastSessionId; - if (sessionExists) { - setTimeout(function () { - checkClientExtence(_sessionId); - }, 20); - } else { - f.return(); + Object.entries(client._subscriptions).forEach(([, sub]) => { + sub?.stop(); + }); + + client.disconnect(); + + function checkClientExistence (_sessionId) { + let sessionExists; + if (Meteor.server.sessions instanceof Map) { + // Meteor 1.8.1 and newer + sessionExists = Meteor.server.sessions.has(_sessionId); + } else { + sessionExists = Meteor.server.sessions[_sessionId]; + } + + if (sessionExists) { + setTimeout(function () { + checkClientExistence(_sessionId); + }, 20); + } else { + resolve(); + } } - } - checkClientExtence(sessionId); - return f.wait(); + + checkClientExistence(sessionId); + }); }; -export const WithDocCacheGetSize = function (fn, patchedSize) { +export const withDocCacheGetSize = async function (fn, patchedSize) { let original = Kadira.docSzCache.getSize; Kadira.docSzCache.getSize = function () { @@ -223,7 +284,7 @@ export const WithDocCacheGetSize = function (fn, patchedSize) { }; try { - fn(); + await fn(); } finally { Kadira.docSzCache.getSize = original; } @@ -233,7 +294,38 @@ export const WithDocCacheGetSize = function (fn, patchedSize) { let release = Meteor.release === 'none' ? 'METEOR@2.5.0' : Meteor.release; export const releaseParts = release.split('METEOR@')[1].split('.').map(num => parseInt(num, 10)); -export const withRoundedTime = (fn) => (test) => { + +const asyncTest = fn => async (test, done) => { + await cleanTestData(); + + const client = getMeteorClient(); + + test.stableEqual = (a, b) => { + const _a = EJSON.parse(EJSON.stringify(a)); + const _b = EJSON.parse(EJSON.stringify(b)); + + if (!util.isDeepStrictEqual(_a, _b)) { + dumpEvents(a); + + diffObjects(a, b); + } + + test.equal(_a, _b); + }; + + // Cleans stuff from the test engine. + Kadira._setInfo(null); + + await fn(test, client); + + await closeClient(client); + + await cleanTestData(); + + done(); +}; + +export const withRoundedTime = (fn) => async (test, done) => { const date = new Date(); date.setSeconds(0,0); const timestamp = date.getTime(); @@ -242,13 +334,112 @@ export const withRoundedTime = (fn) => (test) => { Date.now = () => timestamp; - fn(test); + await asyncTest(fn)(test, () => {}); Date.now = old; + + done(); }; export function addTestWithRoundedTime (name, fn) { - Tinytest.add(name, withRoundedTime(fn)); + Tinytest.addAsync(name, withRoundedTime(fn)); +} + +addTestWithRoundedTime.only = function (name, fn) { + Tinytest.onlyAsync(name, withRoundedTime(fn)); +}; + +addTestWithRoundedTime.skip = function () {}; + +export function addAsyncTest (name, fn) { + Tinytest.addAsync(name, asyncTest(fn)); +} + +addAsyncTest.only = function (name, fn) { + Tinytest.onlyAsync(name, asyncTest(fn)); +}; + +addAsyncTest.skip = function () {}; + +export function cleanTrace (trace) { + delete trace.rootAsyncId; + + cleanEvents(trace.events); +} + +export function cleanEvents (events) { + events?.forEach(function (event) { + if (event.endAt > event.at) { + event.endAt = 10; + } else if (event.endAt) { + delete event.endAt; + } + + delete event.at; + delete event._id; + delete event.asyncId; + delete event.triggerAsyncId; + delete event.level; + delete event.duration; + + if (event.nested?.length === 0) { + delete event.nested; + } else { + cleanEvents(event.nested); + } + }); +} + +export function cleanBuiltEvents (events, roundTo = 10) { + return events + .filter(event => event[0] !== 'compute' || event[1] > 5) + .map(event => { + let [, duration, , details] = event; + if (typeof duration === 'number') { + // round down to nearest 10 + event[1] = Math.floor(duration / roundTo) * roundTo; + } + + if (details) { + delete details.at; + delete details.endAt; + if (details.nested) { + details.nested = cleanBuiltEvents(details.nested, roundTo); + } + + // We only care about the properties that survive being stringified + // (are not undefined) + event[3] = JSON.parse(JSON.stringify(details)); + if (event[3].offset) { + // round down to nearest 10 + event[3].offset = Math.floor(event[3].offset / roundTo) * roundTo; + } + } + + return event; + }); +} + +export const dumpEvents = (events) => { + console.log(JSON.stringify(events)); +}; + +export function deepFreeze (obj) { + if (Array.isArray(obj)) { + obj.forEach(val => { + if (!Object.isFrozen(val)) { + deepFreeze(val); + } + }); + } else { + Object.values(obj).forEach(val => { + if (!Object.isFrozen(val)) { + deepFreeze(val); + } + }); + } + + Object.freeze(obj); } addTestWithRoundedTime.only = (name, fn) => { @@ -260,20 +451,18 @@ export const isRedisOplogEnabled = !!process.env.REDIS_OPLOG_SETTINGS; export const TestHelpers = { methodStore: MethodStore, getLatestEventsFromMethodStore: () => MethodStore[MethodStore.length - 1].events, - getMeteorClient: GetMeteorClient, + getMeteorClient, registerMethod: RegisterMethod, - registerPublication: RegisterPublication, + registerPublication, enableTrackingMethods: EnableTrackingMethods, - getLastMethodEvents: GetLastMethodEvents, getPubSubMetrics: GetPubSubMetrics, findMetricsForPub: FindMetricsForPub, - getPubSubPayload: GetPubSubPayload, + getPubSubPayload, wait: Wait, cleanTestData: CleanTestData, - subscribeAndWait: SubscribeAndWait, + subscribeAndWait, compareNear, - closeClient: CloseClient, - withDocCacheGetSize: WithDocCacheGetSize, + closeClient, withRoundedTime, addTestWithRoundedTime, }; diff --git a/tests/_helpers/http.js b/tests/_helpers/http.js new file mode 100644 index 00000000..42aa2969 --- /dev/null +++ b/tests/_helpers/http.js @@ -0,0 +1,12 @@ + +export const asyncHttpGet = function (url) { + return new Promise((resolve, reject) => { + Package?.['http']?.HTTP?.get(url, function (err, res) { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); +}; diff --git a/tests/_helpers/init.js b/tests/_helpers/init.js index e575f810..0987f64d 100644 --- a/tests/_helpers/init.js +++ b/tests/_helpers/init.js @@ -1,18 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { MethodStore, TestData } from './globals'; +import { sleep } from '../../lib/utils'; Kadira.connect('foo', 'bar', {enableErrorTracking: true}); -let http = Npm.require('http'); -let Future = Npm.require('fibers/future'); -let server3301 = new Future(); -let server8808 = new Future(); +let http = require('http'); http.createServer(function (req, res) { res.writeHead(200); res.end('hello'); -}).listen(3301, server3301.return.bind(server3301)); +}).listen(3301); http.createServer(function (req, res) { let data = ''; @@ -45,23 +43,20 @@ http.createServer(function (req, res) { res.end('internal-error-here'); } }); -}).listen(8808, server8808.return.bind(server8808)); +}).listen(8808); -server3301.wait(); -server8808.wait(); - -// TODO use RegisterPublication instead of these +// TODO use registerPublication instead of these Meteor.publish('tinytest-data', function () { return TestData.find(); }); Meteor.publish('tinytest-data-with-no-oplog', function () { - return TestData.find({}, {_disableOplog: true}); + return TestData.find({}, { disableOplog: true }); }); Meteor.publish('tinytest-data-random', function () { - return TestData.find({aa: {$ne: Random.id()}}); + return TestData.find({ aa: {$ne: Random.id()}}); }); Meteor.publish('tinytest-wait-time', function () { @@ -70,29 +65,30 @@ Meteor.publish('tinytest-wait-time', function () { }); -Meteor.publish('tinytest-waited-on', function () { - Meteor._sleepForMs(25); +Meteor.publish('tinytest-data-cursor-fetch', async function () { + await TestData.find({}).fetchAsync(); + this.ready(); +}); + +Meteor.publish('tinytest-waited-on', async function () { + await sleep(100); return TestData.find(); }); -Meteor.publish('tinytest-waited-on2', function () { - Meteor._sleepForMs(10); +Meteor.publish('tinytest-waited-on2', async function () { + await sleep(10); if (this.unblock) this.unblock(); - Meteor._sleepForMs(40); + await sleep(40); return TestData.find(); }); -Meteor.publish('tinytest-data-cursor-fetch', function () { - TestData.find({}).fetch(); - this.ready(); -}); Meteor.publish('tinytest-data-2', function () { return TestData.find(); }); Meteor.publish('tinytest-data-delayed', function () { - Meteor._wrapAsync(function (done) { + Meteor.wrapAsync(function (done) { setTimeout(done, 200); })(); return TestData.find(); @@ -102,7 +98,7 @@ Meteor.publish('tinytest-data-delayed', function () { let doneOnce = false; Meteor.publish('tinytest-data-multi', function () { let pub = this; - Meteor._wrapAsync(function () { + Meteor.wrapAsync(function () { setTimeout(function () { if (!doneOnce) { pub.ready(); diff --git a/tests/_helpers/pretty-log.js b/tests/_helpers/pretty-log.js index 0f321a2d..696d9db1 100644 --- a/tests/_helpers/pretty-log.js +++ b/tests/_helpers/pretty-log.js @@ -3,6 +3,9 @@ import highlight from 'cli-highlight'; import { stringify } from 'yaml'; import Diff from 'diff'; +// Force color +chalk.level = 1; + export function diffStrings (a, b) { const diff = Diff.diffLines(b, a); diff --git a/tests/check_for_oplog.js b/tests/check_for_oplog.js index 6ddf2f5f..471a3987 100644 --- a/tests/check_for_oplog.js +++ b/tests/check_for_oplog.js @@ -1,21 +1,20 @@ import { TestData } from './_helpers/globals'; -import { CleanTestData, CloseClient, GetMeteorClient, RegisterPublication, SubscribeAndWait } from './_helpers/helpers'; +import { addAsyncTest, getMeteorClient, registerPublication, subscribeAndWait } from './_helpers/helpers'; import { OplogCheck } from '../lib/check_for_oplog'; import { _ } from 'meteor/underscore'; -Tinytest.addAsync('CheckForOplog - Kadira.checkWhyNoOplog - reactive publish', function (test, done) { +addAsyncTest.skip('CheckForOplog - Kadira.checkWhyNoOplog - reactive publish', async function (test) { const old = process.env.MONGO_OPLOG_URL; - process.env.MONGO_OPLOG_URL = 'mongodb://ssdsd'; - - CleanTestData(); + process.env.MONGO_OPLOG_URL = 'mongodb://ssdsd/local'; let observeChangesEvent; - TestData.insert({ foo: 'bar'}); + await TestData.insertAsync({ foo: 'bar'}); - const pubId = RegisterPublication(function () { - this.autorun(function () { - TestData.findOne({ foo: 'bar' }, { + const pubId = registerPublication(function () { + // `this.autorun` no longer there + this.autorun(async function () { + await TestData.findOneAsync({ foo: 'bar' }, { fields: { _id: 1}, sort: { _id: 1 } }); @@ -30,9 +29,9 @@ Tinytest.addAsync('CheckForOplog - Kadira.checkWhyNoOplog - reactive publish', f }); }); - const client = GetMeteorClient(); + const client = getMeteorClient(); - const sub = SubscribeAndWait(client, pubId); + const sub = await subscribeAndWait(client, pubId); const { data } = observeChangesEvent; @@ -41,10 +40,8 @@ Tinytest.addAsync('CheckForOplog - Kadira.checkWhyNoOplog - reactive publish', f test.equal(data.noOplogCode, 'TRACKER_ACTIVE'); sub.stop(); - CloseClient(client); process.env.MONGO_OPLOG_URL = old; - done(); }); Tinytest.add( @@ -289,7 +286,7 @@ Tinytest.addAsync( ); function WithMongoOplogUrl (fn) { - process.env.MONGO_OPLOG_URL = 'mongodb://ssdsd'; + process.env.MONGO_OPLOG_URL = 'mongodb://ssdsd/local'; fn(); delete process.env.MONGO_OPLOG_URL; } diff --git a/tests/client/models/errors.js b/tests/client/models/errors.js index 7b8689f0..363260cd 100644 --- a/tests/client/models/errors.js +++ b/tests/client/models/errors.js @@ -119,7 +119,7 @@ Tinytest.addAsync( function (test, done) { let em = new ErrorModel({ maxErrorsPerInterval: 2, - intervalInMillis: 200 + intervalInMillis: 100 }); em.sendError({name: 'hoo'}); @@ -130,7 +130,7 @@ Tinytest.addAsync( test.equal(em.canSendErrors(), true); em.close(); done(); - }, 250); + }, 200); } ); diff --git a/tests/client/utils.js b/tests/client/utils.js index 98df51ae..0b7bd543 100644 --- a/tests/client/utils.js +++ b/tests/client/utils.js @@ -1,10 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; -import { - checkSizeAndPickFields, - getBrowserInfo, - getErrorStack -} from '../../lib/client/utils'; +import { checkSizeAndPickFields, getBrowserInfo, getErrorStack } from '../../lib/client/utils'; Tinytest.addAsync( 'Client Side - Settings - publication', diff --git a/tests/docsize_cache.js b/tests/docsize_cache.js index 7e3dd32a..8608a2b5 100644 --- a/tests/docsize_cache.js +++ b/tests/docsize_cache.js @@ -1,7 +1,8 @@ import { Random } from 'meteor/random'; -const LRU = Npm.require('lru-cache'); import { DocSzCache, DocSzCacheItem } from '../lib/docsize_cache'; +const LRU = require('lru-cache'); + Tinytest.add( 'DocSize Cache - DocSzCache - constructor', function (test) { diff --git a/tests/error_tracking.js b/tests/error_tracking.js index c4b7d04b..18bbff5d 100644 --- a/tests/error_tracking.js +++ b/tests/error_tracking.js @@ -1,3 +1,4 @@ +import { addAsyncTest } from './_helpers/helpers'; Tinytest.add( 'Errors - enableErrorTracking', @@ -19,31 +20,41 @@ Tinytest.add( } ); -Tinytest.add( +addAsyncTest( 'Errors - Custom Errors - simple', - function (test) { + async function (test) { let originalTrackError = Kadira.models.error.trackError; + let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; + Kadira.enableErrorTracking(); + Kadira.models.error.trackError = function (err, trace) { test.equal(err.message, 'msg'); - test.equal(err.stack.includes('tinytest.js'), true); + test.equal(err.stack.includes('error_tracking.js'), true); + delete trace.at; - test.equal(trace, { + + const expected = { type: 'type', subType: 'server', name: 'msg', errored: true, // at: 123, events: [ - ['start', 0, {}], + ['start'], ['error', 0, {error: {message: 'msg', stack: err.stack}}] ], metrics: {total: 0} - }); + }; + + test.equal(trace, expected); }; + Kadira.trackError('type', 'msg'); + Kadira.models.error.trackError = originalTrackError; + _resetErrorTracking(originalErrorTrackingStatus); } ); @@ -64,7 +75,7 @@ Tinytest.add( errored: true, // at: 123, events: [ - ['start', 0, {}], + ['start'], ['error', 0, {error: {message: 'msg', stack: 's'}}] ], metrics: {total: 0} @@ -76,7 +87,7 @@ Tinytest.add( } ); -Tinytest.add( +addAsyncTest( 'Errors - Custom Errors - error object', function (test) { let originalTrackError = Kadira.models.error.trackError; @@ -86,18 +97,21 @@ Tinytest.add( Kadira.models.error.trackError = function (err, trace) { test.equal(err, {message: 'test', stack: error.stack}); delete trace.at; - test.equal(trace, { + + const expected = { type: 'server-internal', subType: 'server', name: 'test', errored: true, // at: 123, events: [ - ['start', 0, {}], + ['start'], ['error', 0, {error: {message: 'test', stack: error.stack}}] ], metrics: {total: 0} - }); + }; + + test.equal(trace, expected); }; Kadira.trackError(error); Kadira.models.error.trackError = originalTrackError; @@ -122,7 +136,7 @@ Tinytest.add( errored: true, // at: 123, events: [ - ['start', 0, {}], + ['start'], ['error', 0, {error: {message: 'error-message', stack: error.stack}}] ], metrics: {total: 0} @@ -151,7 +165,7 @@ Tinytest.add( errored: true, // at: 123, events: [ - ['start', 0, {}], + ['start'], ['error', 0, {error: {message: 'error-message', stack: err.stack}}] ], metrics: {total: 0} @@ -180,7 +194,7 @@ Tinytest.add( errored: true, // at: 123, events: [ - ['start', 0, {}], + ['start'], ['error', 0, {error: {message: 'error-message', stack: err.stack}}] ], metrics: {total: 0} diff --git a/tests/hijack/async.js b/tests/hijack/async.js deleted file mode 100644 index 515c0b6a..00000000 --- a/tests/hijack/async.js +++ /dev/null @@ -1,101 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TestData } from '../_helpers/globals'; -import { - CleanTestData, - EnableTrackingMethods, - GetLastMethodEvents, - GetMeteorClient, - RegisterMethod -} from '../_helpers/helpers'; - -Tinytest.add( - 'Async - track with Meteor._wrapAsync', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { - let wait = Meteor._wrapAsync(function (waitTime, callback) { - setTimeout(callback, waitTime); - }); - wait(100); - }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0]); - let expected = [ - ['start'], - ['wait'], - ['async'], - ['complete'] - ]; - test.equal(events, expected); - CleanTestData(); - } -); - -Tinytest.add( - 'Async - track with Meteor._wrapAsync with error', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { - let wait = Meteor._wrapAsync(function (waitTime, callback) { - setTimeout(function () { - callback(new Error('error')); - }, waitTime); - }); - try { - wait(100); - } catch (ex) { /* empty */ } - }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0]); - let expected = [ - ['start'], - ['wait'], - ['async'], - ['complete'] - ]; - test.equal(events, expected); - CleanTestData(); - } -); - -Tinytest.add( - 'Async - end event on throwInto', - function (test) { - const methodId = RegisterMethod(function () { - try { - Promise.await( - new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error('Fake Error')); - }, 100); - }), - ); - } catch (err) { - TestData.find({}).fetch(); - - return Kadira._getInfo(); - } - }); - - - let client = GetMeteorClient(); - let result = client.call(methodId); - const events = result.trace.events.filter(event => event[0] !== 'compute'); - - // remove complete event - events.pop(); - - const dbEvent = events.pop(); - const asyncEvent = events.pop(); - - // If the async event was not ended in throwInto, - // the db event will be nested in the async event - test.equal(asyncEvent[0], 'async'); - // If there are nested events or forcedEnd is true, then [3] will be an object - test.equal(asyncEvent[3], undefined); - - test.equal(dbEvent[0], 'db'); - } -); diff --git a/tests/hijack/base.js b/tests/hijack/base.js index 81fa63b2..7b56d1d3 100644 --- a/tests/hijack/base.js +++ b/tests/hijack/base.js @@ -1,37 +1,25 @@ import { TestData } from '../_helpers/globals'; -import { - CleanTestData, - EnableTrackingMethods, - GetLastMethodEvents, - GetMeteorClient, - RegisterMethod -} from '../_helpers/helpers'; +import { addAsyncTest, callAsync, getLastMethodEvents, registerMethod } from '../_helpers/helpers'; -Tinytest.add( +addAsyncTest( 'Base - method params', - function (test) { - EnableTrackingMethods(); - - let methodId = RegisterMethod(function () { - TestData.insert({aa: 10}); + async function (test) { + let methodId = registerMethod(async function () { + await TestData.insertAsync({ aa: 10 }); }); - let client = GetMeteorClient(); - - client.call(methodId, 10, 'abc'); + await callAsync(methodId, 10, 'abc'); - let events = GetLastMethodEvents([0, 2]); + let events = getLastMethodEvents([0, 2, 3]); let expected = [ - ['start',undefined, {userId: null, params: '[10,"abc"]'}], - ['wait',undefined, {waitOn: []}], - ['db',undefined, {coll: 'tinytest-data', func: 'insert'}], + ['start',{userId: null,params: '[10,"abc"]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data',func: 'insertAsync'}], ['complete'] ]; - test.equal(events, expected); - - CleanTestData(); + test.stableEqual(events, expected); } ); diff --git a/tests/hijack/db.js b/tests/hijack/db.js index 09cf1c32..d419bd5e 100644 --- a/tests/hijack/db.js +++ b/tests/hijack/db.js @@ -1,536 +1,520 @@ import { TestData } from '../_helpers/globals'; import { - CleanTestData, - EnableTrackingMethods, - GetLastMethodEvents, - GetMeteorClient, + addAsyncTest, + callAsync, + dumpEvents, + getLastMethodEvents, isRedisOplogEnabled, + registerMethod, RegisterMethod } from '../_helpers/helpers'; +import assert from 'assert'; -Tinytest.add( +addAsyncTest( 'Database - insert', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { - TestData.insert({aa: 10}); + async function (test) { + const methodId = RegisterMethod(async function () { + await TestData.insertAsync({aa: 10}); return 'insert'; }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2]); - let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', func: 'insert'}], - ['complete'] - ]; - test.equal(events, expected); - CleanTestData(); - } -); -Tinytest.add( - 'Database - insert with async callback', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { - TestData.insert({aa: 10}, function () { - // body... - }); - return 'insert'; - }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + await callAsync(methodId); + + let events = getLastMethodEvents([0, 2, 3]); + + dumpEvents(events); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{ - coll: 'tinytest-data', - func: 'insert', - ...!isRedisOplogEnabled ? { async: true } : {} - }], + ['start',{userId: null,params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data',func: 'insertAsync'}], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - throw error and catch', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { + async function (test) { + let methodId = registerMethod(async function () { try { - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'aa', aa: 10}); + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'aa', aa: 10}); } catch (ex) { /* empty */ } return 'insert'; }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2]); - if (events && events[3] && events[3][2] && events[3][2].err) { - events[3][2].err = events[3][2].err.indexOf('E11000') >= 0 ? 'E11000' : null; - } + + await callAsync(methodId); + + let events = getLastMethodEvents([0, 2, 3]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', func: 'insert'}], - ['db',undefined,{coll: 'tinytest-data', func: 'insert', err: 'E11000'}], + ['start',{userId: null,params: '[]'}], + ['wait',{waitOn: []}],['db',{coll: 'tinytest-data',func: 'insertAsync'}], + ['db',{coll: 'tinytest-data',func: 'insertAsync',err: 'E11000'}], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - update', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa', dd: 10}); - let methodId = RegisterMethod(function () { - TestData.update({_id: 'aa'}, {$set: {dd: 30}}); + async function (test) { + await TestData.insertAsync({_id: 'aa', dd: 10}); + + let methodId = registerMethod(async function () { + await TestData.updateAsync({_id: 'aa'}, {$set: {dd: 30}}); return 'update'; }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + await callAsync(methodId); + + let events = getLastMethodEvents([0, 2, 3]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], ... isRedisOplogEnabled ? [ - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docSize: 12, docsFetched: 1, limit: 1, projection: JSON.stringify({_id: 1})}], - ['db',undefined,{coll: 'tinytest-data', func: 'update', selector: JSON.stringify({_id: { $in: ['aa']}}), updatedDocs: 1}] - ] : [['db',undefined, {coll: 'tinytest-data', func: 'update', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1}]], + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docSize: 12, docsFetched: 1, limit: 1, projection: JSON.stringify({_id: 1})}], + ['db',{coll: 'tinytest-data', func: 'updateAsync', selector: JSON.stringify({_id: { $in: ['aa']}}), updatedDocs: 1}] + ] : [['db', {coll: 'tinytest-data', func: 'updateAsync', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1}]], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - remove', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa', dd: 10}); - let methodId = RegisterMethod(function () { - TestData.remove({_id: 'aa'}); + async function (test) { + await TestData.insertAsync({_id: 'aa', dd: 10}); + + let methodId = registerMethod(async function () { + await TestData.removeAsync({_id: 'aa'}); return 'remove'; }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + await callAsync(methodId); + + let events = getLastMethodEvents([0, 2, 3]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], ...isRedisOplogEnabled ? [ - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docSize: 12, docsFetched: 1, projection: JSON.stringify({_id: 1})}], - ['db',undefined,{coll: 'tinytest-data', func: 'remove', selector: JSON.stringify({_id: 'aa'}), removedDocs: 1}] - ] : [['db',undefined, {coll: 'tinytest-data', func: 'remove', selector: JSON.stringify({_id: 'aa'}), removedDocs: 1}]], + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docSize: 12, docsFetched: 1, projection: JSON.stringify({_id: 1})}], + ['db',{coll: 'tinytest-data', func: 'removeAsync', selector: JSON.stringify({_id: 'aa'}), removedDocs: 1}] + ] : [['db', {coll: 'tinytest-data', func: 'removeAsync', selector: JSON.stringify({_id: 'aa'}), removedDocs: 1}]], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - findOne', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa', dd: 10}); - let methodId = RegisterMethod(function () { - return TestData.findOne({_id: 'aa'}); + async function (test) { + await TestData.insertAsync({_id: 'aa', dd: 10}); + + let methodId = registerMethod(async function () { + return TestData.findOneAsync({_id: 'aa'}); }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2], ['async']); + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2, 3]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{ - coll: 'tinytest-data', - func: 'fetch', - cursor: true, - docSize: 20, - docsFetched: 1, - limit: 1, - selector: JSON.stringify({_id: 'aa'}) - }], + ['start',{userId: null,params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data',selector: '{"_id":"aa"}',func: 'fetch',cursor: true,limit: 1,docsFetched: 1,docSize: 1}], ['complete'] ]; test.equal(result, {_id: 'aa', dd: 10}); - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - findOne with sort and fields', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - TestData.insert({_id: 'aa', dd: 10}); - let methodId = RegisterMethod(function () { - return TestData.findOne({_id: 'aa'}, { + async function (test) { + await TestData.insertAsync({_id: 'aa', dd: 10}); + + let methodId = registerMethod(async function () { + return TestData.findOneAsync({_id: 'aa'}, { sort: {dd: -1}, fields: {dd: 1} }); }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2], ['async']); - let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{ - coll: 'tinytest-data', - func: 'fetch', - cursor: true, - docSize: 20, - docsFetched: 1, - limit: 1, - selector: JSON.stringify({_id: 'aa'}), - sort: JSON.stringify({dd: -1}), - }], - ['complete'] - ]; - const projection = JSON.stringify({dd: 1}); + let result = await callAsync(methodId); - if (events[2][2].projection) { - expected[2][2].projection = projection; - } else { - expected[2][2].fields = projection; - } + let events = getLastMethodEvents([0, 1, 2, 3]); test.equal(result, {_id: 'aa', dd: 10}); - test.equal(events, expected); - CleanTestData(); + + const expected = [ + ['start',0,{userId: null,params: '[]'}], + ['wait',0,{waitOn: []}], + ['db',0,{coll: 'tinytest-data',selector: '{"_id":"aa"}',func: 'fetch',cursor: true,projection: '{"dd":1}',sort: '{"dd":-1}',limit: 1,docsFetched: 1,docSize: 1}], + ['complete'] + ]; + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - upsert', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { - TestData.upsert({_id: 'aa'}, {$set: {bb: 20}}); - TestData.upsert({_id: 'aa'}, {$set: {bb: 30}}); + async function (test) { + let methodId = registerMethod(async function () { + await TestData.upsertAsync({_id: 'aa'}, {$set: {bb: 20}}); + await TestData.upsertAsync({_id: 'aa'}, {$set: {bb: 30}}); return 'upsert'; }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + await callAsync(methodId); + + let events = getLastMethodEvents([0, 2, 3]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], ...isRedisOplogEnabled ? [ - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 0, docSize: 0, limit: 1, projection: JSON.stringify({_id: 1})}], - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: 'aa'}], - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 12, limit: 1, projection: JSON.stringify({_id: 1})}], - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: undefined}], - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 20 }] + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 0, docSize: 0, limit: 1, projection: JSON.stringify({_id: 1})}], + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: 'aa'}], + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 12, limit: 1, projection: JSON.stringify({_id: 1})}], + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: undefined}], + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 20 }] ] : [ - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: 'aa'}], - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: undefined}] + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: 'aa'}], + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: undefined}] ], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - upsert with update', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { - TestData.update({_id: 'aa'}, {$set: {bb: 20}}, {upsert: true}); - TestData.update({_id: 'aa'}, {$set: {bb: 30}}, {upsert: true}); + async function (test) { + let methodId = registerMethod(async function () { + await TestData.updateAsync({_id: 'aa'}, {$set: {bb: 20}}, {upsert: true}); + await TestData.updateAsync({_id: 'aa'}, {$set: {bb: 30}}, {upsert: true}); return 'upsert'; }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + await callAsync(methodId); + + let events = getLastMethodEvents([0, 2]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], ...isRedisOplogEnabled ? [ - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 0, docSize: 0, limit: 1, projection: JSON.stringify({_id: 1})}], - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: 'aa'}], - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 12, limit: 1, projection: JSON.stringify({_id: 1})}], - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: undefined}], - ['db',undefined,{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 20 }] + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 0, docSize: 0, limit: 1, projection: JSON.stringify({_id: 1})}], + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: 'aa'}], + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 12, limit: 1, projection: JSON.stringify({_id: 1})}], + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1, insertedId: undefined}], + ['db',{coll: 'tinytest-data', func: 'fetch', selector: JSON.stringify({_id: 'aa'}), cursor: true, docsFetched: 1, docSize: 20 }] ] : [ - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1}], - ['db',undefined,{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1}] + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1}], + ['db',{coll: 'tinytest-data', func: 'upsert', selector: JSON.stringify({_id: 'aa'}), updatedDocs: 1}] ], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - indexes', - function (test) { - EnableTrackingMethods(); - let name = typeof TestData.createIndex === 'function' ? 'createIndex' : '_ensureIndex'; - let methodId = RegisterMethod(function () { - try { - TestData[name]({aa: 1, bb: 1}); - Meteor._sleepForMs(100); - TestData._dropIndex({aa: 1, bb: 1}); - return 'indexes'; - } catch (e) { - console.error(e); - } + async function (test) { + let methodId = registerMethod(async function () { + await TestData.createIndexAsync({aa: 1, bb: 1}); + await TestData.dropIndexAsync({aa: 1, bb: 1}); + return 'indexes'; }); - let client = GetMeteorClient(); - client.call(methodId); - let events = GetLastMethodEvents([0, 2], ['async']); + + await callAsync(methodId); + + let events = getLastMethodEvents([0, 2]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', func: name, index: JSON.stringify({aa: 1, bb: 1})}], - ['db',undefined,{coll: 'tinytest-data', func: '_dropIndex', index: JSON.stringify({aa: 1, bb: 1})}], + ['start',{userId: null,params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data',func: 'createIndexAsync',index: '{"aa":1,"bb":1}'}], + ['db',{coll: 'tinytest-data',func: 'dropIndexAsync',index: '{"aa":1,"bb":1}'}], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - Cursor - count', - function (test) { - EnableTrackingMethods(); - TestData.insert({aa: 100}); - TestData.insert({aa: 300}); - let methodId = RegisterMethod(function () { - return TestData.find().count(); + async function (test) { + await TestData.insertAsync({aa: 100}); + await TestData.insertAsync({aa: 300}); + + let methodId = RegisterMethod(async function () { + return TestData.find().countAsync(); }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'count', selector: JSON.stringify({})}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', cursor: true, func: 'countAsync', selector: JSON.stringify({})}], ['complete'] ]; + test.equal(result, 2); test.equal(events, expected); - CleanTestData(); } ); -Tinytest.add( +addAsyncTest( 'Database - Cursor - fetch', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'bb'}); - let methodId = RegisterMethod(function () { - return TestData.find({_id: {$exists: true}}).fetch(); + async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = RegisterMethod(async function () { + return TestData.find({_id: {$exists: true}}).fetchAsync(); }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2], ['docSize', 'docsFetched']); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'fetch', selector: JSON.stringify({_id: {$exists: true}}), docsFetched: 2, docSize: JSON.stringify({_id: 'aa'}).length * 2}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', cursor: true, func: 'fetch', selector: JSON.stringify({_id: {$exists: true}}), docsFetched: 2, docSize: JSON.stringify({_id: 'aa'}).length * 2}], ['complete'] ]; - test.equal(result, [{_id: 'aa'}, {_id: 'bb'}]); - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(result, [{_id: 'aa'}, {_id: 'bb'}]); + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - Cursor - map', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'bb'}); - let methodId = RegisterMethod(function () { - return TestData.find({_id: {$exists: true}}).map(function (doc) { + async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = RegisterMethod(async function () { + return TestData.find({_id: {$exists: true}}).mapAsync(function (doc) { return doc._id; }); }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2], ['docsFetched']); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'map', selector: JSON.stringify({_id: {$exists: true}}), docsFetched: 2}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', cursor: true, func: 'map', selector: JSON.stringify({_id: {$exists: true}}), docsFetched: 2}], ['complete'] ]; - test.equal(result, ['aa', 'bb']); - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(result, ['aa', 'bb']); + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - Cursor - forEach', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'bb'}); - let methodId = RegisterMethod(function () { + async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = RegisterMethod(async function () { let res = []; - TestData.find({_id: {$exists: true}}).forEach(function (doc) { + + await TestData.find({_id: {$exists: true}}).forEachAsync(function (doc) { res.push(doc._id); }); + return res; }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'forEach', selector: JSON.stringify({_id: {$exists: true}})}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', cursor: true, func: 'forEach', selector: JSON.stringify({_id: {$exists: true}})}], ['complete'] ]; - test.equal(result, ['aa', 'bb']); - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(result, ['aa', 'bb']); + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - Cursor - forEach:findOne inside', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'bb'}); - let methodId = RegisterMethod(function () { + async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = RegisterMethod(async function () { let res = []; - TestData.find({_id: {$exists: true}}).forEach(function (doc) { + + await TestData.find({_id: {$exists: true}}).forEachAsync(async function (doc) { res.push(doc._id); - TestData.findOne(); + await TestData.findOneAsync(); }); + return res; }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2]); + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'forEach', selector: JSON.stringify({_id: {$exists: true}})}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', cursor: true, func: 'forEach', selector: JSON.stringify({_id: {$exists: true}})}], ['complete'] ]; - test.equal(result, ['aa', 'bb']); - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(result, ['aa', 'bb']); + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - Cursor - observeChanges', - function (test) { - EnableTrackingMethods(); - CleanTestData(); - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'bb'}); - let methodId = RegisterMethod(function () { + async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = registerMethod(async function () { let data = []; - let handle = TestData.find({}).observeChanges({ + + let handle = await TestData.find({}).observeChanges({ added (id, fields) { fields._id = id; data.push(fields); } }); + handle.stop(); + return data; }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); - events[2][2].oplog = false; + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2], ['noOfCachedDocs']); + + events[2][1].oplog = false; + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'observeChanges', selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2, wasMultiplexerReady: false}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', cursor: true, func: 'observeChanges', selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2}], ['complete'] ]; - test.equal(result, [{_id: 'aa'}, {_id: 'bb'}]); - clearAdditionalObserverInfo(events[2][2]); - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(result, [{_id: 'aa'}, {_id: 'bb'}]); + + clearAdditionalObserverInfo(events[2][1]); + + test.stableEqual(events, expected); } ); -Tinytest.add( - 'Database - Cursor - observeChanges:re-using-multiflexer', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'bb'}); - let methodId = RegisterMethod(function () { + +/** + * @warning `wasMultiplexerReady` is true for both when it should be false for the first one. Which might be an issue in Meteor code, so let's not test that. + */ +addAsyncTest( + 'Database - Cursor - observeChanges:re-using-multiplexer', + async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = registerMethod(async function () { let data = []; - let handle = TestData.find({}).observeChanges({ + + let handle1 = await TestData.find({}).observeChanges({ added (id, fields) { fields._id = id; data.push(fields); } }); - let handle2 = TestData.find({}).observeChanges({ + + let handle2 = await TestData.find({}).observeChanges({ added () { // body } }); - handle.stop(); + + assert.strictEqual(handle1._multiplexer, handle2._multiplexer, 'Multiplexer should be the same for both handles'); + + handle1.stop(); handle2.stop(); return data; }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); - events[2][2].oplog = false; - events[3][2].oplog = false; + let result = await callAsync(methodId); + let events = getLastMethodEvents([0, 2], ['noOfCachedDocs']); + + events[2][1].oplog = false; + events[3][1].oplog = false; let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'observeChanges', selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2, wasMultiplexerReady: false}], - ['db',undefined,{coll: 'tinytest-data', cursor: true, func: 'observeChanges', selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2, wasMultiplexerReady: true}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', cursor: true, func: 'observeChanges', selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2 }], + ['db',{coll: 'tinytest-data', cursor: true, func: 'observeChanges', selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2 }], ['complete'] ]; - test.equal(result, [{_id: 'aa'}, {_id: 'bb'}]); - clearAdditionalObserverInfo(events[2][2]); - clearAdditionalObserverInfo(events[3][2]); - test.equal(events, expected); - CleanTestData(); + + test.stableEqual(result, [{_id: 'aa'}, {_id: 'bb'}]); + + clearAdditionalObserverInfo(events[2][1]); + clearAdditionalObserverInfo(events[3][1]); + + test.stableEqual(events, expected); } ); -Tinytest.add( +addAsyncTest( 'Database - Cursor - observe', - function (test) { - EnableTrackingMethods(); - TestData.insert({_id: 'aa'}); - TestData.insert({_id: 'bb'}); - let methodId = RegisterMethod(function () { + async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = registerMethod(async function () { let data = []; - let handle = TestData.find({}).observe({ + let handle = await TestData.find({}).observe({ added (doc) { data.push(doc); } @@ -538,26 +522,64 @@ Tinytest.add( handle.stop(); return data; }); - let client = GetMeteorClient(); - let result = client.call(methodId); - let events = GetLastMethodEvents([0, 2]); - events[2][2].oplog = false; + + let result = await callAsync(methodId); + let events = getLastMethodEvents([0, 2], ['noOfCachedDocs']); + + events[2][1].oplog = false; + let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', func: 'observe', cursor: true, selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2, wasMultiplexerReady: false}], + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data', func: 'observe', cursor: true, selector: JSON.stringify({}), oplog: false, noOfCachedDocs: 2 }], ['complete'] ]; test.equal(result, [{_id: 'aa'}, {_id: 'bb'}]); - clearAdditionalObserverInfo(events[2][2]); - test.equal(events, expected); - CleanTestData(); + clearAdditionalObserverInfo(events[2][1]); + test.stableEqual(events, expected); } ); +addAsyncTest('Database - AsynchronousCursor - _nextObjectPromise', async function (test) { + await TestData.insertAsync({_id: 'aa'}); + await TestData.insertAsync({_id: 'bb'}); + + let methodId = registerMethod(async function () { + let data = []; + let cursor = TestData.find({}); + + await cursor.forEach(function (doc) { + data.push(doc); + }); + + return data; + }); + + let result = await callAsync(methodId); + + let events = getLastMethodEvents([0, 2, 3], ['noOfCachedDocs']); + + let expected = [ + ['start',{userId: null, params: '[]'}], + ['wait',{waitOn: []}], + ['db', { coll: 'tinytest-data', func: 'forEach', cursor: true, selector: JSON.stringify({}) }, { + nested: [ + ['db', { coll: 'tinytest-data', func: '_nextObjectPromise'}], + ['db', { coll: 'tinytest-data', func: '_nextObjectPromise'}], + ['db', { coll: 'tinytest-data', func: '_nextObjectPromise'}], + ] + }], + ['complete'] + ]; + + test.stableEqual(result, [{_id: 'aa'}, {_id: 'bb'}]); + test.stableEqual(events, expected); +}); + function clearAdditionalObserverInfo (info) { delete info.queueLength; delete info.initialPollingTime; delete info.elapsedPollingTime; + delete info.wasMultiplexerReady; } diff --git a/tests/hijack/email.js b/tests/hijack/email.js index 8caaba90..08a5e09d 100644 --- a/tests/hijack/email.js +++ b/tests/hijack/email.js @@ -1,25 +1,21 @@ -import { TestHelpers } from '../_helpers/helpers'; +import { addAsyncTest, callAsync, getLastMethodEvents, registerMethod } from '../_helpers/helpers'; -const Email = Package['email'].Email; +async function sendTestEmailThroughMethod () { + const Email = Package['email'].Email; -function sendTestEmailThroughMethod (func) { - const methodId = TestHelpers.registerMethod(function () { - return Email[func]({ from: 'arunoda@meteorhacks.com', to: 'hello@meteor.com' }); + const methodId = registerMethod(async function () { + await Email.sendAsync({ from: 'arunoda@meteorhacks.com', to: 'hello@meteor.com' }); }); - const client = TestHelpers.getMeteorClient(); - - client.call(methodId); + await callAsync(methodId); } -Tinytest.add( +addAsyncTest( 'Email - success', - function (test) { - TestHelpers.enableTrackingMethods(); - - sendTestEmailThroughMethod('send'); + async function (test) { + await sendTestEmailThroughMethod(); - const events = TestHelpers.getLastMethodEvents([0]); + const events = getLastMethodEvents([0]); const expected = [ ['start'], @@ -29,32 +25,5 @@ Tinytest.add( ]; test.equal(events, expected); - - TestHelpers.cleanTestData(); } ); - -if (Email.sendAsync) { - Tinytest.add( - 'Email - sendAsync', - function (test) { - TestHelpers.enableTrackingMethods(); - - sendTestEmailThroughMethod('sendAsync'); - - const events = TestHelpers.getLastMethodEvents([0]); - - const expected = [ - ['start'], - ['wait'], - ['email'], - ['async'], - ['complete'] - ]; - - test.equal(events, expected); - - TestHelpers.cleanTestData(); - } - ); -} diff --git a/tests/hijack/error.js b/tests/hijack/error.js index 7086c1a6..489f723d 100644 --- a/tests/hijack/error.js +++ b/tests/hijack/error.js @@ -1,8 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { ErrorModel } from '../../lib/models/errors'; -import { GetMeteorClient, RegisterMethod, RegisterPublication } from '../_helpers/helpers'; - -const HTTP = Package['http'].HTTP; +import { + addAsyncTest, + callAsync, + getMeteorClient, + registerMethod, + RegisterMethod, + registerPublication +} from '../_helpers/helpers'; +const HTTP = Package['http']?.HTTP; Tinytest.add( 'Errors - Meteor._debug - track with Meteor._debug', @@ -62,17 +68,19 @@ Tinytest.add( } ); -Tinytest.add( +addAsyncTest( 'Errors - Meteor._debug - do not track method errors', - function (test) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; + Kadira.enableErrorTracking(); + Kadira.models.error = new ErrorModel('foo'); + let method = RegisterMethod(causeError); - let client = GetMeteorClient(); try { - client.call(method); + await callAsync(method); } catch (e) { // ignore the error } @@ -96,8 +104,8 @@ Tinytest.addAsync( let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; Kadira.enableErrorTracking(); Kadira.models.error = new ErrorModel('foo'); - let pubsub = RegisterPublication(causeError); - let client = GetMeteorClient(); + let pubsub = registerPublication(causeError); + let client = getMeteorClient(); client.subscribe(pubsub, { onError () { let payload = Kadira.models.error.buildPayload(); @@ -116,9 +124,9 @@ Tinytest.addAsync( } ); -Tinytest.addAsync( +addAsyncTest( 'Errors - Meteor._debug - do not track when no arguments', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; Kadira.enableErrorTracking(); Kadira.models.error = new ErrorModel('foo'); @@ -126,16 +134,15 @@ Tinytest.addAsync( let payload = Kadira.models.error.buildPayload(); test.equal(0, payload.errors.length); _resetErrorTracking(originalErrorTrackingStatus); - done(); } ); // How Meteor gives errors to Meteor._debug versions 1.4 - 1.6 // is already tested above. It changed in 1.7 which these tests covers if (!['1.4', '1.5', '1.6'].find(prefix => Meteor.release.startsWith(`METEOR@${prefix}`))) { - Tinytest.addAsync( + addAsyncTest( 'Errors - Meteor._debug - preserve error thrown in Meteor.bindEnvironment', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; Kadira.enableErrorTracking(); const error = new Error('test'); @@ -147,7 +154,6 @@ if (!['1.4', '1.5', '1.6'].find(prefix => Meteor.release.startsWith(`METEOR@${pr test.equal(error.message, loggedError.message); test.equal(error.stack, loggedError.stack); _resetErrorTracking(originalErrorTrackingStatus); - done(); }; Meteor.bindEnvironment(function () { @@ -156,18 +162,22 @@ if (!['1.4', '1.5', '1.6'].find(prefix => Meteor.release.startsWith(`METEOR@${pr } ); - Tinytest.addAsync( + addAsyncTest( 'Errors - Meteor._debug - track Meteor Error thrown in Meteor.bindEnvironment', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; Kadira.enableErrorTracking(); Kadira.models.error = new ErrorModel('foo'); const error = new Meteor.Error('test'); + Meteor.bindEnvironment(function () { throw error; })(); + let payload = Kadira.models.error.buildPayload(); + let errorTrace = payload.errors[0]; + let expected = { appId: 'foo', name: 'Exception in callback of async function: [test]', @@ -192,20 +202,23 @@ if (!['1.4', '1.5', '1.6'].find(prefix => Meteor.release.startsWith(`METEOR@${pr delete errorTrace.startTime; delete errorTrace.trace.at; + test.equal(expected, errorTrace); _resetErrorTracking(originalErrorTrackingStatus); - done(); } ); } -Tinytest.addAsync( +addAsyncTest( 'Errors - unhandledRejection - track unhandledRejection', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; + Kadira.enableErrorTracking(); Kadira.models.error = new ErrorModel('foo'); + let error = new Error('rejected'); + Promise.reject(error); Meteor.defer(function () { @@ -218,14 +231,13 @@ Tinytest.addAsync( test.equal(error.subType, 'unhandledRejection'); _resetErrorTracking(originalErrorTrackingStatus); - done(); }); } ); -Tinytest.addAsync( +addAsyncTest( 'Errors - unhandledRejection - undefined reason', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; Kadira.enableErrorTracking(); Kadira.models.error = new ErrorModel('foo'); @@ -242,23 +254,24 @@ Tinytest.addAsync( test.equal(error.subType, 'unhandledRejection'); _resetErrorTracking(originalErrorTrackingStatus); - done(); }); } ); -Tinytest.addAsync( +addAsyncTest( 'Errors - method error - track Meteor.Error', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; - let methodId = RegisterMethod(function () { + + let methodId = registerMethod(function () { throw new Meteor.Error('ERR_CODE', 'reason'); }); - let client = GetMeteorClient(); + try { - client.call(methodId); + await callAsync(methodId); } catch (ex) { let errorMessage = 'reason [ERR_CODE]'; + test.equal(ex.message, errorMessage); let payload = Kadira.models.error.buildPayload(); let error = payload.errors[0]; @@ -267,23 +280,23 @@ Tinytest.addAsync( let lastEvent = error.trace.events[error.trace.events.length - 1]; test.isTrue(lastEvent[2].error.message.indexOf(errorMessage) >= 0); test.isTrue(lastEvent[2].error.stack.indexOf(errorMessage) >= 0); - done(); } _resetErrorTracking(originalErrorTrackingStatus); } ); -Tinytest.addAsync( +addAsyncTest( 'Errors - method error - store error details property', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; - let methodId = RegisterMethod(function () { + + let methodId = registerMethod(function () { throw new Meteor.Error('ERR_CODE', 'reason', 'details'); }); - let client = GetMeteorClient(); + try { - client.call(methodId); + await callAsync(methodId); } catch (ex) { let errorMessage = 'reason [ERR_CODE]'; test.equal(ex.message, errorMessage); @@ -296,23 +309,23 @@ Tinytest.addAsync( test.isTrue(lastEvent[2].error.message.indexOf(errorMessage) >= 0); test.isTrue(lastEvent[2].error.stack.indexOf(errorMessage) >= 0); test.equal(lastEvent[2].error.details, 'details'); - done(); } _resetErrorTracking(originalErrorTrackingStatus); } ); -Tinytest.addAsync( +addAsyncTest( 'Errors - method error - track NodeJs Error', - function (test, done) { + async function (test) { let originalErrorTrackingStatus = Kadira.options.enableErrorTracking; + let methodId = RegisterMethod(function () { throw new Error('the-message'); }); - let client = GetMeteorClient(); + try { - client.call(methodId); + await callAsync(methodId); } catch (ex) { let errorMessage = 'the-message'; test.isTrue(ex.message.match(/Internal server error/)); @@ -323,7 +336,6 @@ Tinytest.addAsync( let lastEvent = error.trace.events[error.trace.events.length - 1]; test.isTrue(lastEvent[2].error.message.indexOf(errorMessage) >= 0); test.isTrue(lastEvent[2].error.stack.indexOf(errorMessage) >= 0); - done(); } _resetErrorTracking(originalErrorTrackingStatus); diff --git a/tests/hijack/http.js b/tests/hijack/http.js index 72bdff2a..840e911c 100644 --- a/tests/hijack/http.js +++ b/tests/hijack/http.js @@ -1,50 +1,27 @@ -import { HTTP } from 'meteor/http'; -import { CleanTestData, GetLastMethodEvents, GetMeteorClient, RegisterMethod } from '../_helpers/helpers'; -const Future = Npm.require('fibers/future'); +import { addAsyncTest, callAsync, getLastMethodEvents, registerMethod } from '../_helpers/helpers'; +import { asyncHttpGet } from '../_helpers/http'; -Tinytest.add('HTTP - meteor/http - call a server', function (test) { - const methodId = RegisterMethod(function () { - const result = HTTP.get('http://localhost:3301'); +/** + * @warning Every HTTP call should be async since Release 3.0 + */ +addAsyncTest('HTTP - meteor/http - call a server', async function (test) { + const methodId = registerMethod(async function () { + const result = await asyncHttpGet('http://localhost:3301'); return result.statusCode; }); - const client = GetMeteorClient(); - const result = client.call(methodId); - const events = GetLastMethodEvents([0, 2]); - const expected = [ - ['start', undefined, { userId: null, params: '[]' }], - ['wait', undefined, { waitOn: [] }], - ['http', undefined, { url: 'http://localhost:3301', method: 'GET', statusCode: 200, library: 'meteor/http' }], - ['complete'] - ]; - test.equal(events, expected); - test.equal(result, 200); - CleanTestData(); -} -); -Tinytest.add('HTTP - meteor/http - async callback', function (test) { - const methodId = RegisterMethod(function () { - const f = new Future(); - let result; - HTTP.get('http://localhost:3301', function (err, res) { - result = res; - f.return(); - }); - f.wait(); - return result.statusCode; - }); - const client = GetMeteorClient(); - const result = client.call(methodId); - const events = GetLastMethodEvents([0, 2]); + const result = await callAsync(methodId); + + const events = getLastMethodEvents([0, 2, 3]); + const expected = [ - ['start', undefined, { userId: null, params: '[]' }], - ['wait', undefined, { waitOn: [] }], - ['http', undefined, { url: 'http://localhost:3301', method: 'GET', async: true, library: 'meteor/http' }], - ['async', undefined, {}], + ['start',{userId: null,params: '[]'}], + ['wait',{waitOn: []}], + ['http',{method: 'GET',url: 'http://localhost:3301',library: 'meteor/http',statusCode: 1,async: true}], + ['http',{method: 'GET', url: 'http://localhost:3301/',library: 'meteor/fetch',}, {offset: 1}], ['complete'] ]; - test.equal(events, expected); + + test.stableEqual(events, expected); test.equal(result, 200); - CleanTestData(); -} -); +}); diff --git a/tests/hijack/http_fetch.js b/tests/hijack/http_fetch.js index 85414e13..cdd3da4c 100644 --- a/tests/hijack/http_fetch.js +++ b/tests/hijack/http_fetch.js @@ -1,69 +1,62 @@ import { fetch } from 'meteor/fetch'; -import { CleanTestData, GetLastMethodEvents, GetMeteorClient, RegisterMethod } from '../_helpers/helpers'; -const Future = Npm.require('fibers/future'); +import { addAsyncTest, callAsync, getLastMethodEvents, registerMethod, } from '../_helpers/helpers'; + +addAsyncTest('HTTP - meteor/fetch - async call', async function (test) { + const methodId = registerMethod(async function () { + const result = await fetch('http://127.0.0.1:3301/'); -Tinytest.add('HTTP - meteor/fetch - async call', function (test) { - const methodId = RegisterMethod(function () { - const f = new Future(); - let result; - fetch('http://localhost:3301/').then(function (res) { - result = res; - f.return(); - }); - f.wait(); return result.status; }); - const client = GetMeteorClient(); - const result = client.call(methodId); - const events = GetLastMethodEvents([0, 2]); + + const result = await callAsync(methodId); + + const events = getLastMethodEvents([0, 2]); + const expected = [ - ['start', undefined, { userId: null, params: '[]' }], - ['wait', undefined, { waitOn: [] }], - ['http', undefined, { method: 'GET', url: 'http://localhost:3301/', library: 'meteor/fetch' }], + ['start', { userId: null, params: '[]' }], + ['wait', { waitOn: [] }], + ['http', { method: 'GET', url: 'http://127.0.0.1:3301/', library: 'meteor/fetch' }], ['complete'] ]; - test.equal(events, expected); + + test.stableEqual(events, expected); test.equal(result, 200); - CleanTestData(); -} -); +}); -Tinytest.add('HTTP - meteor/fetch - trace error', function (test) { - const methodId = RegisterMethod(function () { - const f = new Future(); - let error; - fetch('http://localhost:9999/').catch(function (err) { - error = err; - f.return(); - }); - f.wait(); - return error; +addAsyncTest('HTTP - meteor/fetch - trace error', async function (test) { + const methodId = registerMethod(async function () { + try { + await fetch('http://127.0.0.1:9999/'); + } catch (error) { + return error; + } }); - const client = GetMeteorClient(); - const result = client.call(methodId); - const events = GetLastMethodEvents([0, 2]); + + const result = await callAsync(methodId); + const events = getLastMethodEvents([0, 2]); + const expected = [ - ['start', undefined, { userId: null, params: '[]' }], - ['wait', undefined, { waitOn: [] }], + ['start', { userId: null, params: '[]' }], + ['wait', { waitOn: [] }], [ - 'http',undefined, + 'http', { method: 'GET', - url: 'http://localhost:9999/', + url: 'http://127.0.0.1:9999/', library: 'meteor/fetch', - err: 'request to http://localhost:9999/ failed, reason: connect ECONNREFUSED 127.0.0.1:9999', + err: 'request to http://127.0.0.1:9999/ failed, reason: connect ECONNREFUSED 127.0.0.1:9999', }, ], ['complete'], ]; - test.equal(events, expected); - test.equal(result, { + + test.stableEqual(events, expected); + + test.stableEqual(result, { message: - 'request to http://localhost:9999/ failed, reason: connect ECONNREFUSED 127.0.0.1:9999', + 'request to http://127.0.0.1:9999/ failed, reason: connect ECONNREFUSED 127.0.0.1:9999', type: 'system', errno: 'ECONNREFUSED', code: 'ECONNREFUSED', }); - CleanTestData(); -} -); +}); diff --git a/tests/hijack/info.js b/tests/hijack/info.js index bc6222e3..92e27243 100644 --- a/tests/hijack/info.js +++ b/tests/hijack/info.js @@ -1,20 +1,16 @@ import { Meteor } from 'meteor/meteor'; -import { CleanTestData, EnableTrackingMethods, GetMeteorClient, RegisterMethod } from '../_helpers/helpers'; +import { addAsyncTest, callAsync, registerMethod } from '../_helpers/helpers'; -Tinytest.add( +addAsyncTest( 'Info - Meteor.EnvironmentVariable', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(testMethod); - let client = GetMeteorClient(); + async function (test) { + let methodId = registerMethod(testMethod); - client.call(methodId, 10, 'abc'); - - CleanTestData(); + await callAsync(methodId, 10, 'abc'); function testMethod () { Meteor.setTimeout(function () { - let kadirainfo = Kadira._getInfo(null, true); + let kadirainfo = Kadira._getInfo(); test.equal(!!kadirainfo, true); }, 0); } diff --git a/tests/hijack/mongo_driver_events.js b/tests/hijack/mongo_driver_events.js index c2117421..ef66176d 100644 --- a/tests/hijack/mongo_driver_events.js +++ b/tests/hijack/mongo_driver_events.js @@ -1,43 +1,30 @@ import { getMongoDriverStats, resetMongoDriverStats } from '../../lib/hijack/mongo_driver_events.js'; -import { releaseParts } from '../_helpers/helpers'; +import { addAsyncTest } from '../_helpers/helpers'; import { TestData } from '../_helpers/globals'; -// Check if Meteor 2.2 or newer, which is the first version that enabled -// useUnifiedTopology by default -const mongoMonitoringEnabled = releaseParts[0] > 2 || - (releaseParts[0] > 1 && releaseParts[1] > 1); - -function checkRange (value, disabledValue, min, max) { +function checkRange (value, min, max) { if (typeof value !== 'number') { throw new Error(`${value} is not a number`); } - if (!mongoMonitoringEnabled) { - if (value !== disabledValue) { - throw new Error(`${value} does not equal ${disabledValue}`); - } - - return; - } - if (value < min || value > max) { throw new Error(`Value (${value}) is outside of range (${min} - ${max})`); } } -/** - * @flaky - */ -Tinytest.addAsync( +addAsyncTest( 'Mongo Driver Events - getMongoDriverStats', - async function (test, done) { + async function (test) { resetMongoDriverStats(); const promises = []; + let raw = TestData.rawCollection(); + let countFn = raw.estimatedDocumentCount ? raw.estimatedDocumentCount.bind(raw) : raw.count.bind(raw); + for (let i = 0; i < 200; i++) { promises.push(countFn()); } @@ -46,16 +33,15 @@ Tinytest.addAsync( const stats = getMongoDriverStats(); - checkRange(stats.poolSize, 0, 10, 100); - test.equal(stats.primaryCheckouts, mongoMonitoringEnabled ? 200 : 0); + checkRange(stats.poolSize, 10, 100); + test.equal(stats.primaryCheckouts, 200); test.equal(stats.otherCheckouts, 0); // TODO: these maximum numbers seem too high - checkRange(stats.checkoutTime, 0, 100, 40000); - checkRange(stats.maxCheckoutTime, 0, 10, 300); - checkRange(stats.pending, 0, 0, 200); - checkRange(stats.checkedOut, 0, 0, 25); - checkRange(stats.created, 0, 1, 100); - done(); + checkRange(stats.checkoutTime, 100, 40000); + checkRange(stats.maxCheckoutTime, 10, 300); + checkRange(stats.pending, 0, 200); + checkRange(stats.checkedOut, 0, 20); + checkRange(stats.created, 1, 100); } ); diff --git a/tests/hijack/redis_oplog.js b/tests/hijack/redis_oplog.js index 5c4ffbdd..a5991e39 100644 --- a/tests/hijack/redis_oplog.js +++ b/tests/hijack/redis_oplog.js @@ -1,27 +1,28 @@ +import { sleep } from '../../lib/utils'; import { TestData, TestDataRedis, TestDataRedisNoRaceProtection } from '../_helpers/globals'; -import { FindMetricsForPub, GetMeteorClient, RegisterMethod, RegisterPublication, SubscribeAndWait, addTestWithRoundedTime } from '../_helpers/helpers'; +import { subscribeAndWait, addTestWithRoundedTime, getMeteorClient, registerMethod, registerPublication, FindMetricsForPub } from '../_helpers/helpers'; /** * We only track the observers coming from subscriptions (which have `ownerInfo`) */ -addTestWithRoundedTime('Database - Redis Oplog - Added', function (test) { - const pub = RegisterPublication(() => TestData.find({})); +addTestWithRoundedTime('Database - Redis Oplog - Added', async function (test) { + const pub = registerPublication(() => TestData.find({})); - TestData.remove({}); + await TestData.removeAsync({}); - TestData.insert({ name: 'test1' }); - TestData.insert({ name: 'test2' }); - TestData.insert({ name: 'test3' }); - TestData.insert({ name: 'test4' }); + await TestData.insertAsync({ name: 'test1' }); + await TestData.insertAsync({ name: 'test2' }); + await TestData.insertAsync({ name: 'test3' }); + await TestData.insertAsync({ name: 'test4' }); - const client = GetMeteorClient(); - const sub = SubscribeAndWait(client, pub); + const client = getMeteorClient(); + const sub = await subscribeAndWait(client, pub); - TestData.insert({ name: 'test5' }); - TestData.insert({ name: 'test6' }); - TestData.insert({ name: 'test7' }); + await TestData.insertAsync({ name: 'test5' }); + await TestData.insertAsync({ name: 'test6' }); + await TestData.insertAsync({ name: 'test7' }); - Meteor._sleepForMs(100); + await sleep(100); const metrics = FindMetricsForPub(pub); @@ -31,35 +32,35 @@ addTestWithRoundedTime('Database - Redis Oplog - Added', function (test) { test.equal(metrics.liveAddedDocuments, 3); sub.stop(); - TestData.remove({}); + await TestData.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - Added with limit/skip', function (test) { - const pub = RegisterPublication(() => TestData.find({name: 'test'}, {limit: 2, skip: 0})); +addTestWithRoundedTime('Database - Redis Oplog - Added with limit/skip', async function (test) { + const pub = registerPublication(() => TestData.find({name: 'test'}, {limit: 2, skip: 0})); - TestData.remove({}); + await TestData.removeAsync({}); - TestData.insert({ name: 'test' }); + await TestData.insertAsync({ name: 'test' }); - const client = GetMeteorClient(); - const sub = SubscribeAndWait(client, pub); + const client = getMeteorClient(); + const sub = await subscribeAndWait(client, pub); let metrics = FindMetricsForPub(pub); test.equal(metrics.polledDocuments, 1); - TestData.insert({ name: 'test' }); - Meteor._sleepForMs(100); + await TestData.insertAsync({ name: 'test' }); + await sleep(100); // as the selector IS matched, redis-oplog triggers a requery metrics = FindMetricsForPub(pub); // 1 from initial subscription, 1 findOne + requery(2) test.equal(metrics.polledDocuments, 4); - TestData.insert({ name: 'doesnt-match-cursor' }); + await TestData.insertAsync({ name: 'doesnt-match-cursor' }); // as the selector IS NOT matched, redis-oplog does not trigger a requery - Meteor._sleepForMs(100); + await sleep(100); metrics = FindMetricsForPub(pub); @@ -72,53 +73,53 @@ addTestWithRoundedTime('Database - Redis Oplog - Added with limit/skip', functio sub.stop(); - TestData.remove({}); + await TestData.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - With protect against race condition - Check Trace', function (test) { +addTestWithRoundedTime('Database - Redis Oplog - With protect against race condition - Check Trace', async function (test) { // in this case, the mutator will refetch the doc when publishing it - const methodId = RegisterMethod(() => TestDataRedis.update({name: 'test'}, {$set: {name: 'abv'}})); + const methodId = registerMethod(() => TestDataRedis.update({name: 'test'}, {$set: {name: 'abv'}})); - TestDataRedis.remove({}); + await TestDataRedis.removeAsync({}); - TestDataRedis.insert({ name: 'test' }); + await TestDataRedis.insertAsync({ name: 'test' }); - const client = GetMeteorClient(); - client.call(methodId); + const client = getMeteorClient(); + await client.callAsync(methodId); Meteor._sleepForMs(1000); let trace = Kadira.models.methods.tracerStore.currentMaxTrace[`method::${methodId}`]; const dbEvents = trace.events.filter((o) => o[0] === 'db'); test.equal(dbEvents.length, 2); - TestDataRedis.remove({}); + await TestDataRedis.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - With protect against race condition - check for finds after receiving the msg', function (test) { +addTestWithRoundedTime('Database - Redis Oplog - With protect against race condition - check for finds after receiving the msg', async function (test) { // in this case, every subscriber will refetch the doc once when receiving it - const pub = RegisterPublication(() => TestDataRedis.find({name: 'test'})); + const pub = registerPublication(() => TestDataRedis.find({name: 'test'})); - TestDataRedis.remove({}); + await TestDataRedis.removeAsync({}); - TestDataRedis.insert({ name: 'test' }); + await TestDataRedis.insertAsync({ name: 'test' }); - const client = GetMeteorClient(); - const client2 = GetMeteorClient(); - const sub = SubscribeAndWait(client, pub); - const sub2 = SubscribeAndWait(client2, pub); + const client = getMeteorClient(); + const client2 = getMeteorClient(); + const sub = await subscribeAndWait(client, pub); + const sub2 = await subscribeAndWait(client2, pub); let metrics = FindMetricsForPub(pub); test.equal(metrics.polledDocuments, 1); - TestDataRedis.insert({ name: 'test' }); - Meteor._sleepForMs(100); + await TestDataRedis.insertAsync({ name: 'test' }); + await sleep(100); metrics = FindMetricsForPub(pub); test.equal(metrics.polledDocuments, 2); - Meteor._sleepForMs(100); + await sleep(100); metrics = FindMetricsForPub(pub); @@ -131,33 +132,33 @@ addTestWithRoundedTime('Database - Redis Oplog - With protect against race condi sub.stop(); sub2.stop(); - TestDataRedis.remove({}); + await TestDataRedis.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - Without protect against race condition - no extraneous finds', function (test) { +addTestWithRoundedTime('Database - Redis Oplog - Without protect against race condition - no extraneous finds', async function (test) { // in this case, no subscriber will refetch the doc when receiving it - const pub = RegisterPublication(() => TestDataRedisNoRaceProtection.find({})); + const pub = registerPublication(() => TestDataRedisNoRaceProtection.find({})); - TestDataRedisNoRaceProtection.remove({}); + await TestDataRedisNoRaceProtection.removeAsync({}); - TestDataRedisNoRaceProtection.insert({ name: 'test' }); + await TestDataRedisNoRaceProtection.insertAsync({ name: 'test' }); - const client = GetMeteorClient(); - const sub = SubscribeAndWait(client, pub); - const sub2 = SubscribeAndWait(client, pub); + const client = getMeteorClient(); + const sub = await subscribeAndWait(client, pub); + const sub2 = await subscribeAndWait(client, pub); let metrics = FindMetricsForPub(pub); test.equal(metrics.polledDocuments, 1); - TestDataRedisNoRaceProtection.insert({ name: 'test' }); - Meteor._sleepForMs(100); + await TestDataRedisNoRaceProtection.insertAsync({ name: 'test' }); + await sleep(100); metrics = FindMetricsForPub(pub); test.equal(metrics.polledDocuments, 1); - Meteor._sleepForMs(100); + await sleep(100); metrics = FindMetricsForPub(pub); @@ -170,20 +171,20 @@ addTestWithRoundedTime('Database - Redis Oplog - Without protect against race co sub.stop(); sub2.stop(); - TestDataRedisNoRaceProtection.remove({}); + await TestDataRedisNoRaceProtection.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - Without protect against race condition - Check Trace', function (test) { +addTestWithRoundedTime('Database - Redis Oplog - Without protect against race condition - Check Trace', async function (test) { // in this case, the mutator will refetch the doc when publishing it - const methodId = RegisterMethod(() => TestDataRedisNoRaceProtection.update({name: 'test'}, {$set: {name: 'abv'}})); + const methodId = registerMethod(() => TestDataRedisNoRaceProtection.update({name: 'test'}, {$set: {name: 'abv'}})); - TestDataRedisNoRaceProtection.remove({}); + await TestDataRedisNoRaceProtection.removeAsync({}); - TestDataRedisNoRaceProtection.insert({ name: 'test' }); + await TestDataRedisNoRaceProtection.insertAsync({ name: 'test' }); - const client = GetMeteorClient(); - client.call(methodId); + const client = getMeteorClient(); + await client.callAsync(methodId); Meteor._sleepForMs(1000); let trace = Kadira.models.methods.tracerStore.currentMaxTrace[`method::${methodId}`]; @@ -191,37 +192,37 @@ addTestWithRoundedTime('Database - Redis Oplog - Without protect against race co test.equal(dbEvents.length, 3); test.equal(dbEvents[2][2].func, 'fetch'); - TestDataRedisNoRaceProtection.remove({}); + await TestDataRedisNoRaceProtection.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - Removed', function (test) { - const pub = RegisterPublication(() => TestData.find({})); +addTestWithRoundedTime('Database - Redis Oplog - Removed', async function (test) { + const pub = registerPublication(() => TestData.find({})); - TestData.remove({}); + await TestData.removeAsync({}); - const client = GetMeteorClient(); - const sub = SubscribeAndWait(client, pub); + const client = getMeteorClient(); + const sub = await subscribeAndWait(client, pub); - TestData.insert({ name: 'test1' }); - TestData.insert({ name: 'test2' }); - TestData.insert({ name: 'test3' }); + await TestData.insertAsync({ name: 'test1' }); + await TestData.insertAsync({ name: 'test2' }); + await TestData.insertAsync({ name: 'test3' }); - Meteor._sleepForMs(100); + await sleep(100); - TestData.remove({ name: 'test2' }); + await TestData.removeAsync({ name: 'test2' }); - Meteor._sleepForMs(100); + await sleep(100); let metrics = FindMetricsForPub(pub); test.equal(metrics.totalObservers, 1); test.equal(metrics.liveRemovedDocuments, 1); - TestData.remove({}); + await TestData.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); metrics = FindMetricsForPub(pub); @@ -229,28 +230,28 @@ addTestWithRoundedTime('Database - Redis Oplog - Removed', function (test) { test.equal(metrics.liveRemovedDocuments, 3); sub.stop(); - TestData.remove({}); + await TestData.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - Changed', function (test) { - const pub = RegisterPublication(() => TestData.find({})); +addTestWithRoundedTime('Database - Redis Oplog - Changed', async function (test) { + const pub = registerPublication(() => TestData.find({})); - TestData.remove({}); + await TestData.removeAsync({}); - const client = GetMeteorClient(); - const sub = SubscribeAndWait(client, pub); + const client = getMeteorClient(); + const sub = await subscribeAndWait(client, pub); - TestData.insert({ name: 'test1' }); - TestData.insert({ name: 'test2' }); - TestData.insert({ name: 'test3' }); + await TestData.insertAsync({ name: 'test1' }); + await TestData.insertAsync({ name: 'test2' }); + await TestData.insertAsync({ name: 'test3' }); - Meteor._sleepForMs(100); + await sleep(100); TestData.update({ name: 'test2' }, { $set: { name: 'test4' } }); - Meteor._sleepForMs(100); + await sleep(100); let metrics = FindMetricsForPub(pub); @@ -259,7 +260,7 @@ addTestWithRoundedTime('Database - Redis Oplog - Changed', function (test) { TestData.update({}, { $set: { name: 'test5' } }, { multi: true }); - Meteor._sleepForMs(200); + await sleep(100); metrics = FindMetricsForPub(pub); @@ -267,35 +268,35 @@ addTestWithRoundedTime('Database - Redis Oplog - Changed', function (test) { test.equal(metrics.liveChangedDocuments, 4); sub.stop(); - TestData.remove({}); + await TestData.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); }); -addTestWithRoundedTime('Database - Redis Oplog - Remove with limit', function (test) { - const pub = RegisterPublication(() => TestData.find({}, { limit: 100 })); - TestData.remove({}); - const client = GetMeteorClient(); +addTestWithRoundedTime('Database - Redis Oplog - Remove with limit', async function (test) { + const pub = registerPublication(() => TestData.find({}, { limit: 100 })); + await TestData.removeAsync({}); + const client = getMeteorClient(); - const sub = SubscribeAndWait(client, pub); + const sub = await subscribeAndWait(client, pub); TestData.insert({ name: 'test1' }); TestData.insert({ name: 'test2' }); TestData.insert({ name: 'test3' }); - Meteor._sleepForMs(100); - TestData.remove({ name: 'test2' }); + await sleep(100); + await TestData.removeAsync({ name: 'test2' }); - Meteor._sleepForMs(100); + await sleep(100); let metrics = FindMetricsForPub(pub); test.equal(metrics.totalObservers, 1, 'observers'); test.equal(metrics.liveRemovedDocuments, 1, 'removed'); - TestData.remove({}); + await TestData.removeAsync({}); - Meteor._sleepForMs(100); + await sleep(100); metrics = FindMetricsForPub(pub); diff --git a/tests/hijack/subscriptions.js b/tests/hijack/subscriptions.js index 9a11f320..0a29f07e 100644 --- a/tests/hijack/subscriptions.js +++ b/tests/hijack/subscriptions.js @@ -1,74 +1,71 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { - CleanTestData, - CloseClient, - EnableTrackingMethods, + addAsyncTest, FindMetricsForPub, - GetMeteorClient, + getMeteorClient, GetPubSubMetrics, - GetPubSubPayload, - RegisterPublication, - SubscribeAndWait, - TestHelpers, Wait, waitForConnection + getPubSubPayload, + registerPublication, + subscribeAndWait, + TestHelpers, + waitForConnection } from '../_helpers/helpers'; +import { sleep } from '../../lib/utils'; -Tinytest.add( +addAsyncTest( 'Subscriptions - Sub/Unsub - subscribe only', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - let client = GetMeteorClient(); - - let h1 = SubscribeAndWait(client, 'tinytest-data'); - let h2 = SubscribeAndWait(client, 'tinytest-data'); + async function (test, client) { + let h1 = await subscribeAndWait(client, 'tinytest-data'); + let h2 = await subscribeAndWait(client, 'tinytest-data'); let metrics = GetPubSubMetrics(); + test.equal(metrics.length, 1); test.equal(metrics[0].pubs['tinytest-data'].subs, 2); + h1.stop(); h2.stop(); - CloseClient(client); } ); -Tinytest.add( +addAsyncTest( 'Subscriptions - Sub/Unsub - subscribe and unsubscribe', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - let client = GetMeteorClient(); + async function (test, client) { + let h1 = await subscribeAndWait(client, 'tinytest-data'); + let h2 = await subscribeAndWait(client, 'tinytest-data'); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - let h2 = SubscribeAndWait(client, 'tinytest-data'); h1.stop(); h2.stop(); - Wait(100); + + await sleep(100); let metrics = GetPubSubMetrics(); + test.equal(metrics.length, 1); test.equal(metrics[0].pubs['tinytest-data'].subs, 2); test.equal(metrics[0].pubs['tinytest-data'].unsubs, 2); - CloseClient(client); } ); -Tinytest.add( +addAsyncTest( 'Subscriptions - Response Time - single', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); + async function (test, client) { let pubName = `pub-${Random.id()}`; - Meteor.publish(pubName, function () { - Wait(200); + + Meteor.publish(pubName, async function () { + await sleep(200); this.ready(); }); - let h1 = SubscribeAndWait(client, pubName); + + let h1 = await subscribeAndWait(client, pubName); + let metrics = FindMetricsForPub(pubName); + test.isTrue(TestHelpers.compareNear(metrics.resTime, 200, 100)); + h1.stop(); - CloseClient(client); } ); @@ -76,8 +73,8 @@ Tinytest.add( // 'Subscriptions - Response Time - multiple', // function (test) { // EnableTrackingMethods(); -// var client = GetMeteorClient(); -// var Future = Npm.require('fibers/future'); +// var client = getMeteorClient(); +// var Future = require('fibers/future'); // var f = new Future(); // var h1, h2; // h1 = client.subscribe('tinytest-data-multi', function() { @@ -85,13 +82,13 @@ Tinytest.add( // f.return(); // }); // f.wait(); -// var metrics = GetPubSubPayload(); +// var metrics = getPubSubPayload(); // var resTimeOne = metrics[0].pubs['tinytest-data-multi'].resTime; // Wait(700); // var H2_SUB; // h2 = client.subscribe('tinytest-data-multi'); // Wait(300); -// var metrics2 = GetPubSubPayload(); +// var metrics2 = getPubSubPayload(); // var resTimeTwo = metrics2[0].pubs['tinytest-data-multi'].resTime; // test.isTrue(resTimeTwo == 0); // h1.stop(); @@ -101,23 +98,18 @@ Tinytest.add( // } // ); -/** - * The `lifeTime` metric seems flaky on Meteor v2.8.2 specifically, - * shouldn't be something worry about but leaving this comment just in case. - */ -Tinytest.add( +addAsyncTest( 'Subscriptions - Lifetime - sub', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - let client = GetMeteorClient(); - waitForConnection(client); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - Wait(50); + async function (test, client) { + let h1 = await subscribeAndWait(client, 'tinytest-data'); + + await sleep(50); + h1.stop(); - CloseClient(client); + let metrics = FindMetricsForPub('tinytest-data'); - test.isTrue(TestHelpers.compareNear(metrics.lifeTime, 50, 200)); + + test.isTrue(TestHelpers.compareNear(metrics.lifeTime, 50, 75)); } ); @@ -126,8 +118,8 @@ Tinytest.add( // // function (test) { // // // test.fail('no pubs for null(autopublish)'); // // // EnableTrackingMethods(); -// // // var client = GetMeteorClient(); -// // // var Future = Npm.require('fibers/future'); +// // // var client = getMeteorClient(); +// // // var Future = require('fibers/future'); // // // var f = new Future(); // // // var interval = setInterval(function () { // // // if (client.status().connected) { @@ -144,27 +136,24 @@ Tinytest.add( // // } // // ); -/** - * @flaky - */ -Tinytest.add( +addAsyncTest( 'Subscriptions - ObserverLifetime - sub', - function (test) { + async function (test) { TestHelpers.cleanTestData(); TestHelpers.enableTrackingMethods(); let client = TestHelpers.getMeteorClient(); - waitForConnection(client); + await waitForConnection(client); let start = Date.now(); let st = Date.now(); - let h1 = TestHelpers.subscribeAndWait(client, 'tinytest-data'); + let h1 = await subscribeAndWait(client, 'tinytest-data'); let elapsedTime = Date.now() - st; console.log('elapsed 1', elapsedTime); - TestHelpers.wait(100); + await sleep(100); Kadira.EventBus.once('pubsub', 'observerDeleted', (ownerInfo) => console.log('on sub stop:', Date.now(), JSON.stringify(ownerInfo))); @@ -174,114 +163,103 @@ Tinytest.add( console.log('elapsed 2', Date.now() - st); console.log('elapsed total', Date.now() - start); - TestHelpers.wait(100); + await sleep(100); let metrics = TestHelpers.findMetricsForPub('tinytest-data'); - console.dir(metrics); - console.log({elapsedTime}); - test.isTrue(TestHelpers.compareNear(metrics.observerLifetime, 100, 60)); - TestHelpers.closeClient(client); + + test.isTrue(TestHelpers.compareNear(metrics.observerLifetime, 100 + elapsedTime, 60)); } ); -Tinytest.add( +addAsyncTest( 'Subscriptions - active subs', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - let client = GetMeteorClient(); + async function (test, client) { + let h1 = await subscribeAndWait(client, 'tinytest-data'); + let h2 = await subscribeAndWait(client, 'tinytest-data'); + let h3 = await subscribeAndWait(client, 'tinytest-data-2'); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - let h2 = SubscribeAndWait(client, 'tinytest-data'); - let h3 = SubscribeAndWait(client, 'tinytest-data-2'); + let payload = getPubSubPayload(); - let payload = GetPubSubPayload(); test.equal(payload[0].pubs['tinytest-data'].activeSubs === 2, true); test.equal(payload[0].pubs['tinytest-data-2'].activeSubs === 1, true); + h1.stop(); h2.stop(); h3.stop(); - CloseClient(client); } ); -Tinytest.add( +addAsyncTest( 'Subscriptions - avoiding multiple ready', - function (test) { - CleanTestData(); - EnableTrackingMethods(); + async function (test, client) { let ReadyCounts = 0; - let pubId = RegisterPublication(function () { + + let pubId = registerPublication(function () { this.ready(); this.ready(); }); + let original = Kadira.models.pubsub._trackReady; + Kadira.models.pubsub._trackReady = function (session, sub) { if (sub._name === pubId) { ReadyCounts++; } }; - let client = GetMeteorClient(); - SubscribeAndWait(client, pubId); + + await subscribeAndWait(client, pubId); test.equal(ReadyCounts, 1); Kadira.models.pubsub._trackReady = original; - CloseClient(client); } ); -Tinytest.add( +addAsyncTest( 'Subscriptions - Observer Cache - single publication and single subscription', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - - Wait(100); - let metrics = GetPubSubPayload(); + async function (test, client) { + let h1 = await subscribeAndWait(client, 'tinytest-data'); + + await sleep(100); + + let metrics = getPubSubPayload(); + test.equal(metrics[0].pubs['tinytest-data'].totalObservers, 1); test.equal(metrics[0].pubs['tinytest-data'].cachedObservers, 0); test.equal(metrics[0].pubs['tinytest-data'].avgObserverReuse, 0); h1.stop(); - CloseClient(client); } ); -Tinytest.add( +addAsyncTest( 'Subscriptions - Observer Cache - single publication and multiple subscriptions', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - let client = GetMeteorClient(); + async function (test, client) { + let h1 = await subscribeAndWait(client, 'tinytest-data'); + let h2 = await subscribeAndWait(client, 'tinytest-data'); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - let h2 = SubscribeAndWait(client, 'tinytest-data'); + await sleep(100); - Wait(100); - let metrics = GetPubSubPayload(); + let metrics = getPubSubPayload(); test.equal(metrics[0].pubs['tinytest-data'].totalObservers, 2); test.equal(metrics[0].pubs['tinytest-data'].cachedObservers, 1); test.equal(metrics[0].pubs['tinytest-data'].avgObserverReuse, 0.5); h1.stop(); h2.stop(); - CloseClient(client); } ); -Tinytest.add( +addAsyncTest( 'Subscriptions - Observer Cache - multiple publication and multiple subscriptions', - function (test) { - CleanTestData(); - EnableTrackingMethods(); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - let h2 = SubscribeAndWait(client, 'tinytest-data-2'); - - Wait(100); - let metrics = GetPubSubPayload(); + async function (test) { + let client = getMeteorClient(); + let h1 = await subscribeAndWait(client, 'tinytest-data'); + let h2 = await subscribeAndWait(client, 'tinytest-data-2'); + + await sleep(100); + + let metrics = getPubSubPayload(); + test.equal(metrics[0].pubs['tinytest-data'].totalObservers, 1); test.equal(metrics[0].pubs['tinytest-data'].cachedObservers, 0); test.equal(metrics[0].pubs['tinytest-data'].avgObserverReuse, 0); @@ -289,8 +267,8 @@ Tinytest.add( test.equal(metrics[0].pubs['tinytest-data-2'].totalObservers, 1); test.equal(metrics[0].pubs['tinytest-data-2'].cachedObservers, 1); test.equal(metrics[0].pubs['tinytest-data-2'].avgObserverReuse, 1); + h1.stop(); h2.stop(); - CloseClient(client); } ); diff --git a/tests/hijack/user.js b/tests/hijack/user.js index 70e5f6ed..70aea1c7 100644 --- a/tests/hijack/user.js +++ b/tests/hijack/user.js @@ -1,31 +1,29 @@ +import { sleep } from '../../lib/utils'; import { TestData } from '../_helpers/globals'; -import { - CleanTestData, - EnableTrackingMethods, - GetLastMethodEvents, - GetMeteorClient, - RegisterMethod -} from '../_helpers/helpers'; +import { addAsyncTest, callAsync, getLastMethodEvents, registerMethod } from '../_helpers/helpers'; -Tinytest.add( +addAsyncTest( 'User - not logged in', - function (test) { - EnableTrackingMethods(); - let methodId = RegisterMethod(function () { - TestData.insert({aa: 10}); + async function (test) { + let methodId = registerMethod(async function () { + await TestData.insertAsync({ aa: 10 }); + + return 'foo'; }); - let client = GetMeteorClient(); - client.call(methodId); + await callAsync(methodId); + + await sleep(200); + + let events = getLastMethodEvents([0, 2, 3]); - let events = GetLastMethodEvents([0, 2]); let expected = [ - ['start',undefined,{userId: null, params: '[]'}], - ['wait',undefined,{waitOn: []}], - ['db',undefined,{coll: 'tinytest-data', func: 'insert'}], + ['start',{userId: null,params: '[]'}], + ['wait',{waitOn: []}], + ['db',{coll: 'tinytest-data',func: 'insertAsync'}], ['complete'] ]; - test.equal(events, expected); - CleanTestData(); + console.error(events); + test.stableEqual(events, expected); } ); diff --git a/tests/hijack/webapp.js b/tests/hijack/webapp.js index 9a065bff..e946a49e 100644 --- a/tests/hijack/webapp.js +++ b/tests/hijack/webapp.js @@ -1,7 +1,6 @@ -import { WebApp, WebAppInternals } from 'meteor/webapp'; -import { checkHandlersInFiber, wrapWebApp } from '../../lib/hijack/wrap_webapp'; +import { WebApp } from 'meteor/webapp'; +import { getRouter, wrapWebApp } from '../../lib/hijack/wrap_webapp'; import { releaseParts } from '../_helpers/helpers'; -import { HTTP } from 'meteor/http'; // Check if Meteor 1.7 or newer, which are the // versions that wrap connect handlers in a fiber and are easy @@ -9,13 +8,6 @@ import { HTTP } from 'meteor/http'; const httpMonitoringEnabled = releaseParts[0] > 1 || (releaseParts[0] > 0 && releaseParts[1] > 6); -Tinytest.add( - 'Webapp - checkHandlersInFiber', - function (test) { - const expected = httpMonitoringEnabled; - test.equal(checkHandlersInFiber(), expected); - } -); if (httpMonitoringEnabled) { wrapWebApp(); @@ -42,11 +34,10 @@ if (httpMonitoringEnabled) { headers: { 'content-type': 'application/json', 'content-length': '1000', - 'x--test--authorization': 'secret' - } + 'x--test--authorization': 'secret', + }, }; - - WebApp.rawConnectHandlers.stack[0].handle( + getRouter(WebApp.rawHandlers).stack[0].handle( req, {on () {}}, function () { @@ -59,72 +50,4 @@ if (httpMonitoringEnabled) { done(); }); }); - - Tinytest.add( - 'Webapp - filter body', - function (test) { - Kadira.tracer.redactField('httpSecret'); - - let req = { - url: '/test', - method: 'POST', - headers: { - 'content-type': 'application/json', - 'content-length': '1000' - }, - body: { httpSecret: 'secret', otherData: 5 } - }; - - let listener; - - WebApp.rawConnectHandlers.stack[0].handle( - req, - {on (event, _listener) { listener = _listener; }}, - () => {} - ); - - listener(); - - let expected = JSON.stringify({ - httpSecret: 'Monti: redacted', - otherData: 5 - }); - - test.equal(req.__kadiraInfo.trace.events[0][2].body, expected); - }); - - Tinytest.add( - 'Webapp - static middleware', - function (test) { - const result = HTTP.get(Meteor.absoluteUrl('global-imports.js')); - test.isTrue(result.content.includes('Package[')); - let payload = Kadira.models.http.buildPayload(); - - let staticMetrics = payload.httpMetrics[0].routes['GET-']; - test.isTrue(staticMetrics.count > 0); - }); -} - -if (releaseParts[0] > 1 || - (releaseParts[0] === 1 && releaseParts[1] > 8)) { - // Meteor 1.8.1 and newer started replacing staticFilesByArch instead of - // mutating the existing object - Tinytest.add( - 'Webapp - use latest staticFilesByArch', - function (test) { - let origStaticFiles = WebAppInternals.staticFilesByArch; - let staticFiles = { - 'web.browser': { - '/test.txt': { - content: '5' - } - } - }; - - WebAppInternals.staticFilesByArch = staticFiles; - const result = HTTP.get(Meteor.absoluteUrl('test.txt')); - test.equal(result.content, '5'); - - WebAppInternals.staticFilesByArch = origStaticFiles; - }); } diff --git a/tests/models/errors.js b/tests/models/errors.js index 1695dc8e..2f7aacb9 100644 --- a/tests/models/errors.js +++ b/tests/models/errors.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { ErrorModel } from '../../lib/models/errors'; +import { addAsyncTest } from '../_helpers/helpers'; Tinytest.add( 'Models - Errors - empty', @@ -60,9 +61,9 @@ Tinytest.add( } ); -Tinytest.add( +addAsyncTest( 'Models - Errors - buildPayload', - function (test) { + async function (test) { let model = new ErrorModel('_appId'); let error = {name: '_name', message: '_message', stack: '_stack'}; let trace = {type: '_type', subType: '_subType', name: '_name'}; @@ -83,6 +84,7 @@ Tinytest.add( }; test.equal(typeof payload.startTime, 'number'); delete payload.startTime; + test.equal(payload, expected); } ); diff --git a/tests/models/jobs.js b/tests/models/jobs.js index 3d4344c2..c00f2b20 100644 --- a/tests/models/jobs.js +++ b/tests/models/jobs.js @@ -1,13 +1,14 @@ import { EJSON } from 'meteor/ejson'; import { JobsModel } from '../../lib/models/jobs'; -import { CleanTestData } from '../_helpers/helpers'; -import { TestData } from '../_helpers/globals'; +import { cleanTestData, CleanTestData } from '../_helpers/helpers'; +import { MontiAsyncStorage } from '../../lib/async/als'; +import { sleep } from '../../lib/utils'; const model = new JobsModel(); -Tinytest.add( +Tinytest.addAsync( 'Models - Jobs - buildPayload simple', - function (test) { + async function (test) { createCompletedJob('hello', 100, 5); createCompletedJob('hello', 800, 10); @@ -53,32 +54,45 @@ Tinytest.add( expected.jobMetrics[0].startTime = Kadira.syncedDate.syncTime(startTime); // TODO comparing without parsing and stringifing fails test.equal(EJSON.parse(EJSON.stringify(payload)), EJSON.parse(EJSON.stringify(expected))); - CleanTestData(); + await CleanTestData(); + } +); + +Tinytest.addAsync( + 'Models - Jobs - should not have NaN with active job', + async function (test) { + await cleanTestData(); + Kadira.models.jobs.trackActiveJobs('hello', 1); + + let payload = Kadira.models.jobs.buildPayload(); + test.equal(payload.jobMetrics[0].jobs.hello.count, 0); + test.equal(payload.jobMetrics[0].jobs.hello.active, 1); + test.equal(payload.jobMetrics[0].jobs.hello.async, 0); + await cleanTestData(); } ); -Tinytest.add( +Tinytest.addAsync( 'Models - Jobs - track new jobs', - function (test) { + async function (test) { model.trackNewJob('analyze'); model.trackNewJob('analyze'); model.trackNewJob('analyze'); let payload = model.buildPayload(); test.equal(payload.jobMetrics[0].jobs.analyze.added, 3); - CleanTestData(); + await CleanTestData(); } ); -Tinytest.add( +Tinytest.addAsync( 'Models - Jobs - track active jobs', - function (test) { + async function (test) { model.activeJobCounts.clear(); model.trackActiveJobs('analyze', 1); model.trackActiveJobs('analyze', 1); let payload = model.buildPayload(); - console.dir(payload, { depth: 10 }); test.equal(payload.jobMetrics[0].jobs.analyze.active, 2); model.trackActiveJobs('analyze', -1); @@ -86,35 +100,21 @@ Tinytest.add( payload = model.buildPayload(); test.equal(payload.jobMetrics[0].jobs.analyze.active, 1); - CleanTestData(); + await CleanTestData(); } ); -Tinytest.add( - 'Models - Jobs - should not have NaN with active job', - function (test) { - CleanTestData(); - Kadira.models.jobs.trackActiveJobs('hello', 1); - - let payload = Kadira.models.jobs.buildPayload(); - test.equal(payload.jobMetrics[0].jobs.hello.count, 0); - test.equal(payload.jobMetrics[0].jobs.hello.active, 1); - test.equal(payload.jobMetrics[0].jobs.hello.async, 0); - CleanTestData(); - } -); - -Tinytest.add( +Tinytest.addAsync( 'Models - Jobs - Monti.recordNewJob', async function (test) { Kadira.recordNewJob('hello'); let payload = Kadira.models.jobs.buildPayload(); test.equal(payload.jobMetrics[0].jobs.hello.added, 1); - CleanTestData(); + await CleanTestData(); } ); -Tinytest.add( +Tinytest.addAsync( 'Models - Jobs - Monti.recordPendingJobs', async function (test) { Kadira.recordPendingJobs('hello', 5); @@ -124,43 +124,63 @@ Tinytest.add( Kadira.recordPendingJobs('hello', 0); payload = Kadira.models.jobs.buildPayload(); test.equal(payload.jobMetrics.length, 0); - CleanTestData(); + await CleanTestData(); } ); -Tinytest.add( +Tinytest.addAsync( 'Models - Jobs - traceJob - return sync value', - function (test) { - let result = Kadira.traceJob({ name: 'hello' }, () => 5); + async function (test) { + let result = runTraceJob({ name: 'hello' }, () => 5); test.equal(result, 5); - CleanTestData(); + await cleanTestData(); } ); Tinytest.addAsync( 'Models - Jobs - traceJob - return async value', async function (test, done) { - let result = await Kadira.traceJob({ name: 'hello' }, () => Promise.resolve(5)); + let result = await runTraceJob({ name: 'hello' }, () => Promise.resolve(5)); test.equal(result, 5); - CleanTestData(); + await cleanTestData(); done(); } ); -Tinytest.add( +Tinytest.addAsync( 'Models - Jobs - traceJob - track sync processor', - function (test) { - Kadira.traceJob({ name: 'hello' }, () => { - TestData.find().fetch(); + async function (test) { + await cleanTestData(); + + runTraceJob({ name: 'hello' }, () => { + let now = Date.now(); + while (Date.now() - 5 < now) { + // nothing + } }); let payload = Kadira.models.jobs.buildPayload(); + test.equal(payload.jobMetrics[0].jobs.hello.count, 1); - test.ok(payload.jobMetrics[0].jobs.hello.total > 0); - test.ok(payload.jobMetrics[0].jobs.hello.db > 0); - CleanTestData(); + test.isTrue(payload.jobMetrics[0].jobs.hello.total > 0); + test.isTrue(payload.jobMetrics[0].jobs.hello.compute > 0); + } +); + +Tinytest.addAsync( + 'Models - Jobs - traceJob - track async time', + async function (test) { + await runTraceJob({ name: 'hello' }, async () => { + await sleep(10); + }); + + let payload = Kadira.models.jobs.buildPayload(); + test.equal(payload.jobMetrics[0].jobs.hello.count, 1); + test.isTrue(payload.jobMetrics[0].jobs.hello.total > 0); + test.isTrue(payload.jobMetrics[0].jobs.hello.async > 1); + await cleanTestData(); } ); @@ -174,7 +194,7 @@ Tinytest.addAsync( resolver = resolve; }); - let jobPromise = Kadira.traceJob({ name: 'hello' }, async () => { + let jobPromise = runTraceJob({ name: 'hello' }, async () => { await promise; }); @@ -189,7 +209,7 @@ Tinytest.addAsync( test.equal(payload.jobMetrics[0].jobs.hello.active, undefined); test.equal(payload.jobMetrics[0].jobs.hello.count, 1); - CleanTestData(); + await CleanTestData(); done(); } ); @@ -201,3 +221,9 @@ function createCompletedJob (jobName, startTime, totalTime = 5) { method = Kadira.tracer.buildTrace(method); model.processJob(method); } + +function runTraceJob (options, fn) { + return MontiAsyncStorage.run(null, () => { + return Monti.traceJob(options, fn); + }); +} diff --git a/tests/models/methods.js b/tests/models/methods.js index e6bce0f6..a721f577 100644 --- a/tests/models/methods.js +++ b/tests/models/methods.js @@ -1,14 +1,14 @@ -import { EJSON } from 'meteor/ejson'; import { MethodsModel } from '../../lib/models/methods'; +import { Ntp } from '../../lib/ntp'; +import { sleep } from '../../lib/utils'; import { TestData } from '../_helpers/globals'; -import { CleanTestData, CloseClient, GetMeteorClient, RegisterMethod, Wait, WithDocCacheGetSize, callPromise, findMetricsForMethod, waitForConnection } from '../_helpers/helpers'; -import { Meteor } from 'meteor/meteor'; +import { CleanTestData, addAsyncTest, callAsync, clientCallAsync, closeClient, findMetricsForMethod, getMeteorClient, registerMethod, waitForConnection, withDocCacheGetSize } from '../_helpers/helpers'; -Tinytest.add( +addAsyncTest( 'Models - Method - buildPayload simple', - function (test) { - CreateMethodCompleted('aa', 'hello', 1, 100, 5); - CreateMethodCompleted('aa', 'hello', 2, 800 , 10); + async function (test) { + createMethodCompleted('aa', 'hello', 1, 100, 5); + createMethodCompleted('aa', 'hello', 2, 800 , 10); let payload = model.buildPayload(); payload.methodRequests = []; @@ -49,19 +49,17 @@ Tinytest.add( methodRequests: [] }; - let startTime = expected.methodMetrics[0].startTime; - expected.methodMetrics[0].startTime = Kadira.syncedDate.syncTime(startTime); - // TODO comparing without parsing and stringifing fails - test.equal(EJSON.parse(EJSON.stringify(payload)), EJSON.parse(EJSON.stringify(expected))); - CleanTestData(); + expected.methodMetrics[0].startTime = payload.methodMetrics[0].startTime; + + test.stableEqual(payload, expected); } ); -Tinytest.add( +addAsyncTest( 'Models - Method - buildPayload with errors', - function (test) { - CreateMethodCompleted('aa', 'hello', 1, 100, 5); - CreateMethodErrored('aa', 'hello', 2, 'the-error', 800, 10); + async function (test) { + createMethodCompleted('aa', 'hello', 1, 100, 5); + createMethodErrored('aa', 'hello', 2, 'the-error', 800, 10); let payload = model.buildPayload(); let expected = [{ startTime: 100, @@ -93,97 +91,95 @@ Tinytest.add( } } }]; - // TODO comparing without stringify fails - expected[0].startTime = Kadira.syncedDate.syncTime(expected[0].startTime); - test.equal(EJSON.parse(EJSON.stringify(payload.methodMetrics)), EJSON.parse(EJSON.stringify(expected))); - CleanTestData(); + expected[0].startTime = payload.methodMetrics[0].startTime; + test.stableEqual(payload.methodMetrics,expected); } ); -Tinytest.add( +addAsyncTest( 'Models - Method - Metrics - fetchedDocSize', - function (test) { + async function (test) { let docs = [{data: 'data1'}, {data: 'data2'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); - let methodId = RegisterMethod(function () { - TestData.find({}).fetch(); + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + let methodId = registerMethod(async function () { + return TestData.find({}).fetchAsync(); }); - let client = GetMeteorClient(); - WithDocCacheGetSize(function () { - client.call(methodId); + await withDocCacheGetSize(async function () { + await callAsync(methodId); }, 30); - Wait(100); + let payload = Kadira.models.methods.buildPayload(); + let index = payload.methodMetrics.findIndex(methodMetrics => methodId in methodMetrics.methods); test.equal(payload.methodMetrics[index].methods[methodId].fetchedDocSize, 60); - CleanTestData(); } ); -Tinytest.add( +addAsyncTest( 'Models - Method - Metrics - sentMsgSize', - function (test) { + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); - let returnValue = 'Some return value'; - let methodId = RegisterMethod(function () { - TestData.find({}).fetch(); + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + const returnValue = 'Some return value'; + + const methodId = registerMethod(async function () { + await TestData.find({}).fetchAsync(); return returnValue; }); - let client = GetMeteorClient(); - client.call(methodId); + // Needs to call a client isolated from other tests for reliability + await clientCallAsync(client, methodId); + await sleep(100); let payload = Kadira.models.methods.buildPayload(); let expected = (JSON.stringify({ msg: 'updated', methods: ['1'] }) + JSON.stringify({ msg: 'result', id: '1', result: returnValue })).length; - test.equal(payload.methodMetrics[0].methods[methodId].sentMsgSize, expected); - CleanTestData(); } ); -Tinytest.add( +addAsyncTest( 'Models - Method - Trace - filter params', - function (test) { + async function (test) { Kadira.tracer.redactField('__test1'); - let methodId = RegisterMethod(function () { - }); - let client = GetMeteorClient(); - client.call(methodId, { __test1: 'value', abc: true }, { xyz: false, __test1: 'value2' }); + let methodId = registerMethod(function () {}); + + await callAsync(methodId, { __test1: 'value', abc: true }, { xyz: false, __test1: 'value2' }); let trace = Kadira.models.methods.tracerStore.currentMaxTrace[`method::${methodId}`]; let expected = JSON.stringify([{ __test1: 'Monti: redacted', abc: true }, { xyz: false, __test1: 'Monti: redacted'}]); + test.equal(trace.events[0][2].params, expected); - CleanTestData(); } ); -Tinytest.add( +addAsyncTest( 'Models - Method - Trace - filter params with null', - function (test) { + async function (test) { Kadira.tracer.redactField('__test1'); - let methodId = RegisterMethod(function () { - }); - let client = GetMeteorClient(); - client.call(methodId, { __test1: 'value', abc: true }, null, { xyz: false, __test1: 'value2' }); + let methodId = registerMethod(function () {}); + + await callAsync(methodId, { __test1: 'value', abc: true }, null, { xyz: false, __test1: 'value2' }); let trace = Kadira.models.methods.tracerStore.currentMaxTrace[`method::${methodId}`]; let expected = JSON.stringify([{ __test1: 'Monti: redacted', abc: true }, null, { xyz: false, __test1: 'Monti: redacted' }]); + test.equal(trace.events[0][2].params, expected); CleanTestData(); } @@ -191,16 +187,16 @@ Tinytest.add( Tinytest.addAsync('Models - Method - Waited On - track wait time of queued messages', async (test, done) => { - let methodId = RegisterMethod( function (id) { - Meteor._sleepForMs(25); + let methodId = registerMethod( async function (id) { + await sleep(25); return id; }); - let client = GetMeteorClient(); + let client = getMeteorClient(); let promises = []; for (let i = 0; i < 10; i++) { - promises.push(callPromise(client, methodId, i)); + promises.push(clientCallAsync(client, methodId, i)); } await Promise.all(promises); @@ -217,25 +213,25 @@ Tinytest.addAsync('Models - Method - Waited On - track wait time of queued messa Tinytest.addAsync('Models - Method - Waited On - track waitedOn without wait time', async (test, done) => { CleanTestData(); - let slowMethod = RegisterMethod(function () { + let slowMethod = registerMethod(async function () { console.log('slow method start'); - Meteor._sleepForMs(100); + await sleep(100); console.log('slow method end'); }); - let unblockedMethod = RegisterMethod(function () { + let unblockedMethod = registerMethod(async function () { this.unblock(); - Meteor._sleepForMs(100); + await sleep(100); console.log('slow method end'); }); - let fastMethod = RegisterMethod(function () { + let fastMethod = registerMethod(function () { console.log('fastMethod'); }); - let client = GetMeteorClient(); + let client = getMeteorClient(); // subscriptions and method calls made before connected are not run in order - waitForConnection(client); + await waitForConnection(client); client.call(slowMethod, () => {}); client.call(unblockedMethod, () => {}); @@ -244,22 +240,22 @@ Tinytest.addAsync('Models - Method - Waited On - track waitedOn without wait tim const metrics = findMetricsForMethod(unblockedMethod); test.isTrue(metrics.waitedOn < 10, `${metrics.waitedOn} should be less than 10`); - CloseClient(client); + closeClient(client); done(); }); Tinytest.addAsync('Models - Method - Waited On - check unblock time', async (test, done) => { - let methodId = RegisterMethod( function (id) { + let methodId = registerMethod( async function (id) { this.unblock(); - Meteor._sleepForMs(25); + await sleep(25); return id; }); - let client = GetMeteorClient(); + let client = getMeteorClient(); let promises = []; for (let i = 0; i < 10; i++) { - promises.push(callPromise(client, methodId, i)); + promises.push(clientCallAsync(client, methodId, i)); } await Promise.all(promises); @@ -272,16 +268,16 @@ Tinytest.addAsync('Models - Method - Waited On - check unblock time', async (tes }); Tinytest.addAsync('Models - Method - Waited On - track wait time of next message', async (test, done) => { - let slowMethod = RegisterMethod( function () { - Meteor._sleepForMs(25); + let slowMethod = registerMethod( async function () { + await sleep(25); }); - let fastMethod = RegisterMethod( function () {}); + let fastMethod = registerMethod( function () {}); - let client = GetMeteorClient(); + let client = getMeteorClient(); await Promise.all([ - callPromise(client, slowMethod), - callPromise(client, fastMethod), + clientCallAsync(client, slowMethod), + clientCallAsync(client, fastMethod), ]); const metrics = findMetricsForMethod(slowMethod); @@ -292,20 +288,23 @@ Tinytest.addAsync('Models - Method - Waited On - track wait time of next message export const model = new MethodsModel(); -function CreateMethodCompleted (sessionName, methodName, methodId, startTime, methodDelay) { +function createMethodCompleted (sessionName, methodName, methodId, startTime, methodDelay) { methodDelay = methodDelay || 5; let method = {session: sessionName, name: methodName, id: methodId, events: []}; - method.events.push({type: 'start', at: startTime}); - method.events.push({type: 'complete', at: startTime + methodDelay}); + method.events.push({type: 'start', at: Ntp._now() - startTime}); + method.events.push({type: 'complete', at: Ntp._now() - startTime + methodDelay}); method = Kadira.tracer.buildTrace(method); model.processMethod(method); } -function CreateMethodErrored (sessionName, methodName, methodId, errorMessage, startTime, methodDelay) { +function createMethodErrored (sessionName, methodName, methodId, errorMessage, startTime, methodDelay) { methodDelay = methodDelay || 5; let method = {session: sessionName, name: methodName, id: methodId, events: []}; - method.events.push({type: 'start', at: startTime}); - method.events.push({type: 'error', at: startTime + methodDelay, data: {error: errorMessage}}); + method.events.push({type: 'start', at: Ntp._now() - startTime}); + method.events.push({type: 'error', + at: Ntp._now() - startTime + methodDelay, + endAt: Ntp._now() - startTime + methodDelay, + data: {error: errorMessage}}); method = Kadira.tracer.buildTrace(method); model.processMethod(method); } diff --git a/tests/models/pubsub.js b/tests/models/pubsub.js index c33efef8..6cb0b6f6 100644 --- a/tests/models/pubsub.js +++ b/tests/models/pubsub.js @@ -1,25 +1,27 @@ import { PubsubModel } from '../../lib/models/pubsub'; +import { Ntp } from '../../lib/ntp'; +import { sleep } from '../../lib/utils'; import { + addAsyncTest, addTestWithRoundedTime, + cleanTestData, CleanTestData, - CloseClient, + closeClient, FindMetricsForPub, - GetMeteorClient, - GetPubSubPayload, - RegisterMethod, + getMeteorClient, + getPubSubPayload, + registerMethod, releaseParts, - SubscribeAndWait, + subscribeAndWait, subscribePromise, - Wait, waitForConnection, - WithDocCacheGetSize + withDocCacheGetSize, } from '../_helpers/helpers'; import { TestData } from '../_helpers/globals'; -import { Meteor } from 'meteor/meteor'; addTestWithRoundedTime( 'Models - PubSub - Metrics - same date', - function (test) { + async function (test) { let pub = 'postsList'; let d = new Date('2013 Dec 10 20:30').getTime(); let model = new PubsubModel(); @@ -32,7 +34,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - Metrics - multi date', - function (test) { + async function (test) { let pub = 'postsList'; let d1 = new Date('2013 Dec 10 20:31:12').getTime(); let d2 = new Date('2013 Dec 11 20:31:10').getTime(); @@ -71,7 +73,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - BuildPayload - subs', - function (test) { + async function (test) { let pub = 'postsList'; let d1 = new Date('2013 Dec 10 20:31:12').getTime(); let model = new PubsubModel(); @@ -89,7 +91,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - BuildPayload - routes', - function (test) { + async function (test) { let pub = 'postsList'; let d1 = new Date('2013 Dec 10 20:31:12').getTime(); let route = 'route1'; @@ -110,7 +112,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - BuildPayload - response time', - function (test) { + async function (test) { let pub = 'postsList'; let d1 = new Date('2013 Dec 10 20:31:12').getTime(); let model = new PubsubModel(); @@ -128,7 +130,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - BuildPayload - lifetime', - function (test) { + async function (test) { let pub = 'postsList'; let d1 = new Date('2013 Dec 10 20:31:12').getTime(); let model = new PubsubModel(); @@ -163,7 +165,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - BuildPayload - multiple dates', - function (test) { + async function (test) { let d1 = new Date('2013 Dec 10 20:31:12').getTime(); let d2 = new Date('2013 Dec 11 20:31:12').getTime(); let model = new PubsubModel(); @@ -181,7 +183,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - BuildPayload - multiple subscriptions and dates', - function (test) { + async function (test) { let d1 = new Date('2013 Dec 10 20:31:12').getTime(); let d2 = new Date('2013 Dec 11 20:31:12').getTime(); let model = new PubsubModel(); @@ -199,7 +201,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - Observer Cache - no cache', - function (test) { + async function (test) { let original = Kadira.syncedDate.getTime; let dates = [ new Date('2013 Dec 10 20:31:12').getTime(), @@ -223,7 +225,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - Observer Cache - single cache', - function (test) { + async function (test) { let original = Kadira.syncedDate.getTime; let dates = [ new Date('2013 Dec 10 20:31:12').getTime(), @@ -247,7 +249,7 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - Observer Cache - multiple dates', - function (test) { + async function (test) { let original = Date.now; let dates = [ new Date('2013 Dec 10 20:31:12').getTime(), @@ -273,496 +275,496 @@ addTestWithRoundedTime( addTestWithRoundedTime( 'Models - PubSub - ActiveDocs - Single Sub - simple', - function (test) { - CleanTestData(); + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}, {data: 'data3'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + await subscribeAndWait(client, 'tinytest-data'); + + await sleep(200); + + let payload = getPubSubPayload(); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - Wait(200); - let payload = GetPubSubPayload(); test.equal(payload[0].pubs['tinytest-data'].activeDocs, 3); - h1.stop(); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - ActiveDocs - Single Sub - docs added', - function (test) { - CleanTestData(); + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}, {data: 'data3'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + await subscribeAndWait(client, 'tinytest-data'); + + await TestData.insertAsync({data: 'data4'}); + + await sleep(200); + + let payload = getPubSubPayload(); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - TestData.insert({data: 'data4'}); - Wait(200); - let payload = GetPubSubPayload(); test.equal(payload[0].pubs['tinytest-data'].activeDocs, 4); - h1.stop(); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - ActiveDocs - Single Sub - docs removed', - function (test) { - CleanTestData(); + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}, {data: 'data3'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - TestData.remove({data: 'data3'}); - Wait(200); - let payload = GetPubSubPayload(); + + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + await subscribeAndWait(client, 'tinytest-data'); + + await TestData.removeAsync({data: 'data3'}); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].activeDocs, 2); - h1.stop(); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - ActiveDocs - Single Sub - unsub before payload', - function (test) { - CleanTestData(); + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}, {data: 'data3'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - h1.stop(); - Wait(200); - let payload = GetPubSubPayload(); + + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + const sub = await subscribeAndWait(client, 'tinytest-data'); + + sub.stop(); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].activeDocs, 0); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - ActiveDocs - Single Sub - close before payload', - function (test) { - CleanTestData(); + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}, {data: 'data3'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - CloseClient(client); - Wait(200); - let payload = GetPubSubPayload(); + + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + const sub = await subscribeAndWait(client, 'tinytest-data'); + sub.stop(); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].activeDocs, 0); - h1.stop(); } ); addTestWithRoundedTime( 'Models - PubSub - ActiveDocs - Multiple Subs - simple', - function (test) { - CleanTestData(); + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}, {data: 'data3'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - let h2 = SubscribeAndWait(client, 'tinytest-data'); - let h3 = SubscribeAndWait(client, 'tinytest-data'); - Wait(200); - let payload = GetPubSubPayload(); + + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + await subscribeAndWait(client, 'tinytest-data'); + await subscribeAndWait(client, 'tinytest-data'); + await subscribeAndWait(client, 'tinytest-data'); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].activeDocs, 9); - h1.stop(); - h2.stop(); - h3.stop(); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - ActiveDocs - Multiple Subs - sub and unsub', - function (test) { - CleanTestData(); + async function (test, client) { let docs = [{data: 'data1'}, {data: 'data2'}, {data: 'data3'}]; - docs.forEach(function (doc) { - TestData.insert(doc); - }); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - let h2 = SubscribeAndWait(client, 'tinytest-data'); - let h3 = SubscribeAndWait(client, 'tinytest-data'); + + for (const doc of docs) { + await TestData.insertAsync(doc); + } + + let h1 = await subscribeAndWait(client, 'tinytest-data'); + await subscribeAndWait(client, 'tinytest-data'); + await subscribeAndWait(client, 'tinytest-data'); + + // Stop early h1.stop(); - Wait(200); - let payload = GetPubSubPayload(); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].activeDocs, 6); - h2.stop(); - h3.stop(); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - simple', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - let h1 = SubscribeAndWait(client, 'tinytest-data'); - Wait(200); - let payload = GetPubSubPayload(); + async function (test, client) { + let h1 = await subscribeAndWait(client, 'tinytest-data'); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].createdObservers, 1); test.equal(payload[0].pubs['tinytest-data'].deletedObservers, 0); + h1.stop(); - Wait(200); - payload = GetPubSubPayload(); + + await sleep(200); + + payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].deletedObservers, 1); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - polledDocuments with oplog', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - SubscribeAndWait(client, 'tinytest-data'); - Wait(200); - let payload = GetPubSubPayload(); + async function (test, client) { + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await subscribeAndWait(client, 'tinytest-data'); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].polledDocuments, 2); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - oplogInsertedDocuments with oplog', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); + async function (test, client) { + await subscribeAndWait(client, 'tinytest-data'); + + await sleep(50); + + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); - SubscribeAndWait(client, 'tinytest-data'); + await sleep(100); + + let payload = getPubSubPayload(); - Wait(50); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(100); - let payload = GetPubSubPayload(); test.equal(payload[0].pubs['tinytest-data'].oplogInsertedDocuments, 2); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - oplogDeletedDocuments with oplog', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(50); - - SubscribeAndWait(client, 'tinytest-data'); - - Wait(200); - TestData.remove({}); - Wait(100); - let payload = GetPubSubPayload(); + async function (test, client) { + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await sleep(50); + + await subscribeAndWait(client, 'tinytest-data'); + + await sleep(200); + + await TestData.removeAsync({}); + + await sleep(100); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].oplogDeletedDocuments, 2); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - oplogUpdatedDocuments with oplog', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - SubscribeAndWait(client, 'tinytest-data'); - Wait(200); - TestData.update({}, {$set: {kk: 20}}, {multi: true}); - Wait(100); - let payload = GetPubSubPayload(); + async function (test, client) { + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await subscribeAndWait(client, 'tinytest-data'); + + await sleep(200); + + await TestData.updateAsync({}, {$set: {kk: 20}}, {multi: true}); + + await sleep(100); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data'].oplogUpdatedDocuments, 2); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - polledDocuments with no oplog', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - let h1 = SubscribeAndWait(client, 'tinytest-data-with-no-oplog'); - Wait(200); - let payload = GetPubSubPayload(); + async function (test, client) { + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await subscribeAndWait(client, 'tinytest-data-with-no-oplog'); + + await sleep(200); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data-with-no-oplog'].polledDocuments, 2); - h1.stop(); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - initiallyAddedDocuments', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); + async function (test, client) { // This will create two observers - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(50); - SubscribeAndWait(client, 'tinytest-data-random'); - SubscribeAndWait(client, 'tinytest-data-random'); - Wait(100); - let payload = GetPubSubPayload(); + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await sleep(50); + + await subscribeAndWait(client, 'tinytest-data-random'); + await subscribeAndWait(client, 'tinytest-data-random'); + + await sleep(100); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data-random'].initiallyAddedDocuments, 4); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - liveAddedDocuments', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); + async function (test, client) { // This will create two observers - SubscribeAndWait(client, 'tinytest-data-random'); - SubscribeAndWait(client, 'tinytest-data-random'); - Wait(50); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(100); - let payload = GetPubSubPayload(); + await subscribeAndWait(client, 'tinytest-data-random'); + await subscribeAndWait(client, 'tinytest-data-random'); + + await sleep(50); + + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await sleep(100); + + let payload = getPubSubPayload(); test.equal(payload[0].pubs['tinytest-data-random'].liveAddedDocuments, 4); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - liveChangedDocuments', - function (test) { - CleanTestData(); - - let client = GetMeteorClient(); - + async function (test, client) { // This will create two observers - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); - Wait(50); + await sleep(50); - SubscribeAndWait(client, 'tinytest-data-random'); - SubscribeAndWait(client, 'tinytest-data-random'); + await subscribeAndWait(client, 'tinytest-data-random'); + await subscribeAndWait(client, 'tinytest-data-random'); - Wait(100); + await sleep(100); - TestData.update({}, {$set: {kk: 20}}, {multi: true}); + await TestData.updateAsync({}, {$set: {kk: 20}}, {multi: true}); - Wait(50); + await sleep(50); - let payload = GetPubSubPayload(); + let payload = getPubSubPayload(); if (!payload[0].pubs['tinytest-data-random']) { console.log(JSON.stringify(payload, null, 2)); } test.equal(payload[0].pubs['tinytest-data-random'].liveChangedDocuments, 4); - - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - liveRemovedDocuments', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); + async function (test, client) { // This will create two observers - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - SubscribeAndWait(client, 'tinytest-data-random'); - SubscribeAndWait(client, 'tinytest-data-random'); - Wait(100); - TestData.remove({}); - Wait(50); - let payload = GetPubSubPayload(); + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await subscribeAndWait(client, 'tinytest-data-random'); + await subscribeAndWait(client, 'tinytest-data-random'); + + await sleep(100); + await TestData.removeAsync({}); + await sleep(50); + + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data-random'].liveRemovedDocuments, 4); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - initiallySentMsgSize', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(50); - SubscribeAndWait(client, 'tinytest-data-random'); - Wait(100); - let payload = GetPubSubPayload(); + async function (test, client) { + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await sleep(50); + await subscribeAndWait(client, 'tinytest-data-random'); + + await sleep(100); + + let payload = getPubSubPayload(); let templateMsg = '{"msg":"added","collection":"tinytest-data","id":"17digitslongidxxx","fields":{"aa":10}}'; let expectedMsgSize = templateMsg.length * 2; test.equal(payload[0].pubs['tinytest-data-random'].initiallySentMsgSize, expectedMsgSize); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - liveSentMsgSize', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - SubscribeAndWait(client, 'tinytest-data-random'); - Wait(50); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(100); - let payload = GetPubSubPayload(); + async function (test, client) { + await subscribeAndWait(client, 'tinytest-data-random'); + + await sleep(50); + + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await sleep(100); + + let payload = getPubSubPayload(); let templateMsg = '{"msg":"added","collection":"tinytest-data","id":"17digitslongidxxx","fields":{"aa":10}}'; let expectedMsgSize = templateMsg.length * 2; test.equal(payload[0].pubs['tinytest-data-random'].liveSentMsgSize, expectedMsgSize); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - initiallyFetchedDocSize', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(100); - - WithDocCacheGetSize(function () { - SubscribeAndWait(client, 'tinytest-data-random'); - Wait(200); + async function (test, client) { + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + await sleep(100); + + await withDocCacheGetSize(async function () { + await subscribeAndWait(client, 'tinytest-data-random'); + await sleep(200); }, 30); - let payload = GetPubSubPayload(); + let payload = getPubSubPayload(); test.equal(payload[0].pubs['tinytest-data-random'].initiallyFetchedDocSize, 60); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - liveFetchedDocSize', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - - WithDocCacheGetSize(function () { - SubscribeAndWait(client, 'tinytest-data-random'); - Wait(100); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(200); + async function (test, client) { + await withDocCacheGetSize(async function () { + await subscribeAndWait(client, 'tinytest-data-random'); + await sleep(100); + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + await sleep(200); }, 25); - let payload = GetPubSubPayload(); + let payload = getPubSubPayload(); test.equal(payload[0].pubs['tinytest-data-random'].liveFetchedDocSize, 50); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - fetchedDocSize', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - - let h1; - WithDocCacheGetSize(function () { - h1 = SubscribeAndWait(client, 'tinytest-data-cursor-fetch'); - Wait(200); + async function (test, client) { + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + await withDocCacheGetSize(async function () { + await subscribeAndWait(client, 'tinytest-data-cursor-fetch'); + await sleep(200); }, 30); - - let payload = GetPubSubPayload(); + let payload = getPubSubPayload(); test.equal(payload[0].pubs['tinytest-data-cursor-fetch'].fetchedDocSize, 60); - h1.stop(); - CloseClient(client); } ); addTestWithRoundedTime( 'Models - PubSub - Observers - polledDocSize', - function (test) { - CleanTestData(); - let client = GetMeteorClient(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); - Wait(100); - - let h1; - WithDocCacheGetSize(function () { - h1 = SubscribeAndWait(client, 'tinytest-data-with-no-oplog'); - Wait(200); + async function (test, client) { + await cleanTestData(); + + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); + + await sleep(100); + + await withDocCacheGetSize(async function () { + await subscribeAndWait(client, 'tinytest-data-with-no-oplog'); + await sleep(200); }, 30); - let payload = GetPubSubPayload(); + let payload = getPubSubPayload(); + test.equal(payload[0].pubs['tinytest-data-with-no-oplog'].polledDocSize, 60); - h1.stop(); - CloseClient(client); + closeClient(client); } ); -Tinytest.addAsync('Models - PubSub - Wait Time - track wait time', async (test, done) => { + +addAsyncTest('Models - PubSub - Wait Time - track wait time', async (test, client) => { CleanTestData(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); - let client = GetMeteorClient(); - waitForConnection(client); + await waitForConnection(client); - let slowMethod = RegisterMethod(function () { - Meteor._sleepForMs(50); + let slowMethod = registerMethod(async function () { + await sleep(50); }); client.call(slowMethod, () => {}); const pubName = 'tinytest-waited-on'; - SubscribeAndWait(client, pubName); + await subscribeAndWait(client, pubName); - Meteor._sleepForMs(100); + await sleep(100); const metrics = FindMetricsForPub(pubName); test.isTrue(metrics.waitTime > 0, `${metrics.waitTime} should be greater than 0`); - CloseClient(client); - - done(); + closeClient(client); }); -Tinytest.addAsync('Models - PubSub - Waited On - track wait time of queued messages', async (test, done) => { +addAsyncTest('Models - PubSub - Waited On - track wait time of queued messages', async (test, client) => { CleanTestData(); - TestData.insert({aa: 10}); - TestData.insert({aa: 20}); + await TestData.insertAsync({aa: 10}); + await TestData.insertAsync({aa: 20}); - let client = GetMeteorClient(); + await waitForConnection(client); const pubName = 'tinytest-waited-on'; @@ -776,9 +778,7 @@ Tinytest.addAsync('Models - PubSub - Waited On - track wait time of queued messa const metrics = FindMetricsForPub(pubName); test.isTrue(metrics.waitedOn > 500, `${metrics.waitedOn} should be greater than 500`); - CloseClient(client); - - done(); + closeClient(client); }); Meteor.publish('tinytest-unblock-fast', function () { @@ -789,22 +789,22 @@ Meteor.publish('tinytest-unblock-fast', function () { }); Tinytest.addAsync('Models - PubSub - Waited On - track waitedOn without wait time', async (test, done) => { - CleanTestData(); + await CleanTestData(); - let slowMethod = RegisterMethod(function () { + let slowMethod = registerMethod(async function () { console.log('slow method start'); - Meteor._sleepForMs(100); + await sleep(100); console.log('slow method end'); }); - let fastMethod = RegisterMethod(function () { + let fastMethod = registerMethod(function async () { console.log('fastMethod'); }); - let client = GetMeteorClient(); + let client = getMeteorClient(); // subscriptions and method calls made before connected are not run in order - waitForConnection(client); + await waitForConnection(client); const pubName = 'tinytest-unblock-fast'; client.call(slowMethod, () => {}); @@ -814,25 +814,24 @@ Tinytest.addAsync('Models - PubSub - Waited On - track waitedOn without wait tim const metrics = FindMetricsForPub(pubName); test.isTrue(metrics.waitedOn < 10 && metrics.waitedOn >= 0, `${metrics.waitedOn} should be less than 10`); - CloseClient(client); + closeClient(client); done(); }); -Tinytest.addAsync('Models - PubSub - Waited On - track waited on time of next message', async (test, done) => { +Tinytest.addAsync('Models - PubSub - Waited On - track waited on time of next message', async (test) => { CleanTestData(); - let fastMethod = RegisterMethod(function () { + let fastMethod = registerMethod(function () { console.log('fastMethod'); }); - - let client = GetMeteorClient(); + let client = getMeteorClient(); // If we subscribe and call before connected // Meteor sends the messages in the wrong order - waitForConnection(client); + await waitForConnection(client); client.subscribe('tinytest-waited-on'); - client.call(fastMethod); + await client.callAsync(fastMethod); const metrics = FindMetricsForPub('tinytest-waited-on'); @@ -841,27 +840,25 @@ Tinytest.addAsync('Models - PubSub - Waited On - track waited on time of next me } test.isTrue(metrics.waitedOn > 10, `${metrics.waitedOn} should be greater than 10`); - CloseClient(client); - - done(); + closeClient(client); }); -Tinytest.addAsync('Models - PubSub - Waited On - track wait when unblock', async (test, done) => { +addAsyncTest('Models - PubSub - Waited On - track wait when unblock', async (test, client) => { CleanTestData(); - let fastMethod = RegisterMethod(function () { + let fastMethod = registerMethod(function () { console.log('fastMethod'); }); - let client = GetMeteorClient(); // If we subscribe and call before connected // Meteor sends the messages in the wrong order - waitForConnection(client); + await waitForConnection(client); client.subscribe('tinytest-waited-on2'); client.call(fastMethod); - const metrics = FindMetricsForPub('tinytest-waited-on2'); + await sleep(200); + const metrics = Kadira.models.pubsub._getMetrics(Ntp._now(), 'tinytest-waited-on2'); test.isTrue(metrics.waitedOn > 8, `${metrics.waitedOn} should be greater than 8`); @@ -872,7 +869,5 @@ Tinytest.addAsync('Models - PubSub - Waited On - track wait when unblock', async test.isTrue(metrics.waitedOn <= 150, 'waitedOn should be less or equal than 150'); } - CloseClient(client); - - done(); + closeClient(client); }); diff --git a/tests/models/system.js b/tests/models/system.js index 00df0d89..17564283 100644 --- a/tests/models/system.js +++ b/tests/models/system.js @@ -3,18 +3,24 @@ import fs from 'fs'; import { Meteor } from 'meteor/meteor'; import sinon from 'sinon'; import { MEMORY_ROUNDING_FACTOR, SystemModel } from '../../lib/models/system'; -import { Wait, releaseParts } from '../_helpers/helpers'; +import { sleep } from '../../lib/utils'; +import { Wait, addAsyncTest, releaseParts } from '../_helpers/helpers'; + /** * @flaky */ -Tinytest.add( +addAsyncTest( 'Models - System - buildPayload', - function (test) { + async function (test) { let model = new SystemModel(); - Meteor._wrapAsync(function (callback) { - setTimeout(callback, 500); - })(); - let payload = model.buildPayload().systemMetrics[0]; + + await sleep(500); + + let payload = model.buildPayload(); + await sleep(500); + + payload = payload.systemMetrics[0]; + test.isTrue(payload.memory > 0, `memory: ${payload.memory}`); test.isTrue((payload.memory * 1024 * 1024 /* in bytes */) % MEMORY_ROUNDING_FACTOR === 0, 'memory is rounded'); test.isTrue((payload.freeMemory * 1024 * 1024 /* in bytes */) % MEMORY_ROUNDING_FACTOR === 0, 'memory is rounded'); @@ -260,15 +266,21 @@ Tinytest.add( } ); -Tinytest.add( +addAsyncTest( 'Models - System - new Sessions - inactive ddp client', - function (test) { + async function (test) { let model = new SystemModel(); + model.sessionTimeout = 100; + let session = {socket: {headers: {'x-forwarded-for': '1.1.1.1'}}}; + model.handleSessionActivity({msg: 'connect'}, session); - Wait(200); + + await sleep(200); + model.handleSessionActivity({msg: 'sub'}, session); + test.equal(model.newSessions, 2); } ); diff --git a/tests/ping.js b/tests/ping.js index 79fd96da..b4802daf 100644 --- a/tests/ping.js +++ b/tests/ping.js @@ -1,13 +1,14 @@ -import { GetMeteorClient, RegisterMethod } from './_helpers/helpers'; +import { addAsyncTest, callAsync, registerMethod } from './_helpers/helpers'; -Tinytest.add( +addAsyncTest( 'Helpers - ddp server connection', - function (test) { - let methodId = RegisterMethod(function () { + async function (test) { + const methodId = registerMethod(function () { return 'pong'; }); - let client = GetMeteorClient(); - let result = client.call(methodId); + + const result = await callAsync(methodId); + test.equal(result, 'pong'); } ); diff --git a/tests/timeout.js b/tests/timeout.js index 7c061a69..13b0f020 100644 --- a/tests/timeout.js +++ b/tests/timeout.js @@ -1,22 +1,21 @@ import { TimeoutManager } from '../lib/hijack/timeout_manager'; import assert from 'assert'; -import { GetMeteorClient, RegisterMethod } from './_helpers/helpers'; +import { addAsyncTest, callAsync, registerMethod } from './_helpers/helpers'; +import { sleep } from '../lib/utils'; -Tinytest.add( +addAsyncTest( 'Stalled - Method Timeout', - function (test) { + async function (test) { const oldTimeout = Kadira.options.stalledTimeout; Kadira.options.stalledTimeout = 250; - let methodId = RegisterMethod(function () { - Meteor._sleepForMs(500); + const methodId = registerMethod(async function () { + await sleep(500); return 'pong'; }); - let client = GetMeteorClient(); - let error = null; Kadira.EventBus.once('method', 'timeout', (kadiraInfo, err) => { @@ -25,7 +24,7 @@ Tinytest.add( const lastId = TimeoutManager.id; - let result = client.call(methodId); + let result = await callAsync(methodId); assert(lastId < TimeoutManager.id, 'The timeout id must be incremented'); assert(error && error.constructor.name === 'Error'); diff --git a/tests/tracer/tracer.js b/tests/tracer/tracer.js index 8de6da50..dbee9e06 100644 --- a/tests/tracer/tracer.js +++ b/tests/tracer/tracer.js @@ -1,24 +1,44 @@ import { _ } from 'meteor/underscore'; import { Tracer } from '../../lib/tracer/tracer'; -import { Wait } from '../_helpers/helpers'; +import { + addAsyncTest, + callAsync, + cleanBuiltEvents, + cleanTrace, + deepFreeze, + getLastMethodEvents, + registerMethod, + subscribeAndWait +} from '../_helpers/helpers'; +import { sleep } from '../../lib/utils'; +import { TestData } from '../_helpers/globals'; +import { getInfo, MontiAsyncStorage } from '../../lib/async/als'; +import { EventType } from '../../lib/constants'; +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; +import { Ntp } from '../../lib/ntp'; let eventDefaults = { endAt: 0, nested: [], }; -Tinytest.add( +addAsyncTest( 'Tracer - Trace Method - method', - function (test) { + async function (test) { let ddpMessage = { id: 'the-id', msg: 'method', method: 'method-name' }; + let traceInfo = Kadira.tracer.start({id: 'session-id', userId: 'uid'}, ddpMessage); + Kadira.tracer.event(traceInfo, 'start', {abc: 100}); Kadira.tracer.event(traceInfo, 'end', {abc: 200}); + cleanTrace(traceInfo); + let expected = { _id: 'session-id::the-id', id: 'the-id', @@ -31,11 +51,12 @@ Tinytest.add( {type: 'end', data: {abc: 200}} ] }; + test.equal(traceInfo, expected); } ); -Tinytest.add( +addAsyncTest( 'Tracer - Trace Method - complete after errored', function (test) { let ddpMessage = { @@ -43,11 +64,15 @@ Tinytest.add( msg: 'method', method: 'method-name' }; + let traceInfo = Kadira.tracer.start({id: 'session-id', userId: 'uid'}, ddpMessage); + Kadira.tracer.event(traceInfo, 'start'); Kadira.tracer.event(traceInfo, 'error'); Kadira.tracer.event(traceInfo, 'complete'); + cleanTrace(traceInfo); + let expected = { _id: 'session-id::the-id', id: 'the-id', @@ -60,6 +85,7 @@ Tinytest.add( {type: 'error'} ], }; + test.equal(traceInfo, expected); } ); @@ -105,21 +131,29 @@ Tinytest.add( } ); -Tinytest.add( +addAsyncTest( 'Tracer - trace other events', - function (test) { + async function (test) { let ddpMessage = { id: 'the-id', msg: 'method', method: 'method-name' }; + let traceInfo = Kadira.tracer.start({id: 'session-id'}, ddpMessage); + Kadira.tracer.event(traceInfo, 'start', {abc: 100}); + let eventId = Kadira.tracer.event(traceInfo, 'db'); - Wait(25); + + await sleep(25); + Kadira.tracer.eventEnd(traceInfo, eventId); + Kadira.tracer.event(traceInfo, 'end', {abc: 200}); + cleanTrace(traceInfo); + let expected = { _id: 'session-id::the-id', id: 'the-id', @@ -132,26 +166,34 @@ Tinytest.add( {type: 'end', data: {abc: 200}} ], }; + delete traceInfo.userId; - test.equal(traceInfo, expected); + + test.stableEqual(traceInfo, expected); } ); -Tinytest.add( +addAsyncTest( 'Tracer - end last event', - function (test) { + async function (test) { let ddpMessage = { id: 'the-id', msg: 'method', method: 'method-name' }; + let traceInfo = Kadira.tracer.start({id: 'session-id'}, ddpMessage); + Kadira.tracer.event(traceInfo, 'start', {abc: 100}); Kadira.tracer.event(traceInfo, 'db'); - Wait(20); + + await sleep(25); + Kadira.tracer.endLastEvent(traceInfo); Kadira.tracer.event(traceInfo, 'end', {abc: 200}); + cleanTrace(traceInfo); + let expected = { _id: 'session-id::the-id', id: 'the-id', @@ -164,25 +206,35 @@ Tinytest.add( {type: 'end', data: {abc: 200}} ] }; + delete traceInfo.userId; + test.equal(traceInfo, expected); } ); -Tinytest.add( +addAsyncTest( 'Tracer - trace same event twice', - function (test) { + async function (test) { let ddpMessage = { id: 'the-id', msg: 'method', method: 'method-name' }; + let traceInfo = Kadira.tracer.start({id: 'session-id'}, ddpMessage); + Kadira.tracer.event(traceInfo, 'start', {abc: 100}); + let eventId = Kadira.tracer.event(traceInfo, 'db'); + + // Due to parallelization, we should not assume the order of events. Kadira.tracer.event(traceInfo, 'db'); - Wait(20); + + await sleep(20); + Kadira.tracer.eventEnd(traceInfo, eventId); + Kadira.tracer.event(traceInfo, 'end', {abc: 200}); cleanTrace(traceInfo); @@ -195,42 +247,252 @@ Tinytest.add( name: 'method-name', events: [ {type: 'start', data: {abc: 100}}, - {type: 'db', endAt: 10, nested: [{ type: 'db', endAt: null }]}, + {type: 'db', endAt: 10}, + {type: 'db'}, {type: 'end', data: {abc: 200}} ] }; + delete traceInfo.userId; - test.equal(traceInfo, expected); + + test.stableEqual(traceInfo, expected); } ); -Tinytest.add( +addAsyncTest( + 'Tracer - Monti.event - run function outside of trace', + async function (test) { + MontiAsyncStorage.run(undefined, () => { + test.equal(getInfo(), undefined); + let result = Monti.event('test', () => 'result'); + test.equal(result, 'result'); + }); + } +); + +addAsyncTest( + 'Tracer - Monti.event - provide data', + async function (test) { + let info; + let data = { value: 5 }; + let ran = false; + + const methodId = registerMethod(async function () { + Monti.event('test', data, () => { + ran = true; + }); + + info = getInfo(); + }); + + await callAsync(methodId); + + const expected = [ + 'custom', + 0, + data, + { name: 'test', nested: [] } + ]; + let actualEvent = cleanBuiltEvents(info.trace.events) + .find(event => event[0] === 'custom'); + + test.equal(actualEvent, expected); + test.equal(ran, true); + } +); + +addAsyncTest( 'Tracer - Build Trace - simple', - function (test) { - let now = new Date().getTime(); + async function (test) { + let now = Ntp._now(); + let traceInfo = { events: [ {...eventDefaults, type: 'start', at: now, endAt: now}, {...eventDefaults, type: 'wait', at: now, endAt: now + 1000}, {...eventDefaults, type: 'db', at: now + 2000, endAt: now + 2500}, - {type: 'complete', at: now + 2500} + {type: EventType.Complete, at: now + 4500} ] }; + Kadira.tracer.buildTrace(traceInfo); - test.equal(traceInfo.metrics, { - total: 2500, + + const expected = { + total: 4500, wait: 1000, db: 500, + compute: 3000, + async: 0, + }; + + test.stableEqual(traceInfo.metrics, expected); + test.stableEqual(traceInfo.errored, false); + } +); + +addAsyncTest( + 'Tracer - Build Trace - compute time with force end events', + async function (test) { + let now = Ntp._now(); + + // TODO: work around needed since optimizeEvent sets missing endAt as + // the current time + let oldNow = Ntp._now; + Ntp._now = () => now + 4500; + + let traceInfo = { + events: [ + {...eventDefaults, type: 'start', at: now, endAt: now}, + {...eventDefaults, type: 'wait', at: now, endAt: now + 1000}, + {...eventDefaults, type: 'db', at: now + 2000, endAt: undefined}, + {type: EventType.Complete, at: now + 4500} + ] + }; + + Kadira.tracer.buildTrace(traceInfo); + + Ntp._now = oldNow; + + const expected = { + total: 4500, + wait: 1000, + db: 2500, compute: 1000, + async: 0, + }; + + test.stableEqual(traceInfo.metrics, expected); + test.stableEqual(traceInfo.errored, false); + } +); + +addAsyncTest( + 'Tracer - Build Trace - compute time at beginning of nested events', + async function (test) { + let info; + + const methodId = registerMethod(async function () { + doCompute(20); + await Kadira.event('test', async () => { + doCompute(41); + await TestData.insertAsync({}); + }); + doCompute(10); + info = getInfo(); }); - test.equal(traceInfo.errored, false); + + await callAsync(methodId); + + const expected = [ + ['start', 0, { userId: null, params: '[]' }], + ['wait', 0, { waitOn: [] }], + ['compute', 20], + ['custom', 40, {}, { + name: 'test', + nested: [ + ['compute', 40], + ['db', 0, { coll: 'tinytest-data', func: 'insertAsync' }], + ] + }], + ['compute', 0], + ['complete'] + ]; + let actual = cleanBuiltEvents(info.trace.events, 20); + + test.stableEqual(actual, expected); } ); -Tinytest.add( +addAsyncTest( + 'Tracer - Build Trace - compute time at end of nested events', + async function (test) { + let info; + + const methodId = registerMethod(async function () { + doCompute(20); + await Kadira.event('test', async () => { + await TestData.insertAsync({}); + doCompute(41); + }); + doCompute(20); + info = getInfo(); + }); + + await callAsync(methodId); + + const expected = [ + ['start', 0, { userId: null, params: '[]' }], + ['wait', 0, { waitOn: [] }], + ['compute', 20], + ['custom', 40, {}, { + name: 'test', + nested: [ + ['db', 0, { coll: 'tinytest-data', func: 'insertAsync' }], + ['compute', 40], + ] + }], + ['compute', 20], + ['complete'] + ]; + + let actual = cleanBuiltEvents(info.trace.events, 20); + + test.stableEqual(actual, expected); + + test.equal(info.trace.metrics.compute >= 70, true); + test.equal(info.trace.metrics.async < 5, true); + test.equal(info.trace.metrics.custom, undefined); + } +); + +addAsyncTest( + 'Tracer - Build Trace - use events nested under custom for metrics', + async function (test) { + let info; + + const methodId = registerMethod(async function () { + await Kadira.event('test', async () => { + doCompute(50); + await TestData.insertAsync({ a: 2 }); + await TestData.insertAsync({ b: 3 }); + await TestData.insertAsync({ b: 3 }); + await TestData.insertAsync({ b: 3 }); + await sleep(11); + }); + info = getInfo(); + }); + + await callAsync(methodId); + test.equal(info.trace.metrics.compute >= 50, true, `${info.trace.metrics.compute} >= 50`); + test.equal(info.trace.metrics.db > 0, true, `${info.trace.metrics.db} > 0`); + test.equal(info.trace.metrics.async >= 10, true, `${info.trace.metrics.async} >= 10`); + test.equal(info.trace.metrics.custom, undefined, `${info.trace.metrics.custom} == undefined`); + } +); + +addAsyncTest( + 'Tracer - Build Trace - compute time for custom event without nested events', + async function (test) { + let info; + + const methodId = registerMethod(async function () { + Kadira.event('test', async () => { + doCompute(50); + }); + info = getInfo(); + }); + + await callAsync(methodId); + test.equal(info.trace.metrics.compute > 40, true); + test.equal(info.trace.metrics.custom, undefined); + } +); + +addAsyncTest( 'Tracer - Build Trace - errored', function (test) { let now = new Date().getTime(); + let traceInfo = { events: [ {...eventDefaults, type: 'start', at: now}, @@ -239,13 +501,18 @@ Tinytest.add( {...eventDefaults, type: 'error', at: now + 2500} ] }; + Kadira.tracer.buildTrace(traceInfo); - test.equal(traceInfo.metrics, { + + const expected = { total: 2500, wait: 1000, db: 500, compute: 1000, - }); + async: 0, + }; + + test.equal(traceInfo.metrics, expected); test.equal(traceInfo.errored, true); } ); @@ -282,11 +549,12 @@ Tinytest.add( } ); -Tinytest.add( +addAsyncTest( 'Tracer - Build Trace - event not ended', function (test) { - let now = new Date().getTime(); - let traceInfo = { + const now = 0; + + const traceInfo = { events: [ {type: 'start', at: now}, {type: 'wait', at: now, endAt: null}, @@ -294,12 +562,21 @@ Tinytest.add( {type: 'complete', at: now + 2500} ] }; + Kadira.tracer.buildTrace(traceInfo); - test.equal(traceInfo.metrics, undefined); + + const expected = [ + ['start'], + ['wait', traceInfo.events[1][1], {}, { forcedEnd: true }], + ['db', 500, {}, { offset: traceInfo.events[2][3].offset }], + ['complete'] + ]; + + test.stableEqual(traceInfo.events, expected); } ); -Tinytest.add( +addAsyncTest( 'Tracer - Build Trace - truncate events', function (test) { let now = new Date().getTime(); @@ -447,36 +724,367 @@ Tinytest.add( } ); +addAsyncTest('Tracer - Build Trace - each ms counted once with parallel events', async function (test) { + const Email = Package['email'].Email; + let info; + + let methodId = registerMethod(async function () { + let backgroundPromise; + // Compute + await sleep(30); + + await TestData.insertAsync({ _id: 'a', n: 1 }); + await TestData.insertAsync({ _id: 'b', n: 2 }); + await TestData.insertAsync({ _id: 'c', n: 3 }); + + backgroundPromise = Promise.resolve().then(async () => { + // Email + Email.sendAsync({ from: 'arunoda@meteorhacks.com', to: 'hello@meteor.com' }); + }); + + // Compute + await sleep(30); + + const ids = ['a', 'b', 'c']; + + // DB + await Promise.all(ids.map(_id => TestData.findOneAsync({ _id }))); + + await TestData.findOneAsync({ _id: 'a1' }).then(() => + // Is this nested under the previous findOneAsync or is it a sibling? + TestData.findOneAsync({ _id: 'a2' }) + ); + + info = getInfo(); + + return backgroundPromise; + }); + + await callAsync(methodId); + + let metrics = info.trace.metrics; + let total = metrics.total; + let sum = 0; + Object.keys(metrics).forEach(key => { + if (key !== 'total') { + sum += metrics[key]; + } + }); + + test.equal(sum, total); +}); + +addAsyncTest('Tracer - Build Trace - custom with nested parallel events', async function (test) { + const Email = Package['email'].Email; + + let methodId = registerMethod(async function () { + let backgroundPromise; + // Compute + await sleep(30); + + await Kadira.event('test', async (event) => { + await TestData.insertAsync({ _id: 'a', n: 1 }); + await TestData.insertAsync({ _id: 'b', n: 2 }); + await TestData.insertAsync({ _id: 'c', n: 3 }); + + backgroundPromise = Promise.resolve().then(async () => { + // Email + Email.sendAsync({ from: 'arunoda@meteorhacks.com', to: 'hello@meteor.com' }); + }); + + // Compute + await sleep(30); + + const ids = ['a', 'b', 'c']; + + // DB + await Promise.all(ids.map(_id => TestData.findOneAsync({_id}))); + + await TestData.findOneAsync({ _id: 'a1'}).then(() => + // Is this nested under the previous findOneAsync or is it a sibling? + TestData.findOneAsync({ _id: 'a2' }) + ); + + Kadira.endEvent(event); + }); + + return backgroundPromise; + }); + + await callAsync(methodId); + + const events = getLastMethodEvents([0, 2, 3]); + + const expected = [ + ['start',{userId: null,params: '[]'}], + ['wait',{waitOn: []}], + ['custom', {}, {name: 'test', + nested: [ + ['db',{coll: 'tinytest-data',func: 'insertAsync'}], + ['db',{coll: 'tinytest-data',func: 'insertAsync'}], + ['db',{coll: 'tinytest-data',func: 'insertAsync'}], + ['email',{from: 'arunoda@meteorhacks.com',to: 'hello@meteor.com', func: 'emailAsync'}, { offset: 1 }], + ['db',{coll: 'tinytest-data',selector: '{"_id":"a"}',func: 'fetch',cursor: true,limit: 1,docsFetched: 1,docSize: 1}], + ['db',{coll: 'tinytest-data',selector: '{"_id":"b"}',func: 'fetch',cursor: true,limit: 1,docsFetched: 1,docSize: 1}, { offset: 1 }], + ['db',{coll: 'tinytest-data',selector: '{"_id":"c"}',func: 'fetch',cursor: true,limit: 1,docsFetched: 1,docSize: 1}, { offset: 1 }], + ['db',{coll: 'tinytest-data',selector: '{"_id":"a1"}',func: 'fetch',cursor: true,limit: 1,docsFetched: 0,docSize: 0}], + ['db',{coll: 'tinytest-data',selector: '{"_id":"a2"}',func: 'fetch',cursor: true,limit: 1,docsFetched: 0,docSize: 0}] + ]} + ], + ['complete'] + ]; + + test.stableEqual(events, expected); +}); + +addAsyncTest('Tracer - Build Trace - offset is reversible', async function (test) { + const Email = Package['email'].Email; + + let origEvents; + let info; + let methodId = registerMethod(async function () { + let backgroundPromise; + // Compute + await sleep(30); + + await Kadira.event('test', async (event) => { + await TestData.insertAsync({ _id: 'a', n: 1 }); + await TestData.insertAsync({ _id: 'b', n: 2 }); + await TestData.insertAsync({ _id: 'c', n: 3 }); + + backgroundPromise = Promise.resolve().then(async () => { + // Email + Email.sendAsync({ from: 'arunoda@meteorhacks.com', to: 'hello@meteor.com' }); + }); + + // Compute + await sleep(30); + + const ids = ['a', 'b', 'c']; + + // DB + await Promise.all(ids.map(_id => TestData.findOneAsync({ _id }))); + + await TestData.findOneAsync({ _id: 'a1' }).then(() => + // Is this nested under the previous findOneAsync or is it a sibling? + TestData.findOneAsync({ _id: 'a2' }) + ); + + Kadira.endEvent(event); + }); + + origEvents = getInfo().trace.events; + info = getInfo(); + + return backgroundPromise; + }); + + await callAsync(methodId); + + let origTimestamps = origEvents.reduce((timestamps, event) => { + if (event.type !== 'async') { + timestamps.push(event.at); + } + if (event.nested && event.type === 'custom') { + event.nested.forEach(nestedEvent => { + if (nestedEvent.type !== 'async') { + timestamps.push(nestedEvent.at); + } + }); + } + + return timestamps; + }, []); + + let calculatedTimestamps = []; + let total = info.trace.at; + info.trace.events.forEach((event) => { + let [type, duration = 0, , details = {}] = event; + let offset = details.offset || 0; + + total -= offset; + if (type !== 'async' && type !== 'compute') { + calculatedTimestamps.push(total); + } + + if (details.nested && type === 'custom') { + let nestedTotal = total; + details.nested.forEach(nestedEvent => { + if (nestedEvent[3] && nestedEvent[3].offset) { + nestedTotal -= nestedEvent[3].offset; + } + + if (nestedEvent[0] !== 'async' && nestedEvent[0] !== 'compute') { + calculatedTimestamps.push(nestedTotal); + } + + nestedTotal += nestedEvent[1] || 0; + }); + } + + total += duration; + }); + + test.stableEqual(calculatedTimestamps, origTimestamps); +}); + +addAsyncTest('Tracer - Build Trace - should end custom event', async (test) => { + let info; + + const methodId = registerMethod(async function () { + Kadira.event('test', { async: false }, () => { + Kadira.event('test2', { value: true }, () => {}); + }); + Kadira.event('test3', () => {}); + + info = getInfo(); + }); + + await callAsync(methodId); + + const expected = [ + ['start', 0, { userId: null, params: '[]' }], + ['wait', 0, { waitOn: []}], + ['custom', 0, { async: false }, { + name: 'test', + nested: [ + ['custom', 0, { value: true }, { name: 'test2', nested: []}], + ] + }], + ['custom', 0, {}, { name: 'test3', nested: [] }], + ['complete'] + ]; + let actual = cleanBuiltEvents(info.trace.events); + + test.stableEqual(actual, expected); +}); + +addAsyncTest('Tracer - Build Trace - should end async events', async (test) => { + let info; + + const methodId = registerMethod(async function () { + await sleep(21); + + info = getInfo(); + }); + + await callAsync(methodId); + + const expected = [ + ['start', 0, { userId: null, params: '[]' }], + ['wait', 0, { waitOn: [] }], + ['async', 20], + ['complete'] + ]; + let actual = cleanBuiltEvents(info.trace.events); + + test.stableEqual(actual, expected); +}); + +addAsyncTest('Tracer - Build Trace - the correct number of async events are captured for methods', async (test) => { + let info; + + const methodId = registerMethod(async function () { + await sleep(15); + await sleep(20); + + info = getInfo(); + + return await sleep(20); + }); + + await callAsync(methodId); + + const asyncEvents = info.trace.events.filter(([type, duration]) => type === EventType.Async && duration >= 10); + + test.equal(asyncEvents.length, 3); +}); + +addAsyncTest('Tracer - Build Trace - the correct number of async events are captured for pubsub', async (test, client) => { + const subName = `sub_${Random.id()}`; + + let info; + + Meteor.publish(subName, async function () { + await sleep(20); + + info = getInfo(); + + return []; + }); + + await subscribeAndWait(client, subName); + + const asyncEvents = info.trace.events.filter(([type, duration]) => type === EventType.Async && duration >= 15); + + test.equal(asyncEvents.length,1); +}); + +addAsyncTest('Tracer - Optimize Events - no mutation', async (test) => { + let partialEvents; + + const methodId = registerMethod(async function () { + const Email = Package['email'].Email; + + await TestData.insertAsync({ _id: 'a', n: 1 }); + await sleep(20); + + await Email.sendAsync({ from: 'arunoda@meteorhacks.com', to: 'hello@meteor.com' }); + + let info = getInfo(); + let events = info.trace.events.slice(); + deepFreeze(events); + + // testing this doesn't throw + partialEvents = Kadira.tracer.optimizeEvents(events); + + info.trace.events = [Kadira.tracer.event(info.trace, 'start')]; + }); + + await callAsync(methodId); + + test.equal(Array.isArray(partialEvents), true); +}); + +addAsyncTest('Tracer - Optimize Events - without metrics', async (test) => { + let now = Ntp._now(); + + let events = [ + { ...eventDefaults, type: 'start', at: now, endAt: now }, + { ...eventDefaults, type: 'wait', at: now, endAt: now + 1000 }, + { ...eventDefaults, type: 'db', at: now + 2000, endAt: now + 2500 }, + { type: EventType.Complete, at: now + 4500 } + ]; + + let expected = [ + ['start'], + ['wait', 1000], + ['compute', 1000], + ['db', 500], + ['compute', 2000], + ['complete'] + ]; + + let optimized = Kadira.tracer.optimizeEvents(events); + + test.stableEqual(optimized, expected); +}); + function startTrace () { - let ddpMessage = { + const ddpMessage = { id: 'the-id', msg: 'method', method: 'method-name' }; - let info = {id: 'session-id', userId: 'uid'}; - let traceInfo = Kadira.tracer.start(info, ddpMessage); - return traceInfo; -} + const info = {id: 'session-id', userId: 'uid'}; -function cleanTrace (traceInfo) { - cleanEvents(traceInfo.events); + return Kadira.tracer.start(info, ddpMessage); } -function cleanEvents (events) { - events.forEach(function (event) { - if (event.endAt > event.at) { - event.endAt = 10; - } else if (event.endAt) { - delete event.endAt; - } - delete event.at; - delete event._id; - - if (event.nested.length === 0) { - delete event.nested; - } else { - cleanEvents(event.nested); - } - }); +function doCompute (ms) { + let start = Date.now(); + while (Date.now() - start < ms) { + // do work... + } } diff --git a/tests/utils.js b/tests/utils.js index 575425c0..bf05d13d 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -1,9 +1,9 @@ import { Random } from 'meteor/random'; import { _ } from 'meteor/underscore'; -import { OptimizedApply } from '../lib/utils'; +import { optimizedApply } from '../lib/utils'; Tinytest.addAsync( - 'Utils - OptimizedApply - calling arguments', + 'Utils - optimizedApply - calling arguments', function (test, done) { runWithArgs(0); function runWithArgs (argCount) { @@ -16,7 +16,7 @@ Tinytest.addAsync( return retValue; }; - let ret = OptimizedApply(context, fn, args); + let ret = optimizedApply(context, fn, args); test.equal(ret, retValue); if (argCount > 10) { diff --git a/types.d.ts b/types.d.ts index 16edbd84..5bb892f7 100644 --- a/types.d.ts +++ b/types.d.ts @@ -48,7 +48,7 @@ export namespace Monti { function ignoreErrorTracking(error: Error): void; - function startEvent(name: string, data?: Record): MontiEvent | false; + function startEvent(name: string, data?: Record, fn?: Function): MontiEvent | false; function endEvent(event: MontiEvent | false, data?: Record): void; function traceJob any>(options: TraceJobOptions, fn: T): ReturnType