i get push notifications when i have issues or PRs open in my browser = i see live edits and updates without page reload. maybe we can abuse that? but then, will that scale to a million subscriptions? a billion subscriptions? i guess github will not like this
```js
import type {AliveEvent, MetadataUpdate, Notifier, Subscription} from '@github/alive-client'
import {PresenceMetadataSet, SubscriptionSet, isPresenceChannel} from '@github/alive-client'
import {AliveSession} from './session'
import {debounce} from '@github/mini-throttle'
import {ready} from '@github-ui/document-ready'
import safeStorage from '@github-ui/safe-storage'
import {workerSrcRelPolicy, SourceRelNotFoundError} from '@github-ui/trusted-types-policies/worker-src-rel'
export interface Dispatchable {
dispatchEvent: (e: Event) => unknown
}
function isSharedWorkerSupported(): boolean {
return 'SharedWorker' in window && safeStorage('localStorage').getItem('bypassSharedWorker') !== 'true'
}
function workerSrc(): string | null {
try {
return workerSrcRelPolicy.createScriptURL('shared-web-socket-src')
} catch (e) {
if (e instanceof SourceRelNotFoundError) {
return null
}
throw e
}
}
function socketUrl(): string | null {
return document.head.querySelector('link[rel=shared-web-socket]')?.href ?? null
}
function socketRefreshUrl(): string | null {
return (
document.head.querySelector('link[rel=shared-web-socket]')?.getAttribute('data-refresh-url') ??
null
)
}
function sessionIdentifier(): string | null {
return (
document.head.querySelector('link[rel=shared-web-socket]')?.getAttribute('data-session-id') ?? null
)
}
function notify(subscribers: Iterable, {channel, type, data}: AliveEvent) {
for (const el of subscribers) {
el.dispatchEvent(
new CustomEvent(`socket:${type}`, {
bubbles: false,
cancelable: false,
detail: {name: channel, data},
}),
)
}
}
class AliveSessionProxy {
private worker: SharedWorker
private subscriptions = new SubscriptionSet()
private presenceMetadata = new PresenceMetadataSet()
private notify: Notifier
constructor(src: string, url: string, refreshUrl: string, sessionId: string, notifier: Notifier) {
this.notify = notifier
// eslint-disable-next-line ssr-friendly/no-dom-globals-in-constructor
this.worker = new SharedWorker(src, `github-socket-worker-v2-${sessionId}`)
this.worker.port.onmessage = ({data}) => this.receive(data)
this.worker.port.postMessage({connect: {url, refreshUrl}})
}
subscribe(subs: Array>) {
const added = this.subscriptions.add(...subs)
if (added.length) {
this.worker.port.postMessage({subscribe: added})
}
// We may be adding a subscription to a presence channel which is already subscribed.
// In this case, we need to explicitly ask the SharedWorker to send us the presence data.
const addedChannels = new Set(added.map(topic => topic.name))
const redundantPresenceChannels = subs.reduce((redundantChannels, subscription) => {
const channel = subscription.topic.name
if (isPresenceChannel(channel) && !addedChannels.has(channel)) {
redundantChannels.add(channel)
}
return redundantChannels
}, new Set())
if (redundantPresenceChannels.size) {
this.worker.port.postMessage({requestPresence: Array.from(redundantPresenceChannels)})
}
}
unsubscribeAll(...subscribers: Dispatchable[]) {
const removed = this.subscriptions.drain(...subscribers)
if (removed.length) {
this.worker.port.postMessage({unsubscribe: removed})
}
const updatedPresenceChannels = this.presenceMetadata.removeSubscribers(subscribers)
this.sendPresenceMetadataUpdate(updatedPresenceChannels)
}
updatePresenceMetadata(metadataUpdates: Array>) {
const updatedChannels = new Set()
for (const update of metadataUpdates) {
// update the local metadata for this specific element
this.presenceMetadata.setMetadata(update)
updatedChannels.add(update.channelName)
}
// Send the full local metadata for these channels to the SharedWorker
this.sendPresenceMetadataUpdate(updatedChannels)
}
sendPresenceMetadataUpdate(channelNames: Set) {
if (!channelNames.size) {
return
}
const updatesForSharedWorker: Array, 'subscriber'>> = []
for (const channelName of channelNames) {
// get all metadata for this channel (from all elements) to send to the SharedWorker
updatesForSharedWorker.push({
channelName,
metadata: this.presenceMetadata.getChannelMetadata(channelName),
})
}
// Send the full metadata updates to the SharedWorker
this.worker.port.postMessage({updatePresenceMetadata: updatesForSharedWorker})
}
online() {
this.worker.port.postMessage({online: true})
}
offline() {
this.worker.port.postMessage({online: false})
}
hangup() {
this.worker.port.postMessage({hangup: true})
}
private notifyPresenceDebouncedByChannel = new Map>()
private receive(event: AliveEvent) {
const {channel} = event
if (event.type === 'presence') {
// There are times when we get a flood of messages from the SharedWorker, such as a tab that has been idle for a long time and then comes back to the foreground.
// Since each presence message for a channel contains the full list of users, we can debounce the events and only notify subscribers with the last one
let debouncedNotify = this.notifyPresenceDebouncedByChannel.get(channel)
if (!debouncedNotify) {
debouncedNotify = debounce((subscribers, debouncedEvent) => {
this.notify(subscribers, debouncedEvent)
this.notifyPresenceDebouncedByChannel.delete(channel)
}, 100)
this.notifyPresenceDebouncedByChannel.set(channel, debouncedNotify)
}
debouncedNotify(this.subscriptions.subscribers(channel), event)
return
}
// For non-presence messages, we can send them through immediately since they may contain different messages/data
this.notify(this.subscriptions.subscribers(channel), event)
}
}
async function connect() {
const src = workerSrc()
if (!src) return
const url = socketUrl()
if (!url) return
const refreshUrl = socketRefreshUrl()
if (!refreshUrl) return
const sessionId = sessionIdentifier()
if (!sessionId) return
const createSession = () => {
if (isSharedWorkerSupported()) {
try {
return new AliveSessionProxy(src, url, refreshUrl, sessionId, notify)
} catch (_) {
// ignore errors. CSP will some times block SharedWorker creation. Fall back to standard AliveSession.
}
}
return new AliveSession(url, refreshUrl, false, notify)
}
const session = createSession()
window.addEventListener('online', () => session.online())
window.addEventListener('offline', () => session.offline())
window.addEventListener('pagehide', () => {
if ('hangup' in session) session.hangup()
})
return session
}
async function connectWhenReady() {
await ready
return connect()
}
let sessionPromise: undefined | ReturnType
export function getSession() {
return (sessionPromise ||= connectWhenReady())
}
```
```js
import {hasInteractions} from './has-interactions'
// eslint-disable-next-line no-restricted-imports
import {observe} from '@github/selector-observer'
import {parseHTML} from './parse-html'
import {preserveAnchorNodePosition} from 'scroll-anchoring'
import {replaceState} from './history'
const pendingRequests = new WeakMap()
const staleRecords: {[key: string]: string} = {}
// Wrapper around `window.location.reload()` that forceably cleans out the
// `staleRecords` state associated with the entry at the top of the history
// stack before reloading.
export function reload() {
for (const key of Object.keys(staleRecords)) {
delete staleRecords[key]
}
const stateObject = history.state || {}
stateObject.staleRecords = staleRecords
replaceState(stateObject, '', location.href)
window.location.reload()
}
// Associates the `staleRecords` object, if it contains any entries, with the
// entry at top of the history stack.
export function registerStaleRecords() {
if (Object.keys(staleRecords).length > 0) {
const stateObject = history.state || {}
stateObject.staleRecords = staleRecords
replaceState(stateObject, '', location.href)
}
}
// Fetch and replace container with its data-url.
//
// This replacement uses conservative checks to safely replace the element.
// If a user is interacting with any element within the container, the
// replacement will be aborted.
export async function updateContent(el: HTMLElement): Promise {
if (pendingRequests.get(el)) return
const retainFocus = el.hasAttribute('data-retain-focus')
const url = el.getAttribute('data-url')
if (!url) throw new Error('could not get url')
const controller = new AbortController()
pendingRequests.set(el, controller)
try {
const response = await fetch(url, {
signal: controller.signal,
headers: {
Accept: 'text/html',
'X-Requested-With': 'XMLHttpRequest',
},
})
if (!response.ok) return
const data = await response.text()
if (hasInteractions(el, retainFocus)) {
// eslint-disable-next-line no-console
console.warn('Failed to update content with interactions', el)
return
}
staleRecords[url] = data
return replace(el, data, retainFocus)
} catch {
// Ignore failed request.
} finally {
pendingRequests.delete(el)
}
}
// Abort any in-flight replacements and replace element without any interaction checks.
export async function replaceContent(el: HTMLElement, data: string, wasStale = false): Promise {
const controller = pendingRequests.get(el)
controller?.abort()
const updatable = el.closest('.js-updatable-content[data-url], .js-updatable-content [data-url]')
if (!wasStale && updatable && updatable === el) {
staleRecords[updatable.getAttribute('data-url') || ''] = data
}
return replace(el, data)
}
function replace(el: HTMLElement, data: string, retainFocus = false): Promise {
return preserveAnchorNodePosition(document, () => {
const newContent = parseHTML(document, data.trim())
const elementToRefocus =
retainFocus && el.ownerDocument && el === el.ownerDocument.activeElement ? newContent.querySelector('*') : null
const detailsIds = Array.from(el.querySelectorAll('details[open][id]')).map(element => element.id)
if (el.tagName === 'DETAILS' && el.id && el.hasAttribute('open')) detailsIds.push(el.id)
// Check the elements we are about replace to see if we want to preserve the scroll position of any of them
for (const preserveElement of el.querySelectorAll('.js-updatable-content-preserve-scroll-position')) {
const id = preserveElement.getAttribute('data-updatable-content-scroll-position-id') || ''
heights.set(id, preserveElement.scrollTop)
}
for (const id of detailsIds) {
const details = newContent.querySelector(`#${id}`)
if (details) details.setAttribute('open', '')
}
el.replaceWith(newContent)
if (elementToRefocus instanceof HTMLElement) {
elementToRefocus.focus()
}
})
}
const heights = new Map()
observe('.js-updatable-content-preserve-scroll-position', {
constructor: HTMLElement,
add(el) {
// When element is added to the DOM, check the map for the last scroll position we have on record for it.
const id = el.getAttribute('data-updatable-content-scroll-position-id')
if (!id) return
const height = heights.get(id)
if (height == null) return
el.scrollTop = height
},
})
```
keywords: github push events per issue, service worker, alive client, alive.ts, websocket, pubsub, presence, liveupdate, live update, liveview, live view, server sent events, updatable-content.ts, updatable-content-observer.ts
1
push event callstack
https://github.githubassets.com/assets/ui/packages/alive/alive.ts
https://github.githubassets.com/assets/app/assets/modules/github/behaviors/updatable-content-observer.ts
https://github.githubassets.com/assets/app/assets/modules/github/updatable-content.ts
keywords: github push events per issue, service worker, alive client, alive.ts, websocket, pubsub, presence, liveupdate, live update, liveview, live view, server sent events, updatable-content.ts, updatable-content-observer.ts