FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,162 @@
import type {
inferObservableValue,
Unsubscribable,
} from '@trpc/server/observable';
import { observableToPromise, share } from '@trpc/server/observable';
import type {
AnyRouter,
inferAsyncIterableYield,
InferrableClientTypes,
Maybe,
TypeError,
} from '@trpc/server/unstable-core-do-not-import';
import { createChain } from '../links/internals/createChain';
import type { TRPCConnectionState } from '../links/internals/subscriptions';
import type {
OperationContext,
OperationLink,
TRPCClientRuntime,
TRPCLink,
} from '../links/types';
import { TRPCClientError } from '../TRPCClientError';
type TRPCType = 'mutation' | 'query' | 'subscription';
export interface TRPCRequestOptions {
/**
* Pass additional context to links
*/
context?: OperationContext;
signal?: AbortSignal;
}
export interface TRPCSubscriptionObserver<TValue, TError> {
onStarted: (opts: { context: OperationContext | undefined }) => void;
onData: (value: inferAsyncIterableYield<TValue>) => void;
onError: (err: TError) => void;
onStopped: () => void;
onComplete: () => void;
onConnectionStateChange: (state: TRPCConnectionState<TError>) => void;
}
/** @internal */
export type CreateTRPCClientOptions<TRouter extends InferrableClientTypes> = {
links: TRPCLink<TRouter>[];
transformer?: TypeError<'The transformer property has moved to httpLink/httpBatchLink/wsLink'>;
};
export class TRPCUntypedClient<TInferrable extends InferrableClientTypes> {
private readonly links: OperationLink<TInferrable>[];
public readonly runtime: TRPCClientRuntime;
private requestId: number;
constructor(opts: CreateTRPCClientOptions<TInferrable>) {
this.requestId = 0;
this.runtime = {};
// Initialize the links
this.links = opts.links.map((link) => link(this.runtime));
}
private $request<TInput = unknown, TOutput = unknown>(opts: {
type: TRPCType;
input: TInput;
path: string;
context?: OperationContext;
signal: Maybe<AbortSignal>;
}) {
const chain$ = createChain<AnyRouter, TInput, TOutput>({
links: this.links as OperationLink<any, any, any>[],
op: {
...opts,
context: opts.context ?? {},
id: ++this.requestId,
},
});
return chain$.pipe(share());
}
private async requestAsPromise<TInput = unknown, TOutput = unknown>(opts: {
type: TRPCType;
input: TInput;
path: string;
context?: OperationContext;
signal: Maybe<AbortSignal>;
}): Promise<TOutput> {
try {
const req$ = this.$request<TInput, TOutput>(opts);
type TValue = inferObservableValue<typeof req$>;
const envelope = await observableToPromise<TValue>(req$);
const data = (envelope.result as any).data;
return data;
} catch (err) {
throw TRPCClientError.from(err as Error);
}
}
public query(path: string, input?: unknown, opts?: TRPCRequestOptions) {
return this.requestAsPromise<unknown, unknown>({
type: 'query',
path,
input,
context: opts?.context,
signal: opts?.signal,
});
}
public mutation(path: string, input?: unknown, opts?: TRPCRequestOptions) {
return this.requestAsPromise<unknown, unknown>({
type: 'mutation',
path,
input,
context: opts?.context,
signal: opts?.signal,
});
}
public subscription(
path: string,
input: unknown,
opts: Partial<
TRPCSubscriptionObserver<unknown, TRPCClientError<AnyRouter>>
> &
TRPCRequestOptions,
): Unsubscribable {
const observable$ = this.$request({
type: 'subscription',
path,
input,
context: opts.context,
signal: opts.signal,
});
return observable$.subscribe({
next(envelope) {
switch (envelope.result.type) {
case 'state': {
opts.onConnectionStateChange?.(envelope.result);
break;
}
case 'started': {
opts.onStarted?.({
context: envelope.context,
});
break;
}
case 'stopped': {
opts.onStopped?.();
break;
}
case 'data':
case undefined: {
opts.onData?.(envelope.result.data);
break;
}
}
},
error(err) {
opts.onError?.(err);
},
complete() {
opts.onComplete?.();
},
});
}
}

160
node_modules/@trpc/client/src/internals/dataLoader.ts generated vendored Normal file
View File

