@cratis/arc 22.87.0 → 22.87.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 (77) hide show
  1. package/commands/for_Command/when_executing/and_fetch_throws_exception.ts +2 -0
  2. package/commands/for_Command/when_validating/and_fetch_throws_exception.ts +2 -0
  3. package/dist/cjs/queries/ObservableQueryConnection.d.ts +1 -0
  4. package/dist/cjs/queries/ObservableQueryConnection.d.ts.map +1 -1
  5. package/dist/cjs/queries/ObservableQueryConnection.js +11 -6
  6. package/dist/cjs/queries/ObservableQueryConnection.js.map +1 -1
  7. package/dist/cjs/queries/ServerSentEventHubConnection.d.ts +1 -0
  8. package/dist/cjs/queries/ServerSentEventHubConnection.d.ts.map +1 -1
  9. package/dist/cjs/queries/ServerSentEventHubConnection.js +9 -7
  10. package/dist/cjs/queries/ServerSentEventHubConnection.js.map +1 -1
  11. package/dist/cjs/queries/ServerSentEventQueryConnection.d.ts +1 -0
  12. package/dist/cjs/queries/ServerSentEventQueryConnection.d.ts.map +1 -1
  13. package/dist/cjs/queries/ServerSentEventQueryConnection.js +17 -13
  14. package/dist/cjs/queries/ServerSentEventQueryConnection.js.map +1 -1
  15. package/dist/cjs/queries/WebSocketHubConnection.d.ts.map +1 -1
  16. package/dist/cjs/queries/WebSocketHubConnection.js +3 -0
  17. package/dist/cjs/queries/WebSocketHubConnection.js.map +1 -1
  18. package/dist/cjs/validation/rules/NotEmptyRule.js +1 -1
  19. package/dist/cjs/validation/rules/NotEmptyRule.js.map +1 -1
  20. package/dist/esm/commands/for_Command/when_executing/and_fetch_throws_exception.js +1 -0
  21. package/dist/esm/commands/for_Command/when_executing/and_fetch_throws_exception.js.map +1 -1
  22. package/dist/esm/commands/for_Command/when_validating/and_fetch_throws_exception.js +1 -0
  23. package/dist/esm/commands/for_Command/when_validating/and_fetch_throws_exception.js.map +1 -1
  24. package/dist/esm/queries/ObservableQueryConnection.d.ts +1 -0
  25. package/dist/esm/queries/ObservableQueryConnection.d.ts.map +1 -1
  26. package/dist/esm/queries/ObservableQueryConnection.js +11 -6
  27. package/dist/esm/queries/ObservableQueryConnection.js.map +1 -1
  28. package/dist/esm/queries/ServerSentEventHubConnection.d.ts +1 -0
  29. package/dist/esm/queries/ServerSentEventHubConnection.d.ts.map +1 -1
  30. package/dist/esm/queries/ServerSentEventHubConnection.js +9 -7
  31. package/dist/esm/queries/ServerSentEventHubConnection.js.map +1 -1
  32. package/dist/esm/queries/ServerSentEventQueryConnection.d.ts +1 -0
  33. package/dist/esm/queries/ServerSentEventQueryConnection.d.ts.map +1 -1
  34. package/dist/esm/queries/ServerSentEventQueryConnection.js +17 -13
  35. package/dist/esm/queries/ServerSentEventQueryConnection.js.map +1 -1
  36. package/dist/esm/queries/WebSocketHubConnection.d.ts.map +1 -1
  37. package/dist/esm/queries/WebSocketHubConnection.js +3 -0
  38. package/dist/esm/queries/WebSocketHubConnection.js.map +1 -1
  39. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_server_validation_failing.d.ts +2 -0
  40. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_server_validation_failing.d.ts.map +1 -0
  41. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_server_validation_failing.js +36 -0
  42. package/dist/esm/queries/for_ObservableQueryFor/when_performing/with_server_validation_failing.js.map +1 -0
  43. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_server_validation_failing.d.ts +2 -0
  44. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_server_validation_failing.d.ts.map +1 -0
  45. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_server_validation_failing.js +110 -0
  46. package/dist/esm/queries/for_ObservableQueryFor/when_subscribing/with_server_validation_failing.js.map +1 -0
  47. package/dist/esm/queries/for_QueryFor/when_performing/with_server_validation_failing.d.ts +2 -0
  48. package/dist/esm/queries/for_QueryFor/when_performing/with_server_validation_failing.d.ts.map +1 -0
  49. package/dist/esm/queries/for_QueryFor/when_performing/with_server_validation_failing.js +36 -0
  50. package/dist/esm/queries/for_QueryFor/when_performing/with_server_validation_failing.js.map +1 -0
  51. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  52. package/dist/esm/validation/rules/NotEmptyRule.js +1 -1
  53. package/dist/esm/validation/rules/NotEmptyRule.js.map +1 -1
  54. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_a_byte_order_mark.d.ts +2 -0
  55. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_a_byte_order_mark.d.ts.map +1 -0
  56. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_a_byte_order_mark.js +8 -0
  57. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_a_byte_order_mark.js.map +1 -0
  58. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_mixed_unicode_whitespace.d.ts +2 -0
  59. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_mixed_unicode_whitespace.d.ts.map +1 -0
  60. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_mixed_unicode_whitespace.js +8 -0
  61. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_mixed_unicode_whitespace.js.map +1 -0
  62. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_next_line_whitespace.d.ts +2 -0
  63. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_next_line_whitespace.d.ts.map +1 -0
  64. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_next_line_whitespace.js +8 -0
  65. package/dist/esm/validation/rules/for_NotEmptyRule/when_validating/with_next_line_whitespace.js.map +1 -0
  66. package/package.json +1 -1
  67. package/queries/ObservableQueryConnection.ts +13 -8
  68. package/queries/ServerSentEventHubConnection.ts +10 -9
  69. package/queries/ServerSentEventQueryConnection.ts +18 -13
  70. package/queries/WebSocketHubConnection.ts +3 -0
  71. package/queries/for_ObservableQueryFor/when_performing/with_server_validation_failing.ts +43 -0
  72. package/queries/for_ObservableQueryFor/when_subscribing/with_server_validation_failing.ts +130 -0
  73. package/queries/for_QueryFor/when_performing/with_server_validation_failing.ts +43 -0
  74. package/validation/rules/NotEmptyRule.ts +1 -1
  75. package/validation/rules/for_NotEmptyRule/when_validating/with_a_byte_order_mark.ts +11 -0
  76. package/validation/rules/for_NotEmptyRule/when_validating/with_mixed_unicode_whitespace.ts +11 -0
  77. package/validation/rules/for_NotEmptyRule/when_validating/with_next_line_whitespace.ts +11 -0
