milahu / nixpkgs-watcher

notifications for my favorite apps
MIT License
3 stars 0 forks source link

push feed of some issues #6

Open milahu opened 1 year ago

milahu commented 1 year ago

1

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

push event callstack

d @ updatable-content.ts:49
setTimeout (async)    
(anonymous) @ updatable-content-observer.ts:23
y @ alive.ts:47
receive @ alive.ts:170
AliveSessionProxy.worker.port.onmessage @ alive.ts:67

https://github.githubassets.com/assets/ui/packages/alive/alive.ts

```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()) } ```

https://github.githubassets.com/assets/app/assets/modules/github/behaviors/updatable-content-observer.ts

```js // // Updatable Content // // Markup // //
//
// import {fromEvent} from '../subscription' // eslint-disable-next-line no-restricted-imports import {observe} from '@github/selector-observer' import {updateContent} from '../updatable-content' observe('.js-socket-channel.js-updatable-content', { subscribe: el => fromEvent(el, 'socket:message', function (event: Event) { const {gid, wait} = (event as CustomEvent).detail.data const container = event.target as HTMLElement const target = gid ? findByGid(container, gid) : container if (target) setTimeout(updateContent, wait || 0, target) }), }) function findByGid(root: HTMLElement, gid: string): HTMLElement | null { if (root.getAttribute('data-gid') === gid) return root for (const el of root.querySelectorAll(`[data-url][data-gid]`)) { if (el.getAttribute('data-gid') === gid) { return el } } return null } ```

https://github.githubassets.com/assets/app/assets/modules/github/updatable-content.ts

```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