@cratis/arc 20.3.3 → 20.4.1

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.
Files changed (58) hide show
  1. package/dist/cjs/queries/ObservableQueryConnection.d.ts.map +1 -1
  2. package/dist/cjs/queries/ObservableQueryConnection.js +1 -1
  3. package/dist/cjs/queries/ObservableQueryConnection.js.map +1 -1
  4. package/dist/cjs/queries/ServerSentEventHubConnection.d.ts.map +1 -1
  5. package/dist/cjs/queries/ServerSentEventHubConnection.js +6 -1
  6. package/dist/cjs/queries/ServerSentEventHubConnection.js.map +1 -1
  7. package/dist/cjs/queries/WebSocketHubConnection.d.ts.map +1 -1
  8. package/dist/cjs/queries/WebSocketHubConnection.js +7 -1
  9. package/dist/cjs/queries/WebSocketHubConnection.js.map +1 -1
  10. package/dist/cjs/queries/for_ObservableQueryConnection/given/an_observable_query_connection_with_websocket.d.ts +21 -0
  11. package/dist/cjs/queries/for_ObservableQueryConnection/given/an_observable_query_connection_with_websocket.d.ts.map +1 -0
  12. package/dist/cjs/queries/for_ObservableQueryConnection/when_receiving_data_message_with_type.d.ts +2 -0
  13. package/dist/cjs/queries/for_ObservableQueryConnection/when_receiving_data_message_with_type.d.ts.map +1 -0
  14. package/dist/cjs/queries/for_ObservableQueryConnection/when_receiving_legacy_data_message.d.ts +2 -0
  15. package/dist/cjs/queries/for_ObservableQueryConnection/when_receiving_legacy_data_message.d.ts.map +1 -0
  16. package/dist/cjs/queries/for_ServerSentEventHubConnection/when_unsubscribing/nulls_event_source_handlers_before_closing.d.ts +2 -0
  17. package/dist/cjs/queries/for_ServerSentEventHubConnection/when_unsubscribing/nulls_event_source_handlers_before_closing.d.ts.map +1 -0
  18. package/dist/cjs/queries/for_WebSocketHubConnection/when_unsubscribing/nulls_socket_handlers_before_closing.d.ts +2 -0
  19. package/dist/cjs/queries/for_WebSocketHubConnection/when_unsubscribing/nulls_socket_handlers_before_closing.d.ts.map +1 -0
  20. package/dist/esm/queries/ObservableQueryConnection.d.ts.map +1 -1
  21. package/dist/esm/queries/ObservableQueryConnection.js +1 -1
  22. package/dist/esm/queries/ObservableQueryConnection.js.map +1 -1
  23. package/dist/esm/queries/ServerSentEventHubConnection.d.ts.map +1 -1
  24. package/dist/esm/queries/ServerSentEventHubConnection.js +6 -1
  25. package/dist/esm/queries/ServerSentEventHubConnection.js.map +1 -1
  26. package/dist/esm/queries/WebSocketHubConnection.d.ts.map +1 -1
  27. package/dist/esm/queries/WebSocketHubConnection.js +7 -1
  28. package/dist/esm/queries/WebSocketHubConnection.js.map +1 -1
  29. package/dist/esm/queries/for_ObservableQueryConnection/given/an_observable_query_connection_with_websocket.d.ts +21 -0
  30. package/dist/esm/queries/for_ObservableQueryConnection/given/an_observable_query_connection_with_websocket.d.ts.map +1 -0
  31. package/dist/esm/queries/for_ObservableQueryConnection/given/an_observable_query_connection_with_websocket.js +35 -0
  32. package/dist/esm/queries/for_ObservableQueryConnection/given/an_observable_query_connection_with_websocket.js.map +1 -0
  33. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_data_message_with_type.d.ts +2 -0
  34. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_data_message_with_type.d.ts.map +1 -0
  35. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_data_message_with_type.js +37 -0
  36. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_data_message_with_type.js.map +1 -0
  37. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_legacy_data_message.d.ts +2 -0
  38. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_legacy_data_message.d.ts.map +1 -0
  39. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_legacy_data_message.js +36 -0
  40. package/dist/esm/queries/for_ObservableQueryConnection/when_receiving_legacy_data_message.js.map +1 -0
  41. package/dist/esm/queries/for_ServerSentEventHubConnection/when_unsubscribing/nulls_event_source_handlers_before_closing.d.ts +2 -0
  42. package/dist/esm/queries/for_ServerSentEventHubConnection/when_unsubscribing/nulls_event_source_handlers_before_closing.d.ts.map +1 -0
  43. package/dist/esm/queries/for_ServerSentEventHubConnection/when_unsubscribing/nulls_event_source_handlers_before_closing.js +26 -0
  44. package/dist/esm/queries/for_ServerSentEventHubConnection/when_unsubscribing/nulls_event_source_handlers_before_closing.js.map +1 -0
  45. package/dist/esm/queries/for_WebSocketHubConnection/when_unsubscribing/nulls_socket_handlers_before_closing.d.ts +2 -0
  46. package/dist/esm/queries/for_WebSocketHubConnection/when_unsubscribing/nulls_socket_handlers_before_closing.d.ts.map +1 -0
  47. package/dist/esm/queries/for_WebSocketHubConnection/when_unsubscribing/nulls_socket_handlers_before_closing.js +25 -0
  48. package/dist/esm/queries/for_WebSocketHubConnection/when_unsubscribing/nulls_socket_handlers_before_closing.js.map +1 -0
  49. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +1 -1
  51. package/queries/ObservableQueryConnection.ts +3 -2
  52. package/queries/ServerSentEventHubConnection.ts +10 -1
  53. package/queries/WebSocketHubConnection.ts +12 -1
  54. package/queries/for_ObservableQueryConnection/given/an_observable_query_connection_with_websocket.ts +58 -0
  55. package/queries/for_ObservableQueryConnection/when_receiving_data_message_with_type.ts +48 -0
  56. package/queries/for_ObservableQueryConnection/when_receiving_legacy_data_message.ts +48 -0
  57. package/queries/for_ServerSentEventHubConnection/when_unsubscribing/nulls_event_source_handlers_before_closing.ts +36 -0
  58. package/queries/for_WebSocketHubConnection/when_unsubscribing/nulls_socket_handlers_before_closing.ts +33 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/arc",
