@vaadin/hilla-react-signals 24.5.0-beta2 → 24.5.0-beta4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"FullStackSignal.d.ts","sourceRoot":"","sources":["src/FullStackSignal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE1E,OAAO,EAAoB,MAAM,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAInE;;;;;GAKG;AACH,8BAAsB,wBAAwB,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC;;IAQjE,SAAS,aAAa,KAAK,EAAE,CAAC,GAAG,SAAS,EAAE,gBAAgB,EAAE,MAAM,IAAI,EAAE,iBAAiB,EAAE,MAAM,IAAI;cAMpF,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;cAQtB,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;CAO1C;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C;;OAEG;IACH,MAAM,EAAE,aAAa,CAAC;IAEtB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH;;GAEG;AACH,cAAM,gBAAgB,CAAC,CAAC;;gBAKV,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAKtD,IAAI,YAAY,4CAEf;IAED,OAAO;IAaD,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjD,UAAU;CAIX;AAED,eAAO,MAAM,OAAO,eAAmB,CAAC;AACxC,eAAO,MAAM,sBAAsB,eAAkC,CAAC;AAEtE;;;;;;;GAOG;AACH,8BAAsB,eAAe,CAAC,CAAC,CAAE,SAAQ,wBAAwB,CAAC,CAAC,CAAC;;IAC1E;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAY;IAEvB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAErC;;OAEG;IACH,QAAQ,CAAC,OAAO,yDAAuC;IAEvD;;OAEG;IACH,QAAQ,CAAC,KAAK,mEAAqC;gBASvC,KAAK,EAAE,CAAC,GAAG,SAAS,EAAE,MAAM,EAAE,sBAAsB;IAmBhE;;;;OAIG;IACH,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAMvC;;;;OAIG;IACH,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IAW/C;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;CAgBxE"}
1
+ {"version":3,"file":"FullStackSignal.d.ts","sourceRoot":"","sources":["src/FullStackSignal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,aAAa,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEpG,OAAO,EAAoB,MAAM,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAInE;;;;;GAKG;AACH,8BAAsB,wBAAwB,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC;;IAQjE,SAAS,aAAa,KAAK,EAAE,CAAC,GAAG,SAAS,EAAE,gBAAgB,EAAE,MAAM,IAAI,EAAE,iBAAiB,EAAE,MAAM,IAAI;cAMpF,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;cAQtB,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;CAO1C;AAED;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C;;OAEG;IACH,MAAM,EAAE,aAAa,CAAC;IAEtB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CAAC,CAAC;AAEH;;GAEG;AACH,cAAM,gBAAgB,CAAC,CAAC;;gBAKV,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAKtD,IAAI,YAAY,4CAEf;IAED,OAAO;IAaD,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjD,UAAU;CAIX;AAED,eAAO,MAAM,OAAO,eAAmB,CAAC;AACxC,eAAO,MAAM,sBAAsB,eAAkC,CAAC;AAEtE;;;;;;;GAOG;AACH,8BAAsB,eAAe,CAAC,CAAC,CAAE,SAAQ,wBAAwB,CAAC,CAAC,CAAC;;IAC1E;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAY;IAEvB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAErC;;OAEG;IACH,QAAQ,CAAC,OAAO,yDAAuC;IAEvD;;OAEG;IACH,QAAQ,CAAC,KAAK,mEAAqC;gBASvC,KAAK,EAAE,CAAC,GAAG,SAAS,EAAE,MAAM,EAAE,sBAAsB;IAmBhE;;;;OAIG;IACH,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAMvC;;;;OAIG;IACH,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;IAW/C;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;CAmBxE"}
@@ -124,7 +124,7 @@ class FullStackSignal extends DependencyTrackingSignal {
124
124
  });
125
125
  }
126
126
  #connect() {
