@cratis/chronicle 6.32.1 → 6.34.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.
- package/ChronicleClient.ts +35 -33
- package/ChronicleOptions.ts +27 -1
- package/EventStore.ts +14 -12
- package/Metrics.ts +70 -96
- package/Tracing.ts +4 -2
- package/WellKnownTelemetryNames.ts +67 -0
- package/artifacts/validateArtifactSchemas.spec.ts +1 -1
- package/artifacts/withActivatedArtifact.ts +7 -5
- package/connection/ChronicleConnection.ts +18 -6
- package/connection/CompatibilityPreflight.ts +2 -1
- package/connection/KernelKeepAlive.ts +13 -7
- package/connection/TokenProvider.ts +3 -1
- package/connection/TraceContextMiddleware.ts +30 -0
- package/connection/for_ChronicleConnection/when_creating_clients/with_correlated_multi_host_callers.spec.ts +73 -0
- package/connection/for_OAuthTokenProvider/when_acquiring_tokens/with_http_instrumentation.spec.ts +102 -0
- package/connection/for_TraceContextMiddleware/when_calling/with_active_context.spec.ts +235 -0
- package/connection/for_TraceContextMiddleware/when_calling/with_grpc_instrumentation.spec.ts +102 -0
- package/correlation/CorrelationIdManager.ts +10 -0
- package/dist/ChronicleClient.d.ts.map +1 -1
- package/dist/ChronicleClient.js +35 -33
- package/dist/ChronicleClient.js.map +1 -1
- package/dist/ChronicleOptions.d.ts +8 -0
- package/dist/ChronicleOptions.d.ts.map +1 -1
- package/dist/ChronicleOptions.js +19 -1
- package/dist/ChronicleOptions.js.map +1 -1
- package/dist/EventStore.d.ts +3 -1
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +14 -12
- package/dist/EventStore.js.map +1 -1
- package/dist/Metrics.d.ts +23 -64
- package/dist/Metrics.d.ts.map +1 -1
- package/dist/Metrics.js +62 -92
- package/dist/Metrics.js.map +1 -1
- package/dist/Tracing.d.ts +1 -1
- package/dist/Tracing.d.ts.map +1 -1
- package/dist/Tracing.js +4 -2
- package/dist/Tracing.js.map +1 -1
- package/dist/WellKnownTelemetryNames.d.ts +65 -0
- package/dist/WellKnownTelemetryNames.d.ts.map +1 -0
- package/dist/WellKnownTelemetryNames.js +67 -0
- package/dist/WellKnownTelemetryNames.js.map +1 -0
- package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
- package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
- package/dist/artifacts/withActivatedArtifact.js +5 -4
- package/dist/artifacts/withActivatedArtifact.js.map +1 -1
- package/dist/connection/ChronicleConnection.d.ts +5 -0
- package/dist/connection/ChronicleConnection.d.ts.map +1 -1
- package/dist/connection/ChronicleConnection.js +13 -6
- package/dist/connection/ChronicleConnection.js.map +1 -1
- package/dist/connection/CompatibilityPreflight.d.ts.map +1 -1
- package/dist/connection/CompatibilityPreflight.js +2 -1
- package/dist/connection/CompatibilityPreflight.js.map +1 -1
- package/dist/connection/KernelKeepAlive.d.ts +3 -1
- package/dist/connection/KernelKeepAlive.d.ts.map +1 -1
- package/dist/connection/KernelKeepAlive.js +10 -7
- package/dist/connection/KernelKeepAlive.js.map +1 -1
- package/dist/connection/TokenProvider.d.ts.map +1 -1
- package/dist/connection/TokenProvider.js +3 -1
- package/dist/connection/TokenProvider.js.map +1 -1
- package/dist/connection/TraceContextMiddleware.d.ts +4 -0
- package/dist/connection/TraceContextMiddleware.d.ts.map +1 -0
- package/dist/connection/TraceContextMiddleware.js +30 -0
- package/dist/connection/TraceContextMiddleware.js.map +1 -0
- package/dist/correlation/CorrelationIdManager.d.ts +4 -0
- package/dist/correlation/CorrelationIdManager.d.ts.map +1 -1
- package/dist/correlation/CorrelationIdManager.js +8 -0
- package/dist/correlation/CorrelationIdManager.js.map +1 -1
- package/dist/eventSequences/EventLog.d.ts +2 -1
- package/dist/eventSequences/EventLog.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.js +2 -2
- package/dist/eventSequences/EventLog.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +4 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +63 -80
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.d.ts.map +1 -1
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js +3 -4
- package/dist/eventStoreSubscriptions/EventStoreSubscriptions.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ChronicleLogEntry.d.ts +11 -0
- package/dist/logging/ChronicleLogEntry.d.ts.map +1 -0
- package/dist/logging/ChronicleLogEntry.js +4 -0
- package/dist/logging/ChronicleLogEntry.js.map +1 -0
- package/dist/logging/ChronicleLogLevel.d.ts +9 -0
- package/dist/logging/ChronicleLogLevel.d.ts.map +1 -0
- package/dist/logging/ChronicleLogLevel.js +12 -0
- package/dist/logging/ChronicleLogLevel.js.map +1 -0
- package/dist/logging/DiagChronicleLogger.d.ts +7 -0
- package/dist/logging/DiagChronicleLogger.d.ts.map +1 -0
- package/dist/logging/DiagChronicleLogger.js +10 -0
- package/dist/logging/DiagChronicleLogger.js.map +1 -0
- package/dist/logging/IChronicleLogger.d.ts +7 -0
- package/dist/logging/IChronicleLogger.d.ts.map +1 -0
- package/dist/logging/IChronicleLogger.js +4 -0
- package/dist/logging/IChronicleLogger.js.map +1 -0
- package/dist/logging/createLogger.d.ts +10 -0
- package/dist/logging/createLogger.d.ts.map +1 -0
- package/dist/logging/createLogger.js +55 -0
- package/dist/logging/createLogger.js.map +1 -0
- package/dist/projections/Projections.d.ts.map +1 -1
- package/dist/projections/Projections.js +4 -3
- package/dist/projections/Projections.js.map +1 -1
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +26 -24
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/readModels/IReadModels.d.ts +14 -0
- package/dist/readModels/IReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.d.ts +6 -0
- package/dist/readModels/ReadModels.d.ts.map +1 -1
- package/dist/readModels/ReadModels.js +110 -0
- package/dist/readModels/ReadModels.js.map +1 -1
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts +20 -0
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.d.ts.map +1 -0
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.js +79 -0
- package/dist/readModels/for_ReadModels/given/a_stored_read_model.js.map +1 -0
- package/dist/reducers/Reducers.d.ts.map +1 -1
- package/dist/reducers/Reducers.js +28 -25
- package/dist/reducers/Reducers.js.map +1 -1
- package/dist/seeding/EventSeeding.d.ts.map +1 -1
- package/dist/seeding/EventSeeding.js +4 -3
- package/dist/seeding/EventSeeding.js.map +1 -1
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts +11 -0
- package/dist/telemetry/ChronicleTelemetryOptions.d.ts.map +1 -0
- package/dist/telemetry/ChronicleTelemetryOptions.js +4 -0
- package/dist/telemetry/ChronicleTelemetryOptions.js.map +1 -0
- package/dist/telemetry/observeOperation.d.ts +6 -0
- package/dist/telemetry/observeOperation.d.ts.map +1 -0
- package/dist/telemetry/observeOperation.js +23 -0
- package/dist/telemetry/observeOperation.js.map +1 -0
- package/dist/telemetry/runInBackgroundContext.d.ts +3 -0
- package/dist/telemetry/runInBackgroundContext.d.ts.map +1 -0
- package/dist/telemetry/runInBackgroundContext.js +9 -0
- package/dist/telemetry/runInBackgroundContext.js.map +1 -0
- package/dist/telemetry/spanAttributes.d.ts +14 -0
- package/dist/telemetry/spanAttributes.d.ts.map +1 -0
- package/dist/telemetry/spanAttributes.js +47 -0
- package/dist/telemetry/spanAttributes.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +64 -81
- package/eventStoreSubscriptions/EventStoreSubscriptions.ts +5 -5
- package/for_ChronicleClient/when_starting_background_work/with_a_correlated_caller.spec.ts +150 -0
- package/for_ChronicleOptions/when_configuring_telemetry/with_event_source_identifiers.spec.ts +44 -0
- package/index.ts +7 -1
- package/logging/ChronicleLogEntry.ts +14 -0
- package/logging/ChronicleLogLevel.ts +11 -0
- package/logging/DiagChronicleLogger.ts +13 -0
- package/logging/IChronicleLogger.ts +10 -0
- package/logging/createLogger.ts +50 -0
- package/logging/for_ChronicleLogger/when_logging/with_client_owned_sinks.spec.ts +96 -0
- package/package.json +7 -1
- package/projections/Projections.ts +4 -3
- package/reactors/Reactors.ts +27 -25
- package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +14 -14
- package/readModels/IReadModels.ts +15 -0
- package/readModels/ReadModels.ts +118 -1
- package/readModels/for_ReadModels/given/a_stored_read_model.ts +62 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_default_subject.spec.ts +39 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_frozen_document.spec.ts +28 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_malformed_response.spec.ts +24 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_a_reported_failure.spec.ts +38 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_an_empty_released_value.spec.ts +19 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_an_unknown_subject_property.spec.ts +42 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_bookkeeping_fields.spec.ts +27 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_conflicting_or_missing_properties.spec.ts +32 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_empty_subject_metadata.spec.ts +56 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_invalid_subject_metadata.spec.ts +27 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_mixed_subjects.spec.ts +38 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_property_subjects.spec.ts +52 -0
- package/readModels/for_ReadModels/when_releasing_a_document/with_whitespace_subjects.spec.ts +24 -0
- package/readModels/for_ReadModels/when_releasing_a_document/without_subjects.spec.ts +24 -0
- package/reducers/Reducers.ts +29 -26
- package/seeding/EventSeeding.ts +6 -4
- package/telemetry/ChronicleTelemetryOptions.ts +8 -0
- package/telemetry/for_ChronicleTelemetry/given/a_telemetry_session.fixture.ts +36 -0
- package/telemetry/for_ChronicleTelemetry/given/an_event_sequence.fixture.ts +29 -0
- package/telemetry/for_ChronicleTelemetry/when_appending/with_shared_and_legacy_attributes.spec.ts +58 -0
- package/telemetry/for_ChronicleTelemetry/when_appending/without_an_sdk.spec.ts +15 -0
- package/telemetry/for_ChronicleTelemetry/when_documenting/with_public_names.spec.ts +15 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_failures/with_sensitive_exception_details.spec.ts +34 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_identifiers/with_per_client_privacy.spec.ts +92 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_metrics/with_legacy_overlap.spec.ts +40 -0
- package/telemetry/for_ChronicleTelemetry/when_recording_sequence_numbers/with_integer_precision.spec.ts +23 -0
- package/telemetry/observeOperation.ts +25 -0
- package/telemetry/runInBackgroundContext.ts +10 -0
- package/telemetry/spanAttributes.ts +49 -0
|
@@ -0,0 +1,39 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { JsonSchemaGenerator } from '../../../schemas/index.js';
|
|
6
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
|
|
10
|
+
describe('when releasing a document with a default subject', () => {
|
|
11
|
+
let context: a_stored_read_model;
|
|
12
|
+
let result: Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
context = new a_stored_read_model();
|
|
16
|
+
context.release.mockResolvedValue({ HasError: false, Error: '', Payload: '{"id":"account","name":"released name"}' });
|
|
17
|
+
result = await context.readModels.releaseDocument(AccountDocument, {
|
|
18
|
+
id: 'account', name: 'encrypted name', __subject: 'owner'
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should release all present properties together', () => context.release.mock.calls.should.have.lengthOf(1));
|
|
23
|
+
it('should use the stored subject rather than the id', () => context.requestFor('owner').Subject.should.equal('owner'));
|
|
24
|
+
it('should use the current event store and namespace', () => {
|
|
25
|
+
const { EventStore, Namespace } = context.requestFor('owner');
|
|
26
|
+
({ EventStore, Namespace }).should.deep.equal({ EventStore: 'store', Namespace: 'tenant' });
|
|
27
|
+
});
|
|
28
|
+
it('should send only data properties', () => JSON.parse(context.requestFor('owner').Payload).should.deep.equal({ id: 'account', name: 'encrypted name' }));
|
|
29
|
+
it('should preserve compliance metadata in the relevant schema', () => {
|
|
30
|
+
const schema = JsonSchemaGenerator.generate(AccountDocument);
|
|
31
|
+
JSON.parse(context.requestFor('owner').Schema).should.deep.equal({
|
|
32
|
+
...schema,
|
|
33
|
+
properties: { id: schema.properties!.id, name: schema.properties!.name },
|
|
34
|
+
required: ['id', 'name']
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
it('should return the released values', () => result.should.deep.equal({ id: 'account', name: 'released name' }));
|
|
38
|
+
it('should return a plain document rather than a model instance', () => Object.getPrototypeOf(result).should.equal(Object.prototype));
|
|
39
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when releasing a frozen document', () => {
|
|
10
|
+
let document: Readonly<Record<string, unknown>>;
|
|
11
|
+
let result: Record<string, unknown>;
|
|
12
|
+
let original: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
const context = new a_stored_read_model();
|
|
16
|
+
document = Object.freeze({
|
|
17
|
+
name: 'encrypted name', contact: Object.freeze({ phone: 'unchanged phone' }),
|
|
18
|
+
__subjects: Object.freeze({ name: 'owner' })
|
|
19
|
+
});
|
|
20
|
+
original = JSON.stringify(document);
|
|
21
|
+
context.release.mockResolvedValue({ HasError: false, Error: '', Payload: '{"name":"released name"}' });
|
|
22
|
+
result = await context.readModels.releaseDocument(AccountDocument, document);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should not mutate the document or its metadata', () => JSON.stringify(document).should.equal(original));
|
|
26
|
+
it('should release into a separate document', () => result.should.deep.equal({ name: 'released name', contact: { phone: 'unchanged phone' } }));
|
|
27
|
+
it('should copy even the subtrees that are not released', () => result.contact!.should.not.equal(document.contact));
|
|
28
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
const should = chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each(['private@example.test', '', 'null', '[]', '"private@example.test"', '42'])('when releasing a document with malformed response %s', payload => {
|
|
10
|
+
let result: Record<string, unknown> | undefined;
|
|
11
|
+
let error: Error;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
const context = new a_stored_read_model();
|
|
15
|
+
result = undefined;
|
|
16
|
+
context.release.mockResolvedValue({ HasError: false, Error: '', Payload: payload });
|
|
17
|
+
await context.readModels.releaseDocument(AccountDocument, { name: 'encrypted name', __subject: 'owner' }).then(
|
|
18
|
+
released => { result = released; }, caught => { error = caught as Error; }
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should reject without returning data', () => should.not.exist(result));
|
|
23
|
+
it('should sanitize parsing failures', () => error.message.should.equal('Failed to release PII in stored read model document.'));
|
|
24
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
const should = chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each(['kernel', 'transport'])('when releasing a document and a later subject reports a %s failure', failure => {
|
|
10
|
+
let context: a_stored_read_model;
|
|
11
|
+
let result: Record<string, unknown> | undefined;
|
|
12
|
+
let error: Error;
|
|
13
|
+
let document: Record<string, unknown>;
|
|
14
|
+
let original: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
context = new a_stored_read_model();
|
|
18
|
+
result = undefined;
|
|
19
|
+
document = { name: 'encrypted name', email: 'encrypted email', __subjects: { name: 'owner', email: 'other-owner' } };
|
|
20
|
+
original = JSON.stringify(document);
|
|
21
|
+
context.release.mockResolvedValueOnce({ HasError: false, Error: '', Payload: '{"name":"released name"}' });
|
|
22
|
+
if (failure === 'kernel') {
|
|
23
|
+
context.release.mockResolvedValueOnce({ HasError: true, Error: 'PII private@example.test', Payload: '{"email":"private@example.test"}' });
|
|
24
|
+
} else {
|
|
25
|
+
context.release.mockRejectedValueOnce(new Error('Transport details: private@example.test'));
|
|
26
|
+
}
|
|
27
|
+
await context.readModels.releaseDocument(AccountDocument, document).then(
|
|
28
|
+
released => { result = released; }, caught => { error = caught as Error; }
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should have attempted both subjects', () => context.release.mock.calls.should.have.lengthOf(2));
|
|
33
|
+
it('should reject the whole operation', () => error.should.be.instanceOf(Error));
|
|
34
|
+
it('should never expose the earlier released group', () => should.not.exist(result));
|
|
35
|
+
it('should leave the input unchanged even after a partial release', () => JSON.stringify(document).should.equal(original));
|
|
36
|
+
it('should not expose PII from errors or payloads', () => error.message.should.equal('Failed to release PII in stored read model document.'));
|
|
37
|
+
it('should not attach an unsafe cause', () => should.not.exist(error.cause));
|
|
38
|
+
});
|
package/readModels/for_ReadModels/when_releasing_a_document/with_an_empty_released_value.spec.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when releasing a document and the kernel reports success with an empty value', () => {
|
|
10
|
+
let result: Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
const context = new a_stored_read_model();
|
|
14
|
+
context.release.mockResolvedValue({ HasError: false, Error: '', Payload: '{"name":""}' });
|
|
15
|
+
result = await context.readModels.releaseDocument(AccountDocument, { name: 'encrypted name', __subject: 'owner' });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should accept the reported success without guessing whether decryption failed', () => result.should.deep.equal({ name: '' }));
|
|
19
|
+
});
|
package/readModels/for_ReadModels/when_releasing_a_document/with_an_unknown_subject_property.spec.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each(['unknown', 'contact.phone', '__subject', '__subjects', 'toString'])('when releasing a document with unknown subject property %s', property => {
|
|
10
|
+
let context: a_stored_read_model;
|
|
11
|
+
let error: Error;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
context = new a_stored_read_model();
|
|
15
|
+
await context.readModels.releaseDocument(AccountDocument, {
|
|
16
|
+
name: 'encrypted name', contact: { phone: 'encrypted phone' }, unknown: 'not declared',
|
|
17
|
+
'contact.phone': 'not declared', toString: 'not declared',
|
|
18
|
+
__subject: 'owner', __subjects: { [property]: 'other-owner' }
|
|
19
|
+
}).catch(caught => { error = caught as Error; });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should reject undeclared, nested, reserved and inherited property names', () => error.message.should.equal('Stored read model document has an unknown subject property.'));
|
|
23
|
+
it('should validate the entire subject map before attempting any releases', () => context.release.mock.calls.should.have.lengthOf(0));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe.each(['email', 'removedProperty', 'contact.phone', 'toString'])('when releasing a document with a stale subject property %s', property => {
|
|
27
|
+
let context: a_stored_read_model;
|
|
28
|
+
let result: Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
context = new a_stored_read_model();
|
|
32
|
+
result = await context.readModels.releaseDocument(AccountDocument, {
|
|
33
|
+
name: 'encrypted name', __subject: 'owner', __subjects: { [property]: 'other-owner' }
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should ignore mappings without a corresponding document property', () => result.should.deep.equal({ name: 'encrypted name' }));
|
|
38
|
+
it('should release only the existing properties under their subject', () => {
|
|
39
|
+
context.release.mock.calls.should.have.lengthOf(1);
|
|
40
|
+
JSON.parse(context.requestFor('owner').Payload).should.deep.equal({ name: 'encrypted name' });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each([undefined, 'owner'])('when releasing a document with bookkeeping and default subject %s', defaultSubject => {
|
|
10
|
+
let context: a_stored_read_model;
|
|
11
|
+
let result: Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
context = new a_stored_read_model();
|
|
15
|
+
result = await context.readModels.releaseDocument(AccountDocument, {
|
|
16
|
+
id: 'account', name: 'stored name', _id: 'storage-id', __generation: 1,
|
|
17
|
+
__subject: defaultSubject, __subjects: {}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should strip storage-only and subject metadata', () => result.should.deep.equal({ id: 'account', name: 'stored name' }));
|
|
22
|
+
it('should never send bookkeeping for release', () => {
|
|
23
|
+
context.release.mock.calls.forEach(([request]) => {
|
|
24
|
+
JSON.parse(request.Payload).should.deep.equal({ id: 'account', name: 'stored name' });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
const should = chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each([
|
|
10
|
+
'{}',
|
|
11
|
+
'{"name":"wrong group"}',
|
|
12
|
+
'{"email":"released email","name":"overwrite released group"}',
|
|
13
|
+
'{"email":"released email","id":"overwrite unreleased property"}',
|
|
14
|
+
'{"email":"released email","__proto__":{"polluted":true}}'
|
|
15
|
+
])('when releasing a document with missing or conflicting response properties %s', payload => {
|
|
16
|
+
let result: Record<string, unknown> | undefined;
|
|
17
|
+
let error: Error;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
const context = new a_stored_read_model();
|
|
21
|
+
result = undefined;
|
|
22
|
+
context.release.mockResolvedValueOnce({ HasError: false, Error: '', Payload: '{"name":"released name"}' });
|
|
23
|
+
context.release.mockResolvedValueOnce({ HasError: false, Error: '', Payload: payload });
|
|
24
|
+
await context.readModels.releaseDocument(AccountDocument, {
|
|
25
|
+
id: 'account', name: 'encrypted name', email: 'encrypted email',
|
|
26
|
+
__subjects: { name: 'owner', email: 'other-owner' }
|
|
27
|
+
}).then(released => { result = released; }, caught => { error = caught as Error; });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should reject the whole operation', () => error.message.should.equal('Failed to release stored read model document: missing or conflicting properties.'));
|
|
31
|
+
it('should not expose a partial result', () => should.not.exist(result));
|
|
32
|
+
});
|
package/readModels/for_ReadModels/when_releasing_a_document/with_empty_subject_metadata.spec.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each([
|
|
10
|
+
{ __subject: null }, { __subject: '' },
|
|
11
|
+
{ __subjects: { name: null } }, { __subjects: { name: '' } },
|
|
12
|
+
{ __subjects: null }, { __subjects: [] }, { __subjects: 'owner' }, { __subjects: 42 }
|
|
13
|
+
])('when releasing a document without usable subject metadata %j', metadata => {
|
|
14
|
+
let context: a_stored_read_model;
|
|
15
|
+
let result: Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
context = new a_stored_read_model();
|
|
19
|
+
result = await context.readModels.releaseDocument(AccountDocument, { name: 'stored name', ...metadata });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should copy properties without a subject unchanged', () => result.should.deep.equal({ name: 'stored name' }));
|
|
23
|
+
it('should not attempt release', () => context.release.mock.calls.should.have.lengthOf(0));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe.each([null, ''])('when releasing a document with an empty property subject %j and a default subject', subject => {
|
|
27
|
+
let context: a_stored_read_model;
|
|
28
|
+
let result: Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
context = new a_stored_read_model();
|
|
32
|
+
context.release.mockResolvedValue({ HasError: false, Error: '', Payload: JSON.stringify({ name: 'released name' }) });
|
|
33
|
+
result = await context.readModels.releaseDocument(AccountDocument, {
|
|
34
|
+
name: 'encrypted name', __subject: 'owner', __subjects: { name: subject }
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should fall back to the default subject', () => context.requestFor('owner').Subject.should.equal('owner'));
|
|
39
|
+
it('should release the property', () => result.should.deep.equal({ name: 'released name' }));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe.each([null, ''])('when releasing a document with an empty default subject %j and a property subject', subject => {
|
|
43
|
+
let context: a_stored_read_model;
|
|
44
|
+
let result: Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
context = new a_stored_read_model();
|
|
48
|
+
context.release.mockResolvedValue({ HasError: false, Error: '', Payload: JSON.stringify({ name: 'released name' }) });
|
|
49
|
+
result = await context.readModels.releaseDocument(AccountDocument, {
|
|
50
|
+
id: 'account', name: 'encrypted name', __subject: subject, __subjects: { name: 'owner' }
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should release only the property with a subject', () => JSON.parse(context.requestFor('owner').Payload).should.deep.equal({ name: 'encrypted name' }));
|
|
55
|
+
it('should retain properties without a subject', () => result.should.deep.equal({ id: 'account', name: 'released name' }));
|
|
56
|
+
});
|
package/readModels/for_ReadModels/when_releasing_a_document/with_invalid_subject_metadata.spec.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each([
|
|
10
|
+
{ __subject: 42 }, { __subject: false }, { __subject: [] }, { __subject: {} },
|
|
11
|
+
{ __subjects: { name: 42 } }, { __subjects: { name: false } },
|
|
12
|
+
{ __subjects: { name: [] } }, { __subjects: { name: { subject: 'private@example.test' } } },
|
|
13
|
+
{ __subjects: { email: 42 } }, { __subjects: { removedProperty: 42 } }
|
|
14
|
+
])('when releasing a document with invalid subject metadata %j', metadata => {
|
|
15
|
+
let context: a_stored_read_model;
|
|
16
|
+
let error: Error;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
context = new a_stored_read_model();
|
|
20
|
+
await context.readModels.releaseDocument(AccountDocument, { name: 'encrypted name', ...metadata })
|
|
21
|
+
.catch(caught => { error = caught as Error; });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should reject the document', () => error.should.be.instanceOf(Error));
|
|
25
|
+
it('should not expose metadata values in the error', () => error.message.should.not.contain('private@example.test'));
|
|
26
|
+
it('should not attempt release with invalid metadata', () => context.release.mock.calls.should.have.lengthOf(0));
|
|
27
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when releasing a document with mixed default and property subjects', () => {
|
|
10
|
+
let context: a_stored_read_model;
|
|
11
|
+
let result: Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
context = new a_stored_read_model();
|
|
15
|
+
context.release.mockImplementation(async request => ({
|
|
16
|
+
HasError: false, Error: '', Payload: JSON.stringify(request.Subject === 'owner'
|
|
17
|
+
? { id: 'account', name: 'released name' } : { email: 'released email' })
|
|
18
|
+
}));
|
|
19
|
+
result = await context.readModels.releaseDocument(AccountDocument, {
|
|
20
|
+
id: 'account', name: 'encrypted name', email: 'encrypted email',
|
|
21
|
+
__subject: 'owner', __subjects: { name: 'owner', email: 'joined-owner' }
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should reuse the default subject group for matching property subjects', () => context.release.mock.calls.should.have.lengthOf(2));
|
|
26
|
+
it('should apply the default to unmapped properties', () => {
|
|
27
|
+
JSON.parse(context.requestFor('owner').Payload).should.deep.equal({ id: 'account', name: 'encrypted name' });
|
|
28
|
+
});
|
|
29
|
+
it('should override the default for mapped properties', () => {
|
|
30
|
+
JSON.parse(context.requestFor('joined-owner').Payload).should.deep.equal({ email: 'encrypted email' });
|
|
31
|
+
});
|
|
32
|
+
it('should send a schema limited to the overridden property', () => {
|
|
33
|
+
Object.keys(JSON.parse(context.requestFor('joined-owner').Schema).properties).should.deep.equal(['email']);
|
|
34
|
+
});
|
|
35
|
+
it('should merge the released properties without bookkeeping', () => result.should.deep.equal({
|
|
36
|
+
id: 'account', name: 'released name', email: 'released email'
|
|
37
|
+
}));
|
|
38
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { JsonSchemaGenerator } from '../../../schemas/index.js';
|
|
6
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
7
|
+
|
|
8
|
+
chai.should();
|
|
9
|
+
|
|
10
|
+
describe('when releasing a document with property subjects', () => {
|
|
11
|
+
let context: a_stored_read_model;
|
|
12
|
+
let result: Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
context = new a_stored_read_model();
|
|
16
|
+
context.release.mockImplementation(async request => ({
|
|
17
|
+
HasError: false, Error: '', Payload: JSON.stringify(request.Subject === 'owner'
|
|
18
|
+
? { name: 'released name', email: 'released email' }
|
|
19
|
+
: { contact: { phone: 'released phone' }, contacts: [{ phone: 'released list phone' }] })
|
|
20
|
+
}));
|
|
21
|
+
result = await context.readModels.releaseDocument(AccountDocument, {
|
|
22
|
+
id: 'account', name: 'encrypted name', email: 'encrypted email',
|
|
23
|
+
contact: { phone: 'encrypted phone' }, contacts: [{ phone: 'encrypted list phone' }],
|
|
24
|
+
__subjects: { name: 'owner', email: 'owner', contact: 'contact-owner', contacts: 'contact-owner' }
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should batch properties by distinct subject', () => context.release.mock.calls.should.have.lengthOf(2));
|
|
29
|
+
it('should send only the owner properties to that subject', () => {
|
|
30
|
+
JSON.parse(context.requestFor('owner').Payload).should.deep.equal({ name: 'encrypted name', email: 'encrypted email' });
|
|
31
|
+
});
|
|
32
|
+
it('should release each nested subtree and array under its top-level subject', () => {
|
|
33
|
+
JSON.parse(context.requestFor('contact-owner').Payload).should.deep.equal({
|
|
34
|
+
contact: { phone: 'encrypted phone' }, contacts: [{ phone: 'encrypted list phone' }]
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
it('should retain nested compliance schemas', () => {
|
|
38
|
+
const properties = JsonSchemaGenerator.generate(AccountDocument).properties!;
|
|
39
|
+
JSON.parse(context.requestFor('contact-owner').Schema).properties.should.deep.equal({
|
|
40
|
+
contact: properties.contact, contacts: properties.contacts
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('should include the array item phone PII metadata in the subject-specific schema', () => {
|
|
44
|
+
JSON.parse(context.requestFor('contact-owner').Schema).properties.contacts.items.properties.phone.should.deep.equal({
|
|
45
|
+
type: 'string', compliance: [{ metadataType: 'PII', details: '' }]
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should combine the released groups and retain properties without a subject', () => result.should.deep.equal({
|
|
49
|
+
id: 'account', name: 'released name', email: 'released email',
|
|
50
|
+
contact: { phone: 'released phone' }, contacts: [{ phone: 'released list phone' }]
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe.each([
|
|
10
|
+
{ __subject: ' \t' },
|
|
11
|
+
{ __subject: 'owner', __subjects: { name: ' \t' } }
|
|
12
|
+
])('when releasing a document with a whitespace-only subject %j', metadata => {
|
|
13
|
+
let context: a_stored_read_model;
|
|
14
|
+
let result: Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
context = new a_stored_read_model();
|
|
18
|
+
context.release.mockResolvedValue({ HasError: false, Error: '', Payload: JSON.stringify({ name: 'released name' }) });
|
|
19
|
+
result = await context.readModels.releaseDocument(AccountDocument, { name: 'encrypted name', ...metadata });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should preserve the exact subject in the release request', () => context.requestFor(' \t').Subject.should.equal(' \t'));
|
|
23
|
+
it('should release the property', () => result.should.deep.equal({ name: 'released name' }));
|
|
24
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { beforeEach, chai, describe, it } from 'vitest';
|
|
5
|
+
import { AccountDocument, a_stored_read_model } from '../given/a_stored_read_model.js';
|
|
6
|
+
|
|
7
|
+
chai.should();
|
|
8
|
+
|
|
9
|
+
describe('when releasing a document without subjects', () => {
|
|
10
|
+
let context: a_stored_read_model;
|
|
11
|
+
let document: Record<string, unknown>;
|
|
12
|
+
let result: Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
context = new a_stored_read_model();
|
|
16
|
+
document = { id: 'account', name: 'unchanged', contact: { phone: 'unchanged phone' } };
|
|
17
|
+
result = await context.readModels.releaseDocument(AccountDocument, document);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should leave data values unchanged', () => result.should.deep.equal(document));
|
|
21
|
+
it('should not infer a subject from id or subject decorators', () => context.release.mock.calls.should.have.lengthOf(0));
|
|
22
|
+
it('should return a copy', () => result.should.not.equal(document));
|
|
23
|
+
it('should not share unreleased subtrees with the input', () => result.contact!.should.not.equal(document.contact));
|
|
24
|
+
});
|
package/reducers/Reducers.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import 'reflect-metadata';
|
|
5
|
-
import {
|
|
5
|
+
import { createLogger } from '../logging/createLogger.js';
|
|
6
|
+
import { runInBackgroundContext } from '../telemetry/runInBackgroundContext.js';
|
|
6
7
|
import { Constructor } from '@cratis/fundamentals';
|
|
7
8
|
import { ObservationState, ReadModelObserverType, ReducerMessage, ReplayState } from '@cratis/chronicle.contracts';
|
|
8
9
|
import { IClientArtifactsProvider } from '../artifacts/index.js';
|
|
@@ -102,7 +103,7 @@ export class Reducers implements IReducers {
|
|
|
102
103
|
/** How long to wait before re-establishing an observation whose stream ended. */
|
|
103
104
|
private static readonly _reobserveDelayMs = 2000;
|
|
104
105
|
|
|
105
|
-
private readonly _logger
|
|
106
|
+
private readonly _logger: ReturnType<typeof createLogger>;
|
|
106
107
|
private readonly _lifecycle: ConnectionLifecycle;
|
|
107
108
|
private readonly _reducers = new Map<string, Constructor>();
|
|
108
109
|
private readonly _queues = new Map<string, AsyncQueue<ReducerMessage>>();
|
|
@@ -133,6 +134,7 @@ export class Reducers implements IReducers {
|
|
|
133
134
|
private readonly _artifactActivator?: ClientArtifactsActivator,
|
|
134
135
|
private readonly _readModelNamingPolicy?: ReadModelNamingPolicy
|
|
135
136
|
) {
|
|
137
|
+
this._logger = createLogger('@cratis/chronicle/reducers', _connection.logger);
|
|
136
138
|
this._lifecycle = lifecycle;
|
|
137
139
|
lifecycle.onDisconnected(async () => {
|
|
138
140
|
this._logger.info('Disconnected — stopping all reducer observations');
|
|
@@ -235,21 +237,23 @@ export class Reducers implements IReducers {
|
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
private startObservation(id: string, reducerType: Constructor): void {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
this._logger.info('Starting reducer observation', {
|
|
245
|
-
reducerId: id,
|
|
246
|
-
eventSequenceId,
|
|
247
|
-
readModel: readModelName,
|
|
248
|
-
handlerCount: dispatcher.handlers.length,
|
|
249
|
-
handlers: dispatcher.handlers.map(e => e.methodName)
|
|
250
|
-
});
|
|
240
|
+
runInBackgroundContext(() => {
|
|
241
|
+
if (this._disposed) return;
|
|
242
|
+
const metadata = getReducerMetadata(reducerType)!;
|
|
243
|
+
const eventSequenceId = metadata.eventSequenceId ?? EventSequenceId.eventLog.value;
|
|
244
|
+
const dispatcher = new ReducerEventDispatcher(reducerType, this._clientArtifacts.eventTypes);
|
|
245
|
+
const readModelName = this.getReducerReadModelIdentifier(reducerType);
|
|
251
246
|
|
|
252
|
-
|
|
247
|
+
this._logger.info('Starting reducer observation', {
|
|
248
|
+
reducerId: id,
|
|
249
|
+
eventSequenceId,
|
|
250
|
+
readModel: readModelName,
|
|
251
|
+
handlerCount: dispatcher.handlers.length,
|
|
252
|
+
handlers: dispatcher.handlers.map(e => e.methodName)
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
void this.runObservation(id, reducerType, eventSequenceId, readModelName, dispatcher);
|
|
256
|
+
});
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
private async runObservation(
|
|
@@ -262,7 +266,7 @@ export class Reducers implements IReducers {
|
|
|
262
266
|
try {
|
|
263
267
|
await this.observeReducer(id, reducerType, eventSequenceId, readModelName, dispatcher);
|
|
264
268
|
} catch (error) {
|
|
265
|
-
this._logger.error('Reducer observation loop exited with error', { reducerId: id, error
|
|
269
|
+
this._logger.error('Reducer observation loop exited with error', { reducerId: id, error });
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
this.scheduleReobserve(id, reducerType);
|
|
@@ -285,14 +289,14 @@ export class Reducers implements IReducers {
|
|
|
285
289
|
return;
|
|
286
290
|
}
|
|
287
291
|
|
|
288
|
-
const handle = setTimeout(() => {
|
|
292
|
+
const handle = runInBackgroundContext(() => setTimeout(() => {
|
|
289
293
|
if (!this._registered || this._disposed) {
|
|
290
294
|
return;
|
|
291
295
|
}
|
|
292
296
|
|
|
293
297
|
this._logger.info('Re-establishing reducer observation', { reducerId: id });
|
|
294
298
|
this.startObservation(id, reducerType);
|
|
295
|
-
}, Reducers._reobserveDelayMs);
|
|
299
|
+
}, Reducers._reobserveDelayMs));
|
|
296
300
|
|
|
297
301
|
handle.unref?.();
|
|
298
302
|
}
|
|
@@ -359,7 +363,6 @@ export class Reducers implements IReducers {
|
|
|
359
363
|
|
|
360
364
|
this._logger.debug('Received reduce operation', {
|
|
361
365
|
reducerId: id,
|
|
362
|
-
partition: operation.Partition,
|
|
363
366
|
count: operation.Events.length,
|
|
364
367
|
hasInitialState: operation.InitialState !== '',
|
|
365
368
|
replayState: operation.ReplayState
|
|
@@ -375,12 +378,12 @@ export class Reducers implements IReducers {
|
|
|
375
378
|
replayState: operation.ReplayState
|
|
376
379
|
}, this._artifactActivator, artifact => runActivated(artifact, () =>
|
|
377
380
|
notifyReplayLifecycle(artifact.instance, operation.ReplayState, operation.Partition),
|
|
378
|
-
{ delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState }));
|
|
381
|
+
{ delivery: ArtifactDelivery.ReplayNotification, replayState: operation.ReplayState }), this._connection.logger);
|
|
379
382
|
} else if (reducerInstance) {
|
|
380
383
|
await notifyReplayLifecycle(reducerInstance, operation.ReplayState, operation.Partition);
|
|
381
384
|
}
|
|
382
385
|
} catch (err) {
|
|
383
|
-
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error:
|
|
386
|
+
this._logger.error('Error notifying reducer of replay lifecycle transition', { reducerId: id, error: err });
|
|
384
387
|
if (err instanceof ArtifactCompletionFailed && err.processingError !== undefined) {
|
|
385
388
|
exceptionMessages.push(String(err.processingError));
|
|
386
389
|
}
|
|
@@ -424,7 +427,7 @@ export class Reducers implements IReducers {
|
|
|
424
427
|
{ delivery: ArtifactDelivery.Events, eventContext: context, methodName: entry.methodName });
|
|
425
428
|
lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
|
|
426
429
|
} catch (err) {
|
|
427
|
-
this._logger.error('Error handling event in reducer', { reducerId: id, error:
|
|
430
|
+
this._logger.error('Error handling event in reducer', { reducerId: id, error: err });
|
|
428
431
|
exceptionMessages.push(String(err));
|
|
429
432
|
exceptionStackTrace = err instanceof Error ? (err.stack ?? '') : '';
|
|
430
433
|
state = ObservationState.Failed;
|
|
@@ -445,9 +448,9 @@ export class Reducers implements IReducers {
|
|
|
445
448
|
readModels: this._eventStore.readModels, eventSequenceId, partition: operation.Partition,
|
|
446
449
|
signal: controller.signal, delivery: ArtifactDelivery.Events,
|
|
447
450
|
eventContext: toClientEventContext(firstInvocableEvent.Context!)
|
|
448
|
-
}, this._artifactActivator, processEvents);
|
|
451
|
+
}, this._artifactActivator, processEvents, this._connection.logger);
|
|
449
452
|
} catch (err) {
|
|
450
|
-
this._logger.error('Error activating reducer', { reducerId: id, error:
|
|
453
|
+
this._logger.error('Error activating reducer', { reducerId: id, error: err });
|
|
451
454
|
exceptionMessages.push(String(err));
|
|
452
455
|
const completionStack = err instanceof Error ? (err.stack ?? '') : '';
|
|
453
456
|
exceptionStackTrace = err instanceof ArtifactCompletionFailed && exceptionStackTrace
|
|
@@ -483,7 +486,7 @@ export class Reducers implements IReducers {
|
|
|
483
486
|
if (!this._queues.has(id)) {
|
|
484
487
|
this._logger.debug('Reducer observation stream closed cleanly', { reducerId: id });
|
|
485
488
|
} else {
|
|
486
|
-
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error:
|
|
489
|
+
this._logger.error('Reducer observation stream ended unexpectedly', { reducerId: id, error: err });
|
|
487
490
|
}
|
|
488
491
|
} finally {
|
|
489
492
|
// Only retire our own queue: a reconnect can already have replaced it,
|