first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
// Simple event emitter implementation, but has a slightly unusual API for
// removing specific listeners. If a specific listener needs to be removed
// (instead of all listeners), then it needs to use a "namespace":
// Create a listener on the foo event with bar namespace: .on 'foo.bar'
// Trigger all events for the foo event (including namespaces): .trigger 'foo'
// Remove all listeners for the foo event (including namespaces): .off 'foo'
// Remove a listener for the foo event with the bar namespace: .off 'foo.bar'
export default class EventEmitter {
events: Record<
string,
{
callback: (...args: any[]) => void
namespace: string
}[]
>
constructor() {
this.events = {}
}
on(event: string, callback: (...args: any[]) => void) {
if (!this.events) {
this.events = {}
}
let namespace
;[event, namespace] = Array.from(event.split('.'))
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push({
callback,
namespace,
})
}
off(event?: string, callback?: (...args: any[]) => void) {
if (!this.events) {
this.events = {}
}
if (event) {
let namespace
;[event, namespace] = event.split('.')
if (!this.events[event]) {
this.events[event] = []
}
if (callback) {
this.events[event] = this.events[event].filter(
e => e.callback !== callback
)
} else if (!namespace) {
// Clear all listeners for event
delete this.events[event]
} else {
// Clear only namespaced listeners
this.events[event] = this.events[event].filter(
e => e.namespace !== namespace
)
}
} else {
// Remove all listeners
this.events = {}
}
}
trigger(event: string, ...args: any[]) {
if (!this.events) {
this.events = {}
}
if (this.events[event]) {
this.events[event].forEach(e => e.callback(...args))
}
}
emit(event: string, ...args: any[]) {
this.trigger(event, ...args)
}
}

View File

@@ -0,0 +1,25 @@
export const supportsModernAbortSignal =
typeof AbortSignal.any === 'function' &&
typeof AbortSignal.timeout === 'function'
export const signalWithTimeout = (signal: AbortSignal, timeout: number) => {
if (supportsModernAbortSignal) {
return AbortSignal.any([signal, AbortSignal.timeout(timeout)])
}
const abortController = new AbortController()
const abort = () => {
window.clearTimeout(timer)
signal.removeEventListener('abort', abort)
abortController.abort()
}
// abort after timeout has expired
const timer = window.setTimeout(abort, timeout)
// abort when the original signal is aborted
signal.addEventListener('abort', abort)
return abortController.signal
}

View File

@@ -0,0 +1,13 @@
import moment from 'moment'
export function formatDate(date: moment.MomentInput, format?: string) {
if (!date) return 'N/A'
if (format == null) {
format = 'Do MMM YYYY, h:mm a'
}
return moment(date).format(format)
}
export function fromNowDate(date: moment.MomentInput | string) {
return moment(date).fromNow()
}

View File

@@ -0,0 +1,13 @@
/* eslint-disable no-console */
type DebugConsole = {
debug(...data: any[]): void
log(...data: any[]): void
warn(...data: any[]): void
error(...data: any[]): void
}
export const debugging =
new URLSearchParams(window.location.search).get('debug') === 'true'
export const debugConsole: DebugConsole = debugging
? console
: { debug() {}, log() {}, warn: console.warn, error: console.error }

View File

@@ -0,0 +1,5 @@
// TODO: MIGRATION: Can we use TextDecoder now? https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
// See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
export function decodeUtf8(text: string) {
return decodeURIComponent(escape(text))
}

View File

@@ -0,0 +1,6 @@
export function callFnsInSequence<
Args extends Array<any>,
Fn extends ((...args: Args) => void) | void,
>(...fns: Fn[]) {
return (...args: Args) => fns.forEach(fn => fn?.(...args))
}

View File

@@ -0,0 +1,8 @@
import getMeta from '@/utils/meta'
export function isIEEEBranded() {
const brandVariation = getMeta('ol-brandVariation')
const { ieeeBrandId } = getMeta('ol-ExposedSettings')
return brandVariation?.brand_id === ieeeBrandId
}

View File