3
- "version": "20.3.3",
3
+ "version": "20.4.1",
4
4
  "description": "",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -169,8 +169,9 @@ export class ObservableQueryConnection<TDataType> implements IObservableQueryCon
169
169
  if (message.type === WebSocketMessageType.Pong) {
170
170
  this.handlePong(message);
171
171
  } else if (message.type === WebSocketMessageType.Data || !message.type) {
172
- // For backward compatibility, treat messages without a type as data messages
173
- const data = message.data ?? message;
172
+ // For new-style messages (type === 'Data') the query result is wrapped in message.data.
173
+ // For legacy/backward-compatible messages (no type) the entire message IS the query result.
174
+ const data = message.type === WebSocketMessageType.Data ? message.data : message;
174
175
  dataReceived(data as QueryResult<TDataType>);
175
176
  }
176
177
  } catch (error) {
@@ -153,7 +153,16 @@ export class ServerSentEventHubConnection implements IObservableQueryHubConnecti
153
153
  this._policy.cancel();
154
154
  this._keepAlive.stop();
155
155
  this.clearConnectTimeout();
156
- this._eventSource?.close();
156
+ if (this._eventSource) {
157
+ // Detach all handlers BEFORE closing so that the async onerror / onmessage
158
+ // events that fire after close() cannot observe the new _disconnected=false
159
+ // state set by an immediately following ensureConnected() call. Without this,
160
+ // a stale onerror triggers an unintended reconnect with exponential back-off.
161
+ this._eventSource.onopen = null;
162
+ this._eventSource.onmessage = null;
163
+ this._eventSource.onerror = null;
164
+ this._eventSource.close();
165
+ }
157
166
  this._eventSource = undefined;
158
167
  this._connectionId = undefined;
159
168
  }
@@ -175,7 +175,18 @@ export class WebSocketHubConnection {
175
175
  this._disconnected = true;
176
176
  this._keepAlive.stop();
177
177
  this._policy.cancel();
178
- this._socket?.close();
178
+ if (this._socket) {
179
+ // Detach all handlers BEFORE closing so that the async onclose event cannot
180
+ // fire after a new subscription has reset _disconnected to false and opened a
181
+ // fresh socket. Without this, the stale onclose triggers an unintended
182
+ // reconnect via the back-off policy, causing a 1-10 second delay before the
183
+ // new page's queries receive their first data.
184
+ this._socket.onopen = null;
185
+ this._socket.onclose = null;
186
+ this._socket.onerror = null;
187
+ this._socket.onmessage = null;
188
+ this._socket.close();
189
+ }
179
190
  this._socket = undefined;
180
191
  }
181
192
 
