@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.
Files changed (84) hide show
  1. package/dist/cjs/queries/ObservableQueryConnectionFactory.js +11 -2
  2. package/dist/cjs/queries/ObservableQueryConnectionFactory.js.map +1 -1
  3. package/dist/cjs/queries/ObservableQueryMultiplexer.d.ts +1 -1
  4. package/dist/cjs/queries/ObservableQueryMultiplexer.d.ts.map +1 -1
  5. package/dist/cjs/queries/ObservableQueryMultiplexer.js +2 -2
  6. package/dist/cjs/queries/ObservableQueryMultiplexer.js.map +1 -1
  7. package/dist/cjs/queries/QueryInstanceCache.d.ts +0 -1
  8. package/dist/cjs/queries/QueryInstanceCache.d.ts.map +1 -1
  9. package/dist/cjs/queries/QueryInstanceCache.js +10 -25
  10. package/dist/cjs/queries/QueryInstanceCache.js.map +1 -1
  11. package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.d.ts +2 -0
  12. 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
  13. package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.d.ts +2 -0
  14. 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
  15. package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts +2 -0
  16. package/dist/cjs/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts.map +1 -0
  17. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts +2 -0
  18. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts.map +1 -0
  19. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts +2 -0
  20. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts.map +1 -0
  21. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts +2 -0
  22. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts.map +1 -0
  23. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts +2 -0
  24. package/dist/cjs/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts.map +1 -0
  25. package/dist/cjs/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts +2 -0
  26. package/dist/cjs/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts.map +1 -0
  27. package/dist/esm/queries/ObservableQueryConnectionFactory.js +11 -2
  28. package/dist/esm/queries/ObservableQueryConnectionFactory.js.map +1 -1
  29. package/dist/esm/queries/ObservableQueryMultiplexer.d.ts +1 -1
  30. package/dist/esm/queries/ObservableQueryMultiplexer.d.ts.map +1 -1
  31. package/dist/esm/queries/ObservableQueryMultiplexer.js +2 -2
  32. package/dist/esm/queries/ObservableQueryMultiplexer.js.map +1 -1
  33. package/dist/esm/queries/QueryInstanceCache.d.ts +0 -1
  34. package/dist/esm/queries/QueryInstanceCache.d.ts.map +1 -1
  35. package/dist/esm/queries/QueryInstanceCache.js +10 -25
  36. package/dist/esm/queries/QueryInstanceCache.js.map +1 -1
  37. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.d.ts +2 -0
  38. 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
  39. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.js +55 -0
  40. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.js.map +1 -0
  41. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.d.ts +2 -0
  42. 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
  43. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.js +49 -0
  44. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.js.map +1 -0
  45. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts +2 -0
  46. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.d.ts.map +1 -0
  47. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.js +48 -0
  48. package/dist/esm/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.js.map +1 -0
  49. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts +2 -0
  50. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.d.ts.map +1 -0
  51. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.js +32 -0
  52. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.js.map +1 -0
  53. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts +2 -0
  54. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.d.ts.map +1 -0
  55. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.js +23 -0
  56. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.js.map +1 -0
  57. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts +2 -0
  58. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.d.ts.map +1 -0
  59. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.js +23 -0
  60. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.js.map +1 -0
  61. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts +2 -0
  62. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.d.ts.map +1 -0
  63. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.js +25 -0
  64. package/dist/esm/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.js.map +1 -0
  65. package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts +2 -0
  66. package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.d.ts.map +1 -0
  67. package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.js +23 -0
  68. package/dist/esm/queries/for_QueryInstanceCache/when_acquiring/after_release.js.map +1 -0
  69. package/dist/esm/queries/for_QueryInstanceCache/when_releasing/the_only_subscriber_outside_development_mode.js +4 -1
  70. package/dist/esm/queries/for_QueryInstanceCache/when_releasing/the_only_subscriber_outside_development_mode.js.map +1 -1
  71. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +1 -1
  73. package/queries/ObservableQueryConnectionFactory.ts +23 -2
  74. package/queries/ObservableQueryMultiplexer.ts +3 -2
  75. package/queries/QueryInstanceCache.ts +27 -42
  76. package/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_exceeding_safe_limit.ts +69 -0
  77. package/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_sse_transport_and_connection_count_within_safe_limit.ts +61 -0
  78. package/queries/for_ObservableQueryConnectionFactory/when_creating/with_hub_mode_and_websocket_transport_and_high_connection_count.ts +60 -0
  79. package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_changed_cache_key.ts +42 -0
  80. package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_a_new_cache_key.ts +34 -0
  81. package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_an_explicit_size.ts +33 -0
  82. package/queries/for_ObservableQueryMultiplexer/when_getting_or_creating/with_the_same_cache_key.ts +35 -0
  83. package/queries/for_QueryInstanceCache/when_acquiring/after_release.ts +31 -0
  84. package/queries/for_QueryInstanceCache/when_releasing/the_only_subscriber_outside_development_mode.ts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/arc",
3
- "version": "20.3.1",
3
+ "version": "20.3.2",
4
4
  "description": "",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
@@ -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 cacheKey = `${Globals.queryConnectionCount}|${transport}|${descriptor.origin}|${descriptor.apiBasePath}|${descriptor.microservice}`;
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(Globals.queryConnectionCount, connectionFactory);
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 when running in development mode.
54
- * Allows React StrictMode re-mounts to cancel the pending teardown
55
- * so the connection is reused instead of torn down and recreated.
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 When true, teardown is deferred on release so React StrictMode
76
- * re-mounts can re-acquire the entry without an unnecessary disconnect/reconnect cycle.
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
- this._development = development;
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} in development mode),
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 the teardown
234
- * function is called (if set) and the entry is evicted.
235
- *
236
- * In development mode, both teardown and eviction are deferred by one microtask so that
237
- * React StrictMode re-mounts can re-acquire the entry and cancel the cleanup. This prevents
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
- if (this._development) {
250
- // Defer both teardown and deletion so StrictMode re-mounts can cancel.
251
- entry.pendingCleanup = setTimeout(() => {
252
- const current = this._entries.get(key);
253
-
254
- if (current && current.subscriberCount <= 0) {
255
- current.subscribed = false;
256
- current.teardown?.();
257
- current.teardown = undefined;
258
- current.pendingCleanup = undefined;
259
- this._entries.delete(key);
260
- }
261
- }, 0);
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
+ }));
@@ -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
+ });
@@ -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
+
@@ -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
+ });
@@ -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.true);
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
  });