@@ -0,0 +1,73 @@
const NETWORK_ERRORS = [
// fetch
'Failed to fetch',
// fetch
'NetworkError when attempting to fetch resource.',
// download body
'Load failed',
// dns
'A server with the specified hostname could not be found.',
'Es wurde kein Server mit dem angegebenen Hostnamen gefunden.',
'Impossibile trovare un server con il nome host specificato.',
'未能找到使用指定主机名的服务器。',
// offline
'The Internet connection appears to be offline.',
'Internetanslutningen verkar vara nedkopplad.',
// connection error
'Could not connect to the server.',
'Impossible de se connecter au serveur.',
'Verbindung zum Server konnte nicht hergestellt werden.',
'Не удалось подключиться к серверу.',
'يبدو أنه لا يوجد اتصال بالإنترنت.',
'无法连接服务器。',
// disconnected
'The network connection was lost.',
'A conexão de rede foi perdida.',
'A hálózati kapcsolat megszakadt.',
'A ligação de rede foi cortada.',
'Ağ bağlantısı kesildi.',
'Conexiunea de rețea a fost pierdută.',
'De netwerkverbinding is verbroken.',
'Die Netzwerkverbindung wurde unterbrochen.',
'La conexión de red se ha perdido.',
'La conexión de red se perdió.',
'La connessione è stata persa.',
'La connexion réseau a été perdue.',
'La connexió de xarxa sha perdut.',
'Mistet nettverksforbindelsen.',
'Netværksforbindelsen gik tabt.',
'Nätverksanslutningen förlorades.',
'Połączenie sieciowe zostało przerwane.',
'Veza s mrežom je prekinuta.',
'la connessione è stata persa.',
'Đã mất kết nối mạng.',
'Сетевое соединение потеряно.',
'החיבור לרשת אבד.',
'تم فقدان اتصال الشبكة.',
'キャンセルしました',
'ネットワーク接続が切れました。',
'已取消',
'網絡連線中斷。',
'網路連線中斷。',
// slow network
'The request timed out.',
'Begäran nådde en maxtidsgräns.',
'Esgotou-se o tempo limite da solicitação.',
'Il tempo di attesa della richiesta è scaduto.',
'La requête a expiré.',
'Przekroczenie limitu czasu żądania.',
'Se agotó el tiempo de espera.',
'Se ha agotado el tiempo de espera.',
'Tempo di richiesta scaduto.',
'Temps esgotat per a la sol·licitud.',
'Zeitüberschreitung bei der Anforderung.',
'Превышен лимит времени на запрос.',
'انتهت مهلة الطلب.',
'データベースの要求が時間切れになりました。',
'要求逾時。',
'요청한 시간이 초과되었습니다.',
]
export function isNetworkError(err?: Error) {
return err && NETWORK_ERRORS.includes(err.message)
}

View File

@@ -0,0 +1,10 @@
import getMeta from './meta'
// Should be `never` when no experiments are active. Otherwise it should be a
// union of active experiment names e.g. `'experiment1' | 'experiment2'`
export type ActiveExperiment = 'editor-redesign'
export const isInExperiment = (experiment: ActiveExperiment): boolean => {
const experiments = getMeta('ol-labsExperiments')
return Boolean(experiments?.includes(experiment))
}

View File