@@ -0,0 +1,58 @@
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 { ObservableQueryConnection } from '../../ObservableQueryConnection';
6
+
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+
9
+ export type FakeWebSocket = {
10
+ onopen: (() => void) | null;
11
+ onclose: (() => void) | null;
12
+ onerror: ((error: unknown) => void) | null;
13
+ onmessage: ((event: { data: string }) => void) | null;
14
+ close: sinon.SinonStub;
15
+ send: sinon.SinonStub;
16
+ readyState: number;
17
+ };
18
+
19
+ export class an_observable_query_connection_with_websocket {
20
+ connection: ObservableQueryConnection<unknown>;
21
+ fakeWebSocket!: FakeWebSocket;
22
+
23
+ constructor() {
24
+ this.fakeWebSocket = {
25
+ onopen: null,
26
+ onclose: null,
27
+ onerror: null,
28
+ onmessage: null,
29
+ close: sinon.stub(),
30
+ send: sinon.stub(),
31
+ readyState: WebSocket.OPEN,
32
+ };
33
+
34
+ const fakeWebSocket = this.fakeWebSocket;
35
+ const FakeWebSocketClass = function (this: any) {
36
+ fakeWebSocket.onopen = null;
37
+ fakeWebSocket.onclose = null;
38
+ fakeWebSocket.onerror = null;
39
+ fakeWebSocket.onmessage = null;
40
+ return fakeWebSocket;
41
+ };
42
+ (globalThis as any)['WebSocket'] = FakeWebSocketClass;
43
+
44
+ this.connection = new ObservableQueryConnection<unknown>(
45
+ new URL('https://example.com/api/test'),
46
+ 'test-microservice'
47
+ );
48
+ }
49
+
50
+ simulateMessage(payload: object): void {
51
+ this.fakeWebSocket.onmessage?.({ data: JSON.stringify(payload) });
52
+ }
53
+
54
+ simulateOpen(): void {
55
+ this.fakeWebSocket.readyState = WebSocket.OPEN;
56
+ this.fakeWebSocket.onopen?.();
57
+ }
58
+ }
@@ -0,0 +1,48 @@
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 { given } from '../../given';
5
+ import { an_observable_query_connection_with_websocket } from './given/an_observable_query_connection_with_websocket';
6
+ import { QueryResult } from '../QueryResult';
7
+ import { WebSocketMessageType } from '../WebSocketMessage';
8
+
9
+ /* eslint-disable @typescript-eslint/no-explicit-any */
10
+
11
+ describe('when receiving data message with Data type', given(an_observable_query_connection_with_websocket, context => {
12
+ const innerQueryResult = {
13
+ data: [{ id: '2', name: 'Item' }],
14
+ isSuccess: true,
15
+ isAuthorized: true,
16
+ isValid: true,
17
+ hasExceptions: false,
18
+ validationResults: [],
19
+ exceptionMessages: [],
20
+ exceptionStackTrace: '',
21
+ paging: { page: 0, size: 0, totalItems: 1, totalPages: 1 }
22
+ };
23
+
24
+ let receivedData: QueryResult<unknown> | null = null;
25
+
26
+ beforeEach(() => {
27
+ receivedData = null;
28
+ context.connection.connect((data) => {
29
+ receivedData = data as QueryResult<unknown>;
30
+ });
31
+ context.simulateMessage({
32
+ type: WebSocketMessageType.Data,
33
+ data: innerQueryResult
34
+ });
35
+ });
36
+
37
+ it('should pass the inner data payload as the query result', () => {
38
+ receivedData!.should.not.be.null;
39
+ });
40
+
41
+ it('should have the correct data field', () => {
42
+ (receivedData as any)!.data.should.deep.equal(innerQueryResult.data);
43
+ });
44
+
45
+ it('should have the correct isSuccess field', () => {
46
+ (receivedData as any)!.isSuccess.should.equal(true);
47
+ });
48
+ }));
@@ -0,0 +1,48 @@
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 { given } from '../../given';
5
+ import { an_observable_query_connection_with_websocket } from './given/an_observable_query_connection_with_websocket';
6
+ import { QueryResult } from '../QueryResult';
7
+
8
+ /* eslint-disable @typescript-eslint/no-explicit-any */
9
+
10
+ describe('when receiving legacy data message without type field', given(an_observable_query_connection_with_websocket, context => {
11
+ const queryResult = {
12
+ data: [{ id: '1', name: 'Test' }],
13
+ isSuccess: true,
14
+ isAuthorized: true,
15
+ isValid: true,
16
+ hasExceptions: false,
17
+ validationResults: [],
18
+ exceptionMessages: [],
19
+ exceptionStackTrace: '',
20
+ paging: { page: 0, size: 0, totalItems: 1, totalPages: 1 }
21
+ };
22
+
23
+ let receivedData: QueryResult<unknown> | null = null;
24
+
25
+ beforeEach(() => {
26
+ receivedData = null;
27
+ context.connection.connect((data) => {
28
+ receivedData = data as QueryResult<unknown>;
29
+ });
30
+ context.simulateMessage(queryResult);
31
+ });
32
+
33
+ it('should pass the entire message as the query result', () => {
34
+ receivedData!.should.not.be.null;
35
+ });
36
+
37
+ it('should have the correct data field', () => {
38
+ (receivedData as any)!.data.should.deep.equal(queryResult.data);
39
+ });
40
+
41
+ it('should have the correct isSuccess field', () => {
42
+ (receivedData as any)!.isSuccess.should.equal(true);
43
+ });
44
+
45
+ it('should have the correct paging field', () => {
46
+ (receivedData as any)!.paging.should.deep.equal(queryResult.paging);
47
+ });
48
+ }));
@@ -0,0 +1,36 @@
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
+ describe('when unsubscribing the only query, nulls event source handlers before closing the event source', given(a_server_sent_event_hub_connection, context => {
10
+ let handlersWereNullWhenClosed: { onopen: boolean; onmessage: boolean; onerror: boolean };
11
+
12
+ beforeEach(async () => {
13
+ context.setup();
14
+ context.connection.subscribe('q1', { queryName: 'MyQuery' }, sinon.stub());
15
+ context.simulateOpen();
16
+ context.simulateMessage({ type: HubMessageType.Connected, payload: 'conn-abc' });
17
+
18
+ handlersWereNullWhenClosed = { onopen: false, onmessage: false, onerror: false };
19
+ context.fakeEventSource.close.callsFake(() => {
20
+ handlersWereNullWhenClosed.onopen = context.fakeEventSource.onopen === null;
21
+ handlersWereNullWhenClosed.onmessage = context.fakeEventSource.onmessage === null;
22
+ handlersWereNullWhenClosed.onerror = context.fakeEventSource.onerror === null;
23
+ });
24
+
25
+ context.connection.unsubscribe('q1');
26
+
27
+ // Allow the micro-task queue to drain so the async fetch completes.
28
+ await Promise.resolve();
29
+ });
30
+
31
+ afterEach(() => sinon.restore());
32
+
33
+ it('should have nulled onopen before calling eventSource.close()', () => handlersWereNullWhenClosed.onopen.should.be.true);
34
+ it('should have nulled onmessage before calling eventSource.close()', () => handlersWereNullWhenClosed.onmessage.should.be.true);
35
+ it('should have nulled onerror before calling eventSource.close()', () => handlersWereNullWhenClosed.onerror.should.be.true);
36
+ }));
@@ -0,0 +1,33 @@
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_web_socket_hub_connection } from '../given/a_web_socket_hub_connection';
6
+ import { given } from '../../../given';
7
+
8
+ describe('when unsubscribing the only query, nulls socket handlers before closing the socket', given(a_web_socket_hub_connection, context => {
9
+ let handlersWereNullWhenClosed: { onopen: boolean; onclose: boolean; onerror: boolean; onmessage: boolean };
10
+
11
+ beforeEach(() => {
12
+ context.setup();
13
+ context.connection.subscribe('q1', { queryName: 'MyQuery' }, sinon.stub());
14
+ context.simulateOpen();
15
+
16
+ handlersWereNullWhenClosed = { onopen: false, onclose: false, onerror: false, onmessage: false };
17
+ context.fakeSocket.close.callsFake(() => {
18
+ handlersWereNullWhenClosed.onopen = context.fakeSocket.onopen === null;
19
+ handlersWereNullWhenClosed.onclose = context.fakeSocket.onclose === null;
20
+ handlersWereNullWhenClosed.onerror = context.fakeSocket.onerror === null;
21
+ handlersWereNullWhenClosed.onmessage = context.fakeSocket.onmessage === null;
22
+ });
23
+
24
+ context.connection.unsubscribe('q1');
25
+ });
26
+
27
+ afterEach(() => sinon.restore());
28
+
29
+ it('should have nulled onopen before calling socket.close()', () => handlersWereNullWhenClosed.onopen.should.be.true);
30
+ it('should have nulled onclose before calling socket.close()', () => handlersWereNullWhenClosed.onclose.should.be.true);
31
+ it('should have nulled onerror before calling socket.close()', () => handlersWereNullWhenClosed.onerror.should.be.true);
32
+ it('should have nulled onmessage before calling socket.close()', () => handlersWereNullWhenClosed.onmessage.should.be.true);
33
+ }));