@vaadin/hilla-react-signals
Version:
Signals for Hilla React
165 lines • 5.15 kB
JavaScript
import { nanoid } from 'nanoid';
import { computed, signal, Signal } from './core.js';
import { createSetStateEvent } from './events.js';
const ENDPOINT = 'SignalsHandler';
export class DependencyTrackingSignal extends Signal {
#onFirstSubscribe;
#onLastUnsubscribe;
#subscribeCount = -1;
constructor(value, onFirstSubscribe, onLastUnsubscribe) {
super(value);
this.#onFirstSubscribe = onFirstSubscribe;
this.#onLastUnsubscribe = onLastUnsubscribe;
}
S(node) {
super.S(node);
if (this.#subscribeCount === 0) {
this.#onFirstSubscribe();
}
this.#subscribeCount += 1;
}
U(node) {
super.U(node);
this.#subscribeCount -= 1;
if (this.#subscribeCount === 0) {
this.#onLastUnsubscribe();
}
}
}
class ServerConnection {
#id;
config;
#subscription;
constructor(id, config) {
this.config = config;
this.#id = id;
}
get subscription() {
return this.#subscription;
}
connect() {
const { client, endpoint, method, params, parentClientSignalId } = this.config;
this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {
providerEndpoint: endpoint,
providerMethod: method,
clientSignalId: this.#id,
params,
parentClientSignalId,
});
return this.#subscription;
}
async update(event) {
const onTheFly = !this.#subscription;
if (onTheFly) {
this.connect();
}
await this.config.client.call(ENDPOINT, 'update', {
clientSignalId: this.#id,
event,
});
if (onTheFly) {
this.disconnect();
}
}
disconnect() {
this.#subscription?.cancel();
this.#subscription = undefined;
}
}
export const $update = Symbol('update');
export const $processServerResponse = Symbol('processServerResponse');
export const $setValueQuietly = Symbol('setValueQuietly');
export const $resolveOperation = Symbol('resolveOperation');
export const $createOperation = Symbol('createOperation');
export class FullStackSignal extends DependencyTrackingSignal {
id;
server;
pending = computed(() => this.#pending.value);
error = computed(() => this.#error.value);
#pending = signal(false);
#error = signal(undefined);
#paused = true;
constructor(value, config, id) {
super(value, () => this.#connect(), () => this.#disconnect());
this.id = id ?? nanoid();
this.server = new ServerConnection(this.id, config);
this.subscribe((v) => {
if (!this.#paused) {
this.#pending.value = true;
this.#error.value = undefined;
const signalId = config.parentClientSignalId !== undefined ? this.id : undefined;
this[$update](createSetStateEvent(v, signalId, config.parentClientSignalId));
}
});
this.#paused = false;
}
#operationPromises = new Map();
[$createOperation]({ id, promise }) {
const thens = this.#operationPromises;
const promises = [];
if (promise) {
promises.push(promise);
}
if (id) {
promises.push(new Promise((resolve, reject) => {
thens.set(id, { resolve, reject });
}));
}
if (promises.length === 0) {
promises.push(Promise.resolve());
}
return {
result: Promise.allSettled(promises).then((results) => {
const lastResult = results[results.length - 1];
if (lastResult.status === 'fulfilled') {
return undefined;
}
throw lastResult.reason;
}),
};
}
[$setValueQuietly](value) {
this.#paused = true;
super.value = value;
this.#paused = false;
}
async [$update](event) {
return this.server
.update(event)
.catch((error) => {
this.#error.value = error instanceof Error ? error : new Error(String(error));
})
.finally(() => {
this.#pending.value = false;
});
}
[$resolveOperation](eventId, reason) {
const operationPromise = this.#operationPromises.get(eventId);
if (operationPromise) {
this.#operationPromises.delete(eventId);
if (reason) {
operationPromise.reject(reason);
}
else {
operationPromise.resolve();
}
}
}
#connect() {
this.server
.connect()
.onSubscriptionLost(() => 'resubscribe')
.onNext((event) => {
this.#paused = true;
this[$processServerResponse](event);
this.#paused = false;
});
}
#disconnect() {
if (this.server.subscription === undefined) {
return;
}
this.server.disconnect();
}
}
//# sourceMappingURL=FullStackSignal.js.map