@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
|
@@ -0,0 +1,34 @@
|
|
|
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 the server re-delivers an identical snapshot after re-subscribing', () => {
|
|
11
|
+
const key = 'MyQuery::';
|
|
12
|
+
let cache: QueryInstanceCache;
|
|
13
|
+
let listener: sinon.SinonStub;
|
|
14
|
+
|
|
15
|
+
const first = QueryResultWithState.empty<Item[]>([{ id: 'a', name: 'First' }]);
|
|
16
|
+
|
|
17
|
+
// A fresh deserialization of the same payload — equal in value, all-new references.
|
|
18
|
+
const second = QueryResultWithState.empty<Item[]>([{ id: 'a', name: 'First' }]);
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
cache = new QueryInstanceCache();
|
|
22
|
+
cache.getOrCreate(key, () => ({}));
|
|
23
|
+
listener = sinon.stub();
|
|
24
|
+
cache.addListener(key, listener);
|
|
25
|
+
|
|
26
|
+
cache.setLastResult(key, first);
|
|
27
|
+
cache.setLastResult(key, second);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => sinon.restore());
|
|
31
|
+
|
|
32
|
+
it('should notify only for the first result', () => listener.calledOnce.should.be.true);
|
|
33
|
+
it('should keep holding the original data reference', () => cache.getLastResult<Item[]>(key)!.data.should.equal(first.data));
|
|
34
|
+
});
|
package/queries/for_ServerSentEventHubConnection/when_keep_alive_is_idle/triggers_reconnect.ts
CHANGED
|
@@ -33,10 +33,10 @@ describe('when keep-alive interval elapses without any server message', given(a_
|
|
|
33
33
|
// Deliver Connected so the connection is fully established.
|
|
34
34
|
context.simulateMessage({ type: HubMessageType.Connected, payload: 'conn-123' });
|
|
35
35
|
|
|
36
|
-
// Advance past the idle threshold (
|
|
37
|
-
// The check interval fires every KEEP_ALIVE_MS; the idle threshold is
|
|
38
|
-
// After the first interval tick (500ms) the elapsed idle time (500ms) is
|
|
39
|
-
// threshold (
|
|
36
|
+
// Advance past the idle threshold (IDLE_THRESHOLD_FACTOR × keep-alive interval) without any
|
|
37
|
+
// further messages. The check interval fires every KEEP_ALIVE_MS; the idle threshold is
|
|
38
|
+
// KEEP_ALIVE_MS * 2. After the first interval tick (500ms) the elapsed idle time (500ms) is
|
|
39
|
+
// below the threshold (1000ms). At the second tick (1000ms) the elapsed time reaches it.
|
|
40
40
|
clock.tick(KEEP_ALIVE_MS * 2 + 1);
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { a_server_sent_event_hub_connection } from '../given/a_server_sent_event_hub_connection';
|
|
6
|
+
import { given } from '../../../given';
|
|
7
|
+
import { HubMessageType } from '../../WebSocketHubConnection';
|
|
8
|
+
|
|
9
|
+
const ASSUMED_KEEP_ALIVE_MS = 500;
|
|
10
|
+
const SERVER_KEEP_ALIVE_MS = 2000;
|
|
11
|
+
|
|
12
|
+
describe('when the server advertises a longer keep-alive interval than the client assumed', given(a_server_sent_event_hub_connection, context => {
|
|
13
|
+
let clock: sinon.SinonFakeTimers;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
clock = sinon.useFakeTimers();
|
|
17
|
+
context.setup();
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
20
|
+
const { ServerSentEventHubConnection } = require('../../ServerSentEventHubConnection');
|
|
21
|
+
context.connection = new ServerSentEventHubConnection(
|
|
22
|
+
'http://localhost/.cratis/queries/sse',
|
|
23
|
+
'http://localhost/.cratis/queries/sse/subscribe',
|
|
24
|
+
'http://localhost/.cratis/queries/sse/unsubscribe',
|
|
25
|
+
'',
|
|
26
|
+
ASSUMED_KEEP_ALIVE_MS,
|
|
27
|
+
15000,
|
|
28
|
+
context.policy
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
context.connection.subscribe('q1', { queryName: 'MyQuery' }, sinon.stub());
|
|
32
|
+
context.simulateOpen();
|
|
33
|
+
|
|
34
|
+
context.simulateMessage({
|
|
35
|
+
type: HubMessageType.Connected,
|
|
36
|
+
payload: 'conn-123',
|
|
37
|
+
keepAliveIntervalMs: SERVER_KEEP_ALIVE_MS
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
clock.restore();
|
|
43
|
+
sinon.restore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('and the assumed threshold elapses', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
// Far past the assumed threshold (2 × 500ms), but the server only promised a message
|
|
49
|
+
// every 2000ms — tearing the connection down here is exactly the bug being fixed.
|
|
50
|
+
clock.tick(ASSUMED_KEEP_ALIVE_MS * 2 + 1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should not schedule a reconnect', () => (context.policy.schedule as sinon.SinonStub).called.should.be.false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('and the advertised threshold elapses', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
// Checks now run on the server's 2000ms cadence; the tick at 4000ms sees 4000ms of silence.
|
|
59
|
+
clock.tick(SERVER_KEEP_ALIVE_MS * 2 + 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should schedule a reconnect', () => (context.policy.schedule as sinon.SinonStub).calledOnce.should.be.true);
|
|
63
|
+
});
|
|
64
|
+
}));
|
|
@@ -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 { a_server_sent_event_hub_connection } from '../given/a_server_sent_event_hub_connection';
|
|
6
|
+
import { given } from '../../../given';
|
|
7
|
+
import { HubMessageType } from '../../WebSocketHubConnection';
|
|
8
|
+
|
|
9
|
+
const ASSUMED_KEEP_ALIVE_MS = 500;
|
|
10
|
+
|
|
11
|
+
describe('when the server reports that keep-alive is disabled', given(a_server_sent_event_hub_connection, context => {
|
|
12
|
+
let clock: sinon.SinonFakeTimers;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
clock = sinon.useFakeTimers();
|
|
16
|
+
context.setup();
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
19
|
+
const { ServerSentEventHubConnection } = require('../../ServerSentEventHubConnection');
|
|
20
|
+
context.connection = new ServerSentEventHubConnection(
|
|
21
|
+
'http://localhost/.cratis/queries/sse',
|
|
22
|
+
'http://localhost/.cratis/queries/sse/subscribe',
|
|
23
|
+
'http://localhost/.cratis/queries/sse/unsubscribe',
|
|
24
|
+
'',
|
|
25
|
+
ASSUMED_KEEP_ALIVE_MS,
|
|
26
|
+
15000,
|
|
27
|
+
context.policy
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
context.connection.subscribe('q1', { queryName: 'MyQuery' }, sinon.stub());
|
|
31
|
+
context.simulateOpen();
|
|
32
|
+
|
|
33
|
+
context.simulateMessage({
|
|
34
|
+
type: HubMessageType.Connected,
|
|
35
|
+
payload: 'conn-123',
|
|
36
|
+
keepAliveIntervalMs: 0
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// A server that never pings is silent by design — watching for silence would otherwise
|
|
40
|
+
// reconnect on a loop forever. Hard drops still surface through onerror.
|
|
41
|
+
clock.tick(ASSUMED_KEEP_ALIVE_MS * 20);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
clock.restore();
|
|
46
|
+
sinon.restore();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should never schedule a reconnect from inactivity', () => (context.policy.schedule as sinon.SinonStub).called.should.be.false);
|
|
50
|
+
}));
|
package/queries/for_reconcileQueryData/when_items_are_added_or_removed/reuses_the_surviving_items.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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 { reconcileQueryData } from '../../reconcileQueryData';
|
|
5
|
+
|
|
6
|
+
describe('when reconciling a snapshot with an item added', () => {
|
|
7
|
+
const previous = [
|
|
8
|
+
{ id: 'a', name: 'First' },
|
|
9
|
+
{ id: 'b', name: 'Second' }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const next = [
|
|
13
|
+
{ id: 'a', name: 'First' },
|
|
14
|
+
{ id: 'b', name: 'Second' },
|
|
15
|
+
{ id: 'c', name: 'Third' }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const result = reconcileQueryData(previous, next);
|
|
19
|
+
|
|
20
|
+
it('should return a new collection', () => result.should.not.equal(previous));
|
|
21
|
+
it('should have the new length', () => result.length.should.equal(3));
|
|
22
|
+
it('should keep the references of the surviving items', () => {
|
|
23
|
+
result[0].should.equal(previous[0]);
|
|
24
|
+
result[1].should.equal(previous[1]);
|
|
25
|
+
});
|
|
26
|
+
it('should take the new reference for the added item', () => result[2].should.equal(next[2]));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when reconciling a snapshot with an item removed', () => {
|
|
30
|
+
const previous = [
|
|
31
|
+
{ id: 'a', name: 'First' },
|
|
32
|
+
{ id: 'b', name: 'Second' },
|
|
33
|
+
{ id: 'c', name: 'Third' }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const next = [
|
|
37
|
+
{ id: 'a', name: 'First' },
|
|
38
|
+
{ id: 'c', name: 'Third' }
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const result = reconcileQueryData(previous, next);
|
|
42
|
+
|
|
43
|
+
it('should have the new length', () => result.length.should.equal(2));
|
|
44
|
+
it('should keep the reference of the item that stayed in place', () => result[0].should.equal(previous[0]));
|
|
45
|
+
it('should keep the reference of the item that shifted position', () => result[1].should.equal(previous[2]));
|
|
46
|
+
});
|
|
@@ -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 { reconcileQueryData } from '../../reconcileQueryData';
|
|
5
|
+
|
|
6
|
+
describe('when reconciling a snapshot in which a single item changed', () => {
|
|
7
|
+
const previous = [
|
|
8
|
+
{ id: 'a', name: 'First' },
|
|
9
|
+
{ id: 'b', name: 'Second' },
|
|
10
|
+
{ id: 'c', name: 'Third' }
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const next = [
|
|
14
|
+
{ id: 'a', name: 'First' },
|
|
15
|
+
{ id: 'b', name: 'Second changed' },
|
|
16
|
+
{ id: 'c', name: 'Third' }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const result = reconcileQueryData(previous, next);
|
|
20
|
+
|
|
21
|
+
it('should return a new collection', () => result.should.not.equal(previous));
|
|
22
|
+
it('should keep the reference of the first unchanged item', () => result[0].should.equal(previous[0]));
|
|
23
|
+
it('should keep the reference of the last unchanged item', () => result[2].should.equal(previous[2]));
|
|
24
|
+
it('should take the new reference for the changed item', () => result[1].should.equal(next[1]));
|
|
25
|
+
it('should carry the changed value', () => result[1].name.should.equal('Second changed'));
|
|
26
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
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 { reconcileQueryData } from '../../reconcileQueryData';
|
|
5
|
+
|
|
6
|
+
describe('when reconciling a re-delivered snapshot that did not change', () => {
|
|
7
|
+
const previous = [
|
|
8
|
+
{ id: 'a', name: 'First' },
|
|
9
|
+
{ id: 'b', name: 'Second' }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
// A fresh deserialization of the same payload — equal in value, all-new references.
|
|
13
|
+
const next = [
|
|
14
|
+
{ id: 'a', name: 'First' },
|
|
15
|
+
{ id: 'b', name: 'Second' }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const result = reconcileQueryData(previous, next);
|
|
19
|
+
|
|
20
|
+
it('should return the previous collection itself', () => result.should.equal(previous));
|
|
21
|
+
it('should not adopt any of the new references', () => result.forEach((item, index) => item.should.equal(previous[index])));
|
|
22
|
+
});
|
package/queries/for_reconcileQueryData/when_reconciling_non_collection_payloads/compares_by_value.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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 { reconcileQueryData } from '../../reconcileQueryData';
|
|
5
|
+
|
|
6
|
+
describe('when reconciling a single object that did not change', () => {
|
|
7
|
+
const previous = { id: 'a', name: 'First' };
|
|
8
|
+
const next = { id: 'a', name: 'First' };
|
|
9
|
+
|
|
10
|
+
const result = reconcileQueryData(previous, next);
|
|
11
|
+
|
|
12
|
+
it('should return the previous object', () => result.should.equal(previous));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('when reconciling a single object that changed', () => {
|
|
16
|
+
const previous = { id: 'a', name: 'First' };
|
|
17
|
+
const next = { id: 'a', name: 'Changed' };
|
|
18
|
+
|
|
19
|
+
const result = reconcileQueryData(previous, next);
|
|
20
|
+
|
|
21
|
+
it('should return the new object', () => result.should.equal(next));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('when reconciling items that carry no identity', () => {
|
|
25
|
+
const previous = [{ name: 'First' }, { name: 'Second' }];
|
|
26
|
+
const next = [{ name: 'First' }, { name: 'Second' }];
|
|
27
|
+
|
|
28
|
+
const result = reconcileQueryData(previous, next);
|
|
29
|
+
|
|
30
|
+
it('should fall back to positional matching and preserve the collection', () => result.should.equal(previous));
|
|
31
|
+
});
|
package/queries/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from './ObservableQuerySubscription';
|
|
|
35
35
|
export * from './WebSocketMessage';
|
|
36
36
|
export * from './QueryTransportMethod';
|
|
37
37
|
export * from './QueryInstanceCache';
|
|
38
|
+
export * from './reconcileQueryData';
|
|
38
39
|
export * from './IQueryProvider';
|
|
39
40
|
export * from './QueryProvider';
|
|
40
41
|
export * from './QueryValidator';
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { deepEqual } from '../deepEqual';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the conventional identity of an item so it can be matched against its previous version.
|
|
8
|
+
*
|
|
9
|
+
* Uses the `id` property, the same strategy the server-side change-set computation applies, and
|
|
10
|
+
* reduces concept-style identities (objects with a meaningful `toString()`) to a comparable value.
|
|
11
|
+
* @param {unknown} item The item to resolve identity for.
|
|
12
|
+
* @returns {unknown} The identity value, or `undefined` when the item carries no usable identity.
|
|
13
|
+
*/
|
|
14
|
+
function getIdentity(item: unknown): unknown {
|
|
15
|
+
const id = (item as Record<string, unknown>)?.id;
|
|
16
|
+
|
|
17
|
+
if (id === null || id === undefined) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof id === 'object') {
|
|
22
|
+
const stringValue = id.toString();
|
|
23
|
+
return stringValue !== '[object Object]' ? stringValue : JSON.stringify(id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reconciles a full payload against the previous one, carrying over the previous object references
|
|
31
|
+
* for every item that did not actually change.
|
|
32
|
+
*
|
|
33
|
+
* Observable queries re-deliver a complete snapshot whenever a subscription is (re-)established.
|
|
34
|
+
* Deserializing that snapshot produces entirely new object references, so consumers that compare by
|
|
35
|
+
* reference — memoized components, effect dependencies — treat every item as changed even when the
|
|
36
|
+
* payload is identical to what they already display. Reconciling restores referential stability:
|
|
37
|
+
* unchanged items keep their previous reference, and when nothing changed at all the previous
|
|
38
|
+
* payload itself is returned so callers can detect "no change" with a single `===`.
|
|
39
|
+
*
|
|
40
|
+
* Items are matched by their `id` when present, falling back to position for identity-less items.
|
|
41
|
+
* @template T The payload type.
|
|
42
|
+
* @param {T} previous The payload currently held.
|
|
43
|
+
* @param {T} next The freshly received payload.
|
|
44
|
+
* @returns {T} `previous` when nothing changed; otherwise the new payload with unchanged items carried over by reference.
|
|
45
|
+
*/
|
|
46
|
+
export function reconcileQueryData<T>(previous: T, next: T): T {
|
|
47
|
+
if (previous === next) {
|
|
48
|
+
return previous;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(previous) && Array.isArray(next)) {
|
|
52
|
+
return reconcileArray(previous, next) as T;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return deepEqual(previous, next) ? previous : next;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Reconciles two arrays element by element, preserving previous references for unchanged items.
|
|
60
|
+
* @param {unknown[]} previous The array currently held.
|
|
61
|
+
* @param {unknown[]} next The freshly received array.
|
|
62
|
+
* @returns {unknown[]} `previous` when the arrays are equivalent; otherwise a new array reusing unchanged items.
|
|
63
|
+
*/
|
|
64
|
+
function reconcileArray(previous: unknown[], next: unknown[]): unknown[] {
|
|
65
|
+
const previousByIdentity = new Map<unknown, unknown>();
|
|
66
|
+
|
|
67
|
+
for (const item of previous) {
|
|
68
|
+
const identity = getIdentity(item);
|
|
69
|
+
|
|
70
|
+
// Keep the first occurrence — duplicate identities are not a shape we can reconcile
|
|
71
|
+
// meaningfully, and preferring the first keeps the result deterministic.
|
|
72
|
+
if (identity !== undefined && !previousByIdentity.has(identity)) {
|
|
73
|
+
previousByIdentity.set(identity, item);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let changed = previous.length !== next.length;
|
|
78
|
+
|
|
79
|
+
const reconciled = next.map((item, index) => {
|
|
80
|
+
const identity = getIdentity(item);
|
|
81
|
+
const candidate = identity !== undefined ? previousByIdentity.get(identity) : previous[index];
|
|
82
|
+
|
|
83
|
+
if (candidate !== undefined && deepEqual(candidate, item)) {
|
|
84
|
+
// An unchanged item that moved still leaves the collection changed as a whole, but the
|
|
85
|
+
// item itself keeps its reference so only the ordering re-renders, not its content.
|
|
86
|
+
if (candidate !== previous[index]) {
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
changed = true;
|
|
94
|
+
return item;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return changed ? reconciled : previous;
|
|
98
|
+
}
|