@vaadin/hilla-react-signals 24.6.0-alpha2 → 24.6.0-alpha4

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,6 +1,12 @@
1
1
  import type { ConnectClient, Subscription } from '@vaadin/hilla-frontend';
2
2
  import { Signal } from './core.js';
3
3
  import { type StateEvent } from './events.js';
4
+ /**
5
+ * A return type for signal operations.
6
+ */
7
+ export type Operation = {
8
+ result: Promise<void>;
9
+ };
4
10
  /**
5
11
  * An abstraction of a signal that tracks the number of subscribers, and calls
6
12
  * the provided `onSubscribe` and `onUnsubscribe` callbacks for the first
@@ -67,6 +73,8 @@ declare class ServerConnection {
67
73
  export declare const $update: unique symbol;
68
74
  export declare const $processServerResponse: unique symbol;
69
75
  export declare const $setValueQuietly: unique symbol;
76
+ export declare const $resolveOperation: unique symbol;
77
+ export declare const $createOperation: unique symbol;
70
78
  /**
71
79
  * A signal that holds a shared value. Each change to the value is propagated to
72
80
  * the server-side signal provider. At the same time, each change received from
@@ -95,6 +103,10 @@ export declare abstract class FullStackSignal<T> extends DependencyTrackingSigna
95
103
  */
96
104
  readonly error: import("@preact/signals-core").ReadonlySignal<Error | undefined>;
97
105
  constructor(value: T | undefined, config: ServerConnectionConfig, id?: string);
106
+ protected [$createOperation]({ id, promise }: {
107
+ id?: string;
108
+ promise?: Promise<void>;
109
+ }): Operation;
98
110
  /**
99
111
  * Sets the local value of the signal without sending any events to the server
100
112
  * @param value - The new value.
@@ -105,8 +117,16 @@ export declare abstract class FullStackSignal<T> extends DependencyTrackingSigna
105
117
  * A method to update the server with the new value.
106
118
  *
107
119
  * @param event - The event to update the server with.
120
+ * @returns The server response promise.
121
+ */
122
+ protected [$update](event: StateEvent): Promise<void>;
123
+ /**
124
+ * Resolves the operation promise associated with the given event id.
125
+ *
126
+ * @param eventId - The event id.
127
+ * @param reason - The reason to reject the promise (if any).
108
128
  */
109
- protected [$update](event: StateEvent): void;
129
+ protected [$resolveOperation](eventId: string, reason?: string): void;
110
130
  /**
111
131
  * A method with to process the server response. The implementation is
112
132
  * specific for each signal type.
@@ -1 +1 @@
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;cAapF,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;IAEjC;;OAEG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC,CAAC;AAEH;;GAEG;AACH,cAAM,gBAAgB;;IAEpB,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;gBAG5B,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAKtD,IAAI,YAAY;;;;;;oBAEf;IAED,OAAO;;;;;;;IAcD,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9C,UAAU;CAIX;AAED,eAAO,MAAM,OAAO,eAAmB,CAAC;AACxC,eAAO,MAAM,sBAAsB,eAAkC,CAAC;AACtE,eAAO,MAAM,gBAAgB,eAA4B,CAAC;AAE1D;;;;;;;GAOG;AACH,8BAAsB,eAAe,CAAC,CAAC,CAAE,SAAQ,wBAAwB,CAAC,CAAC,CAAC;;IAC1E;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAElC;;OAEG;IACH,QAAQ,CAAC,OAAO,yDAAuC;IAEvD;;OAEG;IACH,QAAQ,CAAC,KAAK,mEAAqC;gBASvC,KAAK,EAAE,CAAC,GAAG,SAAS,EAAE,MAAM,EAAE,sBAAsB,EAAE,EAAE,CAAC,EAAE,MAAM;IA2B7E;;;;OAIG;IACH,SAAS,CAAC,CAAC,gBAAgB,CAAC,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAM5C;;;;OAIG;IACH,SAAS,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAW5C;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAmBrE"}
1
+ {"version":3,"file":"FullStackSignal.d.ts","sourceRoot":"","sources":["src/FullStackSignal.ts"],"names":[],"mappings":"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"}
@@ -56,10 +56,17 @@ class ServerConnection {
56
56
  return this.#subscription;
57
57
  }
58
58
  async update(event) {
59
+ const onTheFly = !this.#subscription;
60
+ if (onTheFly) {
61
+ this.connect();
62
+ }
59
63
  await this.config.client.call(ENDPOINT, "update", {
60
64
  clientSignalId: this.#id,
61
65
  event
62
66
  });
67
+ if (onTheFly) {
68
+ this.disconnect();
69
+ }
63
70
  }
64
71
  disconnect() {
65
72
  this.#subscription?.cancel();
@@ -69,6 +76,8 @@ class ServerConnection {
69
76
  const $update = Symbol("update");
70
77
  const $processServerResponse = Symbol("processServerResponse");
71
78
  const $setValueQuietly = Symbol("setValueQuietly");
79
+ const $resolveOperation = Symbol("resolveOperation");
80
+ const $createOperation = Symbol("createOperation");
72
81
  class FullStackSignal extends DependencyTrackingSignal {
73
82
  /**
74
83
  * The unique identifier of the signal necessary to communicate with the
@@ -110,6 +119,35 @@ class FullStackSignal extends DependencyTrackingSignal {
110
119
  });
111
120
  this.#paused = false;
112
121
  }
122
+ // stores the promise handlers associated to operations
123
+ #operationPromises = /* @__PURE__ */ new Map();
124
+ // creates the obejct to be returned by operations to allow defining callbacks
125
+ [$createOperation]({ id, promise }) {
126
+ const thens = this.#operationPromises;
127
+ const promises = [];
128
+ if (promise) {
129
+ promises.push(promise);
130
+ }
131
+ if (id) {
132
+ promises.push(
133
+ new Promise((resolve, reject) => {
134
+ thens.set(id, { resolve, reject });
135
+ })
136
+ );
137
+ }
138
+ if (promises.length === 0) {
139
+ promises.push(Promise.resolve());
140
+ }
141
+ return {
142
+ result: Promise.allSettled(promises).then((results) => {
143
+ const lastResult = results[results.length - 1];
144
+ if (lastResult.status === "fulfilled") {
145
+ return void 0;
146
+ }
147
+ throw lastResult.reason;
148
+ })
149
+ };
150
+ }
113
151
  /**
114
152
  * Sets the local value of the signal without sending any events to the server
115
153
  * @param value - The new value.
@@ -124,14 +162,32 @@ class FullStackSignal extends DependencyTrackingSignal {
124
162
  * A method to update the server with the new value.
125
163
  *
126
164
  * @param event - The event to update the server with.
165
+ * @returns The server response promise.
127
166
  */
