Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea0963d75f | ||
|
|
46d5718011 | ||
|
|
f8740e04f3 | ||
|
|
669717d27b | ||
|
|
715dfc556a | ||
|
|
cafb7d3f7b | ||
|
|
5f92f6ec7f | ||
|
|
eea610dfe8 | ||
|
|
e8733a9bd9 | ||
|
|
d292931073 | ||
|
|
f6a0db753a | ||
|
|
8518e179ee | ||
|
|
3f40cdced7 | ||
|
|
4cf17185da | ||
|
|
b8ae92c2d4 | ||
|
|
bad67020cc | ||
|
|
2afcd498f3 | ||
|
|
ce4c3d5360 | ||
|
|
d575e36879 | ||
|
|
98fcabfcb5 | ||
|
|
1e2773345a | ||
|
|
f847633cf3 | ||
|
|
bd2ddd7702 | ||
|
|
26b7b34bd4 | ||
|
|
6f3e446a79 | ||
|
|
f0d7f2e6c7 | ||
|
|
c8f7abfd53 | ||
|
|
2b29ff1820 | ||
|
|
5627e9c919 | ||
|
|
582b713b84 | ||
|
|
5eb8e58b65 | ||
|
|
403688b18c | ||
|
|
f0d13a7acd | ||
|
|
78f1a7f3e5 | ||
|
|
a57ab4d12a | ||
|
|
3b6c6374e3 | ||
|
|
4b508b0c25 | ||
|
|
1cbef867ce | ||
|
|
05444f48d4 | ||
|
|
2ea227ca06 | ||
|
|
35aaad175c | ||
|
|
863fe05907 | ||
|
|
1c9560044f | ||
|
|
343f06c57d | ||
|
|
e2bb647386 | ||
|
|
a3cb9dab57 | ||
|
|
410ab028c7 | ||
|
|
54dd738f61 | ||
|
|
6f16c7e48d | ||
|
|
82ddd95cf0 | ||
|
|
2b658fae7e | ||
|
|
a822151e92 | ||
|
|
0ae49c6fdc | ||
|
|
1d069d2da3 | ||
|
|
274342a5e1 | ||
|
|
21a5d1bfc4 | ||
|
|
e421bc7df9 | ||
|
|
e41ba2bfde | ||
|
|
d2e75b0f20 | ||
|
|
07b272e024 | ||
|
|
a08caa9bdb | ||
|
|
29afddb3a6 | ||
|
|
f85f856734 | ||
|
|
c6bc19c002 | ||
|
|
538555f000 | ||
|
|
8681036b21 | ||
|
|
e55b909ca6 | ||
|
|
4bb36704ee | ||
|
|
9bbdb35d88 | ||
|
|
2f443fa2ef | ||
|
|
6d7d7da287 | ||
|
|
1f6751f95b | ||
|
|
92081a097d | ||
|
|
ba1a33c3ac | ||
|
|
193ff12b6e | ||
|
|
3296ffaa07 | ||
|
|
0456d92462 |
No files matched your search
+35
-27
@@ -48,12 +48,12 @@ jobs:
|
||||
- checkout:
|
||||
post:
|
||||
- git fetch --all
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# # when lock file changes, use increasingly general patterns to restore cache
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
@@ -107,12 +107,12 @@ jobs:
|
||||
post:
|
||||
- git fetch --all
|
||||
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# # when lock file changes, use increasingly general patterns to restore cache
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
@@ -143,12 +143,12 @@ jobs:
|
||||
- checkout:
|
||||
post:
|
||||
- git fetch --all
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# # when lock file changes, use increasingly general patterns to restore cache
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
@@ -237,11 +237,11 @@ jobs:
|
||||
post:
|
||||
- git fetch --all
|
||||
# Use increasingly general patterns to restore cache
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
@@ -346,7 +346,9 @@ workflows:
|
||||
record: true
|
||||
store_artifacts: true
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
yarn run build
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
@@ -372,7 +374,9 @@ workflows:
|
||||
record: true
|
||||
store_artifacts: true
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build:package
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
yarn run build:package
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
@@ -403,7 +407,9 @@ workflows:
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
@@ -480,7 +486,9 @@ workflows:
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js yarn run build
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
|
||||
@@ -9,6 +9,7 @@ src/version.js
|
||||
junit.xml
|
||||
coverage/
|
||||
.docz/
|
||||
*.js.map
|
||||
|
||||
# YALC (for Erik)
|
||||
.yalc
|
||||
@@ -34,3 +35,4 @@ screenshots/
|
||||
|
||||
# Locize settings
|
||||
.locize
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
cff-version: 1.2.0
|
||||
message: "If you use this software, please cite it as below."
|
||||
authors:
|
||||
- family-names: "Ziegler"
|
||||
given-names: "Erik"
|
||||
orcid: "https://orcid.org/0000-0003-1857-8129"
|
||||
- family-names: "Urban"
|
||||
given-names: "Trinity"
|
||||
- family-names: "Brown"
|
||||
given-names: "Danny"
|
||||
- family-names: "Petts"
|
||||
given-names: "James"
|
||||
- family-names: "Pieper"
|
||||
given-names: "Steve D"
|
||||
- family-names: "Lewis"
|
||||
given-names: "Rob"
|
||||
- family-names: "Hafey"
|
||||
given-names: "Chris"
|
||||
- family-names: "Harris"
|
||||
given-names: "Gordon J"
|
||||
title: "Open Health Imaging Foundation Viewer: An Extensible Open-Source Framework for Building Web-Based Imaging Applications to Support Cancer Research"
|
||||
doi: 10.1200/CCI.19.00131
|
||||
version: 1.0.0
|
||||
url: "https://github.com/OHIF/Viewers"
|
||||
+3
-2
@@ -21,7 +21,7 @@
|
||||
|
||||
# Stage 1: Build the application
|
||||
# docker build -t ohif/viewer:latest .
|
||||
FROM node:10.16.3-slim as builder
|
||||
FROM node:15.13.0-slim as builder
|
||||
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
@@ -39,6 +39,7 @@ COPY package.json /usr/src/app/package.json
|
||||
COPY postcss.config.js /usr/src/app/postcss.config.js
|
||||
COPY yarn.lock /usr/src/app/yarn.lock
|
||||
|
||||
RUN apt-get update && apt-get install -y python make g++
|
||||
# Run the install before copying the rest of the files
|
||||
RUN yarn config set workspaces-experimental true
|
||||
RUN yarn install
|
||||
@@ -52,7 +53,7 @@ RUN yarn run build
|
||||
|
||||
# Stage 2: Bundle the built application into a Docker container
|
||||
# which runs Nginx using Alpine Linux
|
||||
FROM nginx:1.15.5-alpine
|
||||
FROM nginx:1.21.1-alpine
|
||||
RUN apk add --no-cache bash
|
||||
RUN rm -rf /etc/nginx/conf.d
|
||||
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"-fontsettings",
|
||||
"github",
|
||||
"ga",
|
||||
"sitemap",
|
||||
"anchors",
|
||||
"versions"
|
||||
],
|
||||
@@ -24,7 +23,8 @@
|
||||
"token": "UA-110573590-2"
|
||||
},
|
||||
"sitemap": {
|
||||
"hostname": "https://docs.ohif.org"
|
||||
"hostname": "https://docs.ohif.org",
|
||||
"todo": "sitemap plugin removed; causing build errors"
|
||||
},
|
||||
"versions": {
|
||||
"gitbookConfigURL": "https://raw.githubusercontent.com/OHIF/Viewers/master/docs/book.json",
|
||||
|
||||
+2
-2
@@ -8,7 +8,6 @@
|
||||
"-fontsettings",
|
||||
"github",
|
||||
"ga",
|
||||
"sitemap",
|
||||
"anchors",
|
||||
"versions"
|
||||
],
|
||||
@@ -24,7 +23,8 @@
|
||||
"token": "UA-110573590-2"
|
||||
},
|
||||
"sitemap": {
|
||||
"hostname": "https://docs.ohif.org"
|
||||
"hostname": "https://docs.ohif.org",
|
||||
"todo": "sitemap plugin removed; causing build errors"
|
||||
},
|
||||
"versions": {
|
||||
"gitbookConfigURL": "https://raw.githubusercontent.com/OHIF/Viewers/master/docs/book.json",
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.10.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.9...@ohif/extension-cornerstone@2.10.10) (2021-07-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* changed new image handler conditional for 0 edge case ([#2338](https://github.com/OHIF/Viewers/issues/2338)) ([b8ae92c](https://github.com/OHIF/Viewers/commit/b8ae92c2d4f733b3ca89826118029685650b396a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.8...@ohif/extension-cornerstone@2.10.9) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.7...@ohif/extension-cornerstone@2.10.8) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.6...@ohif/extension-cornerstone@2.10.7) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.5...@ohif/extension-cornerstone@2.10.6) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.4...@ohif/extension-cornerstone@2.10.5) (2021-03-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.10.5",
|
||||
"version": "2.10.10",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -36,7 +36,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.3",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay'
|
||||
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -194,6 +195,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
return null;
|
||||
}
|
||||
const { viewportIndex } = this.props;
|
||||
const { inconsistencyWarnings } = this.props.viewportData.displaySet;
|
||||
const {
|
||||
imageIds,
|
||||
currentImageIdIndex,
|
||||
@@ -219,7 +221,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const { StudyInstanceUID } = displaySet;
|
||||
|
||||
if (currentImageIdIndex > 0) {
|
||||
if (currentImageIdIndex >= 0) {
|
||||
this.props.onNewImage({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID: sopInstanceUid,
|
||||
@@ -229,14 +231,19 @@ class OHIFCornerstoneViewport extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
const warningsOverlay = props => {
|
||||
return <OHIFCornerstoneViewportOverlay {...props} inconsistencyWarnings={inconsistencyWarnings} />
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectedCornerstoneViewport
|
||||
viewportIndex={viewportIndex}
|
||||
imageIds={imageIds}
|
||||
imageIdIndex={currentImageIdIndex}
|
||||
onNewImage={newImageHandler}
|
||||
onNewImageDebounceTime={700}
|
||||
onNewImageDebounced={newImageHandler}
|
||||
onNewImageDebounceTime={300}
|
||||
viewportOverlayComponent={warningsOverlay}
|
||||
// ~~ Connected (From REDUX)
|
||||
// frameRate={frameRate}
|
||||
// isPlaying={false}
|
||||
|
||||
@@ -268,6 +268,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex,
|
||||
refreshViewports = true,
|
||||
}) => {
|
||||
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||
|
||||
@@ -278,6 +279,10 @@ const commandsModule = ({ servicesManager }) => {
|
||||
);
|
||||
});
|
||||
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||
displaySet.frameIndex = frameIndex;
|
||||
|
||||
@@ -285,7 +290,9 @@ const commandsModule = ({ servicesManager }) => {
|
||||
setViewportSpecificData(activeViewportIndex, displaySet)
|
||||
);
|
||||
|
||||
refreshCornerstoneViewports();
|
||||
if (refreshViewports) {
|
||||
refreshCornerstoneViewports();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
.imageViewerViewport.empty ~ .OHIFCornerstoneViewportOverlay {
|
||||
display: none;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay {
|
||||
color: #9ccef9;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .overlay-element {
|
||||
position: absolute;
|
||||
font-weight: 400;
|
||||
text-shadow: 1px 1px #000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .top-left {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .top-center {
|
||||
top: 20px;
|
||||
padding-top: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .top-right {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .bottom-left {
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .bottom-left2 {
|
||||
bottom: 140px;
|
||||
left: 20px;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .bottom-right {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay.controlsVisible .topright,
|
||||
.OHIFCornerstoneViewportOverlay.controlsVisible .bottomright {
|
||||
right: calc(20px + 19px);
|
||||
}
|
||||
.OHIFCornerstoneViewportOverlay .warning {
|
||||
position: absolute;
|
||||
font-weight: 400;
|
||||
text-shadow: 1px 1px #000;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
|
||||
.OHIFCornerstoneViewportOverlay .warning svg {
|
||||
opacity: 1;
|
||||
color: #e29e4a;
|
||||
fill: #e29e4a;
|
||||
stroke: #e29e4a;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.OHIFCornerstoneViewportOverlay svg {
|
||||
color: #9ccef9;
|
||||
fill: #9ccef9;
|
||||
stroke: #9ccef9;
|
||||
background-color: transparent;
|
||||
margin: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import './OHIFCornerstoneViewportOverlay.css';
|
||||
import {
|
||||
isValidNumber,
|
||||
formatNumberPrecision,
|
||||
formatDICOMDate,
|
||||
formatDICOMTime,
|
||||
formatPN,
|
||||
getCompression
|
||||
} from '../utils/formatStudy';
|
||||
import classNames from 'classnames';
|
||||
import { Icon } from '@ohif/ui/src/elements/Icon';
|
||||
import { Tooltip } from '@ohif/ui/src/components/tooltip';
|
||||
import { OverlayTrigger } from '@ohif/ui/src/components/overlayTrigger';
|
||||
|
||||
class OHIFCornerstoneViewportOverlay extends PureComponent {
|
||||
static propTypes = {
|
||||
scale: PropTypes.number.isRequired,
|
||||
windowWidth: PropTypes.oneOfType([
|
||||
PropTypes.number.isRequired,
|
||||
PropTypes.string.isRequired,
|
||||
]),
|
||||
windowCenter: PropTypes.oneOfType([
|
||||
PropTypes.number.isRequired,
|
||||
PropTypes.string.isRequired,
|
||||
]),
|
||||
imageId: PropTypes.string.isRequired,
|
||||
imageIndex: PropTypes.number.isRequired,
|
||||
stackSize: PropTypes.number.isRequired,
|
||||
inconsistencyWarnings: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { imageId, scale, windowWidth, windowCenter, inconsistencyWarnings } = this.props;
|
||||
|
||||
if (!imageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const zoomPercentage = formatNumberPrecision(scale * 100, 0);
|
||||
const seriesMetadata =
|
||||
cornerstone.metaData.get('generalSeriesModule', imageId) || {};
|
||||
const imagePlaneModule =
|
||||
cornerstone.metaData.get('imagePlaneModule', imageId) || {};
|
||||
const { rows, columns, sliceThickness, sliceLocation } = imagePlaneModule;
|
||||
const { seriesNumber, seriesDescription } = seriesMetadata;
|
||||
|
||||
const generalStudyModule =
|
||||
cornerstone.metaData.get('generalStudyModule', imageId) || {};
|
||||
const { studyDate, studyTime, studyDescription } = generalStudyModule;
|
||||
|
||||
const patientModule =
|
||||
cornerstone.metaData.get('patientModule', imageId) || {};
|
||||
const { patientId, patientName } = patientModule;
|
||||
|
||||
const generalImageModule =
|
||||
cornerstone.metaData.get('generalImageModule', imageId) || {};
|
||||
const { instanceNumber } = generalImageModule;
|
||||
|
||||
const cineModule = cornerstone.metaData.get('cineModule', imageId) || {};
|
||||
const { frameTime } = cineModule;
|
||||
|
||||
const frameRate = formatNumberPrecision(1000 / frameTime, 1);
|
||||
const compression = getCompression(imageId);
|
||||
const wwwc = `W: ${
|
||||
windowWidth.toFixed ? windowWidth.toFixed(0) : windowWidth
|
||||
} L: ${windowWidth.toFixed ? windowCenter.toFixed(0) : windowCenter}`;
|
||||
const imageDimensions = `${columns} x ${rows}`;
|
||||
|
||||
const { imageIndex, stackSize } = this.props;
|
||||
|
||||
const inconsistencyWarningsOn = inconsistencyWarnings && inconsistencyWarnings.length !== 0 ? true : false;
|
||||
const getWarningContent = (warningList) => {
|
||||
if (Array.isArray(warningList)) {
|
||||
const listedWarnings = warningList.map((warn, index) => {
|
||||
return <li key={index}>{warn}</li>;
|
||||
});
|
||||
|
||||
return <ol>{listedWarnings}</ol>;
|
||||
} else {
|
||||
return <React.Fragment>{warningList}</React.Fragment>;
|
||||
}
|
||||
};
|
||||
|
||||
const getWarningInfo = (seriesNumber, inconsistencyWarnings) => {
|
||||
return(
|
||||
<React.Fragment>
|
||||
{inconsistencyWarnings.length != 0 ? (
|
||||
<OverlayTrigger
|
||||
key={seriesNumber}
|
||||
placement="left"
|
||||
overlay={
|
||||
<Tooltip
|
||||
placement="left"
|
||||
className="in tooltip-warning"
|
||||
id="tooltip-left"
|
||||
>
|
||||
<div className="warningTitle">Series Inconsistencies</div>
|
||||
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div className={classNames('warning')}>
|
||||
<span className="warning-icon">
|
||||
<Icon name="exclamation-triangle" />
|
||||
</span>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const normal = (
|
||||
<React.Fragment>
|
||||
<div className="top-left overlay-element">
|
||||
<div>{formatPN(patientName)}</div>
|
||||
<div>{patientId}</div>
|
||||
</div>
|
||||
<div className="top-right overlay-element">
|
||||
<div>{studyDescription}</div>
|
||||
<div>
|
||||
{formatDICOMDate(studyDate)} {formatDICOMTime(studyTime)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-right overlay-element">
|
||||
<div>Zoom: {zoomPercentage}%</div>
|
||||
<div>{wwwc}</div>
|
||||
<div className="compressionIndicator">{compression}</div>
|
||||
</div>
|
||||
<div className="bottom-left2 warning">
|
||||
<div>{inconsistencyWarningsOn ? getWarningInfo(seriesNumber, inconsistencyWarnings) : ''}</div>
|
||||
</div>
|
||||
<div className="bottom-left overlay-element">
|
||||
<div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div>
|
||||
<div>
|
||||
{stackSize > 1
|
||||
? `Img: ${instanceNumber} ${imageIndex}/${stackSize}`
|
||||
: ''}
|
||||
</div>
|
||||
<div>
|
||||
{frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''}
|
||||
<div>{imageDimensions}</div>
|
||||
<div>
|
||||
{isValidNumber(sliceLocation)
|
||||
? `Loc: ${formatNumberPrecision(sliceLocation, 2)} mm `
|
||||
: ''}
|
||||
{sliceThickness
|
||||
? `Thick: ${formatNumberPrecision(sliceThickness, 2)} mm`
|
||||
: ''}
|
||||
</div>
|
||||
<div>{seriesDescription}</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
return <div className="OHIFCornerstoneViewportOverlay">{normal}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default OHIFCornerstoneViewportOverlay;
|
||||
@@ -38,7 +38,14 @@ export default {
|
||||
},
|
||||
getViewportModule({ commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
/**
|
||||
* TODO: This appears to be used to set the redux parameters for
|
||||
* the viewport when new images are loaded. It's very ugly
|
||||
* and we should remove it.
|
||||
*/
|
||||
const onNewImageHandler = jumpData => {
|
||||
/** Do not trigger all viewports to render unnecessarily */
|
||||
jumpData.refreshViewports = false;
|
||||
commandsManager.runCommand('jumpToImage', jumpData);
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* Checks if value is valid.
|
||||
*
|
||||
* @param {number} value
|
||||
* @returns {boolean} is valid.
|
||||
*/
|
||||
function isValidNumber(value) {
|
||||
return typeof value === 'number' && !isNaN(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats number precision.
|
||||
*
|
||||
* @param {number} number
|
||||
* @param {number} precision
|
||||
* @returns {number} formatted number.
|
||||
*/
|
||||
function formatNumberPrecision(number, precision) {
|
||||
if (number !== null) {
|
||||
return parseFloat(number).toFixed(precision);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats DICOM date.
|
||||
*
|
||||
* @param {string} date
|
||||
* @param {string} strFormat
|
||||
* @returns {string} formatted date.
|
||||
*/
|
||||
function formatDICOMDate(date, strFormat = 'MMM D, YYYY') {
|
||||
return moment(date, 'YYYYMMDD').format(strFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* DICOM Time is stored as HHmmss.SSS, where:
|
||||
* HH 24 hour time:
|
||||
* m mm 0..59 Minutes
|
||||
* s ss 0..59 Seconds
|
||||
* S SS SSS 0..999 Fractional seconds
|
||||
*
|
||||
* Goal: '24:12:12'
|
||||
*
|
||||
* @param {*} time
|
||||
* @param {string} strFormat
|
||||
* @returns {string} formatted name.
|
||||
*/
|
||||
function formatDICOMTime(time, strFormat = 'HH:mm:ss') {
|
||||
return moment(time, 'HH:mm:ss').format(strFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a patient name for display purposes
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {string} formatted name.
|
||||
*/
|
||||
function formatPN(name) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the first ^ to a ', '. String.replace() only affects
|
||||
// the first appearance of the character.
|
||||
const commaBetweenFirstAndLast = name.replace('^', ', ');
|
||||
|
||||
// Replace any remaining '^' characters with spaces
|
||||
const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ');
|
||||
|
||||
// Trim any extraneous whitespace
|
||||
return cleaned.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets compression type
|
||||
*
|
||||
* @param {number} imageId
|
||||
* @returns {string} comrpession type.
|
||||
*/
|
||||
function getCompression(imageId) {
|
||||
const generalImageModule =
|
||||
cornerstone.metaData.get('generalImageModule', imageId) || {};
|
||||
const {
|
||||
lossyImageCompression,
|
||||
lossyImageCompressionRatio,
|
||||
lossyImageCompressionMethod,
|
||||
} = generalImageModule;
|
||||
|
||||
if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
|
||||
const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
|
||||
const compressionRatio = formatNumberPrecision(
|
||||
lossyImageCompressionRatio,
|
||||
2
|
||||
);
|
||||
return compressionMethod + compressionRatio + ' : 1';
|
||||
}
|
||||
|
||||
return 'Lossless / Uncompressed';
|
||||
}
|
||||
|
||||
export { isValidNumber,
|
||||
formatNumberPrecision,
|
||||
formatDICOMDate,
|
||||
formatDICOMTime,
|
||||
formatPN,
|
||||
getCompression
|
||||
};
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.1...@ohif/extension-debugging@0.2.2) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.0...@ohif/extension-debugging@0.2.1) (2021-03-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-debugging",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"description": "OHIF extension for debugging.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -28,13 +28,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.5.2"
|
||||
"dicom-parser": "^1.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"detect-browser": "5.1.1",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"dicomweb-client": "^0.8.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"jszip": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,57 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.9...@ohif/extension-dicom-html@1.3.10) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.8...@ohif/extension-dicom-html@1.3.9) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.7...@ohif/extension-dicom-html@1.3.8) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.6...@ohif/extension-dicom-html@1.3.7) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.5...@ohif/extension-dicom-html@1.3.6) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.4...@ohif/extension-dicom-html@1.3.5) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.3...@ohif/extension-dicom-html@1.3.4) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.3.4",
|
||||
"version": "1.3.10",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dcmjs": "0.18.3",
|
||||
"dcmjs": "0.18.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -168,6 +168,16 @@ const getContentSequence = (data, level = 1) => {
|
||||
Object.keys(data).forEach(key => {
|
||||
const value = data[key];
|
||||
|
||||
if (key === '_meta') {
|
||||
const HeaderDynamicLevel = `h3`;
|
||||
root.push(<hr />);
|
||||
root.push(
|
||||
<HeaderDynamicLevel key="Metadata">
|
||||
DICOM File Meta Information
|
||||
</HeaderDynamicLevel>
|
||||
);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (value instanceof Object) {
|
||||
content = getContentSequence(value, level + 1);
|
||||
@@ -215,6 +225,7 @@ class DicomHtmlViewport extends Component {
|
||||
const dicomData = DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
dataset._meta = DicomMetaDictionary.naturalizeDataset(dataset._meta);
|
||||
|
||||
const mainData = getMainData(dataset);
|
||||
const contentSequence = getContentSequence(dataset);
|
||||
|
||||
@@ -9,6 +9,7 @@ const SOP_CLASS_UIDS = {
|
||||
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
|
||||
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
|
||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
||||
ACQUISITION_CONTEXT_SR_STORAGE: '1.2.840.10008.5.1.4.1.1.88.71',
|
||||
};
|
||||
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
@@ -3,6 +3,65 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.6.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.13...@ohif/extension-dicom-rt@0.6.14) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.12...@ohif/extension-dicom-rt@0.6.13) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.11...@ohif/extension-dicom-rt@0.6.12) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.10...@ohif/extension-dicom-rt@0.6.11) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.9...@ohif/extension-dicom-rt@0.6.10) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.8...@ohif/extension-dicom-rt@0.6.9) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.7...@ohif/extension-dicom-rt@0.6.8) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.6...@ohif/extension-dicom-rt@0.6.7) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-rt",
|
||||
"version": "0.6.7",
|
||||
"version": "0.6.14",
|
||||
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.18.3",
|
||||
"dcmjs": "0.18.8",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
|
||||
@@ -71,8 +71,8 @@ const OHIFDicomRTStructSopClassHandler = {
|
||||
}
|
||||
}
|
||||
|
||||
rtStructDisplaySet.getSourceDisplaySet = function (studies) {
|
||||
return getSourceDisplaySet(studies, rtStructDisplaySet);
|
||||
rtStructDisplaySet.getSourceDisplaySet = function (studies, activateLabelMap = true) {
|
||||
return getSourceDisplaySet(studies, rtStructDisplaySet, activateLabelMap);
|
||||
};
|
||||
|
||||
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||
|
||||
@@ -1,38 +1,14 @@
|
||||
export default function getSourceDisplaySet(studies, rtStructDisplaySet) {
|
||||
const referencedDisplaySet = _getReferencedDisplaySet(
|
||||
import { metadata } from '@ohif/core';
|
||||
|
||||
export default function getSourceDisplaySet(studies, rtStructDisplaySet, activateLabelMap = true) {
|
||||
const referencedDisplaySet = metadata.StudyMetadata.getReferencedDisplaySet(
|
||||
rtStructDisplaySet,
|
||||
studies
|
||||
);
|
||||
|
||||
rtStructDisplaySet.load(referencedDisplaySet, studies);
|
||||
if (activateLabelMap) {
|
||||
rtStructDisplaySet.load(referencedDisplaySet, studies);
|
||||
}
|
||||
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
|
||||
const _getReferencedDisplaySet = (rtStructDisplaySet, studies) => {
|
||||
let allDisplaySets = [];
|
||||
|
||||
studies.forEach(study => {
|
||||
allDisplaySets = allDisplaySets.concat(study.displaySets);
|
||||
});
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
ds => ds.displaySetInstanceUID !== rtStructDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const ReferencedSeriesSequence = Array.isArray(
|
||||
rtStructDisplaySet.metadata.ReferencedSeriesSequence
|
||||
)
|
||||
? rtStructDisplaySet.metadata.ReferencedSeriesSequence
|
||||
: [rtStructDisplaySet.metadata.ReferencedSeriesSequence];
|
||||
|
||||
const referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
|
||||
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const referencedDisplaySet = otherDisplaySets.find(ds =>
|
||||
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
|
||||
);
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import init from './init.js';
|
||||
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
|
||||
import id from './id.js';
|
||||
import RTPanel from './components/RTPanel/RTPanel';
|
||||
import { version } from '../package.json';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
export default {
|
||||
@@ -42,12 +43,48 @@ export default {
|
||||
);
|
||||
};
|
||||
|
||||
const RTPanelTabChangedEvent = 'rt-panel-tab-updated';
|
||||
|
||||
/**
|
||||
* Trigger's an event to update the state of the panel's RoundedButtonGroup.
|
||||
*
|
||||
* This is required to avoid extension state
|
||||
* coupling with the viewer's ToolbarRow component.
|
||||
*
|
||||
* @param {object} data
|
||||
*/
|
||||
const triggerRTPanelUpdatedEvent = data => {
|
||||
const event = new CustomEvent(RTPanelTabChangedEvent, {
|
||||
detail: data,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onRTStructsLoaded = ({ detail }) => {
|
||||
const { rtStructDisplaySet, referencedDisplaySet } = detail;
|
||||
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
rtStructDisplaySet.StudyInstanceUID
|
||||
);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: referencedDisplaySet.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
});
|
||||
triggerRTPanelUpdatedEvent({
|
||||
badgeNumber: referencedDisplaysets.length,
|
||||
target: 'rt-panel',
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('extensiondicomrtrtloaded', onRTStructsLoaded);
|
||||
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'RTSTRUCT',
|
||||
target: 'rt-panel',
|
||||
stateEvent: RTPanelTabChangedEvent,
|
||||
isDisabled: (studies, activeViewport) => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
|
||||
@@ -90,11 +90,11 @@ export default async function loadRTStruct(
|
||||
const sopInstanceUID = ContourImageSequence
|
||||
? ContourImageSequence.ReferencedSOPInstanceUID
|
||||
: _getClosestSOPInstanceUID(
|
||||
ContourData,
|
||||
ContourGeometricType,
|
||||
NumberOfContourPoints,
|
||||
imageIdSopInstanceUidPairs
|
||||
);
|
||||
ContourData,
|
||||
ContourGeometricType,
|
||||
NumberOfContourPoints,
|
||||
imageIdSopInstanceUidPairs
|
||||
);
|
||||
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||
|
||||
if (!imageId) {
|
||||
@@ -171,7 +171,13 @@ export default async function loadRTStruct(
|
||||
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
const event = new CustomEvent('extensiondicomrtrtloaded');
|
||||
const event = new CustomEvent('extensiondicomrtrtloaded', {
|
||||
detail: {
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies,
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
@@ -299,7 +305,9 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
||||
return imageIdSopInstanceUidPairsEntry
|
||||
? imageIdSopInstanceUidPairsEntry.imageId
|
||||
: null;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
|
||||
@@ -3,6 +3,89 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.6.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.10...@ohif/extension-dicom-segmentation@0.6.11) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.9...@ohif/extension-dicom-segmentation@0.6.10) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.8...@ohif/extension-dicom-segmentation@0.6.9) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.7...@ohif/extension-dicom-segmentation@0.6.8) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.6...@ohif/extension-dicom-segmentation@0.6.7) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.5...@ohif/extension-dicom-segmentation@0.6.6) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.4...@ohif/extension-dicom-segmentation@0.6.5) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.3...@ohif/extension-dicom-segmentation@0.6.4) (2021-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.2...@ohif/extension-dicom-segmentation@0.6.3) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.1...@ohif/extension-dicom-segmentation@0.6.2) (2021-03-31)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.0...@ohif/extension-dicom-segmentation@0.6.1) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.11",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.18.3",
|
||||
"dcmjs": "0.18.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
+44
-5
@@ -155,7 +155,9 @@ const SegmentationPanel = ({
|
||||
|
||||
const brushStackState = getBrushStackState();
|
||||
brushStackState.activeLabelmapIndex = newLabelmapIndex;
|
||||
setState(state => ({ ...state, selectedSegmentation }));
|
||||
if (selectedSegmentation) {
|
||||
setState(state => ({ ...state, selectedSegmentation }));
|
||||
}
|
||||
|
||||
refreshViewports();
|
||||
|
||||
@@ -189,6 +191,10 @@ const SegmentationPanel = ({
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
refreshSegmentations
|
||||
);
|
||||
document.addEventListener(
|
||||
'extensiondicomsegmentationsegselected',
|
||||
updateSegmentationComboBox
|
||||
);
|
||||
|
||||
/*
|
||||
* These are specific to each element;
|
||||
@@ -207,6 +213,10 @@ const SegmentationPanel = ({
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
refreshSegmentations
|
||||
);
|
||||
document.removeEventListener(
|
||||
'extensiondicomsegmentationsegselected',
|
||||
updateSegmentationComboBox
|
||||
);
|
||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||
enabledElement.removeEventListener(
|
||||
'cornerstonetoolslabelmapmodified',
|
||||
@@ -214,7 +224,32 @@ const SegmentationPanel = ({
|
||||
)
|
||||
);
|
||||
};
|
||||
}, [activeIndex, viewports]);
|
||||
}, [
|
||||
activeIndex,
|
||||
updateSegmentationComboBox,
|
||||
viewports,
|
||||
]);
|
||||
|
||||
const updateSegmentationComboBox = e => {
|
||||
const index = e.detail.activatedLabelmapIndex;
|
||||
if (index !== -1) {
|
||||
setState(state => ({ ...state, selectedSegmentation: index }));
|
||||
} else {
|
||||
cleanSegmentationComboBox();
|
||||
}
|
||||
};
|
||||
|
||||
const cleanSegmentationComboBox = () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
segmentsHidden: [],
|
||||
segmentNumbers: [],
|
||||
labelMapList: [],
|
||||
segmentList: [],
|
||||
isDisabled: true,
|
||||
selectedSegmentation: -1,
|
||||
}));
|
||||
};
|
||||
|
||||
const refreshSegmentations = () => {
|
||||
const activeViewport = getActiveViewport();
|
||||
@@ -277,7 +312,11 @@ const SegmentationPanel = ({
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||
const filteredReferencedSegDisplaysets = referencedSegDisplaysets.filter(
|
||||
segDisplay => segDisplay.loadError !== true
|
||||
);
|
||||
|
||||
return filteredReferencedSegDisplaysets.map((displaySet, index) => {
|
||||
const {
|
||||
labelmapIndex,
|
||||
originLabelMapIndex,
|
||||
@@ -289,7 +328,7 @@ const SegmentationPanel = ({
|
||||
/* Map to display representation */
|
||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||
const displayDate = date.format('ddd, MMM Do YYYY, h:mm:ss a');
|
||||
const displayDescription = displaySet.SeriesDescription;
|
||||
|
||||
return {
|
||||
@@ -709,7 +748,7 @@ const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
|
||||
referencedDisplaysets.sort((a, b) => {
|
||||
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
|
||||
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
|
||||
return aNumber - bNumber;
|
||||
return bNumber - aNumber;
|
||||
});
|
||||
|
||||
return referencedDisplaysets;
|
||||
|
||||
@@ -3,7 +3,7 @@ import Select from 'react-select';
|
||||
|
||||
const SegmentationSelect = ({ value, formatOptionLabel, options }) => (
|
||||
<Select
|
||||
value={value}
|
||||
value={value === undefined ? null : value}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={segmentationSelectStyles}
|
||||
options={options}
|
||||
|
||||
@@ -55,6 +55,7 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||
labelmapIndex: null, // Assigned when loaded.
|
||||
isLoaded: false,
|
||||
loadError: false,
|
||||
hasOverlapping: false,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
@@ -63,8 +64,8 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
metadata,
|
||||
};
|
||||
|
||||
segDisplaySet.getSourceDisplaySet = function(studies) {
|
||||
return getSourceDisplaySet(studies, segDisplaySet);
|
||||
segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
|
||||
return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler);
|
||||
};
|
||||
|
||||
segDisplaySet.load = async function(referencedDisplaySet, studies) {
|
||||
@@ -82,57 +83,50 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
StudyInstanceUID,
|
||||
referencedDisplaySet.SeriesInstanceUID
|
||||
);
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let results;
|
||||
try {
|
||||
results = _parseSeg(segArrayBuffer, imageIds);
|
||||
} catch (error) {
|
||||
segDisplaySet.isLoaded = false;
|
||||
segDisplaySet.loadError = true;
|
||||
reject(error);
|
||||
}
|
||||
const {
|
||||
labelmapBufferArray,
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
segmentsOnFrameArray,
|
||||
} = results;
|
||||
|
||||
let labelmapIndex;
|
||||
if (labelmapBufferArray.length > 1) {
|
||||
let labelmapIndexes = [];
|
||||
for (let i = 0; i < labelmapBufferArray.length; ++i) {
|
||||
labelmapIndexes.push(
|
||||
await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBufferArray[i],
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
segmentsOnFrameArray[i]
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Since overlapping segmentations have virtual labelmaps,
|
||||
* originLabelMapIndex is used in the panel to select the correct dropdown value.
|
||||
*/
|
||||
segDisplaySet.hasOverlapping = true;
|
||||
segDisplaySet.originLabelMapIndex = labelmapIndexes[0];
|
||||
labelmapIndex = labelmapIndexes[0];
|
||||
console.warn('Overlapping segmentations!');
|
||||
} else {
|
||||
labelmapIndex = await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBufferArray[0],
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
[]
|
||||
const results = await _parseSeg(segArrayBuffer, imageIds);
|
||||
if (results === undefined) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
labelmapBufferArray,
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
segmentsOnFrameArray,
|
||||
} = results;
|
||||
let labelmapIndex;
|
||||
if (labelmapBufferArray.length > 1) {
|
||||
let labelmapIndexes = [];
|
||||
for (let i = 0; i < labelmapBufferArray.length; ++i) {
|
||||
labelmapIndexes.push(
|
||||
await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBufferArray[i],
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
segmentsOnFrameArray[i]
|
||||
)
|
||||
);
|
||||
}
|
||||
resolve(labelmapIndex);
|
||||
});
|
||||
/**
|
||||
* Since overlapping segments have virtual labelmaps,
|
||||
* originLabelMapIndex is used in the panel to select the correct dropdown value.
|
||||
*/
|
||||
segDisplaySet.hasOverlapping = true;
|
||||
segDisplaySet.originLabelMapIndex = labelmapIndexes[0];
|
||||
labelmapIndex = labelmapIndexes[0];
|
||||
console.warn('Overlapping segments!');
|
||||
} else {
|
||||
labelmapIndex = await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBufferArray[0],
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
[]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return segDisplaySet;
|
||||
|
||||
@@ -1,88 +1,16 @@
|
||||
import setActiveLabelmap from './utils/setActiveLabelMap';
|
||||
import { classes } from '@ohif/core';
|
||||
import { metadata } from '@ohif/core';
|
||||
|
||||
const { ImageSet } = classes;
|
||||
export default function getSourceDisplaySet(studies, segDisplaySet, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
|
||||
const referencedDisplaySet = metadata.StudyMetadata.getReferencedDisplaySet(segDisplaySet, studies);
|
||||
|
||||
export default function getSourceDisplaySet(studies, segDisplaySet) {
|
||||
const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies);
|
||||
|
||||
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet);
|
||||
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
|
||||
const _getReferencedDisplaySet = (segDisplaySet, studies) => {
|
||||
let allDisplaySets = [];
|
||||
|
||||
studies.forEach(study => {
|
||||
allDisplaySets = allDisplaySets.concat(study.displaySets);
|
||||
});
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
ds => ds.displaySetInstanceUID !== segDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const { metadata } = segDisplaySet;
|
||||
|
||||
let referencedSeriesInstanceUIDs;
|
||||
|
||||
if (metadata.ReferencedSeriesSequence) {
|
||||
const ReferencedSeriesSequence = _toArray(
|
||||
metadata.ReferencedSeriesSequence
|
||||
);
|
||||
|
||||
referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
|
||||
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
const { PerFrameFunctionalGroupsSequence } = metadata;
|
||||
|
||||
let SourceImageSequence;
|
||||
|
||||
if (metadata.SourceImageSequence) {
|
||||
SourceImageSequence = metadata.SourceImageSequence;
|
||||
} else {
|
||||
const firstFunctionalGroups = _toArray(
|
||||
PerFrameFunctionalGroupsSequence
|
||||
)[0];
|
||||
const { DerivationImageSequence } = firstFunctionalGroups;
|
||||
|
||||
SourceImageSequence = DerivationImageSequence;
|
||||
}
|
||||
|
||||
const firstSourceImage = _toArray(SourceImageSequence)[0];
|
||||
|
||||
const { ReferencedSOPInstanceUID } = firstSourceImage;
|
||||
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
|
||||
otherDisplaySets,
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
let activatedLabelmapPromise;
|
||||
if (activateLabelMap) {
|
||||
activatedLabelmapPromise = setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet, undefined, onDisplaySetLoadFailureHandler);
|
||||
}
|
||||
|
||||
const referencedDisplaySet = otherDisplaySets.find(ds =>
|
||||
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
|
||||
);
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
|
||||
const _findReferencedSeriesInstanceUIDsFromSOPInstanceUID = (
|
||||
displaySets,
|
||||
SOPInstanceUID
|
||||
) => {
|
||||
const imageSets = displaySets.filter(ds => ds instanceof ImageSet);
|
||||
|
||||
for (let i = 0; i < imageSets.length; i++) {
|
||||
const { images } = imageSets[i];
|
||||
for (let j = 0; j < images.length; j++) {
|
||||
if (images[j].SOPInstanceUID === SOPInstanceUID) {
|
||||
return [images[j].getData().metadata.SeriesInstanceUID];
|
||||
}
|
||||
}
|
||||
return {
|
||||
referencedDisplaySet : referencedDisplaySet,
|
||||
activatedLabelmapPromise : activatedLabelmapPromise
|
||||
}
|
||||
};
|
||||
|
||||
function _toArray(arrayOrObject) {
|
||||
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from 'react';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import init from './init.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import getSopClassHandlerModule from './getOHIFDicomSegSopClassHandler.js';
|
||||
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
||||
import { version } from '../package.json';
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -79,12 +82,50 @@ export default {
|
||||
);
|
||||
};
|
||||
|
||||
const SegmentationPanelTabUpdatedEvent = 'segmentation-panel-tab-updated';
|
||||
|
||||
/**
|
||||
* Trigger's an event to update the state of the panel's RoundedButtonGroup.
|
||||
*
|
||||
* This is required to avoid extension state
|
||||
* coupling with the viewer's ToolbarRow component.
|
||||
*
|
||||
* @param {object} data
|
||||
*/
|
||||
const triggerSegmentationPanelTabUpdatedEvent = data => {
|
||||
const event = new CustomEvent(SegmentationPanelTabUpdatedEvent, {
|
||||
detail: data,
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const onSegmentationsLoaded = ({ detail }) => {
|
||||
const { segDisplaySet, segMetadata } = detail;
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
segDisplaySet.StudyInstanceUID
|
||||
);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: segMetadata.seriesInstanceUid,
|
||||
Modality: 'SEG',
|
||||
});
|
||||
triggerSegmentationPanelTabUpdatedEvent({
|
||||
badgeNumber: referencedDisplaysets.length,
|
||||
target: 'segmentation-panel',
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
onSegmentationsLoaded
|
||||
);
|
||||
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'Segmentations',
|
||||
target: 'segmentation-panel',
|
||||
stateEvent: SegmentationPanelTabUpdatedEvent,
|
||||
isDisabled: studies => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
|
||||
@@ -59,7 +59,16 @@ export default async function loadSegmentation(
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
console.log('Segmentation loaded.');
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloaded');
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloaded', {
|
||||
detail: {
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBuffer,
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
labelmapSegments,
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
|
||||
return labelmapIndex;
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async function setActiveLabelmap(
|
||||
displaySet,
|
||||
callback = () => {},
|
||||
onDisplaySetLoadFailure = err => {
|
||||
throw new Error(err.message);
|
||||
console.error(err.message);
|
||||
}
|
||||
) {
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
@@ -48,16 +48,15 @@ export default async function setActiveLabelmap(
|
||||
}
|
||||
|
||||
if (!displaySet.isLoaded) {
|
||||
const loadPromise = displaySet.load(referencedDisplaySet, studies);
|
||||
|
||||
loadPromise.catch(error => {
|
||||
try {
|
||||
await displaySet.load(referencedDisplaySet, studies);
|
||||
} catch (error) {
|
||||
displaySet.isLoaded = false;
|
||||
displaySet.loadError = true;
|
||||
onDisplaySetLoadFailure(error);
|
||||
|
||||
// Return old index.
|
||||
return activeLabelmapIndex;
|
||||
});
|
||||
|
||||
await loadPromise;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
labelmapIndex =
|
||||
@@ -67,8 +66,11 @@ export default async function setActiveLabelmap(
|
||||
|
||||
// This might have just been created, so need to use the non-cached value.
|
||||
state = cornerstoneTools.getModule('segmentation').state;
|
||||
|
||||
brushStackState = state.series[firstImageId];
|
||||
brushStackState.activeLabelmapIndex = labelmapIndex;
|
||||
if (brushStackState) {
|
||||
brushStackState.activeLabelmapIndex = labelmapIndex;
|
||||
}
|
||||
|
||||
refreshViewports();
|
||||
callback();
|
||||
|
||||
@@ -3,6 +3,57 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.10...@ohif/extension-dicom-tag-browser@0.2.11) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.9...@ohif/extension-dicom-tag-browser@0.2.10) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.8...@ohif/extension-dicom-tag-browser@0.2.9) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.7...@ohif/extension-dicom-tag-browser@0.2.8) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.6...@ohif/extension-dicom-tag-browser@0.2.7) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.5...@ohif/extension-dicom-tag-browser@0.2.6) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.4...@ohif/extension-dicom-tag-browser@0.2.5) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-tag-browser",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.11",
|
||||
"description": "OHIF extension for checking DICOM headers.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -28,12 +28,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"dcmjs": "0.18.3",
|
||||
"dcmjs": "0.18.8",
|
||||
"react": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"dicomweb-client": "^0.8.1",
|
||||
"moment": "2.24.0",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,116 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.15...@ohif/extension-vtk@1.11.16) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.14...@ohif/extension-vtk@1.11.15) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.13...@ohif/extension-vtk@1.11.14) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.12...@ohif/extension-vtk@1.11.13) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.11...@ohif/extension-vtk@1.11.12) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.10...@ohif/extension-vtk@1.11.11) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.9...@ohif/extension-vtk@1.11.10) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.8...@ohif/extension-vtk@1.11.9) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.7...@ohif/extension-vtk@1.11.8) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.6...@ohif/extension-vtk@1.11.7) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.5...@ohif/extension-vtk@1.11.6) (2021-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.4...@ohif/extension-vtk@1.11.5) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.3...@ohif/extension-vtk@1.11.4) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.2...@ohif/extension-vtk@1.11.3) (2021-03-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.11.3",
|
||||
"version": "1.11.16",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -35,7 +35,7 @@
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.3",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
@@ -53,8 +53,8 @@
|
||||
"react-vtkjs-viewport": "^0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.13.2",
|
||||
"@ohif/ui": "^1.9.0",
|
||||
"@ohif/core": "^2.13.15",
|
||||
"@ohif/ui": "^1.9.3",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -72,7 +72,7 @@ class OHIFVTKViewport extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onScroll: () => {},
|
||||
onScroll: () => { },
|
||||
};
|
||||
|
||||
static id = 'OHIFVTKViewport';
|
||||
@@ -271,7 +271,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
volumeMapper.setSampleDistance(sampleDistance);
|
||||
|
||||
// Be generous to surpress warnings, as the logging really hurts performance.
|
||||
// Be generous to suppress warnings, as the logging really hurts performance.
|
||||
// TODO: maybe we should auto adjust samples to 1000.
|
||||
volumeMapper.setMaximumSamplesPerRay(4000);
|
||||
|
||||
@@ -398,7 +398,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
const _isDisplaySetReconstructable = (
|
||||
displaySetInstanceUID = '',
|
||||
StudyInstanceUID = ''
|
||||
) => {
|
||||
if (displaySetInstanceUID == '' || StudyInstanceUID == '') {
|
||||
let isVisible = true;
|
||||
|
||||
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
|
||||
if (!viewportSpecificData[activeViewportIndex]) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[
|
||||
activeViewportIndex
|
||||
];
|
||||
|
||||
const studies = studyMetadataManager.all();
|
||||
|
||||
@@ -24,132 +27,38 @@ const _isDisplaySetReconstructable = (
|
||||
return false;
|
||||
}
|
||||
|
||||
const displaySet = study._displaySets.find(
|
||||
set => set.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID);
|
||||
|
||||
if (!displaySet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2D MPR is not currently available for 4D datasets.
|
||||
|
||||
// Assuming that slices at different time have the same position, here we just check if
|
||||
// there are multiple slices for the same ImagePositionPatient and disable MPR.
|
||||
|
||||
// A better heuristic would be checking 4D tags, e.g. the presence of multiple TemporalPositionIdentifier values.
|
||||
// However, some studies (e.g. https://github.com/OHIF/Viewers/issues/2113) do not have such tags.
|
||||
|
||||
for (let ii = 0; ii < displaySet.numImageFrames; ++ii) {
|
||||
const image = displaySet.images[ii];
|
||||
if (!image) continue;
|
||||
|
||||
const imageIdControl = image.getImageId();
|
||||
const instanceMetadataControl = cornerstone.metaData.get(
|
||||
'instance',
|
||||
imageIdControl
|
||||
);
|
||||
|
||||
if (
|
||||
!instanceMetadataControl ||
|
||||
instanceMetadataControl === undefined ||
|
||||
!instanceMetadataControl.ImagePositionPatient ||
|
||||
instanceMetadataControl.ImagePositionPatient === undefined
|
||||
) {
|
||||
// if ImagePositionPatient is missing, skip the 4D datasets check.
|
||||
// do not return false, because it could be a 3D dataset.
|
||||
continue;
|
||||
}
|
||||
|
||||
let xImagePositionPatientControl =
|
||||
instanceMetadataControl.ImagePositionPatient[0];
|
||||
let yImagePositionPatientControl =
|
||||
instanceMetadataControl.ImagePositionPatient[1];
|
||||
let zImagePositionPatientControl =
|
||||
instanceMetadataControl.ImagePositionPatient[2];
|
||||
|
||||
for (let jj = ii + 1; jj < displaySet.numImageFrames; ++jj) {
|
||||
const image = displaySet.images[jj];
|
||||
if (!image) continue;
|
||||
|
||||
const imageId = image.getImageId();
|
||||
const instanceMetadata = cornerstone.metaData.get('instance', imageId);
|
||||
|
||||
if (
|
||||
!instanceMetadata ||
|
||||
instanceMetadata === undefined ||
|
||||
!instanceMetadata.ImagePositionPatient ||
|
||||
instanceMetadata.ImagePositionPatient === undefined
|
||||
) {
|
||||
// if ImagePositionPatient is missing, skip the 4D datasets check.
|
||||
// do not return false, because it could be a 3D dataset.
|
||||
continue;
|
||||
}
|
||||
|
||||
let xImagePositionPatient = instanceMetadata.ImagePositionPatient[0];
|
||||
let yImagePositionPatient = instanceMetadata.ImagePositionPatient[1];
|
||||
let zImagePositionPatient = instanceMetadata.ImagePositionPatient[2];
|
||||
|
||||
// ImagePositionPatient is float
|
||||
if (
|
||||
Math.abs( xImagePositionPatientControl - xImagePositionPatient ) < 1e-6 &&
|
||||
Math.abs( yImagePositionPatientControl - yImagePositionPatient ) < 1e-6 &&
|
||||
Math.abs( zImagePositionPatientControl - zImagePositionPatient ) < 1e-6
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return displaySet.isReconstructable;
|
||||
};
|
||||
|
||||
const selectDisplaySetInstanceUID = state => {
|
||||
if (
|
||||
state.viewports.viewportSpecificData[state.viewports.activeViewportIndex]
|
||||
) {
|
||||
return state.viewports.viewportSpecificData[
|
||||
state.viewports.activeViewportIndex
|
||||
].displaySetInstanceUID;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const selectStudyInstanceUID = state => {
|
||||
if (
|
||||
state.viewports.viewportSpecificData[state.viewports.activeViewportIndex]
|
||||
) {
|
||||
return state.viewports.viewportSpecificData[
|
||||
state.viewports.activeViewportIndex
|
||||
].StudyInstanceUID;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const stateSelector = createSelector(
|
||||
[selectDisplaySetInstanceUID, selectStudyInstanceUID],
|
||||
(displaySetInstanceUID, StudyInstanceUID) => {
|
||||
return {
|
||||
displaySetInstanceUID,
|
||||
StudyInstanceUID,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
function VTKMPRToolbarButton({ toolbarClickCallback, button, isActive }) {
|
||||
function VTKMPRToolbarButton({
|
||||
parentContext,
|
||||
toolbarClickCallback,
|
||||
button,
|
||||
activeButtons,
|
||||
isActive,
|
||||
className,
|
||||
}) {
|
||||
const { id, label, icon } = button;
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = useSelector(
|
||||
stateSelector
|
||||
);
|
||||
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
|
||||
const { viewports = {} } = state;
|
||||
const { viewportSpecificData, activeViewportIndex } = viewports;
|
||||
|
||||
const isVisible = useMemo(() => {
|
||||
return _isDisplaySetReconstructable(
|
||||
displaySetInstanceUID,
|
||||
StudyInstanceUID
|
||||
);
|
||||
}, [displaySetInstanceUID, StudyInstanceUID]);
|
||||
return {
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
}
|
||||
});
|
||||
|
||||
isVisible = _isDisplaySetReconstructable(
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -3,6 +3,119 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.13.15](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.14...@ohif/core@2.13.15) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.14](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.13...@ohif/core@2.13.14) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.13](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.12...@ohif/core@2.13.13) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.12](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.11...@ohif/core@2.13.12) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.10...@ohif/core@2.13.11) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.9...@ohif/core@2.13.10) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.8...@ohif/core@2.13.9) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.7...@ohif/core@2.13.8) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.6...@ohif/core@2.13.7) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.5...@ohif/core@2.13.6) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.4...@ohif/core@2.13.5) (2021-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.3...@ohif/core@2.13.4) (2021-04-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **wl:** fix metadata provider window level issues after dicomweb update (string vs number) ([8681036](https://github.com/OHIF/Viewers/commit/8681036b2133cfc325658691a256a86b5e36705f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.2...@ohif/core@2.13.3) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.1...@ohif/core@2.13.2) (2021-03-25)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.13.2",
|
||||
"version": "2.13.15",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -39,13 +39,14 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "0.18.3",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicomweb-client": "^0.8.1",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.merge": "^4.6.1",
|
||||
"mousetrap": "^1.6.3",
|
||||
"retry": "^0.12.0",
|
||||
"validate.js": "^0.12.0"
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import parseDicomStructuredReport from './parseDicomStructuredReport';
|
||||
import parseMeasurementsData from './parseMeasurementsData';
|
||||
import getAllDisplaySets from './utils/getAllDisplaySets';
|
||||
import errorHandler from '../errorHandler';
|
||||
import getXHRRetryRequestHook from '../utils/xhrRetryRequestHook';
|
||||
|
||||
const VERSION_NAME = 'dcmjs-0.0';
|
||||
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
||||
@@ -23,6 +24,7 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
@@ -74,6 +76,7 @@ const stowSRFromMeasurements = async (measurements, serverUrl) => {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
@@ -77,7 +77,6 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
|
||||
/**
|
||||
* Function to create imagePath with all imageData related
|
||||
*
|
||||
* @param {string} StudyInstanceUID
|
||||
* @param {string} SeriesInstanceUID
|
||||
* @param {string} SOPInstanceUID
|
||||
|
||||
@@ -4,6 +4,7 @@ import dicomParser from 'dicom-parser';
|
||||
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
|
||||
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
|
||||
import validNumber from '../utils/metadataProvider/validNumber';
|
||||
|
||||
class MetadataProvider {
|
||||
constructor() {
|
||||
@@ -230,26 +231,26 @@ class MetadataProvider {
|
||||
let columnCosines;
|
||||
|
||||
if (PixelSpacing) {
|
||||
rowPixelSpacing = PixelSpacing[0];
|
||||
columnPixelSpacing = PixelSpacing[1];
|
||||
rowPixelSpacing = validNumber(PixelSpacing[0]);
|
||||
columnPixelSpacing = validNumber(PixelSpacing[1]);
|
||||
}
|
||||
|
||||
if (ImageOrientationPatient) {
|
||||
rowCosines = ImageOrientationPatient.slice(0, 3);
|
||||
columnCosines = ImageOrientationPatient.slice(3, 6);
|
||||
rowCosines = validNumber(ImageOrientationPatient.slice(0, 3));
|
||||
columnCosines = validNumber(ImageOrientationPatient.slice(3, 6));
|
||||
}
|
||||
|
||||
metadata = {
|
||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
imageOrientationPatient: ImageOrientationPatient,
|
||||
imageOrientationPatient: validNumber(ImageOrientationPatient),
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
imagePositionPatient: instance.ImagePositionPatient,
|
||||
sliceThickness: instance.SliceThickness,
|
||||
sliceLocation: instance.SliceLocation,
|
||||
pixelSpacing: PixelSpacing,
|
||||
imagePositionPatient: validNumber(instance.ImagePositionPatient),
|
||||
sliceThickness: validNumber(instance.SliceThickness),
|
||||
sliceLocation: validNumber(instance.SliceLocation),
|
||||
pixelSpacing: validNumber(PixelSpacing),
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
};
|
||||
@@ -284,7 +285,7 @@ class MetadataProvider {
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
||||
const { WindowCenter, WindowWidth } = instance;
|
||||
let { WindowCenter, WindowWidth } = instance;
|
||||
|
||||
const windowCenter = Array.isArray(WindowCenter)
|
||||
? WindowCenter
|
||||
@@ -294,15 +295,17 @@ class MetadataProvider {
|
||||
: [WindowWidth];
|
||||
|
||||
metadata = {
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
windowCenter: validNumber(windowCenter),
|
||||
windowWidth: validNumber(windowWidth),
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
||||
const rescaleSlope = validNumber(instance.RescaleSlope);
|
||||
const rescaleIntercept = validNumber(instance.RescaleIntercept);
|
||||
metadata = {
|
||||
rescaleIntercept: instance.RescaleIntercept,
|
||||
rescaleSlope: instance.RescaleSlope,
|
||||
rescaleIntercept,
|
||||
rescaleSlope,
|
||||
rescaleType: instance.RescaleType,
|
||||
};
|
||||
break;
|
||||
|
||||
@@ -9,11 +9,12 @@ import { SeriesMetadata } from './SeriesMetadata';
|
||||
import { api } from 'dicomweb-client';
|
||||
// - createStacks
|
||||
import { isImage } from '../../utils/isImage';
|
||||
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
|
||||
import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable';
|
||||
import errorHandler from '../../errorHandler';
|
||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||
import getXHRRetryRequestHook from '../../utils/xhrRetryRequestHook';
|
||||
|
||||
export class StudyMetadata extends Metadata {
|
||||
class StudyMetadata extends Metadata {
|
||||
constructor(data, uid) {
|
||||
super(data, uid);
|
||||
// Initialize Private Properties
|
||||
@@ -226,6 +227,56 @@ export class StudyMetadata extends Metadata {
|
||||
displaySets.map(displaySet => this._derivedDisplaySets.push(displaySet));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the source display set of the derivated display set.
|
||||
* @param {object} derivatedDisplaySet
|
||||
* @param {array[StudyMetadata]} studies
|
||||
* @return {object} source display set.
|
||||
*/
|
||||
static getReferencedDisplaySet(derivatedDisplaySet, studies) {
|
||||
let allDisplaySets = [];
|
||||
|
||||
studies.forEach(study => {
|
||||
allDisplaySets = allDisplaySets.concat(study.displaySets);
|
||||
});
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
ds => ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const { metadata } = derivatedDisplaySet;
|
||||
|
||||
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence
|
||||
(metadata, otherDisplaySets);
|
||||
|
||||
let noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
|
||||
referencedSeriesInstanceUIDs.length === 0;
|
||||
if (noReferencedSeriesAvailable) {
|
||||
referencedSeriesInstanceUIDs =
|
||||
_findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence
|
||||
(metadata);
|
||||
}
|
||||
|
||||
noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
|
||||
referencedSeriesInstanceUIDs.length === 0;
|
||||
if (noReferencedSeriesAvailable) {
|
||||
referencedSeriesInstanceUIDs =
|
||||
_findReferencedSeriesInstanceUIDsFromReferencedImageSequence
|
||||
(metadata, otherDisplaySets);
|
||||
}
|
||||
|
||||
const referencedSeriesAvailable = referencedSeriesInstanceUIDs &&
|
||||
referencedSeriesInstanceUIDs.length !== 0;
|
||||
if (referencedSeriesAvailable) {
|
||||
const referencedDisplaySet = otherDisplaySets.find(ds =>
|
||||
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
|
||||
);
|
||||
;
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of derived datasets in the study, filtered by the given filter.
|
||||
* @param {object} filter An object containing search filters
|
||||
@@ -252,20 +303,7 @@ export class StudyMetadata extends Metadata {
|
||||
if (referencedSeriesInstanceUID) {
|
||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
||||
displaySet => {
|
||||
if (!displaySet.metadata.ReferencedSeriesSequence) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ReferencedSeriesSequence = Array.isArray(
|
||||
displaySet.metadata.ReferencedSeriesSequence
|
||||
)
|
||||
? displaySet.metadata.ReferencedSeriesSequence
|
||||
: [displaySet.metadata.ReferencedSeriesSequence];
|
||||
|
||||
return ReferencedSeriesSequence.some(
|
||||
ReferencedSeries =>
|
||||
ReferencedSeries.SeriesInstanceUID === referencedSeriesInstanceUID
|
||||
);
|
||||
return StudyMetadata.getReferencedDisplaySet(displaySet, [this]).SeriesInstanceUID === referencedSeriesInstanceUID;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -273,7 +311,7 @@ export class StudyMetadata extends Metadata {
|
||||
if (referencedFrameOfReferenceUID) {
|
||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
||||
displaySet =>
|
||||
displaySet.ReferencedFrameOfReferenceUID ===
|
||||
displaySet.FrameOfReferenceUID ===
|
||||
referencedFrameOfReferenceUID
|
||||
);
|
||||
}
|
||||
@@ -767,6 +805,16 @@ const isMultiFrame = instance => {
|
||||
return instance.getTagValue('NumberOfFrames') > 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a display set for a series.
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
* If reconstructable, the frames are sorted.
|
||||
*
|
||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {Object} imageSet.
|
||||
*/
|
||||
const makeDisplaySet = (series, instances) => {
|
||||
const instance = instances[0];
|
||||
const imageSet = new ImageSet(instances);
|
||||
@@ -786,7 +834,7 @@ const makeDisplaySet = (series, instances) => {
|
||||
isMultiFrame: isMultiFrame(instance),
|
||||
});
|
||||
|
||||
// Sort the images in this series if needed
|
||||
// Sort the images in this series by instanceNumber
|
||||
const shallSort = true; //!OHIF.utils.ObjectPath.get(Meteor, 'settings.public.ui.sortSeriesByIncomingOrder');
|
||||
if (shallSort) {
|
||||
imageSet.sortBy((a, b) => {
|
||||
@@ -804,18 +852,32 @@ const makeDisplaySet = (series, instances) => {
|
||||
imageSet.getImage(0).getTagValue('InstanceNumber')
|
||||
);
|
||||
|
||||
const isReconstructable = isDisplaySetReconstructable(instances);
|
||||
|
||||
imageSet.isReconstructable = isReconstructable.value;
|
||||
const displayReconstructableInfo = isDisplaySetReconstructable(instances);
|
||||
imageSet.isReconstructable = displayReconstructableInfo.value;
|
||||
|
||||
let displaySpacingInfo = undefined;
|
||||
if (shallSort && imageSet.isReconstructable) {
|
||||
// sort images by image position
|
||||
imageSet.sortByImagePositionPatient();
|
||||
|
||||
// check if the spacing is uniform and update isReconstructable
|
||||
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find
|
||||
(issue => issue === ReconstructionIssues.DATASET_4D);
|
||||
displaySpacingInfo = isSpacingUniform(imageSet.images, datasetIs4D);
|
||||
imageSet.isReconstructable = displaySpacingInfo.isUniform;
|
||||
|
||||
if (displaySpacingInfo.missingFrames) {
|
||||
// TODO -> This is currently unused, but may be used for reconstructing
|
||||
// Volumes with gaps later on.
|
||||
imageSet.missingFrames = displaySpacingInfo.missingFrames;
|
||||
}
|
||||
}
|
||||
|
||||
if (isReconstructable.missingFrames) {
|
||||
// TODO -> This is currently unused, but may be used for reconstructing
|
||||
// Volumes with gaps later on.
|
||||
imageSet.missingFrames = isReconstructable.missingFrames;
|
||||
if (!imageSet.displayReconstructableInfo) {
|
||||
// It is not reconstrabale Save type of warning
|
||||
imageSet.reconstructionIssues = displaySpacingInfo ?
|
||||
displayReconstructableInfo.reconstructionIssues.concat(displaySpacingInfo.reconstructionIssues) :
|
||||
displayReconstructableInfo.reconstructionIssues;
|
||||
}
|
||||
|
||||
return imageSet;
|
||||
@@ -878,6 +940,7 @@ function _getDisplaySetFromSopClassModule(
|
||||
url: study.getData().wadoRoot,
|
||||
headers,
|
||||
errorInterceptor,
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
});
|
||||
|
||||
let displaySet = plugin.getDisplaySetFromSeries(
|
||||
@@ -892,3 +955,147 @@ function _getDisplaySetFromSopClassModule(
|
||||
}
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedSeriesSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence (
|
||||
metadata,
|
||||
) {
|
||||
if (!metadata.ReferencedSeriesSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
let referencedSeriesInstanceUIDs;
|
||||
const ReferencedSeriesSequence = _toArray(
|
||||
metadata.ReferencedSeriesSequence
|
||||
);
|
||||
|
||||
referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
|
||||
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
|
||||
);
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence (
|
||||
metadata,
|
||||
displaySets
|
||||
) {
|
||||
if (!metadata.ReferencedImageSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
let referencedSeriesInstanceUIDs;
|
||||
const referencedImageArray = _toArray(metadata.ReferencedImageSequence);
|
||||
for (let i = 0; i < referencedImageArray.length; i++) {
|
||||
const { ReferencedSOPInstanceUID } = referencedImageArray[i];
|
||||
if (!ReferencedSOPInstanceUID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
|
||||
displaySets,
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
|
||||
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SourceImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
|
||||
metadata,
|
||||
displaySets
|
||||
) {
|
||||
let SourceImageSequence;
|
||||
|
||||
if (metadata.SourceImageSequence) {
|
||||
SourceImageSequence = metadata.SourceImageSequence;
|
||||
} else {
|
||||
const { PerFrameFunctionalGroupsSequence } = metadata;
|
||||
const firstFunctionalGroups = _toArray(
|
||||
PerFrameFunctionalGroupsSequence
|
||||
)[0];
|
||||
if (firstFunctionalGroups) {
|
||||
const { DerivationImageSequence } = firstFunctionalGroups;
|
||||
SourceImageSequence = DerivationImageSequence;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SourceImageSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceImageArray = _toArray(SourceImageSequence);
|
||||
|
||||
let referencedSeriesInstanceUIDs;
|
||||
for (let i = 0; i < sourceImageArray.length; i++) {
|
||||
const { ReferencedSOPInstanceUID } = sourceImageArray[i];
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
|
||||
displaySets,
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SOPInstanceUID of the displaySets.
|
||||
* @param {array[object]} displaysets
|
||||
* @param {string} SOPInstanceUID
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID (
|
||||
displaySets,
|
||||
SOPInstanceUID
|
||||
) {
|
||||
const imageSets = displaySets.filter(ds => ds instanceof ImageSet);
|
||||
|
||||
for (let i = 0; i < imageSets.length; i++) {
|
||||
const { images } = imageSets[i];
|
||||
if (!images) {
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < images.length; j++) {
|
||||
const image = images[j];
|
||||
if (!image) {
|
||||
continue;
|
||||
}
|
||||
if (image.SOPInstanceUID === SOPInstanceUID) {
|
||||
return [image.getData().metadata.SeriesInstanceUID];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _toArray(arrayOrObject) {
|
||||
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
|
||||
}
|
||||
|
||||
export {StudyMetadata};
|
||||
@@ -0,0 +1,11 @@
|
||||
const ReconstructionIssues = {
|
||||
DATASET_4D: 'datasetis4D',
|
||||
VARYING_IMAGESDIMENSIONS: 'imagesdimensionsvarying',
|
||||
VARYING_IMAGESCOMPONENTS: 'imagescomponentsvarying',
|
||||
VARYING_IMAGESORIENTATION: 'imagesorientationvarying',
|
||||
MISSING_FRAMES: 'missingframes',
|
||||
IRREGULAR_SPACING: 'irregularspacing',
|
||||
MULTIFFRAMES: 'multiframe',
|
||||
};
|
||||
|
||||
export {ReconstructionIssues};
|
||||
@@ -260,7 +260,6 @@ class MeasurementService {
|
||||
const sourceInfo = this._getSourceInfo(source);
|
||||
|
||||
if (!definition) {
|
||||
console.log('TEST');
|
||||
log.warn('No source definition provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { api } from 'dicomweb-client';
|
||||
import DICOMWeb from '../../../DICOMWeb/';
|
||||
|
||||
import errorHandler from '../../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook';
|
||||
|
||||
/**
|
||||
* Creates a QIDO date string for a date range query
|
||||
@@ -118,6 +119,7 @@ export default function Studies(server, filter) {
|
||||
url: server.qidoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from './studyInstanceHelpers';
|
||||
|
||||
import errorHandler from '../../../errorHandler';
|
||||
import { getXHRRetryRequestHook } from '../../../utils/xhrRetryRequestHook';
|
||||
|
||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||
|
||||
@@ -76,6 +77,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
url: server.qidoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
});
|
||||
|
||||
this.client = client;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
|
||||
import errorHandler from '../../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook';
|
||||
|
||||
/**
|
||||
* Class for sync load of study metadata.
|
||||
@@ -61,6 +62,7 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
|
||||
url: server.wadoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
});
|
||||
|
||||
this.client = client;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { api } from 'dicomweb-client';
|
||||
import DICOMWeb from '../DICOMWeb';
|
||||
|
||||
import errorHandler from '../errorHandler';
|
||||
import getXHRRetryRequestHook from './xhrRetryRequestHook';
|
||||
|
||||
const getImageId = imageObj => {
|
||||
if (!imageObj) {
|
||||
@@ -66,6 +67,7 @@ const wadorsRetriever = (
|
||||
url,
|
||||
headers,
|
||||
errorInterceptor,
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import isDicomUid from './isDicomUid';
|
||||
import resolveObjectPath from './resolveObjectPath';
|
||||
import * as hierarchicalListUtils from './hierarchicalListUtils';
|
||||
import * as progressTrackingUtils from './progressTrackingUtils';
|
||||
import xhrRetryRequestHook from './xhrRetryRequestHook';
|
||||
|
||||
const utils = {
|
||||
guid,
|
||||
@@ -40,6 +41,7 @@ const utils = {
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
xhrRetryRequestHook,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -63,6 +65,7 @@ export {
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
xhrRetryRequestHook,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
@@ -23,6 +23,7 @@ describe('Top level exports', () => {
|
||||
'resolveObjectPath',
|
||||
'hierarchicalListUtils',
|
||||
'progressTrackingUtils',
|
||||
'xhrRetryRequestHook',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(utils.default).sort();
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { ReconstructionIssues } from './../enums.js';
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
*
|
||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {Object} value, reconstructionIssues.
|
||||
*/
|
||||
export default function isDisplaySetReconstructable(instances) {
|
||||
function isDisplaySetReconstructable(instances) {
|
||||
if (!instances.length) {
|
||||
return { value: false };
|
||||
}
|
||||
@@ -23,31 +28,45 @@ export default function isDisplaySetReconstructable(instances) {
|
||||
}
|
||||
|
||||
if (isMultiframe) {
|
||||
return processMultiframe(instances[0]);
|
||||
return processMultiframe();
|
||||
} else {
|
||||
return processSingleframe(instances);
|
||||
}
|
||||
}
|
||||
|
||||
function processMultiframe(instance) {
|
||||
//TODO: deal with multriframe checks! return false for now as can't reconstruct.
|
||||
return { value: false };
|
||||
/**
|
||||
* Process reconstructable multiframes checks
|
||||
* TODO: deal with multriframe checks! return false for now as can't reconstruct.
|
||||
* *
|
||||
* @returns {Object} value and reconstructionIssues.
|
||||
*/
|
||||
function processMultiframe() {
|
||||
const reconstructionIssues = [ReconstructionIssues.MULTIFRAMES];
|
||||
return { value: false, reconstructionIssues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process reconstructable single frame checks
|
||||
*
|
||||
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {Object} value and reconstructionIssues.
|
||||
*/
|
||||
function processSingleframe(instances) {
|
||||
const n = instances.length;
|
||||
const firstImage = instances[0].getData().metadata;
|
||||
const firstImageRows = firstImage.Rows;
|
||||
const firstImageColumns = firstImage.Columns;
|
||||
const firstImageSamplesPerPixel = firstImage.SamplesPerPixel;
|
||||
const firstImageOrientationPatient = firstImage.ImageOrientationPatient;
|
||||
const firstImagePositionPatient = firstImage.ImagePositionPatient;
|
||||
|
||||
const reconstructionIssues = [];
|
||||
// Can't reconstruct if we:
|
||||
// -- Have a different dimensions within a displaySet.
|
||||
// -- Have a different number of components within a displaySet.
|
||||
// -- Have different orientations within a displaySet.
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i].getData().metadata;
|
||||
for (let ii = 1; ii < n; ++ii) {
|
||||
const instance = instances[ii].getData().metadata;
|
||||
const {
|
||||
Rows,
|
||||
Columns,
|
||||
@@ -55,67 +74,146 @@ function processSingleframe(instances) {
|
||||
ImageOrientationPatient,
|
||||
} = instance;
|
||||
|
||||
if (
|
||||
Rows !== firstImageRows ||
|
||||
Columns !== firstImageColumns ||
|
||||
SamplesPerPixel !== firstImageSamplesPerPixel ||
|
||||
!_isSameOrientation(ImageOrientationPatient, firstImageOrientationPatient)
|
||||
) {
|
||||
return { value: false };
|
||||
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS);
|
||||
} else if (SamplesPerPixel !== firstImageSamplesPerPixel) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS);
|
||||
} else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION);
|
||||
}
|
||||
|
||||
if (reconstructionIssues.length !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// check if dataset is 4D
|
||||
if (_isDataset4D(instances)) {
|
||||
reconstructionIssues.push(ReconstructionIssues.DATASET_4D);
|
||||
}
|
||||
|
||||
return { value: reconstructionIssues.length === 0 ? true : false, reconstructionIssues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is the spacing is uniform.
|
||||
* The input metadata array has to be ordered by image position.
|
||||
*
|
||||
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
|
||||
* @param {boolean} is the dataset 4D.
|
||||
*
|
||||
* @returns {Object} isUniform, reconstructionIssues and missingFrames
|
||||
*/
|
||||
function isSpacingUniform(instances, datasetIs4D) {
|
||||
const n = instances.length;
|
||||
const firstImage = instances[0].getData().metadata;
|
||||
const firstImagePositionPatient = firstImage.ImagePositionPatient;
|
||||
|
||||
const reconstructionIssues = [];
|
||||
let missingFrames = 0;
|
||||
|
||||
// Check if frame spacing is approximately equal within a spacingTolerance.
|
||||
// If spacing is on a uniform grid but we are missing frames,
|
||||
// Allow reconstruction, but pass back the number of missing frames.
|
||||
if (instances.length > 2) {
|
||||
const lastIpp = instances[instances.length - 1].getData().metadata
|
||||
if (n > 2) {
|
||||
const lastIpp = instances[n - 1].getData().metadata
|
||||
.ImagePositionPatient;
|
||||
|
||||
// We can't reconstruct if we are missing ImagePositionPatient values
|
||||
if (!firstImagePositionPatient || !lastIpp) {
|
||||
return { value: false };
|
||||
}
|
||||
if (firstImagePositionPatient && lastIpp) {
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(n - 1);
|
||||
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(instances.length - 1);
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
for (let ii = 1; ii < n; ++ii) {
|
||||
const instance = instances[ii].getData().metadata;
|
||||
const { ImagePositionPatient } = instance;
|
||||
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i].getData().metadata;
|
||||
const { ImagePositionPatient } = instance;
|
||||
const spacingBetweenFrames = _getPerpendicularDistance(
|
||||
ImagePositionPatient,
|
||||
previousImagePositionPatient
|
||||
);
|
||||
|
||||
const spacingBetweenFrames = _getPerpendicularDistance(
|
||||
ImagePositionPatient,
|
||||
previousImagePositionPatient
|
||||
);
|
||||
const spacingIssue = _getSpacingIssue(
|
||||
spacingBetweenFrames,
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
|
||||
if (spacingIssue) {
|
||||
const issue = spacingIssue.issue;
|
||||
|
||||
if (issue === reconstructionIssues.MISSING_FRAMES) {
|
||||
missingFrames += spacingIssue.missingFrames;
|
||||
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
|
||||
return { value: false };
|
||||
if (datasetIs4D && spacingBetweenFrames < 1.e-3) {
|
||||
// the dataset is 4D, if the distance is zero, means that we are
|
||||
// checking the 4th dimension. Do not return, since we want still to
|
||||
// check the 3rd dimension spacing.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
previousImagePositionPatient = ImagePositionPatient;
|
||||
const spacingIssue = _getSpacingIssue(
|
||||
spacingBetweenFrames,
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
|
||||
if (spacingIssue) {
|
||||
const issue = spacingIssue.issue;
|
||||
|
||||
if (issue === ReconstructionIssues.MISSING_FRAMES) {
|
||||
missingFrames += spacingIssue.missingFrames;
|
||||
} else if (issue === ReconstructionIssues.IRREGULAR_SPACING) {
|
||||
reconstructionIssues.push(issue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
previousImagePositionPatient = ImagePositionPatient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { value: true, missingFrames };
|
||||
return { isUniform: reconstructionIssues.length === 0 ? true : false, missingFrames, reconstructionIssues };
|
||||
}
|
||||
|
||||
function _isSameOrientation(iop1, iop2) {
|
||||
|
||||
/**
|
||||
* Check if 4D dataset.
|
||||
*
|
||||
* Assuming that slices at different time have the same position, here we just check if
|
||||
* there are multiple slices for the same ImagePositionPatient and disable MPR.
|
||||
*
|
||||
* A better heuristic would be checking 4D tags, e.g. the presence of multiple TemporalPositionIdentifier values.
|
||||
* However, some studies (e.g. https://github.com/OHIF/Viewers/issues/2113) do not have such tags.
|
||||
*
|
||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {boolean} dataset4D value.
|
||||
*/
|
||||
function _isDataset4D(instances) {
|
||||
const n = instances.length;
|
||||
for (let ii = 0; ii < n; ++ii) {
|
||||
const instanceMetadataControl = instances[ii].getData().metadata;
|
||||
if (
|
||||
!instanceMetadataControl ||
|
||||
instanceMetadataControl === undefined ||
|
||||
!instanceMetadataControl.ImagePositionPatient ||
|
||||
instanceMetadataControl.ImagePositionPatient === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (let jj = ii + 1; jj < n; ++jj) {
|
||||
const instanceMetadata = instances[jj].getData().metadata;
|
||||
if (
|
||||
!instanceMetadata ||
|
||||
instanceMetadata === undefined ||
|
||||
!instanceMetadata.ImagePositionPatient ||
|
||||
instanceMetadata.ImagePositionPatient === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_isSameArray(instanceMetadataControl.ImagePositionPatient, instanceMetadata.ImagePositionPatient)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _isSameArray(iop1, iop2) {
|
||||
if (iop1 === undefined || !iop2 === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -156,12 +254,12 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
||||
|
||||
if (errorForEachSpacing < spacingTolerance * averageSpacing) {
|
||||
return {
|
||||
issue: reconstructionIssues.MISSING_FRAMES,
|
||||
issue: ReconstructionIssues.MISSING_FRAMES,
|
||||
missingFrames: numberOfSpacings - 1,
|
||||
};
|
||||
}
|
||||
|
||||
return { issue: reconstructionIssues.IRREGULAR_SPACING };
|
||||
return { issue: ReconstructionIssues.IRREGULAR_SPACING };
|
||||
}
|
||||
|
||||
function _getPerpendicularDistance(a, b) {
|
||||
@@ -173,7 +271,5 @@ function _getPerpendicularDistance(a, b) {
|
||||
}
|
||||
|
||||
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
|
||||
const reconstructionIssues = {
|
||||
MISSING_FRAMES: 'missingframes',
|
||||
IRREGULAR_SPACING: 'irregularspacing',
|
||||
};
|
||||
|
||||
export {isDisplaySetReconstructable, isSpacingUniform};
|
||||
@@ -45,9 +45,11 @@ import studyMetadataManager from './studyMetadataManager';
|
||||
* @param {string} referencedDisplaySet.studyInstanceUid
|
||||
* @param {Array} referencedDisplaySet.sopClassUids
|
||||
* @param {Study[]} studies Collection of studies
|
||||
* @param {object} logger
|
||||
* @param {object} snackbar
|
||||
* @returns void
|
||||
*/
|
||||
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
||||
async function loadAndCacheDerivedDisplaySets(referencedDisplaySet, studies, logger, snackbar) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||
const promises = [];
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
@@ -78,11 +80,10 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
||||
});
|
||||
|
||||
// For each type, see if any are loaded, if not load the most recent.
|
||||
Object.keys(displaySetsPerModality).forEach(key => {
|
||||
await Promise.all(Object.keys(displaySetsPerModality).map(async (key) => {
|
||||
const displaySets = displaySetsPerModality[key];
|
||||
|
||||
const isLoaded = displaySets.some(displaySet => displaySet.isLoaded);
|
||||
|
||||
if (isLoaded) {
|
||||
return;
|
||||
}
|
||||
@@ -105,22 +106,34 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
||||
}
|
||||
});
|
||||
|
||||
recentDisplaySet.isLoading = true;
|
||||
try {
|
||||
if (recentDisplaySet.hasOwnProperty('getSourceDisplaySet') &&
|
||||
typeof recentDisplaySet.getSourceDisplaySet === 'function') {
|
||||
await recentDisplaySet.getSourceDisplaySet(studies);
|
||||
} else {
|
||||
await recentDisplaySet.load(referencedDisplaySet, studies);
|
||||
}
|
||||
} catch (error) {
|
||||
recentDisplaySet.isLoaded = false;
|
||||
recentDisplaySet.loadError = true;
|
||||
logger.error({ error, message: error.message });
|
||||
snackbar.show({
|
||||
title: 'Error loading derived display set:',
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
error,
|
||||
autoClose: false,
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
promises.push(recentDisplaySet.load(referencedDisplaySet, studies));
|
||||
});
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app
|
||||
* that depends on derived display sets to be loaded.
|
||||
* (Implement pubsub for better tracking of derived display sets)
|
||||
*/
|
||||
const event = new CustomEvent('deriveddisplaysetsloadedandcached');
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
|
||||
return promises;
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app
|
||||
* that depends on derived display sets to be loaded.
|
||||
* (Implement pubsub for better tracking of derived display sets)
|
||||
*/
|
||||
const event = new CustomEvent('deriveddisplaysetsloadedandcached');
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
export default loadAndCacheDerivedDisplaySets;
|
||||
@@ -4,6 +4,7 @@ import str2ab from '../str2ab';
|
||||
import unpackOverlay from './unpackOverlay';
|
||||
|
||||
import errorHandler from '../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../xhrRetryRequestHook';
|
||||
|
||||
export default async function fetchOverlayData(instance, server) {
|
||||
const OverlayDataPromises = [];
|
||||
@@ -69,6 +70,7 @@ async function _getOverlayData(tag, server) {
|
||||
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
|
||||
@@ -3,6 +3,7 @@ import DICOMWeb from '../../DICOMWeb';
|
||||
import str2ab from '../str2ab';
|
||||
|
||||
import errorHandler from '../../errorHandler';
|
||||
import getXHRRetryRequestHook from '../xhrRetryRequestHook';
|
||||
|
||||
export default async function fetchPaletteColorLookupTableData(
|
||||
instance,
|
||||
@@ -141,6 +142,7 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) {
|
||||
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const validNumber = val => {
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(v => (v !== undefined ? Number(v) : v));
|
||||
} else {
|
||||
return val !== undefined ? Number(val) : val;
|
||||
}
|
||||
};
|
||||
|
||||
export default validNumber;
|
||||
@@ -0,0 +1,103 @@
|
||||
import retry from 'retry';
|
||||
|
||||
const defaultRetryOptions = {
|
||||
retries: 5,
|
||||
factor: 3,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: 60 * 1000,
|
||||
randomize: true,
|
||||
retryableStatusCodes: [429, 500],
|
||||
};
|
||||
|
||||
let retryOptions = { ...defaultRetryOptions };
|
||||
|
||||
/**
|
||||
* Request hook used to add retry functionality to XHR requests.
|
||||
*
|
||||
* @param {XMLHttpRequest} request XHR request instance
|
||||
* @param {object} metadata Metadata about the request
|
||||
* @param {object} metadata.url URL
|
||||
* @param {object} metadata.method HTTP method
|
||||
* @returns {XMLHttpRequest} request instance optionally modified
|
||||
*/
|
||||
const xhrRetryRequestHook = (request, metadata) => {
|
||||
const { url, method } = metadata;
|
||||
|
||||
function faultTolerantRequestSend(...args) {
|
||||
const operation = retry.operation(retryOptions);
|
||||
|
||||
operation.attempt(function operationAttempt(currentAttempt) {
|
||||
const originalOnReadyStateChange = request.onreadystatechange;
|
||||
|
||||
/** Overriding/extending XHR function */
|
||||
request.onreadystatechange = function onReadyStateChange(...args) {
|
||||
originalOnReadyStateChange.apply(request, args);
|
||||
|
||||
if (retryOptions.retryableStatusCodes.includes(request.status)) {
|
||||
const errorMessage = `Attempt to request ${url} failed.`;
|
||||
const attemptFailedError = new Error(errorMessage);
|
||||
operation.retry(attemptFailedError);
|
||||
}
|
||||
};
|
||||
|
||||
/** Call open only on retry (after headers and other things were set in the xhr instance) */
|
||||
if (currentAttempt > 1) {
|
||||
console.warn(`Requesting ${url}... (attempt: ${currentAttempt})`);
|
||||
request.open(method, url, true);
|
||||
}
|
||||
});
|
||||
|
||||
originalRequestSend.apply(request, args);
|
||||
}
|
||||
|
||||
/** Overriding/extending XHR function */
|
||||
const originalRequestSend = request.send;
|
||||
request.send = faultTolerantRequestSend;
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a configured retry request hook function
|
||||
* that can be used to add retry functionality to XHR request.
|
||||
*
|
||||
* Default options:
|
||||
* retries: 5
|
||||
* factor: 3
|
||||
* minTimeout: 1 * 1000
|
||||
* maxTimeout: 60 * 1000
|
||||
* randomize: true
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {number} options.retires number of retries
|
||||
* @param {number} options.factor factor
|
||||
* @param {number} options.minTimeout the min timeout
|
||||
* @param {number} options.maxTimeout the max timeout
|
||||
* @param {boolean} options.randomize randomize
|
||||
* @param {array} options.retryableStatusCodes status codes that can trigger retry
|
||||
* @returns {function} the configured retry request function
|
||||
*/
|
||||
export const getXHRRetryRequestHook = (options = {}) => {
|
||||
retryOptions = { ...defaultRetryOptions };
|
||||
if ('retries' in options) {
|
||||
retryOptions.retries = options.retries;
|
||||
}
|
||||
if ('factor' in options) {
|
||||
retryOptions.factor = options.factor;
|
||||
}
|
||||
if ('minTimeout' in options) {
|
||||
retryOptions.minTimeout = options.minTimeout;
|
||||
}
|
||||
if ('maxTimeout' in options) {
|
||||
retryOptions.maxTimeout = options.maxTimeout;
|
||||
}
|
||||
if ('randomize' in options) {
|
||||
retryOptions.randomize = options.randomize;
|
||||
}
|
||||
if ('retryableStatusCodes' in options) {
|
||||
retryOptions.retryableStatusCodes = options.retryableStatusCodes;
|
||||
}
|
||||
return xhrRetryRequestHook;
|
||||
};
|
||||
|
||||
export default getXHRRetryRequestHook;
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.9.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.2...@ohif/ui@1.9.3) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.1...@ohif/ui@1.9.2) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.9.0...@ohif/ui@1.9.1) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.8.2...@ohif/ui@1.9.0) (2021-03-04)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "1.9.0",
|
||||
"version": "1.9.3",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,6 +4,29 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .badgeNumber-container {
|
||||
background-color: var(--ui-border-color-dark);
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
bottom: 10px;
|
||||
left: 5px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .badgeNumber {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
@@ -36,7 +59,12 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper .roundedButton svg .roundedButtonWrapper .roundedButton span {
|
||||
.roundedButtonWrapper
|
||||
.roundedButton
|
||||
svg
|
||||
.roundedButtonWrapper
|
||||
.roundedButton
|
||||
span {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class RoundedButtonGroup extends Component {
|
||||
PropTypes.shape({
|
||||
value: PropTypes.any,
|
||||
label: PropTypes.string,
|
||||
stateEvent: PropTypes.string,
|
||||
icon: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
@@ -30,6 +31,15 @@ class RoundedButtonGroup extends Component {
|
||||
value: null,
|
||||
};
|
||||
|
||||
state = {
|
||||
badgeNumbers: [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.onStateEvent = this.onStateEvent.bind(this);
|
||||
}
|
||||
|
||||
onClickOption = value => {
|
||||
let newValue = value;
|
||||
if (this.props.value === value) {
|
||||
@@ -41,6 +51,48 @@ class RoundedButtonGroup extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
onStateEvent(event) {
|
||||
const optionIndex = this.props.options.findIndex(
|
||||
o => o.value === event.detail.target
|
||||
);
|
||||
if (optionIndex > -1) {
|
||||
const badgeNumbers = this.state.badgeNumbers;
|
||||
badgeNumbers[optionIndex] = event.detail.badgeNumber;
|
||||
this.setState({ badgeNumbers });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.options.forEach(option => {
|
||||
if (option.stateEvent) {
|
||||
document.addEventListener(option.stateEvent, this.onStateEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.props.options.forEach((option, index) => {
|
||||
if (
|
||||
option.stateEvent &&
|
||||
option.stateEvent !==
|
||||
(prevProps.options[index]
|
||||
? prevProps.options[index].stateEvent
|
||||
: null)
|
||||
) {
|
||||
document.removeEventListener(option.stateEvent, this.onStateEvent);
|
||||
document.addEventListener(option.stateEvent, this.onStateEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.options.forEach(option => {
|
||||
if (option.stateEvent) {
|
||||
document.removeEventListener(option.stateEvent, this.onStateEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = classnames(
|
||||
RoundedButtonGroup.className,
|
||||
@@ -62,6 +114,14 @@ class RoundedButtonGroup extends Component {
|
||||
<div className="bottomLabel">{option.bottomLabel}</div>
|
||||
);
|
||||
|
||||
let badgeNumber = this.state.badgeNumbers[index];
|
||||
const badgeNumberOverflow = String(badgeNumber).length > 2;
|
||||
badgeNumber = badgeNumber
|
||||
? badgeNumberOverflow
|
||||
? 99
|
||||
: badgeNumber
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
@@ -70,6 +130,14 @@ class RoundedButtonGroup extends Component {
|
||||
>
|
||||
<div className="roundedButton">
|
||||
{optionText}
|
||||
{badgeNumber && (
|
||||
<div className="badgeNumber-container">
|
||||
<span className="badgeNumber">
|
||||
{badgeNumber}
|
||||
{badgeNumberOverflow && '+'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{iconProps && <Icon {...iconProps} />}
|
||||
</div>
|
||||
{bottomLabel}
|
||||
|
||||
@@ -3,6 +3,7 @@ import './ImageThumbnail.styl';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
import React, { useState, useEffect, createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator';
|
||||
@@ -15,6 +16,7 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
|
||||
//import cornerstone from 'cornerstone-core';
|
||||
function ImageThumbnail(props) {
|
||||
const {
|
||||
active,
|
||||
width,
|
||||
height,
|
||||
imageSrc,
|
||||
@@ -97,7 +99,7 @@ function ImageThumbnail(props) {
|
||||
}, [fetchImagePromise, image.imageId, imageId, purgeCancelablePromise, setImagePromise]);
|
||||
|
||||
return (
|
||||
<div className="ImageThumbnail">
|
||||
<div className={classNames('ImageThumbnail', { active: active })}>
|
||||
<div className="image-thumbnail-canvas">
|
||||
{shouldRenderToCanvas() ? (
|
||||
<canvas ref={canvasRef} width={width} height={height} />
|
||||
@@ -126,6 +128,7 @@ function ImageThumbnail(props) {
|
||||
}
|
||||
|
||||
ImageThumbnail.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
imageSrc: PropTypes.string,
|
||||
imageId: PropTypes.string,
|
||||
error: PropTypes.bool,
|
||||
@@ -135,6 +138,7 @@ ImageThumbnail.propTypes = {
|
||||
};
|
||||
|
||||
ImageThumbnail.defaultProps = {
|
||||
active: false,
|
||||
error: false,
|
||||
stackPercentComplete: 0,
|
||||
width: 217,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
--sidebar-transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.ThumbnailEntry.active .ImageThumbnail
|
||||
.ImageThumbnail.active
|
||||
border-color: var(--active-color);
|
||||
box-shadow: none
|
||||
transition: var(--sidebar-transition);
|
||||
@@ -14,7 +14,7 @@
|
||||
.ImageThumbnail
|
||||
background-color: var(--primary-background-color);
|
||||
box-shadow: inset 0 0 0 1px var(--ui-border-color-dark);
|
||||
border: 5px solid transparent
|
||||
border: 2px solid transparent
|
||||
border-radius: 12px
|
||||
height: 135px
|
||||
margin: 0 auto
|
||||
|
||||
@@ -20,6 +20,7 @@ function StudyBrowser(props) {
|
||||
return study.thumbnails.map((thumb, thumbIndex) => {
|
||||
// TODO: Thumb has more props than we care about?
|
||||
const {
|
||||
active,
|
||||
altImageText,
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
@@ -28,6 +29,7 @@ function StudyBrowser(props) {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
stackPercentComplete,
|
||||
hasWarnings,
|
||||
} = thumb;
|
||||
|
||||
return (
|
||||
@@ -37,6 +39,7 @@ function StudyBrowser(props) {
|
||||
data-cy="thumbnail-list"
|
||||
>
|
||||
<Thumbnail
|
||||
active={active}
|
||||
supportsDrag={supportsDrag}
|
||||
key={`${studyIndex}_${thumbIndex}`}
|
||||
id={`${studyIndex}_${thumbIndex}`} // Unused?
|
||||
@@ -50,6 +53,7 @@ function StudyBrowser(props) {
|
||||
numImageFrames={numImageFrames}
|
||||
SeriesDescription={SeriesDescription}
|
||||
SeriesNumber={SeriesNumber}
|
||||
hasWarnings={hasWarnings}
|
||||
stackPercentComplete={stackPercentComplete}
|
||||
// Events
|
||||
onClick={onThumbnailClick.bind(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import ImageThumbnail from './ImageThumbnail';
|
||||
import classNames from 'classnames';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { Tooltip } from './../tooltip';
|
||||
import { OverlayTrigger } from './../overlayTrigger';
|
||||
|
||||
import './Thumbnail.styl';
|
||||
|
||||
@@ -11,7 +14,22 @@ function ThumbnailFooter({
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
hasWarnings
|
||||
}) {
|
||||
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false
|
||||
hasWarnings.then(response => {
|
||||
if (!unmounted) {
|
||||
inconsistencyWarningsSet(response)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
unmounted = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const infoOnly = !SeriesDescription;
|
||||
|
||||
const getInfo = (value, icon, className = '') => {
|
||||
@@ -22,28 +40,73 @@ function ThumbnailFooter({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getWarningContent = (inconsistencyWarnings) => {
|
||||
if (Array.isArray(inconsistencyWarnings)) {
|
||||
const listedWarnings = inconsistencyWarnings.map((warn, index) => {
|
||||
return <li key={index}>{warn}</li>;
|
||||
});
|
||||
|
||||
return <ol>{listedWarnings}</ol>;
|
||||
} else {
|
||||
return <React.Fragment>{inconsistencyWarnings}</React.Fragment>;
|
||||
}
|
||||
};
|
||||
|
||||
const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => {
|
||||
return(
|
||||
<React.Fragment>
|
||||
{inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
|
||||
<OverlayTrigger
|
||||
key={SeriesNumber}
|
||||
placement="left"
|
||||
overlay={
|
||||
<Tooltip
|
||||
placement="left"
|
||||
className="in tooltip-warning"
|
||||
id="tooltip-left"
|
||||
>
|
||||
<div className="warningTitle">Series Inconsistencies</div>
|
||||
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div className={classNames('warning')}>
|
||||
<span className="warning-icon">
|
||||
<Icon name="exclamation-triangle" />
|
||||
</span>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const getSeriesInformation = (
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames
|
||||
numImageFrames,
|
||||
inconsistencyWarnings
|
||||
) => {
|
||||
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
const seriesInformation =
|
||||
<div className="series-information">
|
||||
{getInfo(SeriesNumber, 'S:')}
|
||||
{getInfo(InstanceNumber, 'I:')}
|
||||
{getInfo(numImageFrames, '', 'image-frames')}
|
||||
{getWarningInfo(SeriesNumber, inconsistencyWarnings)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (seriesInformation);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
||||
<div className="series-description">{SeriesDescription}</div>
|
||||
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames)}
|
||||
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, inconsistencyWarnings)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -60,6 +123,7 @@ function Thumbnail(props) {
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
hasWarnings,
|
||||
stackPercentComplete,
|
||||
StudyInstanceUID,
|
||||
onClick,
|
||||
@@ -95,6 +159,7 @@ function Thumbnail(props) {
|
||||
{/* SHOW IMAGE */}
|
||||
{hasImage && (
|
||||
<ImageThumbnail
|
||||
active={active}
|
||||
imageSrc={imageSrc}
|
||||
imageId={imageId}
|
||||
error={error}
|
||||
@@ -126,13 +191,14 @@ Thumbnail.propTypes = {
|
||||
stackPercentComplete: PropTypes.number,
|
||||
/**
|
||||
altImageText will be used when no imageId or imageSrc is provided.
|
||||
It will be displayed inside the <div>. This is useful when it is difficult
|
||||
It will be displayed inside the <div>. This is useful when it is difficult
|
||||
to make a preview for a type of DICOM series (e.g. DICOM-SR)
|
||||
*/
|
||||
altImageText: PropTypes.string,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
hasWarnings: PropTypes.instanceOf(Promise),
|
||||
numImageFrames: PropTypes.number,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
:root {
|
||||
--series-count-background-color: #678696;
|
||||
--active-color: #20A5D6;
|
||||
}
|
||||
|
||||
.thumbnail.active
|
||||
.alt-image-text
|
||||
border-color: var(--active-color);
|
||||
box-shadow: none
|
||||
transition: var(--sidebar-transition);
|
||||
|
||||
.thumbnail
|
||||
cursor: pointer
|
||||
display: table
|
||||
@@ -16,7 +23,7 @@
|
||||
justify-content: center;
|
||||
background-color: var(--primary-background-color);
|
||||
box-shadow: inset 0 0 0 1px var(--ui-border-color-dark);
|
||||
border: 5px solid transparent;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
height: 135px;
|
||||
margin: 0 auto;
|
||||
@@ -78,6 +85,17 @@
|
||||
height: 11px
|
||||
width: 11px
|
||||
|
||||
.warning
|
||||
margin: auto 0;
|
||||
opacity: 1
|
||||
color: #e29e4a;
|
||||
|
||||
svg
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
pointer-events: inherit;
|
||||
|
||||
|
||||
.value
|
||||
color: var(--text-secondary-color);
|
||||
display: inline-block
|
||||
|
||||
@@ -41,6 +41,13 @@ module.exports = (env, argv) => {
|
||||
path: DIST_DIR,
|
||||
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
|
||||
publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
|
||||
devtoolModuleFilenameTemplate: function(info) {
|
||||
if (isProdBuild) {
|
||||
return `webpack:///${info.resourcePath}`;
|
||||
} else {
|
||||
return 'file:///' + encodeURI(info.absoluteResourcePath);
|
||||
}
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [...extractStyleChunksRule(isProdBuild)],
|
||||
|
||||
@@ -3,6 +3,168 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [4.9.24](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.23...@ohif/viewer@4.9.24) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.23](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.22...@ohif/viewer@4.9.23) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MPR metadata parsing for spacings ([#2517](https://github.com/OHIF/Viewers/issues/2517)) ([715dfc5](https://github.com/OHIF/Viewers/commit/715dfc556a3883de31d74012dcc99e431d91f57d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.22](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.21...@ohif/viewer@4.9.22) (2021-08-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove internet dependency for workbox (addressing [#2109](https://github.com/OHIF/Viewers/issues/2109)) ([#2151](https://github.com/OHIF/Viewers/issues/2151)) ([5f92f6e](https://github.com/OHIF/Viewers/commit/5f92f6ec7faa11d3fdc33ef8f22971a0bab87a36))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.21](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.20...@ohif/viewer@4.9.21) (2021-07-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.20](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.19...@ohif/viewer@4.9.20) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.18...@ohif/viewer@4.9.19) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.17...@ohif/viewer@4.9.18) (2021-06-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dicom-html:** Add parsed dicom meta info section ([#2419](https://github.com/OHIF/Viewers/issues/2419)) ([403688b](https://github.com/OHIF/Viewers/commit/403688b18c52468fc1101166ec0c1734fb710039))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.17](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.16...@ohif/viewer@4.9.17) (2021-05-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.15...@ohif/viewer@4.9.16) (2021-05-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.14...@ohif/viewer@4.9.15) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.13...@ohif/viewer@4.9.14) (2021-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.12...@ohif/viewer@4.9.13) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.11...@ohif/viewer@4.9.12) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.10...@ohif/viewer@4.9.11) (2021-04-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.9...@ohif/viewer@4.9.10) (2021-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.8...@ohif/viewer@4.9.9) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.7...@ohif/viewer@4.9.8) (2021-03-31)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.6...@ohif/viewer@4.9.7) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.5...@ohif/viewer@4.9.6) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [4.9.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.4...@ohif/viewer@4.9.5) (2021-03-25)
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('OHIF Cornerstone Hotkeys', () => {
|
||||
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .left-mid.orientation-marker'
|
||||
).as('viewport2InfoMidLeft');
|
||||
cy.get(
|
||||
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOverlay > div.bottom-right.overlay-element > div'
|
||||
':nth-child(2) > .viewport-wrapper > .viewport-element > .OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div'
|
||||
).as('viewport2InfoBottomRight');
|
||||
|
||||
// Press multiples hotkeys on viewport #2
|
||||
|
||||
@@ -741,7 +741,7 @@ describe('OHIF User Preferences', () => {
|
||||
|
||||
// Overlay information from 2nd viewport
|
||||
let second_viewport_overlay =
|
||||
'div:nth-child(2) > div > div.viewport-element > div.ViewportOverlay > div.bottom-right.overlay-element > div';
|
||||
'div:nth-child(2) > div > div.viewport-element > div.OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div';
|
||||
|
||||
// Shift active viewport to Viewport #2
|
||||
cy.get('body').type('{rightarrow}');
|
||||
|
||||
@@ -25,10 +25,10 @@ export function initCommonElementsAliases() {
|
||||
'.pull-left > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton'
|
||||
).as('seriesBtn');
|
||||
cy.get('section.sidepanel.from-left').as('seriesPanel');
|
||||
cy.get('div.ViewportOverlay > div.bottom-left.overlay-element > div').as(
|
||||
cy.get('div.OHIFCornerstoneViewportOverlay > div.bottom-left.overlay-element > div').as(
|
||||
'viewportInfoBottomLeft'
|
||||
);
|
||||
cy.get('div.ViewportOverlay > div.bottom-right.overlay-element > div').as(
|
||||
cy.get('div.OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div').as(
|
||||
'viewportInfoBottomRight'
|
||||
);
|
||||
cy.get('.left-mid.orientation-marker').as('viewportInfoMidLeft');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "4.9.5",
|
||||
"version": "4.9.24",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -49,18 +49,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^2.13.2",
|
||||
"@ohif/extension-cornerstone": "^2.10.5",
|
||||
"@ohif/extension-debugging": "^0.2.1",
|
||||
"@ohif/extension-dicom-html": "^1.3.4",
|
||||
"@ohif/core": "^2.13.15",
|
||||
"@ohif/extension-cornerstone": "^2.10.10",
|
||||
"@ohif/extension-debugging": "^0.2.2",
|
||||
"@ohif/extension-dicom-html": "^1.3.10",
|
||||
"@ohif/extension-dicom-microscopy": "^0.52.0",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.6",
|
||||
"@ohif/extension-dicom-rt": "^0.6.7",
|
||||
"@ohif/extension-dicom-segmentation": "^0.6.1",
|
||||
"@ohif/extension-dicom-rt": "^0.6.14",
|
||||
"@ohif/extension-dicom-segmentation": "^0.6.11",
|
||||
"@ohif/extension-lesion-tracker": "^0.2.1",
|
||||
"@ohif/extension-vtk": "^1.11.3",
|
||||
"@ohif/extension-vtk": "^1.11.16",
|
||||
"@ohif/i18n": "^0.53.0",
|
||||
"@ohif/ui": "^1.9.0",
|
||||
"@ohif/ui": "^1.9.3",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
@@ -68,9 +68,9 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.3",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.4.4",
|
||||
"dicomweb-client": "^0.8.1",
|
||||
"hammerjs": "^2.0.8",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
|
||||
@@ -4,6 +4,7 @@ window.config = {
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
disableServersCache: false,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
@@ -123,6 +124,6 @@ window.config = {
|
||||
// If the server is particularly slow to respond to series metadata
|
||||
// requests as it extracts the metadata from raw files everytime,
|
||||
// try setting this to even lower value
|
||||
// Leave it undefined for no limit, sutiable for HTTP/2 enabled servers
|
||||
// Leave it undefined for no limit, suitable for HTTP/2 enabled servers
|
||||
// maxConcurrentMetadataRequests: 5,
|
||||
};
|
||||
@@ -3,7 +3,17 @@
|
||||
// modules, so it's perfectly fine to serve this code to any browsers
|
||||
// (older browsers will just ignore it)
|
||||
//
|
||||
import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0-beta.1/workbox-window.prod.mjs';
|
||||
|
||||
// The workbox code is locally stored and can be updated
|
||||
// by running `npx workbox-cli copyLibraries platform/viewer/public/third_party/workbox`
|
||||
// from the root directory.
|
||||
// Since the workbox version may change with copyLibraries ensure that the
|
||||
// right version is used here and in `service-worker.js`
|
||||
import { Workbox } from './third_party/workbox/workbox-v5.1.4/workbox-window.dev.mjs';
|
||||
|
||||
// Alternatively use the internet version by commenting out the above import and
|
||||
// replace with this line and do the corresponding change in service-worker.js
|
||||
// import { Workbox } from 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0-beta.1/workbox-window.prod.mjs';
|
||||
|
||||
var supportsServiceWorker = 'serviceWorker' in navigator;
|
||||
var isNotLocalDevelopment =
|
||||
@@ -11,6 +21,7 @@ var isNotLocalDevelopment =
|
||||
|
||||
if (supportsServiceWorker && isNotLocalDevelopment) {
|
||||
const swFileLocation = (window.PUBLIC_URL || '/') + 'sw.js';
|
||||
|
||||
const wb = new Workbox(swFileLocation);
|
||||
|
||||
// Add an event listener to detect when the registered
|
||||
|
||||
+818
@@ -0,0 +1,818 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.backgroundSync = (function (exports, WorkboxError_js, logger_js, assert_js, getFriendlyURL_js, DBWrapper_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:background-sync:5.1.4'] && _();
|
||||
} 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.
|
||||
*/
|
||||
const DB_VERSION = 3;
|
||||
const DB_NAME = 'workbox-background-sync';
|
||||
const OBJECT_STORE_NAME = 'requests';
|
||||
const INDEXED_PROP = 'queueName';
|
||||
/**
|
||||
* A class to manage storing requests from a Queue in IndexedDB,
|
||||
* indexed by their queue name for easier access.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
class QueueStore {
|
||||
/**
|
||||
* Associates this instance with a Queue instance, so entries added can be
|
||||
* identified by their queue name.
|
||||
*
|
||||
* @param {string} queueName
|
||||
* @private
|
||||
*/
|
||||
constructor(queueName) {
|
||||
this._queueName = queueName;
|
||||
this._db = new DBWrapper_js.DBWrapper(DB_NAME, DB_VERSION, {
|
||||
onupgradeneeded: this._upgradeDb
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Append an entry last in the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Object} entry.requestData
|
||||
* @param {number} [entry.timestamp]
|
||||
* @param {Object} [entry.metadata]
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async pushEntry(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'pushEntry',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isType(entry.requestData, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'pushEntry',
|
||||
paramName: 'entry.requestData'
|
||||
});
|
||||
} // Don't specify an ID since one is automatically generated.
|
||||
|
||||
|
||||
delete entry.id;
|
||||
entry.queueName = this._queueName;
|
||||
await this._db.add(OBJECT_STORE_NAME, entry);
|
||||
}
|
||||
/**
|
||||
* Prepend an entry first in the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Object} entry.requestData
|
||||
* @param {number} [entry.timestamp]
|
||||
* @param {Object} [entry.metadata]
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async unshiftEntry(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'unshiftEntry',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isType(entry.requestData, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'QueueStore',
|
||||
funcName: 'unshiftEntry',
|
||||
paramName: 'entry.requestData'
|
||||
});
|
||||
}
|
||||
|
||||
const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {
|
||||
count: 1
|
||||
});
|
||||
|
||||
if (firstEntry) {
|
||||
// Pick an ID one less than the lowest ID in the object store.
|
||||
entry.id = firstEntry.id - 1;
|
||||
} else {
|
||||
// Otherwise let the auto-incrementor assign the ID.
|
||||
delete entry.id;
|
||||
}
|
||||
|
||||
entry.queueName = this._queueName;
|
||||
await this._db.add(OBJECT_STORE_NAME, entry);
|
||||
}
|
||||
/**
|
||||
* Removes and returns the last entry in the queue matching the `queueName`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async popEntry() {
|
||||
return this._removeEntry({
|
||||
direction: 'prev'
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first entry in the queue matching the `queueName`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async shiftEntry() {
|
||||
return this._removeEntry({
|
||||
direction: 'next'
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Returns all entries in the store matching the `queueName`.
|
||||
*
|
||||
* @param {Object} options See {@link module:workbox-background-sync.Queue~getAll}
|
||||
* @return {Promise<Array<Object>>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async getAll() {
|
||||
return await this._db.getAllMatching(OBJECT_STORE_NAME, {
|
||||
index: INDEXED_PROP,
|
||||
query: IDBKeyRange.only(this._queueName)
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Deletes the entry for the given ID.
|
||||
*
|
||||
* WARNING: this method does not ensure the deleted enry belongs to this
|
||||
* queue (i.e. matches the `queueName`). But this limitation is acceptable
|
||||
* as this class is not publicly exposed. An additional check would make
|
||||
* this method slower than it needs to be.
|
||||
*
|
||||
* @private
|
||||
* @param {number} id
|
||||
*/
|
||||
|
||||
|
||||
async deleteEntry(id) {
|
||||
await this._db.delete(OBJECT_STORE_NAME, id);
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first or last entry in the queue (based on the
|
||||
* `direction` argument) matching the `queueName`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async _removeEntry({
|
||||
direction
|
||||
}) {
|
||||
const [entry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {
|
||||
direction,
|
||||
index: INDEXED_PROP,
|
||||
query: IDBKeyRange.only(this._queueName),
|
||||
count: 1
|
||||
});
|
||||
|
||||
if (entry) {
|
||||
await this.deleteEntry(entry.id);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Upgrades the database given an `upgradeneeded` event.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_upgradeDb(event) {
|
||||
const db = event.target.result;
|
||||
|
||||
if (event.oldVersion > 0 && event.oldVersion < DB_VERSION) {
|
||||
if (db.objectStoreNames.contains(OBJECT_STORE_NAME)) {
|
||||
db.deleteObjectStore(OBJECT_STORE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
const objStore = db.createObjectStore(OBJECT_STORE_NAME, {
|
||||
autoIncrement: true,
|
||||
keyPath: 'id'
|
||||
});
|
||||
objStore.createIndex(INDEXED_PROP, INDEXED_PROP, {
|
||||
unique: false
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
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 serializableProperties = ['method', 'referrer', 'referrerPolicy', 'mode', 'credentials', 'cache', 'redirect', 'integrity', 'keepalive'];
|
||||
/**
|
||||
* A class to make it easier to serialize and de-serialize requests so they
|
||||
* can be stored in IndexedDB.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
class StorableRequest {
|
||||
/**
|
||||
* Accepts an object of request data that can be used to construct a
|
||||
* `Request` but can also be stored in IndexedDB.
|
||||
*
|
||||
* @param {Object} requestData An object of request data that includes the
|
||||
* `url` plus any relevant properties of
|
||||
* [requestInit]{@link https://fetch.spec.whatwg.org/#requestinit}.
|
||||
* @private
|
||||
*/
|
||||
constructor(requestData) {
|
||||
{
|
||||
assert_js.assert.isType(requestData, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'StorableRequest',
|
||||
funcName: 'constructor',
|
||||
paramName: 'requestData'
|
||||
});
|
||||
assert_js.assert.isType(requestData.url, 'string', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'StorableRequest',
|
||||
funcName: 'constructor',
|
||||
paramName: 'requestData.url'
|
||||
});
|
||||
} // If the request's mode is `navigate`, convert it to `same-origin` since
|
||||
// navigation requests can't be constructed via script.
|
||||
|
||||
|
||||
if (requestData['mode'] === 'navigate') {
|
||||
requestData['mode'] = 'same-origin';
|
||||
}
|
||||
|
||||
this._requestData = requestData;
|
||||
}
|
||||
/**
|
||||
* Converts a Request object to a plain object that can be structured
|
||||
* cloned or JSON-stringified.
|
||||
*
|
||||
* @param {Request} request
|
||||
* @return {Promise<StorableRequest>}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
static async fromRequest(request) {
|
||||
const requestData = {
|
||||
url: request.url,
|
||||
headers: {}
|
||||
}; // Set the body if present.
|
||||
|
||||
if (request.method !== 'GET') {
|
||||
// Use ArrayBuffer to support non-text request bodies.
|
||||
// NOTE: we can't use Blobs becuse Safari doesn't support storing
|
||||
// Blobs in IndexedDB in some cases:
|
||||
// https://github.com/dfahlander/Dexie.js/issues/618#issuecomment-398348457
|
||||
requestData.body = await request.clone().arrayBuffer();
|
||||
} // Convert the headers from an iterable to an object.
|
||||
|
||||
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
requestData.headers[key] = value;
|
||||
} // Add all other serializable request properties
|
||||
|
||||
|
||||
for (const prop of serializableProperties) {
|
||||
if (request[prop] !== undefined) {
|
||||
requestData[prop] = request[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return new StorableRequest(requestData);
|
||||
}
|
||||
/**
|
||||
* Returns a deep clone of the instances `_requestData` object.
|
||||
*
|
||||
* @return {Object}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
toObject() {
|
||||
const requestData = Object.assign({}, this._requestData);
|
||||
requestData.headers = Object.assign({}, this._requestData.headers);
|
||||
|
||||
if (requestData.body) {
|
||||
requestData.body = requestData.body.slice(0);
|
||||
}
|
||||
|
||||
return requestData;
|
||||
}
|
||||
/**
|
||||
* Converts this instance to a Request.
|
||||
*
|
||||
* @return {Request}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
toRequest() {
|
||||
return new Request(this._requestData.url, this._requestData);
|
||||
}
|
||||
/**
|
||||
* Creates and returns a deep clone of the instance.
|
||||
*
|
||||
* @return {StorableRequest}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
clone() {
|
||||
return new StorableRequest(this.toObject());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
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 TAG_PREFIX = 'workbox-background-sync';
|
||||
const MAX_RETENTION_TIME = 60 * 24 * 7; // 7 days in minutes
|
||||
|
||||
const queueNames = new Set();
|
||||
/**
|
||||
* Converts a QueueStore entry into the format exposed by Queue. This entails
|
||||
* converting the request data into a real request and omitting the `id` and
|
||||
* `queueName` properties.
|
||||
*
|
||||
* @param {Object} queueStoreEntry
|
||||
* @return {Object}
|
||||
* @private
|
||||
*/
|
||||
|
||||
const convertEntry = queueStoreEntry => {
|
||||
const queueEntry = {
|
||||
request: new StorableRequest(queueStoreEntry.requestData).toRequest(),
|
||||
timestamp: queueStoreEntry.timestamp
|
||||
};
|
||||
|
||||
if (queueStoreEntry.metadata) {
|
||||
queueEntry.metadata = queueStoreEntry.metadata;
|
||||
}
|
||||
|
||||
return queueEntry;
|
||||
};
|
||||
/**
|
||||
* A class to manage storing failed requests in IndexedDB and retrying them
|
||||
* later. All parts of the storing and replaying process are observable via
|
||||
* callbacks.
|
||||
*
|
||||
* @memberof module:workbox-background-sync
|
||||
*/
|
||||
|
||||
|
||||
class Queue {
|
||||
/**
|
||||
* Creates an instance of Queue with the given options
|
||||
*
|
||||
* @param {string} name The unique name for this queue. This name must be
|
||||
* unique as it's used to register sync events and store requests
|
||||
* in IndexedDB specific to this instance. An error will be thrown if
|
||||
* a duplicate name is detected.
|
||||
* @param {Object} [options]
|
||||
* @param {Function} [options.onSync] A function that gets invoked whenever
|
||||
* the 'sync' event fires. The function is invoked with an object
|
||||
* containing the `queue` property (referencing this instance), and you
|
||||
* can use the callback to customize the replay behavior of the queue.
|
||||
* When not set the `replayRequests()` method is called.
|
||||
* Note: if the replay fails after a sync event, make sure you throw an
|
||||
* error, so the browser knows to retry the sync event later.
|
||||
* @param {number} [options.maxRetentionTime=7 days] The amount of time (in
|
||||
* minutes) a request may be retried. After this amount of time has
|
||||
* passed, the request will be deleted from the queue.
|
||||
*/
|
||||
constructor(name, {
|
||||
onSync,
|
||||
maxRetentionTime
|
||||
} = {}) {
|
||||
this._syncInProgress = false;
|
||||
this._requestsAddedDuringSync = false; // Ensure the store name is not already being used
|
||||
|
||||
if (queueNames.has(name)) {
|
||||
throw new WorkboxError_js.WorkboxError('duplicate-queue-name', {
|
||||
name
|
||||
});
|
||||
} else {
|
||||
queueNames.add(name);
|
||||
}
|
||||
|
||||
this._name = name;
|
||||
this._onSync = onSync || this.replayRequests;
|
||||
this._maxRetentionTime = maxRetentionTime || MAX_RETENTION_TIME;
|
||||
this._queueStore = new QueueStore(this._name);
|
||||
|
||||
this._addSyncListener();
|
||||
}
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
|
||||
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
/**
|
||||
* Stores the passed request in IndexedDB (with its timestamp and any
|
||||
* metadata) at the end of the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Request} entry.request The request to store in the queue.
|
||||
* @param {Object} [entry.metadata] Any metadata you want associated with the
|
||||
* stored request. When requests are replayed you'll have access to this
|
||||
* metadata object in case you need to modify the request beforehand.
|
||||
* @param {number} [entry.timestamp] The timestamp (Epoch time in
|
||||
* milliseconds) when the request was first added to the queue. This is
|
||||
* used along with `maxRetentionTime` to remove outdated requests. In
|
||||
* general you don't need to set this value, as it's automatically set
|
||||
* for you (defaulting to `Date.now()`), but you can update it if you
|
||||
* don't want particular requests to expire.
|
||||
*/
|
||||
|
||||
|
||||
async pushRequest(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'pushRequest',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isInstance(entry.request, Request, {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'pushRequest',
|
||||
paramName: 'entry.request'
|
||||
});
|
||||
}
|
||||
|
||||
await this._addRequest(entry, 'push');
|
||||
}
|
||||
/**
|
||||
* Stores the passed request in IndexedDB (with its timestamp and any
|
||||
* metadata) at the beginning of the queue.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Request} entry.request The request to store in the queue.
|
||||
* @param {Object} [entry.metadata] Any metadata you want associated with the
|
||||
* stored request. When requests are replayed you'll have access to this
|
||||
* metadata object in case you need to modify the request beforehand.
|
||||
* @param {number} [entry.timestamp] The timestamp (Epoch time in
|
||||
* milliseconds) when the request was first added to the queue. This is
|
||||
* used along with `maxRetentionTime` to remove outdated requests. In
|
||||
* general you don't need to set this value, as it's automatically set
|
||||
* for you (defaulting to `Date.now()`), but you can update it if you
|
||||
* don't want particular requests to expire.
|
||||
*/
|
||||
|
||||
|
||||
async unshiftRequest(entry) {
|
||||
{
|
||||
assert_js.assert.isType(entry, 'object', {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'unshiftRequest',
|
||||
paramName: 'entry'
|
||||
});
|
||||
assert_js.assert.isInstance(entry.request, Request, {
|
||||
moduleName: 'workbox-background-sync',
|
||||
className: 'Queue',
|
||||
funcName: 'unshiftRequest',
|
||||
paramName: 'entry.request'
|
||||
});
|
||||
}
|
||||
|
||||
await this._addRequest(entry, 'unshift');
|
||||
}
|
||||
/**
|
||||
* Removes and returns the last request in the queue (along with its
|
||||
* timestamp and any metadata). The returned object takes the form:
|
||||
* `{request, timestamp, metadata}`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
|
||||
|
||||
async popRequest() {
|
||||
return this._removeRequest('pop');
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first request in the queue (along with its
|
||||
* timestamp and any metadata). The returned object takes the form:
|
||||
* `{request, timestamp, metadata}`.
|
||||
*
|
||||
* @return {Promise<Object>}
|
||||
*/
|
||||
|
||||
|
||||
async shiftRequest() {
|
||||
return this._removeRequest('shift');
|
||||
}
|
||||
/**
|
||||
* Returns all the entries that have not expired (per `maxRetentionTime`).
|
||||
* Any expired entries are removed from the queue.
|
||||
*
|
||||
* @return {Promise<Array<Object>>}
|
||||
*/
|
||||
|
||||
|
||||
async getAll() {
|
||||
const allEntries = await this._queueStore.getAll();
|
||||
const now = Date.now();
|
||||
const unexpiredEntries = [];
|
||||
|
||||
for (const entry of allEntries) {
|
||||
// Ignore requests older than maxRetentionTime. Call this function
|
||||
// recursively until an unexpired request is found.
|
||||
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
|
||||
|
||||
if (now - entry.timestamp > maxRetentionTimeInMs) {
|
||||
await this._queueStore.deleteEntry(entry.id);
|
||||
} else {
|
||||
unexpiredEntries.push(convertEntry(entry));
|
||||
}
|
||||
}
|
||||
|
||||
return unexpiredEntries;
|
||||
}
|
||||
/**
|
||||
* Adds the entry to the QueueStore and registers for a sync event.
|
||||
*
|
||||
* @param {Object} entry
|
||||
* @param {Request} entry.request
|
||||
* @param {Object} [entry.metadata]
|
||||
* @param {number} [entry.timestamp=Date.now()]
|
||||
* @param {string} operation ('push' or 'unshift')
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async _addRequest({
|
||||
request,
|
||||
metadata,
|
||||
timestamp = Date.now()
|
||||
}, operation) {
|
||||
const storableRequest = await StorableRequest.fromRequest(request.clone());
|
||||
const entry = {
|
||||
requestData: storableRequest.toObject(),
|
||||
timestamp
|
||||
}; // Only include metadata if it's present.
|
||||
|
||||
if (metadata) {
|
||||
entry.metadata = metadata;
|
||||
}
|
||||
|
||||
await this._queueStore[`${operation}Entry`](entry);
|
||||
|
||||
{
|
||||
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(request.url)}' has ` + `been added to background sync queue '${this._name}'.`);
|
||||
} // Don't register for a sync if we're in the middle of a sync. Instead,
|
||||
// we wait until the sync is complete and call register if
|
||||
// `this._requestsAddedDuringSync` is true.
|
||||
|
||||
|
||||
if (this._syncInProgress) {
|
||||
this._requestsAddedDuringSync = true;
|
||||
} else {
|
||||
await this.registerSync();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes and returns the first or last (depending on `operation`) entry
|
||||
* from the QueueStore that's not older than the `maxRetentionTime`.
|
||||
*
|
||||
* @param {string} operation ('pop' or 'shift')
|
||||
* @return {Object|undefined}
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async _removeRequest(operation) {
|
||||
const now = Date.now();
|
||||
const entry = await this._queueStore[`${operation}Entry`]();
|
||||
|
||||
if (entry) {
|
||||
// Ignore requests older than maxRetentionTime. Call this function
|
||||
// recursively until an unexpired request is found.
|
||||
const maxRetentionTimeInMs = this._maxRetentionTime * 60 * 1000;
|
||||
|
||||
if (now - entry.timestamp > maxRetentionTimeInMs) {
|
||||
return this._removeRequest(operation);
|
||||
}
|
||||
|
||||
return convertEntry(entry);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Loops through each request in the queue and attempts to re-fetch it.
|
||||
* If any request fails to re-fetch, it's put back in the same position in
|
||||
* the queue (which registers a retry for the next sync event).
|
||||
*/
|
||||
|
||||
|
||||
async replayRequests() {
|
||||
let entry;
|
||||
|
||||
while (entry = await this.shiftRequest()) {
|
||||
try {
|
||||
await fetch(entry.request.clone());
|
||||
|
||||
if ("dev" !== 'production') {
|
||||
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `has been replayed in queue '${this._name}'`);
|
||||
}
|
||||
} catch (error) {
|
||||
await this.unshiftRequest(entry);
|
||||
|
||||
{
|
||||
logger_js.logger.log(`Request for '${getFriendlyURL_js.getFriendlyURL(entry.request.url)}'` + `failed to replay, putting it back in queue '${this._name}'`);
|
||||
}
|
||||
|
||||
throw new WorkboxError_js.WorkboxError('queue-replay-failed', {
|
||||
name: this._name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
logger_js.logger.log(`All requests in queue '${this.name}' have successfully ` + `replayed; the queue is now empty!`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Registers a sync event with a tag unique to this instance.
|
||||
*/
|
||||
|
||||
|
||||
async registerSync() {
|
||||
if ('sync' in self.registration) {
|
||||
try {
|
||||
await self.registration.sync.register(`${TAG_PREFIX}:${this._name}`);
|
||||
} catch (err) {
|
||||
// This means the registration failed for some reason, possibly due to
|
||||
// the user disabling it.
|
||||
{
|
||||
logger_js.logger.warn(`Unable to register sync event for '${this._name}'.`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* In sync-supporting browsers, this adds a listener for the sync event.
|
||||
* In non-sync-supporting browsers, this will retry the queue on service
|
||||
* worker startup.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_addSyncListener() {
|
||||
if ('sync' in self.registration) {
|
||||
self.addEventListener('sync', event => {
|
||||
if (event.tag === `${TAG_PREFIX}:${this._name}`) {
|
||||
{
|
||||
logger_js.logger.log(`Background sync for tag '${event.tag}'` + `has been received`);
|
||||
}
|
||||
|
||||
const syncComplete = async () => {
|
||||
this._syncInProgress = true;
|
||||
let syncError;
|
||||
|
||||
try {
|
||||
await this._onSync({
|
||||
queue: this
|
||||
});
|
||||
} catch (error) {
|
||||
syncError = error; // Rethrow the error. Note: the logic in the finally clause
|
||||
// will run before this gets rethrown.
|
||||
|
||||
throw syncError;
|
||||
} finally {
|
||||
// New items may have been added to the queue during the sync,
|
||||
// so we need to register for a new sync if that's happened...
|
||||
// Unless there was an error during the sync, in which
|
||||
// case the browser will automatically retry later, as long
|
||||
// as `event.lastChance` is not true.
|
||||
if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) {
|
||||
await this.registerSync();
|
||||
}
|
||||
|
||||
this._syncInProgress = false;
|
||||
this._requestsAddedDuringSync = false;
|
||||
}
|
||||
};
|
||||
|
||||
event.waitUntil(syncComplete());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
{
|
||||
logger_js.logger.log(`Background sync replaying without background sync event`);
|
||||
} // If the browser doesn't support background sync, retry
|
||||
// every time the service worker starts up as a fallback.
|
||||
|
||||
|
||||
this._onSync({
|
||||
queue: this
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the set of queue names. This is primarily used to reset the list
|
||||
* of queue names in tests.
|
||||
*
|
||||
* @return {Set}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
static get _queueNames() {
|
||||
return queueNames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
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 class implementing the `fetchDidFail` lifecycle callback. This makes it
|
||||
* easier to add failed requests to a background sync Queue.
|
||||
*
|
||||
* @memberof module:workbox-background-sync
|
||||
*/
|
||||
|
||||
class BackgroundSyncPlugin {
|
||||
/**
|
||||
* @param {string} name See the [Queue]{@link module:workbox-background-sync.Queue}
|
||||
* documentation for parameter details.
|
||||
* @param {Object} [options] See the
|
||||
* [Queue]{@link module:workbox-background-sync.Queue} documentation for
|
||||
* parameter details.
|
||||
*/
|
||||
constructor(name, options) {
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Request} options.request
|
||||
* @private
|
||||
*/
|
||||
this.fetchDidFail = async ({
|
||||
request
|
||||
}) => {
|
||||
await this._queue.pushRequest({
|
||||
request
|
||||
});
|
||||
};
|
||||
|
||||
this._queue = new Queue(name, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.BackgroundSyncPlugin = BackgroundSyncPlugin;
|
||||
exports.Queue = Queue;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
|
||||
//# sourceMappingURL=workbox-background-sync.dev.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-background-sync.dev.js","sources":["../_version.js","../lib/QueueStore.js","../lib/StorableRequest.js","../Queue.js","../BackgroundSyncPlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:background-sync:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { DBWrapper } from 'workbox-core/_private/DBWrapper.js';\nimport '../_version.js';\nconst DB_VERSION = 3;\nconst DB_NAME = 'workbox-background-sync';\nconst OBJECT_STORE_NAME = 'requests';\nconst INDEXED_PROP = 'queueName';\n/**\n * A class to manage storing requests from a Queue in IndexedDB,\n * indexed by their queue name for easier access.\n *\n * @private\n */\nexport class QueueStore {\n /**\n * Associates this instance with a Queue instance, so entries added can be\n * identified by their queue name.\n *\n * @param {string} queueName\n * @private\n */\n constructor(queueName) {\n this._queueName = queueName;\n this._db = new DBWrapper(DB_NAME, DB_VERSION, {\n onupgradeneeded: this._upgradeDb,\n });\n }\n /**\n * Append an entry last in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async pushEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry.requestData',\n });\n }\n // Don't specify an ID since one is automatically generated.\n delete entry.id;\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Prepend an entry first in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async unshiftEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry.requestData',\n });\n }\n const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {\n count: 1,\n });\n if (firstEntry) {\n // Pick an ID one less than the lowest ID in the object store.\n entry.id = firstEntry.id - 1;\n }\n else {\n // Otherwise let the auto-incrementor assign the ID.\n delete entry.id;\n }\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Removes and returns the last entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async popEntry() {\n return this._removeEntry({ direction: 'prev' });\n }\n /**\n * Removes and returns the first entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async shiftEntry() {\n return this._removeEntry({ direction: 'next' });\n }\n /**\n * Returns all entries in the store matching the `queueName`.\n *\n * @param {Object} options See {@link module:workbox-background-sync.Queue~getAll}\n * @return {Promise<Array<Object>>}\n * @private\n */\n async getAll() {\n return await this._db.getAllMatching(OBJECT_STORE_NAME, {\n index: INDEXED_PROP,\n query: IDBKeyRange.only(this._queueName),\n });\n }\n /**\n * Deletes the entry for the given ID.\n *\n * WARNING: this method does not ensure the deleted enry belongs to this\n * queue (i.e. matches the `queueName`). But this limitation is acceptable\n * as this class is not publicly exposed. An additional check would make\n * this method slower than iLine truncated
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
this.workbox=this.workbox||{},this.workbox.backgroundSync=function(t,e,s,i,n){"use strict";try{self["workbox:background-sync:5.1.4"]&&_()}catch(t){}class a{constructor(t){this.t=t,this.s=new n.DBWrapper("workbox-background-sync",3,{onupgradeneeded:this.i})}async pushEntry(t){delete t.id,t.queueName=this.t,await this.s.add("requests",t)}async unshiftEntry(t){const[e]=await this.s.getAllMatching("requests",{count:1});e?t.id=e.id-1:delete t.id,t.queueName=this.t,await this.s.add("requests",t)}async popEntry(){return this.h({direction:"prev"})}async shiftEntry(){return this.h({direction:"next"})}async getAll(){return await this.s.getAllMatching("requests",{index:"queueName",query:IDBKeyRange.only(this.t)})}async deleteEntry(t){await this.s.delete("requests",t)}async h({direction:t}){const[e]=await this.s.getAllMatching("requests",{direction:t,index:"queueName",query:IDBKeyRange.only(this.t),count:1});if(e)return await this.deleteEntry(e.id),e}i(t){const e=t.target.result;t.oldVersion>0&&t.oldVersion<3&&e.objectStoreNames.contains("requests")&&e.deleteObjectStore("requests");e.createObjectStore("requests",{autoIncrement:!0,keyPath:"id"}).createIndex("queueName","queueName",{unique:!1})}}const r=["method","referrer","referrerPolicy","mode","credentials","cache","redirect","integrity","keepalive"];class c{constructor(t){"navigate"===t.mode&&(t.mode="same-origin"),this.u=t}static async fromRequest(t){const e={url:t.url,headers:{}};"GET"!==t.method&&(e.body=await t.clone().arrayBuffer());for(const[s,i]of t.headers.entries())e.headers[s]=i;for(const s of r)void 0!==t[s]&&(e[s]=t[s]);return new c(e)}toObject(){const t=Object.assign({},this.u);return t.headers=Object.assign({},this.u.headers),t.body&&(t.body=t.body.slice(0)),t}toRequest(){return new Request(this.u.url,this.u)}clone(){return new c(this.toObject())}}const h=new Set,u=t=>{const e={request:new c(t.requestData).toRequest(),timestamp:t.timestamp};return t.metadata&&(e.metadata=t.metadata),e};class o{constructor(t,{onSync:s,maxRetentionTime:i}={}){if(this.o=!1,this.q=!1,h.has(t))throw new e.WorkboxError("duplicate-queue-name",{name:t});h.add(t),this.l=t,this.m=s||this.replayRequests,this.p=i||10080,this.g=new a(this.l),this.R()}get name(){return this.l}async pushRequest(t){await this.k(t,"push")}async unshiftRequest(t){await this.k(t,"unshift")}async popRequest(){return this.D("pop")}async shiftRequest(){return this.D("shift")}async getAll(){const t=await this.g.getAll(),e=Date.now(),s=[];for(const i of t){const t=60*this.p*1e3;e-i.timestamp>t?await this.g.deleteEntry(i.id):s.push(u(i))}return s}async k({request:t,metadata:e,timestamp:s=Date.now()},i){const n={requestData:(await c.fromRequest(t.clone())).toObject(),timestamp:s};e&&(n.metadata=e),await this.g[i+"Entry"](n),this.o?this.q=!0:await this.registerSync()}async D(t){const e=Date.now(),s=await this.g[t+"Entry"]();if(s){const i=60*this.p*1e3;return e-s.timestamp>i?this.D(t):u(s)}}async replayRequests(){let t;for(;t=await this.shiftRequest();)try{await fetch(t.request.clone())}catch(s){throw await this.unshiftRequest(t),new e.WorkboxError("queue-replay-failed",{name:this.l})}}async registerSync(){if("sync"in self.registration)try{await self.registration.sync.register("workbox-background-sync:"+this.l)}catch(t){}}R(){"sync"in self.registration?self.addEventListener("sync",t=>{if(t.tag==="workbox-background-sync:"+this.l){const e=async()=>{let e;this.o=!0;try{await this.m({queue:this})}catch(t){throw e=t,e}finally{!this.q||e&&!t.lastChance||await this.registerSync(),this.o=!1,this.q=!1}};t.waitUntil(e())}}):this.m({queue:this})}static get _(){return h}}return t.BackgroundSyncPlugin=class{constructor(t,e){this.fetchDidFail=async({request:t})=>{await this.v.pushRequest({request:t})},this.v=new o(t,e)}},t.Queue=o,t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private);
|
||||
//# sourceMappingURL=workbox-background-sync.prod.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-background-sync.prod.js","sources":["../_version.js","../lib/QueueStore.js","../lib/StorableRequest.js","../Queue.js","../BackgroundSyncPlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:background-sync:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { DBWrapper } from 'workbox-core/_private/DBWrapper.js';\nimport '../_version.js';\nconst DB_VERSION = 3;\nconst DB_NAME = 'workbox-background-sync';\nconst OBJECT_STORE_NAME = 'requests';\nconst INDEXED_PROP = 'queueName';\n/**\n * A class to manage storing requests from a Queue in IndexedDB,\n * indexed by their queue name for easier access.\n *\n * @private\n */\nexport class QueueStore {\n /**\n * Associates this instance with a Queue instance, so entries added can be\n * identified by their queue name.\n *\n * @param {string} queueName\n * @private\n */\n constructor(queueName) {\n this._queueName = queueName;\n this._db = new DBWrapper(DB_NAME, DB_VERSION, {\n onupgradeneeded: this._upgradeDb,\n });\n }\n /**\n * Append an entry last in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async pushEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'pushEntry',\n paramName: 'entry.requestData',\n });\n }\n // Don't specify an ID since one is automatically generated.\n delete entry.id;\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Prepend an entry first in the queue.\n *\n * @param {Object} entry\n * @param {Object} entry.requestData\n * @param {number} [entry.timestamp]\n * @param {Object} [entry.metadata]\n * @private\n */\n async unshiftEntry(entry) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(entry, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry',\n });\n assert.isType(entry.requestData, 'object', {\n moduleName: 'workbox-background-sync',\n className: 'QueueStore',\n funcName: 'unshiftEntry',\n paramName: 'entry.requestData',\n });\n }\n const [firstEntry] = await this._db.getAllMatching(OBJECT_STORE_NAME, {\n count: 1,\n });\n if (firstEntry) {\n // Pick an ID one less than the lowest ID in the object store.\n entry.id = firstEntry.id - 1;\n }\n else {\n // Otherwise let the auto-incrementor assign the ID.\n delete entry.id;\n }\n entry.queueName = this._queueName;\n await this._db.add(OBJECT_STORE_NAME, entry);\n }\n /**\n * Removes and returns the last entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async popEntry() {\n return this._removeEntry({ direction: 'prev' });\n }\n /**\n * Removes and returns the first entry in the queue matching the `queueName`.\n *\n * @return {Promise<Object>}\n * @private\n */\n async shiftEntry() {\n return this._removeEntry({ direction: 'next' });\n }\n /**\n * Returns all entries in the store matching the `queueName`.\n *\n * @param {Object} options See {@link module:workbox-background-sync.Queue~getAll}\n * @return {Promise<Array<Object>>}\n * @private\n */\n async getAll() {\n return await this._db.getAllMatching(OBJECT_STORE_NAME, {\n index: INDEXED_PROP,\n query: IDBKeyRange.only(this._queueName),\n });\n }\n /**\n * Deletes the entry for the given ID.\n *\n * WARNING: this method does not ensure the deleted enry belongs to this\n * queue (i.e. matches the `queueName`). But this limitation is acceptable\n * as this class is not publicly exposed. An additional check would make\n * this method slower than Line truncated
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.broadcastUpdate = (function (exports, assert_js, timeout_js, resultingClientExists_js, logger_js, WorkboxError_js, dontWaitFor_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:broadcast-update:5.1.4'] && _();
|
||||
} 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.
|
||||
*/
|
||||
/**
|
||||
* Given two `Response's`, compares several header values to see if they are
|
||||
* the same or not.
|
||||
*
|
||||
* @param {Response} firstResponse
|
||||
* @param {Response} secondResponse
|
||||
* @param {Array<string>} headersToCheck
|
||||
* @return {boolean}
|
||||
*
|
||||
* @memberof module:workbox-broadcast-update
|
||||
*/
|
||||
|
||||
const responsesAreSame = (firstResponse, secondResponse, headersToCheck) => {
|
||||
{
|
||||
if (!(firstResponse instanceof Response && secondResponse instanceof Response)) {
|
||||
throw new WorkboxError_js.WorkboxError('invalid-responses-are-same-args');
|
||||
}
|
||||
}
|
||||
|
||||
const atLeastOneHeaderAvailable = headersToCheck.some(header => {
|
||||
return firstResponse.headers.has(header) && secondResponse.headers.has(header);
|
||||
});
|
||||
|
||||
if (!atLeastOneHeaderAvailable) {
|
||||
{
|
||||
logger_js.logger.warn(`Unable to determine where the response has been updated ` + `because none of the headers that would be checked are present.`);
|
||||
logger_js.logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck);
|
||||
} // Just return true, indicating the that responses are the same, since we
|
||||
// can't determine otherwise.
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return headersToCheck.every(header => {
|
||||
const headerStateComparison = firstResponse.headers.has(header) === secondResponse.headers.has(header);
|
||||
const headerValueComparison = firstResponse.headers.get(header) === secondResponse.headers.get(header);
|
||||
return headerStateComparison && headerValueComparison;
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
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 CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';
|
||||
const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';
|
||||
const DEFAULT_HEADERS_TO_CHECK = ['content-length', 'etag', 'last-modified'];
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
// TODO(philipwalton): remove once this Safari bug fix has been released.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=201169
|
||||
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
/**
|
||||
* Generates the default payload used in update messages. By default the
|
||||
* payload includes the `cacheName` and `updatedURL` fields.
|
||||
*
|
||||
* @return Object
|
||||
* @private
|
||||
*/
|
||||
|
||||
function defaultPayloadGenerator(data) {
|
||||
return {
|
||||
cacheName: data.cacheName,
|
||||
updatedURL: data.request.url
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Uses the `postMessage()` API to inform any open windows/tabs when a cached
|
||||
* response has been updated.
|
||||
*
|
||||
* For efficiency's sake, the underlying response bodies are not compared;
|
||||
* only specific response headers are checked.
|
||||
*
|
||||
* @memberof module:workbox-broadcast-update
|
||||
*/
|
||||
|
||||
|
||||
class BroadcastCacheUpdate {
|
||||
/**
|
||||
* Construct a BroadcastCacheUpdate instance with a specific `channelName` to
|
||||
* broadcast messages on
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]
|
||||
* A list of headers that will be used to determine whether the responses
|
||||
* differ.
|
||||
* @param {string} [options.generatePayload] A function whose return value
|
||||
* will be used as the `payload` field in any cache update messages sent
|
||||
* to the window clients.
|
||||
*/
|
||||
constructor({
|
||||
headersToCheck,
|
||||
generatePayload
|
||||
} = {}) {
|
||||
this._headersToCheck = headersToCheck || DEFAULT_HEADERS_TO_CHECK;
|
||||
this._generatePayload = generatePayload || defaultPayloadGenerator;
|
||||
}
|
||||
/**
|
||||
* Compares two [Responses](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* and sends a message (via `postMessage()`) to all window clients if the
|
||||
* responses differ (note: neither of the Responses can be
|
||||
* {@link http://stackoverflow.com/questions/39109789|opaque}).
|
||||
*
|
||||
* The message that's posted has the following format (where `payload` can
|
||||
* be customized via the `generatePayload` option the instance is created
|
||||
* with):
|
||||
*
|
||||
* ```
|
||||
* {
|
||||
* type: 'CACHE_UPDATED',
|
||||
* meta: 'workbox-broadcast-update',
|
||||
* payload: {
|
||||
* cacheName: 'the-cache-name',
|
||||
* updatedURL: 'https://example.com/'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Response} [options.oldResponse] Cached response to compare.
|
||||
* @param {Response} options.newResponse Possibly updated response to compare.
|
||||
* @param {Request} options.request The request.
|
||||
* @param {string} options.cacheName Name of the cache the responses belong
|
||||
* to. This is included in the broadcast message.
|
||||
* @param {Event} [options.event] event An optional event that triggered
|
||||
* this possible cache update.
|
||||
* @return {Promise} Resolves once the update is sent.
|
||||
*/
|
||||
|
||||
|
||||
async notifyIfUpdated(options) {
|
||||
{
|
||||
assert_js.assert.isType(options.cacheName, 'string', {
|
||||
moduleName: 'workbox-broadcast-update',
|
||||
className: 'BroadcastCacheUpdate',
|
||||
funcName: 'notifyIfUpdated',
|
||||
paramName: 'cacheName'
|
||||
});
|
||||
assert_js.assert.isInstance(options.newResponse, Response, {
|
||||
moduleName: 'workbox-broadcast-update',
|
||||
className: 'BroadcastCacheUpdate',
|
||||
funcName: 'notifyIfUpdated',
|
||||
paramName: 'newResponse'
|
||||
});
|
||||
assert_js.assert.isInstance(options.request, Request, {
|
||||
moduleName: 'workbox-broadcast-update',
|
||||
className: 'BroadcastCacheUpdate',
|
||||
funcName: 'notifyIfUpdated',
|
||||
paramName: 'request'
|
||||
});
|
||||
} // Without two responses there is nothing to compare.
|
||||
|
||||
|
||||
if (!options.oldResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!responsesAreSame(options.oldResponse, options.newResponse, this._headersToCheck)) {
|
||||
{
|
||||
logger_js.logger.log(`Newer response found (and cached) for:`, options.request.url);
|
||||
}
|
||||
|
||||
const messageData = {
|
||||
type: CACHE_UPDATED_MESSAGE_TYPE,
|
||||
meta: CACHE_UPDATED_MESSAGE_META,
|
||||
payload: this._generatePayload(options)
|
||||
}; // For navigation requests, wait until the new window client exists
|
||||
// before sending the message
|
||||
|
||||
if (options.request.mode === 'navigate') {
|
||||
let resultingClientId;
|
||||
|
||||
if (options.event instanceof FetchEvent) {
|
||||
resultingClientId = options.event.resultingClientId;
|
||||
}
|
||||
|
||||
const resultingWin = await resultingClientExists_js.resultingClientExists(resultingClientId); // Safari does not currently implement postMessage buffering and
|
||||
// there's no good way to feature detect that, so to increase the
|
||||
// chances of the message being delivered in Safari, we add a timeout.
|
||||
// We also do this if `resultingClientExists()` didn't return a client,
|
||||
// which means it timed out, so it's worth waiting a bit longer.
|
||||
|
||||
if (!resultingWin || isSafari) {
|
||||
// 3500 is chosen because (according to CrUX data) 80% of mobile
|
||||
// websites hit the DOMContentLoaded event in less than 3.5 seconds.
|
||||
// And presumably sites implementing service worker are on the
|
||||
// higher end of the performance spectrum.
|
||||
await timeout_js.timeout(3500);
|
||||
}
|
||||
}
|
||||
|
||||
const windows = await self.clients.matchAll({
|
||||
type: 'window'
|
||||
});
|
||||
|
||||
for (const win of windows) {
|
||||
win.postMessage(messageData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
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 plugin will automatically broadcast a message whenever a cached response
|
||||
* is updated.
|
||||
*
|
||||
* @memberof module:workbox-broadcast-update
|
||||
*/
|
||||
|
||||
class BroadcastUpdatePlugin {
|
||||
/**
|
||||
* Construct a BroadcastCacheUpdate instance with the passed options and
|
||||
* calls its [`notifyIfUpdated()`]{@link module:workbox-broadcast-update.BroadcastCacheUpdate~notifyIfUpdated}
|
||||
* method whenever the plugin's `cacheDidUpdate` callback is invoked.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]
|
||||
* A list of headers that will be used to determine whether the responses
|
||||
* differ.
|
||||
* @param {string} [options.generatePayload] A function whose return value
|
||||
* will be used as the `payload` field in any cache update messages sent
|
||||
* to the window clients.
|
||||
*/
|
||||
constructor(options) {
|
||||
/**
|
||||
* A "lifecycle" callback that will be triggered automatically by the
|
||||
* `workbox-sw` and `workbox-runtime-caching` handlers when an entry is
|
||||
* added to a cache.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} options The input object to this function.
|
||||
* @param {string} options.cacheName Name of the cache being updated.
|
||||
* @param {Response} [options.oldResponse] The previous cached value, if any.
|
||||
* @param {Response} options.newResponse The new value in the cache.
|
||||
* @param {Request} options.request The request that triggered the update.
|
||||
* @param {Request} [options.event] The event that triggered the update.
|
||||
*/
|
||||
this.cacheDidUpdate = async options => {
|
||||
dontWaitFor_js.dontWaitFor(this._broadcastUpdate.notifyIfUpdated(options));
|
||||
};
|
||||
|
||||
this._broadcastUpdate = new BroadcastCacheUpdate(options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.BroadcastCacheUpdate = BroadcastCacheUpdate;
|
||||
exports.BroadcastUpdatePlugin = BroadcastUpdatePlugin;
|
||||
exports.responsesAreSame = responsesAreSame;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
|
||||
//# sourceMappingURL=workbox-broadcast-update.dev.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-broadcast-update.dev.js","sources":["../_version.js","../responsesAreSame.js","../utils/constants.js","../BroadcastCacheUpdate.js","../BroadcastUpdatePlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:broadcast-update:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport './_version.js';\n/**\n * Given two `Response's`, compares several header values to see if they are\n * the same or not.\n *\n * @param {Response} firstResponse\n * @param {Response} secondResponse\n * @param {Array<string>} headersToCheck\n * @return {boolean}\n *\n * @memberof module:workbox-broadcast-update\n */\nconst responsesAreSame = (firstResponse, secondResponse, headersToCheck) => {\n if (process.env.NODE_ENV !== 'production') {\n if (!(firstResponse instanceof Response &&\n secondResponse instanceof Response)) {\n throw new WorkboxError('invalid-responses-are-same-args');\n }\n }\n const atLeastOneHeaderAvailable = headersToCheck.some((header) => {\n return firstResponse.headers.has(header) &&\n secondResponse.headers.has(header);\n });\n if (!atLeastOneHeaderAvailable) {\n if (process.env.NODE_ENV !== 'production') {\n logger.warn(`Unable to determine where the response has been updated ` +\n `because none of the headers that would be checked are present.`);\n logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck);\n }\n // Just return true, indicating the that responses are the same, since we\n // can't determine otherwise.\n return true;\n }\n return headersToCheck.every((header) => {\n const headerStateComparison = firstResponse.headers.has(header) ===\n secondResponse.headers.has(header);\n const headerValueComparison = firstResponse.headers.get(header) ===\n secondResponse.headers.get(header);\n return headerStateComparison && headerValueComparison;\n });\n};\nexport { responsesAreSame };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nexport const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';\nexport const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';\nexport const DEFAULT_HEADERS_TO_CHECK = [\n 'content-length',\n 'etag',\n 'last-modified',\n];\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { timeout } from 'workbox-core/_private/timeout.js';\nimport { resultingClientExists } from 'workbox-core/_private/resultingClientExists.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { responsesAreSame } from './responsesAreSame.js';\nimport { CACHE_UPDATED_MESSAGE_TYPE, CACHE_UPDATED_MESSAGE_META, DEFAULT_HEADERS_TO_CHECK } from './utils/constants.js';\nimport './_version.js';\n// UA-sniff Safari: https://stackoverflow.com/questions/7944460/detect-safari-browser\n// TODO(philipwalton): remove once this Safari bug fix has been released.\n// https://bugs.webkit.org/show_bug.cgi?id=201169\nconst isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n/**\n * Generates the default payload used in update messages. By default the\n * payload includes the `cacheName` and `updatedURL` fields.\n *\n * @return Object\n * @private\n */\nfunction defaultPayloadGenerator(data) {\n return {\n cacheName: data.cacheName,\n updatedURL: data.request.url,\n };\n}\n/**\n * Uses the `postMessage()` API to inform any open windows/tabs when a cached\n * response has been updated.\n *\n * For efficiency's sake, the underlying response bodies are not compared;\n * only specific response headers are checked.\n *\n * @memberof module:workbox-broadcast-update\n */\nclass BroadcastCacheUpdate {\n /**\n * Construct a BroadcastCacheUpdate instance with a specific `channelName` to\n * broadcast messages on\n *\n * @param {Object} options\n * @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]\n * A list of headers that will be used to determine whether the responses\n * differ.\n * @param {string} [options.generatePayload] A function whose return value\n * will be used as the `payload` field in any cache update messaLine truncated
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
this.workbox=this.workbox||{},this.workbox.broadcastUpdate=function(t,a,o,s){"use strict";try{self["workbox:broadcast-update:5.1.4"]&&_()}catch(t){}const e=(t,a,o)=>!o.some(o=>t.headers.has(o)&&a.headers.has(o))||o.every(o=>{const s=t.headers.has(o)===a.headers.has(o),e=t.headers.get(o)===a.headers.get(o);return s&&e}),n=["content-length","etag","last-modified"],i=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);function c(t){return{cacheName:t.cacheName,updatedURL:t.request.url}}class r{constructor({headersToCheck:t,generatePayload:a}={}){this.t=t||n,this.o=a||c}async notifyIfUpdated(t){if(t.oldResponse&&!e(t.oldResponse,t.newResponse,this.t)){const s={type:"CACHE_UPDATED",meta:"workbox-broadcast-update",payload:this.o(t)};if("navigate"===t.request.mode){let s;t.event instanceof FetchEvent&&(s=t.event.resultingClientId);await o.resultingClientExists(s)&&!i||await a.timeout(3500)}const e=await self.clients.matchAll({type:"window"});for(const t of e)t.postMessage(s)}}}return t.BroadcastCacheUpdate=r,t.BroadcastUpdatePlugin=class{constructor(t){this.cacheDidUpdate=async t=>{s.dontWaitFor(this.s.notifyIfUpdated(t))},this.s=new r(t)}},t.responsesAreSame=e,t}({},workbox.core._private,workbox.core._private,workbox.core._private);
|
||||
//# sourceMappingURL=workbox-broadcast-update.prod.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-broadcast-update.prod.js","sources":["../_version.js","../responsesAreSame.js","../utils/constants.js","../BroadcastCacheUpdate.js","../BroadcastUpdatePlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:broadcast-update:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport './_version.js';\n/**\n * Given two `Response's`, compares several header values to see if they are\n * the same or not.\n *\n * @param {Response} firstResponse\n * @param {Response} secondResponse\n * @param {Array<string>} headersToCheck\n * @return {boolean}\n *\n * @memberof module:workbox-broadcast-update\n */\nconst responsesAreSame = (firstResponse, secondResponse, headersToCheck) => {\n if (process.env.NODE_ENV !== 'production') {\n if (!(firstResponse instanceof Response &&\n secondResponse instanceof Response)) {\n throw new WorkboxError('invalid-responses-are-same-args');\n }\n }\n const atLeastOneHeaderAvailable = headersToCheck.some((header) => {\n return firstResponse.headers.has(header) &&\n secondResponse.headers.has(header);\n });\n if (!atLeastOneHeaderAvailable) {\n if (process.env.NODE_ENV !== 'production') {\n logger.warn(`Unable to determine where the response has been updated ` +\n `because none of the headers that would be checked are present.`);\n logger.debug(`Attempting to compare the following: `, firstResponse, secondResponse, headersToCheck);\n }\n // Just return true, indicating the that responses are the same, since we\n // can't determine otherwise.\n return true;\n }\n return headersToCheck.every((header) => {\n const headerStateComparison = firstResponse.headers.has(header) ===\n secondResponse.headers.has(header);\n const headerValueComparison = firstResponse.headers.get(header) ===\n secondResponse.headers.get(header);\n return headerStateComparison && headerValueComparison;\n });\n};\nexport { responsesAreSame };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nexport const CACHE_UPDATED_MESSAGE_TYPE = 'CACHE_UPDATED';\nexport const CACHE_UPDATED_MESSAGE_META = 'workbox-broadcast-update';\nexport const DEFAULT_HEADERS_TO_CHECK = [\n 'content-length',\n 'etag',\n 'last-modified',\n];\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { timeout } from 'workbox-core/_private/timeout.js';\nimport { resultingClientExists } from 'workbox-core/_private/resultingClientExists.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { responsesAreSame } from './responsesAreSame.js';\nimport { CACHE_UPDATED_MESSAGE_TYPE, CACHE_UPDATED_MESSAGE_META, DEFAULT_HEADERS_TO_CHECK } from './utils/constants.js';\nimport './_version.js';\n// UA-sniff Safari: https://stackoverflow.com/questions/7944460/detect-safari-browser\n// TODO(philipwalton): remove once this Safari bug fix has been released.\n// https://bugs.webkit.org/show_bug.cgi?id=201169\nconst isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n/**\n * Generates the default payload used in update messages. By default the\n * payload includes the `cacheName` and `updatedURL` fields.\n *\n * @return Object\n * @private\n */\nfunction defaultPayloadGenerator(data) {\n return {\n cacheName: data.cacheName,\n updatedURL: data.request.url,\n };\n}\n/**\n * Uses the `postMessage()` API to inform any open windows/tabs when a cached\n * response has been updated.\n *\n * For efficiency's sake, the underlying response bodies are not compared;\n * only specific response headers are checked.\n *\n * @memberof module:workbox-broadcast-update\n */\nclass BroadcastCacheUpdate {\n /**\n * Construct a BroadcastCacheUpdate instance with a specific `channelName` to\n * broadcast messages on\n *\n * @param {Object} options\n * @param {Array<string>} [options.headersToCheck=['content-length', 'etag', 'last-modified']]\n * A list of headers that will be used to determine whether the responses\n * differ.\n * @param {string} [options.generatePayload] A function whose return value\n * will be used as the `payload` field in any cache update messLine truncated
|
||||
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.cacheableResponse = (function (exports, assert_js, WorkboxError_js, getFriendlyURL_js, logger_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:cacheable-response:5.1.4'] && _();
|
||||
} 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.
|
||||
*/
|
||||
/**
|
||||
* This class allows you to set up rules determining what
|
||||
* status codes and/or headers need to be present in order for a
|
||||
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* to be considered cacheable.
|
||||
*
|
||||
* @memberof module:workbox-cacheable-response
|
||||
*/
|
||||
|
||||
class CacheableResponse {
|
||||
/**
|
||||
* To construct a new CacheableResponse instance you must provide at least
|
||||
* one of the `config` properties.
|
||||
*
|
||||
* If both `statuses` and `headers` are specified, then both conditions must
|
||||
* be met for the `Response` to be considered cacheable.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Array<number>} [config.statuses] One or more status codes that a
|
||||
* `Response` can have and be considered cacheable.
|
||||
* @param {Object<string,string>} [config.headers] A mapping of header names
|
||||
* and expected values that a `Response` can have and be considered cacheable.
|
||||
* If multiple headers are provided, only one needs to be present.
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
{
|
||||
if (!(config.statuses || config.headers)) {
|
||||
throw new WorkboxError_js.WorkboxError('statuses-or-headers-required', {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'constructor'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.statuses) {
|
||||
assert_js.assert.isArray(config.statuses, {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.statuses'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.headers) {
|
||||
assert_js.assert.isType(config.headers, 'object', {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.headers'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._statuses = config.statuses;
|
||||
this._headers = config.headers;
|
||||
}
|
||||
/**
|
||||
* Checks a response to see whether it's cacheable or not, based on this
|
||||
* object's configuration.
|
||||
*
|
||||
* @param {Response} response The response whose cacheability is being
|
||||
* checked.
|
||||
* @return {boolean} `true` if the `Response` is cacheable, and `false`
|
||||
* otherwise.
|
||||
*/
|
||||
|
||||
|
||||
isResponseCacheable(response) {
|
||||
{
|
||||
assert_js.assert.isInstance(response, Response, {
|
||||
moduleName: 'workbox-cacheable-response',
|
||||
className: 'CacheableResponse',
|
||||
funcName: 'isResponseCacheable',
|
||||
paramName: 'response'
|
||||
});
|
||||
}
|
||||
|
||||
let cacheable = true;
|
||||
|
||||
if (this._statuses) {
|
||||
cacheable = this._statuses.includes(response.status);
|
||||
}
|
||||
|
||||
if (this._headers && cacheable) {
|
||||
cacheable = Object.keys(this._headers).some(headerName => {
|
||||
return response.headers.get(headerName) === this._headers[headerName];
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
if (!cacheable) {
|
||||
logger_js.logger.groupCollapsed(`The request for ` + `'${getFriendlyURL_js.getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`);
|
||||
logger_js.logger.groupCollapsed(`View cacheability criteria here.`);
|
||||
logger_js.logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses));
|
||||
logger_js.logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2));
|
||||
logger_js.logger.groupEnd();
|
||||
const logFriendlyHeaders = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
logFriendlyHeaders[key] = value;
|
||||
});
|
||||
logger_js.logger.groupCollapsed(`View response status and headers here.`);
|
||||
logger_js.logger.log(`Response status: ` + response.status);
|
||||
logger_js.logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2));
|
||||
logger_js.logger.groupEnd();
|
||||
logger_js.logger.groupCollapsed(`View full response details here.`);
|
||||
logger_js.logger.log(response.headers);
|
||||
logger_js.logger.log(response);
|
||||
logger_js.logger.groupEnd();
|
||||
logger_js.logger.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
return cacheable;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
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 class implementing the `cacheWillUpdate` lifecycle callback. This makes it
|
||||
* easier to add in cacheability checks to requests made via Workbox's built-in
|
||||
* strategies.
|
||||
*
|
||||
* @memberof module:workbox-cacheable-response
|
||||
*/
|
||||
|
||||
class CacheableResponsePlugin {
|
||||
/**
|
||||
* To construct a new CacheableResponsePlugin instance you must provide at
|
||||
* least one of the `config` properties.
|
||||
*
|
||||
* If both `statuses` and `headers` are specified, then both conditions must
|
||||
* be met for the `Response` to be considered cacheable.
|
||||
*
|
||||
* @param {Object} config
|
||||
* @param {Array<number>} [config.statuses] One or more status codes that a
|
||||
* `Response` can have and be considered cacheable.
|
||||
* @param {Object<string,string>} [config.headers] A mapping of header names
|
||||
* and expected values that a `Response` can have and be considered cacheable.
|
||||
* If multiple headers are provided, only one needs to be present.
|
||||
*/
|
||||
constructor(config) {
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Response} options.response
|
||||
* @return {Response|null}
|
||||
* @private
|
||||
*/
|
||||
this.cacheWillUpdate = async ({
|
||||
response
|
||||
}) => {
|
||||
if (this._cacheableResponse.isResponseCacheable(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
this._cacheableResponse = new CacheableResponse(config);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.CacheableResponse = CacheableResponse;
|
||||
exports.CacheableResponsePlugin = CacheableResponsePlugin;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private));
|
||||
//# sourceMappingURL=workbox-cacheable-response.dev.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-cacheable-response.dev.js","sources":["../_version.js","../CacheableResponse.js","../CacheableResponsePlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:cacheable-response:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport './_version.js';\n/**\n * This class allows you to set up rules determining what\n * status codes and/or headers need to be present in order for a\n * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * to be considered cacheable.\n *\n * @memberof module:workbox-cacheable-response\n */\nclass CacheableResponse {\n /**\n * To construct a new CacheableResponse instance you must provide at least\n * one of the `config` properties.\n *\n * If both `statuses` and `headers` are specified, then both conditions must\n * be met for the `Response` to be considered cacheable.\n *\n * @param {Object} config\n * @param {Array<number>} [config.statuses] One or more status codes that a\n * `Response` can have and be considered cacheable.\n * @param {Object<string,string>} [config.headers] A mapping of header names\n * and expected values that a `Response` can have and be considered cacheable.\n * If multiple headers are provided, only one needs to be present.\n */\n constructor(config = {}) {\n if (process.env.NODE_ENV !== 'production') {\n if (!(config.statuses || config.headers)) {\n throw new WorkboxError('statuses-or-headers-required', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n });\n }\n if (config.statuses) {\n assert.isArray(config.statuses, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.statuses',\n });\n }\n if (config.headers) {\n assert.isType(config.headers, 'object', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.headers',\n });\n }\n }\n this._statuses = config.statuses;\n this._headers = config.headers;\n }\n /**\n * Checks a response to see whether it's cacheable or not, based on this\n * object's configuration.\n *\n * @param {Response} response The response whose cacheability is being\n * checked.\n * @return {boolean} `true` if the `Response` is cacheable, and `false`\n * otherwise.\n */\n isResponseCacheable(response) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(response, Response, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'isResponseCacheable',\n paramName: 'response',\n });\n }\n let cacheable = true;\n if (this._statuses) {\n cacheable = this._statuses.includes(response.status);\n }\n if (this._headers && cacheable) {\n cacheable = Object.keys(this._headers).some((headerName) => {\n return response.headers.get(headerName) === this._headers[headerName];\n });\n }\n if (process.env.NODE_ENV !== 'production') {\n if (!cacheable) {\n logger.groupCollapsed(`The request for ` +\n `'${getFriendlyURL(response.url)}' returned a response that does ` +\n `not meet the criteria for being cached.`);\n logger.groupCollapsed(`View cacheability criteria here.`);\n logger.log(`Cacheable statuses: ` +\n JSON.stringify(this._statuses));\n logger.log(`Cacheable headers: ` +\n JSON.stringify(this._headers, null, 2));\n logger.groupEnd();\n const logFriendlyHeaders = {};\n response.headers.forEach((value, key) => {\n logFriendlyHeaders[key] = value;\n });\n logger.groupCollapsed(`View response status and headers here.`);\n logger.log(`Response status: ` + responsLine truncated
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
this.workbox=this.workbox||{},this.workbox.cacheableResponse=function(s){"use strict";try{self["workbox:cacheable-response:5.1.4"]&&_()}catch(s){}class t{constructor(s={}){this.s=s.statuses,this.t=s.headers}isResponseCacheable(s){let t=!0;return this.s&&(t=this.s.includes(s.status)),this.t&&t&&(t=Object.keys(this.t).some(t=>s.headers.get(t)===this.t[t])),t}}return s.CacheableResponse=t,s.CacheableResponsePlugin=class{constructor(s){this.cacheWillUpdate=async({response:s})=>this.i.isResponseCacheable(s)?s:null,this.i=new t(s)}},s}({});
|
||||
//# sourceMappingURL=workbox-cacheable-response.prod.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-cacheable-response.prod.js","sources":["../_version.js","../CacheableResponse.js","../CacheableResponsePlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:cacheable-response:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport './_version.js';\n/**\n * This class allows you to set up rules determining what\n * status codes and/or headers need to be present in order for a\n * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)\n * to be considered cacheable.\n *\n * @memberof module:workbox-cacheable-response\n */\nclass CacheableResponse {\n /**\n * To construct a new CacheableResponse instance you must provide at least\n * one of the `config` properties.\n *\n * If both `statuses` and `headers` are specified, then both conditions must\n * be met for the `Response` to be considered cacheable.\n *\n * @param {Object} config\n * @param {Array<number>} [config.statuses] One or more status codes that a\n * `Response` can have and be considered cacheable.\n * @param {Object<string,string>} [config.headers] A mapping of header names\n * and expected values that a `Response` can have and be considered cacheable.\n * If multiple headers are provided, only one needs to be present.\n */\n constructor(config = {}) {\n if (process.env.NODE_ENV !== 'production') {\n if (!(config.statuses || config.headers)) {\n throw new WorkboxError('statuses-or-headers-required', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n });\n }\n if (config.statuses) {\n assert.isArray(config.statuses, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.statuses',\n });\n }\n if (config.headers) {\n assert.isType(config.headers, 'object', {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'constructor',\n paramName: 'config.headers',\n });\n }\n }\n this._statuses = config.statuses;\n this._headers = config.headers;\n }\n /**\n * Checks a response to see whether it's cacheable or not, based on this\n * object's configuration.\n *\n * @param {Response} response The response whose cacheability is being\n * checked.\n * @return {boolean} `true` if the `Response` is cacheable, and `false`\n * otherwise.\n */\n isResponseCacheable(response) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(response, Response, {\n moduleName: 'workbox-cacheable-response',\n className: 'CacheableResponse',\n funcName: 'isResponseCacheable',\n paramName: 'response',\n });\n }\n let cacheable = true;\n if (this._statuses) {\n cacheable = this._statuses.includes(response.status);\n }\n if (this._headers && cacheable) {\n cacheable = Object.keys(this._headers).some((headerName) => {\n return response.headers.get(headerName) === this._headers[headerName];\n });\n }\n if (process.env.NODE_ENV !== 'production') {\n if (!cacheable) {\n logger.groupCollapsed(`The request for ` +\n `'${getFriendlyURL(response.url)}' returned a response that does ` +\n `not meet the criteria for being cached.`);\n logger.groupCollapsed(`View cacheability criteria here.`);\n logger.log(`Cacheable statuses: ` +\n JSON.stringify(this._statuses));\n logger.log(`Cacheable headers: ` +\n JSON.stringify(this._headers, null, 2));\n logger.groupEnd();\n const logFriendlyHeaders = {};\n response.headers.forEach((value, key) => {\n logFriendlyHeaders[key] = value;\n });\n logger.groupCollapsed(`View response status and headers here.`);\n logger.log(`Response status: ` + responLine truncated
|
||||
+1858
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-core.dev.js","sources":["../_version.js","../_private/logger.js","../models/messages/messages.js","../models/messages/messageGenerator.js","../_private/WorkboxError.js","../_private/assert.js","../models/quotaErrorCallbacks.js","../registerQuotaErrorCallback.js","../_private/cacheNames.js","../_private/executeQuotaErrorCallbacks.js","../_private/getFriendlyURL.js","../utils/pluginUtils.js","../_private/cacheWrapper.js","../_private/canConstructReadableStream.js","../_private/canConstructResponseFromBodyStream.js","../_private/dontWaitFor.js","../_private/DBWrapper.js","../_private/Deferred.js","../_private/deleteDatabase.js","../_private/fetchWrapper.js","../_private/timeout.js","../_private/resultingClientExists.js","../_private.js","../cacheNames.js","../copyResponse.js","../clientsClaim.js","../setCacheNameDetails.js","../skipWaiting.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:core:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2019 Google LLC\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nconst logger = (process.env.NODE_ENV === 'production' ? null : (() => {\n // Don't overwrite this value if it's already set.\n // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923\n if (!('__WB_DISABLE_DEV_LOGS' in self)) {\n self.__WB_DISABLE_DEV_LOGS = false;\n }\n let inGroup = false;\n const methodToColorMap = {\n debug: `#7f8c8d`,\n log: `#2ecc71`,\n warn: `#f39c12`,\n error: `#c0392b`,\n groupCollapsed: `#3498db`,\n groupEnd: null,\n };\n const print = function (method, args) {\n if (self.__WB_DISABLE_DEV_LOGS) {\n return;\n }\n if (method === 'groupCollapsed') {\n // Safari doesn't print all console.groupCollapsed() arguments:\n // https://bugs.webkit.org/show_bug.cgi?id=182754\n if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {\n console[method](...args);\n return;\n }\n }\n const styles = [\n `background: ${methodToColorMap[method]}`,\n `border-radius: 0.5em`,\n `color: white`,\n `font-weight: bold`,\n `padding: 2px 0.5em`,\n ];\n // When in a group, the workbox prefix is not displayed.\n const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')];\n console[method](...logPrefix, ...args);\n if (method === 'groupCollapsed') {\n inGroup = true;\n }\n if (method === 'groupEnd') {\n inGroup = false;\n }\n };\n const api = {};\n const loggerMethods = Object.keys(methodToColorMap);\n for (const key of loggerMethods) {\n const method = key;\n api[method] = (...args) => {\n print(method, args);\n };\n }\n return api;\n})());\nexport { logger };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../../_version.js';\nexport const messages = {\n 'invalid-value': ({ paramName, validValueDescription, value }) => {\n if (!paramName || !validValueDescription) {\n throw new Error(`Unexpected input to 'invalid-value' error.`);\n }\n return `The '${paramName}' parameter was given a value with an ` +\n `unexpected value. ${validValueDescription} Received a value of ` +\n `${JSON.stringify(value)}.`;\n },\n 'not-an-array': ({ moduleName, className, funcName, paramName }) => {\n if (!moduleName || !className || !funcName || !paramName) {\n throw new Error(`Unexpected input to 'not-an-array' error.`);\n }\n return `The parameter '${paramName}' passed into ` +\n `'${moduleName}.${className}.${funcName}()' must be an array.`;\n },\n 'incorrect-type': ({ expectedType, paramName, moduleName, className, funcName }) => {\n if (!expectedType || !paramName || !moduleName || !funcName) {\n throw new Error(`Unexpected input to 'incorrect-type' error.`);\n }\n return `The parameter '${paramName}' passed into ` +\n `'${moduleName}.${className ? (className + '.') : ''}` +\n `${funcName}()' must be of type ${expectedType}.`;\n },\n 'incorrect-class': ({ expectedClass, paramName, moduleName, className, funcName, isReturnValueProblem }) => {\n if (!expectedClass || !moduleName || !funcName) {\n throw new Error(`Unexpected input to 'incorrect-class' error.`);\n }\n if (isReturnValueProblem) {\n return `The return value from ` +\n `'${moduleName}.${classNaLine truncated
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
this.workbox=this.workbox||{},this.workbox.core=function(e){"use strict";try{self["workbox:core:5.1.4"]&&_()}catch(e){}const t=(e,...t)=>{let n=e;return t.length>0&&(n+=" :: "+JSON.stringify(t)),n};class n extends Error{constructor(e,n){super(t(e,n)),this.name=e,this.details=n}}const s=new Set;const r={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},i=e=>[r.prefix,e,r.suffix].filter(e=>e&&e.length>0).join("-"),o={updateDetails:e=>{(e=>{for(const t of Object.keys(r))e(t)})(t=>{"string"==typeof e[t]&&(r[t]=e[t])})},getGoogleAnalyticsName:e=>e||i(r.googleAnalytics),getPrecacheName:e=>e||i(r.precache),getPrefix:()=>r.prefix,getRuntimeName:e=>e||i(r.runtime),getSuffix:()=>r.suffix};async function a(){for(const e of s)await e()}const c=e=>new URL(String(e),location.href).href.replace(new RegExp("^"+location.origin),""),u=(e,t)=>e.filter(e=>t in e),l=async({request:e,mode:t,plugins:n=[]})=>{const s=u(n,"cacheKeyWillBeUsed");let r=e;for(const e of s)r=await e.cacheKeyWillBeUsed.call(e,{mode:t,request:r}),"string"==typeof r&&(r=new Request(r));return r},f=async({cacheName:e,request:t,event:n,matchOptions:s,plugins:r=[]})=>{const i=await self.caches.open(e),o=await l({plugins:r,request:t,mode:"read"});let a=await i.match(o,s);for(const t of r)if("cachedResponseWillBeUsed"in t){const r=t.cachedResponseWillBeUsed;a=await r.call(t,{cacheName:e,event:n,matchOptions:s,cachedResponse:a,request:o})}return a},h={put:async({cacheName:e,request:t,response:s,event:r,plugins:i=[],matchOptions:o})=>{const h=await l({plugins:i,request:t,mode:"write"});if(!s)throw new n("cache-put-with-no-response",{url:c(h.url)});const w=await(async({request:e,response:t,event:n,plugins:s=[]})=>{let r=t,i=!1;for(const t of s)if("cacheWillUpdate"in t){i=!0;const s=t.cacheWillUpdate;if(r=await s.call(t,{request:e,response:r,event:n}),!r)break}return i||(r=r&&200===r.status?r:void 0),r||null})({event:r,plugins:i,response:s,request:h});if(!w)return;const p=await self.caches.open(e),d=u(i,"cacheDidUpdate"),g=d.length>0?await f({cacheName:e,matchOptions:o,request:h}):null;try{await p.put(h,w)}catch(e){throw"QuotaExceededError"===e.name&&await a(),e}for(const t of d)await t.cacheDidUpdate.call(t,{cacheName:e,event:r,oldResponse:g,newResponse:w,request:h})},match:f};let w,p;function d(){if(void 0===p){const e=new Response("");if("body"in e)try{new Response(e.body),p=!0}catch(e){p=!1}p=!1}return p}class g{constructor(e,t,{onupgradeneeded:n,onversionchange:s}={}){this.t=null,this.s=e,this.i=t,this.o=n,this.u=s||(()=>this.close())}get db(){return this.t}async open(){if(!this.t)return this.t=await new Promise((e,t)=>{let n=!1;setTimeout(()=>{n=!0,t(new Error("The open request was blocked and timed out"))},this.OPEN_TIMEOUT);const s=indexedDB.open(this.s,this.i);s.onerror=()=>t(s.error),s.onupgradeneeded=e=>{n?(s.transaction.abort(),s.result.close()):"function"==typeof this.o&&this.o(e)},s.onsuccess=()=>{const t=s.result;n?t.close():(t.onversionchange=this.u.bind(this),e(t))}}),this}async getKey(e,t){return(await this.getAllKeys(e,t,1))[0]}async getAll(e,t,n){return await this.getAllMatching(e,{query:t,count:n})}async getAllKeys(e,t,n){return(await this.getAllMatching(e,{query:t,count:n,includeKeys:!0})).map(e=>e.key)}async getAllMatching(e,{index:t,query:n=null,direction:s="next",count:r,includeKeys:i=!1}={}){return await this.transaction([e],"readonly",(o,a)=>{const c=o.objectStore(e),u=t?c.index(t):c,l=[],f=u.openCursor(n,s);f.onsuccess=()=>{const e=f.result;e?(l.push(i?e:e.value),r&&l.length>=r?a(l):e.continue()):a(l)}})}async transaction(e,t,n){return await this.open(),await new Promise((s,r)=>{const i=this.t.transaction(e,t);i.onabort=()=>r(i.error),i.oncomplete=()=>s(),n(i,e=>s(e))})}async l(e,t,n,...s){return await this.transaction([t],n,(n,r)=>{const i=n.objectStore(t),o=i[e].apply(i,s);o.onsuccess=()=>r(o.result)})}close(){this.t&&(this.t.close(),this.t=null)}}g.prototype.OPEN_TIMEOUT=2e3;const y={readonly:["get","count","getKey","getAll","getAllKeys"],readwrite:["add","put","clear","delete"]};for(const[e,t]of Object.entries(y))for(const n of t)n in IDBObjectStore.prototype&&(g.prototype[n]=async function(t,...s){return await this.l(n,t,e,...s)});const m={fetch:async({request:e,fetchOptions:t,event:s,plugins:r=[]})=>{if("string"==typeof e&&(e=new Request(e)),s instanceof FetchEvent&&s.preloadResponse){const e=await s.preloadResponse;if(e)return e}const i=u(r,"fetchDidFail"),o=i.length>0?e.clone():null;try{for(const t of r)if("requestWillFetch"in t){const n=t.requestWillFetch,r=e.clone();e=await n.call(t,{request:r,event:s})}}catch(e){throw new n("plugin-error-request-will-fetch",{thrownError:e})}const a=e.clone();try{let n;n="navigate"===e.mode?await fetch(e):await fetch(e,t);for(const e of r)"fetchDidSucceed"in e&&(n=await e.fetchDidSucceed.call(e,{event:s,request:a,response:n}));return n}catch(e){for(const t of i)await t.fetchDidFail.call(t,{Line truncated
|
||||
//# sourceMappingURL=workbox-core.prod.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-core.prod.js","sources":["../_version.js","../_private/logger.js","../models/messages/messageGenerator.js","../_private/WorkboxError.js","../_private/assert.js","../models/quotaErrorCallbacks.js","../_private/cacheNames.js","../_private/executeQuotaErrorCallbacks.js","../_private/getFriendlyURL.js","../utils/pluginUtils.js","../_private/cacheWrapper.js","../_private/canConstructReadableStream.js","../_private/canConstructResponseFromBodyStream.js","../_private/DBWrapper.js","../_private/deleteDatabase.js","../_private/fetchWrapper.js","../_private/timeout.js","../_private/dontWaitFor.js","../_private/Deferred.js","../_private/resultingClientExists.js","../cacheNames.js","../clientsClaim.js","../copyResponse.js","../registerQuotaErrorCallback.js","../setCacheNameDetails.js","../skipWaiting.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:core:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2019 Google LLC\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nconst logger = (process.env.NODE_ENV === 'production' ? null : (() => {\n // Don't overwrite this value if it's already set.\n // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923\n if (!('__WB_DISABLE_DEV_LOGS' in self)) {\n self.__WB_DISABLE_DEV_LOGS = false;\n }\n let inGroup = false;\n const methodToColorMap = {\n debug: `#7f8c8d`,\n log: `#2ecc71`,\n warn: `#f39c12`,\n error: `#c0392b`,\n groupCollapsed: `#3498db`,\n groupEnd: null,\n };\n const print = function (method, args) {\n if (self.__WB_DISABLE_DEV_LOGS) {\n return;\n }\n if (method === 'groupCollapsed') {\n // Safari doesn't print all console.groupCollapsed() arguments:\n // https://bugs.webkit.org/show_bug.cgi?id=182754\n if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {\n console[method](...args);\n return;\n }\n }\n const styles = [\n `background: ${methodToColorMap[method]}`,\n `border-radius: 0.5em`,\n `color: white`,\n `font-weight: bold`,\n `padding: 2px 0.5em`,\n ];\n // When in a group, the workbox prefix is not displayed.\n const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')];\n console[method](...logPrefix, ...args);\n if (method === 'groupCollapsed') {\n inGroup = true;\n }\n if (method === 'groupEnd') {\n inGroup = false;\n }\n };\n const api = {};\n const loggerMethods = Object.keys(methodToColorMap);\n for (const key of loggerMethods) {\n const method = key;\n api[method] = (...args) => {\n print(method, args);\n };\n }\n return api;\n})());\nexport { logger };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { messages } from './messages.js';\nimport '../../_version.js';\nconst fallback = (code, ...args) => {\n let msg = code;\n if (args.length > 0) {\n msg += ` :: ${JSON.stringify(args)}`;\n }\n return msg;\n};\nconst generatorFunction = (code, details = {}) => {\n const message = messages[code];\n if (!message) {\n throw new Error(`Unable to find message for code '${code}'.`);\n }\n return message(details);\n};\nexport const messageGenerator = (process.env.NODE_ENV === 'production') ?\n fallback : generatorFunction;\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { messageGenerator } from '../models/messages/messageGenerator.js';\nimport '../_version.js';\n/**\n * Workbox errors should be thrown with this class.\n * This allows use to ensure the type easily in tests,\n * helps developers identify errors from workbox\n * easily and allows use to optimise error\n * messages correctly.\n *\n * @private\n */\nclass WorkboxError extends Error {\n /**\n *\n * @param {string} errorCode The error code that\n * identifies this particular error.\n * @param {Object=} details Any relevant arguments\n * that will help developers identify issues should\n * be added as a key on the context object.\n */\n constructor(errorCode, details) {\n const message = messageGenerator(errorCode, details);\n super(message);\n this.name = errorCode;\n this.details = details;\n }\n}\nexport { WorkboxError };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by aLine truncated
|
||||
+649
@@ -0,0 +1,649 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.expiration = (function (exports, assert_js, dontWaitFor_js, logger_js, WorkboxError_js, DBWrapper_js, deleteDatabase_js, cacheNames_js, getFriendlyURL_js, registerQuotaErrorCallback_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:expiration:5.1.4'] && _();
|
||||
} 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.
|
||||
*/
|
||||
const DB_NAME = 'workbox-expiration';
|
||||
const OBJECT_STORE_NAME = 'cache-entries';
|
||||
|
||||
const normalizeURL = unNormalizedUrl => {
|
||||
const url = new URL(unNormalizedUrl, location.href);
|
||||
url.hash = '';
|
||||
return url.href;
|
||||
};
|
||||
/**
|
||||
* Returns the timestamp model.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
class CacheTimestampsModel {
|
||||
/**
|
||||
*
|
||||
* @param {string} cacheName
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
constructor(cacheName) {
|
||||
this._cacheName = cacheName;
|
||||
this._db = new DBWrapper_js.DBWrapper(DB_NAME, 1, {
|
||||
onupgradeneeded: event => this._handleUpgrade(event)
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Should perform an upgrade of indexedDB.
|
||||
*
|
||||
* @param {Event} event
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_handleUpgrade(event) {
|
||||
const db = event.target.result; // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we
|
||||
// have to use the `id` keyPath here and create our own values (a
|
||||
// concatenation of `url + cacheName`) instead of simply using
|
||||
// `keyPath: ['url', 'cacheName']`, which is supported in other browsers.
|
||||
|
||||
const objStore = db.createObjectStore(OBJECT_STORE_NAME, {
|
||||
keyPath: 'id'
|
||||
}); // TODO(philipwalton): once we don't have to support EdgeHTML, we can
|
||||
// create a single index with the keyPath `['cacheName', 'timestamp']`
|
||||
// instead of doing both these indexes.
|
||||
|
||||
objStore.createIndex('cacheName', 'cacheName', {
|
||||
unique: false
|
||||
});
|
||||
objStore.createIndex('timestamp', 'timestamp', {
|
||||
unique: false
|
||||
}); // Previous versions of `workbox-expiration` used `this._cacheName`
|
||||
// as the IDBDatabase name.
|
||||
|
||||
deleteDatabase_js.deleteDatabase(this._cacheName);
|
||||
}
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {number} timestamp
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async setTimestamp(url, timestamp) {
|
||||
url = normalizeURL(url);
|
||||
const entry = {
|
||||
url,
|
||||
timestamp,
|
||||
cacheName: this._cacheName,
|
||||
// Creating an ID from the URL and cache name won't be necessary once
|
||||
// Edge switches to Chromium and all browsers we support work with
|
||||
// array keyPaths.
|
||||
id: this._getId(url)
|
||||
};
|
||||
await this._db.put(OBJECT_STORE_NAME, entry);
|
||||
}
|
||||
/**
|
||||
* Returns the timestamp stored for a given URL.
|
||||
*
|
||||
* @param {string} url
|
||||
* @return {number}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async getTimestamp(url) {
|
||||
const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url));
|
||||
return entry.timestamp;
|
||||
}
|
||||
/**
|
||||
* Iterates through all the entries in the object store (from newest to
|
||||
* oldest) and removes entries once either `maxCount` is reached or the
|
||||
* entry's timestamp is less than `minTimestamp`.
|
||||
*
|
||||
* @param {number} minTimestamp
|
||||
* @param {number} maxCount
|
||||
* @return {Array<string>}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
async expireEntries(minTimestamp, maxCount) {
|
||||
const entriesToDelete = await this._db.transaction(OBJECT_STORE_NAME, 'readwrite', (txn, done) => {
|
||||
const store = txn.objectStore(OBJECT_STORE_NAME);
|
||||
const request = store.index('timestamp').openCursor(null, 'prev');
|
||||
const entriesToDelete = [];
|
||||
let entriesNotDeletedCount = 0;
|
||||
|
||||
request.onsuccess = () => {
|
||||
const cursor = request.result;
|
||||
|
||||
if (cursor) {
|
||||
const result = cursor.value; // TODO(philipwalton): once we can use a multi-key index, we
|
||||
// won't have to check `cacheName` here.
|
||||
|
||||
if (result.cacheName === this._cacheName) {
|
||||
// Delete an entry if it's older than the max age or
|
||||
// if we already have the max number allowed.
|
||||
if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) {
|
||||
// TODO(philipwalton): we should be able to delete the
|
||||
// entry right here, but doing so causes an iteration
|
||||
// bug in Safari stable (fixed in TP). Instead we can
|
||||
// store the keys of the entries to delete, and then
|
||||
// delete the separate transactions.
|
||||
// https://github.com/GoogleChrome/workbox/issues/1978
|
||||
// cursor.delete();
|
||||
// We only need to return the URL, not the whole entry.
|
||||
entriesToDelete.push(cursor.value);
|
||||
} else {
|
||||
entriesNotDeletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
cursor.continue();
|
||||
} else {
|
||||
done(entriesToDelete);
|
||||
}
|
||||
};
|
||||
}); // TODO(philipwalton): once the Safari bug in the following issue is fixed,
|
||||
// we should be able to remove this loop and do the entry deletion in the
|
||||
// cursor loop above:
|
||||
// https://github.com/GoogleChrome/workbox/issues/1978
|
||||
|
||||
const urlsDeleted = [];
|
||||
|
||||
for (const entry of entriesToDelete) {
|
||||
await this._db.delete(OBJECT_STORE_NAME, entry.id);
|
||||
urlsDeleted.push(entry.url);
|
||||
}
|
||||
|
||||
return urlsDeleted;
|
||||
}
|
||||
/**
|
||||
* Takes a URL and returns an ID that will be unique in the object store.
|
||||
*
|
||||
* @param {string} url
|
||||
* @return {string}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_getId(url) {
|
||||
// Creating an ID from the URL and cache name won't be necessary once
|
||||
// Edge switches to Chromium and all browsers we support work with
|
||||
// array keyPaths.
|
||||
return this._cacheName + '|' + normalizeURL(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
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 `CacheExpiration` class allows you define an expiration and / or
|
||||
* limit on the number of responses stored in a
|
||||
* [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
|
||||
*
|
||||
* @memberof module:workbox-expiration
|
||||
*/
|
||||
|
||||
class CacheExpiration {
|
||||
/**
|
||||
* To construct a new CacheExpiration instance you must provide at least
|
||||
* one of the `config` properties.
|
||||
*
|
||||
* @param {string} cacheName Name of the cache to apply restrictions to.
|
||||
* @param {Object} config
|
||||
* @param {number} [config.maxEntries] The maximum number of entries to cache.
|
||||
* Entries used the least will be removed as the maximum is reached.
|
||||
* @param {number} [config.maxAgeSeconds] The maximum age of an entry before
|
||||
* it's treated as stale and removed.
|
||||
*/
|
||||
constructor(cacheName, config = {}) {
|
||||
this._isRunning = false;
|
||||
this._rerunRequested = false;
|
||||
|
||||
{
|
||||
assert_js.assert.isType(cacheName, 'string', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'CacheExpiration',
|
||||
funcName: 'constructor',
|
||||
paramName: 'cacheName'
|
||||
});
|
||||
|
||||
if (!(config.maxEntries || config.maxAgeSeconds)) {
|
||||
throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'CacheExpiration',
|
||||
funcName: 'constructor'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.maxEntries) {
|
||||
assert_js.assert.isType(config.maxEntries, 'number', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'CacheExpiration',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.maxEntries'
|
||||
}); // TODO: Assert is positive
|
||||
}
|
||||
|
||||
if (config.maxAgeSeconds) {
|
||||
assert_js.assert.isType(config.maxAgeSeconds, 'number', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'CacheExpiration',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.maxAgeSeconds'
|
||||
}); // TODO: Assert is positive
|
||||
}
|
||||
}
|
||||
|
||||
this._maxEntries = config.maxEntries;
|
||||
this._maxAgeSeconds = config.maxAgeSeconds;
|
||||
this._cacheName = cacheName;
|
||||
this._timestampModel = new CacheTimestampsModel(cacheName);
|
||||
}
|
||||
/**
|
||||
* Expires entries for the given cache and given criteria.
|
||||
*/
|
||||
|
||||
|
||||
async expireEntries() {
|
||||
if (this._isRunning) {
|
||||
this._rerunRequested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._isRunning = true;
|
||||
const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0;
|
||||
const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); // Delete URLs from the cache
|
||||
|
||||
const cache = await self.caches.open(this._cacheName);
|
||||
|
||||
for (const url of urlsExpired) {
|
||||
await cache.delete(url);
|
||||
}
|
||||
|
||||
{
|
||||
if (urlsExpired.length > 0) {
|
||||
logger_js.logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` + `${urlsExpired.length === 1 ? 'it' : 'them'} from the ` + `'${this._cacheName}' cache.`);
|
||||
logger_js.logger.log(`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`);
|
||||
urlsExpired.forEach(url => logger_js.logger.log(` ${url}`));
|
||||
logger_js.logger.groupEnd();
|
||||
} else {
|
||||
logger_js.logger.debug(`Cache expiration ran and found no entries to remove.`);
|
||||
}
|
||||
}
|
||||
|
||||
this._isRunning = false;
|
||||
|
||||
if (this._rerunRequested) {
|
||||
this._rerunRequested = false;
|
||||
dontWaitFor_js.dontWaitFor(this.expireEntries());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update the timestamp for the given URL. This ensures the when
|
||||
* removing entries based on maximum entries, most recently used
|
||||
* is accurate or when expiring, the timestamp is up-to-date.
|
||||
*
|
||||
* @param {string} url
|
||||
*/
|
||||
|
||||
|
||||
async updateTimestamp(url) {
|
||||
{
|
||||
assert_js.assert.isType(url, 'string', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'CacheExpiration',
|
||||
funcName: 'updateTimestamp',
|
||||
paramName: 'url'
|
||||
});
|
||||
}
|
||||
|
||||
await this._timestampModel.setTimestamp(url, Date.now());
|
||||
}
|
||||
/**
|
||||
* Can be used to check if a URL has expired or not before it's used.
|
||||
*
|
||||
* This requires a look up from IndexedDB, so can be slow.
|
||||
*
|
||||
* Note: This method will not remove the cached entry, call
|
||||
* `expireEntries()` to remove indexedDB and Cache entries.
|
||||
*
|
||||
* @param {string} url
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
||||
|
||||
async isURLExpired(url) {
|
||||
if (!this._maxAgeSeconds) {
|
||||
{
|
||||
throw new WorkboxError_js.WorkboxError(`expired-test-without-max-age`, {
|
||||
methodName: 'isURLExpired',
|
||||
paramName: 'maxAgeSeconds'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const timestamp = await this._timestampModel.getTimestamp(url);
|
||||
const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000;
|
||||
return timestamp < expireOlderThan;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes the IndexedDB object store used to keep track of cache expiration
|
||||
* metadata.
|
||||
*/
|
||||
|
||||
|
||||
async delete() {
|
||||
// Make sure we don't attempt another rerun if we're called in the middle of
|
||||
// a cache expiration.
|
||||
this._rerunRequested = false;
|
||||
await this._timestampModel.expireEntries(Infinity); // Expires all.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
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 plugin can be used in the Workbox APIs to regularly enforce a
|
||||
* limit on the age and / or the number of cached requests.
|
||||
*
|
||||
* Whenever a cached request is used or updated, this plugin will look
|
||||
* at the used Cache and remove any old or extra requests.
|
||||
*
|
||||
* When using `maxAgeSeconds`, requests may be used *once* after expiring
|
||||
* because the expiration clean up will not have occurred until *after* the
|
||||
* cached request has been used. If the request has a "Date" header, then
|
||||
* a light weight expiration check is performed and the request will not be
|
||||
* used immediately.
|
||||
*
|
||||
* When using `maxEntries`, the entry least-recently requested will be removed
|
||||
* from the cache first.
|
||||
*
|
||||
* @memberof module:workbox-expiration
|
||||
*/
|
||||
|
||||
class ExpirationPlugin {
|
||||
/**
|
||||
* @param {Object} config
|
||||
* @param {number} [config.maxEntries] The maximum number of entries to cache.
|
||||
* Entries used the least will be removed as the maximum is reached.
|
||||
* @param {number} [config.maxAgeSeconds] The maximum age of an entry before
|
||||
* it's treated as stale and removed.
|
||||
* @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to
|
||||
* automatic deletion if the available storage quota has been exceeded.
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
/**
|
||||
* A "lifecycle" callback that will be triggered automatically by the
|
||||
* `workbox-strategies` handlers when a `Response` is about to be returned
|
||||
* from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to
|
||||
* the handler. It allows the `Response` to be inspected for freshness and
|
||||
* prevents it from being used if the `Response`'s `Date` header value is
|
||||
* older than the configured `maxAgeSeconds`.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.cacheName Name of the cache the response is in.
|
||||
* @param {Response} options.cachedResponse The `Response` object that's been
|
||||
* read from a cache and whose freshness should be checked.
|
||||
* @return {Response} Either the `cachedResponse`, if it's
|
||||
* fresh, or `null` if the `Response` is older than `maxAgeSeconds`.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
this.cachedResponseWillBeUsed = async ({
|
||||
event,
|
||||
request,
|
||||
cacheName,
|
||||
cachedResponse
|
||||
}) => {
|
||||
if (!cachedResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFresh = this._isResponseDateFresh(cachedResponse); // Expire entries to ensure that even if the expiration date has
|
||||
// expired, it'll only be used once.
|
||||
|
||||
|
||||
const cacheExpiration = this._getCacheExpiration(cacheName);
|
||||
|
||||
dontWaitFor_js.dontWaitFor(cacheExpiration.expireEntries()); // Update the metadata for the request URL to the current timestamp,
|
||||
// but don't `await` it as we don't want to block the response.
|
||||
|
||||
const updateTimestampDone = cacheExpiration.updateTimestamp(request.url);
|
||||
|
||||
if (event) {
|
||||
try {
|
||||
event.waitUntil(updateTimestampDone);
|
||||
} catch (error) {
|
||||
{
|
||||
// The event may not be a fetch event; only log the URL if it is.
|
||||
if ('request' in event) {
|
||||
logger_js.logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache entry for ` + `'${getFriendlyURL_js.getFriendlyURL(event.request.url)}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isFresh ? cachedResponse : null;
|
||||
};
|
||||
/**
|
||||
* A "lifecycle" callback that will be triggered automatically by the
|
||||
* `workbox-strategies` handlers when an entry is added to a cache.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.cacheName Name of the cache that was updated.
|
||||
* @param {string} options.request The Request for the cached entry.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
this.cacheDidUpdate = async ({
|
||||
cacheName,
|
||||
request
|
||||
}) => {
|
||||
{
|
||||
assert_js.assert.isType(cacheName, 'string', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'Plugin',
|
||||
funcName: 'cacheDidUpdate',
|
||||
paramName: 'cacheName'
|
||||
});
|
||||
assert_js.assert.isInstance(request, Request, {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'Plugin',
|
||||
funcName: 'cacheDidUpdate',
|
||||
paramName: 'request'
|
||||
});
|
||||
}
|
||||
|
||||
const cacheExpiration = this._getCacheExpiration(cacheName);
|
||||
|
||||
await cacheExpiration.updateTimestamp(request.url);
|
||||
await cacheExpiration.expireEntries();
|
||||
};
|
||||
|
||||
{
|
||||
if (!(config.maxEntries || config.maxAgeSeconds)) {
|
||||
throw new WorkboxError_js.WorkboxError('max-entries-or-age-required', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'Plugin',
|
||||
funcName: 'constructor'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.maxEntries) {
|
||||
assert_js.assert.isType(config.maxEntries, 'number', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'Plugin',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.maxEntries'
|
||||
});
|
||||
}
|
||||
|
||||
if (config.maxAgeSeconds) {
|
||||
assert_js.assert.isType(config.maxAgeSeconds, 'number', {
|
||||
moduleName: 'workbox-expiration',
|
||||
className: 'Plugin',
|
||||
funcName: 'constructor',
|
||||
paramName: 'config.maxAgeSeconds'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._maxAgeSeconds = config.maxAgeSeconds;
|
||||
this._cacheExpirations = new Map();
|
||||
|
||||
if (config.purgeOnQuotaError) {
|
||||
registerQuotaErrorCallback_js.registerQuotaErrorCallback(() => this.deleteCacheAndMetadata());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A simple helper method to return a CacheExpiration instance for a given
|
||||
* cache name.
|
||||
*
|
||||
* @param {string} cacheName
|
||||
* @return {CacheExpiration}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_getCacheExpiration(cacheName) {
|
||||
if (cacheName === cacheNames_js.cacheNames.getRuntimeName()) {
|
||||
throw new WorkboxError_js.WorkboxError('expire-custom-caches-only');
|
||||
}
|
||||
|
||||
let cacheExpiration = this._cacheExpirations.get(cacheName);
|
||||
|
||||
if (!cacheExpiration) {
|
||||
cacheExpiration = new CacheExpiration(cacheName, this._config);
|
||||
|
||||
this._cacheExpirations.set(cacheName, cacheExpiration);
|
||||
}
|
||||
|
||||
return cacheExpiration;
|
||||
}
|
||||
/**
|
||||
* @param {Response} cachedResponse
|
||||
* @return {boolean}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_isResponseDateFresh(cachedResponse) {
|
||||
if (!this._maxAgeSeconds) {
|
||||
// We aren't expiring by age, so return true, it's fresh
|
||||
return true;
|
||||
} // Check if the 'date' header will suffice a quick expiration check.
|
||||
// See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for
|
||||
// discussion.
|
||||
|
||||
|
||||
const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse);
|
||||
|
||||
if (dateHeaderTimestamp === null) {
|
||||
// Unable to parse date, so assume it's fresh.
|
||||
return true;
|
||||
} // If we have a valid headerTime, then our response is fresh iff the
|
||||
// headerTime plus maxAgeSeconds is greater than the current time.
|
||||
|
||||
|
||||
const now = Date.now();
|
||||
return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000;
|
||||
}
|
||||
/**
|
||||
* This method will extract the data header and parse it into a useful
|
||||
* value.
|
||||
*
|
||||
* @param {Response} cachedResponse
|
||||
* @return {number|null}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
|
||||
|
||||
_getDateHeaderTimestamp(cachedResponse) {
|
||||
if (!cachedResponse.headers.has('date')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateHeader = cachedResponse.headers.get('date');
|
||||
const parsedDate = new Date(dateHeader);
|
||||
const headerTime = parsedDate.getTime(); // If the Date header was invalid for some reason, parsedDate.getTime()
|
||||
// will return NaN.
|
||||
|
||||
if (isNaN(headerTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return headerTime;
|
||||
}
|
||||
/**
|
||||
* This is a helper method that performs two operations:
|
||||
*
|
||||
* - Deletes *all* the underlying Cache instances associated with this plugin
|
||||
* instance, by calling caches.delete() on your behalf.
|
||||
* - Deletes the metadata from IndexedDB used to keep track of expiration
|
||||
* details for each Cache instance.
|
||||
*
|
||||
* When using cache expiration, calling this method is preferable to calling
|
||||
* `caches.delete()` directly, since this will ensure that the IndexedDB
|
||||
* metadata is also cleanly removed and open IndexedDB instances are deleted.
|
||||
*
|
||||
* Note that if you're *not* using cache expiration for a given cache, calling
|
||||
* `caches.delete()` and passing in the cache's name should be sufficient.
|
||||
* There is no Workbox-specific method needed for cleanup in that case.
|
||||
*/
|
||||
|
||||
|
||||
async deleteCacheAndMetadata() {
|
||||
// Do this one at a time instead of all at once via `Promise.all()` to
|
||||
// reduce the chance of inconsistency if a promise rejects.
|
||||
for (const [cacheName, cacheExpiration] of this._cacheExpirations) {
|
||||
await self.caches.delete(cacheName);
|
||||
await cacheExpiration.delete();
|
||||
} // Reset this._cacheExpirations to its initial state.
|
||||
|
||||
|
||||
this._cacheExpirations = new Map();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exports.CacheExpiration = CacheExpiration;
|
||||
exports.ExpirationPlugin = ExpirationPlugin;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core._private, workbox.core));
|
||||
//# sourceMappingURL=workbox-expiration.dev.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-expiration.dev.js","sources":["../_version.js","../models/CacheTimestampsModel.js","../CacheExpiration.js","../ExpirationPlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:expiration:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { DBWrapper } from 'workbox-core/_private/DBWrapper.js';\nimport { deleteDatabase } from 'workbox-core/_private/deleteDatabase.js';\nimport '../_version.js';\nconst DB_NAME = 'workbox-expiration';\nconst OBJECT_STORE_NAME = 'cache-entries';\nconst normalizeURL = (unNormalizedUrl) => {\n const url = new URL(unNormalizedUrl, location.href);\n url.hash = '';\n return url.href;\n};\n/**\n * Returns the timestamp model.\n *\n * @private\n */\nclass CacheTimestampsModel {\n /**\n *\n * @param {string} cacheName\n *\n * @private\n */\n constructor(cacheName) {\n this._cacheName = cacheName;\n this._db = new DBWrapper(DB_NAME, 1, {\n onupgradeneeded: (event) => this._handleUpgrade(event),\n });\n }\n /**\n * Should perform an upgrade of indexedDB.\n *\n * @param {Event} event\n *\n * @private\n */\n _handleUpgrade(event) {\n const db = event.target.result;\n // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we\n // have to use the `id` keyPath here and create our own values (a\n // concatenation of `url + cacheName`) instead of simply using\n // `keyPath: ['url', 'cacheName']`, which is supported in other browsers.\n const objStore = db.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'id' });\n // TODO(philipwalton): once we don't have to support EdgeHTML, we can\n // create a single index with the keyPath `['cacheName', 'timestamp']`\n // instead of doing both these indexes.\n objStore.createIndex('cacheName', 'cacheName', { unique: false });\n objStore.createIndex('timestamp', 'timestamp', { unique: false });\n // Previous versions of `workbox-expiration` used `this._cacheName`\n // as the IDBDatabase name.\n deleteDatabase(this._cacheName);\n }\n /**\n * @param {string} url\n * @param {number} timestamp\n *\n * @private\n */\n async setTimestamp(url, timestamp) {\n url = normalizeURL(url);\n const entry = {\n url,\n timestamp,\n cacheName: this._cacheName,\n // Creating an ID from the URL and cache name won't be necessary once\n // Edge switches to Chromium and all browsers we support work with\n // array keyPaths.\n id: this._getId(url),\n };\n await this._db.put(OBJECT_STORE_NAME, entry);\n }\n /**\n * Returns the timestamp stored for a given URL.\n *\n * @param {string} url\n * @return {number}\n *\n * @private\n */\n async getTimestamp(url) {\n const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url));\n return entry.timestamp;\n }\n /**\n * Iterates through all the entries in the object store (from newest to\n * oldest) and removes entries once either `maxCount` is reached or the\n * entry's timestamp is less than `minTimestamp`.\n *\n * @param {number} minTimestamp\n * @param {number} maxCount\n * @return {Array<string>}\n *\n * @private\n */\n async expireEntries(minTimestamp, maxCount) {\n const entriesToDelete = await this._db.transaction(OBJECT_STORE_NAME, 'readwrite', (txn, done) => {\n const store = txn.objectStore(OBJECT_STORE_NAME);\n const request = store.index('timestamp').openCursor(null, 'prev');\n const entriesToDelete = [];\n let entriesNotDeletedCount = 0;\n request.onsuccess = () => {\n const cursor = request.result;\n if (cursor) {\n const result = cursor.value;\n // TODO(philipwalton): once we can use a multi-key index, we\n // won't have to check `cacheName` here.\n if (result.cacheName === this._cacheName) {\n // Delete an entry if it's older than the max age or\n // if we already have the max number allowed.\n if ((minTimestamp && result.timestamp < minTimestamp) ||\n (maxCount && entriesNotDeletedCount >= maxCount)) {\n // TODO(philipwalton): we should be able to delete the\n // entry right here, but doing so causes an iteration\n // bug in Safari stable (fixed in TP). Instead we can\n //Line truncated
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
this.workbox=this.workbox||{},this.workbox.expiration=function(t,e,s,i,a,n,h){"use strict";try{self["workbox:expiration:5.1.4"]&&_()}catch(t){}const r=t=>{const e=new URL(t,location.href);return e.hash="",e.href};class c{constructor(t){this.t=t,this.s=new i.DBWrapper("workbox-expiration",1,{onupgradeneeded:t=>this.i(t)})}i(t){const e=t.target.result.createObjectStore("cache-entries",{keyPath:"id"});e.createIndex("cacheName","cacheName",{unique:!1}),e.createIndex("timestamp","timestamp",{unique:!1}),a.deleteDatabase(this.t)}async setTimestamp(t,e){const s={url:t=r(t),timestamp:e,cacheName:this.t,id:this.h(t)};await this.s.put("cache-entries",s)}async getTimestamp(t){return(await this.s.get("cache-entries",this.h(t))).timestamp}async expireEntries(t,e){const s=await this.s.transaction("cache-entries","readwrite",(s,i)=>{const a=s.objectStore("cache-entries").index("timestamp").openCursor(null,"prev"),n=[];let h=0;a.onsuccess=()=>{const s=a.result;if(s){const i=s.value;i.cacheName===this.t&&(t&&i.timestamp<t||e&&h>=e?n.push(s.value):h++),s.continue()}else i(n)}}),i=[];for(const t of s)await this.s.delete("cache-entries",t.id),i.push(t.url);return i}h(t){return this.t+"|"+r(t)}}class o{constructor(t,e={}){this.o=!1,this.u=!1,this.l=e.maxEntries,this.m=e.maxAgeSeconds,this.t=t,this.p=new c(t)}async expireEntries(){if(this.o)return void(this.u=!0);this.o=!0;const t=this.m?Date.now()-1e3*this.m:0,s=await this.p.expireEntries(t,this.l),i=await self.caches.open(this.t);for(const t of s)await i.delete(t);this.o=!1,this.u&&(this.u=!1,e.dontWaitFor(this.expireEntries()))}async updateTimestamp(t){await this.p.setTimestamp(t,Date.now())}async isURLExpired(t){if(this.m){return await this.p.getTimestamp(t)<Date.now()-1e3*this.m}return!1}async delete(){this.u=!1,await this.p.expireEntries(1/0)}}return t.CacheExpiration=o,t.ExpirationPlugin=class{constructor(t={}){this.cachedResponseWillBeUsed=async({event:t,request:s,cacheName:i,cachedResponse:a})=>{if(!a)return null;const n=this.k(a),h=this.D(i);e.dontWaitFor(h.expireEntries());const r=h.updateTimestamp(s.url);if(t)try{t.waitUntil(r)}catch(t){}return n?a:null},this.cacheDidUpdate=async({cacheName:t,request:e})=>{const s=this.D(t);await s.updateTimestamp(e.url),await s.expireEntries()},this.N=t,this.m=t.maxAgeSeconds,this.g=new Map,t.purgeOnQuotaError&&h.registerQuotaErrorCallback(()=>this.deleteCacheAndMetadata())}D(t){if(t===n.cacheNames.getRuntimeName())throw new s.WorkboxError("expire-custom-caches-only");let e=this.g.get(t);return e||(e=new o(t,this.N),this.g.set(t,e)),e}k(t){if(!this.m)return!0;const e=this._(t);if(null===e)return!0;return e>=Date.now()-1e3*this.m}_(t){if(!t.headers.has("date"))return null;const e=t.headers.get("date"),s=new Date(e).getTime();return isNaN(s)?null:s}async deleteCacheAndMetadata(){for(const[t,e]of this.g)await self.caches.delete(t),await e.delete();this.g=new Map}},t}({},workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core._private,workbox.core);
|
||||
//# sourceMappingURL=workbox-expiration.prod.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"workbox-expiration.prod.js","sources":["../_version.js","../models/CacheTimestampsModel.js","../CacheExpiration.js","../ExpirationPlugin.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:expiration:5.1.4'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { DBWrapper } from 'workbox-core/_private/DBWrapper.js';\nimport { deleteDatabase } from 'workbox-core/_private/deleteDatabase.js';\nimport '../_version.js';\nconst DB_NAME = 'workbox-expiration';\nconst OBJECT_STORE_NAME = 'cache-entries';\nconst normalizeURL = (unNormalizedUrl) => {\n const url = new URL(unNormalizedUrl, location.href);\n url.hash = '';\n return url.href;\n};\n/**\n * Returns the timestamp model.\n *\n * @private\n */\nclass CacheTimestampsModel {\n /**\n *\n * @param {string} cacheName\n *\n * @private\n */\n constructor(cacheName) {\n this._cacheName = cacheName;\n this._db = new DBWrapper(DB_NAME, 1, {\n onupgradeneeded: (event) => this._handleUpgrade(event),\n });\n }\n /**\n * Should perform an upgrade of indexedDB.\n *\n * @param {Event} event\n *\n * @private\n */\n _handleUpgrade(event) {\n const db = event.target.result;\n // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we\n // have to use the `id` keyPath here and create our own values (a\n // concatenation of `url + cacheName`) instead of simply using\n // `keyPath: ['url', 'cacheName']`, which is supported in other browsers.\n const objStore = db.createObjectStore(OBJECT_STORE_NAME, { keyPath: 'id' });\n // TODO(philipwalton): once we don't have to support EdgeHTML, we can\n // create a single index with the keyPath `['cacheName', 'timestamp']`\n // instead of doing both these indexes.\n objStore.createIndex('cacheName', 'cacheName', { unique: false });\n objStore.createIndex('timestamp', 'timestamp', { unique: false });\n // Previous versions of `workbox-expiration` used `this._cacheName`\n // as the IDBDatabase name.\n deleteDatabase(this._cacheName);\n }\n /**\n * @param {string} url\n * @param {number} timestamp\n *\n * @private\n */\n async setTimestamp(url, timestamp) {\n url = normalizeURL(url);\n const entry = {\n url,\n timestamp,\n cacheName: this._cacheName,\n // Creating an ID from the URL and cache name won't be necessary once\n // Edge switches to Chromium and all browsers we support work with\n // array keyPaths.\n id: this._getId(url),\n };\n await this._db.put(OBJECT_STORE_NAME, entry);\n }\n /**\n * Returns the timestamp stored for a given URL.\n *\n * @param {string} url\n * @return {number}\n *\n * @private\n */\n async getTimestamp(url) {\n const entry = await this._db.get(OBJECT_STORE_NAME, this._getId(url));\n return entry.timestamp;\n }\n /**\n * Iterates through all the entries in the object store (from newest to\n * oldest) and removes entries once either `maxCount` is reached or the\n * entry's timestamp is less than `minTimestamp`.\n *\n * @param {number} minTimestamp\n * @param {number} maxCount\n * @return {Array<string>}\n *\n * @private\n */\n async expireEntries(minTimestamp, maxCount) {\n const entriesToDelete = await this._db.transaction(OBJECT_STORE_NAME, 'readwrite', (txn, done) => {\n const store = txn.objectStore(OBJECT_STORE_NAME);\n const request = store.index('timestamp').openCursor(null, 'prev');\n const entriesToDelete = [];\n let entriesNotDeletedCount = 0;\n request.onsuccess = () => {\n const cursor = request.result;\n if (cursor) {\n const result = cursor.value;\n // TODO(philipwalton): once we can use a multi-key index, we\n // won't have to check `cacheName` here.\n if (result.cacheName === this._cacheName) {\n // Delete an entry if it's older than the max age or\n // if we already have the max number allowed.\n if ((minTimestamp && result.timestamp < minTimestamp) ||\n (maxCount && entriesNotDeletedCount >= maxCount)) {\n // TODO(philipwalton): we should be able to delete the\n // entry right here, but doing so causes an iteration\n // bug in Safari stable (fixed in TP). Instead we can\n /Line truncated
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
this.workbox = this.workbox || {};
|
||||
this.workbox.navigationPreload = (function (exports, logger_js) {
|
||||
'use strict';
|
||||
|
||||
try {
|
||||
self['workbox:navigation-preload:5.1.4'] && _();
|
||||
} 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.
|
||||
*/
|
||||
/**
|
||||
* @return {boolean} Whether or not the current browser supports enabling
|
||||
* navigation preload.
|
||||
*
|
||||
* @memberof module:workbox-navigation-preload
|
||||
*/
|
||||
|
||||
function isSupported() {
|
||||
return Boolean(self.registration && self.registration.navigationPreload);
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* If the browser supports Navigation Preload, then this will disable it.
|
||||
*
|
||||
* @memberof module:workbox-navigation-preload
|
||||
*/
|
||||
|
||||
function disable() {
|
||||
if (isSupported()) {
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(self.registration.navigationPreload.disable().then(() => {
|
||||
{
|
||||
logger_js.logger.log(`Navigation preload is disabled.`);
|
||||
}
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
{
|
||||
logger_js.logger.log(`Navigation preload is not supported in this browser.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
/**
|
||||
* If the browser supports Navigation Preload, then this will enable it.
|
||||
*
|
||||
* @param {string} [headerValue] Optionally, allows developers to
|
||||
* [override](https://developers.google.com/web/updates/2017/02/navigation-preload#changing_the_header)
|
||||
* the value of the `Service-Worker-Navigation-Preload` header which will be
|
||||
* sent to the server when making the navigation request.
|
||||
*
|
||||
* @memberof module:workbox-navigation-preload
|
||||
*/
|
||||
|
||||
function enable(headerValue) {
|
||||
if (isSupported()) {
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(self.registration.navigationPreload.enable().then(() => {
|
||||
// Defaults to Service-Worker-Navigation-Preload: true if not set.
|
||||
if (headerValue) {
|
||||
self.registration.navigationPreload.setHeaderValue(headerValue);
|
||||
}
|
||||
|
||||
{
|
||||
logger_js.logger.log(`Navigation preload is enabled.`);
|
||||
}
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
{
|
||||
logger_js.logger.log(`Navigation preload is not supported in this browser.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.disable = disable;
|
||||
exports.enable = enable;
|
||||
exports.isSupported = isSupported;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}, workbox.core._private));
|
||||
//# sourceMappingURL=workbox-navigation-preload.dev.js.map
|
||||
Loaded 100 of 150 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user