@@ -0,0 +1,303 @@
import { User, Features } from '../../../types/user'
import { User as MinimalUser } from '../../../types/admin/user'
import { User as ManagedUser } from '../../../types/group-management/user'
import { UserSettings } from '../../../types/user-settings'
import { OAuthProviders } from '../../../types/oauth-providers'
import { ExposedSettings } from '../../../types/exposed-settings'
import {
type AllowedImageName,
OverallThemeMeta,
type SpellCheckLanguage,
} from '../../../types/project-settings'
import { CurrencyCode } from '../../../types/subscription/currency'
import { PricingFormState } from '../../../types/subscription/payment-context-value'
import { Plan } from '../../../types/subscription/plan'
import { Affiliation } from '../../../types/affiliation'
import type { PortalTemplate } from '../../../types/portal-template'
import { UserEmailData } from '../../../types/user-email'
import {
GroupsAndEnterpriseBannerVariant,
Institution as InstitutionType,
Notification as NotificationType,
PendingGroupSubscriptionEnrollment,
USGovBannerVariant,
} from '../../../types/project/dashboard/notification'
import { Survey } from '../../../types/project/dashboard/survey'
import { GetProjectsResponseBody } from '../../../types/project/dashboard/api'
import { Tag } from '../../../app/src/Features/Tags/types'
import { Institution } from '../../../types/institution'
import {
GroupPolicy,
ManagedGroupSubscription,
MemberGroupSubscription,
} from '../../../types/subscription/dashboard/subscription'
import { SplitTestInfo } from '../../../types/split-test'
import { ValidationStatus } from '../../../types/group-management/validation'
import { ManagedInstitution } from '../../../types/subscription/dashboard/managed-institution'
import { GroupSSOTestResult } from '../../../modules/group-settings/frontend/js/utils/types'
import {
AccessToken,
InstitutionLink,
SAMLError,
} from '../../../types/settings-page'
import { SuggestedLanguage } from '../../../types/system-message'
import type { TeamInvite } from '../../../types/team-invite'
import { GroupPlans } from '../../../types/subscription/dashboard/group-plans'
import { GroupSSOLinkingStatus } from '../../../types/subscription/sso'
import { PasswordStrengthOptions } from '../../../types/password-strength-options'
import { Subscription as ProjectDashboardSubscription } from '../../../types/project/dashboard/subscription'
import { ThirdPartyIds } from '../../../types/third-party-ids'
import { Publisher } from '../../../types/subscription/dashboard/publisher'
import { SubscriptionChangePreview } from '../../../types/subscription/subscription-change-preview'
import { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata'
import { FooterMetadata } from '@/features/ui/components/types/footer-metadata'
import type { ScriptLogType } from '../../../modules/admin-panel/frontend/js/features/script-logs/script-log'
import { ActiveExperiment } from './labs-utils'
export interface Meta {
'ol-ExposedSettings': ExposedSettings
'ol-addonPrices': Record<string, { annual: string; monthly: string }>
'ol-allInReconfirmNotificationPeriods': UserEmailData[]
'ol-allowedExperiments': string[]
'ol-allowedImageNames': AllowedImageName[]
'ol-anonymous': boolean
'ol-baseAssetPath': string
'ol-bootstrapVersion': 3 | 5
'ol-brandVariation': Record<string, any>
// dynamic keys based on permissions
'ol-canUseAddSeatsFeature': boolean
'ol-canUseFlexibleLicensing': boolean
'ol-canUseFlexibleLicensingForConsolidatedPlans': boolean
'ol-cannot-add-secondary-email': boolean
'ol-cannot-change-password': boolean
'ol-cannot-delete-own-account': boolean
'ol-cannot-join-subscription': boolean
'ol-cannot-leave-group-subscription': boolean
'ol-cannot-link-google-sso': boolean
'ol-cannot-link-other-third-party-sso': boolean
'ol-cannot-reactivate-subscription': boolean
'ol-cannot-use-ai': boolean
'ol-chatEnabled': boolean
'ol-compilesUserContentDomain': string
'ol-countryCode': PricingFormState['country']
'ol-couponCode': PricingFormState['coupon']
'ol-createdAt': Date
'ol-csrfToken': string
'ol-currentInstitutionsWithLicence': Institution[]
'ol-currentManagedUserAdminEmail': string
'ol-currentUrl': string
'ol-customerIoEnabled': boolean
'ol-debugPdfDetach': boolean
'ol-detachRole': 'detached' | 'detacher' | ''
'ol-dictionariesRoot': 'string'
'ol-dropbox': { error: boolean; registered: boolean }
'ol-editorThemes': string[]
'ol-email': string
'ol-emailAddressLimit': number
'ol-error': { name: string } | undefined
'ol-expired': boolean
'ol-features': Features
'ol-footer': FooterMetadata
'ol-fromPlansPage': boolean
'ol-galleryTagName': string
'ol-gitBridgeEnabled': boolean
'ol-gitBridgePublicBaseUrl': string
'ol-github': { enabled: boolean; error: boolean }
'ol-groupId': string
'ol-groupName': string
'ol-groupPlans': GroupPlans
'ol-groupPolicy': GroupPolicy
'ol-groupSSOActive': boolean
'ol-groupSSOTestResult': GroupSSOTestResult
'ol-groupSettingsAdvertisedFor': string[]
'ol-groupSettingsEnabledFor': string[]
'ol-groupSize': number
'ol-groupSsoSetupSuccess': boolean
'ol-groupSubscriptionsPendingEnrollment': PendingGroupSubscriptionEnrollment[]
'ol-groupsAndEnterpriseBannerVariant': GroupsAndEnterpriseBannerVariant
'ol-hasAiAssistViaWritefull': boolean
'ol-hasGroupSSOFeature': boolean
'ol-hasIndividualRecurlySubscription': boolean
'ol-hasManagedUsersFeature': boolean
'ol-hasPassword': boolean
'ol-hasSubscription': boolean
'ol-hasTrackChangesFeature': boolean
'ol-hideLinkingWidgets': boolean // CI only
'ol-i18n': { currentLangCode: string }
'ol-inactiveTutorials': string[]
'ol-institutionEmailNonCanonical': string | undefined
'ol-institutionLinked': InstitutionLink | undefined
'ol-inviteToken': string
'ol-inviterName': string
'ol-isExternalAuthenticationSystemUsed': boolean
'ol-isManagedAccount': boolean
'ol-isPaywallChangeCompileTimeoutEnabled': boolean
'ol-isProfessional': boolean
'ol-isRegisteredViaGoogle': boolean
'ol-isRestrictedTokenMember': boolean
'ol-isReviewerRoleEnabled': boolean
'ol-isSaas': boolean
'ol-itm_campaign': string
'ol-itm_content': string
'ol-itm_referrer': string
'ol-labs': boolean
'ol-labsExperiments': ActiveExperiment[] | undefined
'ol-languages': SpellCheckLanguage[]
'ol-learnedWords': string[]
'ol-legacyEditorThemes': string[]
'ol-licenseQuantity': number | undefined
'ol-loadingText': string
'ol-managedGroupSubscriptions': ManagedGroupSubscription[]
'ol-managedInstitutions': ManagedInstitution[]
'ol-managedPublishers': Publisher[]
'ol-managedUsersActive': boolean
'ol-managedUsersEnabled': boolean
'ol-managers': MinimalUser[]
'ol-mathJaxPath': string
'ol-maxDocLength': number
'ol-maxReconnectGracefullyIntervalMs': number
'ol-memberGroupSubscriptions': MemberGroupSubscription[]
'ol-memberOfSSOEnabledGroups': GroupSSOLinkingStatus[]
'ol-members': MinimalUser[]
'ol-navbar': DefaultNavbarMetadata
'ol-no-single-dollar': boolean
'ol-notifications': NotificationType[]
'ol-notificationsInstitution': InstitutionType[]
'ol-oauthProviders': OAuthProviders
'ol-odcRole': string
'ol-overallThemes': OverallThemeMeta[]
'ol-pages': number
'ol-passwordStrengthOptions': PasswordStrengthOptions
'ol-paywallPlans': { [key: string]: string }
'ol-personalAccessTokens': AccessToken[] | undefined
'ol-plan': Plan
'ol-planCode': string
'ol-planCodesChangingAtTermEnd': string[] | undefined
'ol-plans': Plan[]
'ol-portalTemplates': PortalTemplate[]
'ol-postCheckoutRedirect': string
'ol-postUrl': string
'ol-prefetchedProjectsBlob': GetProjectsResponseBody | undefined
'ol-preventCompileOnLoad'?: boolean
'ol-primaryEmail': { email: string; confirmed: boolean }
'ol-project': any // TODO
'ol-projectHistoryBlobsEnabled': boolean
'ol-projectName': string
'ol-projectSyncSuccessMessage': string
'ol-projectTags': Tag[]
'ol-project_id': string
'ol-recommendedCurrency': CurrencyCode
'ol-reconfirmationRemoveEmail': string
'ol-reconfirmedViaSAML': string
'ol-recurlyApiKey': string
'ol-recurlySubdomain': string
'ol-ro-mirror-on-client-no-local-storage': boolean
'ol-samlError': SAMLError | undefined
'ol-script-log': ScriptLogType
'ol-script-logs': ScriptLogType[]
'ol-settingsGroupSSO': { enabled: boolean } | undefined
'ol-settingsPlans': Plan[]
'ol-shouldAllowEditingDetails': boolean
'ol-shouldLoadHotjar': boolean
'ol-showAiErrorAssistant': boolean
'ol-showBrlGeoBanner': boolean
'ol-showCouponField': boolean
'ol-showGroupDiscount': boolean
'ol-showGroupsAndEnterpriseBanner': boolean
'ol-showInrGeoBanner': boolean
'ol-showLATAMBanner': boolean
'ol-showSupport': boolean
'ol-showSymbolPalette': boolean
'ol-showTemplatesServerPro': boolean
'ol-showUSGovBanner': boolean
'ol-showUpgradePrompt': boolean
'ol-skipUrl': string
'ol-splitTestInfo': { [name: string]: SplitTestInfo }
'ol-splitTestVariants': { [name: string]: string }
'ol-ssoDisabled': boolean
'ol-ssoErrorMessage': string
'ol-subscription': any // TODO: mixed types, split into two fields
'ol-subscriptionChangePreview': SubscriptionChangePreview
'ol-subscriptionId': string
'ol-suggestedLanguage': SuggestedLanguage | undefined
'ol-survey': Survey | undefined
'ol-symbolPaletteAvailable': boolean
'ol-tags': Tag[]
'ol-teamInvites': TeamInvite[]
'ol-thirdPartyIds': ThirdPartyIds
'ol-totalLicenses': number
'ol-translationIoNotLoaded': string
'ol-translationLoadErrorMessage': string
'ol-translationMaintenance': string
'ol-translationUnableToJoin': string
'ol-usGovBannerVariant': USGovBannerVariant
'ol-useShareJsHash': boolean
'ol-user': User
'ol-userAffiliations': Affiliation[]
'ol-userCanExtendTrial': boolean
'ol-userCanNotStartRequestedTrial': boolean
'ol-userEmails': UserEmailData[]
'ol-userSettings': UserSettings
'ol-user_id': string | undefined
'ol-users': ManagedUser[]
'ol-usersBestSubscription': ProjectDashboardSubscription | undefined
'ol-usersEmail': string | undefined
'ol-validationStatus': ValidationStatus
'ol-wikiEnabled': boolean
'ol-writefullCssUrl': string
'ol-writefullEnabled': boolean
'ol-writefullJsUrl': string
'ol-wsUrl': string
}
type DeepPartial<T> =
T extends Record<string, any> ? { [P in keyof T]?: DeepPartial<T[P]> } : T
export type PartialMeta = DeepPartial<Meta>
export type MetaAttributesCache<
K extends keyof PartialMeta = keyof PartialMeta,
> = Map<K, PartialMeta[K]>
export type MetaTag = {
[K in keyof Meta]: {
name: K
value: Meta[K]
}
}[keyof Meta]
// cache for parsed values
window.metaAttributesCache = window.metaAttributesCache || new Map()
export default function getMeta<T extends keyof Meta>(name: T): Meta[T] {
if (window.metaAttributesCache.has(name)) {
return window.metaAttributesCache.get(name)
}
const element = document.head.querySelector(
`meta[name="${name}"]`
) as HTMLMetaElement
if (!element) {
return undefined!
}
const plainTextValue = element.content
let value
switch (element.dataset.type) {
case 'boolean':
// in pug: content=false -> no content field
// in pug: content=true -> empty content field
value = element.hasAttribute('content')
break
case 'json':
if (!plainTextValue) {
// JSON.parse('') throws
value = undefined
} else {
value = JSON.parse(plainTextValue)
}
break
default:
value = plainTextValue
}
window.metaAttributesCache.set(name, value)
return value
}

View File

@@ -0,0 +1,22 @@
import mapKeys from 'lodash/mapKeys'
export interface NormalizedObject<T> {
[p: string]: T
}
type Data<T> = T[]
type Config = Partial<{
idAttribute: string
}>
export function normalize<T>(
data: Data<T>,
config: Config = {}
): NormalizedObject<T> | undefined {
const { idAttribute = 'id' } = config
const mapped = mapKeys(data, idAttribute)
return Object.prototype.hasOwnProperty.call(mapped, 'undefined')
? undefined
: mapped
}

View File

@@ -0,0 +1,43 @@
import {
AnyOperation,
Change,
CommentOperation,
DeleteOperation,
EditOperation,
InsertOperation,
Operation,
} from '../../../types/change'
export const isInsertOperation = (op: Operation): op is InsertOperation =>
'i' in op
export const isCommentOperation = (op: Operation): op is CommentOperation =>
'c' in op
export const isDeleteOperation = (op: Operation): op is DeleteOperation =>
'd' in op
export const isEditOperation = (op: Operation): op is EditOperation =>
isInsertOperation(op) || isDeleteOperation(op)
export const isInsertChange = (
change: Change<EditOperation>
): change is Change<InsertOperation> => isInsertOperation(change.op)
export const isCommentChange = (
change: Change<CommentOperation>
): change is Change<CommentOperation> => isCommentOperation(change.op)
export const isDeleteChange = (
change: Change<EditOperation>
): change is Change<DeleteOperation> => isDeleteOperation(change.op)
export const visibleTextLength = (op: AnyOperation) => {
if (isCommentOperation(op)) {
return op.c.length
}
if (isInsertOperation(op)) {
return op.i.length
}
return 0
}

View File

@@ -0,0 +1,21 @@
import { forwardRef } from 'react'
export const fixedForwardRef = <
T,
P = object,
A extends Record<string, React.FunctionComponent> = Record<
string,
React.FunctionComponent
>,
>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null,
propsToAttach: A = {} as A
): ((props: P & React.RefAttributes<T>) => React.ReactElement | null) & A => {
const ForwardReferredComponent = forwardRef(render) as any
for (const i in propsToAttach) {
ForwardReferredComponent[i] = propsToAttach[i]
}
return ForwardReferredComponent
}