@@ -16,7 +16,7 @@ import { PropertyRule } from '../PropertyRule.js';
16
16
  if (value === null || value === undefined) {
17
17
  return false;
18
18
  }
19
- if (typeof value === 'string' && value.trim() === '') {
19
+ if (typeof value === 'string' && /^\p{White_Space}*$/u.test(value)) {
20
20
  return false;
21
21
  }
22
22
  if (Array.isArray(value) && value.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"NotEmptyRule.js","sources":["../../../../validation/rules/NotEmptyRule.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { PropertyRule } from '../PropertyRule.js';\n\n/**\n * Validation rule that checks if a value is not null, undefined, or empty.\n * @template T The type being validated.\n */\nexport class NotEmptyRule<T> extends PropertyRule<T, unknown> {\n /**\n * Initializes a new instance of the {@link NotEmptyRule} class.\n * @param propertyAccessor Function to access the property value from the instance.\n */\n constructor(propertyAccessor: (instance: T) => unknown) {\n super(propertyAccessor, \"'{PropertyName}' must not be empty.\");\n }\n\n /** @inheritdoc */\n protected isValid(value: unknown): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return false;\n }\n if (Array.isArray(value) && value.length === 0) {\n return false;\n }\n return true;\n }\n}\n\n/**\n * Validation rule that checks if a value is not null or undefined.\n * @template T The type being validated.\n */\nexport class NotNullRule<T> extends PropertyRule<T, unknown> {\n /**\n * Initializes a new instance of the {@link NotNullRule} class.\n * @param propertyAccessor Function to access the property value from the instance.\n */\n constructor(propertyAccessor: (instance: T) => unknown) {\n super(propertyAccessor, \"'{PropertyName}' must not be empty.\");\n }\n\n /** @inheritdoc */\n protected isValid(value: unknown): boolean {\n return value !== null && value !== undefined;\n }\n}\n"],"names":["NotEmptyRule","PropertyRule","propertyAccessor","isValid","value","undefined","trim","Array","isArray","length","NotNullRule"],"mappings":";;AAAA;AACA;AAIA;;;IAIO,MAAMA,YAAAA,SAAwBC,YAAAA,CAAAA;AACjC;;;QAIA,WAAA,CAAYC,gBAA0C,CAAE;AACpD,QAAA,KAAK,CAACA,gBAAAA,EAAkB,qCAAA,CAAA;AAC5B,IAAA;AAEA,uBACUC,OAAAA,CAAQC,KAAc,EAAW;QACvC,IAAIA,KAAAA,KAAU,IAAA,IAAQA,KAAAA,KAAUC,SAAAA,EAAW;YACvC,OAAO,KAAA;AACX,QAAA;AACA,QAAA,IAAI,OAAOD,KAAAA,KAAU,QAAA,IAAYA,KAAAA,CAAME,IAAI,OAAO,EAAA,EAAI;YAClD,OAAO,KAAA;AACX,QAAA;AACA,QAAA,IAAIC,MAAMC,OAAO,CAACJ,UAAUA,KAAAA,CAAMK,MAAM,KAAK,CAAA,EAAG;YAC5C,OAAO,KAAA;AACX,QAAA;QACA,OAAO,IAAA;AACX,IAAA;AACJ;AAEA;;;IAIO,MAAMC,WAAAA,SAAuBT,YAAAA,CAAAA;AAChC;;;QAIA,WAAA,CAAYC,gBAA0C,CAAE;AACpD,QAAA,KAAK,CAACA,gBAAAA,EAAkB,qCAAA,CAAA;AAC5B,IAAA;AAEA,uBACUC,OAAAA,CAAQC,KAAc,EAAW;QACvC,OAAOA,KAAAA,KAAU,QAAQA,KAAAA,KAAUC,SAAAA;AACvC,IAAA;AACJ;;"}
1
+ {"version":3,"file":"NotEmptyRule.js","sources":["../../../../validation/rules/NotEmptyRule.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { PropertyRule } from '../PropertyRule.js';\n\n/**\n * Validation rule that checks if a value is not null, undefined, or empty.\n * @template T The type being validated.\n */\nexport class NotEmptyRule<T> extends PropertyRule<T, unknown> {\n /**\n * Initializes a new instance of the {@link NotEmptyRule} class.\n * @param propertyAccessor Function to access the property value from the instance.\n */\n constructor(propertyAccessor: (instance: T) => unknown) {\n super(propertyAccessor, \"'{PropertyName}' must not be empty.\");\n }\n\n /** @inheritdoc */\n protected isValid(value: unknown): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n if (typeof value === 'string' && /^\\p{White_Space}*$/u.test(value)) {\n return false;\n }\n if (Array.isArray(value) && value.length === 0) {\n return false;\n }\n return true;\n }\n}\n\n/**\n * Validation rule that checks if a value is not null or undefined.\n * @template T The type being validated.\n */\nexport class NotNullRule<T> extends PropertyRule<T, unknown> {\n /**\n * Initializes a new instance of the {@link NotNullRule} class.\n * @param propertyAccessor Function to access the property value from the instance.\n */\n constructor(propertyAccessor: (instance: T) => unknown) {\n super(propertyAccessor, \"'{PropertyName}' must not be empty.\");\n }\n\n /** @inheritdoc */\n protected isValid(value: unknown): boolean {\n return value !== null && value !== undefined;\n }\n}\n"],"names":["NotEmptyRule","PropertyRule","propertyAccessor","isValid","value","undefined","test","Array","isArray","length","NotNullRule"],"mappings":";;AAAA;AACA;AAIA;;;IAIO,MAAMA,YAAAA,SAAwBC,YAAAA,CAAAA;AACjC;;;QAIA,WAAA,CAAYC,gBAA0C,CAAE;AACpD,QAAA,KAAK,CAACA,gBAAAA,EAAkB,qCAAA,CAAA;AAC5B,IAAA;AAEA,uBACUC,OAAAA,CAAQC,KAAc,EAAW;QACvC,IAAIA,KAAAA,KAAU,IAAA,IAAQA,KAAAA,KAAUC,SAAAA,EAAW;YACvC,OAAO,KAAA;AACX,QAAA;AACA,QAAA,IAAI,OAAOD,KAAAA,KAAU,QAAA,IAAY,qBAAA,CAAsBE,IAAI,CAACF,KAAAA,CAAAA,EAAQ;YAChE,OAAO,KAAA;AACX,QAAA;AACA,QAAA,IAAIG,MAAMC,OAAO,CAACJ,UAAUA,KAAAA,CAAMK,MAAM,KAAK,CAAA,EAAG;YAC5C,OAAO,KAAA;AACX,QAAA;QACA,OAAO,IAAA;AACX,IAAA;AACJ;AAEA;;;IAIO,MAAMC,WAAAA,SAAuBT,YAAAA,CAAAA;AAChC;;;QAIA,WAAA,CAAYC,gBAA0C,CAAE;AACpD,QAAA,KAAK,CAACA,gBAAAA,EAAkB,qCAAA,CAAA;AAC5B,IAAA;AAEA,uBACUC,OAAAA,CAAQC,KAAc,EAAW;QACvC,OAAOA,KAAAA,KAAU,QAAQA,KAAAA,KAAUC,SAAAA;AACvC,IAAA;AACJ;;"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=with_a_byte_order_mark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with_a_byte_order_mark.d.ts","sourceRoot":"","sources":["../../../../../../validation/rules/for_NotEmptyRule/when_validating/with_a_byte_order_mark.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { NotEmptyRule } from '../../NotEmptyRule.js';
2
+ describe('with a byte order mark', () => {
3
+ it('should return no validation errors', () => {
4
+ const rule = new NotEmptyRule(t => t.value);
5
+ rule.validate({ value: '\uFEFF' }, 'value').should.have.lengthOf(0);
6
+ });
7
+ });
8
+ //# sourceMappingURL=with_a_byte_order_mark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with_a_byte_order_mark.js","sourceRoot":"","sources":["../../../../../../validation/rules/for_NotEmptyRule/when_validating/with_a_byte_order_mark.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC1C,MAAM,IAAI,GAAG,IAAI,YAAY,CAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC/D,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=with_mixed_unicode_whitespace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with_mixed_unicode_whitespace.d.ts","sourceRoot":"","sources":["../../../../../../validation/rules/for_NotEmptyRule/when_validating/with_mixed_unicode_whitespace.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { NotEmptyRule } from '../../NotEmptyRule.js';
2
+ describe('with mixed unicode whitespace', () => {
3
+ it('should return one validation error', () => {
4
+ const rule = new NotEmptyRule(t => t.value);
5
+ rule.validate({ value: '\t \u3000\n' }, 'value').should.have.lengthOf(1);
6
+ });
7
+ });
8
+ //# sourceMappingURL=with_mixed_unicode_whitespace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with_mixed_unicode_whitespace.js","sourceRoot":"","sources":["../../../../../../validation/rules/for_NotEmptyRule/when_validating/with_mixed_unicode_whitespace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC1C,MAAM,IAAI,GAAG,IAAI,YAAY,CAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC/D,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=with_next_line_whitespace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with_next_line_whitespace.d.ts","sourceRoot":"","sources":["../../../../../../validation/rules/for_NotEmptyRule/when_validating/with_next_line_whitespace.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { NotEmptyRule } from '../../NotEmptyRule.js';
2
+ describe('with next line whitespace', () => {
3
+ it('should return one validation error', () => {
4
+ const rule = new NotEmptyRule(t => t.value);
5
+ rule.validate({ value: '\u0085' }, 'value').should.have.lengthOf(1);
6
+ });
7
+ });
8
+ //# sourceMappingURL=with_next_line_whitespace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with_next_line_whitespace.js","sourceRoot":"","sources":["../../../../../../validation/rules/for_NotEmptyRule/when_validating/with_next_line_whitespace.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC1C,MAAM,IAAI,GAAG,IAAI,YAAY,CAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC/D,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@cratis/arc",
4
- "version": "22.87.0",
4
+ "version": "22.87.2",
5
5
  "description": "TypeScript command, query, validation, identity, and messaging runtime for Cratis Arc — the CQRS application framework for ASP.NET Core — used by generated proxies.",
6
6
  "keywords": [
7
7
  "cratis",
@@ -173,20 +173,25 @@ export class ObservableQueryConnection<TDataType> implements IObservableQueryCon
173
173
  // For new-style messages (type === 'Data') the query result is wrapped in message.data.
174
174
  // For legacy/backward-compatible messages (no type) the entire message IS the query result.
175
175
  const data = message.type === WebSocketMessageType.Data ? message.data : message;
176
- const result = data as QueryResult<TDataType>;
177
-
178
- if (result?.isAuthorized === false) {
179
- this.handleUnauthorized(result, dataReceived);
180
- return;
181
- }
182
-
183
- dataReceived(result);
176
+ this.handleResult(data as QueryResult<TDataType>, dataReceived);
184
177
  }
185
178
  } catch (error) {
186
179
  console.error('Error parsing WebSocket message:', error);
187
180
  }
188
181
  }