127
- this.server.connect().onNext((event) => {
127
+ this.server.connect().onSubscriptionLost(() => "resubscribe").onNext((event) => {
128
128
  this.#paused = true;
129
129
  this[$processServerResponse](event);
130
130
  this.#paused = false;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/FullStackSignal.ts"],
4
- "sourcesContent": ["import type { ConnectClient, Subscription } from '@vaadin/hilla-frontend';\nimport { nanoid } from 'nanoid';\nimport { computed, signal, Signal } from './core.js';\nimport { createSetStateEvent, type StateEvent } from './events.js';\n\nconst ENDPOINT = 'SignalsHandler';\n\n/**\n * An abstraction of a signal that tracks the number of subscribers, and calls\n * the provided `onSubscribe` and `onUnsubscribe` callbacks for the first\n * subscription and the last unsubscription, respectively.\n * @internal\n */\nexport abstract class DependencyTrackingSignal<T> extends Signal<T> {\n readonly #onFirstSubscribe: () => void;\n readonly #onLastUnsubscribe: () => void;\n\n // -1 means to ignore the first subscription that is created internally in the\n // FullStackSignal constructor.\n #subscribeCount = -1;\n\n protected constructor(value: T | undefined, onFirstSubscribe: () => void, onLastUnsubscribe: () => void) {\n super(value);\n this.#onFirstSubscribe = onFirstSubscribe;\n this.#onLastUnsubscribe = onLastUnsubscribe;\n }\n\n protected override S(node: unknown): void {\n super.S(node);\n if (this.#subscribeCount === 0) {\n this.#onFirstSubscribe();\n }\n this.#subscribeCount += 1;\n }\n\n protected override U(node: unknown): void {\n super.U(node);\n this.#subscribeCount -= 1;\n if (this.#subscribeCount === 0) {\n this.#onLastUnsubscribe();\n }\n }\n}\n\n/**\n * An object that describes a data object to connect to the signal provider\n * service.\n */\nexport type ServerConnectionConfig = Readonly<{\n /**\n * The client instance to be used for communication.\n */\n client: ConnectClient;\n\n /**\n * The name of the signal provider service endpoint.\n */\n endpoint: string;\n\n /**\n * The name of the signal provider service method.\n */\n method: string;\n\n /**\n * Optional object with method call arguments to be sent to the endpoint\n * method that provides the signal when subscribing to it.\n */\n params?: Record<string, unknown>;\n}>;\n\n/**\n * A server connection manager.\n */\nclass ServerConnection<T> {\n readonly #id: string;\n readonly #config: ServerConnectionConfig;\n #subscription?: Subscription<StateEvent<T>>;\n\n constructor(id: string, config: ServerConnectionConfig) {\n this.#config = config;\n this.#id = id;\n }\n\n get subscription() {\n return this.#subscription;\n }\n\n connect() {\n const { client, endpoint, method, params } = this.#config;\n\n this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {\n providerEndpoint: endpoint,\n providerMethod: method,\n clientSignalId: this.#id,\n params,\n });\n\n return this.#subscription;\n }\n\n async update(event: StateEvent<T>): Promise<void> {\n await this.#config.client.call(ENDPOINT, 'update', {\n clientSignalId: this.#id,\n event,\n });\n }\n\n disconnect() {\n this.#subscription?.cancel();\n this.#subscription = undefined;\n }\n}\n\nexport const $update = Symbol('update');\nexport const $processServerResponse = Symbol('processServerResponse');\n\n/**\n * A signal that holds a shared value. Each change to the value is propagated to\n * the server-side signal provider. At the same time, each change received from\n * the server-side signal provider is propagated to the local signal and it's\n * subscribers.\n *\n * @internal\n */\nexport abstract class FullStackSignal<T> extends DependencyTrackingSignal<T> {\n /**\n * The unique identifier of the signal necessary to communicate with the\n * server.\n */\n readonly id = nanoid();\n\n /**\n * The server connection manager.\n */\n readonly server: ServerConnection<T>;\n\n /**\n * Defines whether the signal is currently awaits a server-side response.\n */\n readonly pending = computed(() => this.#pending.value);\n\n /**\n * Defines whether the signal has an error.\n */\n readonly error = computed(() => this.#error.value);\n\n readonly #pending = signal(false);\n readonly #error = signal<Error | undefined>(undefined);\n\n // Paused at the very start to prevent the signal from sending the initial\n // value to the server.\n #paused = true;\n\n constructor(value: T | undefined, config: ServerConnectionConfig) {\n super(\n value,\n () => this.#connect(),\n () => this.#disconnect(),\n );\n this.server = new ServerConnection(this.id, config);\n\n this.subscribe((v) => {\n if (!this.#paused) {\n this.#pending.value = true;\n this.#error.value = undefined;\n this[$update](createSetStateEvent(v));\n }\n });\n\n this.#paused = false;\n }\n\n /**\n * Sets the local value of the signal without sending any events to the server\n * @param value - The new value.\n * @internal\n */\n protected setValueLocal(value: T): void {\n this.#paused = true;\n this.value = value;\n this.#paused = false;\n }\n\n /**\n * A method to update the server with the new value.\n *\n * @param event - The event to update the server with.\n */\n protected [$update](event: StateEvent<T>): void {\n this.server\n .update(event)\n .catch((error: unknown) => {\n this.#error.value = error instanceof Error ? error : new Error(String(error));\n })\n .finally(() => {\n this.#pending.value = false;\n });\n }\n\n /**\n * A method with to process the server response. The implementation is\n * specific for each signal type.\n *\n * @param event - The server response event.\n */\n protected abstract [$processServerResponse](event: StateEvent<T>): void;\n\n #connect() {\n this.server.connect().onNext((event: StateEvent<T>) => {\n this.#paused = true;\n this[$processServerResponse](event);\n this.#paused = false;\n });\n }\n\n #disconnect() {\n if (this.server.subscription === undefined) {\n return;\n }\n this.server.disconnect();\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ActionOnLostSubscription, ConnectClient, Subscription } from '@vaadin/hilla-frontend';\nimport { nanoid } from 'nanoid';\nimport { computed, signal, Signal } from './core.js';\nimport { createSetStateEvent, type StateEvent } from './events.js';\n\nconst ENDPOINT = 'SignalsHandler';\n\n/**\n * An abstraction of a signal that tracks the number of subscribers, and calls\n * the provided `onSubscribe` and `onUnsubscribe` callbacks for the first\n * subscription and the last unsubscription, respectively.\n * @internal\n */\nexport abstract class DependencyTrackingSignal<T> extends Signal<T> {\n readonly #onFirstSubscribe: () => void;\n readonly #onLastUnsubscribe: () => void;\n\n // -1 means to ignore the first subscription that is created internally in the\n // FullStackSignal constructor.\n #subscribeCount = -1;\n\n protected constructor(value: T | undefined, onFirstSubscribe: () => void, onLastUnsubscribe: () => void) {\n super(value);\n this.#onFirstSubscribe = onFirstSubscribe;\n this.#onLastUnsubscribe = onLastUnsubscribe;\n }\n\n protected override S(node: unknown): void {\n super.S(node);\n if (this.#subscribeCount === 0) {\n this.#onFirstSubscribe();\n }\n this.#subscribeCount += 1;\n }\n\n protected override U(node: unknown): void {\n super.U(node);\n this.#subscribeCount -= 1;\n if (this.#subscribeCount === 0) {\n this.#onLastUnsubscribe();\n }\n }\n}\n\n/**\n * An object that describes a data object to connect to the signal provider\n * service.\n */\nexport type ServerConnectionConfig = Readonly<{\n /**\n * The client instance to be used for communication.\n */\n client: ConnectClient;\n\n /**\n * The name of the signal provider service endpoint.\n */\n endpoint: string;\n\n /**\n * The name of the signal provider service method.\n */\n method: string;\n\n /**\n * Optional object with method call arguments to be sent to the endpoint\n * method that provides the signal when subscribing to it.\n */\n params?: Record<string, unknown>;\n}>;\n\n/**\n * A server connection manager.\n */\nclass ServerConnection<T> {\n readonly #id: string;\n readonly #config: ServerConnectionConfig;\n #subscription?: Subscription<StateEvent<T>>;\n\n constructor(id: string, config: ServerConnectionConfig) {\n this.#config = config;\n this.#id = id;\n }\n\n get subscription() {\n return this.#subscription;\n }\n\n connect() {\n const { client, endpoint, method, params } = this.#config;\n\n this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {\n providerEndpoint: endpoint,\n providerMethod: method,\n clientSignalId: this.#id,\n params,\n });\n\n return this.#subscription;\n }\n\n async update(event: StateEvent<T>): Promise<void> {\n await this.#config.client.call(ENDPOINT, 'update', {\n clientSignalId: this.#id,\n event,\n });\n }\n\n disconnect() {\n this.#subscription?.cancel();\n this.#subscription = undefined;\n }\n}\n\nexport const $update = Symbol('update');\nexport const $processServerResponse = Symbol('processServerResponse');\n\n/**\n * A signal that holds a shared value. Each change to the value is propagated to\n * the server-side signal provider. At the same time, each change received from\n * the server-side signal provider is propagated to the local signal and it's\n * subscribers.\n *\n * @internal\n */\nexport abstract class FullStackSignal<T> extends DependencyTrackingSignal<T> {\n /**\n * The unique identifier of the signal necessary to communicate with the\n * server.\n */\n readonly id = nanoid();\n\n /**\n * The server connection manager.\n */\n readonly server: ServerConnection<T>;\n\n /**\n * Defines whether the signal is currently awaits a server-side response.\n */\n readonly pending = computed(() => this.#pending.value);\n\n /**\n * Defines whether the signal has an error.\n */\n readonly error = computed(() => this.#error.value);\n\n readonly #pending = signal(false);\n readonly #error = signal<Error | undefined>(undefined);\n\n // Paused at the very start to prevent the signal from sending the initial\n // value to the server.\n #paused = true;\n\n constructor(value: T | undefined, config: ServerConnectionConfig) {\n super(\n value,\n () => this.#connect(),\n () => this.#disconnect(),\n );\n this.server = new ServerConnection(this.id, config);\n\n this.subscribe((v) => {\n if (!this.#paused) {\n this.#pending.value = true;\n this.#error.value = undefined;\n this[$update](createSetStateEvent(v));\n }\n });\n\n this.#paused = false;\n }\n\n /**\n * Sets the local value of the signal without sending any events to the server\n * @param value - The new value.\n * @internal\n */\n protected setValueLocal(value: T): void {\n this.#paused = true;\n this.value = value;\n this.#paused = false;\n }\n\n /**\n * A method to update the server with the new value.\n *\n * @param event - The event to update the server with.\n */\n protected [$update](event: StateEvent<T>): void {\n this.server\n .update(event)\n .catch((error: unknown) => {\n this.#error.value = error instanceof Error ? error : new Error(String(error));\n })\n .finally(() => {\n this.#pending.value = false;\n });\n }\n\n /**\n * A method with to process the server response. The implementation is\n * specific for each signal type.\n *\n * @param event - The server response event.\n */\n protected abstract [$processServerResponse](event: StateEvent<T>): void;\n\n #connect() {\n this.server\n .connect()\n .onSubscriptionLost(() => 'resubscribe' as ActionOnLostSubscription)\n .onNext((event: StateEvent<T>) => {\n this.#paused = true;\n this[$processServerResponse](event);\n this.#paused = false;\n });\n }\n\n #disconnect() {\n if (this.server.subscription === undefined) {\n return;\n }\n this.server.disconnect();\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/NumberSignal.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { type StateEvent } from './events.js';
2
+ import { $processServerResponse } from './FullStackSignal.js';
1
3
  import { ValueSignal } from './ValueSignal.js';
2
4
  /**
3
5
  * A signal that holds a number value. The underlying
@@ -23,6 +25,7 @@ import { ValueSignal } from './ValueSignal.js';
23
25
  * ```
24
26
  */
25
27
  export declare class NumberSignal extends ValueSignal<number> {
28
+ #private;
26
29
  /**
27
30
  * Increments the value by the specified delta. The delta can be negative to
28
31
  * decrease the value.
@@ -36,5 +39,6 @@ export declare class NumberSignal extends ValueSignal<number> {
36
39
  * negative.
37
40
  */
38
41
  incrementBy(delta: number): void;
42
+ protected [$processServerResponse](event: StateEvent<number>): void;
39
43
  }
40
44
  //# sourceMappingURL=NumberSignal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NumberSignal.d.ts","sourceRoot":"","sources":["src/NumberSignal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,YAAa,SAAQ,WAAW,CAAC,MAAM,CAAC;IACnD;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAQjC"}
1
+ {"version":3,"file":"NumberSignal.d.ts","sourceRoot":"","sources":["src/NumberSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAW,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,YAAa,SAAQ,WAAW,CAAC,MAAM,CAAC;;IAEnD;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;cAUb,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,GAAG,IAAI;CAW7E"}
package/NumberSignal.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createIncrementStateEvent } from "./events.js";
2
- import { $update } from "./FullStackSignal.js";
2
+ import { $processServerResponse, $update } from "./FullStackSignal.js";
3
3
  import { ValueSignal } from "./ValueSignal.js";
4
4
  class NumberSignal extends ValueSignal {
5
+ #sentIncrementEvents = /* @__PURE__ */ new Map();
5
6
  /**
6
7
  * Increments the value by the specified delta. The delta can be negative to
7
8
  * decrease the value.
@@ -20,8 +21,20 @@ class NumberSignal extends ValueSignal {
20
21
  }
21
22
  this.setValueLocal(this.value + delta);
22
23
  const event = createIncrementStateEvent(delta);
24
+ this.#sentIncrementEvents.set(event.id, event);
23
25
  this[$update](event);
24
26
  }
27
+ [$processServerResponse](event) {
28
+ if (event.accepted && event.type === "increment") {
29
+ if (this.#sentIncrementEvents.has(event.id)) {
30
+ this.#sentIncrementEvents.delete(event.id);
31
+ return;
32
+ }
33
+ this.setValueLocal(this.value + event.value);
34
+ } else {
35
+ super[$processServerResponse](event);
36
+ }
37
+ }
25
38
  }
26
39
  export {
27
40
  NumberSignal
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/NumberSignal.ts"],
4
- "sourcesContent": ["import { createIncrementStateEvent } from './events.js';\nimport { $update } from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\n/**\n * A signal that holds a number value. The underlying\n * value of this signal is stored and updated as a\n * shared value on the server.\n *\n * After obtaining the NumberSignal instance from\n * a server-side service that returns one, the value\n * can be updated using the `value` property,\n * and it can be read with or without the\n * `value` property (similar to a normal signal):\n *\n * @example\n * ```tsx\n * const counter = CounterService.counter();\n *\n * return (\n * <Button onClick={() => counter.incrementBy(1)}>\n * Click count: { counter }\n * </Button>\n * <Button onClick={() => counter.value = 0}>Reset</Button>\n * );\n * ```\n */\nexport class NumberSignal extends ValueSignal<number> {\n /**\n * Increments the value by the specified delta. The delta can be negative to\n * decrease the value.\n *\n * This method differs from using the `++` or `+=` operators directly on the\n * signal value. It performs an atomic operation to prevent conflicts from\n * concurrent changes, ensuring that other users' modifications are not\n * accidentally overwritten.\n *\n * @param delta - The delta to increment the value by. The delta can be\n * negative.\n */\n incrementBy(delta: number): void {\n if (delta === 0) {\n return;\n }\n this.setValueLocal(this.value + delta);\n const event = createIncrementStateEvent(delta);\n this[$update](event);\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,iCAAiC;AAC1C,SAAS,eAAe;AACxB,SAAS,mBAAmB;AAyBrB,MAAM,qBAAqB,YAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAapD,YAAY,OAAqB;AAC/B,QAAI,UAAU,GAAG;AACf;AAAA,IACF;AACA,SAAK,cAAc,KAAK,QAAQ,KAAK;AACrC,UAAM,QAAQ,0BAA0B,KAAK;AAC7C,SAAK,OAAO,EAAE,KAAK;AAAA,EACrB;AACF;",
4
+ "sourcesContent": ["import { createIncrementStateEvent, type StateEvent } from './events.js';\nimport { $processServerResponse, $update } from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\n/**\n * A signal that holds a number value. The underlying\n * value of this signal is stored and updated as a\n * shared value on the server.\n *\n * After obtaining the NumberSignal instance from\n * a server-side service that returns one, the value\n * can be updated using the `value` property,\n * and it can be read with or without the\n * `value` property (similar to a normal signal):\n *\n * @example\n * ```tsx\n * const counter = CounterService.counter();\n *\n * return (\n * <Button onClick={() => counter.incrementBy(1)}>\n * Click count: { counter }\n * </Button>\n * <Button onClick={() => counter.value = 0}>Reset</Button>\n * );\n * ```\n */\nexport class NumberSignal extends ValueSignal<number> {\n readonly #sentIncrementEvents = new Map<string, StateEvent<number>>();\n /**\n * Increments the value by the specified delta. The delta can be negative to\n * decrease the value.\n *\n * This method differs from using the `++` or `+=` operators directly on the\n * signal value. It performs an atomic operation to prevent conflicts from\n * concurrent changes, ensuring that other users' modifications are not\n * accidentally overwritten.\n *\n * @param delta - The delta to increment the value by. The delta can be\n * negative.\n */\n incrementBy(delta: number): void {\n if (delta === 0) {\n return;\n }\n this.setValueLocal(this.value + delta);\n const event = createIncrementStateEvent(delta);\n this.#sentIncrementEvents.set(event.id, event);\n this[$update](event);\n }\n\n protected override [$processServerResponse](event: StateEvent<number>): void {\n if (event.accepted && event.type === 'increment') {\n if (this.#sentIncrementEvents.has(event.id)) {\n this.#sentIncrementEvents.delete(event.id);\n return;\n }\n this.setValueLocal(this.value + event.value);\n } else {\n super[$processServerResponse](event);\n }\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iCAAkD;AAC3D,SAAS,wBAAwB,eAAe;AAChD,SAAS,mBAAmB;AAyBrB,MAAM,qBAAqB,YAAoB;AAAA,EAC3C,uBAAuB,oBAAI,IAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAapE,YAAY,OAAqB;AAC/B,QAAI,UAAU,GAAG;AACf;AAAA,IACF;AACA,SAAK,cAAc,KAAK,QAAQ,KAAK;AACrC,UAAM,QAAQ,0BAA0B,KAAK;AAC7C,SAAK,qBAAqB,IAAI,MAAM,IAAI,KAAK;AAC7C,SAAK,OAAO,EAAE,KAAK;AAAA,EACrB;AAAA,EAEA,CAAoB,sBAAsB,EAAE,OAAiC;AAC3E,QAAI,MAAM,YAAY,MAAM,SAAS,aAAa;AAChD,UAAI,KAAK,qBAAqB,IAAI,MAAM,EAAE,GAAG;AAC3C,aAAK,qBAAqB,OAAO,MAAM,EAAE;AACzC;AAAA,MACF;AACA,WAAK,cAAc,KAAK,QAAQ,MAAM,KAAK;AAAA,IAC7C,OAAO;AACL,YAAM,sBAAsB,EAAE,KAAK;AAAA,IACrC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ValueSignal.d.ts","sourceRoot":"","sources":["src/ValueSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAW,eAAe,EAAE,MAAM,sBAAsB,CAAC;AASxF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,IAAI,IAAI,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,WAAW,CAAC,CAAC,CAAE,SAAQ,eAAe,CAAC,CAAC,CAAC;;IAGpD;;;;;;;;;OASG;IACH,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAInB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI;IAIvC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,qBAAqB;cAerC,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;CAoBxE"}
1
+ {"version":3,"file":"ValueSignal.d.ts","sourceRoot":"","sources":["src/ValueSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,sBAAsB,EAAW,eAAe,EAAE,MAAM,sBAAsB,CAAC;AASxF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,MAAM,IAAI,IAAI,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,WAAW,CAAC,CAAC,CAAE,SAAQ,eAAe,CAAC,CAAC,CAAC;;IAGpD;;;;;;;;;OASG;IACH,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAInB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI;IAIvC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,qBAAqB;cAerC,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI;CA8BxE"}
package/ValueSignal.js CHANGED
@@ -57,15 +57,24 @@ class ValueSignal extends FullStackSignal {
57
57
  if (record) {
58
58
  this.#pendingRequests.delete(event.id);
59
59
  }
60
- if (event.type === "snapshot") {
60
+ if (!event.accepted && record) {
61
+ if (!record.canceled) {
62
+ this.update(record.callback);
63
+ }
64
+ }
65
+ if (event.accepted || event.type === "snapshot") {
61
66
  if (record) {
62
67
  record.waiter.resolve();
63
68
  }
64
- this.value = event.value;
69
+ this.#applyAcceptedEvent(event);
65
70
  }
66
- if (event.type === "reject" && record) {
67
- if (!record.canceled) {
68
- this.update(record.callback);
71
+ }
72
+ #applyAcceptedEvent(event) {
73
+ if (event.type === "set" || event.type === "snapshot") {
74
+ this.value = event.value;
75
+ } else if (event.type === "replace") {
76
+ if (JSON.stringify(this.value) === JSON.stringify(event.expected)) {
77
+ this.value = event.value;
69
78
  }
70
79
  }
71
80
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/ValueSignal.ts"],
4
- "sourcesContent": ["import { createReplaceStateEvent, type StateEvent } from './events.js';\nimport { $processServerResponse, $update, FullStackSignal } from './FullStackSignal.js';\n\ntype PromiseWithResolvers = ReturnType<typeof Promise.withResolvers<void>>;\ntype PendingRequestsRecord<T> = Readonly<{\n waiter: PromiseWithResolvers;\n callback(value: T): T;\n canceled: boolean;\n}>;\n\n/**\n * An operation subscription that can be canceled.\n */\nexport interface OperationSubscription {\n cancel(): void;\n}\n\n/**\n * A full-stack signal that holds an arbitrary value.\n */\nexport class ValueSignal<T> extends FullStackSignal<T> {\n readonly #pendingRequests = new Map<string, PendingRequestsRecord<T>>();\n\n /**\n * Sets the value.\n * Note that the value change event that is propagated to the server as the\n * result of this operation is not taking the last seen value into account and\n * will overwrite the shared value on the server unconditionally (AKA: \"Last\n * Write Wins\"). If you need to perform a conditional update, use the\n * `replace` method instead.\n *\n * @param value - The new value.\n */\n set(value: T): void {\n this.value = value;\n }\n\n /**\n * Replaces the value with a new one only if the current value is equal to the\n * expected value.\n *\n * @param expected - The expected value.\n * @param newValue - The new value.\n */\n replace(expected: T, newValue: T): void {\n this[$update](createReplaceStateEvent(expected, newValue));\n }\n\n /**\n * Tries to update the value by applying the callback function to the current\n * value. In case of a concurrent change, the callback is run again with an\n * updated input value. This is repeated until the result can be applied\n * without concurrent changes, or the operation is canceled.\n *\n * Note that there is no guarantee that cancel() will be effective always,\n * since a succeeding operation might already be on its way to the server.\n *\n * @param callback - The function that is applied on the current value to\n * produce the new value.\n * @returns An operation subscription that can be canceled.\n */\n update(callback: (value: T) => T): OperationSubscription {\n const newValue = callback(this.value);\n const event = createReplaceStateEvent(this.value, newValue);\n this[$update](event);\n const waiter = Promise.withResolvers<void>();\n const pendingRequest = { callback, waiter, canceled: false };\n this.#pendingRequests.set(event.id, pendingRequest);\n return {\n cancel: () => {\n pendingRequest.canceled = true;\n pendingRequest.waiter.resolve();\n },\n };\n }\n\n protected override [$processServerResponse](event: StateEvent<T>): void {\n const record = this.#pendingRequests.get(event.id);\n if (record) {\n this.#pendingRequests.delete(event.id);\n }\n\n if (event.type === 'snapshot') {\n if (record) {\n record.waiter.resolve();\n }\n this.value = event.value;\n }\n\n if (event.type === 'reject' && record) {\n if (!record.canceled) {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.update(record.callback);\n }\n }\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,+BAAgD;AACzD,SAAS,wBAAwB,SAAS,uBAAuB;AAmB1D,MAAM,oBAAuB,gBAAmB;AAAA,EAC5C,mBAAmB,oBAAI,IAAsC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYtE,IAAI,OAAgB;AAClB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,UAAa,UAAmB;AACtC,SAAK,OAAO,EAAE,wBAAwB,UAAU,QAAQ,CAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,OAAO,UAAkD;AACvD,UAAM,WAAW,SAAS,KAAK,KAAK;AACpC,UAAM,QAAQ,wBAAwB,KAAK,OAAO,QAAQ;AAC1D,SAAK,OAAO,EAAE,KAAK;AACnB,UAAM,SAAS,QAAQ,cAAoB;AAC3C,UAAM,iBAAiB,EAAE,UAAU,QAAQ,UAAU,MAAM;AAC3D,SAAK,iBAAiB,IAAI,MAAM,IAAI,cAAc;AAClD,WAAO;AAAA,MACL,QAAQ,MAAM;AACZ,uBAAe,WAAW;AAC1B,uBAAe,OAAO,QAAQ;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,CAAoB,sBAAsB,EAAE,OAA4B;AACtE,UAAM,SAAS,KAAK,iBAAiB,IAAI,MAAM,EAAE;AACjD,QAAI,QAAQ;AACV,WAAK,iBAAiB,OAAO,MAAM,EAAE;AAAA,IACvC;AAEA,QAAI,MAAM,SAAS,YAAY;AAC7B,UAAI,QAAQ;AACV,eAAO,OAAO,QAAQ;AAAA,MACxB;AACA,WAAK,QAAQ,MAAM;AAAA,IACrB;AAEA,QAAI,MAAM,SAAS,YAAY,QAAQ;AACrC,UAAI,CAAC,OAAO,UAAU;AAEpB,aAAK,OAAO,OAAO,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { createReplaceStateEvent, type StateEvent } from './events.js';\nimport { $processServerResponse, $update, FullStackSignal } from './FullStackSignal.js';\n\ntype PromiseWithResolvers = ReturnType<typeof Promise.withResolvers<void>>;\ntype PendingRequestsRecord<T> = Readonly<{\n waiter: PromiseWithResolvers;\n callback(value: T): T;\n canceled: boolean;\n}>;\n\n/**\n * An operation subscription that can be canceled.\n */\nexport interface OperationSubscription {\n cancel(): void;\n}\n\n/**\n * A full-stack signal that holds an arbitrary value.\n */\nexport class ValueSignal<T> extends FullStackSignal<T> {\n readonly #pendingRequests = new Map<string, PendingRequestsRecord<T>>();\n\n /**\n * Sets the value.\n * Note that the value change event that is propagated to the server as the\n * result of this operation is not taking the last seen value into account and\n * will overwrite the shared value on the server unconditionally (AKA: \"Last\n * Write Wins\"). If you need to perform a conditional update, use the\n * `replace` method instead.\n *\n * @param value - The new value.\n */\n set(value: T): void {\n this.value = value;\n }\n\n /**\n * Replaces the value with a new one only if the current value is equal to the\n * expected value.\n *\n * @param expected - The expected value.\n * @param newValue - The new value.\n */\n replace(expected: T, newValue: T): void {\n this[$update](createReplaceStateEvent(expected, newValue));\n }\n\n /**\n * Tries to update the value by applying the callback function to the current\n * value. In case of a concurrent change, the callback is run again with an\n * updated input value. This is repeated until the result can be applied\n * without concurrent changes, or the operation is canceled.\n *\n * Note that there is no guarantee that cancel() will be effective always,\n * since a succeeding operation might already be on its way to the server.\n *\n * @param callback - The function that is applied on the current value to\n * produce the new value.\n * @returns An operation subscription that can be canceled.\n */\n update(callback: (value: T) => T): OperationSubscription {\n const newValue = callback(this.value);\n const event = createReplaceStateEvent(this.value, newValue);\n this[$update](event);\n const waiter = Promise.withResolvers<void>();\n const pendingRequest = { callback, waiter, canceled: false };\n this.#pendingRequests.set(event.id, pendingRequest);\n return {\n cancel: () => {\n pendingRequest.canceled = true;\n pendingRequest.waiter.resolve();\n },\n };\n }\n\n protected override [$processServerResponse](event: StateEvent<T>): void {\n const record = this.#pendingRequests.get(event.id);\n if (record) {\n this.#pendingRequests.delete(event.id);\n }\n\n if (!event.accepted && record) {\n if (!record.canceled) {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.update(record.callback);\n }\n }\n\n if (event.accepted || event.type === 'snapshot') {\n if (record) {\n record.waiter.resolve();\n }\n this.#applyAcceptedEvent(event);\n }\n }\n\n #applyAcceptedEvent(event: StateEvent<T>): void {\n if (event.type === 'set' || event.type === 'snapshot') {\n this.value = event.value;\n } else if (event.type === 'replace') {\n if (JSON.stringify(this.value) === JSON.stringify(event.expected)) {\n this.value = event.value;\n }\n }\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,+BAAgD;AACzD,SAAS,wBAAwB,SAAS,uBAAuB;AAmB1D,MAAM,oBAAuB,gBAAmB;AAAA,EAC5C,mBAAmB,oBAAI,IAAsC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYtE,IAAI,OAAgB;AAClB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,QAAQ,UAAa,UAAmB;AACtC,SAAK,OAAO,EAAE,wBAAwB,UAAU,QAAQ,CAAC;AAAA,EAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,OAAO,UAAkD;AACvD,UAAM,WAAW,SAAS,KAAK,KAAK;AACpC,UAAM,QAAQ,wBAAwB,KAAK,OAAO,QAAQ;AAC1D,SAAK,OAAO,EAAE,KAAK;AACnB,UAAM,SAAS,QAAQ,cAAoB;AAC3C,UAAM,iBAAiB,EAAE,UAAU,QAAQ,UAAU,MAAM;AAC3D,SAAK,iBAAiB,IAAI,MAAM,IAAI,cAAc;AAClD,WAAO;AAAA,MACL,QAAQ,MAAM;AACZ,uBAAe,WAAW;AAC1B,uBAAe,OAAO,QAAQ;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,CAAoB,sBAAsB,EAAE,OAA4B;AACtE,UAAM,SAAS,KAAK,iBAAiB,IAAI,MAAM,EAAE;AACjD,QAAI,QAAQ;AACV,WAAK,iBAAiB,OAAO,MAAM,EAAE;AAAA,IACvC;AAEA,QAAI,CAAC,MAAM,YAAY,QAAQ;AAC7B,UAAI,CAAC,OAAO,UAAU;AAEpB,aAAK,OAAO,OAAO,QAAQ;AAAA,MAC7B;AAAA,IACF;AAEA,QAAI,MAAM,YAAY,MAAM,SAAS,YAAY;AAC/C,UAAI,QAAQ;AACV,eAAO,OAAO,QAAQ;AAAA,MACxB;AACA,WAAK,oBAAoB,KAAK;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,oBAAoB,OAA4B;AAC9C,QAAI,MAAM,SAAS,SAAS,MAAM,SAAS,YAAY;AACrD,WAAK,QAAQ,MAAM;AAAA,IACrB,WAAW,MAAM,SAAS,WAAW;AACnC,UAAI,KAAK,UAAU,KAAK,KAAK,MAAM,KAAK,UAAU,MAAM,QAAQ,GAAG;AACjE,aAAK,QAAQ,MAAM;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/events.d.ts CHANGED
@@ -6,16 +6,15 @@ type CreateStateEventType<V, T extends string, C extends Record<string, unknown>
6
6
  id: string;
7
7
  type: T;
8
8
  value: V;
9
+ accepted: boolean;
9
10
  }> & Readonly<C>>;
10
11
  /**
11
12
  * A state event received from the server describing the current state of the
12
13
  * signal.
13
14
  */
14
15
  export type SnapshotStateEvent<T> = CreateStateEventType<T, 'snapshot'>;
15
- export type RejectStateEvent = CreateStateEventType<never, 'reject'>;
16
16
  /**
17
17
  * A state event defines a new value of the signal shared with the server. The
18
- *
19
18
  */
20
19
  export type SetStateEvent<T> = CreateStateEventType<T, 'set'>;
21
20
  export declare function createSetStateEvent<T>(value: T): SetStateEvent<T>;
@@ -28,6 +27,6 @@ export declare function createIncrementStateEvent(delta: number): IncrementState
28
27
  /**
29
28
  * An object that describes the change of the signal state.
30
29
  */
31
- export type StateEvent<T> = IncrementStateEvent | RejectStateEvent | ReplaceStateEvent<T> | SetStateEvent<T> | SnapshotStateEvent<T>;
30
+ export type StateEvent<T> = IncrementStateEvent | ReplaceStateEvent<T> | SetStateEvent<T> | SnapshotStateEvent<T>;
32
31
  export {};
33
32
  //# sourceMappingURL=events.d.ts.map
package/events.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["src/events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C;;GAEG;AACH,KAAK,oBAAoB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,QAAQ,CACjH,QAAQ,CAAC;IACP,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,CAAC,CAAC;CACV,CAAC,GACA,QAAQ,CAAC,CAAC,CAAC,CACd,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;AAExE,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAE9D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAMjE;AAED,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,EAAE,SAAS,EAAE;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAEvF,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAOtF;AAED,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAE5E,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAM5E;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IACpB,mBAAmB,GACnB,gBAAgB,GAChB,iBAAiB,CAAC,CAAC,CAAC,GACpB,aAAa,CAAC,CAAC,CAAC,GAChB,kBAAkB,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["src/events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C;;GAEG;AACH,KAAK,oBAAoB,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,QAAQ,CACjH,QAAQ,CAAC;IACP,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC,GACA,QAAQ,CAAC,CAAC,CAAC,CACd,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAE9D,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAOjE;AAED,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,EAAE,SAAS,EAAE;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,CAAC;AAEvF,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAQtF;AAED,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAE5E,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAO5E;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,mBAAmB,GAAG,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC"}
package/events.js CHANGED
@@ -3,7 +3,8 @@ function createSetStateEvent(value) {
3
3
  return {
4
4
  id: nanoid(),
5
5
  type: "set",
6
- value
6
+ value,
7
+ accepted: false
7
8
  };
8
9
  }
9
10
  function createReplaceStateEvent(expected, value) {
@@ -11,14 +12,16 @@ function createReplaceStateEvent(expected, value) {
11
12
  id: nanoid(),
12
13
  type: "replace",
13
14
  value,
14
- expected
15
+ expected,
16
+ accepted: false
15
17
  };
16
18
  }
17
19
  function createIncrementStateEvent(delta) {
18
20
  return {
19
21
  id: nanoid(),
20
22
  type: "increment",
21
- value: delta
23
+ value: delta,
24
+ accepted: false
22
25
  };
23
26
  }
24
27
  export {
package/events.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/events.ts"],
4
- "sourcesContent": ["import { nanoid } from 'nanoid';\nimport type { Simplify } from 'type-fest';\n\n/**\n * Creates a new state event type.\n */\ntype CreateStateEventType<V, T extends string, C extends Record<string, unknown> = Record<never, never>> = Simplify<\n Readonly<{\n id: string;\n type: T;\n value: V;\n }> &\n Readonly<C>\n>;\n\n/**\n * A state event received from the server describing the current state of the\n * signal.\n */\nexport type SnapshotStateEvent<T> = CreateStateEventType<T, 'snapshot'>;\n\nexport type RejectStateEvent = CreateStateEventType<never, 'reject'>;\n\n/**\n * A state event defines a new value of the signal shared with the server. The\n *\n */\nexport type SetStateEvent<T> = CreateStateEventType<T, 'set'>;\n\nexport function createSetStateEvent<T>(value: T): SetStateEvent<T> {\n return {\n id: nanoid(),\n type: 'set',\n value,\n };\n}\n\nexport type ReplaceStateEvent<T> = CreateStateEventType<T, 'replace', { expected: T }>;\n\nexport function createReplaceStateEvent<T>(expected: T, value: T): ReplaceStateEvent<T> {\n return {\n id: nanoid(),\n type: 'replace',\n value,\n expected,\n };\n}\n\nexport type IncrementStateEvent = CreateStateEventType<number, 'increment'>;\n\nexport function createIncrementStateEvent(delta: number): IncrementStateEvent {\n return {\n id: nanoid(),\n type: 'increment',\n value: delta,\n };\n}\n\n/**\n * An object that describes the change of the signal state.\n */\nexport type StateEvent<T> =\n | IncrementStateEvent\n | RejectStateEvent\n | ReplaceStateEvent<T>\n | SetStateEvent<T>\n | SnapshotStateEvent<T>;\n"],
5
- "mappings": "AAAA,SAAS,cAAc;AA6BhB,SAAS,oBAAuB,OAA4B;AACjE,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM;AAAA,IACN;AAAA,EACF;AACF;AAIO,SAAS,wBAA2B,UAAa,OAAgC;AACtF,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA;AAAA,EACF;AACF;AAIO,SAAS,0BAA0B,OAAoC;AAC5E,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import { nanoid } from 'nanoid';\nimport type { Simplify } from 'type-fest';\n\n/**\n * Creates a new state event type.\n */\ntype CreateStateEventType<V, T extends string, C extends Record<string, unknown> = Record<never, never>> = Simplify<\n Readonly<{\n id: string;\n type: T;\n value: V;\n accepted: boolean;\n }> &\n Readonly<C>\n>;\n\n/**\n * A state event received from the server describing the current state of the\n * signal.\n */\nexport type SnapshotStateEvent<T> = CreateStateEventType<T, 'snapshot'>;\n\n/**\n * A state event defines a new value of the signal shared with the server. The\n */\nexport type SetStateEvent<T> = CreateStateEventType<T, 'set'>;\n\nexport function createSetStateEvent<T>(value: T): SetStateEvent<T> {\n return {\n id: nanoid(),\n type: 'set',\n value,\n accepted: false,\n };\n}\n\nexport type ReplaceStateEvent<T> = CreateStateEventType<T, 'replace', { expected: T }>;\n\nexport function createReplaceStateEvent<T>(expected: T, value: T): ReplaceStateEvent<T> {\n return {\n id: nanoid(),\n type: 'replace',\n value,\n expected,\n accepted: false,\n };\n}\n\nexport type IncrementStateEvent = CreateStateEventType<number, 'increment'>;\n\nexport function createIncrementStateEvent(delta: number): IncrementStateEvent {\n return {\n id: nanoid(),\n type: 'increment',\n value: delta,\n accepted: false,\n };\n}\n\n/**\n * An object that describes the change of the signal state.\n */\nexport type StateEvent<T> = IncrementStateEvent | ReplaceStateEvent<T> | SetStateEvent<T> | SnapshotStateEvent<T>;\n"],
5
+ "mappings": "AAAA,SAAS,cAAc;AA2BhB,SAAS,oBAAuB,OAA4B;AACjE,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAIO,SAAS,wBAA2B,UAAa,OAAgC;AACtF,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAIO,SAAS,0BAA0B,OAAoC;AAC5E,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP,UAAU;AAAA,EACZ;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/hilla-react-signals",
3
- "version": "24.5.0-beta2",
3
+ "version": "24.5.0-beta4",
4
4
  "description": "Signals for Hilla React",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@preact/signals-react": "^2.0.0",
50
- "@vaadin/hilla-frontend": "24.5.0-beta2",
50
+ "@vaadin/hilla-frontend": "24.5.0-beta4",
51
51
  "nanoid": "^5.0.7"
52
52
  },
53
53
  "peerDependencies": {