View File

@@ -0,0 +1,44 @@
// @ts-nocheck
/**
* A polyfill for `ReadableStream.protototype[Symbol.asyncIterator]`,
* aligning as closely as possible to the specification.
*
* from https://gist.github.com/MattiasBuelens/496fc1d37adb50a733edd43853f2f60e
*
* @see https://streams.spec.whatwg.org/#rs-asynciterator
* @see https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#async_iteration
*/
ReadableStream.prototype.values ??= function ({ preventCancel = false } = {}) {
const reader = this.getReader()
return {
async next() {
try {
const result = await reader.read()
if (result.done) {
reader.releaseLock()
}
return result
} catch (e) {
reader.releaseLock()
throw e
}
},
async return(value) {
if (!preventCancel) {
const cancelPromise = reader.cancel(value)
reader.releaseLock()
await cancelPromise
} else {
reader.releaseLock()
}
return { done: true, value }
},
[Symbol.asyncIterator]() {
return this
},
}
}
ReadableStream.prototype[Symbol.asyncIterator] ??=
ReadableStream.prototype.values

View File

@@ -0,0 +1,34 @@
type BreakpointName = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
type Breakpoint = {
name: BreakpointName
minWidth: number
}
/**
* Maps window width to Bootstrap 5 breakpoint names
* Breakpoints based on Bootstrap 5 documentation:
* xs: 0-575px
* sm: 576-767px
* md: 768-991px
* lg: 992-1199px
* xl: 1200-1399px
* xxl: ≥1400px
* @param {number} width - Window width in pixels
* @returns {BreakpointName} Bootstrap breakpoint name
*/
export function getBootstrap5Breakpoint(
width: number
): BreakpointName | undefined {
const breakpoints: Breakpoint[] = [
{ name: 'xxl', minWidth: 1400 },
{ name: 'xl', minWidth: 1200 },
{ name: 'lg', minWidth: 992 },
{ name: 'md', minWidth: 768 },
{ name: 'sm', minWidth: 576 },
{ name: 'xs', minWidth: 0 },
]
const breakpoint = breakpoints.find(bp => width >= bp.minWidth)
return breakpoint?.name
}

