@cratis/arc 20.3.1 → 20.3.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/ObservableQueryConnectionFactory.js +11 -2
- package/dist/cjs/queries/ObservableQueryConnectionFactory.js.map +1 -1
- package/dist/cjs/queries/ObservableQueryMultiplexer.d.ts +1 -1
- package/dist/cjs/queries/ObservableQueryMultiplexer.d.ts.map +1 -1
- package/dist/cjs/queries/ObservableQueryMultiplexer.js +2 -2
- package/dist/cjs/queries/ObservableQueryMultiplexer.js.map +1 -1
- package/dist/cjs/queries/QueryInstanceCache.d.ts +0 -1
- package/dist/cjs/queries/QueryInstanceCache.d.ts.map +1 -1
- package/dist/cjs/queries/QueryInstanceCache.js +10 -25
- package/dist/cjs/queries/QueryInstanceCache.js.map +1 -1
- package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.d.ts +2 -0
- package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.d.ts.map +1 -0
- package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.d.ts +2 -0
- package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.d.ts.map +1 -0
- package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts +2 -0
- package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts.map +1 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts +2 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts.map +1 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts +2 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts.map +1 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts +2 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts.map +1 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts +2 -0
- package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts.map +1 -0
- package/dist/cjs/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts +2 -0
- package/dist/cjs/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts.map +1 -0
- package/dist/esm/queries/ObservableQueryConnectionFactory.js +11 -2
- package/dist/esm/queries/ObservableQueryConnectionFactory.js.map +1 -1
- package/dist/esm/queries/ObservableQueryMultiplexer.d.ts +1 -1
- package/dist/esm/queries/ObservableQueryMultiplexer.d.ts.map +1 -1
- package/dist/esm/queries/ObservableQueryMultiplexer.js +2 -2
- package/dist/esm/queries/ObservableQueryMultiplexer.js.map +1 -1
- package/dist/esm/queries/QueryInstanceCache.d.ts +0 -1
- package/dist/esm/queries/QueryInstanceCache.d.ts.map +1 -1
- package/dist/esm/queries/QueryInstanceCache.js +10 -25
- package/dist/esm/queries/QueryInstanceCache.js.map +1 -1
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.js +55 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.js +49 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.js +48 -0
- package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.js +32 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.js +23 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.js +23 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.js.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts +2 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts.map +1 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.js +25 -0
- package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.js.map +1 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts +2 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts.map +1 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.js +23 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.js.map +1 -0
- package/dist/esm/queries/for_QueryInstanceCache/when_releasing/the_only_subscriber_outside_development_mode.js +4 -1
- package/dist/esm/queries/for_QueryInstanceCache/when_releasing/the_only_subscriber_outside_development_mode.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/queries/ObservableQueryConnectionFactory.ts +23 -2
- package/queries/ObservableQueryMultiplexer.ts +3 -2
- package/queries/QueryInstanceCache.ts +27 -42
- package/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.ts +69 -0
- package/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.ts +61 -0
- package/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.ts +60 -0
- package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.ts +42 -0
- package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.ts +34 -0
- package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.ts +33 -0
- package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.ts +35 -0
- package/queries/for_QueryInstanceCache/when_acquiring/after_release.ts +31 -0
- package/queries/for_QueryInstanceCache/when_releasing/the_only_subscriber_outside_development_mode.ts +4 -1
package/package.json
CHANGED
|
@@ -87,9 +87,30 @@ function createDirectSSEConnection<TDataType>(descriptor: QueryConnectionDescrip
|
|
|
87
87
|
|
|
88
88
|
// ---- Multiplexed mode: centralized endpoint, multiple queries share connections ----
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Maximum number of SSE hub connections that fits safely within the HTTP/1.1 browser
|
|
92
|
+
* per-origin connection limit (typically 6 for Chrome/Firefox). Each SSE EventSource
|
|
93
|
+
* occupies one persistent connection slot indefinitely. Exceeding this cap blocks the
|
|
94
|
+
* subscribe/unsubscribe POST requests that share the same pool, causing queries to hang.
|
|
95
|
+
* Servers configured for HTTP/2 do not have this restriction.
|
|
96
|
+
*/
|
|
97
|
+
const MAX_SAFE_SSE_CONNECTIONS = 4;
|
|
98
|
+
|
|
90
99
|
function createMultiplexedConnection<TDataType>(descriptor: QueryConnectionDescriptor, isSSE: boolean): IObservableQueryConnection<TDataType> {
|
|
91
100
|
const transport = isSSE ? 'sse' : 'ws';
|
|
92
|
-
const
|
|
101
|
+
const requestedCount = Globals.queryConnectionCount;
|
|
102
|
+
const effectiveCount = isSSE ? Math.min(requestedCount, MAX_SAFE_SSE_CONNECTIONS) : requestedCount;
|
|
103
|
+
|
|
104
|
+
if (isSSE && requestedCount > MAX_SAFE_SSE_CONNECTIONS) {
|
|
105
|
+
console.warn(
|
|
106
|
+
`[Arc] queryConnectionCount (${requestedCount}) exceeds the safe limit for SSE transport (${MAX_SAFE_SSE_CONNECTIONS}). ` +
|
|
107
|
+
`HTTP/1.1 browsers allow at most 6 concurrent connections per origin; ` +
|
|
108
|
+
`using more SSE connections blocks subscribe/unsubscribe requests, causing queries to hang. ` +
|
|
109
|
+
`Capping at ${MAX_SAFE_SSE_CONNECTIONS}. Enable HTTP/2 on your server to use a higher connection count.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const cacheKey = `${requestedCount}|${transport}|${descriptor.origin}|${descriptor.apiBasePath}|${descriptor.microservice}`;
|
|
93
114
|
|
|
94
115
|
const multiplexer = getOrCreateMultiplexer(() => {
|
|
95
116
|
if (isSSE) {
|
|
@@ -112,7 +133,7 @@ function createMultiplexedConnection<TDataType>(descriptor: QueryConnectionDescr
|
|
|
112
133
|
const wsUrl = `${secure ? 'wss' : 'ws'}://${hubUrl.host}${hubUrl.pathname}${hubUrl.search}`;
|
|
113
134
|
return new WebSocketHubConnection(wsUrl, descriptor.microservice);
|
|
114
135
|
}
|
|
115
|
-
}, cacheKey);
|
|
136
|
+
}, cacheKey, effectiveCount);
|
|
116
137
|
|
|
117
138
|
return new MultiplexedObservableQueryConnection<TDataType>(multiplexer, descriptor.queryName);
|
|
118
139
|
}
|
|
@@ -190,15 +190,16 @@ let _sharedMultiplexerKey = '';
|
|
|
190
190
|
* configuration (connection count, origin, base path, microservice, transport) changes.
|
|
191
191
|
* @param {() => IObservableQueryHubConnection} connectionFactory Factory to create individual connections.
|
|
192
192
|
* @param {string} cacheKey A string that identifies the current configuration for invalidation.
|
|
193
|
+
* @param {number} size Number of physical connections to create. Defaults to {@link Globals.queryConnectionCount}.
|
|
193
194
|
* @returns The shared multiplexer instance.
|
|
194
195
|
*/
|
|
195
|
-
export function getOrCreateMultiplexer(connectionFactory: () => IObservableQueryHubConnection, cacheKey: string): ObservableQueryMultiplexer {
|
|
196
|
+
export function getOrCreateMultiplexer(connectionFactory: () => IObservableQueryHubConnection, cacheKey: string, size: number = Globals.queryConnectionCount): ObservableQueryMultiplexer {
|
|
196
197
|
if (_sharedMultiplexer && _sharedMultiplexerKey === cacheKey) {
|
|
197
198
|
return _sharedMultiplexer;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
_sharedMultiplexer?.dispose();
|
|
201
|
-
_sharedMultiplexer = new ObservableQueryMultiplexer(
|
|
202
|
+
_sharedMultiplexer = new ObservableQueryMultiplexer(size, connectionFactory);
|
|
202
203
|
_sharedMultiplexerKey = cacheKey;
|
|
203
204
|
return _sharedMultiplexer;
|
|
204
205
|
}
|
|
@@ -50,9 +50,9 @@ export interface QueryCacheEntry<TDataType> {
|
|
|
50
50
|
subscribed: boolean;
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Timer handle for deferred cleanup
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Timer handle for deferred cleanup. Allows React StrictMode re-mounts (in any build
|
|
54
|
+
* environment) to cancel the pending teardown so the connection is reused instead of
|
|
55
|
+
* torn down and recreated.
|
|
56
56
|
*/
|
|
57
57
|
pendingCleanup?: ReturnType<typeof setTimeout>;
|
|
58
58
|
}
|
|
@@ -67,16 +67,17 @@ export interface QueryCacheEntry<TDataType> {
|
|
|
67
67
|
*/
|
|
68
68
|
export class QueryInstanceCache {
|
|
69
69
|
private readonly _entries = new Map<QueryCacheKey, QueryCacheEntry<unknown>>();
|
|
70
|
-
private readonly _development: boolean;
|
|
71
70
|
private _pendingDispose?: ReturnType<typeof setTimeout>;
|
|
72
71
|
|
|
73
72
|
/**
|
|
74
73
|
* Initializes a new instance of {@link QueryInstanceCache}.
|
|
75
|
-
* @param development
|
|
76
|
-
*
|
|
74
|
+
* @param development Accepted for API compatibility. No longer changes teardown behavior —
|
|
75
|
+
* teardown is always deferred to handle React StrictMode re-mounts in any environment.
|
|
77
76
|
*/
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
78
78
|
constructor(development: boolean = false) {
|
|
79
|
-
|
|
79
|
+
// The development parameter is kept for API compatibility only.
|
|
80
|
+
// Teardown is always deferred regardless of this flag.
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
@@ -131,7 +132,7 @@ export class QueryInstanceCache {
|
|
|
131
132
|
|
|
132
133
|
/**
|
|
133
134
|
* Increments the active subscriber count for the given key.
|
|
134
|
-
* If a deferred cleanup was pending (from a recent {@link release}
|
|
135
|
+
* If a deferred cleanup was pending (from a recent {@link release}),
|
|
135
136
|
* it is cancelled so the existing subscription is reused.
|
|
136
137
|
* Call from `useEffect` setup to pair with {@link release} in the cleanup.
|
|
137
138
|
* @param key The cache key produced by {@link buildKey}.
|
|
@@ -230,13 +231,11 @@ export class QueryInstanceCache {
|
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
/**
|
|
233
|
-
* Decrements the subscriber count for the given key. When the count reaches zero
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* an unnecessary disconnect/reconnect cycle during the synthetic unmount/remount that
|
|
239
|
-
* StrictMode performs in development builds.
|
|
234
|
+
* Decrements the subscriber count for the given key. When the count reaches zero, both teardown
|
|
235
|
+
* and eviction are deferred by one microtask so that React StrictMode re-mounts — in any build
|
|
236
|
+
* environment — can re-acquire the entry and cancel the cleanup before the timeout fires. This
|
|
237
|
+
* prevents an unnecessary disconnect/reconnect cycle during the synthetic unmount/remount that
|
|
238
|
+
* StrictMode performs.
|
|
240
239
|
* @param key The cache key produced by {@link buildKey}.
|
|
241
240
|
*/
|
|
242
241
|
release(key: QueryCacheKey): void {
|
|
@@ -246,33 +245,19 @@ export class QueryInstanceCache {
|
|
|
246
245
|
entry.subscriberCount--;
|
|
247
246
|
|
|
248
247
|
if (entry.subscriberCount <= 0) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
} else {
|
|
263
|
-
entry.subscribed = false;
|
|
264
|
-
entry.teardown?.();
|
|
265
|
-
entry.teardown = undefined;
|
|
266
|
-
|
|
267
|
-
// Defer deletion so React Strict Mode re-mounts can re-acquire the entry.
|
|
268
|
-
setTimeout(() => {
|
|
269
|
-
const current = this._entries.get(key);
|
|
270
|
-
|
|
271
|
-
if (current && current.subscriberCount <= 0) {
|
|
272
|
-
this._entries.delete(key);
|
|
273
|
-
}
|
|
274
|
-
}, 0);
|
|
275
|
-
}
|
|
248
|
+
// Defer both teardown and deletion so React StrictMode re-mounts in any environment
|
|
249
|
+
// can cancel by calling acquire() before the timeout fires.
|
|
250
|
+
entry.pendingCleanup = setTimeout(() => {
|
|
251
|
+
const current = this._entries.get(key);
|
|
252
|
+
|
|
253
|
+
if (current && current.subscriberCount <= 0) {
|
|
254
|
+
current.subscribed = false;
|
|
255
|
+
current.teardown?.();
|
|
256
|
+
current.teardown = undefined;
|
|
257
|
+
current.pendingCleanup = undefined;
|
|
258
|
+
this._entries.delete(key);
|
|
259
|
+
}
|
|
260
|
+
}, 0);
|
|
276
261
|
}
|
|
277
262
|
}
|
|
278
263
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { a_descriptor } from '../given/a_descriptor';
|
|
5
|
+
import { given } from '../../../given';
|
|
6
|
+
import { Globals } from '../../../Globals';
|
|
7
|
+
import { QueryTransportMethod } from '../../QueryTransportMethod';
|
|
8
|
+
import { createObservableQueryConnection } from '../../ObservableQueryConnectionFactory';
|
|
9
|
+
import { MultiplexedObservableQueryConnection, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer';
|
|
10
|
+
import { IObservableQueryConnection } from '../../IObservableQueryConnection';
|
|
11
|
+
|
|
12
|
+
import * as sinon from 'sinon';
|
|
13
|
+
|
|
14
|
+
describe('when creating with hub mode and SSE transport and connection count exceeding safe limit', given(a_descriptor, context => {
|
|
15
|
+
let connection: IObservableQueryConnection<unknown>;
|
|
16
|
+
let warnStub: sinon.SinonStub;
|
|
17
|
+
let originalConnectionCount: number;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
originalConnectionCount = Globals.queryConnectionCount;
|
|
21
|
+
Globals.queryDirectMode = false;
|
|
22
|
+
Globals.queryTransportMethod = QueryTransportMethod.ServerSentEvents;
|
|
23
|
+
Globals.queryConnectionCount = 10;
|
|
24
|
+
|
|
25
|
+
warnStub = sinon.stub(console, 'warn');
|
|
26
|
+
|
|
27
|
+
const FakeEventSourceConstructor = function (this: EventSource) {
|
|
28
|
+
Object.assign(this, {
|
|
29
|
+
onopen: null,
|
|
30
|
+
onerror: null,
|
|
31
|
+
onmessage: null,
|
|
32
|
+
close: sinon.stub(),
|
|
33
|
+
addEventListener: sinon.stub(),
|
|
34
|
+
removeEventListener: sinon.stub(),
|
|
35
|
+
readyState: 0,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
(globalThis as Record<string, unknown>)['EventSource'] = FakeEventSourceConstructor;
|
|
39
|
+
(globalThis as Record<string, unknown>)['fetch'] = sinon.stub().resolves({ ok: true } as Response);
|
|
40
|
+
|
|
41
|
+
connection = createObservableQueryConnection(context.descriptor);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
Globals.queryDirectMode = context.originalDirectMode;
|
|
46
|
+
Globals.queryTransportMethod = context.originalTransportMethod;
|
|
47
|
+
Globals.queryConnectionCount = originalConnectionCount;
|
|
48
|
+
warnStub.restore();
|
|
49
|
+
delete (globalThis as Record<string, unknown>)['EventSource'];
|
|
50
|
+
delete (globalThis as Record<string, unknown>)['fetch'];
|
|
51
|
+
resetSharedMultiplexer();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return a MultiplexedObservableQueryConnection', () => {
|
|
55
|
+
connection.should.be.instanceOf(MultiplexedObservableQueryConnection);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should log a warning about the connection count limit', () => {
|
|
59
|
+
warnStub.calledOnce.should.be.true;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should mention the requested connection count in the warning', () => {
|
|
63
|
+
warnStub.firstCall.args[0].should.include('10');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should mention the safe limit in the warning', () => {
|
|
67
|
+
warnStub.firstCall.args[0].should.include('4');
|
|
68
|
+
});
|
|
69
|
+
}));
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { a_descriptor } from '../given/a_descriptor';
|
|
5
|
+
import { given } from '../../../given';
|
|
6
|
+
import { Globals } from '../../../Globals';
|
|
7
|
+
import { QueryTransportMethod } from '../../QueryTransportMethod';
|
|
8
|
+
import { createObservableQueryConnection } from '../../ObservableQueryConnectionFactory';
|
|
9
|
+
import { MultiplexedObservableQueryConnection, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer';
|
|
10
|
+
import { IObservableQueryConnection } from '../../IObservableQueryConnection';
|
|
11
|
+
|
|
12
|
+
import * as sinon from 'sinon';
|
|
13
|
+
|
|
14
|
+
describe('when creating with hub mode and SSE transport and connection count within safe limit', given(a_descriptor, context => {
|
|
15
|
+
let connection: IObservableQueryConnection<unknown>;
|
|
16
|
+
let warnStub: sinon.SinonStub;
|
|
17
|
+
let originalConnectionCount: number;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
originalConnectionCount = Globals.queryConnectionCount;
|
|
21
|
+
Globals.queryDirectMode = false;
|
|
22
|
+
Globals.queryTransportMethod = QueryTransportMethod.ServerSentEvents;
|
|
23
|
+
Globals.queryConnectionCount = 4;
|
|
24
|
+
|
|
25
|
+
warnStub = sinon.stub(console, 'warn');
|
|
26
|
+
|
|
27
|
+
const FakeEventSourceConstructor = function (this: EventSource) {
|
|
28
|
+
Object.assign(this, {
|
|
29
|
+
onopen: null,
|
|
30
|
+
onerror: null,
|
|
31
|
+
onmessage: null,
|
|
32
|
+
close: sinon.stub(),
|
|
33
|
+
addEventListener: sinon.stub(),
|
|
34
|
+
removeEventListener: sinon.stub(),
|
|
35
|
+
readyState: 0,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
(globalThis as Record<string, unknown>)['EventSource'] = FakeEventSourceConstructor;
|
|
39
|
+
(globalThis as Record<string, unknown>)['fetch'] = sinon.stub().resolves({ ok: true } as Response);
|
|
40
|
+
|
|
41
|
+
connection = createObservableQueryConnection(context.descriptor);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
Globals.queryDirectMode = context.originalDirectMode;
|
|
46
|
+
Globals.queryTransportMethod = context.originalTransportMethod;
|
|
47
|
+
Globals.queryConnectionCount = originalConnectionCount;
|
|
48
|
+
warnStub.restore();
|
|
49
|
+
delete (globalThis as Record<string, unknown>)['EventSource'];
|
|
50
|
+
delete (globalThis as Record<string, unknown>)['fetch'];
|
|
51
|
+
resetSharedMultiplexer();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return a MultiplexedObservableQueryConnection', () => {
|
|
55
|
+
connection.should.be.instanceOf(MultiplexedObservableQueryConnection);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should not log a warning', () => {
|
|
59
|
+
warnStub.called.should.be.false;
|
|
60
|
+
});
|
|
61
|
+
}));
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { a_descriptor } from '../given/a_descriptor';
|
|
5
|
+
import { given } from '../../../given';
|
|
6
|
+
import { Globals } from '../../../Globals';
|
|
7
|
+
import { QueryTransportMethod } from '../../QueryTransportMethod';
|
|
8
|
+
import { createObservableQueryConnection } from '../../ObservableQueryConnectionFactory';
|
|
9
|
+
import { MultiplexedObservableQueryConnection, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer';
|
|
10
|
+
import { IObservableQueryConnection } from '../../IObservableQueryConnection';
|
|
11
|
+
|
|
12
|
+
import * as sinon from 'sinon';
|
|
13
|
+
|
|
14
|
+
describe('when creating with hub mode and WebSocket transport and high connection count', given(a_descriptor, context => {
|
|
15
|
+
let connection: IObservableQueryConnection<unknown>;
|
|
16
|
+
let warnStub: sinon.SinonStub;
|
|
17
|
+
let originalConnectionCount: number;
|
|
18
|
+
let originalWebSocket: typeof WebSocket;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
originalConnectionCount = Globals.queryConnectionCount;
|
|
22
|
+
Globals.queryDirectMode = false;
|
|
23
|
+
Globals.queryTransportMethod = QueryTransportMethod.WebSocket;
|
|
24
|
+
Globals.queryConnectionCount = 10;
|
|
25
|
+
|
|
26
|
+
warnStub = sinon.stub(console, 'warn');
|
|
27
|
+
|
|
28
|
+
originalWebSocket = global.WebSocket;
|
|
29
|
+
(global as Record<string, unknown>).WebSocket = function () {
|
|
30
|
+
return {
|
|
31
|
+
onopen: null,
|
|
32
|
+
onclose: null,
|
|
33
|
+
onerror: null,
|
|
34
|
+
onmessage: null,
|
|
35
|
+
close: sinon.stub(),
|
|
36
|
+
send: sinon.stub(),
|
|
37
|
+
readyState: 0,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
connection = createObservableQueryConnection(context.descriptor);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
Globals.queryDirectMode = context.originalDirectMode;
|
|
46
|
+
Globals.queryTransportMethod = context.originalTransportMethod;
|
|
47
|
+
Globals.queryConnectionCount = originalConnectionCount;
|
|
48
|
+
warnStub.restore();
|
|
49
|
+
global.WebSocket = originalWebSocket;
|
|
50
|
+
resetSharedMultiplexer();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return a MultiplexedObservableQueryConnection', () => {
|
|
54
|
+
connection.should.be.instanceOf(MultiplexedObservableQueryConnection);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not log a warning', () => {
|
|
58
|
+
warnStub.called.should.be.false;
|
|
59
|
+
});
|
|
60
|
+
}));
|
package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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 { IObservableQueryHubConnection } from '../../IObservableQueryHubConnection';
|
|
5
|
+
import { getOrCreateMultiplexer, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer';
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
|
|
9
|
+
describe('when getting or creating with a changed cache key', () => {
|
|
10
|
+
let factoryCallCount: number;
|
|
11
|
+
let disposedConnectionCount: number;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
factoryCallCount = 0;
|
|
15
|
+
disposedConnectionCount = 0;
|
|
16
|
+
const factory = (): IObservableQueryHubConnection => {
|
|
17
|
+
factoryCallCount++;
|
|
18
|
+
return {
|
|
19
|
+
queryCount: 0,
|
|
20
|
+
lastPingLatency: 0,
|
|
21
|
+
averageLatency: 0,
|
|
22
|
+
subscribe: () => {},
|
|
23
|
+
unsubscribe: () => {},
|
|
24
|
+
dispose: () => { disposedConnectionCount++; },
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
getOrCreateMultiplexer(factory, 'key-one', 1);
|
|
28
|
+
getOrCreateMultiplexer(factory, 'key-two', 1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
resetSharedMultiplexer();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should dispose the connections of the previous multiplexer', () => {
|
|
36
|
+
disposedConnectionCount.should.be.greaterThan(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should create connections for both multiplexers', () => {
|
|
40
|
+
factoryCallCount.should.equal(2);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.ts
ADDED
|
@@ -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 { IObservableQueryHubConnection } from '../../IObservableQueryHubConnection';
|
|
5
|
+
import { getOrCreateMultiplexer, ObservableQueryMultiplexer, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer';
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
|
|
9
|
+
describe('when getting or creating with a new cache key', () => {
|
|
10
|
+
let multiplexer: ObservableQueryMultiplexer;
|
|
11
|
+
let factoryCallCount: number;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
factoryCallCount = 0;
|
|
15
|
+
const factory = (): IObservableQueryHubConnection => {
|
|
16
|
+
factoryCallCount++;
|
|
17
|
+
return { queryCount: 0, lastPingLatency: 0, averageLatency: 0, subscribe: () => {}, unsubscribe: () => {}, dispose: () => {} };
|
|
18
|
+
};
|
|
19
|
+
multiplexer = getOrCreateMultiplexer(factory, 'test-key', 1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
resetSharedMultiplexer();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return a multiplexer', () => {
|
|
27
|
+
multiplexer.should.be.instanceOf(ObservableQueryMultiplexer);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should call the factory to create connections', () => {
|
|
31
|
+
factoryCallCount.should.be.greaterThan(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.ts
ADDED
|
@@ -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 { IObservableQueryHubConnection } from '../../IObservableQueryHubConnection';
|
|
5
|
+
import { getOrCreateMultiplexer, ObservableQueryMultiplexer, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer';
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
|
|
9
|
+
describe('when getting or creating with an explicit size', () => {
|
|
10
|
+
let multiplexer: ObservableQueryMultiplexer;
|
|
11
|
+
let factoryCallCount: number;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
factoryCallCount = 0;
|
|
15
|
+
const factory = (): IObservableQueryHubConnection => {
|
|
16
|
+
factoryCallCount++;
|
|
17
|
+
return { queryCount: 0, lastPingLatency: 0, averageLatency: 0, subscribe: () => {}, unsubscribe: () => {}, dispose: () => {} };
|
|
18
|
+
};
|
|
19
|
+
multiplexer = getOrCreateMultiplexer(factory, 'test-key', 3);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
resetSharedMultiplexer();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should create a multiplexer with the given size', () => {
|
|
27
|
+
multiplexer.size.should.equal(3);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should create exactly that many connections', () => {
|
|
31
|
+
factoryCallCount.should.equal(3);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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 { IObservableQueryHubConnection } from '../../IObservableQueryHubConnection';
|
|
5
|
+
import { getOrCreateMultiplexer, ObservableQueryMultiplexer, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer';
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
|
|
9
|
+
describe('when getting or creating with the same cache key', () => {
|
|
10
|
+
let first: ObservableQueryMultiplexer;
|
|
11
|
+
let second: ObservableQueryMultiplexer;
|
|
12
|
+
let factoryCallCount: number;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
factoryCallCount = 0;
|
|
16
|
+
const factory = (): IObservableQueryHubConnection => {
|
|
17
|
+
factoryCallCount++;
|
|
18
|
+
return { queryCount: 0, lastPingLatency: 0, averageLatency: 0, subscribe: () => {}, unsubscribe: () => {}, dispose: () => {} };
|
|
19
|
+
};
|
|
20
|
+
first = getOrCreateMultiplexer(factory, 'test-key', 1);
|
|
21
|
+
second = getOrCreateMultiplexer(factory, 'test-key', 1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
resetSharedMultiplexer();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return the same instance', () => {
|
|
29
|
+
second.should.equal(first);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should only call the factory once', () => {
|
|
33
|
+
factoryCallCount.should.equal(1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -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 { QueryInstanceCache } from '../../QueryInstanceCache';
|
|
5
|
+
|
|
6
|
+
describe('when acquiring after release', () => {
|
|
7
|
+
let cache: QueryInstanceCache;
|
|
8
|
+
let teardownCalled: boolean;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.useFakeTimers();
|
|
12
|
+
teardownCalled = false;
|
|
13
|
+
cache = new QueryInstanceCache();
|
|
14
|
+
cache.getOrCreate('MyQuery::', () => ({}));
|
|
15
|
+
cache.acquire('MyQuery::');
|
|
16
|
+
cache.setTeardown('MyQuery::', () => { teardownCalled = true; });
|
|
17
|
+
cache.release('MyQuery::');
|
|
18
|
+
|
|
19
|
+
// Re-acquire before the deferred timer fires (simulates StrictMode re-mount).
|
|
20
|
+
cache.acquire('MyQuery::');
|
|
21
|
+
vi.advanceTimersByTime(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.useRealTimers();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should not call teardown', () => teardownCalled.should.be.false);
|
|
29
|
+
it('should keep the entry', () => cache.has('MyQuery::').should.be.true);
|
|
30
|
+
it('should still report as subscribed', () => cache.isSubscribed('MyQuery::').should.be.true);
|
|
31
|
+
});
|
|
@@ -21,13 +21,16 @@ describe('when releasing the only subscriber outside development mode', () => {
|
|
|
21
21
|
vi.useRealTimers();
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it('should call teardown synchronously', () => teardownCalled.should.be.
|
|
24
|
+
it('should not call teardown synchronously', () => teardownCalled.should.be.false);
|
|
25
|
+
it('should keep the entry before the timer fires', () => cache.has('MyQuery::').should.be.true);
|
|
26
|
+
it('should still report as subscribed before the timer fires', () => cache.isSubscribed('MyQuery::').should.be.true);
|
|
25
27
|
|
|
26
28
|
describe('and the deferred timer fires', () => {
|
|
27
29
|
beforeEach(() => {
|
|
28
30
|
vi.advanceTimersByTime(0);
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
it('should call teardown', () => teardownCalled.should.be.true);
|
|
31
34
|
it('should evict the entry', () => cache.has('MyQuery::').should.be.false);
|
|
32
35
|
});
|
|
33
36
|
});
|