+
+
+
+
+
+
diff --git a/apps/docs/docs/.vitepress/theme/index.ts b/apps/docs/docs/.vitepress/theme/index.ts
index c495bc1b8..3d8bf3f09 100644
--- a/apps/docs/docs/.vitepress/theme/index.ts
+++ b/apps/docs/docs/.vitepress/theme/index.ts
@@ -1,4 +1,13 @@
import DefaultTheme from "vitepress/theme";
+import ImageModal from "./components/ImageModal.vue";
import "./custom.css";
+import { h } from "vue";
-export default DefaultTheme;
+export default {
+ extends: DefaultTheme,
+ Layout() {
+ return h(DefaultTheme.Layout, null, {
+ "layout-bottom": () => h(ImageModal),
+ });
+ },
+};
diff --git a/apps/docs/docs/hosting-and-deployment/how-to/index.md b/apps/docs/docs/hosting-and-deployment/how-to/index.md
index a18cd563c..439204fc4 100644
--- a/apps/docs/docs/hosting-and-deployment/how-to/index.md
+++ b/apps/docs/docs/hosting-and-deployment/how-to/index.md
@@ -3,4 +3,4 @@
In this section, we provide documentation for organizations wanting to deploy and host their own instance of HydroServer.
* **[Setting Up a Production Deployment](/hosting-and-deployment/how-to/setting-up-a-production-deployment.md)**: Learn how to deploy a HydroServer instance in a production environment.
-* **[Using the Administrator Dashboard](/hosting-and-deployment/how-to/using-the-administrator-dashboard.md)**: Learn how to use the administrator dashboard to set up, manage, and customize your HydroServer instance.
\ No newline at end of file
+* **[Using the Django Admin Dashboard](/hosting-and-deployment/how-to/using-the-administrator-dashboard.md)**: Learn how to use the Django admin dashboard to set up, manage, and customize your HydroServer instance.
diff --git a/apps/docs/docs/hosting-and-deployment/how-to/using-the-administrator-dashboard.md b/apps/docs/docs/hosting-and-deployment/how-to/using-the-administrator-dashboard.md
index 4f195d7d9..22f217245 100644
--- a/apps/docs/docs/hosting-and-deployment/how-to/using-the-administrator-dashboard.md
+++ b/apps/docs/docs/hosting-and-deployment/how-to/using-the-administrator-dashboard.md
@@ -1,93 +1,240 @@
-# Using the Administrator Dashboard
+# Using the Django Admin Dashboard
-Once HydroServer is running and accessible, additional configuration settings can be managed through the admin
-dashboard.
+Once HydroServer is running and accessible, instance administrators can use the Django admin dashboard to manage
+deployment-wide settings, default metadata, controlled vocabularies, authentication providers, and selected maintenance
+actions.
## Access the Admin Dashboard
-You can log in to the administrator dashboard at:
+Log in to the administrator dashboard at:
```
-https:///admin`
+https:///admin
```
-Only administrators and staff accounts can access the dashboard. To create an administrator account programmatically,
-run the following command and follow the prompts to set an email and password:
+Only users with **Staff status** can access the dashboard. Users with **Superuser status** can manage all admin models
+and run privileged admin actions. To create an administrator account programmatically, run the following command and
+follow the prompts to set an email and password:
```bash
python manage.py createsuperuser
```
-Admin users can also upgrade other user accounts to administrators through the admin dashboard by clicking on
-**Identity and Access Management** → **Users**, selecting a user, then toggling both **Superuser Status** and
+Admin users can also upgrade other user accounts to administrators through the admin dashboard by clicking on
+**Identity and Access Management** > **Users**, selecting a user, then toggling both **Superuser Status** and
**Staff Status** to `true`.
-## HydroServer User Accounts
+## User Accounts and Access
-HydroServer can be configured to limit access to certain features to new users by default via the
-**ACCOUNT_OWNERSHIP_ENABLED** setting. If not enabled, all new users will automatically be assigned a limited account.
-Limited accounts cannot create or own workspaces, however, they can be added as collaborators to existing workspaces.
+HydroServer can be configured to limit access to certain features for new users by default via the
+`ACCOUNT_OWNERSHIP_ENABLED` setting. If this setting is not enabled, new users are assigned limited accounts. Limited
+accounts cannot create or own workspaces, but they can be added as collaborators to existing workspaces.
-Administrators can manually upgrade a limited account to a standard account by clicking on
-**Identity and Access Management** → **Users**, selecting a user, and toggling **Is Ownership Allowed** to `true`.
+Administrators can manually upgrade a limited account to a standard account by clicking on
+**Identity and Access Management** > **Users**, selecting a user, and toggling **Is Ownership Allowed** to `true`.
+
+The **Identity and Access Management** section also exposes the following admin models:
+
+| Model | Purpose |
+| --- | --- |
+| **Users** | Manage user profile fields, account status, staff/superuser access, and ownership permission. |
+| **Organizations** | Manage organizations that can be associated with users and workspaces. |
+| **Workspaces** | Review or edit workspace ownership, privacy, and workspace metadata. |
+| **Roles** | Configure reusable permission bundles for collaborators and API keys. |
+| **Permissions** | Review the resource-level permissions that belong to roles. |
+| **Collaborators** | Review or edit user access to workspaces. |
+| **API Keys** | Review API key metadata, deactivate keys, or regenerate a selected key. |
+
+To regenerate an API key, open **Identity and Access Management** > **API Keys**, select exactly one API key, choose
+**Regenerate selected API key** from the action menu, and click **Go**. The new key is shown once after the action
+completes.
## HydroServer Configuration
-To update the HydroServer domain used in emails and notifications, go to **Sites** → **example.com** and change the
+To update the HydroServer domain used in emails and notifications, go to **Sites** > **example.com** and change the
domain to match your HydroServer domain.
The following website settings can be configured under **Website Configuration**:
-- **Instance Configuration**: Customize the `About` page with information about your organization.
-- **Analytics Configuration**: Optionally enable [Microsoft Clarity](https://clarity.microsoft.com/).
-- **Map Configuration**: Define default map layers, view, geospatial, and elevation services.
-- **Map Layers**: Add additional map layer options for pages that use maps.
-- **Contact Information**: Add organization contact information for display on the `About` page.
+| Model | Purpose |
+| --- | --- |
+| **Instance Configuration** | Configure instance-wide website content and legal links. |
+| **Analytics Configuration** | Enable or disable Microsoft Clarity analytics. |
+| **Map Configuration** | Configure the default map view, default map layers, and map service providers. |
+| **Map Layers** | Create the map layers available to users in HydroServer map controls. |
+| **Contact Information** | Add contact cards shown on the About page. |
+| **Site Type Icons** | Customize which icons are used for different monitoring site types. |
+
+### Instance Configuration
+
+Use **Website Configuration** > **Instance Configuration** to manage general website settings:
+
+| Field | Purpose |
+| --- | --- |
+| **Show about information** | Shows or hides the About page information block. |
+| **About page title** | Sets the heading for the About page information block. |
+| **About page text** | Sets the body content for the About page information block. |
+| **Terms of use link** | Sets the Terms of Use link exposed by the web application. |
+| **Privacy policy link** | Sets the Privacy Policy link exposed by the web application. |
+| **Copyright** | Sets the copyright notice shown by the web application. |
+
+### Analytics Configuration
+
+Use **Website Configuration** > **Analytics Configuration** to optionally enable
+[Microsoft Clarity](https://clarity.microsoft.com/) analytics:
+
+| Field | Purpose |
+| --- | --- |
+| **Enable clarity analytics** | Turns Microsoft Clarity tracking on or off. |
+| **Clarity project id** | Stores the project ID from the Microsoft Clarity project settings. |
+
+### Map Configuration
+
+Use **Website Configuration** > **Map Configuration** to control the initial map state and map service providers:
+
+| Field | Purpose |
+| --- | --- |
+| **Default latitude** | Sets the latitude used when a map first loads without a more specific site extent. |
+| **Default longitude** | Sets the longitude used when a map first loads without a more specific site extent. |
+| **Default zoom level** | Sets the initial map zoom level. |
+| **Default base layer** | Selects the default basemap from the configured **Map Layers**. |
+| **Default satellite layer** | Selects the default satellite imagery layer from the configured **Map Layers**. |
+| **Elevation service** | Selects the elevation lookup provider. Available options are **Open Elevation** and **Google**. |
+| **Geo service** | Selects the geocoding/search provider. Available options are **Nominatim** and **Google**. |
+
+If a Google-backed service is selected, make sure the deployment has the required Google service credentials or API key
+configuration before relying on that option in production.
+
+### Map Layers
+
+Use **Website Configuration** > **Map Layers** to add basemap and overlay options:
+
+| Field | Purpose |
+| --- | --- |
+| **Name** | Sets the display name shown to users. |
+| **Type** | Classifies the layer as a **Basemap** or **Overlay**. |
+| **Priority** | Controls layer ordering when multiple layers are available. |
+| **Source** | Stores the map tile or layer source URL used by the frontend map. |
+| **Attribution** | Sets the attribution text required by the layer provider. |
+
+After creating a basemap or satellite layer, return to **Map Configuration** to select it as a default layer.
+
+### Contact Information
+
+Use **Website Configuration** > **Contact Information** to add organization contact options to the About page:
+
+| Field | Purpose |
+| --- | --- |
+| **Title** | Sets the contact option title. |
+| **Text** | Adds supporting details for the contact option. |
+| **Action** | Sets the call-to-action text shown for the contact option. |
+| **Icon** | Stores the icon identifier used by the web application. |
+| **Link** | Sets the URL or address opened by the contact action. |
+
+Contact information is only visible when About page information is enabled in **Instance Configuration**.
+
+### Site Type Icons
+
+Use **Website Configuration** > **Site Type Icons** to customize monitoring site icons in the Data Management app. The
+available icon records are fixed, but administrators can edit the site type names and keywords associated with each icon.
+
+Enter one site type name or keyword per line. Matching ignores case and punctuation, and longer matching keywords take
+precedence. For example, a site type of `Stream Gage` can map to the gauge icon while `Stream` maps to the water icon.
+Each keyword can be assigned to only one icon. Leave an icon's keyword list empty if it should not be used.
+
+If no configured keyword matches a site's type, the Data Management app uses the default outlined map marker icon.
+
+## Data and Metadata Administration
+
+The **Measurement Data** section exposes the core HydroServer data models. Most data entry should happen through the
+HydroServer web application, API, or client libraries, but the Django admin can be useful for reviewing records and
+performing administrator-only maintenance.
+
+Additional vocabulary models also appear in this admin section and are documented under **Controlled Vocabularies**
+below.
+
+| Model | Purpose |
+| --- | --- |
+| **Things** | Manage monitoring sites and their workspace/privacy metadata. |
+| **Locations** | Review or edit site location records. |
+| **Sensors** | Manage reusable sensor metadata. |
+| **Observed Properties** | Manage reusable observed property metadata. |
+| **Units** | Manage reusable unit metadata. |
+| **Processing Levels** | Manage reusable processing level metadata. |
+| **Datastreams** | Review datastream metadata and run selected maintenance actions. |
+| **Result Qualifiers** | Manage result qualifier codes. |
+| **File Attachment Types** | Manage file attachment type vocabulary values. |
+| **Thing File Attachments** | Review files attached to monitoring sites. |
+| **Datastream File Attachments** | Review files attached to datastreams. |
+| **Thing Tags** | Review key/value tags attached to monitoring sites. |
+| **Datastream Tags** | Review key/value tags attached to datastreams. |
+
+The **Datastreams** admin page includes two actions for selected datastreams:
+
+- **Populate with test observations**: Generates test observations for selected datastreams. This is intended for
+ development and demonstration environments.
+- **Delete datastream observations**: Deletes observations for selected datastreams and resets the datastream
+ phenomenon/result time ranges and value count.
+
+Use these actions carefully. They change observation data directly and should not be used on production data unless that
+is the intended administrative operation.
+
+## Default Data
+
+Several HydroServer models can be populated with bundled default data through the admin dashboard. Open one of the
+models below and click its **Load Default ...** button in the upper-right corner of the changelist page:
+
+| Section | Model | Default data |
+| --- | --- | --- |
+| **Identity and Access Management** | **Roles** | Default workspace roles and permissions. Load these before users add collaborators or API keys to workspaces. |
+| **Identity and Access Management** | **User Types** | Default user type vocabulary values. |
+| **Identity and Access Management** | **Organization Types** | Default organization type vocabulary values. |
+| **Measurement Data** | **Observed Properties** | Default observed property metadata. |
+| **Measurement Data** | **Processing Levels** | Default processing level metadata. |
+| **Measurement Data** | **Units** | Default unit metadata. |
+| **Measurement Data** | **Site Types** | Default monitoring site type vocabulary values. |
+| **Measurement Data** | **Sampling Feature Types** | Default sampling feature type vocabulary values. |
+| **Measurement Data** | **Method Types** | Default method type vocabulary values. |
+| **Measurement Data** | **Sensor Encoding Types** | Default sensor encoding type vocabulary values. |
+| **Measurement Data** | **Variable Types** | Default variable type vocabulary values. |
+| **Measurement Data** | **Unit Types** | Default unit type vocabulary values. |
+| **Measurement Data** | **Datastream Aggregations** | Default aggregation statistic vocabulary values. |
+| **Measurement Data** | **Datastream Statuses** | Default datastream status vocabulary values. |
+| **Measurement Data** | **Sampled Mediums** | Default sampled medium vocabulary values. |
+| **Measurement Data** | **File Attachment Types** | Default file attachment type vocabulary values. |
+
+The load action imports fixture data into the database. It does not replace the need to review locally customized
+metadata after upgrades.
+
+## Controlled Vocabularies
+
+HydroServer forms use several administrator-managed vocabularies. Add or edit these records to control the choices users
+see in metadata forms:
+
+| Section | Models |
+| --- | --- |
+| **Identity and Access Management** | **User Types**, **Organization Types**, **Roles** |
+| **Measurement Data** | **Datastream Aggregations**, **Datastream Statuses**, **File Attachment Types**, **Method Types**, **Sampled Mediums**, **Sampling Feature Types**, **Sensor Encoding Types**, **Site Types**, **Unit Types**, **Variable Types** |
+
+Workspace-scoped metadata such as observed properties, units, processing levels, sensors, and result qualifiers can also
+be reviewed in the admin dashboard. When a record has no workspace, it is available as system-level metadata.
## Third-Party Identity Providers
To enable authentication through third-party identity providers, do the following:
-- Navigate to **Social Applications** → **Add Social Application**.
-- Select a supported provider (e.g., Google, HydroShare).
-- Enter a unique ID, name, client ID/key, and secret key from the provider.
-- (Optional) Add JSON settings, for example:
+- Navigate to **Social Applications** > **Add Social Application**.
+- Select a supported provider, such as Google or HydroShare.
+- Enter a unique ID, name, client ID/key, and secret key from the provider.
+- Optionally add JSON settings, for example:
```json
{
"allowSignUp": true,
"allowConnection": true
}
- ```
- - **allowSignUp**: (true/false) — whether users can sign up and log in with this provider.
- - **allowConnection**: (true/false) — whether users can connect the provider to an existing HydroServer account.
-- Assign the default site that the provider can authenticate against.
-
-## HydroServer Default Data
-
-Several HydroServer models can be populated with default data, or be customized through the admin dashboard. The
-following models have a **Load Default Data** option that can be used to populate the model with default
-data:
-
-- **Identity and Access Management** → **Roles** (Default data must be loaded before users can add collaborators or API keys to workspaces)
-- **Measurement Data** → **Observed Properties**
-- **Measurement Data** → **Processing Levels**
-- **Measurement Data** → **Result Qualifiers**
-- **Measurement Data** → **Sensors**
-- **Measurement Data** → **Units**
-
-## HydroServer Vocabularies
-
-There are several user forms throughout HydroServer that can be populated with custom values. These values can be
-added through the admin dashboard by populating the following models:
-
-- **Identity and Access Management** → **User Types**
-- **Identity and Access Management** → **Organization Types**
-- **Measurement Data** → **Datastream Aggregations**
-- **Measurement Data** → **Datastream Statuses**
-- **Measurement Data** → **Method Types**
-- **Measurement Data** → **Sampled Mediums**
-- **Measurement Data** → **Sampling Feature Types**
-- **Measurement Data** → **Sensor Encoding Types**
-- **Measurement Data** → **Site Types**
-- **Measurement Data** → **Unit Types**
-- **Measurement Data** → **Variable Types**
+ ```
+- Use **allowSignUp** to control whether users can sign up with this provider.
+- Use **allowConnection** to control whether users can connect the provider to an existing HydroServer account.
+- When omitted, **allowSignUp** defaults to enabled, but **allowConnection** is enabled only when explicitly set to
+ `true`.
+- Assign the default site that the provider can authenticate against.
diff --git a/apps/docs/docs/user-guides/how-to/quality-controlling-data.md b/apps/docs/docs/user-guides/how-to/quality-controlling-data.md
index 7f33656d3..bb6f7c57a 100644
--- a/apps/docs/docs/user-guides/how-to/quality-controlling-data.md
+++ b/apps/docs/docs/user-guides/how-to/quality-controlling-data.md
@@ -413,14 +413,14 @@ Every filter, edit, and add operation appends a row to **Edit history** in the r
-The header carries the count chip and four icon buttons (left to right): **undo**, **redo**, **save QC script** (tray-arrow-down), **load QC script** (tray-arrow-up), and **open in window** (the pop-out icon, which reopens the same panel inside a modal). Keyboard shortcuts: `Ctrl+Z` to undo, `Ctrl+Y` or `Ctrl+Shift+Z` to redo.
+The header carries the count chip and four icon buttons (left to right): **undo**, **redo**, **save QC history** (tray-arrow-down), **load QC history** (tray-arrow-up), and **open in window** (the pop-out icon, which reopens the same panel inside a modal). Keyboard shortcuts: `Ctrl+Z` to undo, `Ctrl+Y` or `Ctrl+Shift+Z` to redo.
The body shows:
- A baseline **Data loaded** row at the top, with a reload-from-server button.
- One row per history entry, each with:
- The operation icon and Title-Case name.
- - A failure badge (red `!`) if the op threw at author time. Common after a script import that references something missing in this datastream.
+ - A failure badge (red `!`) if the op threw at author time. Common after a QC history import that references something missing in this datastream.
- A duration badge.
- In dev mode, a small chip showing whether the op ran inline or on a worker.
- A **reload-from-this-step** button that replays history up to but not including this entry.
@@ -438,7 +438,7 @@ The QC history is the canonical save format. It's a JSON file you can keep, re-a
In the Edit history header, click the tray-arrow-down icon ("Save QC history"). The browser downloads a file named like:
```
-qc-script--.json
+qc-history--.json
```
The file contains:
@@ -450,19 +450,18 @@ A Snackbar confirms "QC history saved."
### Load
-Click the tray-arrow-up icon ("Load QC script") and pick a JSON file. The app will:
+Click the tray-arrow-up icon ("Load QC history") and pick a JSON file. The app will:
1. Fetch the QC history's authored window into your current QC datastream (the indices in selection-coupled ops reference the *windowed* dataset, so the window has to match).
2. Replay each operation in order.
3. Show a Snackbar with `Loaded N operations`, plus a warning if any ops failed.
-Per-op failures do not abort the replay. The app keeps going. If your script targets columns that don't exist in the new datastream (e.g., a qualifier code that isn't registered), that specific op fails and shows a red `!` badge on its history row, but the rest still run.
+Per-op failures do not abort the replay. The app keeps going. If your QC history targets columns that don't exist in the new datastream (e.g., a qualifier code that isn't registered), that specific op fails and shows a red `!` badge on its history row, but the rest still run.
### When to use it
- **Repeatable QC.** Apply the same QC routine to your raw dataset to generate the quality controlled dataset.
-- **Audit trail.** Save the QC History script before submitting, so you have a record of every transformation you applied.
-- **Iterate offline.** Edit the QC History script's JSON if you want to tweak a threshold without re-clicking through the panels.
+- **Audit trail.** Save the QC history before submitting, so you have a record of every transformation you applied.
## Submit (Save / Save & Close)
@@ -516,7 +515,7 @@ See the QC App [PERFORMANCE](https://github.com/hydroserver2/hydroserver-qc-app/
1. Load the new week of data on the same datastream.
2. Open the Edit history header → click the tray-arrow-up icon → pick last week's JSON.
-3. The script's authored window may differ from this week's; the app will fetch the script's window. To re-apply against the new window instead, save the new window first, edit the script's `window` field in a text editor, then re-import.
+3. The QC history's authored window may differ from this week's; the app will fetch the QC history's window. To re-apply against the new window instead, save the new window first, edit the QC history's `window` field in a text editor, then re-import.
4. Review the history. Click **Save**.
### "I picked the wrong workspace."
@@ -532,7 +531,7 @@ Click the grid icon in the nav rail → pick another. If you have unsaved edits,
| Pencil ("Edit") icon is greyed out | No QC datastream selected. | Plot at least one datastream. The first becomes the QC target. |
| Big edits freeze the page | `SharedArrayBuffer` not available; running inline. | Have your admin re-enable COOP/COEP headers, or accept the slower fallback. |
| Save fails with a backend error | Permissions / workspace issue / network. | The Snackbar shows the backend message verbatim. Share that with your admin. |
-| The history shows a red failed entry after Load | The script referenced something missing in this datastream. | The rest of the script still ran. Click the chevron on the row to see its arguments. |
+| The history shows a red failed entry after Load | The QC history referenced something missing in this datastream. | The rest of the QC history still ran. Click the chevron on the row to see its arguments. |
## See also
diff --git a/apps/docs/package-lock.json b/apps/docs/package-lock.json
index 3f36aa22f..7a4a07ccd 100644
--- a/apps/docs/package-lock.json
+++ b/apps/docs/package-lock.json
@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
+ "name": "docs",
"devDependencies": {
"vitepress": "^1.3.4"
}
@@ -848,9 +849,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.0.tgz",
- "integrity": "sha512-/IZQvg6ZR0tAkEi4tdXOraQoWeJy9gbQ/cx4I7k9dJaCk9qrXEcdouxRVz5kZXt5C2bQ9pILoAA+KB4C/d3pfw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz",
+ "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==",
"cpu": [
"arm"
],
@@ -862,9 +863,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.0.tgz",
- "integrity": "sha512-ETHi4bxrYnvOtXeM7d4V4kZWixib2jddFacJjsOjwbgYSRsyXYtZHC4ht134OsslPIcnkqT+TKV4eU8rNBKyyQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz",
+ "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==",
"cpu": [
"arm64"
],
@@ -876,9 +877,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.0.tgz",
- "integrity": "sha512-ZWgARzhSKE+gVUX7QWaECoRQsPwaD8ZR0Oxb3aUpzdErTvlEadfQpORPXkKSdKbFci9v8MJfkTtoEHnnW9Ulng==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz",
+ "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==",
"cpu": [
"arm64"
],
@@ -890,9 +891,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.0.tgz",
- "integrity": "sha512-h0ZAtOfHyio8Az6cwIGS+nHUfRMWBDO5jXB8PQCARVF6Na/G6XS2SFxDl8Oem+S5ZsHQgtsI7RT4JQnI1qrlaw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz",
+ "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==",
"cpu": [
"x64"
],
@@ -903,10 +904,38 @@
"darwin"
]
},
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz",
+ "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz",
+ "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.0.tgz",
- "integrity": "sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz",
+ "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==",
"cpu": [
"arm"
],
@@ -918,9 +947,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.0.tgz",
- "integrity": "sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz",
+ "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==",
"cpu": [
"arm"
],
@@ -932,9 +961,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.0.tgz",
- "integrity": "sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz",
+ "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==",
"cpu": [
"arm64"
],
@@ -946,9 +975,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.0.tgz",
- "integrity": "sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz",
+ "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==",
"cpu": [
"arm64"
],
@@ -959,10 +988,52 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.0.tgz",
- "integrity": "sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==",
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz",
+ "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz",
+ "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz",
+ "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz",
+ "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==",
"cpu": [
"ppc64"
],
@@ -974,9 +1045,23 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.0.tgz",
- "integrity": "sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz",
+ "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz",
+ "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==",
"cpu": [
"riscv64"
],
@@ -988,9 +1073,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.0.tgz",
- "integrity": "sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz",
+ "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==",
"cpu": [
"s390x"
],
@@ -1002,9 +1087,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.0.tgz",
- "integrity": "sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
"cpu": [
"x64"
],
@@ -1016,9 +1101,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.0.tgz",
- "integrity": "sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz",
+ "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==",
"cpu": [
"x64"
],
@@ -1029,10 +1114,38 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz",
+ "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz",
+ "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.0.tgz",
- "integrity": "sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz",
+ "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==",
"cpu": [
"arm64"
],
@@ -1044,9 +1157,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.0.tgz",
- "integrity": "sha512-+3qZ4rer7t/QsC5JwMpcvCVPRcJt1cJrYS/TMJZzXIJbxWFQEVhrIc26IhB+5Z9fT9umfVc+Es2mOZgl+7jdJQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz",
+ "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==",
"cpu": [
"ia32"
],
@@ -1057,10 +1170,24 @@
"win32"
]
},
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.0.tgz",
- "integrity": "sha512-YdicNOSJONVx/vuPkgPTyRoAPx3GbknBZRCOUkK84FJ/YTfs/F0vl/YsMscrB6Y177d+yDRcj+JWMPMCgshwrA==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz",
+ "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==",
"cpu": [
"x64"
],
@@ -1138,9 +1265,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
- "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
@@ -1894,9 +2021,9 @@
"dev": true
},
"node_modules/mdast-util-to-hast": {
- "version": "13.2.0",
- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
- "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2024,9 +2151,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.7",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
- "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -2063,16 +2190,16 @@
"license": "MIT"
},
"node_modules/picocolors": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
- "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -2090,8 +2217,8 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -2135,13 +2262,13 @@
"license": "MIT"
},
"node_modules/rollup": {
- "version": "4.22.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.0.tgz",
- "integrity": "sha512-W21MUIFPZ4+O2Je/EU+GP3iz7PH4pVPUXSbEZdatQnxo29+3rsUjgrJmzuAZU24z7yRAnFN6ukxeAhZh/c7hzg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz",
+ "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.5"
+ "@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -2151,22 +2278,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.22.0",
- "@rollup/rollup-android-arm64": "4.22.0",
- "@rollup/rollup-darwin-arm64": "4.22.0",
- "@rollup/rollup-darwin-x64": "4.22.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.22.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.22.0",
- "@rollup/rollup-linux-arm64-gnu": "4.22.0",
- "@rollup/rollup-linux-arm64-musl": "4.22.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.22.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.22.0",
- "@rollup/rollup-linux-s390x-gnu": "4.22.0",
- "@rollup/rollup-linux-x64-gnu": "4.22.0",
- "@rollup/rollup-linux-x64-musl": "4.22.0",
- "@rollup/rollup-win32-arm64-msvc": "4.22.0",
- "@rollup/rollup-win32-ia32-msvc": "4.22.0",
- "@rollup/rollup-win32-x64-msvc": "4.22.0",
+ "@rollup/rollup-android-arm-eabi": "4.61.1",
+ "@rollup/rollup-android-arm64": "4.61.1",
+ "@rollup/rollup-darwin-arm64": "4.61.1",
+ "@rollup/rollup-darwin-x64": "4.61.1",
+ "@rollup/rollup-freebsd-arm64": "4.61.1",
+ "@rollup/rollup-freebsd-x64": "4.61.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.61.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.61.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.61.1",
+ "@rollup/rollup-linux-arm64-musl": "4.61.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.61.1",
+ "@rollup/rollup-linux-loong64-musl": "4.61.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.61.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.61.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.61.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.61.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-musl": "4.61.1",
+ "@rollup/rollup-openbsd-x64": "4.61.1",
+ "@rollup/rollup-openharmony-arm64": "4.61.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.61.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.61.1",
+ "@rollup/rollup-win32-x64-gnu": "4.61.1",
+ "@rollup/rollup-win32-x64-msvc": "4.61.1",
"fsevents": "~2.3.2"
}
},
@@ -2384,9 +2520,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
- "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/apps/qc-app/.gitignore b/apps/qc-app/.gitignore
new file mode 100644
index 000000000..8b872873f
--- /dev/null
+++ b/apps/qc-app/.gitignore
@@ -0,0 +1,33 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+dev-dist
+*.local
+
+# Editor directories and files
+.vscode/*
+coverage/*
+!.vscode/extensions.json
+.idea
+.env
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Playwright
+test-results/
+playwright-report/
+playwright/.cache/
+/blob-report/
diff --git a/apps/qc-app/.prettierrc b/apps/qc-app/.prettierrc
new file mode 100644
index 000000000..66e7e941c
--- /dev/null
+++ b/apps/qc-app/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": false,
+ "singleQuote": true
+}
\ No newline at end of file
diff --git a/apps/qc-app/LICENSE b/apps/qc-app/LICENSE
new file mode 100644
index 000000000..e854b79ad
--- /dev/null
+++ b/apps/qc-app/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2023, Utah State University
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/apps/qc-app/README.md b/apps/qc-app/README.md
new file mode 100644
index 000000000..cb0042628
--- /dev/null
+++ b/apps/qc-app/README.md
@@ -0,0 +1,129 @@
+# HydroServer Quality Control App
+
+This web app facilitates QC/QA for time series observations stored in a HydroServer database instance. It features a multi-axis plot for visually overlaying datastreams and includes QC tools such as linear drift correction and interpolation.
+
+[Documentation](https://hydroserver2.github.io/hydroserver/)
+
+[Contributing](https://github.com/hydroserver2/hydroserver/blob/main/CONTRIBUTING.md)
+
+[Issue Tracker](https://github.com/hydroserver2/hydroserver/issues)
+
+[License](LICENSE)
+
+## What it does
+
+The app is the operator's view of HydroServer's QC pipeline:
+
+1. **Browse** — pick a workspace, filter datastreams by site / observed property / processing level, and plot up to five at once on a synchronized multi-axis chart.
+2. **QC one stream at a time** — the first plotted stream is the QC target. The other plotted traces are read-only context.
+3. **Filter / edit / add** — every operation (Value Threshold, Find Gaps, Persistence, Interpolate, Drift Correction, Fill Gaps, Add Points, etc.) commits a `HistoryItem` to a replayable edit history backed by [`@uwrl/qc-utils`](https://www.npmjs.com/package/@uwrl/qc-utils).
+4. **Save / load a QC History** — export the history as a JSON document, replay it on the same datastream a week later, or templatize across stations.
+5. **Submit** — push the quality-controlled observations back to HydroServer in `replace` mode.
+
+The heavy lifting (worker-parallelized typed-array kernels, calibration, history replay, save / load wire format) lives in `@uwrl/qc-utils`. This repo is the Vue / Vuetify / Pinia / Plotly UI plus the orchestration around it.
+
+## Quick start
+
+```bash
+npm install
+npm run dev # internal QC dev server at http://127.0.0.1:5173
+```
+
+During normal HydroServer development, open QC through the Data Management app at `http://127.0.0.1:1203/qc/`. Data Management owns authentication and proxies `/qc/` to this app's Vite server.
+
+**Use `127.0.0.1`, not `localhost`.** The HydroServer backend's CORS allowlist pins the IP literal, and the host name variant fails API requests silently.
+
+### Configuration
+
+`.env.local` (or `.env`) drives the runtime config. The keys the app reads:
+
+| Var | Required | Purpose |
+|----------------------------------|----------|-------------------------------------------------------------------------|
+| `VITE_APP_DISABLE_COOP` | no | Drop the `Cross-Origin-Opener-Policy` + `Cross-Origin-Embedder-Policy` headers. Use only when the backend you're hitting doesn't serve `Cross-Origin-Resource-Policy` (older HydroServer deployments). The `qc-utils` worker layer falls back to inline kernels when SAB isn't available, so the app still works — just slower on large edits. |
+| `VITE_APP_E2E_HOOKS` | no | Set to `1` to expose `window.__vbwTestHooks` for Playwright. CI sets this automatically. |
+
+Cross-origin isolation is on by default (`vite.config.ts` sends `Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy: require-corp`) so `SharedArrayBuffer` is available to the qc-utils worker pool.
+
+## Architecture at a glance
+
+```
+src/
+├─ pages/ Top-level routed views (Visualize, Edit, Submit, Auth).
+├─ components/
+│ ├─ Navigation/ NavigationRail, EditDrawer, SelectDrawer.
+│ ├─ FilterPoints/ One panel per filter op (ValueThreshold, GapFinder, …).
+│ ├─ EditData/ One panel per edit op (FillGaps, ChangeValues, …) + EditHistory.
+│ └─ VisualizeData/ DataVisualization, DataVisDatasetsTable, DatastreamInformationCard.
+├─ composables/
+│ ├─ useDataSelection.ts The bridge between Plotly's selectedpoints and the Pinia store.
+│ ├─ useFilterDispatch.ts Shared "open panel → run op → highlight result" sequence.
+│ └─ useQcHistory.ts Wiring around qc-utils' serializeHistory / applyHistory.
+├─ store/
+│ ├─ plotly.ts Plot ref, edit history, redraw, suppressedEchoSelection sentinel.
+│ ├─ dataVisualization.ts Selected datastream, plotted streams, selectedData.
+│ ├─ userInterface.ts Drawer state, operator + filter inputs, persisted prefs.
+│ └─ workspaces.ts Workspace list + selected workspace (persisted).
+├─ utils/plotting/
+│ ├─ events.ts plotly_click / plotly_relayout / mousemove handlers.
+│ ├─ relayout.ts Debounced viewport recomputation, tick alignment.
+│ ├─ selected.ts handleSelected — translates Plotly selection into a SELECTION dispatch.
+│ ├─ staging.ts Ghost-fill markers + drag-resizable stage shape.
+│ └─ plotly.ts Trace builders + low-level setSelectedPoints / clearSelection.
+└─ router/ vue-router 5 setup with workspace + auth guards.
+```
+
+The data flow for an edit is always: panel collects args → `useFilterDispatch` / `useQcHistory` calls `selectedSeries.data.dispatch(...)` (qc-utils) → qc-utils mutates typed arrays + appends a `HistoryItem` → `redraw()` pushes the new x / y into Plotly. The UI never touches the typed arrays directly.
+
+## Working with `@uwrl/qc-utils` locally
+
+The app consumes `@uwrl/qc-utils` from the root workspace. In dev mode,
+Vite aliases the package directly to `packages/qc-utils/src`, so you can
+edit QC engine source without rebuilding the package:
+
+```bash
+npm run dev
+```
+
+Build `qc-utils` first only for production-style app builds or when you
+need to verify the published package artifacts.
+
+## Testing
+
+### Unit tests (Vitest)
+
+```bash
+npm test # watch mode
+npm run coverage # one shot + v8 coverage
+```
+
+Specs live next to source under `src/**/__tests__/`. Plotly is mocked at the module boundary; qc-utils workers use the inline mocks in `packages/qc-utils/src/utils/plotting/__tests__/workerMocks.ts`.
+
+### End-to-end tests (Playwright)
+
+End-to-end specs cover the QC golden path: load a datastream, apply a filter, apply an edit, submit. Browser matrix: **chromium** and **firefox**. WebKit is intentionally excluded — `SharedArrayBuffer` + COOP / COEP behaviour differs in Safari and needs separate validation.
+
+One-time setup:
+
+```bash
+npx playwright install chromium firefox
+```
+
+Run modes:
+
+```bash
+npm run e2e # headless — Chromium + Firefox (CI mode, fast for local)
+npm run e2e:live # same-origin smoke via Data Management
+```
+
+For interactive debugging, append Playwright flags, e.g. `npm run e2e -- --ui` or
+`npm run e2e -- --headed`.
+
+The Playwright config in `playwright.config.ts` auto-starts the QC Vite dev server at `http://127.0.0.1:15173` and reuses an existing one outside CI. The dev server is what serves the COOP / COEP headers `SharedArrayBuffer` needs — running e2e against a static `file://` build won't work.
+
+The mocked specs intercept HydroServer routes via `page.route()` and serve fixture JSON, so most runs need no backend. The live golden-path spec expects both frontends to be running and enters QC through `http://127.0.0.1:1203/qc/`.
+
+E2E specs live in `e2e/` (separate from the Vitest unit specs under `src/**/__tests__/`).
+
+## Funding and Acknowledgements
+
+Funding for this project was provided by the National Oceanic & Atmospheric Administration (NOAA), awarded to the Cooperative Institute for Research to Operations in Hydrology (CIROH) through the NOAA Cooperative Agreement with The University of Alabama (NA22NWS4320003). Utah State University is a founding member of CIROH and receives funding under subaward from the University of Alabama. Additional funding and support have been provided by the State of Utah Division of Water Rights, the World Meorological Organization, and the Utah Water Research laboratory at Utah State University.
diff --git a/apps/qc-app/dev-dist/sw.js b/apps/qc-app/dev-dist/sw.js
new file mode 100644
index 000000000..38ad7770f
--- /dev/null
+++ b/apps/qc-app/dev-dist/sw.js
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// If the loader is already loaded, just stop.
+if (!self.define) {
+ let registry = {};
+
+ // Used for `eval` and `importScripts` where we can't get script URL by other means.
+ // In both cases, it's safe to use a global var because those functions are synchronous.
+ let nextDefineUri;
+
+ const singleRequire = (uri, parentUri) => {
+ uri = new URL(uri + ".js", parentUri).href;
+ return registry[uri] || (
+
+ new Promise(resolve => {
+ if ("document" in self) {
+ const script = document.createElement("script");
+ script.src = uri;
+ script.onload = resolve;
+ document.head.appendChild(script);
+ } else {
+ nextDefineUri = uri;
+ importScripts(uri);
+ resolve();
+ }
+ })
+
+ .then(() => {
+ let promise = registry[uri];
+ if (!promise) {
+ throw new Error(`Module ${uri} didn’t register its module`);
+ }
+ return promise;
+ })
+ );
+ };
+
+ self.define = (depsNames, factory) => {
+ const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
+ if (registry[uri]) {
+ // Module is already loading or loaded.
+ return;
+ }
+ let exports = {};
+ const require = depUri => singleRequire(depUri, uri);
+ const specialDeps = {
+ module: { uri },
+ exports,
+ require
+ };
+ registry[uri] = Promise.all(depsNames.map(
+ depName => specialDeps[depName] || require(depName)
+ )).then(deps => {
+ factory(...deps);
+ return exports;
+ });
+ };
+}
+define(['./workbox-9637eeee'], (function (workbox) { 'use strict';
+
+ self.addEventListener('message', event => {
+ if (event.data && event.data.type === 'SKIP_WAITING') {
+ self.skipWaiting();
+ }
+ });
+
+ /**
+ * The precacheAndRoute() method efficiently caches and responds to
+ * requests for URLs in the manifest.
+ * See https://goo.gl/S9QRab
+ */
+ workbox.precacheAndRoute([{
+ "url": "index.html",
+ "revision": "0.q6teuhr8o48"
+ }], {});
+ workbox.cleanupOutdatedCaches();
+ workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
+ allowlist: [/^\/$/]
+ }));
+
+}));
diff --git a/apps/qc-app/dev-dist/workbox-16108a29.js b/apps/qc-app/dev-dist/workbox-16108a29.js
new file mode 100644
index 000000000..1ce62d9aa
--- /dev/null
+++ b/apps/qc-app/dev-dist/workbox-16108a29.js
@@ -0,0 +1,3376 @@
+define(['exports'], (function (exports) { 'use strict';
+
+ // @ts-ignore
+ try {
+ self['workbox:core:7.0.0'] && _();
+ } catch (e) {}
+
+ /*
+ Copyright 2019 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const logger = (() => {
+ // Don't overwrite this value if it's already set.
+ // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923
+ if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) {
+ self.__WB_DISABLE_DEV_LOGS = false;
+ }
+ let inGroup = false;
+ const methodToColorMap = {
+ debug: `#7f8c8d`,
+ log: `#2ecc71`,
+ warn: `#f39c12`,
+ error: `#c0392b`,
+ groupCollapsed: `#3498db`,
+ groupEnd: null // No colored prefix on groupEnd
+ };
+
+ const print = function (method, args) {
+ if (self.__WB_DISABLE_DEV_LOGS) {
+ return;
+ }
+ if (method === 'groupCollapsed') {
+ // Safari doesn't print all console.groupCollapsed() arguments:
+ // https://bugs.webkit.org/show_bug.cgi?id=182754
+ if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
+ console[method](...args);
+ return;
+ }
+ }
+ const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`];
+ // When in a group, the workbox prefix is not displayed.
+ const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')];
+ console[method](...logPrefix, ...args);
+ if (method === 'groupCollapsed') {
+ inGroup = true;
+ }
+ if (method === 'groupEnd') {
+ inGroup = false;
+ }
+ };
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const api = {};
+ const loggerMethods = Object.keys(methodToColorMap);
+ for (const key of loggerMethods) {
+ const method = key;
+ api[method] = (...args) => {
+ print(method, args);
+ };
+ }
+ return api;
+ })();
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const messages = {
+ 'invalid-value': ({
+ paramName,
+ validValueDescription,
+ value
+ }) => {
+ if (!paramName || !validValueDescription) {
+ throw new Error(`Unexpected input to 'invalid-value' error.`);
+ }
+ return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`;
+ },
+ 'not-an-array': ({
+ moduleName,
+ className,
+ funcName,
+ paramName
+ }) => {
+ if (!moduleName || !className || !funcName || !paramName) {
+ throw new Error(`Unexpected input to 'not-an-array' error.`);
+ }
+ return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`;
+ },
+ 'incorrect-type': ({
+ expectedType,
+ paramName,
+ moduleName,
+ className,
+ funcName
+ }) => {
+ if (!expectedType || !paramName || !moduleName || !funcName) {
+ throw new Error(`Unexpected input to 'incorrect-type' error.`);
+ }
+ const classNameStr = className ? `${className}.` : '';
+ return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}` + `${funcName}()' must be of type ${expectedType}.`;
+ },
+ 'incorrect-class': ({
+ expectedClassName,
+ paramName,
+ moduleName,
+ className,
+ funcName,
+ isReturnValueProblem
+ }) => {
+ if (!expectedClassName || !moduleName || !funcName) {
+ throw new Error(`Unexpected input to 'incorrect-class' error.`);
+ }
+ const classNameStr = className ? `${className}.` : '';
+ if (isReturnValueProblem) {
+ return `The return value from ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`;
+ }
+ return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`;
+ },
+ 'missing-a-method': ({
+ expectedMethod,
+ paramName,
+ moduleName,
+ className,
+ funcName
+ }) => {
+ if (!expectedMethod || !paramName || !moduleName || !className || !funcName) {
+ throw new Error(`Unexpected input to 'missing-a-method' error.`);
+ }
+ return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`;
+ },
+ 'add-to-cache-list-unexpected-type': ({
+ entry
+ }) => {
+ return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`;
+ },
+ 'add-to-cache-list-conflicting-entries': ({
+ firstEntry,
+ secondEntry
+ }) => {
+ if (!firstEntry || !secondEntry) {
+ throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`);
+ }
+ return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry} but different revision details. Workbox is ` + `unable to cache and version the asset correctly. Please remove one ` + `of the entries.`;
+ },
+ 'plugin-error-request-will-fetch': ({
+ thrownErrorMessage
+ }) => {
+ if (!thrownErrorMessage) {
+ throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`);
+ }
+ return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownErrorMessage}'.`;
+ },
+ 'invalid-cache-name': ({
+ cacheNameId,
+ value
+ }) => {
+ if (!cacheNameId) {
+ throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`);
+ }
+ return `You must provide a name containing at least one character for ` + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`;
+ },
+ 'unregister-route-but-not-found-with-method': ({
+ method
+ }) => {
+ if (!method) {
+ throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`);
+ }
+ return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`;
+ },
+ 'unregister-route-route-not-registered': () => {
+ return `The route you're trying to unregister was not previously ` + `registered.`;
+ },
+ 'queue-replay-failed': ({
+ name
+ }) => {
+ return `Replaying the background sync queue '${name}' failed.`;
+ },
+ 'duplicate-queue-name': ({
+ name
+ }) => {
+ return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`;
+ },
+ 'expired-test-without-max-age': ({
+ methodName,
+ paramName
+ }) => {
+ return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`;
+ },
+ 'unsupported-route-type': ({
+ moduleName,
+ className,
+ funcName,
+ paramName
+ }) => {
+ return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`;
+ },
+ 'not-array-of-class': ({
+ value,
+ expectedClass,
+ moduleName,
+ className,
+ funcName,
+ paramName
+ }) => {
+ return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`;
+ },
+ 'max-entries-or-age-required': ({
+ moduleName,
+ className,
+ funcName
+ }) => {
+ return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`;
+ },
+ 'statuses-or-headers-required': ({
+ moduleName,
+ className,
+ funcName
+ }) => {
+ return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`;
+ },
+ 'invalid-string': ({
+ moduleName,
+ funcName,
+ paramName
+ }) => {
+ if (!paramName || !moduleName || !funcName) {
+ throw new Error(`Unexpected input to 'invalid-string' error.`);
+ }
+ return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`;
+ },
+ 'channel-name-required': () => {
+ return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`;
+ },
+ 'invalid-responses-are-same-args': () => {
+ return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`;
+ },
+ 'expire-custom-caches-only': () => {
+ return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`;
+ },
+ 'unit-must-be-bytes': ({
+ normalizedRangeHeader
+ }) => {
+ if (!normalizedRangeHeader) {
+ throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`);
+ }
+ return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`;
+ },
+ 'single-range-only': ({
+ normalizedRangeHeader
+ }) => {
+ if (!normalizedRangeHeader) {
+ throw new Error(`Unexpected input to 'single-range-only' error.`);
+ }
+ return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`;
+ },
+ 'invalid-range-values': ({
+ normalizedRangeHeader
+ }) => {
+ if (!normalizedRangeHeader) {
+ throw new Error(`Unexpected input to 'invalid-range-values' error.`);
+ }
+ return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`;
+ },
+ 'no-range-header': () => {
+ return `No Range header was found in the Request provided.`;
+ },
+ 'range-not-satisfiable': ({
+ size,
+ start,
+ end
+ }) => {
+ return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`;
+ },
+ 'attempt-to-cache-non-get-request': ({
+ url,
+ method
+ }) => {
+ return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`;
+ },
+ 'cache-put-with-no-response': ({
+ url
+ }) => {
+ return `There was an attempt to cache '${url}' but the response was not ` + `defined.`;
+ },
+ 'no-response': ({
+ url,
+ error
+ }) => {
+ let message = `The strategy could not generate a response for '${url}'.`;
+ if (error) {
+ message += ` The underlying error is ${error}.`;
+ }
+ return message;
+ },
+ 'bad-precaching-response': ({
+ url,
+ status
+ }) => {
+ return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`);
+ },
+ 'non-precached-url': ({
+ url
+ }) => {
+ return `createHandlerBoundToURL('${url}') was called, but that URL is ` + `not precached. Please pass in a URL that is precached instead.`;
+ },
+ 'add-to-cache-list-conflicting-integrities': ({
+ url
+ }) => {
+ return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${url} with different integrity values. Please remove one of them.`;
+ },
+ 'missing-precache-entry': ({
+ cacheName,
+ url
+ }) => {
+ return `Unable to find a precached response in ${cacheName} for ${url}.`;
+ },
+ 'cross-origin-copy-response': ({
+ origin
+ }) => {
+ return `workbox-core.copyResponse() can only be used with same-origin ` + `responses. It was passed a response with origin ${origin}.`;
+ },
+ 'opaque-streams-source': ({
+ type
+ }) => {
+ const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`;
+ if (type === 'opaqueredirect') {
+ return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`;
+ }
+ return `${message} Please ensure your sources are CORS-enabled.`;
+ }
+ };
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const generatorFunction = (code, details = {}) => {
+ const message = messages[code];
+ if (!message) {
+ throw new Error(`Unable to find message for code '${code}'.`);
+ }
+ return message(details);
+ };
+ const messageGenerator = generatorFunction;
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Workbox errors should be thrown with this class.
+ * This allows use to ensure the type easily in tests,
+ * helps developers identify errors from workbox
+ * easily and allows use to optimise error
+ * messages correctly.
+ *
+ * @private
+ */
+ class WorkboxError extends Error {
+ /**
+ *
+ * @param {string} errorCode The error code that
+ * identifies this particular error.
+ * @param {Object=} details Any relevant arguments
+ * that will help developers identify issues should
+ * be added as a key on the context object.
+ */
+ constructor(errorCode, details) {
+ const message = messageGenerator(errorCode, details);
+ super(message);
+ this.name = errorCode;
+ this.details = details;
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /*
+ * This method throws if the supplied value is not an array.
+ * The destructed values are required to produce a meaningful error for users.
+ * The destructed and restructured object is so it's clear what is
+ * needed.
+ */
+ const isArray = (value, details) => {
+ if (!Array.isArray(value)) {
+ throw new WorkboxError('not-an-array', details);
+ }
+ };
+ const hasMethod = (object, expectedMethod, details) => {
+ const type = typeof object[expectedMethod];
+ if (type !== 'function') {
+ details['expectedMethod'] = expectedMethod;
+ throw new WorkboxError('missing-a-method', details);
+ }
+ };
+ const isType = (object, expectedType, details) => {
+ if (typeof object !== expectedType) {
+ details['expectedType'] = expectedType;
+ throw new WorkboxError('incorrect-type', details);
+ }
+ };
+ const isInstance = (object,
+ // Need the general type to do the check later.
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ expectedClass, details) => {
+ if (!(object instanceof expectedClass)) {
+ details['expectedClassName'] = expectedClass.name;
+ throw new WorkboxError('incorrect-class', details);
+ }
+ };
+ const isOneOf = (value, validValues, details) => {
+ if (!validValues.includes(value)) {
+ details['validValueDescription'] = `Valid values are ${JSON.stringify(validValues)}.`;
+ throw new WorkboxError('invalid-value', details);
+ }
+ };
+ const isArrayOfClass = (value,
+ // Need general type to do check later.
+ expectedClass,
+ // eslint-disable-line
+ details) => {
+ const error = new WorkboxError('not-array-of-class', details);
+ if (!Array.isArray(value)) {
+ throw error;
+ }
+ for (const item of value) {
+ if (!(item instanceof expectedClass)) {
+ throw error;
+ }
+ }
+ };
+ const finalAssertExports = {
+ hasMethod,
+ isArray,
+ isInstance,
+ isOneOf,
+ isType,
+ isArrayOfClass
+ };
+
+ // @ts-ignore
+ try {
+ self['workbox:routing:7.0.0'] && _();
+ } catch (e) {}
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * The default HTTP method, 'GET', used when there's no specific method
+ * configured for a route.
+ *
+ * @type {string}
+ *
+ * @private
+ */
+ const defaultMethod = 'GET';
+ /**
+ * The list of valid HTTP methods associated with requests that could be routed.
+ *
+ * @type {Array}
+ *
+ * @private
+ */
+ const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'];
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * @param {function()|Object} handler Either a function, or an object with a
+ * 'handle' method.
+ * @return {Object} An object with a handle method.
+ *
+ * @private
+ */
+ const normalizeHandler = handler => {
+ if (handler && typeof handler === 'object') {
+ {
+ finalAssertExports.hasMethod(handler, 'handle', {
+ moduleName: 'workbox-routing',
+ className: 'Route',
+ funcName: 'constructor',
+ paramName: 'handler'
+ });
+ }
+ return handler;
+ } else {
+ {
+ finalAssertExports.isType(handler, 'function', {
+ moduleName: 'workbox-routing',
+ className: 'Route',
+ funcName: 'constructor',
+ paramName: 'handler'
+ });
+ }
+ return {
+ handle: handler
+ };
+ }
+ };
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A `Route` consists of a pair of callback functions, "match" and "handler".
+ * The "match" callback determine if a route should be used to "handle" a
+ * request by returning a non-falsy value if it can. The "handler" callback
+ * is called when there is a match and should return a Promise that resolves
+ * to a `Response`.
+ *
+ * @memberof workbox-routing
+ */
+ class Route {
+ /**
+ * Constructor for Route class.
+ *
+ * @param {workbox-routing~matchCallback} match
+ * A callback function that determines whether the route matches a given
+ * `fetch` event by returning a non-falsy value.
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resolving to a Response.
+ * @param {string} [method='GET'] The HTTP method to match the Route
+ * against.
+ */
+ constructor(match, handler, method = defaultMethod) {
+ {
+ finalAssertExports.isType(match, 'function', {
+ moduleName: 'workbox-routing',
+ className: 'Route',
+ funcName: 'constructor',
+ paramName: 'match'
+ });
+ if (method) {
+ finalAssertExports.isOneOf(method, validMethods, {
+ paramName: 'method'
+ });
+ }
+ }
+ // These values are referenced directly by Router so cannot be
+ // altered by minificaton.
+ this.handler = normalizeHandler(handler);
+ this.match = match;
+ this.method = method;
+ }
+ /**
+ *
+ * @param {workbox-routing-handlerCallback} handler A callback
+ * function that returns a Promise resolving to a Response
+ */
+ setCatchHandler(handler) {
+ this.catchHandler = normalizeHandler(handler);
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * RegExpRoute makes it easy to create a regular expression based
+ * {@link workbox-routing.Route}.
+ *
+ * For same-origin requests the RegExp only needs to match part of the URL. For
+ * requests against third-party servers, you must define a RegExp that matches
+ * the start of the URL.
+ *
+ * @memberof workbox-routing
+ * @extends workbox-routing.Route
+ */
+ class RegExpRoute extends Route {
+ /**
+ * If the regular expression contains
+ * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references},
+ * the captured values will be passed to the
+ * {@link workbox-routing~handlerCallback} `params`
+ * argument.
+ *
+ * @param {RegExp} regExp The regular expression to match against URLs.
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resulting in a Response.
+ * @param {string} [method='GET'] The HTTP method to match the Route
+ * against.
+ */
+ constructor(regExp, handler, method) {
+ {
+ finalAssertExports.isInstance(regExp, RegExp, {
+ moduleName: 'workbox-routing',
+ className: 'RegExpRoute',
+ funcName: 'constructor',
+ paramName: 'pattern'
+ });
+ }
+ const match = ({
+ url
+ }) => {
+ const result = regExp.exec(url.href);
+ // Return immediately if there's no match.
+ if (!result) {
+ return;
+ }
+ // Require that the match start at the first character in the URL string
+ // if it's a cross-origin request.
+ // See https://github.com/GoogleChrome/workbox/issues/281 for the context
+ // behind this behavior.
+ if (url.origin !== location.origin && result.index !== 0) {
+ {
+ logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`);
+ }
+ return;
+ }
+ // If the route matches, but there aren't any capture groups defined, then
+ // this will return [], which is truthy and therefore sufficient to
+ // indicate a match.
+ // If there are capture groups, then it will return their values.
+ return result.slice(1);
+ };
+ super(match, handler, method);
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const getFriendlyURL = url => {
+ const urlObj = new URL(String(url), location.href);
+ // See https://github.com/GoogleChrome/workbox/issues/2323
+ // We want to include everything, except for the origin if it's same-origin.
+ return urlObj.href.replace(new RegExp(`^${location.origin}`), '');
+ };
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * The Router can be used to process a `FetchEvent` using one or more
+ * {@link workbox-routing.Route}, responding with a `Response` if
+ * a matching route exists.
+ *
+ * If no route matches a given a request, the Router will use a "default"
+ * handler if one is defined.
+ *
+ * Should the matching Route throw an error, the Router will use a "catch"
+ * handler if one is defined to gracefully deal with issues and respond with a
+ * Request.
+ *
+ * If a request matches multiple routes, the **earliest** registered route will
+ * be used to respond to the request.
+ *
+ * @memberof workbox-routing
+ */
+ class Router {
+ /**
+ * Initializes a new Router.
+ */
+ constructor() {
+ this._routes = new Map();
+ this._defaultHandlerMap = new Map();
+ }
+ /**
+ * @return {Map>} routes A `Map` of HTTP
+ * method name ('GET', etc.) to an array of all the corresponding `Route`
+ * instances that are registered.
+ */
+ get routes() {
+ return this._routes;
+ }
+ /**
+ * Adds a fetch event listener to respond to events when a route matches
+ * the event's request.
+ */
+ addFetchListener() {
+ // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
+ self.addEventListener('fetch', event => {
+ const {
+ request
+ } = event;
+ const responsePromise = this.handleRequest({
+ request,
+ event
+ });
+ if (responsePromise) {
+ event.respondWith(responsePromise);
+ }
+ });
+ }
+ /**
+ * Adds a message event listener for URLs to cache from the window.
+ * This is useful to cache resources loaded on the page prior to when the
+ * service worker started controlling it.
+ *
+ * The format of the message data sent from the window should be as follows.
+ * Where the `urlsToCache` array may consist of URL strings or an array of
+ * URL string + `requestInit` object (the same as you'd pass to `fetch()`).
+ *
+ * ```
+ * {
+ * type: 'CACHE_URLS',
+ * payload: {
+ * urlsToCache: [
+ * './script1.js',
+ * './script2.js',
+ * ['./script3.js', {mode: 'no-cors'}],
+ * ],
+ * },
+ * }
+ * ```
+ */
+ addCacheListener() {
+ // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705
+ self.addEventListener('message', event => {
+ // event.data is type 'any'
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ if (event.data && event.data.type === 'CACHE_URLS') {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const {
+ payload
+ } = event.data;
+ {
+ logger.debug(`Caching URLs from the window`, payload.urlsToCache);
+ }
+ const requestPromises = Promise.all(payload.urlsToCache.map(entry => {
+ if (typeof entry === 'string') {
+ entry = [entry];
+ }
+ const request = new Request(...entry);
+ return this.handleRequest({
+ request,
+ event
+ });
+ // TODO(philipwalton): TypeScript errors without this typecast for
+ // some reason (probably a bug). The real type here should work but
+ // doesn't: `Array | undefined>`.
+ })); // TypeScript
+ event.waitUntil(requestPromises);
+ // If a MessageChannel was used, reply to the message on success.
+ if (event.ports && event.ports[0]) {
+ void requestPromises.then(() => event.ports[0].postMessage(true));
+ }
+ }
+ });
+ }
+ /**
+ * Apply the routing rules to a FetchEvent object to get a Response from an
+ * appropriate Route's handler.
+ *
+ * @param {Object} options
+ * @param {Request} options.request The request to handle.
+ * @param {ExtendableEvent} options.event The event that triggered the
+ * request.
+ * @return {Promise|undefined} A promise is returned if a
+ * registered route can handle the request. If there is no matching
+ * route and there's no `defaultHandler`, `undefined` is returned.
+ */
+ handleRequest({
+ request,
+ event
+ }) {
+ {
+ finalAssertExports.isInstance(request, Request, {
+ moduleName: 'workbox-routing',
+ className: 'Router',
+ funcName: 'handleRequest',
+ paramName: 'options.request'
+ });
+ }
+ const url = new URL(request.url, location.href);
+ if (!url.protocol.startsWith('http')) {
+ {
+ logger.debug(`Workbox Router only supports URLs that start with 'http'.`);
+ }
+ return;
+ }
+ const sameOrigin = url.origin === location.origin;
+ const {
+ params,
+ route
+ } = this.findMatchingRoute({
+ event,
+ request,
+ sameOrigin,
+ url
+ });
+ let handler = route && route.handler;
+ const debugMessages = [];
+ {
+ if (handler) {
+ debugMessages.push([`Found a route to handle this request:`, route]);
+ if (params) {
+ debugMessages.push([`Passing the following params to the route's handler:`, params]);
+ }
+ }
+ }
+ // If we don't have a handler because there was no matching route, then
+ // fall back to defaultHandler if that's defined.
+ const method = request.method;
+ if (!handler && this._defaultHandlerMap.has(method)) {
+ {
+ debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`);
+ }
+ handler = this._defaultHandlerMap.get(method);
+ }
+ if (!handler) {
+ {
+ // No handler so Workbox will do nothing. If logs is set of debug
+ // i.e. verbose, we should print out this information.
+ logger.debug(`No route found for: ${getFriendlyURL(url)}`);
+ }
+ return;
+ }
+ {
+ // We have a handler, meaning Workbox is going to handle the route.
+ // print the routing details to the console.
+ logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
+ debugMessages.forEach(msg => {
+ if (Array.isArray(msg)) {
+ logger.log(...msg);
+ } else {
+ logger.log(msg);
+ }
+ });
+ logger.groupEnd();
+ }
+ // Wrap in try and catch in case the handle method throws a synchronous
+ // error. It should still callback to the catch handler.
+ let responsePromise;
+ try {
+ responsePromise = handler.handle({
+ url,
+ request,
+ event,
+ params
+ });
+ } catch (err) {
+ responsePromise = Promise.reject(err);
+ }
+ // Get route's catch handler, if it exists
+ const catchHandler = route && route.catchHandler;
+ if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) {
+ responsePromise = responsePromise.catch(async err => {
+ // If there's a route catch handler, process that first
+ if (catchHandler) {
+ {
+ // Still include URL here as it will be async from the console group
+ // and may not make sense without the URL
+ logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`);
+ logger.error(`Error thrown by:`, route);
+ logger.error(err);
+ logger.groupEnd();
+ }
+ try {
+ return await catchHandler.handle({
+ url,
+ request,
+ event,
+ params
+ });
+ } catch (catchErr) {
+ if (catchErr instanceof Error) {
+ err = catchErr;
+ }
+ }
+ }
+ if (this._catchHandler) {
+ {
+ // Still include URL here as it will be async from the console group
+ // and may not make sense without the URL
+ logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`);
+ logger.error(`Error thrown by:`, route);
+ logger.error(err);
+ logger.groupEnd();
+ }
+ return this._catchHandler.handle({
+ url,
+ request,
+ event
+ });
+ }
+ throw err;
+ });
+ }
+ return responsePromise;
+ }
+ /**
+ * Checks a request and URL (and optionally an event) against the list of
+ * registered routes, and if there's a match, returns the corresponding
+ * route along with any params generated by the match.
+ *
+ * @param {Object} options
+ * @param {URL} options.url
+ * @param {boolean} options.sameOrigin The result of comparing `url.origin`
+ * against the current origin.
+ * @param {Request} options.request The request to match.
+ * @param {Event} options.event The corresponding event.
+ * @return {Object} An object with `route` and `params` properties.
+ * They are populated if a matching route was found or `undefined`
+ * otherwise.
+ */
+ findMatchingRoute({
+ url,
+ sameOrigin,
+ request,
+ event
+ }) {
+ const routes = this._routes.get(request.method) || [];
+ for (const route of routes) {
+ let params;
+ // route.match returns type any, not possible to change right now.
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const matchResult = route.match({
+ url,
+ sameOrigin,
+ request,
+ event
+ });
+ if (matchResult) {
+ {
+ // Warn developers that using an async matchCallback is almost always
+ // not the right thing to do.
+ if (matchResult instanceof Promise) {
+ logger.warn(`While routing ${getFriendlyURL(url)}, an async ` + `matchCallback function was used. Please convert the ` + `following route to use a synchronous matchCallback function:`, route);
+ }
+ }
+ // See https://github.com/GoogleChrome/workbox/issues/2079
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ params = matchResult;
+ if (Array.isArray(params) && params.length === 0) {
+ // Instead of passing an empty array in as params, use undefined.
+ params = undefined;
+ } else if (matchResult.constructor === Object &&
+ // eslint-disable-line
+ Object.keys(matchResult).length === 0) {
+ // Instead of passing an empty object in as params, use undefined.
+ params = undefined;
+ } else if (typeof matchResult === 'boolean') {
+ // For the boolean value true (rather than just something truth-y),
+ // don't set params.
+ // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353
+ params = undefined;
+ }
+ // Return early if have a match.
+ return {
+ route,
+ params
+ };
+ }
+ }
+ // If no match was found above, return and empty object.
+ return {};
+ }
+ /**
+ * Define a default `handler` that's called when no routes explicitly
+ * match the incoming request.
+ *
+ * Each HTTP method ('GET', 'POST', etc.) gets its own default handler.
+ *
+ * Without a default handler, unmatched requests will go against the
+ * network as if there were no service worker present.
+ *
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resulting in a Response.
+ * @param {string} [method='GET'] The HTTP method to associate with this
+ * default handler. Each method has its own default.
+ */
+ setDefaultHandler(handler, method = defaultMethod) {
+ this._defaultHandlerMap.set(method, normalizeHandler(handler));
+ }
+ /**
+ * If a Route throws an error while handling a request, this `handler`
+ * will be called and given a chance to provide a response.
+ *
+ * @param {workbox-routing~handlerCallback} handler A callback
+ * function that returns a Promise resulting in a Response.
+ */
+ setCatchHandler(handler) {
+ this._catchHandler = normalizeHandler(handler);
+ }
+ /**
+ * Registers a route with the router.
+ *
+ * @param {workbox-routing.Route} route The route to register.
+ */
+ registerRoute(route) {
+ {
+ finalAssertExports.isType(route, 'object', {
+ moduleName: 'workbox-routing',
+ className: 'Router',
+ funcName: 'registerRoute',
+ paramName: 'route'
+ });
+ finalAssertExports.hasMethod(route, 'match', {
+ moduleName: 'workbox-routing',
+ className: 'Router',
+ funcName: 'registerRoute',
+ paramName: 'route'
+ });
+ finalAssertExports.isType(route.handler, 'object', {
+ moduleName: 'workbox-routing',
+ className: 'Router',
+ funcName: 'registerRoute',
+ paramName: 'route'
+ });
+ finalAssertExports.hasMethod(route.handler, 'handle', {
+ moduleName: 'workbox-routing',
+ className: 'Router',
+ funcName: 'registerRoute',
+ paramName: 'route.handler'
+ });
+ finalAssertExports.isType(route.method, 'string', {
+ moduleName: 'workbox-routing',
+ className: 'Router',
+ funcName: 'registerRoute',
+ paramName: 'route.method'
+ });
+ }
+ if (!this._routes.has(route.method)) {
+ this._routes.set(route.method, []);
+ }
+ // Give precedence to all of the earlier routes by adding this additional
+ // route to the end of the array.
+ this._routes.get(route.method).push(route);
+ }
+ /**
+ * Unregisters a route with the router.
+ *
+ * @param {workbox-routing.Route} route The route to unregister.
+ */
+ unregisterRoute(route) {
+ if (!this._routes.has(route.method)) {
+ throw new WorkboxError('unregister-route-but-not-found-with-method', {
+ method: route.method
+ });
+ }
+ const routeIndex = this._routes.get(route.method).indexOf(route);
+ if (routeIndex > -1) {
+ this._routes.get(route.method).splice(routeIndex, 1);
+ } else {
+ throw new WorkboxError('unregister-route-route-not-registered');
+ }
+ }
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ let defaultRouter;
+ /**
+ * Creates a new, singleton Router instance if one does not exist. If one
+ * does already exist, that instance is returned.
+ *
+ * @private
+ * @return {Router}
+ */
+ const getOrCreateDefaultRouter = () => {
+ if (!defaultRouter) {
+ defaultRouter = new Router();
+ // The helpers that use the default Router assume these listeners exist.
+ defaultRouter.addFetchListener();
+ defaultRouter.addCacheListener();
+ }
+ return defaultRouter;
+ };
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Easily register a RegExp, string, or function with a caching
+ * strategy to a singleton Router instance.
+ *
+ * This method will generate a Route for you if needed and
+ * call {@link workbox-routing.Router#registerRoute}.
+ *
+ * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture
+ * If the capture param is a `Route`, all other arguments will be ignored.
+ * @param {workbox-routing~handlerCallback} [handler] A callback
+ * function that returns a Promise resulting in a Response. This parameter
+ * is required if `capture` is not a `Route` object.
+ * @param {string} [method='GET'] The HTTP method to match the Route
+ * against.
+ * @return {workbox-routing.Route} The generated `Route`.
+ *
+ * @memberof workbox-routing
+ */
+ function registerRoute(capture, handler, method) {
+ let route;
+ if (typeof capture === 'string') {
+ const captureUrl = new URL(capture, location.href);
+ {
+ if (!(capture.startsWith('/') || capture.startsWith('http'))) {
+ throw new WorkboxError('invalid-string', {
+ moduleName: 'workbox-routing',
+ funcName: 'registerRoute',
+ paramName: 'capture'
+ });
+ }
+ // We want to check if Express-style wildcards are in the pathname only.
+ // TODO: Remove this log message in v4.
+ const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture;
+ // See https://github.com/pillarjs/path-to-regexp#parameters
+ const wildcards = '[*:?+]';
+ if (new RegExp(`${wildcards}`).exec(valueToCheck)) {
+ logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`);
+ }
+ }
+ const matchCallback = ({
+ url
+ }) => {
+ {
+ if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) {
+ logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url.toString()}. This route will only handle cross-origin requests ` + `if they match the entire URL.`);
+ }
+ }
+ return url.href === captureUrl.href;
+ };
+ // If `capture` is a string then `handler` and `method` must be present.
+ route = new Route(matchCallback, handler, method);
+ } else if (capture instanceof RegExp) {
+ // If `capture` is a `RegExp` then `handler` and `method` must be present.
+ route = new RegExpRoute(capture, handler, method);
+ } else if (typeof capture === 'function') {
+ // If `capture` is a function then `handler` and `method` must be present.
+ route = new Route(capture, handler, method);
+ } else if (capture instanceof Route) {
+ route = capture;
+ } else {
+ throw new WorkboxError('unsupported-route-type', {
+ moduleName: 'workbox-routing',
+ funcName: 'registerRoute',
+ paramName: 'capture'
+ });
+ }
+ const defaultRouter = getOrCreateDefaultRouter();
+ defaultRouter.registerRoute(route);
+ return route;
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ const _cacheNameDetails = {
+ googleAnalytics: 'googleAnalytics',
+ precache: 'precache-v2',
+ prefix: 'workbox',
+ runtime: 'runtime',
+ suffix: typeof registration !== 'undefined' ? registration.scope : ''
+ };
+ const _createCacheName = cacheName => {
+ return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value && value.length > 0).join('-');
+ };
+ const eachCacheNameDetail = fn => {
+ for (const key of Object.keys(_cacheNameDetails)) {
+ fn(key);
+ }
+ };
+ const cacheNames = {
+ updateDetails: details => {
+ eachCacheNameDetail(key => {
+ if (typeof details[key] === 'string') {
+ _cacheNameDetails[key] = details[key];
+ }
+ });
+ },
+ getGoogleAnalyticsName: userCacheName => {
+ return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics);
+ },
+ getPrecacheName: userCacheName => {
+ return userCacheName || _createCacheName(_cacheNameDetails.precache);
+ },
+ getPrefix: () => {
+ return _cacheNameDetails.prefix;
+ },
+ getRuntimeName: userCacheName => {
+ return userCacheName || _createCacheName(_cacheNameDetails.runtime);
+ },
+ getSuffix: () => {
+ return _cacheNameDetails.suffix;
+ }
+ };
+
+ /*
+ Copyright 2020 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A utility method that makes it easier to use `event.waitUntil` with
+ * async functions and return the result.
+ *
+ * @param {ExtendableEvent} event
+ * @param {Function} asyncFn
+ * @return {Function}
+ * @private
+ */
+ function waitUntil(event, asyncFn) {
+ const returnPromise = asyncFn();
+ event.waitUntil(returnPromise);
+ return returnPromise;
+ }
+
+ // @ts-ignore
+ try {
+ self['workbox:precaching:7.0.0'] && _();
+ } catch (e) {}
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ // Name of the search parameter used to store revision info.
+ const REVISION_SEARCH_PARAM = '__WB_REVISION__';
+ /**
+ * Converts a manifest entry into a versioned URL suitable for precaching.
+ *
+ * @param {Object|string} entry
+ * @return {string} A URL with versioning info.
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function createCacheKey(entry) {
+ if (!entry) {
+ throw new WorkboxError('add-to-cache-list-unexpected-type', {
+ entry
+ });
+ }
+ // If a precache manifest entry is a string, it's assumed to be a versioned
+ // URL, like '/app.abcd1234.js'. Return as-is.
+ if (typeof entry === 'string') {
+ const urlObject = new URL(entry, location.href);
+ return {
+ cacheKey: urlObject.href,
+ url: urlObject.href
+ };
+ }
+ const {
+ revision,
+ url
+ } = entry;
+ if (!url) {
+ throw new WorkboxError('add-to-cache-list-unexpected-type', {
+ entry
+ });
+ }
+ // If there's just a URL and no revision, then it's also assumed to be a
+ // versioned URL.
+ if (!revision) {
+ const urlObject = new URL(url, location.href);
+ return {
+ cacheKey: urlObject.href,
+ url: urlObject.href
+ };
+ }
+ // Otherwise, construct a properly versioned URL using the custom Workbox
+ // search parameter along with the revision info.
+ const cacheKeyURL = new URL(url, location.href);
+ const originalURL = new URL(url, location.href);
+ cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision);
+ return {
+ cacheKey: cacheKeyURL.href,
+ url: originalURL.href
+ };
+ }
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A plugin, designed to be used with PrecacheController, to determine the
+ * of assets that were updated (or not updated) during the install event.
+ *
+ * @private
+ */
+ class PrecacheInstallReportPlugin {
+ constructor() {
+ this.updatedURLs = [];
+ this.notUpdatedURLs = [];
+ this.handlerWillStart = async ({
+ request,
+ state
+ }) => {
+ // TODO: `state` should never be undefined...
+ if (state) {
+ state.originalRequest = request;
+ }
+ };
+ this.cachedResponseWillBeUsed = async ({
+ event,
+ state,
+ cachedResponse
+ }) => {
+ if (event.type === 'install') {
+ if (state && state.originalRequest && state.originalRequest instanceof Request) {
+ // TODO: `state` should never be undefined...
+ const url = state.originalRequest.url;
+ if (cachedResponse) {
+ this.notUpdatedURLs.push(url);
+ } else {
+ this.updatedURLs.push(url);
+ }
+ }
+ }
+ return cachedResponse;
+ };
+ }
+ }
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * A plugin, designed to be used with PrecacheController, to translate URLs into
+ * the corresponding cache key, based on the current revision info.
+ *
+ * @private
+ */
+ class PrecacheCacheKeyPlugin {
+ constructor({
+ precacheController
+ }) {
+ this.cacheKeyWillBeUsed = async ({
+ request,
+ params
+ }) => {
+ // Params is type any, can't change right now.
+ /* eslint-disable */
+ const cacheKey = (params === null || params === void 0 ? void 0 : params.cacheKey) || this._precacheController.getCacheKeyForURL(request.url);
+ /* eslint-enable */
+ return cacheKey ? new Request(cacheKey, {
+ headers: request.headers
+ }) : request;
+ };
+ this._precacheController = precacheController;
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * @param {string} groupTitle
+ * @param {Array} deletedURLs
+ *
+ * @private
+ */
+ const logGroup = (groupTitle, deletedURLs) => {
+ logger.groupCollapsed(groupTitle);
+ for (const url of deletedURLs) {
+ logger.log(url);
+ }
+ logger.groupEnd();
+ };
+ /**
+ * @param {Array} deletedURLs
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function printCleanupDetails(deletedURLs) {
+ const deletionCount = deletedURLs.length;
+ if (deletionCount > 0) {
+ logger.groupCollapsed(`During precaching cleanup, ` + `${deletionCount} cached ` + `request${deletionCount === 1 ? ' was' : 's were'} deleted.`);
+ logGroup('Deleted Cache Requests', deletedURLs);
+ logger.groupEnd();
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * @param {string} groupTitle
+ * @param {Array} urls
+ *
+ * @private
+ */
+ function _nestedGroup(groupTitle, urls) {
+ if (urls.length === 0) {
+ return;
+ }
+ logger.groupCollapsed(groupTitle);
+ for (const url of urls) {
+ logger.log(url);
+ }
+ logger.groupEnd();
+ }
+ /**
+ * @param {Array} urlsToPrecache
+ * @param {Array} urlsAlreadyPrecached
+ *
+ * @private
+ * @memberof workbox-precaching
+ */
+ function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) {
+ const precachedCount = urlsToPrecache.length;
+ const alreadyPrecachedCount = urlsAlreadyPrecached.length;
+ if (precachedCount || alreadyPrecachedCount) {
+ let message = `Precaching ${precachedCount} file${precachedCount === 1 ? '' : 's'}.`;
+ if (alreadyPrecachedCount > 0) {
+ message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? ' is' : 's are'} already cached.`;
+ }
+ logger.groupCollapsed(message);
+ _nestedGroup(`View newly precached URLs.`, urlsToPrecache);
+ _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached);
+ logger.groupEnd();
+ }
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ let supportStatus;
+ /**
+ * A utility function that determines whether the current browser supports
+ * constructing a new `Response` from a `response.body` stream.
+ *
+ * @return {boolean} `true`, if the current browser can successfully
+ * construct a `Response` from a `response.body` stream, `false` otherwise.
+ *
+ * @private
+ */
+ function canConstructResponseFromBodyStream() {
+ if (supportStatus === undefined) {
+ const testResponse = new Response('');
+ if ('body' in testResponse) {
+ try {
+ new Response(testResponse.body);
+ supportStatus = true;
+ } catch (error) {
+ supportStatus = false;
+ }
+ }
+ supportStatus = false;
+ }
+ return supportStatus;
+ }
+
+ /*
+ Copyright 2019 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Allows developers to copy a response and modify its `headers`, `status`,
+ * or `statusText` values (the values settable via a
+ * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax}
+ * object in the constructor).
+ * To modify these values, pass a function as the second argument. That
+ * function will be invoked with a single object with the response properties
+ * `{headers, status, statusText}`. The return value of this function will
+ * be used as the `ResponseInit` for the new `Response`. To change the values
+ * either modify the passed parameter(s) and return it, or return a totally
+ * new object.
+ *
+ * This method is intentionally limited to same-origin responses, regardless of
+ * whether CORS was used or not.
+ *
+ * @param {Response} response
+ * @param {Function} modifier
+ * @memberof workbox-core
+ */
+ async function copyResponse(response, modifier) {
+ let origin = null;
+ // If response.url isn't set, assume it's cross-origin and keep origin null.
+ if (response.url) {
+ const responseURL = new URL(response.url);
+ origin = responseURL.origin;
+ }
+ if (origin !== self.location.origin) {
+ throw new WorkboxError('cross-origin-copy-response', {
+ origin
+ });
+ }
+ const clonedResponse = response.clone();
+ // Create a fresh `ResponseInit` object by cloning the headers.
+ const responseInit = {
+ headers: new Headers(clonedResponse.headers),
+ status: clonedResponse.status,
+ statusText: clonedResponse.statusText
+ };
+ // Apply any user modifications.
+ const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit;
+ // Create the new response from the body stream and `ResponseInit`
+ // modifications. Note: not all browsers support the Response.body stream,
+ // so fall back to reading the entire body into memory as a blob.
+ const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob();
+ return new Response(body, modifiedResponseInit);
+ }
+
+ /*
+ Copyright 2020 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ function stripParams(fullURL, ignoreParams) {
+ const strippedURL = new URL(fullURL);
+ for (const param of ignoreParams) {
+ strippedURL.searchParams.delete(param);
+ }
+ return strippedURL.href;
+ }
+ /**
+ * Matches an item in the cache, ignoring specific URL params. This is similar
+ * to the `ignoreSearch` option, but it allows you to ignore just specific
+ * params (while continuing to match on the others).
+ *
+ * @private
+ * @param {Cache} cache
+ * @param {Request} request
+ * @param {Object} matchOptions
+ * @param {Array} ignoreParams
+ * @return {Promise}
+ */
+ async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) {
+ const strippedRequestURL = stripParams(request.url, ignoreParams);
+ // If the request doesn't include any ignored params, match as normal.
+ if (request.url === strippedRequestURL) {
+ return cache.match(request, matchOptions);
+ }
+ // Otherwise, match by comparing keys
+ const keysOptions = Object.assign(Object.assign({}, matchOptions), {
+ ignoreSearch: true
+ });
+ const cacheKeys = await cache.keys(request, keysOptions);
+ for (const cacheKey of cacheKeys) {
+ const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams);
+ if (strippedRequestURL === strippedCacheKeyURL) {
+ return cache.match(cacheKey, matchOptions);
+ }
+ }
+ return;
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * The Deferred class composes Promises in a way that allows for them to be
+ * resolved or rejected from outside the constructor. In most cases promises
+ * should be used directly, but Deferreds can be necessary when the logic to
+ * resolve a promise must be separate.
+ *
+ * @private
+ */
+ class Deferred {
+ /**
+ * Creates a promise and exposes its resolve and reject functions as methods.
+ */
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+ }
+ }
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ // Callbacks to be executed whenever there's a quota error.
+ // Can't change Function type right now.
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const quotaErrorCallbacks = new Set();
+
+ /*
+ Copyright 2018 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Runs all of the callback functions, one at a time sequentially, in the order
+ * in which they were registered.
+ *
+ * @memberof workbox-core
+ * @private
+ */
+ async function executeQuotaErrorCallbacks() {
+ {
+ logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`);
+ }
+ for (const callback of quotaErrorCallbacks) {
+ await callback();
+ {
+ logger.log(callback, 'is complete.');
+ }
+ }
+ {
+ logger.log('Finished running callbacks.');
+ }
+ }
+
+ /*
+ Copyright 2019 Google LLC
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * Returns a promise that resolves and the passed number of milliseconds.
+ * This utility is an async/await-friendly version of `setTimeout`.
+ *
+ * @param {number} ms
+ * @return {Promise}
+ * @private
+ */
+ function timeout(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ // @ts-ignore
+ try {
+ self['workbox:strategies:7.0.0'] && _();
+ } catch (e) {}
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ function toRequest(input) {
+ return typeof input === 'string' ? new Request(input) : input;
+ }
+ /**
+ * A class created every time a Strategy instance instance calls
+ * {@link workbox-strategies.Strategy~handle} or
+ * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and
+ * cache actions around plugin callbacks and keeps track of when the strategy
+ * is "done" (i.e. all added `event.waitUntil()` promises have resolved).
+ *
+ * @memberof workbox-strategies
+ */
+ class StrategyHandler {
+ /**
+ * Creates a new instance associated with the passed strategy and event
+ * that's handling the request.
+ *
+ * The constructor also initializes the state that will be passed to each of
+ * the plugins handling this request.
+ *
+ * @param {workbox-strategies.Strategy} strategy
+ * @param {Object} options
+ * @param {Request|string} options.request A request to run this strategy for.
+ * @param {ExtendableEvent} options.event The event associated with the
+ * request.
+ * @param {URL} [options.url]
+ * @param {*} [options.params] The return value from the
+ * {@link workbox-routing~matchCallback} (if applicable).
+ */
+ constructor(strategy, options) {
+ this._cacheKeys = {};
+ /**
+ * The request the strategy is performing (passed to the strategy's
+ * `handle()` or `handleAll()` method).
+ * @name request
+ * @instance
+ * @type {Request}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ /**
+ * The event associated with this request.
+ * @name event
+ * @instance
+ * @type {ExtendableEvent}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ /**
+ * A `URL` instance of `request.url` (if passed to the strategy's
+ * `handle()` or `handleAll()` method).
+ * Note: the `url` param will be present if the strategy was invoked
+ * from a workbox `Route` object.
+ * @name url
+ * @instance
+ * @type {URL|undefined}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ /**
+ * A `param` value (if passed to the strategy's
+ * `handle()` or `handleAll()` method).
+ * Note: the `param` param will be present if the strategy was invoked
+ * from a workbox `Route` object and the
+ * {@link workbox-routing~matchCallback} returned
+ * a truthy value (it will be that value).
+ * @name params
+ * @instance
+ * @type {*|undefined}
+ * @memberof workbox-strategies.StrategyHandler
+ */
+ {
+ finalAssertExports.isInstance(options.event, ExtendableEvent, {
+ moduleName: 'workbox-strategies',
+ className: 'StrategyHandler',
+ funcName: 'constructor',
+ paramName: 'options.event'
+ });
+ }
+ Object.assign(this, options);
+ this.event = options.event;
+ this._strategy = strategy;
+ this._handlerDeferred = new Deferred();
+ this._extendLifetimePromises = [];
+ // Copy the plugins list (since it's mutable on the strategy),
+ // so any mutations don't affect this handler instance.
+ this._plugins = [...strategy.plugins];
+ this._pluginStateMap = new Map();
+ for (const plugin of this._plugins) {
+ this._pluginStateMap.set(plugin, {});
+ }
+ this.event.waitUntil(this._handlerDeferred.promise);
+ }
+ /**
+ * Fetches a given request (and invokes any applicable plugin callback
+ * methods) using the `fetchOptions` (for non-navigation requests) and
+ * `plugins` defined on the `Strategy` object.
+ *
+ * The following plugin lifecycle methods are invoked when using this method:
+ * - `requestWillFetch()`
+ * - `fetchDidSucceed()`
+ * - `fetchDidFail()`
+ *
+ * @param {Request|string} input The URL or request to fetch.
+ * @return {Promise}
+ */
+ async fetch(input) {
+ const {
+ event
+ } = this;
+ let request = toRequest(input);
+ if (request.mode === 'navigate' && event instanceof FetchEvent && event.preloadResponse) {
+ const possiblePreloadResponse = await event.preloadResponse;
+ if (possiblePreloadResponse) {
+ {
+ logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`);
+ }
+ return possiblePreloadResponse;
+ }
+ }
+ // If there is a fetchDidFail plugin, we need to save a clone of the
+ // original request before it's either modified by a requestWillFetch
+ // plugin or before the original request's body is consumed via fetch().
+ const originalRequest = this.hasCallback('fetchDidFail') ? request.clone() : null;
+ try {
+ for (const cb of this.iterateCallbacks('requestWillFetch')) {
+ request = await cb({
+ request: request.clone(),
+ event
+ });
+ }
+ } catch (err) {
+ if (err instanceof Error) {
+ throw new WorkboxError('plugin-error-request-will-fetch', {
+ thrownErrorMessage: err.message
+ });
+ }
+ }
+ // The request can be altered by plugins with `requestWillFetch` making
+ // the original request (most likely from a `fetch` event) different
+ // from the Request we make. Pass both to `fetchDidFail` to aid debugging.
+ const pluginFilteredRequest = request.clone();
+ try {
+ let fetchResponse;
+ // See https://github.com/GoogleChrome/workbox/issues/1796
+ fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions);
+ if ("development" !== 'production') {
+ logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`);
+ }
+ for (const callback of this.iterateCallbacks('fetchDidSucceed')) {
+ fetchResponse = await callback({
+ event,
+ request: pluginFilteredRequest,
+ response: fetchResponse
+ });
+ }
+ return fetchResponse;
+ } catch (error) {
+ {
+ logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error);
+ }
+ // `originalRequest` will only exist if a `fetchDidFail` callback
+ // is being used (see above).
+ if (originalRequest) {
+ await this.runCallbacks('fetchDidFail', {
+ error: error,
+ event,
+ originalRequest: originalRequest.clone(),
+ request: pluginFilteredRequest.clone()
+ });
+ }
+ throw error;
+ }
+ }
+ /**
+ * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on
+ * the response generated by `this.fetch()`.
+ *
+ * The call to `this.cachePut()` automatically invokes `this.waitUntil()`,
+ * so you do not have to manually call `waitUntil()` on the event.
+ *
+ * @param {Request|string} input The request or URL to fetch and cache.
+ * @return {Promise}
+ */
+ async fetchAndCachePut(input) {
+ const response = await this.fetch(input);
+ const responseClone = response.clone();
+ void this.waitUntil(this.cachePut(input, responseClone));
+ return response;
+ }
+ /**
+ * Matches a request from the cache (and invokes any applicable plugin
+ * callback methods) using the `cacheName`, `matchOptions`, and `plugins`
+ * defined on the strategy object.
+ *
+ * The following plugin lifecycle methods are invoked when using this method:
+ * - cacheKeyWillByUsed()
+ * - cachedResponseWillByUsed()
+ *
+ * @param {Request|string} key The Request or URL to use as the cache key.
+ * @return {Promise} A matching response, if found.
+ */
+ async cacheMatch(key) {
+ const request = toRequest(key);
+ let cachedResponse;
+ const {
+ cacheName,
+ matchOptions
+ } = this._strategy;
+ const effectiveRequest = await this.getCacheKey(request, 'read');
+ const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), {
+ cacheName
+ });
+ cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);
+ {
+ if (cachedResponse) {
+ logger.debug(`Found a cached response in '${cacheName}'.`);
+ } else {
+ logger.debug(`No cached response found in '${cacheName}'.`);
+ }
+ }
+ for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) {
+ cachedResponse = (await callback({
+ cacheName,
+ matchOptions,
+ cachedResponse,
+ request: effectiveRequest,
+ event: this.event
+ })) || undefined;
+ }
+ return cachedResponse;
+ }
+ /**
+ * Puts a request/response pair in the cache (and invokes any applicable
+ * plugin callback methods) using the `cacheName` and `plugins` defined on
+ * the strategy object.
+ *
+ * The following plugin lifecycle methods are invoked when using this method:
+ * - cacheKeyWillByUsed()
+ * - cacheWillUpdate()
+ * - cacheDidUpdate()
+ *
+ * @param {Request|string} key The request or URL to use as the cache key.
+ * @param {Response} response The response to cache.
+ * @return {Promise} `false` if a cacheWillUpdate caused the response
+ * not be cached, and `true` otherwise.
+ */
+ async cachePut(key, response) {
+ const request = toRequest(key);
+ // Run in the next task to avoid blocking other cache reads.
+ // https://github.com/w3c/ServiceWorker/issues/1397
+ await timeout(0);
+ const effectiveRequest = await this.getCacheKey(request, 'write');
+ {
+ if (effectiveRequest.method && effectiveRequest.method !== 'GET') {
+ throw new WorkboxError('attempt-to-cache-non-get-request', {
+ url: getFriendlyURL(effectiveRequest.url),
+ method: effectiveRequest.method
+ });
+ }
+ // See https://github.com/GoogleChrome/workbox/issues/2818
+ const vary = response.headers.get('Vary');
+ if (vary) {
+ logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` + `has a 'Vary: ${vary}' header. ` + `Consider setting the {ignoreVary: true} option on your strategy ` + `to ensure cache matching and deletion works as expected.`);
+ }
+ }
+ if (!response) {
+ {
+ logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`);
+ }
+ throw new WorkboxError('cache-put-with-no-response', {
+ url: getFriendlyURL(effectiveRequest.url)
+ });
+ }
+ const responseToCache = await this._ensureResponseSafeToCache(response);
+ if (!responseToCache) {
+ {
+ logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache);
+ }
+ return false;
+ }
+ const {
+ cacheName,
+ matchOptions
+ } = this._strategy;
+ const cache = await self.caches.open(cacheName);
+ const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate');
+ const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams(
+ // TODO(philipwalton): the `__WB_REVISION__` param is a precaching
+ // feature. Consider into ways to only add this behavior if using
+ // precaching.
+ cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions) : null;
+ {
+ logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`);
+ }
+ try {
+ await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);
+ } catch (error) {
+ if (error instanceof Error) {
+ // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError
+ if (error.name === 'QuotaExceededError') {
+ await executeQuotaErrorCallbacks();
+ }
+ throw error;
+ }
+ }
+ for (const callback of this.iterateCallbacks('cacheDidUpdate')) {
+ await callback({
+ cacheName,
+ oldResponse,
+ newResponse: responseToCache.clone(),
+ request: effectiveRequest,
+ event: this.event
+ });
+ }
+ return true;
+ }
+ /**
+ * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and
+ * executes any of those callbacks found in sequence. The final `Request`
+ * object returned by the last plugin is treated as the cache key for cache
+ * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have
+ * been registered, the passed request is returned unmodified
+ *
+ * @param {Request} request
+ * @param {string} mode
+ * @return {Promise}
+ */
+ async getCacheKey(request, mode) {
+ const key = `${request.url} | ${mode}`;
+ if (!this._cacheKeys[key]) {
+ let effectiveRequest = request;
+ for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) {
+ effectiveRequest = toRequest(await callback({
+ mode,
+ request: effectiveRequest,
+ event: this.event,
+ // params has a type any can't change right now.
+ params: this.params // eslint-disable-line
+ }));
+ }
+
+ this._cacheKeys[key] = effectiveRequest;
+ }
+ return this._cacheKeys[key];
+ }
+ /**
+ * Returns true if the strategy has at least one plugin with the given
+ * callback.
+ *
+ * @param {string} name The name of the callback to check for.
+ * @return {boolean}
+ */
+ hasCallback(name) {
+ for (const plugin of this._strategy.plugins) {
+ if (name in plugin) {
+ return true;
+ }
+ }
+ return false;
+ }
+ /**
+ * Runs all plugin callbacks matching the given name, in order, passing the
+ * given param object (merged ith the current plugin state) as the only
+ * argument.
+ *
+ * Note: since this method runs all plugins, it's not suitable for cases
+ * where the return value of a callback needs to be applied prior to calling
+ * the next callback. See
+ * {@link workbox-strategies.StrategyHandler#iterateCallbacks}
+ * below for how to handle that case.
+ *
+ * @param {string} name The name of the callback to run within each plugin.
+ * @param {Object} param The object to pass as the first (and only) param
+ * when executing each callback. This object will be merged with the
+ * current plugin state prior to callback execution.
+ */
+ async runCallbacks(name, param) {
+ for (const callback of this.iterateCallbacks(name)) {
+ // TODO(philipwalton): not sure why `any` is needed. It seems like
+ // this should work with `as WorkboxPluginCallbackParam[C]`.
+ await callback(param);
+ }
+ }
+ /**
+ * Accepts a callback and returns an iterable of matching plugin callbacks,
+ * where each callback is wrapped with the current handler state (i.e. when
+ * you call each callback, whatever object parameter you pass it will
+ * be merged with the plugin's current state).
+ *
+ * @param {string} name The name fo the callback to run
+ * @return {Array}
+ */
+ *iterateCallbacks(name) {
+ for (const plugin of this._strategy.plugins) {
+ if (typeof plugin[name] === 'function') {
+ const state = this._pluginStateMap.get(plugin);
+ const statefulCallback = param => {
+ const statefulParam = Object.assign(Object.assign({}, param), {
+ state
+ });
+ // TODO(philipwalton): not sure why `any` is needed. It seems like
+ // this should work with `as WorkboxPluginCallbackParam[C]`.
+ return plugin[name](statefulParam);
+ };
+ yield statefulCallback;
+ }
+ }
+ }
+ /**
+ * Adds a promise to the
+ * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises}
+ * of the event event associated with the request being handled (usually a
+ * `FetchEvent`).
+ *
+ * Note: you can await
+ * {@link workbox-strategies.StrategyHandler~doneWaiting}
+ * to know when all added promises have settled.
+ *
+ * @param {Promise} promise A promise to add to the extend lifetime promises
+ * of the event that triggered the request.
+ */
+ waitUntil(promise) {
+ this._extendLifetimePromises.push(promise);
+ return promise;
+ }
+ /**
+ * Returns a promise that resolves once all promises passed to
+ * {@link workbox-strategies.StrategyHandler~waitUntil}
+ * have settled.
+ *
+ * Note: any work done after `doneWaiting()` settles should be manually
+ * passed to an event's `waitUntil()` method (not this handler's
+ * `waitUntil()` method), otherwise the service worker thread my be killed
+ * prior to your work completing.
+ */
+ async doneWaiting() {
+ let promise;
+ while (promise = this._extendLifetimePromises.shift()) {
+ await promise;
+ }
+ }
+ /**
+ * Stops running the strategy and immediately resolves any pending
+ * `waitUntil()` promises.
+ */
+ destroy() {
+ this._handlerDeferred.resolve(null);
+ }
+ /**
+ * This method will call cacheWillUpdate on the available plugins (or use
+ * status === 200) to determine if the Response is safe and valid to cache.
+ *
+ * @param {Request} options.request
+ * @param {Response} options.response
+ * @return {Promise}
+ *
+ * @private
+ */
+ async _ensureResponseSafeToCache(response) {
+ let responseToCache = response;
+ let pluginsUsed = false;
+ for (const callback of this.iterateCallbacks('cacheWillUpdate')) {
+ responseToCache = (await callback({
+ request: this.request,
+ response: responseToCache,
+ event: this.event
+ })) || undefined;
+ pluginsUsed = true;
+ if (!responseToCache) {
+ break;
+ }
+ }
+ if (!pluginsUsed) {
+ if (responseToCache && responseToCache.status !== 200) {
+ responseToCache = undefined;
+ }
+ {
+ if (responseToCache) {
+ if (responseToCache.status !== 200) {
+ if (responseToCache.status === 0) {
+ logger.warn(`The response for '${this.request.url}' ` + `is an opaque response. The caching strategy that you're ` + `using will not cache opaque responses by default.`);
+ } else {
+ logger.debug(`The response for '${this.request.url}' ` + `returned a status code of '${response.status}' and won't ` + `be cached as a result.`);
+ }
+ }
+ }
+ }
+ }
+ return responseToCache;
+ }
+ }
+
+ /*
+ Copyright 2020 Google LLC
+
+ Use of this source code is governed by an MIT-style
+ license that can be found in the LICENSE file or at
+ https://opensource.org/licenses/MIT.
+ */
+ /**
+ * An abstract base class that all other strategy classes must extend from:
+ *
+ * @memberof workbox-strategies
+ */
+ class Strategy {
+ /**
+ * Creates a new instance of the strategy and sets all documented option
+ * properties as public instance properties.
+ *
+ * Note: if a custom strategy class extends the base Strategy class and does
+ * not need more than these properties, it does not need to define its own
+ * constructor.
+ *
+ * @param {Object} [options]
+ * @param {string} [options.cacheName] Cache name to store and retrieve
+ * requests. Defaults to the cache names provided by
+ * {@link workbox-core.cacheNames}.
+ * @param {Array