128
- [$update](event) {
129
- this.server.update(event).catch((error) => {
167
+ async [$update](event) {
168
+ return this.server.update(event).catch((error) => {
130
169
  this.#error.value = error instanceof Error ? error : new Error(String(error));
131
170
  }).finally(() => {
132
171
  this.#pending.value = false;
133
172
  });
134
173
  }
174
+ /**
175
+ * Resolves the operation promise associated with the given event id.
176
+ *
177
+ * @param eventId - The event id.
178
+ * @param reason - The reason to reject the promise (if any).
179
+ */
180
+ [$resolveOperation](eventId, reason) {
181
+ const operationPromise = this.#operationPromises.get(eventId);
182
+ if (operationPromise) {
183
+ this.#operationPromises.delete(eventId);
184
+ if (reason) {
185
+ operationPromise.reject(reason);
186
+ } else {
187
+ operationPromise.resolve();
188
+ }
189
+ }
190
+ }
135
191
  #connect() {
136
192
  this.server.connect().onSubscriptionLost(() => "resubscribe").onNext((event) => {
137
193
  this.#paused = true;
@@ -147,7 +203,9 @@ class FullStackSignal extends DependencyTrackingSignal {
147
203
  }
148
204
  }
149
205
  export {
206
+ $createOperation,
150
207
  $processServerResponse,
208
+ $resolveOperation,
151
209
  $setValueQuietly,
152
210
  $update,
153
211
  DependencyTrackingSignal,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/FullStackSignal.ts"],
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 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n if (!(window as any).Vaadin?.featureFlags?.fullstackSignals) {\n // Remove when removing feature flag\n throw new Error(\n `The Hilla Fullstack Signals API is currently considered experimental and may change in the future. To use it you need to explicitly enable it in Copilot or by adding com.vaadin.experimental.fullstackSignals=true to vaadin-featureflags.properties`,\n );\n }\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 * The unique identifier of the parent signal in the client.\n */\n parentClientSignalId?: string;\n}>;\n\n/**\n * A server connection manager.\n */\nclass ServerConnection {\n readonly #id: string;\n readonly config: ServerConnectionConfig;\n #subscription?: Subscription<StateEvent>;\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, parentClientSignalId } = this.config;\n\n this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {\n providerEndpoint: endpoint,\n providerMethod: method,\n clientSignalId: this.#id,\n params,\n parentClientSignalId,\n });\n\n return this.#subscription;\n }\n\n async update(event: StateEvent): 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');\nexport const $setValueQuietly = Symbol('setValueQuietly');\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: string;\n\n /**\n * The server connection manager.\n */\n readonly server: ServerConnection;\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, id?: string) {\n super(\n value,\n () => this.#connect(),\n () => this.#disconnect(),\n );\n this.id = id ?? nanoid();\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 // For internal signals, the provided non-null to the constructor should\n // be used along with the parent client side signal id when sending the\n // set event to the server. For internal signals this combination is\n // needed for addressing the correct parent/child signal instances on\n // the server. For a standalone signal, both of them should be passed in\n // as undefined:\n const signalId = config.parentClientSignalId !== undefined ? this.id : undefined;\n this[$update](createSetStateEvent(v, signalId, config.parentClientSignalId));\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 [$setValueQuietly](value: T): void {\n this.#paused = true;\n super.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): 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): void;\n\n #connect() {\n this.server\n .connect()\n .onSubscriptionLost(() => 'resubscribe' as ActionOnLostSubscription)\n .onNext((event: StateEvent) => {\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 * A return type for signal operations.\n */\nexport type Operation = {\n result: Promise<void>;\n};\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 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n if (!(window as any).Vaadin?.featureFlags?.fullstackSignals) {\n // Remove when removing feature flag\n throw new Error(\n `The Hilla Fullstack Signals API is currently considered experimental and may change in the future. To use it you need to explicitly enable it in Copilot or by adding com.vaadin.experimental.fullstackSignals=true to vaadin-featureflags.properties`,\n );\n }\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 * The unique identifier of the parent signal in the client.\n */\n parentClientSignalId?: string;\n}>;\n\n/**\n * A server connection manager.\n */\nclass ServerConnection {\n readonly #id: string;\n readonly config: ServerConnectionConfig;\n #subscription?: Subscription<StateEvent>;\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, parentClientSignalId } = this.config;\n\n this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {\n providerEndpoint: endpoint,\n providerMethod: method,\n clientSignalId: this.#id,\n params,\n parentClientSignalId,\n });\n\n return this.#subscription;\n }\n\n async update(event: StateEvent): Promise<void> {\n const onTheFly = !this.#subscription;\n\n if (onTheFly) {\n this.connect();\n }\n\n await this.config.client.call(ENDPOINT, 'update', {\n clientSignalId: this.#id,\n event,\n });\n\n if (onTheFly) {\n this.disconnect();\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');\nexport const $setValueQuietly = Symbol('setValueQuietly');\nexport const $resolveOperation = Symbol('resolveOperation');\nexport const $createOperation = Symbol('createOperation');\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: string;\n\n /**\n * The server connection manager.\n */\n readonly server: ServerConnection;\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, id?: string) {\n super(\n value,\n () => this.#connect(),\n () => this.#disconnect(),\n );\n this.id = id ?? nanoid();\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 // For internal signals, the provided non-null to the constructor should\n // be used along with the parent client side signal id when sending the\n // set event to the server. For internal signals this combination is\n // needed for addressing the correct parent/child signal instances on\n // the server. For a standalone signal, both of them should be passed in\n // as undefined:\n const signalId = config.parentClientSignalId !== undefined ? this.id : undefined;\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this[$update](createSetStateEvent(v, signalId, config.parentClientSignalId));\n }\n });\n\n this.#paused = false;\n }\n\n // stores the promise handlers associated to operations\n readonly #operationPromises = new Map<\n string,\n {\n resolve(value: PromiseLike<void> | void): void;\n reject(reason?: any): void;\n }\n >();\n\n // creates the obejct to be returned by operations to allow defining callbacks\n protected [$createOperation]({ id, promise }: { id?: string; promise?: Promise<void> }): Operation {\n const thens = this.#operationPromises;\n const promises: Array<Promise<void>> = [];\n\n if (promise) {\n // Add the provided promise to the list of promises\n promises.push(promise);\n }\n\n if (id) {\n // Create a promise to be associated to the provided id\n promises.push(\n new Promise<void>((resolve, reject) => {\n thens.set(id, { resolve, reject });\n }),\n );\n }\n\n if (promises.length === 0) {\n // If no promises were added, return a resolved promise\n promises.push(Promise.resolve());\n }\n\n return {\n result: Promise.allSettled(promises).then((results) => {\n const lastResult = results[results.length - 1];\n if (lastResult.status === 'fulfilled') {\n return undefined;\n }\n throw lastResult.reason;\n }),\n };\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 [$setValueQuietly](value: T): void {\n this.#paused = true;\n super.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 * @returns The server response promise.\n */\n protected async [$update](event: StateEvent): Promise<void> {\n return 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 * Resolves the operation promise associated with the given event id.\n *\n * @param eventId - The event id.\n * @param reason - The reason to reject the promise (if any).\n */\n protected [$resolveOperation](eventId: string, reason?: string): void {\n const operationPromise = this.#operationPromises.get(eventId);\n if (operationPromise) {\n this.#operationPromises.delete(eventId);\n if (reason) {\n operationPromise.reject(reason);\n } else {\n operationPromise.resolve();\n }\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): void;\n\n #connect() {\n this.server\n .connect()\n .onSubscriptionLost(() => 'resubscribe' as ActionOnLostSubscription)\n .onNext((event: StateEvent) => {\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/ListSignal.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CollectionSignal } from './CollectionSignal.js';
2
2
  import { type StateEvent } from './events.js';
3
- import { $processServerResponse, type ServerConnectionConfig } from './FullStackSignal.js';
3
+ import { $processServerResponse, type Operation, type ServerConnectionConfig } from './FullStackSignal.js';
4
4
  import { ValueSignal } from './ValueSignal.js';
5
5
  /**
6
6
  * A {@link FullStackSignal} that represents a shared list of values, where each
@@ -21,11 +21,11 @@ export declare class ListSignal<T> extends CollectionSignal<ReadonlyArray<ValueS
21
21
  * Inserts a new value at the end of the list.
22
22
  * @param value - The value to insert.
23
23
  */
24
- insertLast(value: T): void;
24
+ insertLast(value: T): Operation;
25
25
  /**
26
26
  * Removes the given item from the list.
27
27
  * @param item - The item to remove.
28
28
  */
29
- remove(item: ValueSignal<T>): void;
29
+ remove(item: ValueSignal<T>): Operation;
30
30
  }
31
31
  //# sourceMappingURL=ListSignal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ListSignal.d.ts","sourceRoot":"","sources":["src/ListSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EASL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,sBAAsB,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACtH,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAU/C;;;;;;;;;;GAUG;AACH,qBAAa,UAAU,CAAC,CAAC,CAAE,SAAQ,gBAAgB,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;;gBAMpE,MAAM,EAAE,sBAAsB;cAgBvB,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAoFpE;;;OAGG;IACH,UAAU,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAK1B;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,IAAI;CAQnC"}
1
+ {"version":3,"file":"ListSignal.d.ts","sourceRoot":"","sources":["src/ListSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EASL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,sBAAsB,EAItB,KAAK,SAAS,EACd,KAAK,sBAAsB,EAC5B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAU/C;;;;;;;;;;GAUG;AACH,qBAAa,UAAU,CAAC,CAAC,CAAE,SAAQ,gBAAgB,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;;gBAMpE,MAAM,EAAE,sBAAsB;cAgBvB,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAsFpE;;;OAGG;IACH,UAAU,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS;IAM/B;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS;CASxC"}
package/ListSignal.js CHANGED
@@ -6,7 +6,13 @@ import {
6
6
  isListSnapshotStateEvent,
7
7
  isRemoveStateEvent
8
8
  } from "./events.js";
9
- import { $processServerResponse, $setValueQuietly, $update } from "./FullStackSignal.js";
9
+ import {
10
+ $createOperation,
11
+ $processServerResponse,
12
+ $resolveOperation,
13
+ $setValueQuietly,
14
+ $update
15
+ } from "./FullStackSignal.js";
10
16
  import { ValueSignal } from "./ValueSignal.js";
11
17
  class ListSignal extends CollectionSignal {
12
18
  #head;
@@ -28,6 +34,7 @@ class ListSignal extends CollectionSignal {
28
34
  }
29
35
  [$processServerResponse](event) {
30
36
  if (!event.accepted) {
37
+ this[$resolveOperation](event.id, "server rejected the operation");
31
38
  return;
32
39
  }
33
40
  if (isListSnapshotStateEvent(event)) {
@@ -37,6 +44,7 @@ class ListSignal extends CollectionSignal {
37
44
  } else if (isRemoveStateEvent(event)) {
38
45
  this.#handleRemoveUpdate(event);
39
46
  }
47
+ this[$resolveOperation](event.id);
40
48
  }
41
49
  #handleInsertLastUpdate(event) {
42
50
  if (event.entryId === void 0) {
@@ -111,7 +119,8 @@ class ListSignal extends CollectionSignal {
111
119
  */
112
120
  insertLast(value) {
113
121
  const event = createInsertLastStateEvent(value);
114
- this[$update](event);
122
+ const promise = this[$update](event);
123
+ return this[$createOperation]({ id: event.id, promise });
115
124
  }
116
125
  /**
117
126
  * Removes the given item from the list.
@@ -120,10 +129,11 @@ class ListSignal extends CollectionSignal {
120
129
  remove(item) {
121
130
  const entryToRemove = this.#values.get(item.id);
122
131
  if (entryToRemove === void 0) {
123
- return;
132
+ return { result: Promise.resolve() };
124
133
  }
125
134
  const removeEvent = createRemoveStateEvent(entryToRemove.value.id);
126
- this[$update](removeEvent);
135
+ const promise = this[$update](removeEvent);
136
+ return this[$createOperation]({ id: removeEvent.id, promise });
127
137
  }
128
138
  }
129
139
  export {
package/ListSignal.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/ListSignal.ts"],
4
- "sourcesContent": ["import { CollectionSignal } from './CollectionSignal.js';\nimport {\n createInsertLastStateEvent,\n createRemoveStateEvent,\n type InsertLastStateEvent,\n isInsertLastStateEvent,\n isListSnapshotStateEvent,\n isRemoveStateEvent,\n type ListSnapshotStateEvent,\n type RemoveStateEvent,\n type StateEvent,\n} from './events.js';\nimport { $processServerResponse, $setValueQuietly, $update, type ServerConnectionConfig } from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\ntype EntryId = string;\ntype Entry<T> = {\n id: EntryId;\n value: ValueSignal<T>;\n next?: EntryId;\n prev?: EntryId;\n};\n\n/**\n * A {@link FullStackSignal} that represents a shared list of values, where each\n * value is represented by a {@link ValueSignal}.\n * The list can be modified by calling the defined methods to insert or remove\n * items, but the `value` property of a `ListSignal` instance is read-only and\n * cannot be assigned directly.\n * The value of each item in the list can be manipulated similar to a regular\n * {@link ValueSignal}.\n *\n * @typeParam T - The type of the values in the list.\n */\nexport class ListSignal<T> extends CollectionSignal<ReadonlyArray<ValueSignal<T>>> {\n #head?: EntryId;\n #tail?: EntryId;\n\n readonly #values = new Map<string, Entry<T>>();\n\n constructor(config: ServerConnectionConfig) {\n const initialValue: Array<ValueSignal<T>> = [];\n super(initialValue, config);\n }\n\n #computeItems(): ReadonlyArray<ValueSignal<T>> {\n let current = this.#head;\n const result: Array<ValueSignal<T>> = [];\n while (current !== undefined) {\n const entry = this.#values.get(current)!;\n result.push(entry.value);\n current = entry.next;\n }\n return result;\n }\n\n protected override [$processServerResponse](event: StateEvent): void {\n if (!event.accepted) {\n return;\n }\n if (isListSnapshotStateEvent<T>(event)) {\n this.#handleSnapshotEvent(event);\n } else if (isInsertLastStateEvent<T>(event)) {\n this.#handleInsertLastUpdate(event);\n } else if (isRemoveStateEvent(event)) {\n this.#handleRemoveUpdate(event);\n }\n }\n\n #handleInsertLastUpdate(event: InsertLastStateEvent<T>): void {\n if (event.entryId === undefined) {\n throw new Error('Unexpected state: Entry id should be defined when insert last event is accepted');\n }\n const valueSignal = new ValueSignal<T>(\n event.value,\n { ...this.server.config, parentClientSignalId: this.id },\n event.entryId,\n );\n const newEntry: Entry<T> = { id: valueSignal.id, value: valueSignal };\n\n if (this.#head === undefined) {\n this.#head = newEntry.id;\n this.#tail = this.#head;\n } else {\n const tailEntry = this.#values.get(this.#tail!)!;\n tailEntry.next = newEntry.id;\n newEntry.prev = this.#tail;\n this.#tail = newEntry.id;\n }\n this.#values.set(valueSignal.id, newEntry);\n this[$setValueQuietly](this.#computeItems());\n }\n\n #handleRemoveUpdate(event: RemoveStateEvent): void {\n const entryToRemove = this.#values.get(event.entryId);\n if (entryToRemove === undefined) {\n return;\n }\n this.#values.delete(event.id);\n if (this.#head === entryToRemove.id) {\n if (entryToRemove.next === undefined) {\n this.#head = undefined;\n this.#tail = undefined;\n } else {\n const newHead = this.#values.get(entryToRemove.next)!;\n this.#head = newHead.id;\n newHead.prev = undefined;\n }\n } else {\n const prevEntry = this.#values.get(entryToRemove.prev!)!;\n const nextEntry = entryToRemove.next !== undefined ? this.#values.get(entryToRemove.next) : undefined;\n if (nextEntry === undefined) {\n this.#tail = prevEntry.id;\n prevEntry.next = undefined;\n } else {\n prevEntry.next = nextEntry.id;\n nextEntry.prev = prevEntry.id;\n }\n }\n this[$setValueQuietly](this.#computeItems());\n }\n\n #handleSnapshotEvent(event: ListSnapshotStateEvent<T>): void {\n event.entries.forEach((entry) => {\n this.#values.set(entry.id, {\n id: entry.id,\n prev: entry.prev,\n next: entry.next,\n value: new ValueSignal(entry.value, { ...this.server.config, parentClientSignalId: this.id }, entry.id),\n });\n if (entry.prev === undefined) {\n this.#head = entry.id;\n }\n if (entry.next === undefined) {\n this.#tail = entry.id;\n }\n });\n this[$setValueQuietly](this.#computeItems());\n }\n\n /**\n * Inserts a new value at the end of the list.\n * @param value - The value to insert.\n */\n insertLast(value: T): void {\n const event = createInsertLastStateEvent(value);\n this[$update](event);\n }\n\n /**\n * Removes the given item from the list.\n * @param item - The item to remove.\n */\n remove(item: ValueSignal<T>): void {\n const entryToRemove = this.#values.get(item.id);\n if (entryToRemove === undefined) {\n return;\n }\n const removeEvent = createRemoveStateEvent(entryToRemove.value.id);\n this[$update](removeEvent);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { CollectionSignal } from './CollectionSignal.js';\nimport {\n createInsertLastStateEvent,\n createRemoveStateEvent,\n type InsertLastStateEvent,\n isInsertLastStateEvent,\n isListSnapshotStateEvent,\n isRemoveStateEvent,\n type ListSnapshotStateEvent,\n type RemoveStateEvent,\n type StateEvent,\n} from './events.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n type Operation,\n type ServerConnectionConfig,\n} from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\ntype EntryId = string;\ntype Entry<T> = {\n id: EntryId;\n value: ValueSignal<T>;\n next?: EntryId;\n prev?: EntryId;\n};\n\n/**\n * A {@link FullStackSignal} that represents a shared list of values, where each\n * value is represented by a {@link ValueSignal}.\n * The list can be modified by calling the defined methods to insert or remove\n * items, but the `value` property of a `ListSignal` instance is read-only and\n * cannot be assigned directly.\n * The value of each item in the list can be manipulated similar to a regular\n * {@link ValueSignal}.\n *\n * @typeParam T - The type of the values in the list.\n */\nexport class ListSignal<T> extends CollectionSignal<ReadonlyArray<ValueSignal<T>>> {\n #head?: EntryId;\n #tail?: EntryId;\n\n readonly #values = new Map<string, Entry<T>>();\n\n constructor(config: ServerConnectionConfig) {\n const initialValue: Array<ValueSignal<T>> = [];\n super(initialValue, config);\n }\n\n #computeItems(): ReadonlyArray<ValueSignal<T>> {\n let current = this.#head;\n const result: Array<ValueSignal<T>> = [];\n while (current !== undefined) {\n const entry = this.#values.get(current)!;\n result.push(entry.value);\n current = entry.next;\n }\n return result;\n }\n\n protected override [$processServerResponse](event: StateEvent): void {\n if (!event.accepted) {\n this[$resolveOperation](event.id, 'server rejected the operation');\n return;\n }\n if (isListSnapshotStateEvent<T>(event)) {\n this.#handleSnapshotEvent(event);\n } else if (isInsertLastStateEvent<T>(event)) {\n this.#handleInsertLastUpdate(event);\n } else if (isRemoveStateEvent(event)) {\n this.#handleRemoveUpdate(event);\n }\n this[$resolveOperation](event.id);\n }\n\n #handleInsertLastUpdate(event: InsertLastStateEvent<T>): void {\n if (event.entryId === undefined) {\n throw new Error('Unexpected state: Entry id should be defined when insert last event is accepted');\n }\n const valueSignal = new ValueSignal<T>(\n event.value,\n { ...this.server.config, parentClientSignalId: this.id },\n event.entryId,\n );\n const newEntry: Entry<T> = { id: valueSignal.id, value: valueSignal };\n\n if (this.#head === undefined) {\n this.#head = newEntry.id;\n this.#tail = this.#head;\n } else {\n const tailEntry = this.#values.get(this.#tail!)!;\n tailEntry.next = newEntry.id;\n newEntry.prev = this.#tail;\n this.#tail = newEntry.id;\n }\n this.#values.set(valueSignal.id, newEntry);\n this[$setValueQuietly](this.#computeItems());\n }\n\n #handleRemoveUpdate(event: RemoveStateEvent): void {\n const entryToRemove = this.#values.get(event.entryId);\n if (entryToRemove === undefined) {\n return;\n }\n this.#values.delete(event.id);\n if (this.#head === entryToRemove.id) {\n if (entryToRemove.next === undefined) {\n this.#head = undefined;\n this.#tail = undefined;\n } else {\n const newHead = this.#values.get(entryToRemove.next)!;\n this.#head = newHead.id;\n newHead.prev = undefined;\n }\n } else {\n const prevEntry = this.#values.get(entryToRemove.prev!)!;\n const nextEntry = entryToRemove.next !== undefined ? this.#values.get(entryToRemove.next) : undefined;\n if (nextEntry === undefined) {\n this.#tail = prevEntry.id;\n prevEntry.next = undefined;\n } else {\n prevEntry.next = nextEntry.id;\n nextEntry.prev = prevEntry.id;\n }\n }\n this[$setValueQuietly](this.#computeItems());\n }\n\n #handleSnapshotEvent(event: ListSnapshotStateEvent<T>): void {\n event.entries.forEach((entry) => {\n this.#values.set(entry.id, {\n id: entry.id,\n prev: entry.prev,\n next: entry.next,\n value: new ValueSignal(entry.value, { ...this.server.config, parentClientSignalId: this.id }, entry.id),\n });\n if (entry.prev === undefined) {\n this.#head = entry.id;\n }\n if (entry.next === undefined) {\n this.#tail = entry.id;\n }\n });\n this[$setValueQuietly](this.#computeItems());\n }\n\n /**\n * Inserts a new value at the end of the list.\n * @param value - The value to insert.\n */\n insertLast(value: T): Operation {\n const event = createInsertLastStateEvent(value);\n const promise = this[$update](event);\n return this[$createOperation]({ id: event.id, promise });\n }\n\n /**\n * Removes the given item from the list.\n * @param item - The item to remove.\n */\n remove(item: ValueSignal<T>): Operation {\n const entryToRemove = this.#values.get(item.id);\n if (entryToRemove === undefined) {\n return { result: Promise.resolve() };\n }\n const removeEvent = createRemoveStateEvent(entryToRemove.value.id);\n const promise = this[$update](removeEvent);\n return this[$createOperation]({ id: removeEvent.id, promise });\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/NumberSignal.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type StateEvent } from './events.js';
2
- import { $processServerResponse } from './FullStackSignal.js';
2
+ import { $processServerResponse, type Operation } from './FullStackSignal.js';
3
3
  import { ValueSignal } from './ValueSignal.js';
4
4
  /**
5
5
  * A signal that holds a number value. The underlying
@@ -37,8 +37,9 @@ export declare class NumberSignal extends ValueSignal<number> {
37
37
  *
38
38
  * @param delta - The delta to increment the value by. The delta can be
39
39
  * negative.
40
+ * @returns An operation object that allows to perform additional actions.
40
41
  */
41
- incrementBy(delta: number): void;
42
+ incrementBy(delta: number): Operation;
42
43
  protected [$processServerResponse](event: StateEvent): void;
43
44
  }
44
45
  //# sourceMappingURL=NumberSignal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NumberSignal.d.ts","sourceRoot":"","sources":["src/NumberSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,EAAE,sBAAsB,EAA6B,MAAM,sBAAsB,CAAC;AACzF,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,GAAG,IAAI;CAWrE"}
1
+ {"version":3,"file":"NumberSignal.d.ts","sourceRoot":"","sources":["src/NumberSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAChG,OAAO,EAEL,sBAAsB,EAItB,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,YAAa,SAAQ,WAAW,CAAC,MAAM,CAAC;;IAEnD;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS;cAWlB,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CAarE"}
package/NumberSignal.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import { createIncrementStateEvent, isIncrementStateEvent } from "./events.js";
2
- import { $processServerResponse, $setValueQuietly, $update } from "./FullStackSignal.js";
2
+ import {
3
+ $createOperation,
4
+ $processServerResponse,
5
+ $resolveOperation,
6
+ $setValueQuietly,
7
+ $update
8
+ } from "./FullStackSignal.js";
3
9
  import { ValueSignal } from "./ValueSignal.js";
4
10
  class NumberSignal extends ValueSignal {
5
11
  #sentIncrementEvents = /* @__PURE__ */ new Map();
@@ -14,23 +20,27 @@ class NumberSignal extends ValueSignal {
14
20
  *
15
21
  * @param delta - The delta to increment the value by. The delta can be
16
22
  * negative.
23
+ * @returns An operation object that allows to perform additional actions.
17
24
  */
18
25
  incrementBy(delta) {
19
26
  if (delta === 0) {
20
- return;
27
+ return { result: Promise.resolve() };
21
28
  }
22
29
  this[$setValueQuietly](this.value + delta);
23
30
  const event = createIncrementStateEvent(delta);
24
31
  this.#sentIncrementEvents.set(event.id, event);
25
- this[$update](event);
32
+ const promise = this[$update](event);
33
+ return this[$createOperation]({ id: event.id, promise });
26
34
  }
27
35
  [$processServerResponse](event) {
28
36
  if (event.accepted && isIncrementStateEvent(event)) {
29
- if (this.#sentIncrementEvents.has(event.id)) {
37
+ const sentEvent = this.#sentIncrementEvents.get(event.id);
38
+ if (sentEvent) {
30
39
  this.#sentIncrementEvents.delete(event.id);
31
- return;
40
+ } else {
41
+ this[$setValueQuietly](this.value + event.value);
32
42
  }
33
- this[$setValueQuietly](this.value + event.value);
43
+ this[$resolveOperation](event.id);
34
44
  } else {
35
45
  super[$processServerResponse](event);
36
46
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/NumberSignal.ts"],
4
- "sourcesContent": ["import { createIncrementStateEvent, isIncrementStateEvent, type StateEvent } from './events.js';\nimport { $processServerResponse, $setValueQuietly, $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>();\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[$setValueQuietly](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): void {\n if (event.accepted && isIncrementStateEvent(event)) {\n if (this.#sentIncrementEvents.has(event.id)) {\n this.#sentIncrementEvents.delete(event.id);\n return;\n }\n this[$setValueQuietly](this.value + event.value);\n } else {\n super[$processServerResponse](event);\n }\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,2BAA2B,6BAA8C;AAClF,SAAS,wBAAwB,kBAAkB,eAAe;AAClE,SAAS,mBAAmB;AAyBrB,MAAM,qBAAqB,YAAoB;AAAA,EAC3C,uBAAuB,oBAAI,IAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAa5D,YAAY,OAAqB;AAC/B,QAAI,UAAU,GAAG;AACf;AAAA,IACF;AACA,SAAK,gBAAgB,EAAE,KAAK,QAAQ,KAAK;AACzC,UAAM,QAAQ,0BAA0B,KAAK;AAC7C,SAAK,qBAAqB,IAAI,MAAM,IAAI,KAAK;AAC7C,SAAK,OAAO,EAAE,KAAK;AAAA,EACrB;AAAA,EAEA,CAAoB,sBAAsB,EAAE,OAAyB;AACnE,QAAI,MAAM,YAAY,sBAAsB,KAAK,GAAG;AAClD,UAAI,KAAK,qBAAqB,IAAI,MAAM,EAAE,GAAG;AAC3C,aAAK,qBAAqB,OAAO,MAAM,EAAE;AACzC;AAAA,MACF;AACA,WAAK,gBAAgB,EAAE,KAAK,QAAQ,MAAM,KAAK;AAAA,IACjD,OAAO;AACL,YAAM,sBAAsB,EAAE,KAAK;AAAA,IACrC;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { createIncrementStateEvent, isIncrementStateEvent, type StateEvent } from './events.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n type Operation,\n} 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>();\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 * @returns An operation object that allows to perform additional actions.\n */\n incrementBy(delta: number): Operation {\n if (delta === 0) {\n return { result: Promise.resolve() };\n }\n this[$setValueQuietly](this.value + delta);\n const event = createIncrementStateEvent(delta);\n this.#sentIncrementEvents.set(event.id, event);\n const promise = this[$update](event);\n return this[$createOperation]({ id: event.id, promise });\n }\n\n protected override [$processServerResponse](event: StateEvent): void {\n if (event.accepted && isIncrementStateEvent(event)) {\n const sentEvent = this.#sentIncrementEvents.get(event.id);\n if (sentEvent) {\n this.#sentIncrementEvents.delete(event.id);\n } else {\n this[$setValueQuietly](this.value + event.value);\n }\n this[$resolveOperation](event.id);\n } else {\n super[$processServerResponse](event);\n }\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,2BAA2B,6BAA8C;AAClF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,mBAAmB;AAyBrB,MAAM,qBAAqB,YAAoB;AAAA,EAC3C,uBAAuB,oBAAI,IAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAc5D,YAAY,OAA0B;AACpC,QAAI,UAAU,GAAG;AACf,aAAO,EAAE,QAAQ,QAAQ,QAAQ,EAAE;AAAA,IACrC;AACA,SAAK,gBAAgB,EAAE,KAAK,QAAQ,KAAK;AACzC,UAAM,QAAQ,0BAA0B,KAAK;AAC7C,SAAK,qBAAqB,IAAI,MAAM,IAAI,KAAK;AAC7C,UAAM,UAAU,KAAK,OAAO,EAAE,KAAK;AACnC,WAAO,KAAK,gBAAgB,EAAE,EAAE,IAAI,MAAM,IAAI,QAAQ,CAAC;AAAA,EACzD;AAAA,EAEA,CAAoB,sBAAsB,EAAE,OAAyB;AACnE,QAAI,MAAM,YAAY,sBAAsB,KAAK,GAAG;AAClD,YAAM,YAAY,KAAK,qBAAqB,IAAI,MAAM,EAAE;AACxD,UAAI,WAAW;AACb,aAAK,qBAAqB,OAAO,MAAM,EAAE;AAAA,MAC3C,OAAO;AACL,aAAK,gBAAgB,EAAE,KAAK,QAAQ,MAAM,KAAK;AAAA,MACjD;AACA,WAAK,iBAAiB,EAAE,MAAM,EAAE;AAAA,IAClC,OAAO;AACL,YAAM,sBAAsB,EAAE,KAAK;AAAA,IACrC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/ValueSignal.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type StateEvent } from './events.js';
2
- import { $processServerResponse, FullStackSignal } from './FullStackSignal.js';
2
+ import { $processServerResponse, FullStackSignal, type Operation } from './FullStackSignal.js';
3
3
  /**
4
4
  * An operation subscription that can be canceled.
5
5
  */
6
- export interface OperationSubscription {
6
+ export interface OperationSubscription extends Operation {
7
7
  cancel(): void;
8
8
  }
9
9
  /**
@@ -21,15 +21,16 @@ export declare class ValueSignal<T> extends FullStackSignal<T> {
21
21
  *
22
22
  * @param value - The new value.
23
23
  */
24
- set(value: T): void;
24
+ set(value: T): Operation;
25
25
  /**
26
26
  * Replaces the value with a new one only if the current value is equal to the
27
27
  * expected value.
28
28
  *
29
29
  * @param expected - The expected value.
30
30
  * @param newValue - The new value.
31
+ * @returns An operation object that allows to perform additional actions.
31
32
  */
32
- replace(expected: T, newValue: T): void;
33
+ replace(expected: T, newValue: T): Operation;
33
34
  /**
34
35
  * Tries to update the value by applying the callback function to the current
35
36
  * value. In case of a concurrent change, the callback is run again with an
@@ -41,7 +42,7 @@ export declare class ValueSignal<T> extends FullStackSignal<T> {
41
42
  *
42
43
  * @param callback - The function that is applied on the current value to
43
44
  * produce the new value.
44
- * @returns An operation subscription that can be canceled.
45
+ * @returns An operation object that allows to perform additional actions, including cancellation.
45
46
  */
46
47
  update(callback: (value: T) => T): OperationSubscription;
47
48
  protected [$processServerResponse](event: StateEvent): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ValueSignal.d.ts","sourceRoot":"","sources":["src/ValueSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AACrB,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;IAMvC;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,qBAAqB;cAerC,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CA8BrE"}
1
+ {"version":3,"file":"ValueSignal.d.ts","sourceRoot":"","sources":["src/ValueSignal.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,sBAAsB,EAItB,eAAe,EACf,KAAK,SAAS,EACf,MAAM,sBAAsB,CAAC;AAO9B;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,SAAS;IACtD,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,SAAS;IASxB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,SAAS;IAQ5C;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,qBAAqB;cAcrC,CAAC,sBAAsB,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;CA8BrE"}
package/ValueSignal.js CHANGED
@@ -1,10 +1,19 @@
1
+ import { nanoid } from "nanoid";
1
2
  import {
2
3
  createReplaceStateEvent,
4
+ createSetStateEvent,
3
5
  isReplaceStateEvent,
4
6
  isSetStateEvent,
5
7
  isSnapshotStateEvent
6
8
  } from "./events.js";
7
- import { $processServerResponse, $update, FullStackSignal } from "./FullStackSignal.js";
9
+ import {
10
+ $createOperation,
11
+ $processServerResponse,
12
+ $resolveOperation,
13
+ $setValueQuietly,
14
+ $update,
15
+ FullStackSignal
16
+ } from "./FullStackSignal.js";
8
17
  class ValueSignal extends FullStackSignal {
9
18
  #pendingRequests = /* @__PURE__ */ new Map();
10
19
  /**
@@ -18,7 +27,12 @@ class ValueSignal extends FullStackSignal {
18
27
  * @param value - The new value.
19
28
  */
20
29
  set(value) {
21
- this.value = value;
30
+ const { parentClientSignalId } = this.server.config;
31
+ const signalId = parentClientSignalId !== void 0 ? this.id : void 0;
32
+ const event = createSetStateEvent(value, signalId, parentClientSignalId);
33
+ const promise = this[$update](event);
34
+ this[$setValueQuietly](value);
35
+ return this[$createOperation]({ id: event.id, promise });
22
36
  }
23
37
  /**
24
38
  * Replaces the value with a new one only if the current value is equal to the
@@ -26,11 +40,14 @@ class ValueSignal extends FullStackSignal {
26
40
  *
27
41
  * @param expected - The expected value.
28
42
  * @param newValue - The new value.
43
+ * @returns An operation object that allows to perform additional actions.
29
44
  */
30
45
  replace(expected, newValue) {
31
46
  const { parentClientSignalId } = this.server.config;
32
47
  const signalId = parentClientSignalId !== void 0 ? this.id : void 0;
33
- this[$update](createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId));
48
+ const event = createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId);
49
+ const promise = this[$update](event);
50
+ return this[$createOperation]({ id: event.id, promise });
34
51
  }
35
52
  /**
36
53
  * Tries to update the value by applying the callback function to the current
@@ -43,19 +60,18 @@ class ValueSignal extends FullStackSignal {
43
60
  *
44
61
  * @param callback - The function that is applied on the current value to
45
62
  * produce the new value.
46
- * @returns An operation subscription that can be canceled.
63
+ * @returns An operation object that allows to perform additional actions, including cancellation.
47
64
  */
48
65
  update(callback) {
49
66
  const newValue = callback(this.value);
50
67
  const event = createReplaceStateEvent(this.value, newValue);
51
- this[$update](event);
52
- const waiter = Promise.withResolvers();
53
- const pendingRequest = { callback, waiter, canceled: false };
68
+ const promise = this[$update](event);
69
+ const pendingRequest = { id: nanoid(), callback, canceled: false };
54
70
  this.#pendingRequests.set(event.id, pendingRequest);
55
71
  return {
72
+ ...this[$createOperation]({ id: pendingRequest.id, promise }),
56
73
  cancel: () => {
57
74
  pendingRequest.canceled = true;
58
- pendingRequest.waiter.resolve();
59
75
  }
60
76
  };
61
77
  }
@@ -63,18 +79,17 @@ class ValueSignal extends FullStackSignal {
63
79
  const record = this.#pendingRequests.get(event.id);
64
80
  if (record) {
65
81
  this.#pendingRequests.delete(event.id);
66
- }
67
- if (!event.accepted && record) {
68
- if (!record.canceled) {
82
+ if (!(event.accepted || record.canceled)) {
69
83
  this.update(record.callback);
70
84
  }
71
85
  }
86
+ let reason;
72
87
  if (event.accepted || isSnapshotStateEvent(event)) {
73
- if (record) {
74
- record.waiter.resolve();
75
- }
76
88
  this.#applyAcceptedEvent(event);
89
+ } else {
90
+ reason = "server rejected the operation";
77
91
  }
92
+ [record?.id, event.id].filter(Boolean).forEach((id) => this[$resolveOperation](id, reason));
78
93
  }
79
94
  #applyAcceptedEvent(event) {
80
95
  if (isSetStateEvent(event) || isSnapshotStateEvent(event)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/ValueSignal.ts"],
4
- "sourcesContent": ["import {\n createReplaceStateEvent,\n isReplaceStateEvent,\n isSetStateEvent,\n isSnapshotStateEvent,\n type StateEvent,\n} 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 const { parentClientSignalId } = this.server.config;\n const signalId = parentClientSignalId !== undefined ? this.id : undefined;\n this[$update](createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId));\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): 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 || isSnapshotStateEvent<T>(event)) {\n if (record) {\n record.waiter.resolve();\n }\n this.#applyAcceptedEvent(event);\n }\n }\n\n #applyAcceptedEvent(event: StateEvent): void {\n if (isSetStateEvent<T>(event) || isSnapshotStateEvent<T>(event)) {\n this.value = event.value;\n } else if (isReplaceStateEvent<T>(event)) {\n if (JSON.stringify(this.value) === JSON.stringify(event.expected)) {\n this.value = event.value;\n }\n }\n }\n}\n"],
5
- "mappings": "AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,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,UAAM,EAAE,qBAAqB,IAAI,KAAK,OAAO;AAC7C,UAAM,WAAW,yBAAyB,SAAY,KAAK,KAAK;AAChE,SAAK,OAAO,EAAE,wBAAwB,UAAU,UAAU,UAAU,oBAAoB,CAAC;AAAA,EAC3F;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,OAAyB;AACnE,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,qBAAwB,KAAK,GAAG;AACpD,UAAI,QAAQ;AACV,eAAO,OAAO,QAAQ;AAAA,MACxB;AACA,WAAK,oBAAoB,KAAK;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,oBAAoB,OAAyB;AAC3C,QAAI,gBAAmB,KAAK,KAAK,qBAAwB,KAAK,GAAG;AAC/D,WAAK,QAAQ,MAAM;AAAA,IACrB,WAAW,oBAAuB,KAAK,GAAG;AACxC,UAAI,KAAK,UAAU,KAAK,KAAK,MAAM,KAAK,UAAU,MAAM,QAAQ,GAAG;AACjE,aAAK,QAAQ,MAAM;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { nanoid } from 'nanoid';\nimport {\n createReplaceStateEvent,\n createSetStateEvent,\n isReplaceStateEvent,\n isSetStateEvent,\n isSnapshotStateEvent,\n type StateEvent,\n} from './events.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n FullStackSignal,\n type Operation,\n} from './FullStackSignal.js';\n\ntype PendingRequestsRecord<T> = Readonly<{\n id: string;\n callback(value: T): T;\n}> & { canceled: boolean };\n\n/**\n * An operation subscription that can be canceled.\n */\nexport interface OperationSubscription extends Operation {\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): Operation {\n const { parentClientSignalId } = this.server.config;\n const signalId = parentClientSignalId !== undefined ? this.id : undefined;\n const event = createSetStateEvent(value, signalId, parentClientSignalId);\n const promise = this[$update](event);\n this[$setValueQuietly](value);\n return this[$createOperation]({ id: event.id, promise });\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 * @returns An operation object that allows to perform additional actions.\n */\n replace(expected: T, newValue: T): Operation {\n const { parentClientSignalId } = this.server.config;\n const signalId = parentClientSignalId !== undefined ? this.id : undefined;\n const event = createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId);\n const promise = this[$update](event);\n return this[$createOperation]({ id: event.id, promise });\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 object that allows to perform additional actions, including cancellation.\n */\n update(callback: (value: T) => T): OperationSubscription {\n const newValue = callback(this.value);\n const event = createReplaceStateEvent(this.value, newValue);\n const promise = this[$update](event);\n const pendingRequest = { id: nanoid(), callback, canceled: false };\n this.#pendingRequests.set(event.id, pendingRequest);\n return {\n ...this[$createOperation]({ id: pendingRequest.id, promise }),\n cancel: () => {\n pendingRequest.canceled = true;\n },\n };\n }\n\n protected override [$processServerResponse](event: StateEvent): void {\n const record = this.#pendingRequests.get(event.id);\n if (record) {\n this.#pendingRequests.delete(event.id);\n\n if (!(event.accepted || record.canceled)) {\n this.update(record.callback);\n }\n }\n\n let reason: string | undefined;\n if (event.accepted || isSnapshotStateEvent<T>(event)) {\n this.#applyAcceptedEvent(event);\n } else {\n reason = 'server rejected the operation';\n }\n // `then` callbacks can be associated to the record or the event\n // it depends on the operation that was performed\n [record?.id, event.id].filter(Boolean).forEach((id) => this[$resolveOperation](id!, reason));\n }\n\n #applyAcceptedEvent(event: StateEvent): void {\n if (isSetStateEvent<T>(event) || isSnapshotStateEvent<T>(event)) {\n this.value = event.value;\n } else if (isReplaceStateEvent<T>(event)) {\n if (JSON.stringify(this.value) === JSON.stringify(event.expected)) {\n this.value = event.value;\n }\n }\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAiBA,MAAM,oBAAuB,gBAAmB;AAAA,EAC5C,mBAAmB,oBAAI,IAAsC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYtE,IAAI,OAAqB;AACvB,UAAM,EAAE,qBAAqB,IAAI,KAAK,OAAO;AAC7C,UAAM,WAAW,yBAAyB,SAAY,KAAK,KAAK;AAChE,UAAM,QAAQ,oBAAoB,OAAO,UAAU,oBAAoB;AACvE,UAAM,UAAU,KAAK,OAAO,EAAE,KAAK;AACnC,SAAK,gBAAgB,EAAE,KAAK;AAC5B,WAAO,KAAK,gBAAgB,EAAE,EAAE,IAAI,MAAM,IAAI,QAAQ,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,QAAQ,UAAa,UAAwB;AAC3C,UAAM,EAAE,qBAAqB,IAAI,KAAK,OAAO;AAC7C,UAAM,WAAW,yBAAyB,SAAY,KAAK,KAAK;AAChE,UAAM,QAAQ,wBAAwB,UAAU,UAAU,UAAU,oBAAoB;AACxF,UAAM,UAAU,KAAK,OAAO,EAAE,KAAK;AACnC,WAAO,KAAK,gBAAgB,EAAE,EAAE,IAAI,MAAM,IAAI,QAAQ,CAAC;AAAA,EACzD;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,UAAM,UAAU,KAAK,OAAO,EAAE,KAAK;AACnC,UAAM,iBAAiB,EAAE,IAAI,OAAO,GAAG,UAAU,UAAU,MAAM;AACjE,SAAK,iBAAiB,IAAI,MAAM,IAAI,cAAc;AAClD,WAAO;AAAA,MACL,GAAG,KAAK,gBAAgB,EAAE,EAAE,IAAI,eAAe,IAAI,QAAQ,CAAC;AAAA,MAC5D,QAAQ,MAAM;AACZ,uBAAe,WAAW;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,CAAoB,sBAAsB,EAAE,OAAyB;AACnE,UAAM,SAAS,KAAK,iBAAiB,IAAI,MAAM,EAAE;AACjD,QAAI,QAAQ;AACV,WAAK,iBAAiB,OAAO,MAAM,EAAE;AAErC,UAAI,EAAE,MAAM,YAAY,OAAO,WAAW;AACxC,aAAK,OAAO,OAAO,QAAQ;AAAA,MAC7B;AAAA,IACF;AAEA,QAAI;AACJ,QAAI,MAAM,YAAY,qBAAwB,KAAK,GAAG;AACpD,WAAK,oBAAoB,KAAK;AAAA,IAChC,OAAO;AACL,eAAS;AAAA,IACX;AAGA,KAAC,QAAQ,IAAI,MAAM,EAAE,EAAE,OAAO,OAAO,EAAE,QAAQ,CAAC,OAAO,KAAK,iBAAiB,EAAE,IAAK,MAAM,CAAC;AAAA,EAC7F;AAAA,EAEA,oBAAoB,OAAyB;AAC3C,QAAI,gBAAmB,KAAK,KAAK,qBAAwB,KAAK,GAAG;AAC/D,WAAK,QAAQ,MAAM;AAAA,IACrB,WAAW,oBAAuB,KAAK,GAAG;AACxC,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/core.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["src/core.ts"],"names":[],"mappings":"AAKA,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["src/core.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
package/core.js CHANGED
@@ -1,4 +1,2 @@
1
- import { installAutoSignalTracking } from "@preact/signals-react/runtime";
2
- installAutoSignalTracking();
3
1
  export * from "@preact/signals-react";
4
2
  //# sourceMappingURL=core.js.map
package/core.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["src/core.ts"],
4
- "sourcesContent": ["import { installAutoSignalTracking } from '@preact/signals-react/runtime';\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-call\ninstallAutoSignalTracking();\n\nexport * from '@preact/signals-react';\n"],
5
- "mappings": "AAAA,SAAS,iCAAiC;AAG1C,0BAA0B;AAE1B,cAAc;",
4
+ "sourcesContent": ["export * from '@preact/signals-react';\n"],
5
+ "mappings": "AAAA,cAAc;",
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.6.0-alpha2",
3
+ "version": "24.6.0-alpha4",
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.6.0-alpha2",
50
+ "@vaadin/hilla-frontend": "24.6.0-alpha4",
51
51
  "nanoid": "^5.0.7"
52
52
  },
53
53
  "peerDependencies": {