@cratis/arc 20.60.0 → 20.60.2
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.
- package/dist/cjs/queries/HubConnectionKeepAlive.js +24 -4
- package/dist/cjs/queries/HubConnectionKeepAlive.js.map +1 -1
- package/dist/cjs/queries/NullObservableQueryConnection.js +7 -2
- package/dist/cjs/queries/NullObservableQueryConnection.js.map +1 -1
- package/dist/cjs/queries/ObservableQueryFor.js +11 -1
- package/dist/cjs/queries/ObservableQueryFor.js.map +1 -1
- package/dist/cjs/queries/QueryFor.js +1 -21
- package/dist/cjs/queries/QueryFor.js.map +1 -1
- package/dist/cjs/queries/QueryInstanceCache.js +35 -12
- package/dist/cjs/queries/QueryInstanceCache.js.map +1 -1
- package/dist/cjs/queries/QueryResult.js +30 -0
- package/dist/cjs/queries/QueryResult.js.map +1 -1
- package/dist/cjs/queries/ServerSentEventHubConnection.js +38 -10
- package/dist/cjs/queries/ServerSentEventHubConnection.js.map +1 -1
- package/dist/cjs/queries/WebSocketHubConnection.js.map +1 -1
- package/dist/cjs/queries/index.js +2 -0
- package/dist/cjs/queries/index.js.map +1 -1
- package/dist/cjs/queries/reconcileQueryData.js +84 -0
- package/dist/cjs/queries/reconcileQueryData.js.map +1 -0
- package/dist/esm/queries/HubConnectionKeepAlive.d.ts +5 -3
- package/dist/esm/queries/HubConnectionKeepAlive.d.ts.map +1 -1
- package/dist/esm/queries/HubConnectionKeepAlive.js +24 -4
- package/dist/esm/queries/HubConnectionKeepAlive.js.map +1 -1
- package/dist/esm/queries/NullObservableQueryConnection.d.ts +3 -1
- package/dist/esm/queries/NullObservableQueryConnection.d.ts.map +1 -1
- package/dist/esm/queries/NullObservableQueryConnection.js +7 -2
- package/dist/esm/queries/NullObservableQueryConnection.js.map +1 -1
- package/dist/esm/queries/ObservableQueryFor.d.ts +2 -0
- package/dist/esm/queries/ObservableQueryFor.d.ts.map +1 -1
- package/dist/esm/queries/ObservableQueryFor.js +11 -1
- package/dist/esm/queries/ObservableQueryFor.js.map +1 -1
- package/dist/esm/queries/QueryFor.d.ts.map +1 -1
- package/dist/esm/queries/QueryFor.js +1 -21
- package/dist/esm/queries/QueryFor.js.map +1 -1
- package/dist/esm/queries/QueryInstanceCache.d.ts +1 -0
- package/dist/esm/queries/QueryInstanceCache.d.ts.map +1 -1
- package/dist/esm/queries/QueryInstanceCache.js +35 -12
- package/dist/esm/queries/QueryInstanceCache.js.map +1 -1
- package/dist/esm/queries/QueryResult.d.ts +5 -0
- package/dist/esm/queries/QueryResult.d.ts.map +1 -1
- package/dist/esm/queries/QueryResult.js +30 -0
- package/dist/esm/queries/QueryResult.js.map +1 -1
- package/dist/esm/queries/ServerSentEventHubConnection.d.ts +1 -0
- package/dist/esm/queries/ServerSentEventHubConnection.d.ts.map +1 -1
- package/dist/esm/queries/ServerSentEventHubConnection.js +38 -10
- package/dist/esm/queries/ServerSentEventHubConnection.js.map +1 -1
- package/dist/esm/queries/WebSocketHubConnection.d.ts +1 -0
- package/dist/esm/queries/WebSocketHubConnection.d.ts.map +1 -1
- package/dist/esm/queries/WebSocketHubConnection.js.map +1 -1
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_running.d.ts +2 -0
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_running.d.ts.map +1 -0
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_running.js +37 -0
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_running.js.map +1 -0
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_stopped.d.ts +2 -0
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_stopped.d.ts.map +1 -0
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_stopped.js +22 -0
- package/dist/esm/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_stopped.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.d.ts +23 -0
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.js +28 -0
- package/dist/esm/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.js +21 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.js +18 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.js +21 -0
- package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.js.map +1 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_partially_changed_result.d.ts +2 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_partially_changed_result.d.ts.map +1 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_partially_changed_result.js +32 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_partially_changed_result.js.map +1 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_re_delivered_identical_result.d.ts +2 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_re_delivered_identical_result.d.ts.map +1 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_re_delivered_identical_result.js +22 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_setting_last_result/with_a_re_delivered_identical_result.js.map +1 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_advertises_a_longer_keep_alive_interval/does_not_reconnect_at_the_assumed_threshold.d.ts +2 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_advertises_a_longer_keep_alive_interval/does_not_reconnect_at_the_assumed_threshold.d.ts.map +1 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_advertises_a_longer_keep_alive_interval/does_not_reconnect_at_the_assumed_threshold.js +39 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_advertises_a_longer_keep_alive_interval/does_not_reconnect_at_the_assumed_threshold.js.map +1 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_reports_keep_alive_disabled/never_reconnects_from_idle.d.ts +2 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_reports_keep_alive_disabled/never_reconnects_from_idle.d.ts.map +1 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_reports_keep_alive_disabled/never_reconnects_from_idle.js +28 -0
- package/dist/esm/queries/for_ServerSentEventHubConnection/when_server_reports_keep_alive_disabled/never_reconnects_from_idle.js.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_items_are_added_or_removed/reuses_the_surviving_items.d.ts +2 -0
- package/dist/esm/queries/for_reconcileQueryData/when_items_are_added_or_removed/reuses_the_surviving_items.d.ts.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_items_are_added_or_removed/reuses_the_surviving_items.js +36 -0
- package/dist/esm/queries/for_reconcileQueryData/when_items_are_added_or_removed/reuses_the_surviving_items.js.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_one_item_changed/only_the_changed_item_gets_a_new_reference.d.ts +2 -0
- package/dist/esm/queries/for_reconcileQueryData/when_one_item_changed/only_the_changed_item_gets_a_new_reference.d.ts.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_one_item_changed/only_the_changed_item_gets_a_new_reference.js +20 -0
- package/dist/esm/queries/for_reconcileQueryData/when_one_item_changed/only_the_changed_item_gets_a_new_reference.js.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_an_unchanged_collection/preserves_all_references.d.ts +2 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_an_unchanged_collection/preserves_all_references.d.ts.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_an_unchanged_collection/preserves_all_references.js +15 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_an_unchanged_collection/preserves_all_references.js.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_non_collection_payloads/compares_by_value.d.ts +2 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_non_collection_payloads/compares_by_value.d.ts.map +1 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_non_collection_payloads/compares_by_value.js +20 -0
- package/dist/esm/queries/for_reconcileQueryData/when_reconciling_non_collection_payloads/compares_by_value.js.map +1 -0
- package/dist/esm/queries/index.d.ts +1 -0
- package/dist/esm/queries/index.d.ts.map +1 -1
- package/dist/esm/queries/index.js +1 -0
- package/dist/esm/queries/index.js.map +1 -1
- package/dist/esm/queries/reconcileQueryData.d.ts +2 -0
- package/dist/esm/queries/reconcileQueryData.d.ts.map +1 -0
- package/dist/esm/queries/reconcileQueryData.js +82 -0
- package/dist/esm/queries/reconcileQueryData.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/queries/HubConnectionKeepAlive.ts +30 -3
- package/queries/NullObservableQueryConnection.ts +5 -2
- package/queries/ObservableQueryFor.ts +15 -1
- package/queries/QueryFor.ts +1 -21
- package/queries/QueryInstanceCache.ts +61 -17
- package/queries/QueryResult.ts +34 -0
- package/queries/ServerSentEventHubConnection.ts +45 -10
- package/queries/WebSocketHubConnection.ts +6 -0
- package/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_running.ts +50 -0
- package/queries/for_HubConnectionKeepAlive/when_reconfiguring/while_stopped.ts +28 -0
- package/queries/for_ObservableQueryFor/given/an_observable_query_with_validator.ts +41 -0
- package/queries/for_ObservableQueryFor/when_performing/with_client_validation_failing.ts +30 -0
- package/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_failing.ts +26 -0
- package/queries/for_ObservableQueryFor/when_subscribing/with_client_validation_passing.ts +32 -0
- package/queries/for_QueryInstanceCache/when_setting_last_result/with_a_partially_changed_result.ts +43 -0
- package/queries/for_QueryInstanceCache/when_setting_last_result/with_a_re_delivered_identical_result.ts +34 -0
- package/queries/for_ServerSentEventHubConnection/when_keep_alive_is_idle/triggers_reconnect.ts +4 -4
- package/queries/for_ServerSentEventHubConnection/when_server_advertises_a_longer_keep_alive_interval/does_not_reconnect_at_the_assumed_threshold.ts +64 -0
- package/queries/for_ServerSentEventHubConnection/when_server_reports_keep_alive_disabled/never_reconnects_from_idle.ts +50 -0
- package/queries/for_reconcileQueryData/when_items_are_added_or_removed/reuses_the_surviving_items.ts +46 -0
- package/queries/for_reconcileQueryData/when_one_item_changed/only_the_changed_item_gets_a_new_reference.ts +26 -0
- package/queries/for_reconcileQueryData/when_reconciling_an_unchanged_collection/preserves_all_references.ts +22 -0
- package/queries/for_reconcileQueryData/when_reconciling_non_collection_payloads/compares_by_value.ts +31 -0
- package/queries/index.ts +1 -0
- package/queries/reconcileQueryData.ts +98 -0
package/package.json
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
export class HubConnectionKeepAlive {
|
|
23
23
|
private _lastActivityTime = Date.now();
|
|
24
24
|
private _timer?: ReturnType<typeof setInterval>;
|
|
25
|
-
private
|
|
25
|
+
private _intervalMs: number;
|
|
26
|
+
private _idleThresholdMs: number;
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Initializes a new instance of {@link HubConnectionKeepAlive}.
|
|
@@ -35,11 +36,37 @@ export class HubConnectionKeepAlive {
|
|
|
35
36
|
* cadence to account for network latency and timer jitter.
|
|
36
37
|
*/
|
|
37
38
|
constructor(
|
|
38
|
-
|
|
39
|
+
intervalMs: number,
|
|
39
40
|
private readonly _onIdle: () => void,
|
|
40
41
|
idleThresholdMs?: number,
|
|
41
42
|
) {
|
|
42
|
-
this.
|
|
43
|
+
this._intervalMs = intervalMs;
|
|
44
|
+
this._idleThresholdMs = idleThresholdMs ?? intervalMs;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets how long (in milliseconds) without activity before the connection is considered idle.
|
|
49
|
+
*/
|
|
50
|
+
get idleThresholdMs(): number {
|
|
51
|
+
return this._idleThresholdMs;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Change the check interval and idle threshold, restarting the timer when it is already running.
|
|
56
|
+
*
|
|
57
|
+
* Used when the peer advertises its actual keep-alive cadence after connecting, so the client
|
|
58
|
+
* stops relying on an assumed default that may not match the server's configuration.
|
|
59
|
+
* @param {number} intervalMs How often (in milliseconds) to check for idle connections.
|
|
60
|
+
* @param {number} idleThresholdMs How long (in milliseconds) without activity before the connection is considered idle.
|
|
61
|
+
*/
|
|
62
|
+
reconfigure(intervalMs: number, idleThresholdMs: number): void {
|
|
63
|
+
const wasRunning = this._timer !== undefined;
|
|
64
|
+
this._intervalMs = intervalMs;
|
|
65
|
+
this._idleThresholdMs = idleThresholdMs;
|
|
66
|
+
|
|
67
|
+
if (wasRunning) {
|
|
68
|
+
this.start();
|
|
69
|
+
}
|
|
43
70
|
}
|
|
44
71
|
|
|
45
72
|
/**
|
|
@@ -13,8 +13,11 @@ export class NullObservableQueryConnection<TDataType> implements IObservableQuer
|
|
|
13
13
|
/**
|
|
14
14
|
* Initializes a new instance of the {@link NullObservableQueryConnection} class.
|
|
15
15
|
* @param {TDataType} defaultValue The default value to serve.
|
|
16
|
+
* @param {QueryResult<TDataType>} result Optional result to serve instead of an empty one. Supply it when the
|
|
17
|
+
* subscription failed for a reason the subscriber must be able to see — client-side validation in particular,
|
|
18
|
+
* which would otherwise be indistinguishable from a query that simply has no data yet.
|
|
16
19
|
*/
|
|
17
|
-
constructor(readonly defaultValue: TDataType) {
|
|
20
|
+
constructor(readonly defaultValue: TDataType, readonly result?: QueryResult<TDataType>) {
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/** @inheritdoc */
|
|
@@ -29,7 +32,7 @@ export class NullObservableQueryConnection<TDataType> implements IObservableQuer
|
|
|
29
32
|
|
|
30
33
|
/** @inheritdoc */
|
|
31
34
|
connect(dataReceived: DataReceived<TDataType>) {
|
|
32
|
-
dataReceived(QueryResult.empty(this.defaultValue));
|
|
35
|
+
dataReceived(this.result ?? QueryResult.empty(this.defaultValue));
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** @inheritdoc */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { IObservableQueryFor, OnNextResult } from './IObservableQueryFor';
|
|
5
5
|
import { ObservableQuerySubscription } from './ObservableQuerySubscription';
|
|
6
6
|
import { ValidateRequestArguments } from './ValidateRequestArguments';
|
|
7
|
+
import { QueryValidator } from './QueryValidator';
|
|
7
8
|
import { IObservableQueryConnection } from './IObservableQueryConnection';
|
|
8
9
|
import { NullObservableQueryConnection } from './NullObservableQueryConnection';
|
|
9
10
|
import { createObservableQueryConnection } from './ObservableQueryConnectionFactory';
|
|
@@ -38,6 +39,7 @@ export abstract class ObservableQueryFor<TDataType, TParameters = object> implem
|
|
|
38
39
|
abstract readonly route: string;
|
|
39
40
|
abstract readonly defaultValue: TDataType;
|
|
40
41
|
readonly roles: string[] = [];
|
|
42
|
+
readonly validation?: QueryValidator<any>;
|
|
41
43
|
/** Backend fully-qualified query name used when subscribing via the SSE hub. Overridden in generated proxies. */
|
|
42
44
|
readonly queryName?: string;
|
|
43
45
|
abstract readonly parameterDescriptors: ParameterDescriptor[];
|
|
@@ -97,7 +99,14 @@ export abstract class ObservableQueryFor<TDataType, TParameters = object> implem
|
|
|
97
99
|
this._connection.disconnect();
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
const clientValidationErrors = this.validation?.validate(args as object || {}) || [];
|
|
103
|
+
if (clientValidationErrors.length > 0) {
|
|
104
|
+
// Serve the failure through the connection rather than throwing, so a subscriber sees an invalid result
|
|
105
|
+
// on its normal callback path instead of the empty result an unestablished connection would emit.
|
|
106
|
+
this._connection = new NullObservableQueryConnection(
|
|
107
|
+
this.defaultValue,
|
|
108
|
+
QueryResult.validationFailed(clientValidationErrors, this));
|
|
109
|
+
} else if (!this.validateArguments(args)) {
|
|
101
110
|
this._connection = new NullObservableQueryConnection(this.defaultValue);
|
|
102
111
|
} else {
|
|
103
112
|
this._connection = createObservableQueryConnection({
|
|
@@ -143,6 +152,11 @@ export abstract class ObservableQueryFor<TDataType, TParameters = object> implem
|
|
|
143
152
|
async perform(args?: TParameters): Promise<QueryResult<TDataType>> {
|
|
144
153
|
const noSuccess = { ...QueryResult.noSuccess, ...{ data: this.defaultValue } } as QueryResult<TDataType>;
|
|
145
154
|
|
|
155
|
+
const clientValidationErrors = this.validation?.validate(args as object || {}) || [];
|
|
156
|
+
if (clientValidationErrors.length > 0) {
|
|
157
|
+
return QueryResult.validationFailed(clientValidationErrors, this);
|
|
158
|
+
}
|
|
159
|
+
|
|
146
160
|
if (!this.validateArguments(args)) {
|
|
147
161
|
return new Promise<QueryResult<TDataType>>((resolve) => {
|
|
148
162
|
resolve(noSuccess);
|
package/queries/QueryFor.ts
CHANGED
|
@@ -87,27 +87,7 @@ export abstract class QueryFor<TDataType, TParameters = object> implements IQuer
|
|
|
87
87
|
|
|
88
88
|
const clientValidationErrors = this.validation?.validate(args as object || {}) || [];
|
|
89
89
|
if (clientValidationErrors.length > 0) {
|
|
90
|
-
return
|
|
91
|
-
data: this.defaultValue as object,
|
|
92
|
-
isSuccess: false,
|
|
93
|
-
isAuthorized: true,
|
|
94
|
-
isValid: false,
|
|
95
|
-
hasExceptions: false,
|
|
96
|
-
validationResults: clientValidationErrors.map(_ => ({
|
|
97
|
-
severity: _.severity,
|
|
98
|
-
message: _.message,
|
|
99
|
-
members: _.members,
|
|
100
|
-
state: _.state
|
|
101
|
-
})),
|
|
102
|
-
exceptionMessages: [],
|
|
103
|
-
exceptionStackTrace: '',
|
|
104
|
-
paging: {
|
|
105
|
-
totalItems: 0,
|
|
106
|
-
totalPages: 0,
|
|
107
|
-
page: 0,
|
|
108
|
-
size: 0
|
|
109
|
-
}
|
|
110
|
-
}, this.modelType, this.enumerable) as QueryResult<TDataType>;
|
|
90
|
+
return QueryResult.validationFailed(clientValidationErrors, this);
|
|
111
91
|
}
|
|
112
92
|
|
|
113
93
|
if (!ValidateRequestArguments(this.constructor.name, this.requiredRequestParameters, args as object)) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { QueryResultWithState } from './QueryResultWithState';
|
|
5
5
|
import { CacheDiagnostics, CacheEntryDiagnostics } from './ObservableQueryDiagnosticsSnapshot';
|
|
6
|
+
import { reconcileQueryData } from './reconcileQueryData';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Represents a key that uniquely identifies a query instance in the cache, based on the query type name and its serialized arguments.
|
|
@@ -171,25 +172,68 @@ export class QueryInstanceCache {
|
|
|
171
172
|
setLastResult<TDataType>(key: QueryCacheKey, result: QueryResultWithState<TDataType>): void {
|
|
172
173
|
const entry = this._entries.get(key);
|
|
173
174
|
|
|
174
|
-
if (entry) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Suppress re-renders when the server re-sends identical data after a reconnect.
|
|
179
|
-
// We only compare `data` and `isSuccess` — other fields (e.g. changeSet) are
|
|
180
|
-
// ephemeral and do not affect what the user sees.
|
|
181
|
-
if (
|
|
182
|
-
previousResult !== undefined &&
|
|
183
|
-
previousResult.isSuccess === result.isSuccess &&
|
|
184
|
-
JSON.stringify(previousResult.data) === JSON.stringify(result.data)
|
|
185
|
-
) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
175
|
+
if (!entry) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
188
178
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
179
|
+
const previousResult = entry.lastResult as QueryResultWithState<TDataType> | undefined;
|
|
180
|
+
|
|
181
|
+
// Reconcile the incoming payload against the one already held so items that did not actually
|
|
182
|
+
// change keep their previous references. A full snapshot — which is what the server re-sends
|
|
183
|
+
// whenever a subscription is re-established — otherwise arrives as all-new references and
|
|
184
|
+
// makes every consumer treat every item as changed.
|
|
185
|
+
const reconciled = previousResult !== undefined
|
|
186
|
+
? this.withReconciledData(previousResult, result)
|
|
187
|
+
: result;
|
|
188
|
+
|
|
189
|
+
entry.lastResult = reconciled as QueryResultWithState<unknown>;
|
|
190
|
+
|
|
191
|
+
// Reconciliation returns the previous data reference when nothing changed, so identity is
|
|
192
|
+
// enough to detect it. Only `data` and `isSuccess` matter here — the other fields (e.g.
|
|
193
|
+
// changeSet) are ephemeral and do not affect what the user sees.
|
|
194
|
+
if (
|
|
195
|
+
previousResult !== undefined &&
|
|
196
|
+
previousResult.isSuccess === reconciled.isSuccess &&
|
|
197
|
+
previousResult.data === reconciled.data
|
|
198
|
+
) {
|
|
199
|
+
return;
|
|
192
200
|
}
|
|
201
|
+
|
|
202
|
+
for (const listener of entry.listeners) {
|
|
203
|
+
(listener as QueryCacheListener<TDataType>)(reconciled);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns the incoming result with its data reconciled against the previous result, or the
|
|
209
|
+
* incoming result unchanged when reconciliation found nothing to carry over.
|
|
210
|
+
* @template TDataType The type of data returned by the query.
|
|
211
|
+
* @param previous The previously held result.
|
|
212
|
+
* @param next The freshly received result.
|
|
213
|
+
* @returns A result whose data preserves references for everything that did not change.
|
|
214
|
+
*/
|
|
215
|
+
private withReconciledData<TDataType>(
|
|
216
|
+
previous: QueryResultWithState<TDataType>,
|
|
217
|
+
next: QueryResultWithState<TDataType>
|
|
218
|
+
): QueryResultWithState<TDataType> {
|
|
219
|
+
const reconciledData = reconcileQueryData(previous.data, next.data);
|
|
220
|
+
|
|
221
|
+
if (reconciledData === next.data) {
|
|
222
|
+
return next;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return new QueryResultWithState<TDataType>(
|
|
226
|
+
reconciledData,
|
|
227
|
+
next.paging,
|
|
228
|
+
next.isSuccess,
|
|
229
|
+
next.isAuthorized,
|
|
230
|
+
next.isValid,
|
|
231
|
+
next.validationResults,
|
|
232
|
+
next.hasExceptions,
|
|
233
|
+
next.exceptionMessages,
|
|
234
|
+
next.exceptionStackTrace,
|
|
235
|
+
next.isPerforming,
|
|
236
|
+
next.changeSet);
|
|
193
237
|
}
|
|
194
238
|
|
|
195
239
|
/**
|
package/queries/QueryResult.ts
CHANGED
|
@@ -71,6 +71,40 @@ export class QueryResult<TDataType = object> implements IQueryResult<TDataType>
|
|
|
71
71
|
}, Object, false);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Creates a {@link QueryResult} representing a query rejected by client-side validation, mirroring
|
|
76
|
+
* {@link CommandResult.validationFailed} so a caller reads a failed query the same way it reads a failed command.
|
|
77
|
+
* @param {ValidationResult[]} validationResults The validation results that caused the failure.
|
|
78
|
+
* @param {object} query The query being rejected, which describes how to shape its own result.
|
|
79
|
+
* @returns {QueryResult<TDataType>} A result that is neither successful nor valid.
|
|
80
|
+
*/
|
|
81
|
+
static validationFailed<TDataType>(
|
|
82
|
+
validationResults: ValidationResult[],
|
|
83
|
+
query: { readonly defaultValue: TDataType; readonly modelType: Constructor; readonly enumerable: boolean }): QueryResult<TDataType> {
|
|
84
|
+
const { defaultValue, modelType, enumerable } = query;
|
|
85
|
+
return new QueryResult({
|
|
86
|
+
data: defaultValue as object,
|
|
87
|
+
isSuccess: false,
|
|
88
|
+
isAuthorized: true,
|
|
89
|
+
isValid: false,
|
|
90
|
+
hasExceptions: false,
|
|
91
|
+
validationResults: validationResults.map(_ => ({
|
|
92
|
+
severity: _.severity,
|
|
93
|
+
message: _.message,
|
|
94
|
+
members: _.members,
|
|
95
|
+
state: _.state
|
|
96
|
+
})),
|
|
97
|
+
exceptionMessages: [],
|
|
98
|
+
exceptionStackTrace: '',
|
|
99
|
+
paging: {
|
|
100
|
+
totalItems: 0,
|
|
101
|
+
totalPages: 0,
|
|
102
|
+
page: 0,
|
|
103
|
+
size: 0
|
|
104
|
+
}
|
|
105
|
+
}, modelType, enumerable) as QueryResult<TDataType>;
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
static unauthorized<TDataType>(): QueryResult<TDataType> {
|
|
75
109
|
return new QueryResult({
|
|
76
110
|
data: null as unknown as object,
|
|
@@ -17,6 +17,15 @@ interface ActiveSubscription {
|
|
|
17
17
|
callback: DataReceived<any>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* How many keep-alive intervals of silence are tolerated before the connection is considered dead.
|
|
22
|
+
*
|
|
23
|
+
* The server guarantees a message every interval, so this is pure slack for latency and jitter.
|
|
24
|
+
* It must stay above 1 — a threshold at or below the server's own cadence makes the client and
|
|
25
|
+
* server timers race, and the client tears down connections the server considers healthy.
|
|
26
|
+
*/
|
|
27
|
+
const IDLE_THRESHOLD_FACTOR = 2;
|
|
28
|
+
|
|
20
29
|
/**
|
|
21
30
|
* A multiplexed SSE hub connection that uses EventSource for server→client streaming
|
|
22
31
|
* and fetch POST requests for client→server subscribe/unsubscribe commands.
|
|
@@ -45,8 +54,9 @@ export class ServerSentEventHubConnection implements IObservableQueryHubConnecti
|
|
|
45
54
|
* @param {string} _subscribeUrl The subscribe POST endpoint URL.
|
|
46
55
|
* @param {string} _unsubscribeUrl The unsubscribe POST endpoint URL.
|
|
47
56
|
* @param {string} _microservice The microservice name to pass as a query argument.
|
|
48
|
-
* @param {number} keepAliveIntervalMs
|
|
49
|
-
*
|
|
57
|
+
* @param {number} keepAliveIntervalMs The keep-alive cadence to assume until the server advertises
|
|
58
|
+
* its own on the {@link HubMessageType.Connected} message (default: 30 000 ms). The connection is
|
|
59
|
+
* considered stale after {@link IDLE_THRESHOLD_FACTOR} times this without any server message.
|
|
50
60
|
* @param {number} connectTimeoutMs How long to wait for the {@link HubMessageType.Connected}
|
|
51
61
|
* message after the HTTP connection opens before giving up and retrying (default: 15 000 ms).
|
|
52
62
|
* @param {IReconnectPolicy} _policy The reconnect policy to use (default: {@link ReconnectPolicy}).
|
|
@@ -64,18 +74,20 @@ export class ServerSentEventHubConnection implements IObservableQueryHubConnecti
|
|
|
64
74
|
// inactivity — if the server stops sending messages (including its own keep-alive
|
|
65
75
|
// pings) for the entire idle threshold, the connection is stale and we reconnect.
|
|
66
76
|
//
|
|
67
|
-
// The
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
77
|
+
// The server guarantees a message at least every keep-alive interval, so the threshold
|
|
78
|
+
// only has to absorb network latency, timer jitter and server-side scheduling hiccups.
|
|
79
|
+
// A hard TCP drop surfaces immediately through `onerror`, so this watchdog only needs to
|
|
80
|
+
// catch silent black-holes — that makes a generous tolerance strictly better than a tight
|
|
81
|
+
// one, which would tear down healthy connections.
|
|
82
|
+
//
|
|
83
|
+
// The interval below is only the starting assumption; the server advertises its real
|
|
84
|
+
// cadence on the Connected message and {@link handleConnected} reconfigures from it.
|
|
73
85
|
this._keepAlive = new HubConnectionKeepAlive(keepAliveIntervalMs, () => {
|
|
74
86
|
if (!this._disconnected && this._subscriptions.size > 0) {
|
|
75
|
-
console.warn(`SSE hub: no messages received for ${idleThresholdMs}ms, reconnecting '${this._sseUrl}'`);
|
|
87
|
+
console.warn(`SSE hub: no messages received for ${this._keepAlive.idleThresholdMs}ms, reconnecting '${this._sseUrl}'`);
|
|
76
88
|
this.reconnect();
|
|
77
89
|
}
|
|
78
|
-
},
|
|
90
|
+
}, keepAliveIntervalMs * IDLE_THRESHOLD_FACTOR);
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
/** @inheritdoc */
|
|
@@ -277,6 +289,8 @@ export class ServerSentEventHubConnection implements IObservableQueryHubConnecti
|
|
|
277
289
|
// Connected message arrived — cancel the connect timeout.
|
|
278
290
|
this.clearConnectTimeout();
|
|
279
291
|
|
|
292
|
+
this.applyServerKeepAliveInterval(message.keepAliveIntervalMs);
|
|
293
|
+
|
|
280
294
|
// Send all pending subscriptions now that we have a connection ID.
|
|
281
295
|
for (const [queryId, sub] of this._pendingSubscriptions) {
|
|
282
296
|
this.sendSubscribe(queryId, sub.request);
|
|
@@ -284,6 +298,27 @@ export class ServerSentEventHubConnection implements IObservableQueryHubConnecti
|
|
|
284
298
|
this._pendingSubscriptions.clear();
|
|
285
299
|
}
|
|
286
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Align the idle watchdog with the keep-alive cadence the server actually runs on.
|
|
303
|
+
*
|
|
304
|
+
* Without this the client assumes the default interval, so a server configured with a longer
|
|
305
|
+
* interval — or with keep-alive switched off entirely — would look silent and be reconnected
|
|
306
|
+
* on a loop even though it is perfectly healthy.
|
|
307
|
+
* @param {number | undefined} serverIntervalMs The interval advertised by the server, if any.
|
|
308
|
+
*/
|
|
309
|
+
private applyServerKeepAliveInterval(serverIntervalMs?: number): void {
|
|
310
|
+
if (serverIntervalMs === undefined) return;
|
|
311
|
+
|
|
312
|
+
// Keep-alive disabled server-side: silence is expected, so watching for it would guarantee
|
|
313
|
+
// an endless reconnect loop. Hard drops still surface through `onerror`.
|
|
314
|
+
if (serverIntervalMs <= 0) {
|
|
315
|
+
this._keepAlive.stop();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this._keepAlive.reconfigure(serverIntervalMs, serverIntervalMs * IDLE_THRESHOLD_FACTOR);
|
|
320
|
+
}
|
|
321
|
+
|
|
287
322
|
private handleQueryResult(message: HubMessage): void {
|
|
288
323
|
if (!message.queryId) return;
|
|
289
324
|
|
|
@@ -33,6 +33,12 @@ export interface HubMessage {
|
|
|
33
33
|
queryId?: string;
|
|
34
34
|
payload?: any;
|
|
35
35
|
timestamp?: number;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The server's keep-alive interval in milliseconds, or 0 when keep-alive is disabled.
|
|
39
|
+
* Only present on {@link HubMessageType.Connected} messages.
|
|
40
|
+
*/
|
|
41
|
+
keepAliveIntervalMs?: number;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import sinon from 'sinon';
|
|
5
|
+
import { HubConnectionKeepAlive } from '../../HubConnectionKeepAlive';
|
|
6
|
+
|
|
7
|
+
describe('when reconfiguring a running keep-alive', () => {
|
|
8
|
+
let clock: sinon.SinonFakeTimers;
|
|
9
|
+
let onIdle: sinon.SinonStub;
|
|
10
|
+
let keepAlive: HubConnectionKeepAlive;
|
|
11
|
+
|
|
12
|
+
const originalIntervalMs = 500;
|
|
13
|
+
const originalThresholdMs = 750;
|
|
14
|
+
const newIntervalMs = 1000;
|
|
15
|
+
const newThresholdMs = 2000;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
clock = sinon.useFakeTimers();
|
|
19
|
+
onIdle = sinon.stub();
|
|
20
|
+
keepAlive = new HubConnectionKeepAlive(originalIntervalMs, onIdle, originalThresholdMs);
|
|
21
|
+
keepAlive.start();
|
|
22
|
+
keepAlive.reconfigure(newIntervalMs, newThresholdMs);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
keepAlive.stop();
|
|
27
|
+
clock.restore();
|
|
28
|
+
sinon.restore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should expose the new idle threshold', () => keepAlive.idleThresholdMs.should.equal(newThresholdMs));
|
|
32
|
+
|
|
33
|
+
describe('and the original threshold elapses', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
// Well past the original 750ms threshold, but short of the new 2000ms one.
|
|
36
|
+
clock.tick(originalThresholdMs + 1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should not invoke the onIdle callback', () => onIdle.called.should.be.false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('and the new threshold elapses', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Checks now fire every 1000ms; the tick at 2000ms sees 2000ms of inactivity.
|
|
45
|
+
clock.tick(newThresholdMs + 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should invoke the onIdle callback', () => onIdle.calledOnce.should.be.true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import sinon from 'sinon';
|
|
5
|
+
import { HubConnectionKeepAlive } from '../../HubConnectionKeepAlive';
|
|
6
|
+
|
|
7
|
+
describe('when reconfiguring a keep-alive that has not been started', () => {
|
|
8
|
+
let clock: sinon.SinonFakeTimers;
|
|
9
|
+
let onIdle: sinon.SinonStub;
|
|
10
|
+
let keepAlive: HubConnectionKeepAlive;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
clock = sinon.useFakeTimers();
|
|
14
|
+
onIdle = sinon.stub();
|
|
15
|
+
keepAlive = new HubConnectionKeepAlive(500, onIdle, 750);
|
|
16
|
+
keepAlive.reconfigure(1000, 2000);
|
|
17
|
+
clock.tick(10000);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
keepAlive.stop();
|
|
22
|
+
clock.restore();
|
|
23
|
+
sinon.restore();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should not start the timer', () => onIdle.called.should.be.false);
|
|
27
|
+
it('should still apply the new idle threshold', () => keepAlive.idleThresholdMs.should.equal(2000));
|
|
28
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ObservableQueryFor } from '../../ObservableQueryFor';
|
|
5
|
+
import { QueryValidator } from '../../QueryValidator';
|
|
6
|
+
import { ParameterDescriptor } from '../../../reflection/ParameterDescriptor';
|
|
7
|
+
import '../../../validation/RuleBuilderExtensions';
|
|
8
|
+
|
|
9
|
+
export interface ITestParams {
|
|
10
|
+
minAge: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class TestObservableQueryValidator extends QueryValidator<ITestParams> {
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.ruleFor((c: ITestParams) => c.minAge).greaterThanOrEqual(0).withMessage('Age must be positive');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class TestObservableQuery extends ObservableQueryFor<string, ITestParams> {
|
|
21
|
+
readonly route = '/test';
|
|
22
|
+
readonly validation = new TestObservableQueryValidator();
|
|
23
|
+
readonly parameterDescriptors: ParameterDescriptor[] = [];
|
|
24
|
+
readonly defaultValue = '';
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super(String, false);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get requiredRequestParameters(): string[] {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class an_observable_query_with_validator {
|
|
36
|
+
query: TestObservableQuery;
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
this.query = new TestObservableQuery();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { an_observable_query_with_validator } from '../given/an_observable_query_with_validator';
|
|
5
|
+
import { QueryResult } from '../../QueryResult';
|
|
6
|
+
import { given } from '../../../given';
|
|
7
|
+
|
|
8
|
+
describe('when performing with client validation failing', given(an_observable_query_with_validator, context => {
|
|
9
|
+
let result: QueryResult<string>;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
result = await context.query.perform({ minAge: -5 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should not be success', () => {
|
|
16
|
+
result.isSuccess.should.be.false;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should not be valid', () => {
|
|
20
|
+
result.isValid.should.be.false;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have validation results', () => {
|
|
24
|
+
result.validationResults.should.not.be.empty;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should have error for minAge', () => {
|
|
28
|
+
result.validationResults.some(_ => _.members.includes('minAge')).should.be.true;
|
|
29
|
+
});
|
|
30
|
+
}));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { an_observable_query_with_validator } from '../given/an_observable_query_with_validator';
|
|
5
|
+
import { QueryResult } from '../../QueryResult';
|
|
6
|
+
import { given } from '../../../given';
|
|
7
|
+
|
|
8
|
+
describe('when subscribing with client validation failing', given(an_observable_query_with_validator, context => {
|
|
9
|
+
let result: QueryResult<string>;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
context.query.subscribe(_ => result = _, { minAge: -5 });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should push a result to the subscriber', () => {
|
|
16
|
+
result.should.not.be.undefined;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should not be valid', () => {
|
|
20
|
+
result.isValid.should.be.false;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have error for minAge', () => {
|
|
24
|
+
result.validationResults.some(_ => _.members.includes('minAge')).should.be.true;
|
|
25
|
+
});
|
|
26
|
+
}));
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { an_observable_query_with_validator } from '../given/an_observable_query_with_validator';
|
|
5
|
+
import { given } from '../../../given';
|
|
6
|
+
|
|
7
|
+
import * as sinon from 'sinon';
|
|
8
|
+
import { ObservableQuerySubscription } from '../../ObservableQuerySubscription';
|
|
9
|
+
|
|
10
|
+
describe('when subscribing with client validation passing', given(an_observable_query_with_validator, context => {
|
|
11
|
+
let callback: sinon.SinonStub;
|
|
12
|
+
let subscription: ObservableQuerySubscription<string>;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
context.query.setOrigin('https://example.com'); // Set origin to avoid document access
|
|
16
|
+
callback = sinon.stub();
|
|
17
|
+
|
|
18
|
+
subscription = context.query.subscribe(callback, { minAge: 18 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (subscription) {
|
|
23
|
+
subscription.unsubscribe();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// A real connection is established rather than one that immediately serves a result, which is what
|
|
28
|
+
// distinguishes a subscription that passed validation from one that was rejected by it.
|
|
29
|
+
it('should not call callback immediately', () => {
|
|
30
|
+
callback.called.should.be.false;
|
|
31
|
+
});
|
|
32
|
+
}));
|
package/queries/for_QueryInstanceCache/when_setting_last_result/with_a_partially_changed_result.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import sinon from 'sinon';
|
|
5
|
+
import { QueryInstanceCache } from '../../QueryInstanceCache';
|
|
6
|
+
import { QueryResultWithState } from '../../QueryResultWithState';
|
|
7
|
+
|
|
8
|
+
type Item = { id: string; name: string };
|
|
9
|
+
|
|
10
|
+
describe('when a re-delivered snapshot changed only one of its items', () => {
|
|
11
|
+
const key = 'MyQuery::';
|
|
12
|
+
let cache: QueryInstanceCache;
|
|
13
|
+
let listener: sinon.SinonStub;
|
|
14
|
+
let stored: QueryResultWithState<Item[]>;
|
|
15
|
+
|
|
16
|
+
const first = QueryResultWithState.empty<Item[]>([
|
|
17
|
+
{ id: 'a', name: 'First' },
|
|
18
|
+
{ id: 'b', name: 'Second' }
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const second = QueryResultWithState.empty<Item[]>([
|
|
22
|
+
{ id: 'a', name: 'First' },
|
|
23
|
+
{ id: 'b', name: 'Second changed' }
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
cache = new QueryInstanceCache();
|
|
28
|
+
cache.getOrCreate(key, () => ({}));
|
|
29
|
+
listener = sinon.stub();
|
|
30
|
+
cache.addListener(key, listener);
|
|
31
|
+
|
|
32
|
+
cache.setLastResult(key, first);
|
|
33
|
+
cache.setLastResult(key, second);
|
|
34
|
+
stored = cache.getLastResult<Item[]>(key)!;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => sinon.restore());
|
|
38
|
+
|
|
39
|
+
it('should notify for both results', () => listener.calledTwice.should.be.true);
|
|
40
|
+
it('should keep the reference of the unchanged item', () => stored.data[0].should.equal(first.data[0]));
|
|
41
|
+
it('should take the new reference for the changed item', () => stored.data[1].should.equal(second.data[1]));
|
|
42
|
+
it('should carry the changed value', () => stored.data[1].name.should.equal('Second changed'));
|
|
43
|
+
});
|