213 lines
6.4 KiB
TypeScript
213 lines
6.4 KiB
TypeScript
import { createContext, useCallback, useRef } from "react";
|
|
import type {
|
|
MTPDataValueInput,
|
|
MTPEncodedBytesInput,
|
|
MTPFrame,
|
|
MTPRequestFunction,
|
|
MTPResponseFrame,
|
|
MTPSubscriptionFunction,
|
|
} from "mtp";
|
|
import {
|
|
mtp as mtpSchemas,
|
|
type Calls,
|
|
type Communities,
|
|
type Contacts,
|
|
} from "@tensamin/shared/data";
|
|
import { log } from "@tensamin/shared/log";
|
|
|
|
export type ProtocolMessage<
|
|
Type extends keyof typeof mtpSchemas & string = keyof typeof mtpSchemas &
|
|
string,
|
|
> = MTPResponseFrame<typeof mtpSchemas, Type>;
|
|
|
|
export type BoundSendFn = MTPRequestFunction<typeof mtpSchemas>;
|
|
|
|
export type RelayTarget =
|
|
| { kind: "user"; id: number }
|
|
| { kind: "iota"; id: number };
|
|
|
|
export type SealedRelayOptions = {
|
|
nextHop: RelayTarget;
|
|
finalRecipientId: number;
|
|
metadataRecipients: MTPEncodedBytesInput[];
|
|
contentRecipients: MTPEncodedBytesInput[];
|
|
};
|
|
|
|
export type SealedRelayResult = {
|
|
type: string;
|
|
data: Record<string, unknown>;
|
|
};
|
|
|
|
export function sealedRelayResultFromFrame(frame: MTPFrame): SealedRelayResult {
|
|
return {
|
|
type: frame.type,
|
|
data:
|
|
typeof frame.data === "object" &&
|
|
frame.data !== null &&
|
|
!Array.isArray(frame.data)
|
|
? (frame.data as Record<string, unknown>)
|
|
: {},
|
|
};
|
|
}
|
|
|
|
export class RelayRejectedError extends Error {
|
|
public readonly responseType: string;
|
|
|
|
constructor(responseType: string) {
|
|
super(`Relay rejected with ${responseType}`);
|
|
this.responseType = responseType;
|
|
this.name = "RelayRejectedError";
|
|
}
|
|
}
|
|
|
|
export function requireRelaySuccess(response: SealedRelayResult): void {
|
|
if (response.type !== "Success") {
|
|
throw new RelayRejectedError(response.type);
|
|
}
|
|
}
|
|
|
|
export type SealedRelaySend = (
|
|
type: string,
|
|
data: MTPDataValueInput,
|
|
options: SealedRelayOptions,
|
|
) => Promise<SealedRelayResult>;
|
|
|
|
export type MTPExchange = {
|
|
type: keyof typeof mtpSchemas & string;
|
|
data: unknown;
|
|
response: ProtocolMessage;
|
|
};
|
|
|
|
export type MTPInterceptor = (exchange: MTPExchange) => void | Promise<void>;
|
|
|
|
export type MTPContextType = {
|
|
send: BoundSendFn;
|
|
sendSealedRelay: SealedRelaySend;
|
|
subscribe: MTPSubscriptionFunction<typeof mtpSchemas>;
|
|
addInterceptor: (interceptor: MTPInterceptor) => () => void;
|
|
readyState: number;
|
|
identified: boolean;
|
|
freshContacts: Contacts;
|
|
freshCommunities: Communities;
|
|
freshCalls: Calls;
|
|
contextReady: boolean;
|
|
loadingDescription: string;
|
|
};
|
|
|
|
export const MTPContext = createContext<MTPContextType | undefined>(undefined);
|
|
|
|
export function removeMissingContacts(
|
|
contacts: Contacts,
|
|
message: ProtocolMessage<"GetStates">,
|
|
): Contacts {
|
|
const missing = new Set(message.data.MissingUserIds ?? []);
|
|
return contacts.filter((contact) => !missing.has(contact.UserId));
|
|
}
|
|
|
|
export function useMessageHandlers() {
|
|
const interceptorsRef = useRef(new Set<MTPInterceptor>());
|
|
const subscriptionHandlersRef = useRef(
|
|
new Map<string, Set<(message: ProtocolMessage) => void | Promise<void>>>(),
|
|
);
|
|
const transportRef = useRef<{
|
|
subscribe: MTPSubscriptionFunction<typeof mtpSchemas>;
|
|
} | null>(null);
|
|
const transportGenerationRef = useRef(0);
|
|
const transportUnsubscribersRef = useRef(new Map<string, () => void>());
|
|
const lastInitialStateRef = useRef<ProtocolMessage<"GetStates"> | null>(null);
|
|
|
|
const attachType = useCallback(
|
|
<Type extends keyof typeof mtpSchemas & string>(type: Type) => {
|
|
const transport = transportRef.current;
|
|
if (!transport || transportUnsubscribersRef.current.has(type)) return;
|
|
const generation = transportGenerationRef.current;
|
|
const unsubscribe = transport.subscribe(type, (message) => {
|
|
if (
|
|
transportRef.current !== transport ||
|
|
transportGenerationRef.current !== generation
|
|
)
|
|
return;
|
|
if (type === "GetStates") {
|
|
lastInitialStateRef.current = message as ProtocolMessage<"GetStates">;
|
|
}
|
|
for (const handler of [
|
|
...(subscriptionHandlersRef.current.get(type) ?? []),
|
|
]) {
|
|
void Promise.resolve(handler(message as ProtocolMessage)).catch(
|
|
(error) => {
|
|
log(1, "mtp", "red", "Subscription handler failed", error, {
|
|
type,
|
|
});
|
|
},
|
|
);
|
|
}
|
|
});
|
|
transportUnsubscribersRef.current.set(type, unsubscribe);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const attachSubscriptions = useCallback(
|
|
(transport: { subscribe: MTPSubscriptionFunction<typeof mtpSchemas> }) => {
|
|
for (const unsubscribe of transportUnsubscribersRef.current.values()) {
|
|
unsubscribe();
|
|
}
|
|
transportUnsubscribersRef.current.clear();
|
|
transportRef.current = transport;
|
|
const generation = ++transportGenerationRef.current;
|
|
for (const type of subscriptionHandlersRef.current.keys()) {
|
|
attachType(type as keyof typeof mtpSchemas & string);
|
|
}
|
|
return () => {
|
|
if (
|
|
transportRef.current !== transport ||
|
|
transportGenerationRef.current !== generation
|
|
)
|
|
return;
|
|
transportRef.current = null;
|
|
transportGenerationRef.current += 1;
|
|
for (const unsubscribe of transportUnsubscribersRef.current.values()) {
|
|
unsubscribe();
|
|
}
|
|
transportUnsubscribersRef.current.clear();
|
|
};
|
|
},
|
|
[attachType],
|
|
);
|
|
|
|
const subscribe = useCallback<MTPSubscriptionFunction<typeof mtpSchemas>>(
|
|
(type, handler) => {
|
|
const handlers = subscriptionHandlersRef.current.get(type) ?? new Set();
|
|
const untypedHandler = handler as (
|
|
message: ProtocolMessage,
|
|
) => void | Promise<void>;
|
|
handlers.add(untypedHandler);
|
|
subscriptionHandlersRef.current.set(type, handlers);
|
|
attachType(type);
|
|
const initialState = lastInitialStateRef.current;
|
|
if (type === "GetStates" && initialState) {
|
|
void Promise.resolve(untypedHandler(initialState)).catch(
|
|
() => undefined,
|
|
);
|
|
}
|
|
return () => {
|
|
handlers.delete(untypedHandler);
|
|
if (handlers.size !== 0) return;
|
|
subscriptionHandlersRef.current.delete(type);
|
|
transportUnsubscribersRef.current.get(type)?.();
|
|
transportUnsubscribersRef.current.delete(type);
|
|
};
|
|
},
|
|
[attachType],
|
|
);
|
|
const addInterceptor = useCallback((interceptor: MTPInterceptor) => {
|
|
interceptorsRef.current.add(interceptor);
|
|
return () => interceptorsRef.current.delete(interceptor);
|
|
}, []);
|
|
return {
|
|
addInterceptor,
|
|
attachSubscriptions,
|
|
interceptorsRef,
|
|
subscribe,
|
|
};
|
|
}
|