Loading frontend/src/views/Navbar/UserTitle.tsx +9 −1 Original line number Diff line number Diff line import { Button, ButtonGroup, Classes, Menu, MenuItem, Popover, Position, Tag, } from '@blueprintjs/core' import { cx } from '@emotion/css' import { useAuthIdentity } from '@inject/graphql' import { responsiveButtonGroup } from '@inject/shared' import { useNavigate } from '@tanstack/react-router' Loading Loading @@ -48,7 +50,13 @@ const UserTitle = () => { > <ButtonGroup className={responsiveButtonGroup(90)}> <Button icon='user' minimal> {vanityName} <span className={cx({ [Classes.SKELETON]: !whoAmI, })} > {vanityName ? vanityName : 'Anonymous'} </span> </Button> </ButtonGroup> </Popover> Loading graphql/urql/client.tsx +2 −2 Original line number Diff line number Diff line import { getHost, httpGraphql, notify, setSessionId } from '@inject/shared' import pipeLogger from '@inject/webworker/pipeLogger' import { devtoolsExchange } from '@urql/devtools' import type { Exchange, OperationContext } from 'urql' import { Client, mapExchange } from 'urql' import { multitab } from './multitabController' import pipeLogger from './pipeLogger' export interface CustomOperationContext extends OperationContext { errorHandled?: boolean Loading Loading @@ -46,7 +46,7 @@ export const constructClient = () => // TODO: add proper typing for meta.env.mode from Vite and make it global // @ts-ignore import.meta.env.MODE !== 'production' ? pipeLogger('pre-cache') : null, import.meta.env.MODE !== 'production' ? pipeLogger('[pre-send]') : null, multitab, ].filter(exchange => exchange !== null) as Exchange[], }) graphql/urql/multitabController.ts +108 −136 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ import { getSessionId } from '@inject/shared' import UrqlWorker from '@inject/webworker/worker?sharedworker&url' import type { WorkerData } from '@inject/webworker/workerTyping' import type { Exchange, Operation, OperationResult } from 'urql' import { filter, make, map, merge, pipe, subscribe, tap } from 'wonka' import { filter, make, merge, pipe, subscribe, tap } from 'wonka' import { netvar } from './networkEventVar' const generateTabNonce = () => { Loading @@ -27,14 +27,6 @@ bc.onmessage = ( } } const keyMap = new Map< number, { result: OperationResult | undefined teardown: boolean } >() const operationTabNonce = generateTabNonce() let initted = false Loading @@ -50,15 +42,20 @@ const awaitReadiness = () => }) const receiverChannel = new BroadcastChannel('worker:urql') const receiveMap = new Map< number, { objId: number result: OperationResult } >() const operationMap = new Map<number, Operation>() export const multitab: Exchange = () => op$ => { const opBuffered = op$ let isSessionIdReady = false const teardownCond = (op: Operation) => op.kind === 'teardown' const initConnCond = (op: Operation) => !keyMap.has(op.key) || // always execute if op.key does not exist op.context.requestPolicy === 'network-only' || // network-only forces refetch op.context.requestPolicy === 'cache-and-network' // refetches always const queryCond = (op: Operation) => op.kind === 'query' const mutationCond = (op: Operation) => op.kind === 'mutation' const subscripCond = (op: Operation) => op.kind === 'subscription' const teardowns = pipe( Loading @@ -69,20 +66,16 @@ export const multitab: Exchange = () => op$ => { await awaitReadiness() isSessionIdReady = true } const get = keyMap.get(op.key) if (get) { if (get.teardown) return console.log('[teardown] sending notification: ', op.key) console.log(`{multitab/teardown} [teardown/${op.key}] - sending teardown`) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'teardown', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) get.teardown = true } else { console.log('[teardown] no sub', op.key) } }) ) const mutations = pipe( Loading @@ -93,12 +86,15 @@ export const multitab: Exchange = () => op$ => { await awaitReadiness() isSessionIdReady = true } console.log(`[mutation] sending: `, op.key) console.log(`{multitab/mutation} [mutation/${op.key}] - sending`) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'mutation', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) }) ) Loading @@ -110,111 +106,56 @@ export const multitab: Exchange = () => op$ => { await awaitReadiness() isSessionIdReady = true } if (!keyMap.has(op.key) || keyMap.get(op.key)?.teardown) { console.log(`[${op.kind}] subscribing: `, op.key) console.log( `{multitab/subscription} [subscription/${op.key}] - subscribing` ) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'input', type: 'subscription', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) keyMap.set(op.key, { result: undefined, teardown: false, }) } else { if (keyMap.get(op.key)?.teardown) { throw Error( `Subscription with key ${op.key} is already marked for teardown` ) } console.log(`[${op.kind}] reusing subscription: `, op.key) } }) ) const rest = pipe( const query = pipe( opBuffered, filter(op => !teardownCond(op) && !mutationCond(op) && initConnCond(op)), filter(op => !teardownCond(op) && queryCond(op)), tap(async op => { if (!isSessionIdReady) { await awaitReadiness() isSessionIdReady = true } console.log(`[${op.kind}] sending: `, op.key) const get = keyMap.get(op.key) if (!get) { receiverChannel.postMessage({ type: 'input', tabNonce: operationTabNonce, key: op.key, operation: op, } as WorkerData) console.log(`[${op.kind}] marking for reuse: `, op.key) keyMap.set(op.key, { result: undefined, teardown: false, }) } else { if (get.teardown) { console.log(`[${op.kind}] reusing cached entry: `, op.key, get) get.teardown = false receiverChannel.postMessage({ type: 'input', tabNonce: operationTabNonce, key: op.key, operation: op, } as WorkerData) } else if ( op.context.requestPolicy === 'cache-and-network' || op.context.requestPolicy === 'network-only' ) { console.log( `[${op.kind}] cache policy set to fetch via network, requesting: `, op.key ) console.log(`{multitab/query} [${op.kind}/${op.key}] - sending request`) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'input', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) } } }) ) const reuse = pipe( opBuffered, filter( op => !teardownCond(op) && !initConnCond(op) && !mutationCond(op) && !subscripCond(op) && keyMap.get(op.key)?.result !== undefined && keyMap.get(op.key)?.teardown === false && op.context.requestPolicy !== 'network-only' ), tap(op => { console.log(`[${op.kind}] reusing source from cache:`, keyMap.get(op.key)) }), map(op => keyMap.get(op.key)!.result!) ) const lost = pipe( opBuffered, filter( op => !teardownCond(op) && !initConnCond(op) && !queryCond(op) && !mutationCond(op) && !subscripCond(op) && keyMap.get(op.key) === undefined !subscripCond(op) ), tap(op => { console.warn('[fallthrough]:', op) console.log(`{multitab/lost} [${op.kind}/${op.key}] - not resolved`) }) ) pipe( merge([rest, teardowns, mutations, subscription, lost]), merge([query, teardowns, mutations, subscription, lost]), subscribe(() => { /* * This is just to trigger the subscription Loading @@ -237,44 +178,75 @@ export const multitab: Exchange = () => op$ => { if (type === 'connectionClose' || type === 'connectionInit') { throw Error('Incorrect protocol') } const { tabNonce } = msg.data const { tabNonce, key } = msg.data if (tabNonce !== operationTabNonce) { console.log( `[${type}] - received message for another tab: ${tabNonce} != ${operationTabNonce}` `{multitab/output} [unknown/${key}] - message from another tab` ) return } if (type === 'ok') { initted = true console.log('received ok') console.log( `{multitab/output} [unknown/${key}] - connection established` ) return } console.log(`[${msg.data.type}] received data: `, msg.data.key, msg.data) console.log(`{multitab/output} [${msg.data.type}/${key}] - received data`) switch (type) { case 'result': { const get = keyMap.get(msg.data.key) if ( msg.data.result.operation.kind === 'query' && !get?.teardown && !msg.data.result.stale ) { get!.result = msg.data.result if (msg.data.objId === undefined) { throw Error(`No object id for result ${key}`) } const cachedEntry = receiveMap.get(key) const incomingResult = msg.data.result // Check if the worker sent back functionally identical data if (cachedEntry && cachedEntry.objId === msg.data.objId) { console.log( `{multitab/output} [result/${key}] - objectId matches. Preserving data reference.` ) /* * The data is the same, but the operation state (fetching, stale) has changed. * Create a NEW result object to allow the state transition... */ const operation = operationMap.get(msg.data.opNonce) const newResult = { ...incomingResult, // Use new properties like fetching: false ...(operation ? operation : {}), data: cachedEntry.result.data, // ...but reuse the OLD data object's reference. } if (!get) { throw Error('No entry in keyMap for result message') operationMap.delete(msg.data.opNonce) // Update our map with the newly constructed result receiveMap.set(key, { objId: msg.data.objId, result: newResult, }) sink.next(newResult) } else { // Data is genuinely new or we've never seen it before. console.log( `{multitab/output} [result/${key}] - new data or objectId mismatch.` ) receiveMap.set(key, { objId: msg.data.objId, result: incomingResult, }) sink.next(incomingResult) } sink.next(msg.data.result) } break case 'teardown': { const get = keyMap.get(msg.data.key) if (get) { get.teardown = true } // garbage collection receiveMap.delete(key) sink.complete() } break } } Loading @@ -283,17 +255,17 @@ export const multitab: Exchange = () => op$ => { type: 'connectionInit', tabNonce: operationTabNonce, } as WorkerData) console.log(`[init] - sending init ${operationTabNonce}`) console.log(`{multitab/ws} [init]$${operationTabNonce} - sending init`) return () => { tabWorker.port.removeEventListener('message', fn) tabWorker.port.postMessage({ type: 'connectionClose', tabNonce: operationTabNonce, } as WorkerData) console.log(`[close] - sending close ${operationTabNonce}`) console.log(`{multitab/ws} [close]$${operationTabNonce} - sending close `) tabWorker.port.close() } }) return merge([outputLine, reuse]) return outputLine } graphql/urql/pipeLogger.tsdeleted 100644 → 0 +0 −30 Original line number Diff line number Diff line import type { Exchange } from 'urql' import { map, pipe } from 'wonka' const pipeLogger = (letter: string): Exchange => { const left = `<=${letter}` const right = `${letter}=>` return ({ forward }) => op$ => pipe( forward( pipe( op$, map(x => { new Promise(() => { console.log(right, x) }) return x }) ) ), map(x => { new Promise(() => { console.log(left, x) }) return x }) ) } export default pipeLogger webworker/pipeLogger.ts +20 −19 Original line number Diff line number Diff line import type { Exchange } from 'urql' import { pipe, tap } from 'wonka' const pipeLogger = (letter: string): Exchange => { const left = `<=${letter}` const right = `${letter}=>` return ({ forward }) => const pipeLogger = (letter: string): Exchange => ({ forward }) => op$ => pipe( forward( pipe( op$, tap(x => { console.log(right, x) console.log(`[${x.kind}]/${x.key} - ${letter}=>:`, x) }) ) ), tap(x => { console.log(left, x) console.log( `[${x.operation.kind}]/${x.operation.key} - <=${letter}:`, x ) }) ) } export default pipeLogger Loading
frontend/src/views/Navbar/UserTitle.tsx +9 −1 Original line number Diff line number Diff line import { Button, ButtonGroup, Classes, Menu, MenuItem, Popover, Position, Tag, } from '@blueprintjs/core' import { cx } from '@emotion/css' import { useAuthIdentity } from '@inject/graphql' import { responsiveButtonGroup } from '@inject/shared' import { useNavigate } from '@tanstack/react-router' Loading Loading @@ -48,7 +50,13 @@ const UserTitle = () => { > <ButtonGroup className={responsiveButtonGroup(90)}> <Button icon='user' minimal> {vanityName} <span className={cx({ [Classes.SKELETON]: !whoAmI, })} > {vanityName ? vanityName : 'Anonymous'} </span> </Button> </ButtonGroup> </Popover> Loading
graphql/urql/client.tsx +2 −2 Original line number Diff line number Diff line import { getHost, httpGraphql, notify, setSessionId } from '@inject/shared' import pipeLogger from '@inject/webworker/pipeLogger' import { devtoolsExchange } from '@urql/devtools' import type { Exchange, OperationContext } from 'urql' import { Client, mapExchange } from 'urql' import { multitab } from './multitabController' import pipeLogger from './pipeLogger' export interface CustomOperationContext extends OperationContext { errorHandled?: boolean Loading Loading @@ -46,7 +46,7 @@ export const constructClient = () => // TODO: add proper typing for meta.env.mode from Vite and make it global // @ts-ignore import.meta.env.MODE !== 'production' ? pipeLogger('pre-cache') : null, import.meta.env.MODE !== 'production' ? pipeLogger('[pre-send]') : null, multitab, ].filter(exchange => exchange !== null) as Exchange[], })
graphql/urql/multitabController.ts +108 −136 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ import { getSessionId } from '@inject/shared' import UrqlWorker from '@inject/webworker/worker?sharedworker&url' import type { WorkerData } from '@inject/webworker/workerTyping' import type { Exchange, Operation, OperationResult } from 'urql' import { filter, make, map, merge, pipe, subscribe, tap } from 'wonka' import { filter, make, merge, pipe, subscribe, tap } from 'wonka' import { netvar } from './networkEventVar' const generateTabNonce = () => { Loading @@ -27,14 +27,6 @@ bc.onmessage = ( } } const keyMap = new Map< number, { result: OperationResult | undefined teardown: boolean } >() const operationTabNonce = generateTabNonce() let initted = false Loading @@ -50,15 +42,20 @@ const awaitReadiness = () => }) const receiverChannel = new BroadcastChannel('worker:urql') const receiveMap = new Map< number, { objId: number result: OperationResult } >() const operationMap = new Map<number, Operation>() export const multitab: Exchange = () => op$ => { const opBuffered = op$ let isSessionIdReady = false const teardownCond = (op: Operation) => op.kind === 'teardown' const initConnCond = (op: Operation) => !keyMap.has(op.key) || // always execute if op.key does not exist op.context.requestPolicy === 'network-only' || // network-only forces refetch op.context.requestPolicy === 'cache-and-network' // refetches always const queryCond = (op: Operation) => op.kind === 'query' const mutationCond = (op: Operation) => op.kind === 'mutation' const subscripCond = (op: Operation) => op.kind === 'subscription' const teardowns = pipe( Loading @@ -69,20 +66,16 @@ export const multitab: Exchange = () => op$ => { await awaitReadiness() isSessionIdReady = true } const get = keyMap.get(op.key) if (get) { if (get.teardown) return console.log('[teardown] sending notification: ', op.key) console.log(`{multitab/teardown} [teardown/${op.key}] - sending teardown`) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'teardown', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) get.teardown = true } else { console.log('[teardown] no sub', op.key) } }) ) const mutations = pipe( Loading @@ -93,12 +86,15 @@ export const multitab: Exchange = () => op$ => { await awaitReadiness() isSessionIdReady = true } console.log(`[mutation] sending: `, op.key) console.log(`{multitab/mutation} [mutation/${op.key}] - sending`) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'mutation', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) }) ) Loading @@ -110,111 +106,56 @@ export const multitab: Exchange = () => op$ => { await awaitReadiness() isSessionIdReady = true } if (!keyMap.has(op.key) || keyMap.get(op.key)?.teardown) { console.log(`[${op.kind}] subscribing: `, op.key) console.log( `{multitab/subscription} [subscription/${op.key}] - subscribing` ) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'input', type: 'subscription', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) keyMap.set(op.key, { result: undefined, teardown: false, }) } else { if (keyMap.get(op.key)?.teardown) { throw Error( `Subscription with key ${op.key} is already marked for teardown` ) } console.log(`[${op.kind}] reusing subscription: `, op.key) } }) ) const rest = pipe( const query = pipe( opBuffered, filter(op => !teardownCond(op) && !mutationCond(op) && initConnCond(op)), filter(op => !teardownCond(op) && queryCond(op)), tap(async op => { if (!isSessionIdReady) { await awaitReadiness() isSessionIdReady = true } console.log(`[${op.kind}] sending: `, op.key) const get = keyMap.get(op.key) if (!get) { receiverChannel.postMessage({ type: 'input', tabNonce: operationTabNonce, key: op.key, operation: op, } as WorkerData) console.log(`[${op.kind}] marking for reuse: `, op.key) keyMap.set(op.key, { result: undefined, teardown: false, }) } else { if (get.teardown) { console.log(`[${op.kind}] reusing cached entry: `, op.key, get) get.teardown = false receiverChannel.postMessage({ type: 'input', tabNonce: operationTabNonce, key: op.key, operation: op, } as WorkerData) } else if ( op.context.requestPolicy === 'cache-and-network' || op.context.requestPolicy === 'network-only' ) { console.log( `[${op.kind}] cache policy set to fetch via network, requesting: `, op.key ) console.log(`{multitab/query} [${op.kind}/${op.key}] - sending request`) const opNonce = Math.random() operationMap.set(opNonce, op) receiverChannel.postMessage({ type: 'input', tabNonce: operationTabNonce, key: op.key, operation: op, opNonce, } as WorkerData) } } }) ) const reuse = pipe( opBuffered, filter( op => !teardownCond(op) && !initConnCond(op) && !mutationCond(op) && !subscripCond(op) && keyMap.get(op.key)?.result !== undefined && keyMap.get(op.key)?.teardown === false && op.context.requestPolicy !== 'network-only' ), tap(op => { console.log(`[${op.kind}] reusing source from cache:`, keyMap.get(op.key)) }), map(op => keyMap.get(op.key)!.result!) ) const lost = pipe( opBuffered, filter( op => !teardownCond(op) && !initConnCond(op) && !queryCond(op) && !mutationCond(op) && !subscripCond(op) && keyMap.get(op.key) === undefined !subscripCond(op) ), tap(op => { console.warn('[fallthrough]:', op) console.log(`{multitab/lost} [${op.kind}/${op.key}] - not resolved`) }) ) pipe( merge([rest, teardowns, mutations, subscription, lost]), merge([query, teardowns, mutations, subscription, lost]), subscribe(() => { /* * This is just to trigger the subscription Loading @@ -237,44 +178,75 @@ export const multitab: Exchange = () => op$ => { if (type === 'connectionClose' || type === 'connectionInit') { throw Error('Incorrect protocol') } const { tabNonce } = msg.data const { tabNonce, key } = msg.data if (tabNonce !== operationTabNonce) { console.log( `[${type}] - received message for another tab: ${tabNonce} != ${operationTabNonce}` `{multitab/output} [unknown/${key}] - message from another tab` ) return } if (type === 'ok') { initted = true console.log('received ok') console.log( `{multitab/output} [unknown/${key}] - connection established` ) return } console.log(`[${msg.data.type}] received data: `, msg.data.key, msg.data) console.log(`{multitab/output} [${msg.data.type}/${key}] - received data`) switch (type) { case 'result': { const get = keyMap.get(msg.data.key) if ( msg.data.result.operation.kind === 'query' && !get?.teardown && !msg.data.result.stale ) { get!.result = msg.data.result if (msg.data.objId === undefined) { throw Error(`No object id for result ${key}`) } const cachedEntry = receiveMap.get(key) const incomingResult = msg.data.result // Check if the worker sent back functionally identical data if (cachedEntry && cachedEntry.objId === msg.data.objId) { console.log( `{multitab/output} [result/${key}] - objectId matches. Preserving data reference.` ) /* * The data is the same, but the operation state (fetching, stale) has changed. * Create a NEW result object to allow the state transition... */ const operation = operationMap.get(msg.data.opNonce) const newResult = { ...incomingResult, // Use new properties like fetching: false ...(operation ? operation : {}), data: cachedEntry.result.data, // ...but reuse the OLD data object's reference. } if (!get) { throw Error('No entry in keyMap for result message') operationMap.delete(msg.data.opNonce) // Update our map with the newly constructed result receiveMap.set(key, { objId: msg.data.objId, result: newResult, }) sink.next(newResult) } else { // Data is genuinely new or we've never seen it before. console.log( `{multitab/output} [result/${key}] - new data or objectId mismatch.` ) receiveMap.set(key, { objId: msg.data.objId, result: incomingResult, }) sink.next(incomingResult) } sink.next(msg.data.result) } break case 'teardown': { const get = keyMap.get(msg.data.key) if (get) { get.teardown = true } // garbage collection receiveMap.delete(key) sink.complete() } break } } Loading @@ -283,17 +255,17 @@ export const multitab: Exchange = () => op$ => { type: 'connectionInit', tabNonce: operationTabNonce, } as WorkerData) console.log(`[init] - sending init ${operationTabNonce}`) console.log(`{multitab/ws} [init]$${operationTabNonce} - sending init`) return () => { tabWorker.port.removeEventListener('message', fn) tabWorker.port.postMessage({ type: 'connectionClose', tabNonce: operationTabNonce, } as WorkerData) console.log(`[close] - sending close ${operationTabNonce}`) console.log(`{multitab/ws} [close]$${operationTabNonce} - sending close `) tabWorker.port.close() } }) return merge([outputLine, reuse]) return outputLine }
graphql/urql/pipeLogger.tsdeleted 100644 → 0 +0 −30 Original line number Diff line number Diff line import type { Exchange } from 'urql' import { map, pipe } from 'wonka' const pipeLogger = (letter: string): Exchange => { const left = `<=${letter}` const right = `${letter}=>` return ({ forward }) => op$ => pipe( forward( pipe( op$, map(x => { new Promise(() => { console.log(right, x) }) return x }) ) ), map(x => { new Promise(() => { console.log(left, x) }) return x }) ) } export default pipeLogger
webworker/pipeLogger.ts +20 −19 Original line number Diff line number Diff line import type { Exchange } from 'urql' import { pipe, tap } from 'wonka' const pipeLogger = (letter: string): Exchange => { const left = `<=${letter}` const right = `${letter}=>` return ({ forward }) => const pipeLogger = (letter: string): Exchange => ({ forward }) => op$ => pipe( forward( pipe( op$, tap(x => { console.log(right, x) console.log(`[${x.kind}]/${x.key} - ${letter}=>:`, x) }) ) ), tap(x => { console.log(left, x) console.log( `[${x.operation.kind}]/${x.operation.key} - <=${letter}:`, x ) }) ) } export default pipeLogger