189
182
 
183
+ private handleResult(result: QueryResult<TDataType>, dataReceived: DataReceived<TDataType>) {
184
+ if (result?.isAuthorized === false) {
185
+ this.handleUnauthorized(result, dataReceived);
186
+ return;
187
+ }
188
+
189
+ if (result?.isValid === false) {
190
+ this.disconnect();
191
+ }
192
+ dataReceived(result);
193
+ }
194
+
190
195
  /**
191
196
  * Handles the terminal result the server sends when the caller is no longer authorized for the query it is
192
197
  * subscribed to. The subscription is over for good, so the result is delivered and the automatic reconnect is
@@ -390,24 +390,25 @@ export class ServerSentEventHubConnection implements IObservableQueryHubConnecti
390
390
  }
391
391
 
392
392
  private handleQueryResult(message: HubMessage): void {
393
- if (!message.queryId) return;
394
-
395
- const sub = this._subscriptions.get(message.queryId);
396
- if (!sub || !this.isMessageForCurrentSubscription(message)) return;
397
-
398
393
  const result = message.payload as QueryResult<any>;
399
- sub.callback(result);
394
+ this.deliverResult(message, result, result?.isValid === false);
400
395
  }
401
396
 
402
397
  private handleUnauthorized(message: HubMessage): void {
398
+ this.deliverResult(message, QueryResult.unauthorized(), true);
399
+ }
400
+
401
+ private deliverResult(message: HubMessage, result: QueryResult<any>, terminal: boolean): void {
403
402
  if (!message.queryId) return;
404
403
 
405
404
  const sub = this._subscriptions.get(message.queryId);
406
405
  if (!sub || !this.isMessageForCurrentSubscription(message)) return;
407
406
 
408
- this._subscriptions.delete(message.queryId);
409
- this._pendingSubscriptions.delete(message.queryId);
410
- sub.callback(QueryResult.unauthorized());
407
+ if (terminal) {
408
+ this._subscriptions.delete(message.queryId);
409
+ this._pendingSubscriptions.delete(message.queryId);
410
+ }
411
+ sub.callback(result);
411
412
  }
412
413
 
413
414
  private sendSubscribe(
@@ -61,19 +61,7 @@ export class ServerSentEventQueryConnection<TDataType> implements IObservableQue
61
61
 
62
62
  this._eventSource = Globals.eventSourceFactory ? Globals.eventSourceFactory(url) : new EventSource(url);
63
63
 
64
- this._eventSource.onmessage = (event: MessageEvent) => {
65
- if (this._disconnected) return;
66
- try {
67
- const result = JSON.parse(event.data as string) as QueryResult<TDataType>;
68
- if ((result as unknown as { isAuthorized?: boolean })?.isAuthorized === false) {
69
- this.handleUnauthorized(url, result, dataReceived);
70
- return;
71
- }
72
- dataReceived(result);
73
- } catch (error) {
74
- console.error('SSE: error parsing message', error);
75
- }
76
- };
64
+ this._eventSource.onmessage = (event: MessageEvent) => this.handleMessage(event, url, dataReceived);
77
65
 
78
66
  this._eventSource.onerror = () => {
79
67
  if (this._disconnected || this._unauthorized) return;
@@ -89,6 +77,23 @@ export class ServerSentEventQueryConnection<TDataType> implements IObservableQue
89
77
  this._eventSource = undefined;
90
78
  }
91
79
 
80
+ private handleMessage(event: MessageEvent, url: string, dataReceived: DataReceived<TDataType>): void {
81
+ if (this._disconnected) return;
82
+ try {
83
+ const result = JSON.parse(event.data as string) as QueryResult<TDataType>;
84
+ if ((result as unknown as { isAuthorized?: boolean })?.isAuthorized === false) {
85
+ this.handleUnauthorized(url, result, dataReceived);
86
+ return;
87
+ }
88
+ if (result?.isValid === false) {
89
+ this.disconnect();
90
+ }
91
+ dataReceived(result);
92
+ } catch (error) {
93
+ console.error('SSE: error parsing message', error);
94
+ }
95
+ }
96
+
92
97
  /**
93
98
  * Handles the terminal result the server sends when the caller is no longer authorized for the query it is
94
99
  * subscribed to.
@@ -402,6 +402,9 @@ export class WebSocketHubConnection {
402
402
  if (!sub || !this.isMessageForCurrentSubscription(message)) return;
403
403
 
404
404
  const result = message.payload as QueryResult<any>;
405
+ if (result?.isValid === false) {
406
+ this._subscriptions.delete(message.queryId);
407
+ }
405
408
  sub.callback(result);
406
409
  }
407
410
 
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import sinon from 'sinon';
5
+ import { an_observable_query_for } from '../given/an_observable_query_for.js';
6
+ import { given } from '../../../given.js';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper.js';
8
+ import { QueryResult } from '../../QueryResult.js';
9
+ import { ValidationResult } from '../../../validation/ValidationResult.js';
10
+
11
+ describe('with server validation failing', given(an_observable_query_for, context => {
12
+ let result: QueryResult<string>;
13
+ let fetchHelper: ReturnType<typeof createFetchHelper>;
14
+
15
+ beforeEach(async () => {
16
+ fetchHelper = createFetchHelper();
17
+ fetchHelper.stubFetch().resolves({
18
+ ok: false,
19
+ status: 400,
20
+ json: sinon.stub().resolves({
21
+ data: null,
22
+ isSuccess: false,
23
+ isAuthorized: true,
24
+ isValid: false,
25
+ hasExceptions: false,
26
+ validationResults: [{ severity: 2, message: 'Id is invalid.', members: ['id'], state: {} }],
27
+ exceptionMessages: [],
28
+ exceptionStackTrace: '',
29
+ paging: { page: 0, size: 0, totalItems: 0, totalPages: 0 }
30
+ })
31
+ } as unknown as Response);
32
+ context.query.setOrigin('https://example.com');
33
+ result = await context.query.perform({ id: 'invalid' });
34
+ });
35
+
36
+ afterEach(() => fetchHelper.restore());
37
+
38
+ it('should be invalid', () => result.isValid.should.be.false);
39
+ it('should not be successful', () => result.isSuccess.should.be.false);
40
+ it('should map the validation result', () => result.validationResults[0].should.be.instanceOf(ValidationResult));
41
+ it('should preserve the message', () => result.validationResults[0].message.should.equal('Id is invalid.'));
42
+ it('should preserve the members', () => result.validationResults[0].members.should.deep.equal(['id']));
43
+ }));
@@ -0,0 +1,130 @@
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 { Globals } from '../../../Globals.js';
6
+ import { createFetchHelper } from '../../../helpers/fetchHelper.js';
7
+ import { QueryTransportMethod } from '../../QueryTransportMethod.js';
8
+ import { QueryResult } from '../../QueryResult.js';
9
+ import { ObservableQuerySubscription } from '../../ObservableQuerySubscription.js';
10
+ import { getSharedMultiplexer, resetSharedMultiplexer } from '../../ObservableQueryMultiplexer.js';
11
+ import { an_observable_query_for } from '../given/an_observable_query_for.js';
12
+
13
+ for (const direct of [true, false]) {
14
+ for (const transport of [QueryTransportMethod.WebSocket, QueryTransportMethod.ServerSentEvents]) {
15
+ describe(`with server validation failing over ${direct ? 'direct' : 'hub'} ${transport}`, () => {
16
+ let received: QueryResult<string>[];
17
+ let subscription: ObservableQuerySubscription<string>;
18
+ let originalDirect: boolean;
19
+ let originalTransport: QueryTransportMethod;
20
+ let originalWebSocket: typeof WebSocket;
21
+ let originalEventSource: typeof EventSource;
22
+ let originalEventSourceFactory: typeof Globals.eventSourceFactory;
23
+ let fetchHelper: ReturnType<typeof createFetchHelper>;
24
+ let stream: {
25
+ readyState: number;
26
+ onopen: (() => void) | null;
27
+ onclose: (() => void) | null;
28
+ onerror: (() => void) | null;
29
+ onmessage: ((event: { data: string }) => void) | null;
30
+ send: sinon.SinonStub;
31
+ close: sinon.SinonStub;
32
+ };
33
+ const rejection = {
34
+ data: null,
35
+ isSuccess: false,
36
+ isAuthorized: true,
37
+ isValid: false,
38
+ hasExceptions: false,
39
+ validationResults: [{ severity: 2, message: 'Id is invalid.', members: ['id'], state: {} }],
40
+ exceptionMessages: [],
41
+ exceptionStackTrace: '',
42
+ paging: { page: 0, size: 0, totalItems: 0, totalPages: 0 }
43
+ };
44
+
45
+ const sendDirectRejection = () => {
46
+ stream.onmessage?.({ data: JSON.stringify(rejection) });
47
+ // A server closes the direct transport after the one rejection frame.
48
+ stream.onclose?.();
49
+ stream.onmessage?.({ data: JSON.stringify(rejection) });
50
+ };
51
+
52
+ const hubQueryId = (fetchStub: sinon.SinonStub): string => {
53
+ if (transport === QueryTransportMethod.ServerSentEvents) {
54
+ stream.onmessage?.({ data: JSON.stringify({ type: 'Connected', payload: 'connection-1' }) });
55
+ return JSON.parse(fetchStub.firstCall.args[1].body).queryId;
56
+ }
57
+ return JSON.parse(stream.send.firstCall.args[0]).queryId;
58
+ };
59
+
60
+ const sendHubRejection = (fetchStub: sinon.SinonStub) => {
61
+ const queryId = hubQueryId(fetchStub);
62
+ stream.onmessage?.({ data: JSON.stringify({ type: 'QueryResult', queryId, payload: rejection }) });
63
+ // A terminal subscription must not receive a stale duplicate frame.
64
+ stream.onmessage?.({ data: JSON.stringify({ type: 'QueryResult', queryId, payload: rejection }) });
65
+ };
66
+
67
+ beforeEach(() => {
68
+ originalDirect = Globals.queryDirectMode;
69
+ originalTransport = Globals.queryTransportMethod;
70
+ originalWebSocket = globalThis.WebSocket;
71
+ originalEventSource = globalThis.EventSource;
72
+ originalEventSourceFactory = Globals.eventSourceFactory;
73
+ resetSharedMultiplexer();
74
+ Globals.queryDirectMode = direct;
75
+ Globals.queryTransportMethod = transport;
76
+ received = [];
77
+ stream = {
78
+ readyState: 0,
79
+ onopen: null,
80
+ onclose: null,
81
+ onerror: null,
82
+ onmessage: null,
83
+ send: sinon.stub(),
84
+ close: sinon.stub()
85
+ };
86
+ const socketStub = sinon.stub().returns(stream);
87
+ Object.assign(socketStub, { CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3 });
88
+ (globalThis as Record<string, unknown>)['WebSocket'] = socketStub;
89
+ (globalThis as Record<string, unknown>)['EventSource'] = Object.assign(sinon.stub().returns(stream), { OPEN: 1 });
90
+ Globals.eventSourceFactory = () => stream as unknown as EventSource;
91
+ fetchHelper = createFetchHelper();
92
+ const fetchStub = fetchHelper.stubFetch().resolves({ ok: true } as Response);
93
+ const query = new an_observable_query_for().query;
94
+ query.setOrigin('https://example.com');
95
+ subscription = query.subscribe(result => received.push(result), { id: 'invalid' });
96
+ stream.readyState = 1;
97
+ stream.onopen?.();
98
+
99
+ if (direct) {
100
+ sendDirectRejection();
101
+ } else {
102
+ sendHubRejection(fetchStub);
103
+ }
104
+ });
105
+
106
+ afterEach(() => {
107
+ subscription?.unsubscribe();
108
+ resetSharedMultiplexer();
109
+ Globals.queryDirectMode = originalDirect;
110
+ Globals.queryTransportMethod = originalTransport;
111
+ Globals.eventSourceFactory = originalEventSourceFactory;
112
+ globalThis.WebSocket = originalWebSocket;
113
+ globalThis.EventSource = originalEventSource;
114
+ fetchHelper.restore();
115
+ });
116
+
117
+ it('should deliver one result to the subscriber', () => received.should.have.lengthOf(1));
118
+ it('should be invalid', () => received[0].isValid.should.be.false);
119
+ it('should not be successful', () => received[0].isSuccess.should.be.false);
120
+ it('should preserve the validation results', () => received[0].validationResults.should.deep.equal(rejection.validationResults));
121
+ it('should settle the rejected subscription', () => {
122
+ if (direct) {
123
+ stream.close.should.have.been.calledOnce;
124
+ } else {
125
+ getSharedMultiplexer()!.getConnectionsSnapshot().every(connection => connection.queryCount === 0).should.be.true;
126
+ }
127
+ });
128
+ });
129
+ }
130
+ }
@@ -0,0 +1,43 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import sinon from 'sinon';
5
+ import { a_query_for } from '../given/a_query_for.js';
6
+ import { given } from '../../../given.js';
7
+ import { createFetchHelper } from '../../../helpers/fetchHelper.js';
8
+ import { QueryResult } from '../../QueryResult.js';
9
+ import { ValidationResult } from '../../../validation/ValidationResult.js';
10
+
11
+ describe('with server validation failing', given(a_query_for, context => {
12
+ let result: QueryResult<string>;
13
+ let fetchHelper: ReturnType<typeof createFetchHelper>;
14
+
15
+ beforeEach(async () => {
16
+ fetchHelper = createFetchHelper();
17
+ fetchHelper.stubFetch().resolves({
18
+ ok: false,
19
+ status: 400,
20
+ json: sinon.stub().resolves({
21
+ data: null,
22
+ isSuccess: false,
23
+ isAuthorized: true,
24
+ isValid: false,
25
+ hasExceptions: false,
26
+ validationResults: [{ severity: 2, message: 'Id is invalid.', members: ['id'], state: {} }],
27
+ exceptionMessages: [],
28
+ exceptionStackTrace: '',
29
+ paging: { page: 0, size: 0, totalItems: 0, totalPages: 0 }
30
+ })
31
+ } as unknown as Response);
32
+ context.query.setOrigin('https://example.com');
33
+ result = await context.query.perform({ id: 'invalid' });
34
+ });
35
+
36
+ afterEach(() => fetchHelper.restore());
37
+
38
+ it('should be invalid', () => result.isValid.should.be.false);
39
+ it('should not be successful', () => result.isSuccess.should.be.false);
40
+ it('should map the validation result', () => result.validationResults[0].should.be.instanceOf(ValidationResult));
41
+ it('should preserve the message', () => result.validationResults[0].message.should.equal('Id is invalid.'));
42
+ it('should preserve the members', () => result.validationResults[0].members.should.deep.equal(['id']));
43
+ }));
@@ -21,7 +21,7 @@ export class NotEmptyRule<T> extends PropertyRule<T, unknown> {
21
21
  if (value === null || value === undefined) {
22
22
  return false;
23
23
  }
24
- if (typeof value === 'string' && value.trim() === '') {
24
+ if (typeof value === 'string' && /^\p{White_Space}*$/u.test(value)) {
25
25
  return false;
26
26
  }
27
27
  if (Array.isArray(value) && value.length === 0) {
@@ -0,0 +1,11 @@
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 { NotEmptyRule } from '../../NotEmptyRule.js';
5
+
6
+ describe('with a byte order mark', () => {
7
+ it('should return no validation errors', () => {
8
+ const rule = new NotEmptyRule<{ value: string }>(t => t.value);
9
+ rule.validate({ value: '\uFEFF' }, 'value').should.have.lengthOf(0);
10
+ });
11
+ });
@@ -0,0 +1,11 @@
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 { NotEmptyRule } from '../../NotEmptyRule.js';
5
+
6
+ describe('with mixed unicode whitespace', () => {
7
+ it('should return one validation error', () => {
8
+ const rule = new NotEmptyRule<{ value: string }>(t => t.value);
9
+ rule.validate({ value: '\t \u3000\n' }, 'value').should.have.lengthOf(1);
10
+ });
11
+ });
@@ -0,0 +1,11 @@
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 { NotEmptyRule } from '../../NotEmptyRule.js';
5
+
6
+ describe('with next line whitespace', () => {
7
+ it('should return one validation error', () => {
8
+ const rule = new NotEmptyRule<{ value: string }>(t => t.value);
9
+ rule.validate({ value: '\u0085' }, 'value').should.have.lengthOf(1);
10
+ });
11
+ });