UNPKG

@vaadin/hilla-react-signals

Version:

Signals for Hilla React

169 lines 5.3 kB
import { createSetCommand } from './commands.js'; import { computed, signal, Signal } from './core.js'; import { randomId } from './utils.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 } = this.config; this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', { providerEndpoint: endpoint, providerMethod: method, clientSignalId: this.#id, params, }); return this.#subscription; } async update(command, init) { const onTheFly = !this.#subscription; if (onTheFly) { this.connect(); } await this.config.client.call(ENDPOINT, 'update', { clientSignalId: this.#id, command, }, init ?? { mute: true }); 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; parent; constructor(value, config, id, parent) { super(value, () => (!parent ? this.#connect() : undefined), () => (!parent ? this.#disconnect() : undefined)); this.id = id ?? randomId(); this.server = new ServerConnection(this.id, config); this.parent = parent; this.subscribe((v) => { if (!this.#paused) { this.#pending.value = true; this.#error.value = undefined; this[$update](createSetCommand('', v)); } }); this.#paused = false; } #operationPromises = new Map(); [$createOperation]({ id, promise }) { const thens = this.#operationPromises; const promises = []; if (promise) { promises.push(promise); } if (id) { const { promise: p, resolve, reject } = Promise.withResolvers(); promises.push(p); 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](command) { if (this.parent) { const routedCommand = { ...command, targetNodeId: this.id }; return this.parent[$update](routedCommand); } return this.server .update(command) .catch((error) => { this.#error.value = error instanceof Error ? error : new Error(String(error)); }) .finally(() => { this.#pending.value = false; }); } [$resolveOperation](commandId, reason) { const operationPromise = this.#operationPromises.get(commandId); if (operationPromise) { this.#operationPromises.delete(commandId); if (reason) { operationPromise.reject(reason); } else { operationPromise.resolve(); } } } #connect() { this.server .connect() .onSubscriptionLost(() => 'resubscribe') .onNext((command) => { this.#paused = true; this[$processServerResponse](command); this.#paused = false; }); } #disconnect() { if (this.server.subscription === undefined) { return; } this.server.disconnect(); } } //# sourceMappingURL=FullStackSignal.js.map