@@ -0,0 +1,160 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
type BatchItem<TKey, TValue> = {
aborted: boolean;
key: TKey;
resolve: ((value: TValue) => void) | null;
reject: ((error: Error) => void) | null;
batch: Batch<TKey, TValue> | null;
};
type Batch<TKey, TValue> = {
items: BatchItem<TKey, TValue>[];
};
export type BatchLoader<TKey, TValue> = {
validate: (keys: TKey[]) => boolean;
fetch: (keys: TKey[]) => Promise<TValue[] | Promise<TValue>[]>;
};
/**
* A function that should never be called unless we messed something up.
*/
const throwFatalError = () => {
throw new Error(
'Something went wrong. Please submit an issue at https://github.com/trpc/trpc/issues/new',
);
};
/**
* Dataloader that's very inspired by https://github.com/graphql/dataloader
* Less configuration, no caching, and allows you to cancel requests
* When cancelling a single fetch the whole batch will be cancelled only when _all_ items are cancelled
*/
export function dataLoader<TKey, TValue>(
batchLoader: BatchLoader<TKey, TValue>,
) {
let pendingItems: BatchItem<TKey, TValue>[] | null = null;
let dispatchTimer: ReturnType<typeof setTimeout> | null = null;
const destroyTimerAndPendingItems = () => {
clearTimeout(dispatchTimer as any);
dispatchTimer = null;
pendingItems = null;
};
/**
* Iterate through the items and split them into groups based on the `batchLoader`'s validate function
*/
function groupItems(items: BatchItem<TKey, TValue>[]) {
const groupedItems: BatchItem<TKey, TValue>[][] = [[]];
let index = 0;
while (true) {
const item = items[index];
if (!item) {
// we're done
break;
}
const lastGroup = groupedItems[groupedItems.length - 1]!;
if (item.aborted) {
// Item was aborted before it was dispatched
item.reject?.(new Error('Aborted'));
index++;
continue;
}
const isValid = batchLoader.validate(
lastGroup.concat(item).map((it) => it.key),
);
if (isValid) {
lastGroup.push(item);
index++;
continue;
}
if (lastGroup.length === 0) {
item.reject?.(new Error('Input is too big for a single dispatch'));
index++;
continue;
}
// Create new group, next iteration will try to add the item to that
groupedItems.push([]);
}
return groupedItems;
}
function dispatch() {
const groupedItems = groupItems(pendingItems!);
destroyTimerAndPendingItems();
// Create batches for each group of items
for (const items of groupedItems) {
if (!items.length) {
continue;
}
const batch: Batch<TKey, TValue> = {
items,
};
for (const item of items) {
item.batch = batch;
}
const promise = batchLoader.fetch(batch.items.map((_item) => _item.key));
promise
.then(async (result) => {
await Promise.all(
result.map(async (valueOrPromise, index) => {
const item = batch.items[index]!;
try {
const value = await Promise.resolve(valueOrPromise);
item.resolve?.(value);
} catch (cause) {
item.reject?.(cause as Error);
}
item.batch = null;
item.reject = null;
item.resolve = null;
}),
);
for (const item of batch.items) {
item.reject?.(new Error('Missing result'));
item.batch = null;
}
})
.catch((cause) => {
for (const item of batch.items) {
item.reject?.(cause);
item.batch = null;
}
});
}
}
function load(key: TKey): Promise<TValue> {
const item: BatchItem<TKey, TValue> = {
aborted: false,
key,
batch: null,
resolve: throwFatalError,
reject: throwFatalError,
};
const promise = new Promise<TValue>((resolve, reject) => {
item.reject = reject;
item.resolve = resolve;
pendingItems ??= [];
pendingItems.push(item);
});
dispatchTimer ??= setTimeout(dispatch);
return promise;
}
return {
load,
};
}

View File

@@ -0,0 +1,15 @@
export function inputWithTrackedEventId(
input: unknown,
lastEventId: string | undefined,
) {
if (!lastEventId) {
return input;
}
if (input != null && typeof input !== 'object') {
return input;
}
return {
...(input ?? {}),
lastEventId,
};
}

70
node_modules/@trpc/client/src/internals/signals.ts generated vendored Normal file
View File

@@ -0,0 +1,70 @@
import type { Maybe } from '@trpc/server/unstable-core-do-not-import';
/**
* Like `Promise.all()` but for abort signals
* - When all signals have been aborted, the merged signal will be aborted
* - If one signal is `null`, no signal will be aborted
*/
export function allAbortSignals(...signals: Maybe<AbortSignal>[]): AbortSignal {
const ac = new AbortController();
const count = signals.length;
let abortedCount = 0;
const onAbort = () => {
if (++abortedCount === count) {
ac.abort();
}
};
for (const signal of signals) {
if (signal?.aborted) {
onAbort();
} else {
signal?.addEventListener('abort', onAbort, {
once: true,
});
}
}
return ac.signal;
}
/**
* Like `Promise.race` but for abort signals
*
* Basically, a ponyfill for
* [`AbortSignal.any`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static).
*/
export function raceAbortSignals(
...signals: Maybe<AbortSignal>[]
): AbortSignal {
const ac = new AbortController();
for (const signal of signals) {
if (signal?.aborted) {
ac.abort();
} else {
signal?.addEventListener('abort', () => ac.abort(), { once: true });
}
}
return ac.signal;
}
export function abortSignalToPromise(signal: AbortSignal): Promise<never> {
return new Promise((_, reject) => {
if (signal.aborted) {
reject(signal.reason);
return;
}
signal.addEventListener(
'abort',
() => {
reject(signal.reason);
},
{ once: true },
);
});
}

