@cratis/chronicle 6.9.1 → 6.10.0

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 (268) hide show
  1. package/ChronicleClient.ts +52 -13
  2. package/ChronicleOptions.spec.ts +54 -0
  3. package/ChronicleOptions.ts +20 -5
  4. package/EventStore.ts +7 -1
  5. package/README.md +2 -3
  6. package/artifacts/DefaultClientArtifactsProvider.ts +4 -1
  7. package/connection/ChronicleConnection.auth.spec.ts +219 -0
  8. package/connection/ChronicleConnection.ts +70 -31
  9. package/connection/ChronicleServices.ts +1 -1
  10. package/connection/CompatibilityPreflight.ts +1 -1
  11. package/connection/RejectedChronicleCredentials.ts +10 -0
  12. package/connection/TokenProvider.spec.ts +5 -6
  13. package/connection/TokenProvider.ts +26 -17
  14. package/connection/clientVersion.spec.ts +27 -0
  15. package/connection/clientVersion.ts +9 -0
  16. package/connection/describeRequestError.spec.ts +22 -0
  17. package/connection/fetchOAuthAccessToken.ts +33 -2
  18. package/dist/ChronicleClient.d.ts +3 -0
  19. package/dist/ChronicleClient.d.ts.map +1 -1
  20. package/dist/ChronicleClient.js +55 -13
  21. package/dist/ChronicleClient.js.map +1 -1
  22. package/dist/ChronicleOptions.d.ts +5 -1
  23. package/dist/ChronicleOptions.d.ts.map +1 -1
  24. package/dist/ChronicleOptions.js +20 -5
  25. package/dist/ChronicleOptions.js.map +1 -1
  26. package/dist/EventStore.d.ts +2 -0
  27. package/dist/EventStore.d.ts.map +1 -1
  28. package/dist/EventStore.js +6 -1
  29. package/dist/EventStore.js.map +1 -1
  30. package/dist/artifacts/DefaultClientArtifactsProvider.d.ts.map +1 -1
  31. package/dist/artifacts/DefaultClientArtifactsProvider.js +4 -1
  32. package/dist/artifacts/DefaultClientArtifactsProvider.js.map +1 -1
  33. package/dist/connection/ChronicleConnection.d.ts +3 -3
  34. package/dist/connection/ChronicleConnection.d.ts.map +1 -1
  35. package/dist/connection/ChronicleConnection.js +74 -26
  36. package/dist/connection/ChronicleConnection.js.map +1 -1
  37. package/dist/connection/ChronicleServices.d.ts +1 -2
  38. package/dist/connection/ChronicleServices.d.ts.map +1 -1
  39. package/dist/connection/CompatibilityPreflight.js +1 -1
  40. package/dist/connection/CompatibilityPreflight.js.map +1 -1
  41. package/dist/connection/RejectedChronicleCredentials.d.ts +5 -0
  42. package/dist/connection/RejectedChronicleCredentials.d.ts.map +1 -0
  43. package/dist/connection/RejectedChronicleCredentials.js +10 -0
  44. package/dist/connection/RejectedChronicleCredentials.js.map +1 -0
  45. package/dist/connection/TokenProvider.d.ts +8 -6
  46. package/dist/connection/TokenProvider.d.ts.map +1 -1
  47. package/dist/connection/TokenProvider.js +25 -11
  48. package/dist/connection/TokenProvider.js.map +1 -1
  49. package/dist/connection/clientVersion.d.ts +3 -0
  50. package/dist/connection/clientVersion.d.ts.map +1 -0
  51. package/dist/connection/clientVersion.js +7 -0
  52. package/dist/connection/clientVersion.js.map +1 -0
  53. package/dist/connection/fetchOAuthAccessToken.d.ts +12 -0
  54. package/dist/connection/fetchOAuthAccessToken.d.ts.map +1 -1
  55. package/dist/connection/fetchOAuthAccessToken.js +37 -2
  56. package/dist/connection/fetchOAuthAccessToken.js.map +1 -1
  57. package/dist/externalServices/ExternalServiceBuilder.d.ts +1 -1
  58. package/dist/externalServices/ExternalServiceBuilder.d.ts.map +1 -1
  59. package/dist/externalServices/ExternalServiceBuilder.js +1 -1
  60. package/dist/externalServices/ExternalServiceBuilder.js.map +1 -1
  61. package/dist/externalServices/ExternalServices.d.ts.map +1 -1
  62. package/dist/externalServices/ExternalServices.js +3 -1
  63. package/dist/externalServices/ExternalServices.js.map +1 -1
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/projections/Projections.d.ts.map +1 -1
  69. package/dist/projections/Projections.js +9 -7
  70. package/dist/projections/Projections.js.map +1 -1
  71. package/dist/projections/constantValueExpression.d.ts +3 -0
  72. package/dist/projections/constantValueExpression.d.ts.map +1 -0
  73. package/dist/projections/constantValueExpression.js +26 -0
  74. package/dist/projections/constantValueExpression.js.map +1 -0
  75. package/dist/projections/declarative/ChildrenBuilder.d.ts +2 -0
  76. package/dist/projections/declarative/ChildrenBuilder.d.ts.map +1 -1
  77. package/dist/projections/declarative/ChildrenBuilder.js +5 -0
  78. package/dist/projections/declarative/ChildrenBuilder.js.map +1 -1
  79. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  80. package/dist/projections/declarative/FromBuilder.js +3 -2
  81. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  82. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  83. package/dist/projections/declarative/JoinBuilder.js +2 -1
  84. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  85. package/dist/projections/declarative/NestedBuilder.d.ts.map +1 -1
  86. package/dist/projections/declarative/NestedBuilder.js +3 -1
  87. package/dist/projections/declarative/NestedBuilder.js.map +1 -1
  88. package/dist/projections/declarative/ProjectionBuilderCore.d.ts +5 -0
  89. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  90. package/dist/projections/declarative/ProjectionBuilderCore.js +18 -2
  91. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  92. package/dist/projections/declarative/ProjectionBuilderFor.d.ts +6 -0
  93. package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  94. package/dist/projections/declarative/ProjectionBuilderFor.js +5 -0
  95. package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
  96. package/dist/projections/declarative/SetBuilder.d.ts.map +1 -1
  97. package/dist/projections/declarative/SetBuilder.js +2 -1
  98. package/dist/projections/declarative/SetBuilder.js.map +1 -1
  99. package/dist/projections/modelBound/addFrom.d.ts.map +1 -1
  100. package/dist/projections/modelBound/addFrom.js +3 -2
  101. package/dist/projections/modelBound/addFrom.js.map +1 -1
  102. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts +20 -0
  103. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  104. package/dist/projections/modelBound/childrenAndNestedBuilder.js +14 -11
  105. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  106. package/dist/projections/modelBound/childrenFrom.d.ts +4 -2
  107. package/dist/projections/modelBound/childrenFrom.d.ts.map +1 -1
  108. package/dist/projections/modelBound/childrenFrom.js +8 -4
  109. package/dist/projections/modelBound/childrenFrom.js.map +1 -1
  110. package/dist/projections/modelBound/clearWith.d.ts.map +1 -1
  111. package/dist/projections/modelBound/clearWith.js +3 -2
  112. package/dist/projections/modelBound/clearWith.js.map +1 -1
  113. package/dist/projections/modelBound/count.d.ts.map +1 -1
  114. package/dist/projections/modelBound/count.js +3 -2
  115. package/dist/projections/modelBound/count.js.map +1 -1
  116. package/dist/projections/modelBound/decrement.d.ts.map +1 -1
  117. package/dist/projections/modelBound/decrement.js +3 -2
  118. package/dist/projections/modelBound/decrement.js.map +1 -1
  119. package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
  120. package/dist/projections/modelBound/fromAll.js +3 -2
  121. package/dist/projections/modelBound/fromAll.js.map +1 -1
  122. package/dist/projections/modelBound/fromEvery.d.ts.map +1 -1
  123. package/dist/projections/modelBound/fromEvery.js +3 -2
  124. package/dist/projections/modelBound/fromEvery.js.map +1 -1
  125. package/dist/projections/modelBound/increment.d.ts.map +1 -1
  126. package/dist/projections/modelBound/increment.js +3 -2
  127. package/dist/projections/modelBound/increment.js.map +1 -1
  128. package/dist/projections/modelBound/join.d.ts.map +1 -1
  129. package/dist/projections/modelBound/join.js +3 -2
  130. package/dist/projections/modelBound/join.js.map +1 -1
  131. package/dist/projections/modelBound/nested.d.ts.map +1 -1
  132. package/dist/projections/modelBound/nested.js +3 -2
  133. package/dist/projections/modelBound/nested.js.map +1 -1
  134. package/dist/projections/modelBound/noAutoMap.d.ts.map +1 -1
  135. package/dist/projections/modelBound/noAutoMap.js +3 -2
  136. package/dist/projections/modelBound/noAutoMap.js.map +1 -1
  137. package/dist/projections/modelBound/removedWith.d.ts.map +1 -1
  138. package/dist/projections/modelBound/removedWith.js +3 -2
  139. package/dist/projections/modelBound/removedWith.js.map +1 -1
  140. package/dist/projections/modelBound/removedWithJoin.d.ts.map +1 -1
  141. package/dist/projections/modelBound/removedWithJoin.js +3 -2
  142. package/dist/projections/modelBound/removedWithJoin.js.map +1 -1
  143. package/dist/projections/modelBound/setFrom.d.ts.map +1 -1
  144. package/dist/projections/modelBound/setFrom.js +3 -2
  145. package/dist/projections/modelBound/setFrom.js.map +1 -1
  146. package/dist/projections/modelBound/setFromContext.d.ts.map +1 -1
  147. package/dist/projections/modelBound/setFromContext.js +3 -2
  148. package/dist/projections/modelBound/setFromContext.js.map +1 -1
  149. package/dist/projections/modelBound/setValue.d.ts.map +1 -1
  150. package/dist/projections/modelBound/setValue.js +3 -2
  151. package/dist/projections/modelBound/setValue.js.map +1 -1
  152. package/dist/projections/modelBound/subtractFrom.d.ts.map +1 -1
  153. package/dist/projections/modelBound/subtractFrom.js +3 -2
  154. package/dist/projections/modelBound/subtractFrom.js.map +1 -1
  155. package/dist/projections/notSetPropertyPath.d.ts +3 -0
  156. package/dist/projections/notSetPropertyPath.d.ts.map +1 -0
  157. package/dist/projections/notSetPropertyPath.js +5 -0
  158. package/dist/projections/notSetPropertyPath.js.map +1 -0
  159. package/dist/reactors/Reactors.d.ts +4 -0
  160. package/dist/reactors/Reactors.d.ts.map +1 -1
  161. package/dist/reactors/Reactors.js +24 -6
  162. package/dist/reactors/Reactors.js.map +1 -1
  163. package/dist/reactors/reactor.d.ts +2 -2
  164. package/dist/reactors/reactor.js +2 -2
  165. package/dist/readModels/MaterializedReadModels.d.ts.map +1 -1
  166. package/dist/readModels/MaterializedReadModels.js +2 -4
  167. package/dist/readModels/MaterializedReadModels.js.map +1 -1
  168. package/dist/readModels/ReadModels.d.ts +2 -1
  169. package/dist/readModels/ReadModels.d.ts.map +1 -1
  170. package/dist/readModels/ReadModels.js +16 -10
  171. package/dist/readModels/ReadModels.js.map +1 -1
  172. package/dist/readModels/deserializeReadModel.d.ts +5 -0
  173. package/dist/readModels/deserializeReadModel.d.ts.map +1 -0
  174. package/dist/readModels/deserializeReadModel.js +84 -0
  175. package/dist/readModels/deserializeReadModel.js.map +1 -0
  176. package/dist/readModels/rootReadModelTypes.d.ts +5 -0
  177. package/dist/readModels/rootReadModelTypes.d.ts.map +1 -0
  178. package/dist/readModels/rootReadModelTypes.js +69 -0
  179. package/dist/readModels/rootReadModelTypes.js.map +1 -0
  180. package/dist/reducers/Reducers.d.ts +4 -0
  181. package/dist/reducers/Reducers.d.ts.map +1 -1
  182. package/dist/reducers/Reducers.js +24 -6
  183. package/dist/reducers/Reducers.js.map +1 -1
  184. package/dist/schemas/JsonSchemaGenerator.d.ts.map +1 -1
  185. package/dist/schemas/JsonSchemaGenerator.js +4 -4
  186. package/dist/schemas/JsonSchemaGenerator.js.map +1 -1
  187. package/dist/tsconfig.tsbuildinfo +1 -1
  188. package/dist/types/TypeDiscoverer.d.ts +8 -1
  189. package/dist/types/TypeDiscoverer.d.ts.map +1 -1
  190. package/dist/types/TypeDiscoverer.js +25 -5
  191. package/dist/types/TypeDiscoverer.js.map +1 -1
  192. package/dist/types/TypeIntrospector.d.ts.map +1 -1
  193. package/dist/types/TypeIntrospector.js +3 -2
  194. package/dist/types/TypeIntrospector.js.map +1 -1
  195. package/dist/types/conceptValueType.d.ts +4 -0
  196. package/dist/types/conceptValueType.d.ts.map +1 -0
  197. package/dist/types/conceptValueType.js +10 -0
  198. package/dist/types/conceptValueType.js.map +1 -0
  199. package/dist/types/modelBoundProperty.d.ts +6 -0
  200. package/dist/types/modelBoundProperty.d.ts.map +1 -0
  201. package/dist/types/modelBoundProperty.js +53 -0
  202. package/dist/types/modelBoundProperty.js.map +1 -0
  203. package/externalServices/ExternalServiceBuilder.ts +4 -4
  204. package/externalServices/ExternalServices.spec.ts +87 -0
  205. package/externalServices/ExternalServices.ts +3 -1
  206. package/for_ChronicleClient/connectionAuthentication.spec.ts +67 -0
  207. package/index.ts +1 -0
  208. package/observation/observationDisposal.spec.ts +74 -0
  209. package/package.json +2 -1
  210. package/projections/Projections.childrenAndNested.spec.ts +2 -0
  211. package/projections/Projections.modelBoundCompleteness.spec.ts +47 -0
  212. package/projections/Projections.ts +9 -7
  213. package/projections/constantValueExpression.spec.ts +31 -0
  214. package/projections/constantValueExpression.ts +28 -0
  215. package/projections/declarative/ChildrenBuilder.spec.ts +46 -1
  216. package/projections/declarative/ChildrenBuilder.ts +6 -0
  217. package/projections/declarative/FromBuilder.ts +3 -2
  218. package/projections/declarative/JoinBuilder.ts +2 -1
  219. package/projections/declarative/NestedBuilder.ts +3 -1
  220. package/projections/declarative/ProjectionBuilderCore.ts +20 -2
  221. package/projections/declarative/ProjectionBuilderFor.spec.ts +61 -4
  222. package/projections/declarative/ProjectionBuilderFor.ts +7 -0
  223. package/projections/declarative/SetBuilder.ts +2 -1
  224. package/projections/modelBound/addFrom.ts +3 -2
  225. package/projections/modelBound/childrenAndNestedBuilder.ts +14 -11
  226. package/projections/modelBound/childrenFrom.ts +11 -5
  227. package/projections/modelBound/clearWith.ts +3 -2
  228. package/projections/modelBound/count.ts +3 -2
  229. package/projections/modelBound/decorators.standard.spec.ts +45 -0
  230. package/projections/modelBound/decrement.ts +3 -2
  231. package/projections/modelBound/fromAll.ts +3 -2
  232. package/projections/modelBound/fromEvery.ts +3 -2
  233. package/projections/modelBound/increment.ts +3 -2
  234. package/projections/modelBound/join.ts +3 -2
  235. package/projections/modelBound/nested.ts +3 -2
  236. package/projections/modelBound/noAutoMap.ts +3 -2
  237. package/projections/modelBound/removedWith.ts +3 -2
  238. package/projections/modelBound/removedWithJoin.ts +3 -2
  239. package/projections/modelBound/setFrom.ts +3 -2
  240. package/projections/modelBound/setFromContext.ts +3 -2
  241. package/projections/modelBound/setValue.ts +3 -2
  242. package/projections/modelBound/subtractFrom.ts +3 -2
  243. package/projections/notSetPropertyPath.ts +5 -0
  244. package/reactors/Reactors.ts +22 -7
  245. package/reactors/reactor.ts +2 -2
  246. package/readModels/MaterializedReadModels.spec.ts +22 -0
  247. package/readModels/MaterializedReadModels.ts +2 -4
  248. package/readModels/ReadModels.spec.ts +58 -1
  249. package/readModels/ReadModels.ts +17 -11
  250. package/readModels/conceptValueType.spec.ts +20 -0
  251. package/readModels/deserializeReadModel.spec.ts +84 -0
  252. package/readModels/deserializeReadModel.ts +82 -0
  253. package/readModels/discovery.spec.ts +27 -0
  254. package/readModels/rootReadModelTypes.spec.ts +115 -0
  255. package/readModels/rootReadModelTypes.ts +65 -0
  256. package/reducers/Reducers.ts +22 -7
  257. package/schemas/JsonSchemaGenerator.ts +4 -4
  258. package/schemas/legacyDesignMetadata.spec.ts +24 -2
  259. package/types/TypeDiscoverer.spec.ts +57 -0
  260. package/types/TypeDiscoverer.ts +27 -6
  261. package/types/TypeIntrospector.ts +3 -2
  262. package/types/conceptValueType.ts +11 -0
  263. package/types/modelBoundProperty.ts +53 -0
  264. package/dist/externalServices/ExternalServicesContracts.d.ts +0 -137
  265. package/dist/externalServices/ExternalServicesContracts.d.ts.map +0 -1
  266. package/dist/externalServices/ExternalServicesContracts.js +0 -654
  267. package/dist/externalServices/ExternalServicesContracts.js.map +0 -1
  268. package/externalServices/ExternalServicesContracts.ts +0 -709
