Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(react-signals): simplify the implementation #2657

Merged
merged 27 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
55f8486
refactor(react-signals): simplify approach
Lodin Aug 7, 2024
1755cd9
refactor(react-signals): simplify code
Lodin Aug 8, 2024
331f6a9
Merge branch 'refs/heads/main' into feat/signals/value-signal
Lodin Aug 8, 2024
15b85eb
Merge branch 'refs/heads/main' into feat/signals/value-signal
Lodin Aug 8, 2024
05be3a1
feat(generator-utils): add traverse function for TS AST
Lodin Aug 12, 2024
f3171ca
refactor(generator-utils): update generator to generate functional `c…
Lodin Aug 12, 2024
7016595
refactor(react-signals): re-implement channel & signal implementation…
Lodin Aug 14, 2024
5382125
refactor(generator-plugin-signals): re-implement channel generation
Lodin Aug 14, 2024
8f185db
refactor(react-signals): remove timeout
Lodin Aug 15, 2024
941343c
refactor(react-signals): finalize channel API
Lodin Aug 15, 2024
e79c890
refactor(generator-plugin-signals): improve channel generation
Lodin Aug 15, 2024
3b1142b
test(generator-plugin-signals): update snapshot
Lodin Aug 15, 2024
938f3b4
Merge branch 'main' into feat/signals/value-signal
Lodin Aug 15, 2024
d047415
refactor(react-signals): run update for all dependencies in a single …
Lodin Aug 15, 2024
00bf6f5
refactor(react-signals): merge SignalChannel with Signal
Lodin Aug 15, 2024
6ba6167
refactor(generator-plugin-signals): update code
Lodin Aug 15, 2024
f5b2adc
refactor: split endpoint and method names
Lodin Aug 15, 2024
ab91b7e
refactor(react-signals): remove unnecessary name
Lodin Aug 15, 2024
4dfea71
test(react-signals): update code
Lodin Aug 16, 2024
1e603b7
test(generator-plugin-signals): update fixture
Lodin Aug 16, 2024
3b876b9
Merge branch 'main' into feat/signals/value-signal
taefi Aug 16, 2024
85708aa
fix(react-signals): get "id" field back to a change event
Lodin Aug 16, 2024
353dc71
refactor(react-signals): address review comments
Lodin Aug 19, 2024
8f36c61
refactor(react-signals): return ReadonlySignal instead of a signal value
Lodin Aug 19, 2024
8d2c52f
test(react-signals): use correct naming
Lodin Aug 19, 2024
47b0346
fix(react-signals): reset error signal on the new call
Lodin Aug 19, 2024
fb5add7
Merge branch 'main' into feat/signals/value-signal
Lodin Aug 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 43 additions & 45 deletions packages/ts/generator-plugin-signals/src/SignalProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import type Plugin from '@vaadin/hilla-generator-core/Plugin.js';
import { template, transform } from '@vaadin/hilla-generator-utils/ast.js';
import { template, transform, traverse } from '@vaadin/hilla-generator-utils/ast.js';
import createFullyUniqueIdentifier from '@vaadin/hilla-generator-utils/createFullyUniqueIdentifier.js';
import createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js';
import DependencyManager from '@vaadin/hilla-generator-utils/dependencies/DependencyManager.js';
import PathManager from '@vaadin/hilla-generator-utils/dependencies/PathManager.js';
import ts, { type FunctionDeclaration, type SourceFile } from 'typescript';
import ts, { type FunctionDeclaration, type Identifier, type SourceFile } from 'typescript';

const HILLA_REACT_SIGNALS = '@vaadin/hilla-react-signals';

const NUMBER_SIGNAL_CHANNEL = '$NUMBER_SIGNAL_CHANNEL$';
const CHANNEL = '$CHANNEL$';
const SIGNAL_CHANNEL = '$SIGNAL_CHANNEL$';
const CONNECT_CLIENT = '$CONNECT_CLIENT$';
const METHOD_NAME = '$METHOD_NAME$';
const SIGNAL = '$SIGNAL$';

const signalImportPaths = ['com/vaadin/hilla/signals/NumberSignal'];
const signals = ['NumberSignal', 'RemoteSignal'];

export default class SignalProcessor {
readonly #dependencyManager: DependencyManager;
Expand All @@ -31,54 +35,34 @@ export default class SignalProcessor {
process(): SourceFile {
this.#owner.logger.debug(`Processing signals: ${this.#service}`);
const { imports } = this.#dependencyManager;
const numberSignalChannelId = imports.named.add(HILLA_REACT_SIGNALS, 'NumberSignalChannel');

const signalChannelId = imports.named.add(HILLA_REACT_SIGNALS, 'SignalChannel');

const [, connectClientId] = imports.default.iter().find(([path]) => path.includes('connect-client'))!;

this.#processSignalImports(signalImportPaths);
const initTypeId = imports.named.getIdentifier('@vaadin/hilla-frontend', 'EndpointRequestInit');
let initTypeUsageCount = 0;

const [file] = ts.transform<SourceFile>(this.#sourceFile, [
transform((tsNode) => {
if (ts.isFunctionDeclaration(tsNode) && tsNode.name && this.#methods.has(tsNode.name.text)) {
const methodName = tsNode.name.text;
const signalId = this.#replaceSignalImport(tsNode);
const channel = createFullyUniqueIdentifier('channel');

const body = template(
`
function dummy() {
return new ${NUMBER_SIGNAL_CHANNEL}('${this.#service}.${methodName}', ${CONNECT_CLIENT}).signal;
return template(
`const ${CHANNEL} = new ${SIGNAL_CHANNEL}(${CONNECT_CLIENT}, '${this.#service}.${tsNode.name.text}')
function ${METHOD_NAME}() {
return new ${SIGNAL}(undefined, { channel: ${CHANNEL} });
}`,
(statements) => (statements[0] as FunctionDeclaration).body?.statements,
(statements) => statements,
[
transform((node) =>
ts.isIdentifier(node) && node.text === NUMBER_SIGNAL_CHANNEL ? numberSignalChannelId : node,
),
transform((node) => (ts.isIdentifier(node) && node.text === CHANNEL ? channel : node)),
transform((node) => (ts.isIdentifier(node) && node.text === METHOD_NAME ? tsNode.name : node)),
transform((node) => (ts.isIdentifier(node) && node.text === SIGNAL_CHANNEL ? signalChannelId : node)),
transform((node) => (ts.isIdentifier(node) && node.text === SIGNAL ? signalId : node)),
transform((node) => (ts.isIdentifier(node) && node.text === CONNECT_CLIENT ? connectClientId : node)),
],
);

let returnType = tsNode.type;
if (
returnType &&
ts.isTypeReferenceNode(returnType) &&
'text' in returnType.typeName &&
returnType.typeName.text === 'Promise'
) {
if (returnType.typeArguments && returnType.typeArguments.length > 0) {
returnType = returnType.typeArguments[0];
}
}

return ts.factory.createFunctionDeclaration(
tsNode.modifiers?.filter((modifier) => modifier.kind !== ts.SyntaxKind.AsyncKeyword),
tsNode.asteriskToken,
tsNode.name,
tsNode.typeParameters,
tsNode.parameters.filter(({ name }) => !(ts.isIdentifier(name) && name.text === 'init')),
returnType,
ts.factory.createBlock(body ?? [], false),
);
}
return tsNode;
}),
Expand Down Expand Up @@ -108,17 +92,31 @@ function dummy() {
);
}

#processSignalImports(signalImports: readonly string[]) {
#replaceSignalImport(method: FunctionDeclaration): Identifier {
const { imports } = this.#dependencyManager;

signalImports.forEach((signalImport) => {
const result = imports.default.iter().find(([path]) => path.includes(signalImport));
if (method.type) {
const type = traverse(method.type, (node) =>
ts.isIdentifier(node) && signals.includes(node.text) ? node : undefined,
);

if (result) {
const [path, id] = result;
imports.default.remove(path);
imports.named.add(HILLA_REACT_SIGNALS, id.text, true, id);
if (type) {
const signalId = imports.named.getIdentifier(HILLA_REACT_SIGNALS, type.text);

if (signalId) {
return signalId;
}

const result = imports.default.iter().find(([_p, id]) => id.text === type.text);

if (result) {
const [path] = result;
imports.default.remove(path);
return imports.named.add(HILLA_REACT_SIGNALS, type.text, false, type);
}
}
});
}

throw new Error('Signal type not found');
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { EndpointRequestInit as EndpointRequestInit_1 } from "@vaadin/hilla-frontend";
import { type NumberSignal as NumberSignal_1, NumberSignalChannel as NumberSignalChannel_1 } from "@vaadin/hilla-react-signals";
import { NumberSignal as NumberSignal_1, SignalChannel as SignalChannel_1 } from "@vaadin/hilla-react-signals";
import client_1 from "./connect-client.default.js";
function counter_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.counter", client_1).signal; }
const channel_1 = new SignalChannel_1(client_1, "NumberSignalService.counter");
function counter_1() {
return new NumberSignal_1(undefined, { channel: channel_1 });
}
async function sayHello_1(name: string, init?: EndpointRequestInit_1): Promise<string> { return client_1.call("NumberSignalService", "sayHello", { name }, init); }
function sharedValue_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.sharedValue", client_1).signal; }
const channel_2 = new SignalChannel_1(client_1, "NumberSignalService.sharedValue");
function sharedValue_1() {
return new NumberSignal_1(undefined, { channel: channel_2 });
}
export { counter_1 as counter, sayHello_1 as sayHello, sharedValue_1 as sharedValue };
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { type NumberSignal as NumberSignal_1, NumberSignalChannel as NumberSignalChannel_1 } from "@vaadin/hilla-react-signals";
import { NumberSignal as NumberSignal_1, SignalChannel as SignalChannel_1 } from "@vaadin/hilla-react-signals";
import client_1 from "./connect-client.default.js";
function counter_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.counter", client_1).signal; }
function sharedValue_1(): NumberSignal_1 { return new NumberSignalChannel_1("NumberSignalService.sharedValue", client_1).signal; }
const channel_1 = new SignalChannel_1(client_1, "NumberSignalService.counter");
function counter_1() {
return new NumberSignal_1(undefined, { channel: channel_1 });
}
const channel_2 = new SignalChannel_1(client_1, "NumberSignalService.sharedValue");
function sharedValue_1() {
return new NumberSignal_1(undefined, { channel: channel_2 });
}
export { counter_1 as counter, sharedValue_1 as sharedValue };
8 changes: 8 additions & 0 deletions packages/ts/generator-utils/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,11 @@ export function transform<T extends Node>(
return ts.visitEachChild(root, visitor, context);
};
}

export function traverse<T extends Node>(node: Node, visitor: (node: Node) => T | undefined): T | undefined {
function _visitor(n: Node): T | undefined {
return visitor(n) ?? ts.forEachChild(n, _visitor);
}

return _visitor(node);
}
2 changes: 1 addition & 1 deletion packages/ts/generator-utils/src/createSourceFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ts, { type SourceFile, type Statement } from 'typescript';

export default function createSourceFile(statements: readonly Statement[], fileName: string): SourceFile {
const sourceFile = ts.createSourceFile(fileName, '', ts.ScriptTarget.ES2019, undefined, ts.ScriptKind.TS);
const sourceFile = ts.createSourceFile(fileName, '', ts.ScriptTarget.ES2021, undefined, ts.ScriptKind.TS);
return ts.factory.updateSourceFile(sourceFile, statements);
}
96 changes: 0 additions & 96 deletions packages/ts/react-signals/src/EventChannel.ts

This file was deleted.

100 changes: 100 additions & 0 deletions packages/ts/react-signals/src/SignalChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { ConnectClient, Subscription } from '@vaadin/hilla-frontend';
import { nanoid } from 'nanoid';
import { batch } from './core.js';
import type { ValueSignal } from './Signals.js';

const ENDPOINT = 'SignalsHandler';

/**
* Types of changes that can be produced or processed by a signal.
*/
export enum StateEventType {
SET = 'set',
SNAPSHOT = 'snapshot',
}

/**
* An object that describes the change of the signal state.
*/
export type StateEvent = Readonly<{
id: string;
type: StateEventType;
value: unknown;
}>;

/**
* A signal channel that can be used to communicate with a server-side.
*
* The signal channel is responsible for subscribing to the server-side signal
* and updating the local signal based on the received events.
*
* @typeParam S - The type of the signal instance.
*/
export class SignalChannel {
readonly #id = nanoid();
readonly #client: ConnectClient;
readonly #method: string;
#dependencies: readonly ValueSignal[] = [];
#subscription?: Subscription<StateEvent>;

/**
* @param client - The client instance to be used for communication.
* @param signalProviderEndpointMethod - The method name of the signal provider
* service.
* @returns The signal channel instance.
*/
constructor(client: ConnectClient, signalProviderEndpointMethod: string) {
taefi marked this conversation as resolved.
Show resolved Hide resolved
this.#client = client;
this.#method = signalProviderEndpointMethod;
}

get id(): string {
return this.#id;
}

cancel(): void {
this.#subscription?.cancel();
this.#subscription = undefined;

Check warning on line 57 in packages/ts/react-signals/src/SignalChannel.ts

View check run for this annotation

Codecov / codecov/patch

packages/ts/react-signals/src/SignalChannel.ts#L56-L57

Added lines #L56 - L57 were not covered by tests
}

/**
* Connects the local signal to the server.
*
* @param signal - The signal instance to be connected.
* @param onUpdate - The callback that will be called when the signal is
* updated.
*/
connect(signal: ValueSignal, onUpdate: (promise: Promise<void>) => void): void {
let paused = false;

this.#dependencies = [...this.#dependencies, signal];

this.#subscription ??= this.#client
.subscribe(ENDPOINT, 'subscribe', {
signalProviderEndpointMethod: this.#method,
clientSignalId: this.#id,
})
.onNext((event: StateEvent) => {
if (event.type === StateEventType.SNAPSHOT) {
paused = true;
batch(() => {
for (const dependency of this.#dependencies) {
dependency.value = event.value;
}
});
paused = false;
}
});

signal.subscribe((value) => {
if (!paused) {
onUpdate(
this.#client.call(ENDPOINT, 'update', {
clientSignalId: this.#id,
event: { id: nanoid(), type: StateEventType.SET, value },
}),
);
}
});
}
}
Loading