76
node_modules/@trpc/client/src/internals/transformer.ts generated vendored Normal file
View File

@@ -0,0 +1,76 @@
import type {
AnyClientTypes,
CombinedDataTransformer,
DataTransformerOptions,
TypeError,
} from '@trpc/server/unstable-core-do-not-import';
/**
* @internal
*/
export type CoercedTransformerParameters = {
transformer?: DataTransformerOptions;
};
type TransformerOptionYes = {
/**
* Data transformer
*
* You must use the same transformer on the backend and frontend
* @see https://trpc.io/docs/v11/data-transformers
**/
transformer: DataTransformerOptions;
};
type TransformerOptionNo = {
/**
* Data transformer
*
* You must use the same transformer on the backend and frontend
* @see https://trpc.io/docs/v11/data-transformers
**/
transformer?: TypeError<'You must define a transformer on your your `initTRPC`-object first'>;
};
/**
* @internal
*/
export type TransformerOptions<
TRoot extends Pick<AnyClientTypes, 'transformer'>,
> = TRoot['transformer'] extends true
? TransformerOptionYes
: TransformerOptionNo;
/**
* @internal
*/
/**
* @internal
*/
export function getTransformer(
transformer:
| TransformerOptions<{ transformer: false }>['transformer']
| TransformerOptions<{ transformer: true }>['transformer']
| undefined,
): CombinedDataTransformer {
const _transformer =
transformer as CoercedTransformerParameters['transformer'];
if (!_transformer) {
return {
input: {
serialize: (data) => data,
deserialize: (data) => data,
},
output: {
serialize: (data) => data,
deserialize: (data) => data,
},
};
}
if ('input' in _transformer) {
return _transformer;
}
return {
input: _transformer,
output: _transformer,
};
}

102
node_modules/@trpc/client/src/internals/types.ts generated vendored Normal file
View File

@@ -0,0 +1,102 @@
/**
* A subset of the standard fetch function type needed by tRPC internally.
* @see fetch from lib.dom.d.ts
* @remarks
* If you need a property that you know exists but doesn't exist on this
* interface, go ahead and add it.
*/
export type FetchEsque = (
input: RequestInfo | URL | string,
init?: RequestInit | RequestInitEsque,
) => Promise<ResponseEsque>;
/**
* A simpler version of the native fetch function's type for packages with
* their own fetch types, such as undici and node-fetch.
*/
export type NativeFetchEsque = (
url: URL | string,
init?: NodeFetchRequestInitEsque,
) => Promise<ResponseEsque>;
export interface NodeFetchRequestInitEsque {
body?: string;
}
/**
* A subset of the standard RequestInit properties needed by tRPC internally.
* @see RequestInit from lib.dom.d.ts
* @remarks
* If you need a property that you know exists but doesn't exist on this
* interface, go ahead and add it.
*/
export interface RequestInitEsque {
/**
* Sets the request's body.
*/
body?: FormData | string | null | Uint8Array<ArrayBuffer> | Blob | File;
/**
* Sets the request's associated headers.
*/
headers?: [string, string][] | Record<string, string>;
/**
* The request's HTTP-style method.
*/
method?: string;
/**
* Sets the request's signal.
*/
signal?: AbortSignal | undefined;
}
/**
* A subset of the standard ReadableStream properties needed by tRPC internally.
* @see ReadableStream from lib.dom.d.ts
*/
export type WebReadableStreamEsque = {
getReader: () => ReadableStreamDefaultReader<Uint8Array>;
};
export type NodeJSReadableStreamEsque = {
on(
eventName: string | symbol,
listener: (...args: any[]) => void,
): NodeJSReadableStreamEsque;
};
/**
* A subset of the standard Response properties needed by tRPC internally.
* @see Response from lib.dom.d.ts
*/
export interface ResponseEsque {
readonly ok: boolean;
readonly body?: NodeJSReadableStreamEsque | WebReadableStreamEsque | null;
/**
* @remarks
* The built-in Response::json() method returns Promise<any>, but
* that's not as type-safe as unknown. We use unknown because we're
* more type-safe. You do want more type safety, right? 😉
*/
json(): Promise<unknown>;
}
/**
* @internal
*/
export type NonEmptyArray<TItem> = [TItem, ...TItem[]];
type ClientContext = Record<string, unknown>;
/**
* @public
*/
export interface TRPCProcedureOptions {
/**
* Client-side context
*/
context?: ClientContext;
signal?: AbortSignal;
}