diff --git a/client-js/main/client/client.js b/client-js/main/client/client.js
index b027346f..53506fde 100644
--- a/client-js/main/client/client.js
+++ b/client-js/main/client/client.js
@@ -183,6 +183,16 @@ export class Client {
throw new Error('Not implemented in abstract base.');
}
+ /**
+ * Immediately cancels all active subscriptions.
+ *
+ * This endpoint is handy to use when an end-user chooses to end her session
+ * with the web app. E.g. all subscriptions should be cancelled upon user sign-out.
+ */
+ cancelAllSubscriptions() {
+ throw new Error('Not implemented in abstract base.');
+ }
+
/**
* Subscribes to the given `Topic` instance.
*
diff --git a/client-js/main/client/composite-client.js b/client-js/main/client/composite-client.js
index bb219155..8de05fb2 100644
--- a/client-js/main/client/composite-client.js
+++ b/client-js/main/client/composite-client.js
@@ -90,6 +90,13 @@ export class CompositeClient extends Client {
return this._subscribing.subscribeTo(entityType, this);
}
+ /**
+ * @override
+ */
+ cancelAllSubscriptions() {
+ this._subscribing.cancelAllSubscriptions();
+ }
+
/**
* @override
*/
diff --git a/client-js/main/client/firebase-client.js b/client-js/main/client/firebase-client.js
index 6a61d0e7..b1c0df0a 100644
--- a/client-js/main/client/firebase-client.js
+++ b/client-js/main/client/firebase-client.js
@@ -35,7 +35,6 @@ import {ActorRequestFactory} from './actor-request-factory';
import {AbstractClientFactory} from './client-factory';
import {CommandingClient} from "./commanding-client";
import {CompositeClient} from "./composite-client";
-import {HttpClient} from './http-client';
import {HttpEndpoint} from './http-endpoint';
import {FirebaseDatabaseClient} from './firebase-database-client';
import {FirebaseSubscriptionService} from './firebase-subscription-service';
@@ -84,9 +83,9 @@ class SpineSubscription extends Subscription {
class EntitySubscription extends SpineSubscription {
/**
- * @param {Function} unsubscribe the callbacks that allows to cancel the subscription
+ * @param {Function} unsubscribe the callback that allows to cancel the subscription
* @param {{itemAdded: Observable, itemChanged: Observable, itemRemoved: Observable}} observables
- * the observables for entity changes
+ * the observables for entity change
* @param {SubscriptionObject} subscription the wrapped subscription object
*/
constructor({
@@ -102,7 +101,13 @@ class EntitySubscription extends SpineSubscription {
* @return {EntitySubscriptionObject} a plain object with observables and unsubscribe method
*/
toObject() {
- return Object.assign({}, this._observables, {unsubscribe: () => this.unsubscribe()});
+ return Object.assign({},
+ this._observables,
+ {
+ unsubscribe: () => {
+ return this.unsubscribe();
+ }
+ });
}
}
@@ -130,7 +135,13 @@ class EventSubscription extends SpineSubscription {
*/
toObject() {
return Object.assign(
- {}, {eventEmitted: this._observable}, {unsubscribe: () => this.unsubscribe()}
+ {},
+ {eventEmitted: this._observable},
+ {
+ unsubscribe: () => {
+ return this.unsubscribe();
+ }
+ }
);
}
}
@@ -243,6 +254,7 @@ class FirebaseSubscribingClient extends SubscribingClient {
return new EntitySubscription({
unsubscribedBy: () => {
FirebaseSubscribingClient._unsubscribe(pathSubscriptions);
+ this._subscriptionService.cancelSubscription(subscription);
},
withObservables: {
itemAdded: ObjectToProto.map(itemAdded.asObservable(), typeUrl),
@@ -263,12 +275,20 @@ class FirebaseSubscribingClient extends SubscribingClient {
return new EventSubscription({
unsubscribedBy: () => {
FirebaseSubscribingClient._unsubscribe([pathSubscription]);
+ this._subscriptionService.cancelSubscription(subscription);
},
withObservable: ObjectToProto.map(itemAdded.asObservable(), EVENT_TYPE_URL),
forInternal: subscription
});
}
+ /**
+ * @override
+ */
+ cancelAllSubscriptions() {
+ this._subscriptionService.cancelAllSubscriptions();
+ }
+
/**
* Unsubscribes the provided Firebase subscriptions.
*
diff --git a/client-js/main/client/firebase-subscription-service.js b/client-js/main/client/firebase-subscription-service.js
index 44b80c66..19ea4d89 100644
--- a/client-js/main/client/firebase-subscription-service.js
+++ b/client-js/main/client/firebase-subscription-service.js
@@ -38,8 +38,8 @@ import {Status} from '../proto/spine/core/response_pb';
const DEFAULT_KEEP_UP_INTERVAL = new Duration({minutes: 2});
/**
- * A service that manages the active subscriptions periodically sending requests to keep them
- * running.
+ * A service that manages the active subscriptions periodically sending requests
+ * to keep them running.
*/
export class FirebaseSubscriptionService {
@@ -84,6 +84,33 @@ export class FirebaseSubscriptionService {
}
}
+ /**
+ * Immediately cancels all active subscriptions previously created through this service.
+ */
+ cancelAllSubscriptions() {
+ const activeSubscriptions = this._subscriptions.filter(s => !s.closed);
+ if (activeSubscriptions.length > 0) {
+ const subscriptionMessages = activeSubscriptions.map(s => s.internal())
+ this._endpoint.cancelAll(subscriptionMessages);
+ activeSubscriptions.forEach(s => {
+ s.unsubscribe() /* Calling RxJS's `unsubscribe` to stop propagating the updates. */
+ this._removeSubscription(s)
+ })
+ }
+ }
+
+ /**
+ * Immediately cancels the given subscription, including cancelling it on the server-side.
+ *
+ * @param {SubscriptionObject} subscription the subscription to cancel
+ */
+ cancelSubscription(subscription) {
+ this._endpoint.cancelSubscription(subscription)
+ .then(() => {
+ this._removeSubscription(subscription)
+ });
+ }
+
/**
* Indicates whether this service is running keeping up subscriptions.
*
@@ -150,10 +177,22 @@ export class FirebaseSubscriptionService {
* Removes the provided subscription from subscriptions list, which stops any attempts
* to update it. In case no more subscriptions are left, stops this service.
*
+ * In case the passed subscription is not known to this service, does nothing.
+ *
+ * @param subscription a subscription to cancel;
+ * this method accepts values of both `SpineSubscription`
+ * and Proto `Subscription` types,
+ * and operates based on the subscription ID
* @private
*/
_removeSubscription(subscription) {
- const index = this._subscriptions.indexOf(subscription);
+ let id;
+ if (typeof subscription.id === 'function') {
+ id = subscription.id();
+ } else {
+ id = subscription.getId().getValue();
+ }
+ const index = this._subscriptions.findIndex(item => item.id() === id);
this._subscriptions.splice(index, 1);
if (this._subscriptions.length === 0) {
diff --git a/client-js/main/client/subscribing-client.js b/client-js/main/client/subscribing-client.js
index 62d5856d..6ad02d71 100644
--- a/client-js/main/client/subscribing-client.js
+++ b/client-js/main/client/subscribing-client.js
@@ -85,6 +85,13 @@ export class SubscribingClient {
throw new Error('Not implemented in abstract base.');
}
+ /**
+ * Cancels all subscriptions, which were created through this instance of subscribing client.
+ */
+ cancelAllSubscriptions() {
+ throw new Error('Not implemented in abstract base.');
+ }
+
/**
* Returns a new topic factory instance which can be further used for the `Topic` creation.
*
@@ -123,4 +130,13 @@ export class NoOpSubscribingClient extends SubscribingClient {
subscribeToEvents(topic) {
throw new Error(SUBSCRIPTIONS_NOT_SUPPORTED);
}
+
+ /**
+ * Always throws an error.
+ *
+ * @override
+ */
+ cancelAllSubscriptions() {
+ throw new Error(SUBSCRIPTIONS_NOT_SUPPORTED);
+ }
}
diff --git a/client-js/package.json b/client-js/package.json
index 8d5ff85f..73446ec2 100644
--- a/client-js/package.json
+++ b/client-js/package.json
@@ -1,6 +1,6 @@
{
"name": "spine-web",
- "version": "1.9.0-SNAPSHOT.9",
+ "version": "1.9.0-SNAPSHOT.10",
"license": "Apache-2.0",
"description": "A JS client for interacting with Spine applications.",
"homepage": "https://spine.io",
diff --git a/firebase-web/src/main/java/io/spine/web/firebase/FirebaseCredentials.java b/firebase-web/src/main/java/io/spine/web/firebase/FirebaseCredentials.java
index bbec849e..c00292a7 100644
--- a/firebase-web/src/main/java/io/spine/web/firebase/FirebaseCredentials.java
+++ b/firebase-web/src/main/java/io/spine/web/firebase/FirebaseCredentials.java
@@ -50,8 +50,7 @@
*
*
See Firebase REST docs.
*/
-@SuppressWarnings("deprecation")
-// Use deprecated `GoogleCredential` to retain backward compatibility.
+@SuppressWarnings("deprecation" /*`GoogleCredential` is used to retain backward compatibility.*/)
public final class FirebaseCredentials implements HttpRequestInitializer {
private static final String AUTH_DATABASE = "https://www.googleapis.com/auth/firebase.database";
diff --git a/firebase-web/src/main/java/io/spine/web/firebase/subscription/FirebaseSubscriptionBridge.java b/firebase-web/src/main/java/io/spine/web/firebase/subscription/FirebaseSubscriptionBridge.java
index 56eaac34..83f06c12 100644
--- a/firebase-web/src/main/java/io/spine/web/firebase/subscription/FirebaseSubscriptionBridge.java
+++ b/firebase-web/src/main/java/io/spine/web/firebase/subscription/FirebaseSubscriptionBridge.java
@@ -209,13 +209,18 @@ public Builder setSubscriptionLifeSpan(Duration subscriptionLifeSpan) {
/**
* Creates a new instance of {@code FirebaseQueryBridge}.
*
+ *
Mandatory fields are {@link #setFirebaseClient(FirebaseClient) firebaseClient}
+ * and {@link #setSubscriptionService(SubscriptionServiceImplBase) subscriptionService}.
+ *
* @return new instance of {@code FirebaseQueryBridge}
*/
public FirebaseSubscriptionBridge build() {
checkState(firebaseClient != null,
- "Firebase database client is not set to FirebaseSubscriptionBridge.");
+ "Mandatory Firebase database client" +
+ " is not specified for `FirebaseSubscriptionBridge`.");
checkState(subscriptionService != null,
- "Subscription Service is not set to FirebaseSubscriptionBridge.");
+ "Mandatory Subscription Service is not specified" +
+ " for `FirebaseSubscriptionBridge`.");
return new FirebaseSubscriptionBridge(this);
}
}
diff --git a/firebase-web/src/main/java/io/spine/web/firebase/subscription/HealthLog.java b/firebase-web/src/main/java/io/spine/web/firebase/subscription/HealthLog.java
index 458a33b8..b8d1129f 100644
--- a/firebase-web/src/main/java/io/spine/web/firebase/subscription/HealthLog.java
+++ b/firebase-web/src/main/java/io/spine/web/firebase/subscription/HealthLog.java
@@ -47,7 +47,7 @@
*
To understand whether a client is still listening to the {@code Topic} updates, she
* periodically sends a {@link FirebaseSubscriptionBridge#keepUp(Subscription) keepUp(Subscription)}
* request. The server records the timestamps of these requests in this log and counts the client
- * alive, as long as the {@linkplain #withTimeout(Duration)} configured} timeout does not pass
+ * alive, as long as the {@linkplain #withTimeout(Duration) configured} timeout does not pass
* since the last request.
*/
final class HealthLog {
@@ -113,4 +113,15 @@ boolean isStale(Topic topic) {
Duration elapsed = between(lastUpdate, now);
return compare(elapsed, expirationTimeout) > 0;
}
+
+ /**
+ * Removes the given {@code Topic} from this health log.
+ *
+ *
In case this topic is not known to this registry, does nothing, allowing
+ * to safely clear the health log from stale topics potentially residing in storage
+ * on either client- or server-sides.
+ */
+ void remove(Topic topic) {
+ updateTimes.remove(topic.getId());
+ }
}
diff --git a/firebase-web/src/main/java/io/spine/web/firebase/subscription/SubscriptionRepository.java b/firebase-web/src/main/java/io/spine/web/firebase/subscription/SubscriptionRepository.java
index 32eee4e9..48c1c471 100644
--- a/firebase-web/src/main/java/io/spine/web/firebase/subscription/SubscriptionRepository.java
+++ b/firebase-web/src/main/java/io/spine/web/firebase/subscription/SubscriptionRepository.java
@@ -34,7 +34,6 @@
import com.google.protobuf.Duration;
import io.spine.client.Subscription;
import io.spine.client.Topic;
-import io.spine.json.Json;
import io.spine.web.firebase.FirebaseClient;
import io.spine.web.firebase.NodePath;
import io.spine.web.firebase.NodePaths;
@@ -44,6 +43,7 @@
import java.util.Optional;
import static com.google.common.base.Preconditions.checkNotNull;
+import static io.spine.json.Json.fromJson;
import static io.spine.util.Exceptions.illegalStateWithCauseOf;
import static io.spine.web.firebase.subscription.LazyRepository.lazy;
@@ -81,7 +81,7 @@ final class SubscriptionRepository {
/**
* Fetches all the existing subscriptions from the Firebase and activates them.
*
- *
After calling this method, all the new subscriptions are automatically activates on this
+ *
After calling this method, all the new subscriptions are automatically activated on this
* server instance.
*/
void subscribeToAll() {
@@ -152,6 +152,7 @@ private void delete(Topic topic) {
checkNotNull(topic);
NodePath path = pathForSubscription(topic);
firebase.delete(path);
+ healthLog.remove(topic);
}
private static NodePath pathForSubscription(Topic topic) {
@@ -200,7 +201,7 @@ private static String asJson(DataSnapshot snapshot) {
}
private Topic loadTopic(String json) {
- Topic topic = Json.fromJson(json, Topic.class);
+ Topic topic = fromJson(json, Topic.class);
repository.healthLog.put(topic);
return topic;
}
@@ -216,7 +217,31 @@ private void deleteOrActivate(Topic topic) {
@Override
public void onChildRemoved(DataSnapshot snapshot) {
- // NOP.
+ String json = asJson(snapshot);
+ Optional topic = parseTopic(json);
+ topic.ifPresent(t -> {
+ HealthLog healthLog = repository.healthLog;
+ if (healthLog.isKnown(t)) {
+ repository.delete(t);
+ }
+ });
+ }
+
+ /**
+ * Safely parses the {@code Topic} from the passed JSON.
+ *
+ * @param json
+ * JSON to parse
+ * @return parsed {@code Topic} wrapped as {@code Optional},
+ * or {@code Optional.empty()} if there was a parsing error
+ */
+ private static Optional parseTopic(String json) {
+ try {
+ Topic topic = fromJson(json, Topic.class);
+ return Optional.of(topic);
+ } catch (RuntimeException e) {
+ return Optional.empty();
+ }
}
@Override
diff --git a/integration-tests/js-tests/package.json b/integration-tests/js-tests/package.json
index 8143d89e..b43dd0c1 100644
--- a/integration-tests/js-tests/package.json
+++ b/integration-tests/js-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "client-js-tests",
- "version": "1.9.0-SNAPSHOT.9",
+ "version": "1.9.0-SNAPSHOT.10",
"license": "Apache-2.0",
"description": "Tests of a `spine-web` JS library against the Spine-based application.",
"scripts": {
diff --git a/integration-tests/js-tests/test/firebase-client/subscribe-test.js b/integration-tests/js-tests/test/firebase-client/subscribe-test.js
index eb566942..82f2fd20 100644
--- a/integration-tests/js-tests/test/firebase-client/subscribe-test.js
+++ b/integration-tests/js-tests/test/firebase-client/subscribe-test.js
@@ -221,6 +221,120 @@ describe('FirebaseClient subscription', function () {
.catch(fail(done));
});
+ /**
+ * This test creates two tasks, one after another, and then creates two subscriptions
+ * to changes of the created tasks. Prior to sending the renaming commands,
+ * the test code cancels all known subscriptions, and verifies that no `itemChanged`
+ * promises are resolved when the renaming commands are sent.
+ */
+ it('may be cancelled along with all other subscriptions in a bulk', done => {
+ const prefix = 'spine-web-test-bulk-cancellation';
+ const createFirst = TestEnvironment.createTaskCommand({
+ withPrefix: prefix,
+ named: "first task"
+ });
+ const createSecond = TestEnvironment.createTaskCommand({
+ withPrefix: prefix,
+ named: "second task"
+ });
+
+ const firstId = createFirst.getId();
+ const secondId = createSecond.getId();
+
+ function createFirstTask() {
+ client.command(createFirst)
+ .onOk(() => console.log(`1st task '${firstId.getValue()}' created.`))
+ .onError(fail(done, 'Unexpected error while creating the 1st task.'))
+ .onImmediateRejection(fail(done,
+ 'Unexpected rejection while creating the 1st task.'))
+ .post();
+ }
+
+ function createSecondTask() {
+ client.command(createSecond)
+ .onOk(() => console.log(`2nd task '${secondId.getValue()}' created.`))
+ .onError(fail(done, 'Unexpected error while creating the 2nd task.'))
+ .onImmediateRejection(fail(done,
+ 'Unexpected rejection while creating the 2nd task.'))
+ .post();
+ }
+
+ function renameFirstTask() {
+ const renameFirst = TestEnvironment.renameTaskCommand({
+ withId: firstId.getValue(),
+ to: "first updated"
+ });
+
+ client.command(renameFirst)
+ .onOk(() => console.log(`1st task '${firstId.getValue()}' was renamed.`))
+ .onError(fail(done, 'Unexpected error while renaming the 1st task.'))
+ .onImmediateRejection(fail(done,
+ 'Unexpected rejection while renaming the 1st task.'))
+ .post();
+ }
+
+ function renameSecondTask() {
+ const renameSecond = TestEnvironment.renameTaskCommand({
+ withId: secondId.getValue(),
+ to: "second updated"
+ });
+ client.command(renameSecond)
+ .onOk(() => console.log(`2nd task '${secondId.getValue()}' was renamed.`))
+ .onError(fail(done, 'Unexpected error while renaming the 2nd task.'))
+ .onImmediateRejection(fail(done,
+ 'Unexpected rejection while renaming the 2nd task.'))
+ .post();
+ }
+
+ client.subscribeTo(Task)
+ .byId(firstId)
+ .post()
+ .then(({itemAdded, itemChanged, itemRemoved, unsubscribe}) => {
+ itemAdded.subscribe({
+ next: () => {
+ createSecondTask();
+ }
+ });
+ itemChanged.subscribe({
+ next: () => { fail(done, 'Unexpected 1st task change received, ' +
+ 'while the corresponding subscription had to be cancelled by now.'); }
+ });
+
+ client.subscribeTo(Task)
+ .byId(secondId)
+ .post()
+ .then(({itemAdded, itemChanged, itemRemoved, unsubscribe}) => {
+ itemChanged.subscribe({
+ next: () => { fail(done, 'Unexpected 2nd task change received, ' +
+ 'while the corresponding subscription had to be cancelled by now.'); }
+ });
+ itemAdded.subscribe({
+ next: async () => {
+ client.cancelAllSubscriptions();
+ renameFirstTask();
+ renameSecondTask();
+
+ await oneHundredMs();
+
+ done();
+ }
+ });
+ })
+ })
+ .catch(fail(done));
+ createFirstTask();
+ });
+
+ /**
+ * Returns a promise to be resolved after 100 ms.
+ *
+ * @returns {Promise}
+ */
+ function oneHundredMs() {
+ return new Promise(resolve =>
+ setTimeout(() => resolve(), 100))
+ }
+
it('is notified when the entity no longer matches the subscription criteria', done => {
const initialTaskName = 'Initial task name';
const nameAfterRenamed = 'Renamed task';
@@ -425,15 +539,13 @@ describe('FirebaseClient subscription', function () {
});
});
- it('and canceled on the next keep up interval if unsubscribed', done => {
+ it('and cancel subscription immediately if unsubscribed', done => {
const cancelEndpoint = cancelEndpointSpy();
-
subscribeToAllTasks().then(async ({itemAdded, itemChanged, itemRemoved, unsubscribe}) => {
assert.ok(cancelEndpoint.notCalled);
unsubscribe();
- await nextInterval();
assert.ok(cancelEndpoint.calledOnce);
- const subscriptionMessage = cancelEndpoint.getCall(0).args[0][0];
+ const subscriptionMessage = cancelEndpoint.getCall(0).args[0];
checkAllTasks(subscriptionMessage);
done();
});
@@ -450,10 +562,10 @@ describe('FirebaseClient subscription', function () {
assert.ok(keepUpEndpoint.calledOnce);
assert.ok(cancelEndpoint.notCalled);
unsubscribe();
+ assert.ok(cancelEndpoint.calledOnce);
await nextInterval();
assert.ok(keepUpEndpoint.calledOnce);
- assert.ok(cancelEndpoint.calledOnce);
await nextInterval();
assert.ok(keepUpEndpoint.calledOnce);
@@ -504,7 +616,7 @@ describe('FirebaseClient subscription', function () {
function cancelEndpointSpy() {
const httpEndpoint = client._subscribing._endpoint;
- return sandbox.spy(httpEndpoint, 'cancelAll');
+ return sandbox.spy(httpEndpoint, 'cancelSubscription');
}
/**
diff --git a/integration-tests/js-tests/test/test-helpers.js b/integration-tests/js-tests/test/test-helpers.js
index f92e9e2d..803a5bef 100644
--- a/integration-tests/js-tests/test/test-helpers.js
+++ b/integration-tests/js-tests/test/test-helpers.js
@@ -139,7 +139,7 @@ function arraysEqualDeep(arr1, arr2, compare) {
* @param {EntitySubscriptionObject} subscription a subscription to retrieve values
* @param {(o1: T, o2: T) => boolean} compare a function that compares objects of `T` type;
* returns `true` if objects are considered equal, `false` otherwise
- * @return {Observable} an observable that emits a list of values, composed from the given
+ * @return {Observable} an observable that emits a list of values, composed of the given
* subscription object
*
* @template a class of a subscription target entities
diff --git a/license-report.md b/license-report.md
index 324379a7..ab2ceed5 100644
--- a/license-report.md
+++ b/license-report.md
@@ -1,6 +1,6 @@
-# Dependencies of `io.spine:spine-client-js:1.9.0-SNAPSHOT.9`
+# Dependencies of `io.spine:spine-client-js:1.9.0-SNAPSHOT.10`
## Runtime
1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2
@@ -368,10 +368,10 @@
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Thu Jan 12 13:42:53 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Fri Jan 27 17:43:52 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-#NPM dependencies of `spine-web@1.9.0-SNAPSHOT.9`
+#NPM dependencies of `spine-web@1.9.0-SNAPSHOT.10`
## `Production` dependencies:
@@ -405,7 +405,7 @@ This report was generated on **Thu Jan 12 13:42:53 WET 2023** using [Gradle-Lice
1. **rxjs@6.5.5**
* Licenses: Apache-2.0
* Repository: [https://github.com/reactivex/rxjs](https://github.com/reactivex/rxjs)
-1. **spine-web@1.9.0-SNAPSHOT.9**
+1. **spine-web@1.9.0-SNAPSHOT.10**
* Licenses: Apache-2.0
* Repository: [https://github.com/SpineEventEngine/web](https://github.com/SpineEventEngine/web)
1. **tr46@0.0.3**
@@ -1958,7 +1958,7 @@ This report was generated on **Thu Jan 12 13:42:53 WET 2023** using [Gradle-Lice
1. **spdx-satisfies@4.0.1**
* Licenses: MIT
* Repository: [https://github.com/kemitchell/spdx-satisfies.js](https://github.com/kemitchell/spdx-satisfies.js)
-1. **spine-web@1.9.0-SNAPSHOT.9**
+1. **spine-web@1.9.0-SNAPSHOT.10**
* Licenses: Apache-2.0
* Repository: [https://github.com/SpineEventEngine/web](https://github.com/SpineEventEngine/web)
1. **sprintf-js@1.0.3**
@@ -2140,12 +2140,12 @@ This report was generated on **Thu Jan 12 13:42:53 WET 2023** using [Gradle-Lice
* Repository: [https://github.com/sindresorhus/yocto-queue](https://github.com/sindresorhus/yocto-queue)
-This report was generated on **Thu Jan 12 2023 13:42:54 GMT+0000 (Western European Standard Time)** using [NPM License Checker](https://github.com/davglass/license-checker) library.
+This report was generated on **Fri Jan 27 2023 17:43:53 GMT+0000 (Western European Standard Time)** using [NPM License Checker](https://github.com/davglass/license-checker) library.
-# Dependencies of `io.spine.gcloud:spine-firebase-web:1.9.0-SNAPSHOT.9`
+# Dependencies of `io.spine.gcloud:spine-firebase-web:1.9.0-SNAPSHOT.10`
## Runtime
1. **Group:** com.fasterxml.jackson.core **Name:** jackson-annotations **Version:** 2.9.10
@@ -2932,12 +2932,12 @@ This report was generated on **Thu Jan 12 2023 13:42:54 GMT+0000 (Western Europe
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Thu Jan 12 13:42:59 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Fri Jan 27 17:43:59 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-js-tests:1.9.0-SNAPSHOT.9`
+# Dependencies of `io.spine:spine-js-tests:1.9.0-SNAPSHOT.10`
## Runtime
1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2
@@ -3327,12 +3327,12 @@ This report was generated on **Thu Jan 12 13:42:59 WET 2023** using [Gradle-Lice
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Thu Jan 12 13:43:04 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Fri Jan 27 17:44:03 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-test-app:1.9.0-SNAPSHOT.9`
+# Dependencies of `io.spine:spine-test-app:1.9.0-SNAPSHOT.10`
## Runtime
1. **Group:** com.fasterxml.jackson.core **Name:** jackson-annotations **Version:** 2.9.10
@@ -4906,12 +4906,12 @@ This report was generated on **Thu Jan 12 13:43:04 WET 2023** using [Gradle-Lice
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Thu Jan 12 13:43:06 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Fri Jan 27 17:44:05 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-testutil-web:1.9.0-SNAPSHOT.9`
+# Dependencies of `io.spine:spine-testutil-web:1.9.0-SNAPSHOT.10`
## Runtime
1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4
@@ -5370,12 +5370,12 @@ This report was generated on **Thu Jan 12 13:43:06 WET 2023** using [Gradle-Lice
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Thu Jan 12 13:43:08 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Fri Jan 27 17:44:07 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine:spine-web:1.9.0-SNAPSHOT.9`
+# Dependencies of `io.spine:spine-web:1.9.0-SNAPSHOT.10`
## Runtime
1. **Group:** com.google.android **Name:** annotations **Version:** 4.1.1.4
@@ -5873,4 +5873,4 @@ This report was generated on **Thu Jan 12 13:43:08 WET 2023** using [Gradle-Lice
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Thu Jan 12 13:43:11 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
\ No newline at end of file
+This report was generated on **Fri Jan 27 17:44:10 WET 2023** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 40a93f8e..52255002 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@ all modules and does not describe the project structure per-subproject.
io.spine
spine-web
-1.9.0-SNAPSHOT.9
+1.9.0-SNAPSHOT.10
2015
diff --git a/version.gradle.kts b/version.gradle.kts
index b476a09a..211323c5 100644
--- a/version.gradle.kts
+++ b/version.gradle.kts
@@ -29,5 +29,5 @@ val spineTimeVersion: String by extra("1.9.0-SNAPSHOT.5")
val spineCoreVersion: String by extra("1.9.0-SNAPSHOT.6")
val spineVersion: String by extra(spineCoreVersion)
-val versionToPublish: String by extra("1.9.0-SNAPSHOT.9")
+val versionToPublish: String by extra("1.9.0-SNAPSHOT.10")
val versionToPublishJs: String by extra(versionToPublish)
diff --git a/web/src/main/java/io/spine/web/subscription/BlockingSubscriptionService.java b/web/src/main/java/io/spine/web/subscription/BlockingSubscriptionService.java
index 9eb8df91..57da98f6 100644
--- a/web/src/main/java/io/spine/web/subscription/BlockingSubscriptionService.java
+++ b/web/src/main/java/io/spine/web/subscription/BlockingSubscriptionService.java
@@ -94,8 +94,8 @@ private void checkObserver(MemoizingObserver observer) {
throw illegalStateWithCauseOf(error);
} else {
checkState(observer.isCompleted(),
- "Provided SubscriptionService implementation (`%s`) must complete `%s`" +
- " procedure at once.",
+ "Provided `SubscriptionService` implementation (`%s`)" +
+ " must complete `%s` procedure at once.",
subscriptionService,
SUBSCRIBE_METHOD_NAME);
}