@@ -7,8 +7,11 @@ import { SpanStatusCode } from '@opentelemetry/api';
7
7
  import { ChronicleOptions } from './ChronicleOptions.js';
8
8
  import { ChronicleConnection } from './connection/index.js';
9
9
  import { IncompatibleChronicleServer } from './connection/IncompatibleChronicleServer.js';
10
+ import { OAuthTokenHttpError } from './connection/fetchOAuthAccessToken.js';
11
+ import { RejectedChronicleCredentials } from './connection/RejectedChronicleCredentials.js';
10
12
  import { ensureCommandSuccess, ensureQuerySuccess } from './connection/callResults.js';
11
13
  import { ConnectionLifecycle } from './connection/ConnectionLifecycle.js';
14
+ import { clientVersion } from './connection/clientVersion.js';
12
15
  import { KernelKeepAlive } from './connection/KernelKeepAlive.js';
13
16
  import { EventStore } from './EventStore.js';
14
17
  import { EventStoreName } from './EventStoreName.js';
@@ -52,6 +55,7 @@ export class ChronicleClient implements IChronicleClient {
52
55
  private _keepAliveAbortController?: AbortController;
53
56
  private _healthCheckInFlight = false;
54
57
  private _connectionFailure?: Error;
58
+ private _consecutiveCredentialRejections = 0;
55
59
 
56
60
  /**
57
61
  * Creates a new {@link ChronicleClient} using the provided options.
@@ -194,6 +198,7 @@ export class ChronicleClient implements IChronicleClient {
194
198
  /** @inheritdoc */
195
199
  dispose(): void {
196
200
  this._isDisposed = true;
201
+ for (const store of this._stores.values()) store.disposeObservations();
197
202
 
198
203
  if (this._watchdogHandle) {
199
204
  clearInterval(this._watchdogHandle);
@@ -232,6 +237,7 @@ export class ChronicleClient implements IChronicleClient {
232
237
  // so this effectively waits until the channel reaches READY or fails.
233
238
  await this._connection.server.getVersionInfo({}, { signal: AbortSignal.timeout(10_000) });
234
239
 
240
+ this._consecutiveCredentialRejections = 0;
235
241
  this._logger.info('Connected to Chronicle kernel');
236
242
  await this.startKernelKeepAlive();
237
243
  await this._lifecycle.connected(error => {
@@ -243,9 +249,10 @@ export class ChronicleClient implements IChronicleClient {
243
249
 
244
250
 
245
251
  } catch (error) {
246
- if (error instanceof IncompatibleChronicleServer) {
247
- this.failConnection(error);
248
- throw error;
252
+ const terminal = this.terminalConnectionError(error);
253
+ if (terminal) {
254
+ this.failConnection(terminal);
255
+ throw terminal;
249
256
  }
250
257
  attempt++;
251
258
  const delayMs = await this.backOff(attempt, 'Connection attempt failed, retrying', error);
@@ -303,6 +310,7 @@ export class ChronicleClient implements IChronicleClient {
303
310
  await this._connection.resetChannel();
304
311
  await this._connection.connect();
305
312
  await this._connection.server.getVersionInfo({}, { signal: AbortSignal.timeout(10_000) });
313
+ this._consecutiveCredentialRejections = 0;
306
314
  this._logger.info('Reconnected to Chronicle kernel', { attempt: attempt + 1 });
307
315
  await this.startKernelKeepAlive();
308
316
  await this._lifecycle.connected(connectedError => {
@@ -312,9 +320,10 @@ export class ChronicleClient implements IChronicleClient {
312
320
  });
313
321
  return;
314
322
  } catch (reconnectError) {
315
- if (reconnectError instanceof IncompatibleChronicleServer) {
316
- this.failConnection(reconnectError);
317
- throw reconnectError;
323
+ const terminal = this.terminalConnectionError(reconnectError);
324
+ if (terminal) {
325
+ this.failConnection(terminal);
326
+ throw terminal;
318
327
  }
319
328
  attempt++;
320
329
  await this.backOff(attempt, 'Reconnect attempt failed, retrying', reconnectError);
@@ -351,17 +360,49 @@ export class ChronicleClient implements IChronicleClient {
351
360
  try {
352
361
  return await action();
353
362
  } catch (error) {
354
- if (!this.shouldReconnect(error)) {
355
- throw error;
363
+ const terminal = this.terminalConnectionError(error);
364
+ if (terminal) {
365
+ this.failConnection(terminal);
366
+ throw terminal;
356
367
  }
368
+ if (!this.shouldReconnect(error)) throw error;
357
369
 
358
370
  await this.reconnect(operation, error);
359
- return action();
371
+ try {
372
+ return await action();
373
+ } catch (retryError) {
374
+ const retryTerminal = this.terminalConnectionError(retryError);
375
+ if (retryTerminal) {
376
+ this.failConnection(retryTerminal);
377
+ throw retryTerminal;
378
+ }
379
+ throw retryError;
380
+ }
360
381
  }
361
382
  }
362
383
 
384
+ private terminalConnectionError(error: unknown): Error | undefined {
385
+ if (error instanceof IncompatibleChronicleServer || error instanceof RejectedChronicleCredentials) return error;
386
+ const rejection = (error as { code?: number })?.code === 16 &&
387
+ (this.options.connectionString.apiKey || this.hasOAuthCredentialRejection(error));
388
+ this._consecutiveCredentialRejections = rejection ? this._consecutiveCredentialRejections + 1 : 0;
389
+ if (this._consecutiveCredentialRejections === 3) {
390
+ return new RejectedChronicleCredentials(`Chronicle credentials were rejected: ${this.toErrorMessage(error)}`, { cause: error });
391
+ }
392
+ return undefined;
393
+ }
394
+
395
+ private hasOAuthCredentialRejection(error: unknown): boolean {
396
+ const tokenError = (error as { tokenFailure?: Error })?.tokenFailure;
397
+ const httpError = tokenError?.cause;
398
+ return httpError instanceof OAuthTokenHttpError && [400, 401].includes(httpError.statusCode) &&
399
+ ['invalid_client', 'unauthorized_client', 'invalid_grant'].includes(httpError.errorCode ?? '');
400
+ }
401
+
363
402
  private shouldReconnect(error: unknown): boolean {
364
- if (error instanceof IncompatibleChronicleServer) return false;
403
+ if (error instanceof IncompatibleChronicleServer || error instanceof RejectedChronicleCredentials) return false;
404
+ if ((error as { code?: number })?.code === 16 &&
405
+ (this.options.connectionString.apiKey || !!(error as { tokenFailure?: Error })?.tokenFailure)) return true;
365
406
  const code = Number((error as { code?: number })?.code ?? -1);
366
407
  const details = String((error as { details?: string })?.details ?? '');
367
408
  const message = this.toErrorMessage(error);
@@ -439,9 +480,7 @@ export class ChronicleClient implements IChronicleClient {
439
480
  await keepAlive.start(
440
481
  {
441
482
  ConnectionId: this._lifecycle.connectionId,
442
- // TODO: Not derived from this package's own version anywhere yet; kept as
443
- // the pre-existing hardcoded placeholder until such a mechanism exists.
444
- ClientVersion: '1.0.0',
483
+ ClientVersion: clientVersion,
445
484
  IsRunningWithDebugger: false,
446
485
  ProcessId: process.pid,
447
486
  ProcessPath: process.execPath,
@@ -0,0 +1,54 @@
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 { afterEach, describe, expect, it } from 'vitest';
5
+ import { ChronicleOptions } from './ChronicleOptions.js';
6
+
7
+ const entry = process.argv[1];
8
+ const execArgv = [...process.execArgv];
9
+ const vitest = process.env.VITEST;
10
+ const patterns = [
11
+ '**/*.ts', '**/*.tsx', '!**/*.d.ts', '!**/node_modules', '!**/dist', '!**/build',
12
+ '!**/.git', '!**/.vscode', '!**/*.spec.ts', '!**/*.test.ts', '!**/*.spec.tsx', '!**/*.test.tsx'
13
+ ];
14
+ afterEach(() => {
15
+ process.argv[1] = entry;
16
+ process.execArgv = [...execArgv];
17
+ if (vitest === undefined) delete process.env.VITEST;
18
+ else process.env.VITEST = vitest;
19
+ });
20
+
21
+ describe('default artifact discovery', () => {
22
+ for (const extension of ['ts', 'tsx', 'mts', 'cts']) {
23
+ it(`scans TypeScript sources for a .${extension} entry`, () => {
24
+ process.argv[1] = `/app/index.${extension}`;
25
+ expect(ChronicleOptions.development().discoveryPatterns).toEqual(patterns);
26
+ });
27
+ }
28
+
29
+ it('scans a JavaScript entry under vitest', () => {
30
+ process.argv[1] = '/app/dist/index.js';
31
+ process.env.VITEST = 'true';
32
+ expect(ChronicleOptions.development().discoveryPatterns).toEqual(patterns);
33
+ });
34
+
35
+ it.each(['tsx', 'ts-node', '--experimental-strip-types', '--experimental-transform-types'])(
36
+ 'scans a JavaScript entry with %s loader', loader => {
37
+ process.argv[1] = '/app/dist/index.js';
38
+ process.execArgv = [loader];
39
+ expect(ChronicleOptions.development().discoveryPatterns).toEqual(patterns);
40
+ });
41
+
42
+ it('does not scan files for a compiled JavaScript entry without a TypeScript runtime', () => {
43
+ process.argv[1] = '/app/dist/index.js';
44
+ delete process.env.VITEST;
45
+ process.execArgv = [];
46
+ expect(ChronicleOptions.development().discoveryPatterns).toEqual([]);
47
+ });
48
+
49
+ it('always honors explicit discovery patterns', () => {
50
+ process.argv[1] = '/app/dist/index.js';
51
+ const patterns = ['app/**/*.js', '!app/**/*.spec.js'];
52
+ expect(ChronicleOptions.development({ discoveryPatterns: patterns }).discoveryPatterns).toBe(patterns);
53
+ });
54
+ });
@@ -56,7 +56,10 @@ export class ChronicleOptions {
56
56
  /**
57
57
  * Glob patterns used to discover artifact files at startup.
58
58
  * Patterns prefixed with '!' are treated as exclusions.
59
- * Set to an empty array to disable automatic file discovery.
59
+ * By default, TypeScript entry files (.ts, .tsx, .mts, .cts) or TypeScript runtimes scan TypeScript sources;
60
+ * compiled JavaScript entry files do not scan (imported modules register their artifacts).
61
+ * Explicit patterns are used regardless of the entry file. Set to an empty array to
62
+ * disable automatic file discovery.
60
63
  */
61
64
  readonly discoveryPatterns: string[];
62
65
 
@@ -76,8 +79,20 @@ export class ChronicleOptions {
76
79
  this.softwareVersion = options.softwareVersion ?? '0.0.0';
77
80
  this.softwareCommit = options.softwareCommit ?? 'Unknown';
78
81
  this.clientArtifactsProvider = options.clientArtifactsProvider ?? DefaultClientArtifactsProvider.default;
79
- this.discoveryPatterns = options.discoveryPatterns ?? [
82
+ this.discoveryPatterns = options.discoveryPatterns ?? ChronicleOptions.defaultDiscoveryPatterns();
83
+ this.defaultSinkTypeId = options.defaultSinkTypeId ?? WellKnownSinks.MongoDB;
84
+ this.reactorResultHandler = options.reactorResultHandler;
85
+ }
86
+
87
+ private static defaultDiscoveryPatterns(): string[] {
88
+ const typescriptEntry = /\.(?:ts|tsx|mts|cts)$/i.test(process.argv[1] ?? '');
89
+ const typescriptLoader = process.execArgv.some(arg => /(?:tsx|ts-node|--experimental-strip-types|--experimental-transform-types)/i.test(arg));
90
+ // process.features.typescript is set on every Node.js 24+ process (type stripping), so it
91
+ // does not mean the program was started from TypeScript; it is deliberately not consulted.
92
+ if (!typescriptEntry && !process.env.VITEST && !typescriptLoader) return [];
93
+ return [
80
94
  '**/*.ts',
95
+ '**/*.tsx',
81
96
  '!**/*.d.ts',
82
97
  '!**/node_modules',
83
98
  '!**/dist',
@@ -85,10 +100,10 @@ export class ChronicleOptions {
85
100
  '!**/.git',
86
101
  '!**/.vscode',
87
102
  '!**/*.spec.ts',
88
- '!**/*.test.ts'
103
+ '!**/*.test.ts',
104
+ '!**/*.spec.tsx',
105
+ '!**/*.test.tsx'
89
106
  ];
90
- this.defaultSinkTypeId = options.defaultSinkTypeId ?? WellKnownSinks.MongoDB;
91
- this.reactorResultHandler = options.reactorResultHandler;
92
107
  }
93
108
 
94
109
  /**
package/EventStore.ts CHANGED
@@ -101,7 +101,7 @@ export class EventStore implements IEventStore {
101
101
  this.projections = new Projections(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId);
102
102
  this.reactors = new Reactors(artifacts, _connection, name.value, namespace.value, lifecycle, this.eventLog, reactorResultHandler);
103
103
  this.reducers = new Reducers(artifacts, _connection, name.value, namespace.value, lifecycle, defaultSinkTypeId);
104
- this.readModels = new ReadModels(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId);
104
+ this.readModels = new ReadModels(name.value, namespace.value, _connection, artifacts, defaultSinkTypeId, readModelType => this.projections.hasForModel(readModelType));
105
105
  this.jobs = new Jobs(name.value, namespace.value, _connection);
106
106
  this.webhooks = new Webhooks(name.value, _connection, this.eventTypes, artifacts);
107
107
  this.subscriptions = new EventStoreSubscriptions(this.eventTypes, name.value, _connection);
@@ -157,6 +157,12 @@ export class EventStore implements IEventStore {
157
157
  });
158
158
  }
159
159
 
160
+ /** Stops the store's long-lived observations when the client is disposed. */
161
+ disposeObservations(): void {
162
+ (this.reactors as Reactors).dispose();
163
+ (this.reducers as Reducers).dispose();
164
+ }
165
+
160
166
  /** @inheritdoc */
161
167
  getEventSequence(id: EventSequenceId): IEventSequence {
162
168
  const existing = this._sequences.get(id.value);
package/README.md CHANGED
@@ -59,8 +59,7 @@ class EmployeeHired {
59
59
  }
60
60
  }
61
61
 
62
- // discoveryPatterns: [] relies on this module's imports instead of scanning source files.
63
- const client = new ChronicleClient(ChronicleOptions.development({ discoveryPatterns: [] }));
62
+ const client = new ChronicleClient(ChronicleOptions.development());
64
63
  const store = await client.getEventStore('MyStore');
65
64
  const result = await store.eventLog.append('employee-123', new EmployeeHired('Jane', 'Doe'));
66
65
  console.log(`Appended at sequence number ${result.sequenceNumber.value}`);
@@ -73,7 +72,7 @@ Chronicle decorators work with both TC39 standard decorators (TypeScript 5.2+; d
73
72
 
74
73
  Read models are inferred from `@projection('id', Model)`, `@reducer('id', sequenceId, Model)`, or an exported model with `@fromEvent(Event)` or other model-bound property mappings. Their schema comes from the model type; the default identifier is its class name. See [read models](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/read-models.md) for preserving existing custom identifiers.
75
74
 
76
- `ChronicleOptions` scans `**/*.ts` files for artifacts by default, which needs the `glob` package, and fails when the matched `.ts` files cannot be loaded by Node.js. Import your artifact modules and pass `discoveryPatterns: []`, as in the example above, or see [artifact discovery](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/getting-started.md#artifact-discovery).
75
+ Decorated artifacts register when their modules are imported. When the entry file is TypeScript (run through a loader such as `tsx`), the client also scans `**/*.ts` files by default; compiled JavaScript scans nothing unless you set `discoveryPatterns`. See [artifact discovery](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/getting-started.md#artifact-discovery).
77
76
 
78
77
  The client skips TLS certificate validation unless the connection string sets `skipTlsValidation=false`. Set it for every server other than a local development kernel; see [Connect to Chronicle](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/connecting.md).
79
78
 
@@ -28,7 +28,10 @@ export class DefaultClientArtifactsProvider implements IClientArtifactsProvider
28
28
 
29
29
  /** @inheritdoc */
30
30
  get readModels(): Constructor[] {
31
- const types = new Set(this.discoverer.getTypesByDecoratorType(DecoratorType.ReadModel));
31
+ const types = new Set([
32
+ ...this.discoverer.getTypesByDecoratorType(DecoratorType.ReadModel),
33
+ ...this.discoverer.getPropertyOnlyModels()
34
+ ]);
32
35
  for (const projection of this.projections) {
33
36
  const type = getProjectionMetadata(projection)?.readModelType;
34
37
  if (type) types.add(type);
@@ -0,0 +1,219 @@
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 { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { createServer, ServerError, type ServiceImplementation } from 'nice-grpc';
6
+ import { ConnectionServiceDefinition, EventStoresDefinition, ServerDefinition } from '@cratis/chronicle.contracts';
7
+ import { status } from '@grpc/grpc-js';
8
+ import { ChronicleConnection } from './ChronicleConnection.js';
9
+ import { ChronicleClient } from '../ChronicleClient.js';
10
+ import { ChronicleOptions } from '../ChronicleOptions.js';
11
+ import { OAuthTokenHttpError } from './fetchOAuthAccessToken.js';
12
+ import { RejectedChronicleCredentials } from './RejectedChronicleCredentials.js';
13
+ import { ClientError } from 'nice-grpc-common';
14
+
15
+ const fetchToken = vi.hoisted(() => vi.fn());
16
+ vi.mock('./fetchOAuthAccessToken.js', async importOriginal => ({ ...await importOriginal<typeof import('./fetchOAuthAccessToken.js')>(), fetchOAuthAccessToken: fetchToken }));
17
+ vi.mock('./KernelKeepAlive.js', () => ({ KernelKeepAlive: class { async start() {} } }));
18
+
19
+ const servers: Array<ReturnType<typeof createServer>> = [];
20
+ const connections: ChronicleConnection[] = [];
21
+ const clients: ChronicleClient[] = [];
22
+
23
+ async function listen(rejectAnonymous = false): Promise<number> {
24
+ const server = createServer();
25
+ const unsupported = async () => { throw new Error('Unexpected RPC'); };
26
+ const methods = Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unsupported]));
27
+ server.add(ConnectionServiceDefinition, {
28
+ ...methods, checkCompatibility: async () => {
29
+ if (rejectAnonymous) throw new ServerError(status.UNAUTHENTICATED, 'Unauthenticated');
30
+ return { IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' };
31
+ }
32
+ } as ServiceImplementation<typeof ConnectionServiceDefinition>);
33
+ servers.push(server);
34
+ return server.listen('127.0.0.1:0');
35
+ }
36
+
37
+ async function listenAuthenticated(onCheck?: () => void): Promise<number> {
38
+ const server = createServer();
39
+ const authenticate = async (_request: unknown, context: { metadata: { get(key: string): string | undefined } }) => {
40
+ if (context.metadata.get('authorization') !== 'Bearer fresh' &&
41
+ context.metadata.get('api-key') !== 'valid') throw new ServerError(status.UNAUTHENTICATED, 'Unauthenticated');
42
+ };
43
+ const unsupported = async () => { throw new Error('Unexpected RPC'); };
44
+ server.add(ConnectionServiceDefinition, {
45
+ ...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unsupported])),
46
+ checkCompatibility: async (request, context) => {
47
+ onCheck?.();
48
+ await authenticate(request, context);
49
+ return { IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' };
50
+ }
51
+ } as ServiceImplementation<typeof ConnectionServiceDefinition>);
52
+ server.add(ServerDefinition, {
53
+ getVersionInfo: async () => ({}), resetKernelState: unsupported
54
+ } as ServiceImplementation<typeof ServerDefinition>);
55
+ server.add(EventStoresDefinition, {
56
+ allEventStores: async () => ({ IsAuthorized: true, Data: [] }),
57
+ ensureEventStore: unsupported, observeEventStores: unsupported
58
+ } as ServiceImplementation<typeof EventStoresDefinition>);
59
+ servers.push(server);
60
+ return server.listen('127.0.0.1:0');
61
+ }
62
+
63
+ afterEach(async () => {
64
+ for (const client of clients) client.dispose();
65
+ for (const connection of connections) connection.dispose();
66
+ for (const server of servers) await server.shutdown();
67
+ connections.length = 0;
68
+ clients.length = 0;
69
+ servers.length = 0;
70
+ fetchToken.mockReset();
71
+ vi.useRealTimers();
72
+ });
73
+
74
+ describe('ChronicleConnection authentication', () => {
75
+ it('uses the selected server for credentials and reuses a token when returning to that server', async () => {
76
+ const first = await listen();
77
+ const second = await listen();
78
+ fetchToken.mockImplementation(async (endpoint: string) => ({ access_token: endpoint, expires_in: 3600 }));
79
+ const connection = new ChronicleConnection({ connectionString: `chronicle://user:secret@127.0.0.1:${first},127.0.0.1:${second}?disableTls=true&loadBalancer=round-robin` });
80
+ connections.push(connection);
81
+
82
+ await connection.connect();
83
+ await connection.reconnect();
84
+ await connection.reconnect();
85
+
86
+ expect(fetchToken.mock.calls.map(call => call[0]).sort()).toEqual([
87
+ `http://127.0.0.1:${first}/connect/token`,
88
+ `http://127.0.0.1:${second}/connect/token`
89
+ ].sort());
90
+ });
91
+
92
+ it('keeps an explicit authority across server selections', async () => {
93
+ const first = await listen();
94
+ const second = await listen();
95
+ fetchToken.mockResolvedValue({ access_token: 'token', expires_in: 3600 });
96
+ const connection = new ChronicleConnection({
97
+ connectionString: `chronicle://user:secret@127.0.0.1:${first},127.0.0.1:${second}?disableTls=true&loadBalancer=round-robin`,
98
+ authority: 'http://identity.example:1234'
99
+ });
100
+ connections.push(connection);
101
+
102
+ await connection.connect();
103
+ await connection.reconnect();
104
+ expect(fetchToken).toHaveBeenCalledTimes(1);
105
+ expect(fetchToken.mock.calls[0][0]).toBe('http://identity.example:1234/connect/token');
106
+ });
107
+
108
+ it('includes the token endpoint failure when the unauthenticated RPC is rejected', async () => {
109
+ const port = await listen(true);
110
+ fetchToken.mockRejectedValue(new Error('invalid_client'));
111
+ const connection = new ChronicleConnection({ connectionString: `chronicle://wrong:wrong@127.0.0.1:${port}?disableTls=true` });
112
+ connections.push(connection);
113
+ await expect(connection.connect()).rejects.toThrow(`http://127.0.0.1:${port}/connect/token: invalid_client`);
114
+ });
115
+
116
+ it('preserves the gRPC code, path and original error on a token failure', async () => {
117
+ const port = await listen(true);
118
+ fetchToken.mockRejectedValue(new OAuthTokenHttpError(401, '{"error":"invalid_client"}'));
119
+ const connection = new ChronicleConnection({ connectionString: `chronicle://wrong:wrong@127.0.0.1:${port}?disableTls=true` });
120
+ connections.push(connection);
121
+ const error = await connection.connect().catch(error => error as ClientError);
122
+ expect(error).toBeInstanceOf(ClientError);
123
+ expect(error.code).toBe(status.UNAUTHENTICATED);
124
+ expect(error.path).toContain('CheckCompatibility');
125
+ expect(error.details).toContain('invalid_client');
126
+ expect(error.cause).toBeInstanceOf(ClientError);
127
+ });
128
+
129
+ it('refreshes a rejected cached token and retries one unary call', async () => {
130
+ const port = await listenAuthenticated();
131
+ fetchToken.mockResolvedValueOnce({ access_token: 'stale', expires_in: 3600 })
132
+ .mockResolvedValueOnce({ access_token: 'fresh', expires_in: 3600 });
133
+ const connection = new ChronicleConnection({ connectionString: `chronicle://user:secret@127.0.0.1:${port}?disableTls=true` });
134
+ connections.push(connection);
135
+ await expect(connection.connect()).resolves.toBeUndefined();
136
+ expect(fetchToken).toHaveBeenCalledTimes(2);
137
+ });
138
+
139
+ it('does not retry streaming calls after an unauthenticated response', async () => {
140
+ const server = createServer();
141
+ const attempts = vi.fn();
142
+ const unsupported = async () => { throw new Error('Unexpected RPC'); };
143
+ server.add(ConnectionServiceDefinition, {
144
+ ...Object.fromEntries(Object.keys(ConnectionServiceDefinition.methods).map(name => [name, unsupported])),
145
+ checkCompatibility: async () => ({ IsCompatible: true, Incompatibilities: [], ServerVersion: 'test' }),
146
+ observeConnectedClients: async function* () {
147
+ attempts();
148
+ throw new ServerError(status.UNAUTHENTICATED, 'Token expired');
149
+ }
150
+ } as ServiceImplementation<typeof ConnectionServiceDefinition>);
151
+ servers.push(server);
152
+ const port = await server.listen('127.0.0.1:0');
153
+ fetchToken.mockResolvedValue({ access_token: 'stale', expires_in: 3600 });
154
+ const connection = new ChronicleConnection({ connectionString: `chronicle://user:secret@127.0.0.1:${port}?disableTls=true` });
155
+ connections.push(connection);
156
+ await connection.connect();
157
+ const observe = async () => { for await (const _ of connection.connections.observeConnectedClients({})) { /* drain */ } };
158
+ await expect(observe()).rejects.toMatchObject({ code: status.UNAUTHENTICATED });
159
+ expect(attempts).toHaveBeenCalledTimes(1);
160
+ expect(fetchToken).toHaveBeenCalledTimes(1);
161
+ });
162
+
163
+ it('connects after two startup invalid_client responses', async () => {
164
+ const port = await listenAuthenticated();
165
+ fetchToken.mockRejectedValueOnce(new OAuthTokenHttpError(401, '{"error":"invalid_client"}'))
166
+ .mockRejectedValueOnce(new OAuthTokenHttpError(401, '{"error":"invalid_client"}'))
167
+ .mockResolvedValue({ access_token: 'fresh', expires_in: 3600 });
168
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://user:secret@127.0.0.1:${port}?disableTls=true`, { discoveryPatterns: [] }));
169
+ clients.push(client);
170
+ await expect(client.getEventStores()).resolves.toEqual([]);
171
+ expect(fetchToken).toHaveBeenCalledTimes(3);
172
+ }, 10000);
173
+
174
+ it('rejects persistent OAuth credential rejections after three connection attempts', async () => {
175
+ const port = await listenAuthenticated();
176
+ fetchToken.mockRejectedValue(new OAuthTokenHttpError(400, '{"error":"invalid_client"}'));
177
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://user:secret@127.0.0.1:${port}?disableTls=true`, { discoveryPatterns: [] }));
178
+ clients.push(client);
179
+ await expect(client.getEventStores()).rejects.toBeInstanceOf(RejectedChronicleCredentials);
180
+ expect(fetchToken).toHaveBeenCalledTimes(3);
181
+ }, 10000);
182
+
183
+ it('keeps retrying proxy 403 responses without an OAuth error code', async () => {
184
+ const random = vi.spyOn(Math, 'random').mockReturnValue(0);
185
+ const checks = vi.fn();
186
+ const port = await listenAuthenticated(checks);
187
+ fetchToken.mockRejectedValue(new OAuthTokenHttpError(403, '<html>blocked</html>'));
188
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://user:secret@127.0.0.1:${port}?disableTls=true`, { discoveryPatterns: [] }));
189
+ clients.push(client);
190
+ const pending = client.getEventStores().then(() => undefined, error => error as Error);
191
+ try {
192
+ await new Promise(resolve => setTimeout(resolve, 3900));
193
+ expect(checks.mock.calls.length).toBeGreaterThanOrEqual(4);
194
+ client.dispose();
195
+ expect((await pending).message).toMatch(/disposed/);
196
+ } finally {
197
+ random.mockRestore();
198
+ }
199
+ }, 15000);
200
+
201
+ it('rejects an API key after three unauthenticated kernel responses', async () => {
202
+ const checks = vi.fn();
203
+ const port = await listenAuthenticated(checks);
204
+ const client = new ChronicleClient(ChronicleOptions.fromConnectionString(`chronicle://127.0.0.1:${port}?disableTls=true&apiKey=wrong`, { discoveryPatterns: [] }));
205
+ clients.push(client);
206
+ await expect(client.getEventStores()).rejects.toBeInstanceOf(RejectedChronicleCredentials);
207
+ expect(checks).toHaveBeenCalledTimes(3);
208
+ }, 10000);
209
+
210
+ it('reports the selected endpoint and underlying failure for development credentials', async () => {
211
+ const port = await listen();
212
+ fetchToken.mockRejectedValue(new Error('ECONNREFUSED'));
213
+ const connection = new ChronicleConnection({ connectionString: `chronicle://127.0.0.1:${port}?disableTls=true` });
214
+ connections.push(connection);
215
+
216
+ await expect(connection.connect()).resolves.toBeUndefined();
217
+ expect(fetchToken.mock.calls[0][1]).toBe('chronicle-dev-client');
218
+ });
219
+ });