@ai-sdk/rsc
Version:
[React Server Components](https://react.dev/reference/rsc/server-components) for the [AI SDK](https://ai-sdk.dev/docs):
297 lines (249 loc) • 8.45 kB
text/typescript
import { HANGING_STREAM_WARNING_TIME_MS } from '../util/constants';
import { createResolvablePromise } from '../util/create-resolvable-promise';
import {
STREAMABLE_VALUE_TYPE,
StreamablePatch,
StreamableValue,
} from './streamable-value';
const STREAMABLE_VALUE_INTERNAL_LOCK = Symbol('streamable.value.lock');
/**
* Create a wrapped, changeable value that can be streamed to the client.
* On the client side, the value can be accessed via the readStreamableValue() API.
*/
function createStreamableValue<T = any, E = any>(
initialValue?: T | ReadableStream<T>,
) {
const isReadableStream =
initialValue instanceof ReadableStream ||
(typeof initialValue === 'object' &&
initialValue !== null &&
'getReader' in initialValue &&
typeof initialValue.getReader === 'function' &&
'locked' in initialValue &&
typeof initialValue.locked === 'boolean');
if (!isReadableStream) {
return createStreamableValueImpl<T, E>(initialValue);
}
const streamableValue = createStreamableValueImpl<T, E>();
// Since the streamable value will be from a readable stream, it's not allowed
// to update the value manually as that introduces race conditions and
// unexpected behavior.
// We lock the value to prevent any updates from the user.
streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = true;
(async () => {
try {
// Consume the readable stream and update the value.
const reader = initialValue.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
// Unlock the value to allow updates.
streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = false;
if (typeof value === 'string') {
streamableValue.append(value);
} else {
streamableValue.update(value);
}
// Lock the value again.
streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = true;
}
streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = false;
streamableValue.done();
} catch (e) {
streamableValue[STREAMABLE_VALUE_INTERNAL_LOCK] = false;
streamableValue.error(e);
}
})();
return streamableValue;
}
// It's necessary to define the type manually here, otherwise TypeScript compiler
// will not be able to infer the correct return type as it's circular.
type StreamableValueWrapper<T, E> = {
/**
* The value of the streamable. This can be returned from a Server Action and
* received by the client. To read the streamed values, use the
* `readStreamableValue` or `useStreamableValue` APIs.
*/
readonly value: StreamableValue<T, E>;
/**
* This method updates the current value with a new one.
*/
update(value: T): StreamableValueWrapper<T, E>;
/**
* This method is used to append a delta string to the current value. It
* requires the current value of the streamable to be a string.
*
* @example
* ```jsx
* const streamable = createStreamableValue('hello');
* streamable.append(' world');
*
* // The value will be 'hello world'
* ```
*/
append(value: T): StreamableValueWrapper<T, E>;
/**
* This method is used to signal that there is an error in the value stream.
* It will be thrown on the client side when consumed via
* `readStreamableValue` or `useStreamableValue`.
*/
error(error: any): StreamableValueWrapper<T, E>;
/**
* This method marks the value as finalized. You can either call it without
* any parameters or with a new value as the final state.
* Once called, the value cannot be updated or appended anymore.
*
* This method is always **required** to be called, otherwise the response
* will be stuck in a loading state.
*/
done(...args: [T] | []): StreamableValueWrapper<T, E>;
/**
* @internal This is an internal lock to prevent the value from being
* updated by the user.
*/
[STREAMABLE_VALUE_INTERNAL_LOCK]: boolean;
};
function createStreamableValueImpl<T = any, E = any>(initialValue?: T) {
let closed = false;
let locked = false;
let resolvable = createResolvablePromise<StreamableValue<T, E>>();
let currentValue = initialValue;
let currentError: E | undefined;
let currentPromise: typeof resolvable.promise | undefined =
resolvable.promise;
let currentPatchValue: StreamablePatch;
function assertStream(method: string) {
if (closed) {
throw new Error(method + ': Value stream is already closed.');
}
if (locked) {
throw new Error(
method + ': Value stream is locked and cannot be updated.',
);
}
}
let warningTimeout: NodeJS.Timeout | undefined;
function warnUnclosedStream() {
if (process.env.NODE_ENV === 'development') {
if (warningTimeout) {
clearTimeout(warningTimeout);
}
warningTimeout = setTimeout(() => {
console.warn(
'The streamable value has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.',
);
}, HANGING_STREAM_WARNING_TIME_MS);
}
}
warnUnclosedStream();
function createWrapped(initialChunk?: boolean): StreamableValue<T, E> {
// This makes the payload much smaller if there're mutative updates before the first read.
let init: Partial<StreamableValue<T, E>>;
if (currentError !== undefined) {
init = { error: currentError };
} else {
if (currentPatchValue && !initialChunk) {
init = { diff: currentPatchValue };
} else {
init = { curr: currentValue };
}
}
if (currentPromise) {
init.next = currentPromise;
}
if (initialChunk) {
init.type = STREAMABLE_VALUE_TYPE;
}
return init;
}
// Update the internal `currentValue` and `currentPatchValue` if needed.
function updateValueStates(value: T) {
// If we can only send a patch over the wire, it's better to do so.
currentPatchValue = undefined;
if (typeof value === 'string') {
if (typeof currentValue === 'string') {
if (value.startsWith(currentValue)) {
currentPatchValue = [0, value.slice(currentValue.length)];
}
}
}
currentValue = value;
}
const streamable: StreamableValueWrapper<T, E> = {
set [STREAMABLE_VALUE_INTERNAL_LOCK](state: boolean) {
locked = state;
},
get value() {
return createWrapped(true);
},
update(value: T) {
assertStream('.update()');
const resolvePrevious = resolvable.resolve;
resolvable = createResolvablePromise();
updateValueStates(value);
currentPromise = resolvable.promise;
resolvePrevious(createWrapped());
warnUnclosedStream();
return streamable;
},
append(value: T) {
assertStream('.append()');
if (
typeof currentValue !== 'string' &&
typeof currentValue !== 'undefined'
) {
throw new Error(
`.append(): The current value is not a string. Received: ${typeof currentValue}`,
);
}
if (typeof value !== 'string') {
throw new Error(
`.append(): The value is not a string. Received: ${typeof value}`,
);
}
const resolvePrevious = resolvable.resolve;
resolvable = createResolvablePromise();
if (typeof currentValue === 'string') {
currentPatchValue = [0, value];
(currentValue as string) = currentValue + value;
} else {
currentPatchValue = undefined;
currentValue = value;
}
currentPromise = resolvable.promise;
resolvePrevious(createWrapped());
warnUnclosedStream();
return streamable;
},
error(error: any) {
assertStream('.error()');
if (warningTimeout) {
clearTimeout(warningTimeout);
}
closed = true;
currentError = error;
currentPromise = undefined;
resolvable.resolve({ error });
return streamable;
},
done(...args: [] | [T]) {
assertStream('.done()');
if (warningTimeout) {
clearTimeout(warningTimeout);
}
closed = true;
currentPromise = undefined;
if (args.length) {
updateValueStates(args[0]);
resolvable.resolve(createWrapped());
return streamable;
}
resolvable.resolve({});
return streamable;
},
};
return streamable;
}
export { createStreamableValue };