View File

@@ -0,0 +1,17 @@
export function cleanupServiceWorker() {
try {
navigator.serviceWorker
.getRegistrations()
.catch(() => {
// fail silently if permission not given (e.g. SecurityError)
return []
})
.then(registrations => {
registrations.forEach(worker => {
worker.unregister()
})
})
} catch (e) {
// fail silently if service worker are not available (on the navigator)
}
}

View File

@@ -0,0 +1,18 @@
import getMeta from './meta'
export function isSplitTestEnabled(name: string) {
return getMeta('ol-splitTestVariants')?.[name] === 'enabled'
}
export function getSplitTestVariant(name: string, fallback?: string) {
return getMeta('ol-splitTestVariants')?.[name] || fallback
}
export function parseIntFromSplitTest(name: string, defaultValue: number) {
const v = getMeta('ol-splitTestVariants')?.[name]
const n = parseInt(v, 10)
if (v === 'default' || Number.isNaN(n)) {
return defaultValue
}
return n
}

View File

@@ -0,0 +1 @@
export const supportsWebAssembly = () => typeof window.WebAssembly === 'object'

View File

@@ -0,0 +1,5 @@
import getMeta from './meta'
// Configure dynamically loaded assets (via webpack) to be downloaded from CDN
// See: https://webpack.js.org/guides/public-path/#on-the-fly
__webpack_public_path__ = getMeta('ol-baseAssetPath')