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 all 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
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,24 @@ public SignalsHandler(SecureSignalsRegistry registry) {
/**
* Subscribes to a signal.
*
* @param signalProviderEndpointMethod
* @param providerEndpoint
* the endpoint that provides the signal
* @param providerMethod
* the endpoint method that provides the signal
* @param clientSignalId
* the client signal id
*
* @return a Flux of JSON events
*/
public Flux<ObjectNode> subscribe(String signalProviderEndpointMethod,
String clientSignalId) {
public Flux<ObjectNode> subscribe(String providerEndpoint,
String providerMethod, String clientSignalId) {
try {
String[] endpointMethodParts = signalProviderEndpointMethod
.split("\\.");
var endpointName = endpointMethodParts[0];
var methodName = endpointMethodParts[1];

var signal = registry.get(clientSignalId);
if (signal != null) {
return signal.subscribe();
}

registry.register(clientSignalId, endpointName, methodName);
registry.register(clientSignalId, providerEndpoint, providerMethod);
return registry.get(clientSignalId).subscribe();
} catch (Exception e) {
return Flux.error(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ public void when_signalAlreadyRegistered_subscribe_returnsSubscriptionOfSameInst
.put("type", "snapshot");

// first client subscribe to a signal, it registers the signal:
Flux<ObjectNode> firstFlux = signalsHandler.subscribe("endpoint.method",
CLIENT_SIGNAL_ID_1);
Flux<ObjectNode> firstFlux = signalsHandler.subscribe("endpoint",
"method", CLIENT_SIGNAL_ID_1);
firstFlux.subscribe(next -> {
assertNotNull(next);
assertEquals(expectedSignalEventJson, next);
Expand All @@ -59,8 +59,8 @@ public void when_signalAlreadyRegistered_subscribe_returnsSubscriptionOfSameInst
});

// another client subscribes to the same signal:
Flux<ObjectNode> secondFlux = signalsHandler
.subscribe("endpoint.method", CLIENT_SIGNAL_ID_2);
Flux<ObjectNode> secondFlux = signalsHandler.subscribe("endpoint",
"method", CLIENT_SIGNAL_ID_2);
secondFlux.subscribe(next -> {
assertNotNull(next);
assertEquals(expectedSignalEventJson, next);
Expand All @@ -84,8 +84,8 @@ public void when_signalIsRegistered_update_notifiesTheSubscribers()
var signalId = numberSignal.getId();
when(signalsRegistry.get(CLIENT_SIGNAL_ID_1)).thenReturn(numberSignal);

Flux<ObjectNode> firstFlux = signalsHandler.subscribe("endpoint.method",
CLIENT_SIGNAL_ID_1);
Flux<ObjectNode> firstFlux = signalsHandler.subscribe("endpoint",
"method", CLIENT_SIGNAL_ID_1);

var setEvent = new ObjectNode(mapper.getNodeFactory()).put("value", 42)
.put("id", UUID.randomUUID().toString()).put("type", "set");
Expand Down
79 changes: 34 additions & 45 deletions packages/ts/generator-plugin-signals/src/SignalProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
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 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 CONNECT_CLIENT = '$CONNECT_CLIENT$';
const METHOD_NAME = '$METHOD_NAME$';
const SIGNAL = '$SIGNAL$';

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

export default class SignalProcessor {
readonly #dependencyManager: DependencyManager;
Expand All @@ -31,54 +32,28 @@ 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 [, 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 body = template(
`
function dummy() {
return new ${NUMBER_SIGNAL_CHANNEL}('${this.#service}.${methodName}', ${CONNECT_CLIENT}).signal;
return template(
`function ${METHOD_NAME}() {
return new ${SIGNAL}(undefined, { client: ${CONNECT_CLIENT}, endpoint: '${this.#service}', method: '${tsNode.name.text}' });
}`,
(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 === METHOD_NAME ? tsNode.name : 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 +83,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 (type) {
const signalId = imports.named.getIdentifier(HILLA_REACT_SIGNALS, type.text);

if (result) {
const [path, id] = result;
imports.default.remove(path);
imports.named.add(HILLA_REACT_SIGNALS, id.text, true, id);
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,11 @@
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 } 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 counter_1() {
return new NumberSignal_1(undefined, { client: client_1, endpoint: "NumberSignalService", method: "counter" });
}
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; }
function sharedValue_1() {
return new NumberSignal_1(undefined, { client: client_1, endpoint: "NumberSignalService", method: "sharedValue" });
}
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,9 @@
import { type NumberSignal as NumberSignal_1, NumberSignalChannel as NumberSignalChannel_1 } from "@vaadin/hilla-react-signals";
import { NumberSignal as NumberSignal_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; }
function counter_1() {
return new NumberSignal_1(undefined, { client: client_1, endpoint: "NumberSignalService", method: "counter" });
}
function sharedValue_1() {
return new NumberSignal_1(undefined, { client: client_1, endpoint: "NumberSignalService", method: "sharedValue" });
}
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);
}
1 change: 1 addition & 0 deletions packages/ts/react-signals/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"extends": ["../../../.eslintrc"],
"parserOptions": {
"project": "./tsconfig.json"
}
Expand Down
96 changes: 0 additions & 96 deletions packages/ts/react-signals/src/EventChannel.ts

This file was deleted.

Loading