UNPKG

@vaadin/hilla-react-signals

Version:

Signals for Hilla React

165 lines 5.15 kB
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