@vaadin/hilla-react-signals
Advanced tools
Comparing version
@@ -1,4 +0,2 @@ | ||
import { installAutoSignalTracking } from "@preact/signals-react/runtime"; | ||
installAutoSignalTracking(); | ||
export * from "@preact/signals-react"; | ||
//# sourceMappingURL=core.js.map |
@@ -5,2 +5,8 @@ import type { ConnectClient, Subscription } from '@vaadin/hilla-frontend'; | ||
/** | ||
* A return type for signal operations. | ||
*/ | ||
export type Operation = { | ||
result: Promise<void>; | ||
}; | ||
/** | ||
* An abstraction of a signal that tracks the number of subscribers, and calls | ||
@@ -71,2 +77,4 @@ * the provided `onSubscribe` and `onUnsubscribe` callbacks for the first | ||
export declare const $setValueQuietly: unique symbol; | ||
export declare const $resolveOperation: unique symbol; | ||
export declare const $createOperation: unique symbol; | ||
/** | ||
@@ -100,2 +108,6 @@ * A signal that holds a shared value. Each change to the value is propagated to | ||
constructor(value: T | undefined, config: ServerConnectionConfig, id?: string); | ||
protected [$createOperation]({ id, promise }: { | ||
id?: string; | ||
promise?: Promise<void>; | ||
}): Operation; | ||
/** | ||
@@ -111,5 +123,13 @@ * Sets the local value of the signal without sending any events to the server | ||
* @param event - The event to update the server with. | ||
* @returns The server response promise. | ||
*/ | ||
protected [$update](event: StateEvent): void; | ||
protected [$update](event: StateEvent): Promise<void>; | ||
/** | ||
* Resolves the operation promise associated with the given event id. | ||
* | ||
* @param eventId - The event id. | ||
* @param reason - The reason to reject the promise (if any). | ||
*/ | ||
protected [$resolveOperation](eventId: string, reason?: string): void; | ||
/** | ||
* A method with to process the server response. The implementation is | ||
@@ -116,0 +136,0 @@ * specific for each signal type. |
@@ -59,2 +59,6 @@ import { nanoid } from "nanoid"; | ||
async update(event) { | ||
const onTheFly = !this.#subscription; | ||
if (onTheFly) { | ||
this.connect(); | ||
} | ||
await this.config.client.call(ENDPOINT, "update", { | ||
@@ -64,2 +68,5 @@ clientSignalId: this.#id, | ||
}); | ||
if (onTheFly) { | ||
this.disconnect(); | ||
} | ||
} | ||
@@ -74,2 +81,4 @@ disconnect() { | ||
const $setValueQuietly = Symbol("setValueQuietly"); | ||
const $resolveOperation = Symbol("resolveOperation"); | ||
const $createOperation = Symbol("createOperation"); | ||
class FullStackSignal extends DependencyTrackingSignal { | ||
@@ -116,2 +125,31 @@ /** | ||
} | ||
// stores the promise handlers associated to operations | ||
#operationPromises = /* @__PURE__ */ new Map(); | ||
// creates the obejct to be returned by operations to allow defining callbacks | ||
[$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 void 0; | ||
} | ||
throw lastResult.reason; | ||
}) | ||
}; | ||
} | ||
/** | ||
@@ -131,5 +169,6 @@ * Sets the local value of the signal without sending any events to the server | ||
* @param event - The event to update the server with. | ||
* @returns The server response promise. | ||
*/ | ||
[$update](event) { | ||
this.server.update(event).catch((error) => { | ||
async [$update](event) { | ||
return this.server.update(event).catch((error) => { | ||
this.#error.value = error instanceof Error ? error : new Error(String(error)); | ||
@@ -140,2 +179,19 @@ }).finally(() => { | ||
} | ||
/** | ||
* Resolves the operation promise associated with the given event id. | ||
* | ||
* @param eventId - The event id. | ||
* @param reason - The reason to reject the promise (if any). | ||
*/ | ||
[$resolveOperation](eventId, reason) { | ||
const operationPromise = this.#operationPromises.get(eventId); | ||
if (operationPromise) { | ||
this.#operationPromises.delete(eventId); | ||
if (reason) { | ||
operationPromise.reject(reason); | ||
} else { | ||
operationPromise.resolve(); | ||
} | ||
} | ||
} | ||
#connect() { | ||
@@ -156,3 +212,5 @@ this.server.connect().onSubscriptionLost(() => "resubscribe").onNext((event) => { | ||
export { | ||
$createOperation, | ||
$processServerResponse, | ||
$resolveOperation, | ||
$setValueQuietly, | ||
@@ -159,0 +217,0 @@ $update, |
import { CollectionSignal } from './CollectionSignal.js'; | ||
import { type StateEvent } from './events.js'; | ||
import { $processServerResponse, type ServerConnectionConfig } from './FullStackSignal.js'; | ||
import { $processServerResponse, type Operation, type ServerConnectionConfig } from './FullStackSignal.js'; | ||
import { ValueSignal } from './ValueSignal.js'; | ||
@@ -24,3 +24,3 @@ /** | ||
*/ | ||
insertLast(value: T): void; | ||
insertLast(value: T): Operation; | ||
/** | ||
@@ -30,4 +30,4 @@ * Removes the given item from the list. | ||
*/ | ||
remove(item: ValueSignal<T>): void; | ||
remove(item: ValueSignal<T>): Operation; | ||
} | ||
//# sourceMappingURL=ListSignal.d.ts.map |
@@ -9,3 +9,9 @@ import { CollectionSignal } from "./CollectionSignal.js"; | ||
} from "./events.js"; | ||
import { $processServerResponse, $setValueQuietly, $update } from "./FullStackSignal.js"; | ||
import { | ||
$createOperation, | ||
$processServerResponse, | ||
$resolveOperation, | ||
$setValueQuietly, | ||
$update | ||
} from "./FullStackSignal.js"; | ||
import { ValueSignal } from "./ValueSignal.js"; | ||
@@ -32,2 +38,3 @@ class ListSignal extends CollectionSignal { | ||
if (!event.accepted) { | ||
this[$resolveOperation](event.id, "server rejected the operation"); | ||
return; | ||
@@ -42,2 +49,3 @@ } | ||
} | ||
this[$resolveOperation](event.id); | ||
} | ||
@@ -117,3 +125,4 @@ #handleInsertLastUpdate(event) { | ||
const event = createInsertLastStateEvent(value); | ||
this[$update](event); | ||
const promise = this[$update](event); | ||
return this[$createOperation]({ id: event.id, promise }); | ||
} | ||
@@ -127,6 +136,7 @@ /** | ||
if (entryToRemove === void 0) { | ||
return; | ||
return { result: Promise.resolve() }; | ||
} | ||
const removeEvent = createRemoveStateEvent(entryToRemove.value.id); | ||
this[$update](removeEvent); | ||
const promise = this[$update](removeEvent); | ||
return this[$createOperation]({ id: removeEvent.id, promise }); | ||
} | ||
@@ -133,0 +143,0 @@ } |
import { type StateEvent } from './events.js'; | ||
import { $processServerResponse } from './FullStackSignal.js'; | ||
import { $processServerResponse, type Operation } from './FullStackSignal.js'; | ||
import { ValueSignal } from './ValueSignal.js'; | ||
@@ -40,6 +40,7 @@ /** | ||
* negative. | ||
* @returns An operation object that allows to perform additional actions. | ||
*/ | ||
incrementBy(delta: number): void; | ||
incrementBy(delta: number): Operation; | ||
protected [$processServerResponse](event: StateEvent): void; | ||
} | ||
//# sourceMappingURL=NumberSignal.d.ts.map |
import { createIncrementStateEvent, isIncrementStateEvent } from "./events.js"; | ||
import { $processServerResponse, $setValueQuietly, $update } from "./FullStackSignal.js"; | ||
import { | ||
$createOperation, | ||
$processServerResponse, | ||
$resolveOperation, | ||
$setValueQuietly, | ||
$update | ||
} from "./FullStackSignal.js"; | ||
import { ValueSignal } from "./ValueSignal.js"; | ||
@@ -17,6 +23,7 @@ class NumberSignal extends ValueSignal { | ||
* negative. | ||
* @returns An operation object that allows to perform additional actions. | ||
*/ | ||
incrementBy(delta) { | ||
if (delta === 0) { | ||
return; | ||
return { result: Promise.resolve() }; | ||
} | ||
@@ -26,11 +33,14 @@ this[$setValueQuietly](this.value + delta); | ||
this.#sentIncrementEvents.set(event.id, event); | ||
this[$update](event); | ||
const promise = this[$update](event); | ||
return this[$createOperation]({ id: event.id, promise }); | ||
} | ||
[$processServerResponse](event) { | ||
if (event.accepted && isIncrementStateEvent(event)) { | ||
if (this.#sentIncrementEvents.has(event.id)) { | ||
const sentEvent = this.#sentIncrementEvents.get(event.id); | ||
if (sentEvent) { | ||
this.#sentIncrementEvents.delete(event.id); | ||
return; | ||
} else { | ||
this[$setValueQuietly](this.value + event.value); | ||
} | ||
this[$setValueQuietly](this.value + event.value); | ||
this[$resolveOperation](event.id); | ||
} else { | ||
@@ -37,0 +47,0 @@ super[$processServerResponse](event); |
{ | ||
"name": "@vaadin/hilla-react-signals", | ||
"version": "24.6.0-alpha2", | ||
"version": "24.6.0-alpha3", | ||
"description": "Signals for Hilla React", | ||
@@ -50,3 +50,3 @@ "main": "index.js", | ||
"@preact/signals-react": "^2.0.0", | ||
"@vaadin/hilla-frontend": "24.6.0-alpha2", | ||
"@vaadin/hilla-frontend": "24.6.0-alpha3", | ||
"nanoid": "^5.0.7" | ||
@@ -53,0 +53,0 @@ }, |
import { type StateEvent } from './events.js'; | ||
import { $processServerResponse, FullStackSignal } from './FullStackSignal.js'; | ||
import { $processServerResponse, FullStackSignal, type Operation } from './FullStackSignal.js'; | ||
/** | ||
* An operation subscription that can be canceled. | ||
*/ | ||
export interface OperationSubscription { | ||
export interface OperationSubscription extends Operation { | ||
cancel(): void; | ||
@@ -31,4 +31,5 @@ } | ||
* @param newValue - The new value. | ||
* @returns An operation object that allows to perform additional actions. | ||
*/ | ||
replace(expected: T, newValue: T): void; | ||
replace(expected: T, newValue: T): Operation; | ||
/** | ||
@@ -45,3 +46,3 @@ * Tries to update the value by applying the callback function to the current | ||
* produce the new value. | ||
* @returns An operation subscription that can be canceled. | ||
* @returns An operation object that allows to perform additional actions, including cancellation. | ||
*/ | ||
@@ -48,0 +49,0 @@ update(callback: (value: T) => T): OperationSubscription; |
@@ -0,1 +1,2 @@ | ||
import { nanoid } from "nanoid"; | ||
import { | ||
@@ -7,3 +8,9 @@ createReplaceStateEvent, | ||
} from "./events.js"; | ||
import { $processServerResponse, $update, FullStackSignal } from "./FullStackSignal.js"; | ||
import { | ||
$createOperation, | ||
$processServerResponse, | ||
$resolveOperation, | ||
$update, | ||
FullStackSignal | ||
} from "./FullStackSignal.js"; | ||
class ValueSignal extends FullStackSignal { | ||
@@ -30,2 +37,3 @@ #pendingRequests = /* @__PURE__ */ new Map(); | ||
* @param newValue - The new value. | ||
* @returns An operation object that allows to perform additional actions. | ||
*/ | ||
@@ -35,3 +43,5 @@ replace(expected, newValue) { | ||
const signalId = parentClientSignalId !== void 0 ? this.id : void 0; | ||
this[$update](createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId)); | ||
const event = createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId); | ||
const promise = this[$update](event); | ||
return this[$createOperation]({ id: event.id, promise }); | ||
} | ||
@@ -49,3 +59,3 @@ /** | ||
* produce the new value. | ||
* @returns An operation subscription that can be canceled. | ||
* @returns An operation object that allows to perform additional actions, including cancellation. | ||
*/ | ||
@@ -55,10 +65,9 @@ update(callback) { | ||
const event = createReplaceStateEvent(this.value, newValue); | ||
this[$update](event); | ||
const waiter = Promise.withResolvers(); | ||
const pendingRequest = { callback, waiter, canceled: false }; | ||
const promise = this[$update](event); | ||
const pendingRequest = { id: nanoid(), callback, canceled: false }; | ||
this.#pendingRequests.set(event.id, pendingRequest); | ||
return { | ||
...this[$createOperation]({ id: pendingRequest.id, promise }), | ||
cancel: () => { | ||
pendingRequest.canceled = true; | ||
pendingRequest.waiter.resolve(); | ||
} | ||
@@ -71,14 +80,13 @@ }; | ||
this.#pendingRequests.delete(event.id); | ||
} | ||
if (!event.accepted && record) { | ||
if (!record.canceled) { | ||
if (!(event.accepted || record.canceled)) { | ||
this.update(record.callback); | ||
} | ||
} | ||
let reason; | ||
if (event.accepted || isSnapshotStateEvent(event)) { | ||
if (record) { | ||
record.waiter.resolve(); | ||
} | ||
this.#applyAcceptedEvent(event); | ||
} else { | ||
reason = "server rejected the operation"; | ||
} | ||
[record?.id, event.id].filter(Boolean).forEach((id) => this[$resolveOperation](id, reason)); | ||
} | ||
@@ -85,0 +93,0 @@ #applyAcceptedEvent(event) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
85403
11.95%1016
11.65%+ Added
- Removed