import type * as AtomIO from "atom.io" import type { Canonical } from "atom.io/foundations/canonical" import type { stringified } from "atom.io/foundations/json" import { stringifyJson } from "atom.io/foundations/json" import type { Transceiver } from "atom.io/internal" import { findInStore, getFromStore, getJsonTokenFromStore, getUpdateToken, IMPLICIT, subscribeToState, } from "atom.io/internal" import { employSocket } from "atom.io/realtime" import type { ServerConfig } from "." const isAvailable = ( exposedSubKeys: Iterable, subKey: K, ): boolean => { for (const exposedSubKey of exposedSubKeys) { if (stringifyJson(exposedSubKey) === stringifyJson(subKey)) { return true } } return false } export type MutableFamilyProvider = ReturnType< typeof realtimeMutableFamilyProvider > export function realtimeMutableFamilyProvider({ socket, consumer, store = IMPLICIT.STORE, }: ServerConfig) { return function mutableFamilyProvider< T extends Transceiver, K extends Canonical, >( family: AtomIO.MutableAtomFamilyToken, index: | AtomIO.ReadableToken> | null> | Iterable>, ): () => void { const [dynamicIndex, staticIndex]: | [AtomIO.ReadableToken> | null>, undefined] | [undefined, Iterable>] = (() => { if (typeof index === `object` && `key` in index && `type` in index) { return [index, undefined] as const } return [undefined, index] as const })() const coreSubscriptions = new Set<() => void>() const clearCoreSubscriptions = () => { for (const unsub of coreSubscriptions) unsub() coreSubscriptions.clear() } const requestedFamilyMembers = new Map< stringified, { key: K; stopWatchingForUnsubscribe: () => void } >() const familyMemberSubscriptions = new Map, () => void>() const clearFamilySubscriptions = () => { for (const unsub of familyMemberSubscriptions.values()) unsub() familyMemberSubscriptions.clear() for (const request of requestedFamilyMembers.values()) { request.stopWatchingForUnsubscribe() } requestedFamilyMembers.clear() } const fillUnsubRequest = (serializedKey: stringified) => { const request = requestedFamilyMembers.get(serializedKey) request?.stopWatchingForUnsubscribe() requestedFamilyMembers.delete(serializedKey) const unsub = familyMemberSubscriptions.get(serializedKey) if (unsub) { unsub() familyMemberSubscriptions.delete(serializedKey) } } const exposeFamilyMembers = (subKey: K) => { const serializedKey = stringifyJson(subKey) if (familyMemberSubscriptions.has(serializedKey)) return const token = findInStore(store, family, subKey) getFromStore(store, token) const jsonToken = getJsonTokenFromStore(store, token) const updateToken = getUpdateToken(token) socket.emit(`init:${token.key}`, getFromStore(store, jsonToken)) familyMemberSubscriptions.set( serializedKey, subscribeToState( store, updateToken, `expose-family:${family.key}:${socket.id}`, ({ newValue }) => { socket.emit(`next:${token.key}`, newValue) }, ), ) } const reconcileFamilyMembers = (exposedSubKeys: Iterable | null) => { const availableKeys = exposedSubKeys === null ? null : new Set([...exposedSubKeys].map(stringifyJson)) for (const [serializedKey, request] of requestedFamilyMembers) { const shouldExpose = availableKeys?.has(serializedKey) === true const isExposed = familyMemberSubscriptions.has(serializedKey) if (shouldExpose && !isExposed) { exposeFamilyMembers(request.key) } else if (!shouldExpose && isExposed) { familyMemberSubscriptions.get(serializedKey)?.() familyMemberSubscriptions.delete(serializedKey) socket.emit(`unavailable:${family.key}`, request.key) } } } const start = () => { store.logger.info( `👀`, `user`, consumer, `can subscribe to family "${family.key}"`, ) coreSubscriptions.add( employSocket(socket, `sub:${family.key}`, (subKey: K) => { const serializedKey = stringifyJson(subKey) if (!requestedFamilyMembers.has(serializedKey)) { const token = findInStore(store, family, subKey) const stopWatchingForUnsubscribe = employSocket( socket, `unsub:${token.key}`, () => { fillUnsubRequest(serializedKey) }, ) requestedFamilyMembers.set(serializedKey, { key: subKey, stopWatchingForUnsubscribe, }) } let exposedSubKeys: Iterable | null if (dynamicIndex) { exposedSubKeys = getFromStore(store, dynamicIndex) } else { exposedSubKeys = staticIndex } const shouldExpose = exposedSubKeys && isAvailable(exposedSubKeys, subKey) if (shouldExpose) { store.logger.info( `👀`, `user`, consumer, `is approved for a subscription to`, subKey, `in family "${family.key}"`, ) exposeFamilyMembers(subKey) } else { store.logger.info( `❌`, `user`, consumer, `is denied for a subscription to`, subKey, `in family "${family.key}"`, ) socket.emit(`unavailable:${family.key}`, subKey) } }), ) if (dynamicIndex) { coreSubscriptions.add( subscribeToState( store, dynamicIndex, `expose-family:${family.key}:${socket.id}`, ({ newValue: newExposedSubKeys }) => { store.logger.info( `👀`, `user`, consumer, `has the following keys available for family "${family.key}"`, newExposedSubKeys, ) reconcileFamilyMembers(newExposedSubKeys) }, ), ) } } start() return () => { clearCoreSubscriptions() clearFamilySubscriptions() } } }