@cqrs-ddd/core 0.2.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/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +707 -0
- package/dist/application/base.command.d.ts +44 -0
- package/dist/application/base.command.d.ts.map +1 -0
- package/dist/application/base.command.js +58 -0
- package/dist/application/base.command.js.map +1 -0
- package/dist/application/base.query.d.ts +42 -0
- package/dist/application/base.query.d.ts.map +1 -0
- package/dist/application/base.query.js +52 -0
- package/dist/application/base.query.js.map +1 -0
- package/dist/application/command-base.handler.d.ts +106 -0
- package/dist/application/command-base.handler.d.ts.map +1 -0
- package/dist/application/command-base.handler.js +119 -0
- package/dist/application/command-base.handler.js.map +1 -0
- package/dist/application/index.d.ts +16 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +33 -0
- package/dist/application/index.js.map +1 -0
- package/dist/application/ports/cache.port.d.ts +104 -0
- package/dist/application/ports/cache.port.d.ts.map +1 -0
- package/dist/application/ports/cache.port.js +17 -0
- package/dist/application/ports/cache.port.js.map +1 -0
- package/dist/application/ports/command-repository.port.d.ts +28 -0
- package/dist/application/ports/command-repository.port.d.ts.map +1 -0
- package/dist/application/ports/command-repository.port.js +4 -0
- package/dist/application/ports/command-repository.port.js.map +1 -0
- package/dist/application/ports/domain-event-publisher.port.d.ts +18 -0
- package/dist/application/ports/domain-event-publisher.port.d.ts.map +1 -0
- package/dist/application/ports/domain-event-publisher.port.js +4 -0
- package/dist/application/ports/domain-event-publisher.port.js.map +1 -0
- package/dist/application/ports/query-repository.port.d.ts +26 -0
- package/dist/application/ports/query-repository.port.d.ts.map +1 -0
- package/dist/application/ports/query-repository.port.js +4 -0
- package/dist/application/ports/query-repository.port.js.map +1 -0
- package/dist/application/ports/write-side-aggregate-repository.port.d.ts +33 -0
- package/dist/application/ports/write-side-aggregate-repository.port.d.ts.map +1 -0
- package/dist/application/ports/write-side-aggregate-repository.port.js +4 -0
- package/dist/application/ports/write-side-aggregate-repository.port.js.map +1 -0
- package/dist/application/query.options.d.ts +22 -0
- package/dist/application/query.options.d.ts.map +1 -0
- package/dist/application/query.options.js +4 -0
- package/dist/application/query.options.js.map +1 -0
- package/dist/application/request-fields.helper.d.ts +5 -0
- package/dist/application/request-fields.helper.d.ts.map +1 -0
- package/dist/application/request-fields.helper.js +20 -0
- package/dist/application/request-fields.helper.js.map +1 -0
- package/dist/application/tenant-resolver.d.ts +56 -0
- package/dist/application/tenant-resolver.d.ts.map +1 -0
- package/dist/application/tenant-resolver.js +60 -0
- package/dist/application/tenant-resolver.js.map +1 -0
- package/dist/domain/decorators/apply-mutation.decorator.d.ts +64 -0
- package/dist/domain/decorators/apply-mutation.decorator.d.ts.map +1 -0
- package/dist/domain/decorators/apply-mutation.decorator.js +86 -0
- package/dist/domain/decorators/apply-mutation.decorator.js.map +1 -0
- package/dist/domain/decorators/mutable.decorator.d.ts +54 -0
- package/dist/domain/decorators/mutable.decorator.d.ts.map +1 -0
- package/dist/domain/decorators/mutable.decorator.js +76 -0
- package/dist/domain/decorators/mutable.decorator.js.map +1 -0
- package/dist/domain/events/domain.event.d.ts +13 -0
- package/dist/domain/events/domain.event.d.ts.map +1 -0
- package/dist/domain/events/domain.event.js +20 -0
- package/dist/domain/events/domain.event.js.map +1 -0
- package/dist/domain/events/event.interface.d.ts +9 -0
- package/dist/domain/events/event.interface.d.ts.map +1 -0
- package/dist/domain/events/event.interface.js +4 -0
- package/dist/domain/events/event.interface.js.map +1 -0
- package/dist/domain/events/root-domain.event.d.ts +50 -0
- package/dist/domain/events/root-domain.event.d.ts.map +1 -0
- package/dist/domain/events/root-domain.event.js +156 -0
- package/dist/domain/events/root-domain.event.js.map +1 -0
- package/dist/domain/exceptions/concurrency-conflict.error.d.ts +20 -0
- package/dist/domain/exceptions/concurrency-conflict.error.d.ts.map +1 -0
- package/dist/domain/exceptions/concurrency-conflict.error.js +32 -0
- package/dist/domain/exceptions/concurrency-conflict.error.js.map +1 -0
- package/dist/domain/exceptions/domain.exception.d.ts +30 -0
- package/dist/domain/exceptions/domain.exception.d.ts.map +1 -0
- package/dist/domain/exceptions/domain.exception.js +39 -0
- package/dist/domain/exceptions/domain.exception.js.map +1 -0
- package/dist/domain/exceptions/entity-not-found.exception.d.ts +14 -0
- package/dist/domain/exceptions/entity-not-found.exception.d.ts.map +1 -0
- package/dist/domain/exceptions/entity-not-found.exception.js +23 -0
- package/dist/domain/exceptions/entity-not-found.exception.js.map +1 -0
- package/dist/domain/exceptions/invalid-value.exception.d.ts +35 -0
- package/dist/domain/exceptions/invalid-value.exception.d.ts.map +1 -0
- package/dist/domain/exceptions/invalid-value.exception.js +67 -0
- package/dist/domain/exceptions/invalid-value.exception.js.map +1 -0
- package/dist/domain/exceptions/missing-tenant-context.exception.d.ts +24 -0
- package/dist/domain/exceptions/missing-tenant-context.exception.d.ts.map +1 -0
- package/dist/domain/exceptions/missing-tenant-context.exception.js +33 -0
- package/dist/domain/exceptions/missing-tenant-context.exception.js.map +1 -0
- package/dist/domain/exceptions/transient-operation.error.d.ts +17 -0
- package/dist/domain/exceptions/transient-operation.error.d.ts.map +1 -0
- package/dist/domain/exceptions/transient-operation.error.js +27 -0
- package/dist/domain/exceptions/transient-operation.error.js.map +1 -0
- package/dist/domain/exceptions/unknown-mutable-field.error.d.ts +14 -0
- package/dist/domain/exceptions/unknown-mutable-field.error.d.ts.map +1 -0
- package/dist/domain/exceptions/unknown-mutable-field.error.js +24 -0
- package/dist/domain/exceptions/unknown-mutable-field.error.js.map +1 -0
- package/dist/domain/index.d.ts +26 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +43 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/interfaces/root-entity-snapshot.interface.d.ts +7 -0
- package/dist/domain/interfaces/root-entity-snapshot.interface.d.ts.map +1 -0
- package/dist/domain/interfaces/root-entity-snapshot.interface.js +4 -0
- package/dist/domain/interfaces/root-entity-snapshot.interface.js.map +1 -0
- package/dist/domain/models/aggregate-root.d.ts +87 -0
- package/dist/domain/models/aggregate-root.d.ts.map +1 -0
- package/dist/domain/models/aggregate-root.js +122 -0
- package/dist/domain/models/aggregate-root.js.map +1 -0
- package/dist/domain/models/root.entity.d.ts +208 -0
- package/dist/domain/models/root.entity.d.ts.map +1 -0
- package/dist/domain/models/root.entity.js +350 -0
- package/dist/domain/models/root.entity.js.map +1 -0
- package/dist/domain/rules/number.rule.d.ts +81 -0
- package/dist/domain/rules/number.rule.d.ts.map +1 -0
- package/dist/domain/rules/number.rule.js +81 -0
- package/dist/domain/rules/number.rule.js.map +1 -0
- package/dist/domain/rules/rule-support.d.ts +6 -0
- package/dist/domain/rules/rule-support.d.ts.map +1 -0
- package/dist/domain/rules/rule-support.js +19 -0
- package/dist/domain/rules/rule-support.js.map +1 -0
- package/dist/domain/rules/text.rule.d.ts +98 -0
- package/dist/domain/rules/text.rule.d.ts.map +1 -0
- package/dist/domain/rules/text.rule.js +103 -0
- package/dist/domain/rules/text.rule.js.map +1 -0
- package/dist/domain/rules/value-violation.d.ts +44 -0
- package/dist/domain/rules/value-violation.d.ts.map +1 -0
- package/dist/domain/rules/value-violation.js +4 -0
- package/dist/domain/rules/value-violation.js.map +1 -0
- package/dist/http/domain-error-http-status.d.ts +43 -0
- package/dist/http/domain-error-http-status.d.ts.map +1 -0
- package/dist/http/domain-error-http-status.js +27 -0
- package/dist/http/domain-error-http-status.js.map +1 -0
- package/dist/http/index.d.ts +9 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +26 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/cache/cache-logger.d.ts +29 -0
- package/dist/persistence/cache/cache-logger.d.ts.map +1 -0
- package/dist/persistence/cache/cache-logger.js +23 -0
- package/dist/persistence/cache/cache-logger.js.map +1 -0
- package/dist/persistence/cache/memory.cache.d.ts +72 -0
- package/dist/persistence/cache/memory.cache.d.ts.map +1 -0
- package/dist/persistence/cache/memory.cache.js +183 -0
- package/dist/persistence/cache/memory.cache.js.map +1 -0
- package/dist/persistence/command-repository.abstract.d.ts +39 -0
- package/dist/persistence/command-repository.abstract.d.ts.map +1 -0
- package/dist/persistence/command-repository.abstract.js +43 -0
- package/dist/persistence/command-repository.abstract.js.map +1 -0
- package/dist/persistence/decorators/acknowledge-persisted.decorator.d.ts +82 -0
- package/dist/persistence/decorators/acknowledge-persisted.decorator.d.ts.map +1 -0
- package/dist/persistence/decorators/acknowledge-persisted.decorator.js +82 -0
- package/dist/persistence/decorators/acknowledge-persisted.decorator.js.map +1 -0
- package/dist/persistence/decorators/cache.decorator.d.ts +93 -0
- package/dist/persistence/decorators/cache.decorator.d.ts.map +1 -0
- package/dist/persistence/decorators/cache.decorator.js +200 -0
- package/dist/persistence/decorators/cache.decorator.js.map +1 -0
- package/dist/persistence/decorators/from-cache.decorator.d.ts +65 -0
- package/dist/persistence/decorators/from-cache.decorator.d.ts.map +1 -0
- package/dist/persistence/decorators/from-cache.decorator.js +165 -0
- package/dist/persistence/decorators/from-cache.decorator.js.map +1 -0
- package/dist/persistence/decorators/map-persistence-errors.decorator.d.ts +88 -0
- package/dist/persistence/decorators/map-persistence-errors.decorator.d.ts.map +1 -0
- package/dist/persistence/decorators/map-persistence-errors.decorator.js +89 -0
- package/dist/persistence/decorators/map-persistence-errors.decorator.js.map +1 -0
- package/dist/persistence/decorators/persisted-write.decorator.d.ts +61 -0
- package/dist/persistence/decorators/persisted-write.decorator.d.ts.map +1 -0
- package/dist/persistence/decorators/persisted-write.decorator.js +57 -0
- package/dist/persistence/decorators/persisted-write.decorator.js.map +1 -0
- package/dist/persistence/helpers/cache-barrier.helper.d.ts +70 -0
- package/dist/persistence/helpers/cache-barrier.helper.d.ts.map +1 -0
- package/dist/persistence/helpers/cache-barrier.helper.js +57 -0
- package/dist/persistence/helpers/cache-barrier.helper.js.map +1 -0
- package/dist/persistence/helpers/cache-key.helper.d.ts +73 -0
- package/dist/persistence/helpers/cache-key.helper.d.ts.map +1 -0
- package/dist/persistence/helpers/cache-key.helper.js +132 -0
- package/dist/persistence/helpers/cache-key.helper.js.map +1 -0
- package/dist/persistence/helpers/cache-owner.helper.d.ts +9 -0
- package/dist/persistence/helpers/cache-owner.helper.d.ts.map +1 -0
- package/dist/persistence/helpers/cache-owner.helper.js +22 -0
- package/dist/persistence/helpers/cache-owner.helper.js.map +1 -0
- package/dist/persistence/helpers/cache-snapshot.helper.d.ts +28 -0
- package/dist/persistence/helpers/cache-snapshot.helper.d.ts.map +1 -0
- package/dist/persistence/helpers/cache-snapshot.helper.js +42 -0
- package/dist/persistence/helpers/cache-snapshot.helper.js.map +1 -0
- package/dist/persistence/helpers/cache-version.helper.d.ts +29 -0
- package/dist/persistence/helpers/cache-version.helper.d.ts.map +1 -0
- package/dist/persistence/helpers/cache-version.helper.js +71 -0
- package/dist/persistence/helpers/cache-version.helper.js.map +1 -0
- package/dist/persistence/index.d.ts +23 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +40 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/persistence-dialect.d.ts +35 -0
- package/dist/persistence/persistence-dialect.d.ts.map +1 -0
- package/dist/persistence/persistence-dialect.js +31 -0
- package/dist/persistence/persistence-dialect.js.map +1 -0
- package/dist/persistence/query-repository.abstract.d.ts +76 -0
- package/dist/persistence/query-repository.abstract.d.ts.map +1 -0
- package/dist/persistence/query-repository.abstract.js +67 -0
- package/dist/persistence/query-repository.abstract.js.map +1 -0
- package/dist/types/Method.type.d.ts +2 -0
- package/dist/types/Method.type.d.ts.map +1 -0
- package/dist/types/Method.type.js +4 -0
- package/dist/types/Method.type.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.DEFAULT_BARRIER_TTL_MS = void 0;
|
|
5
|
+
exports.Cache = Cache;
|
|
6
|
+
const cache_port_1 = require("../../application/ports/cache.port");
|
|
7
|
+
const cache_logger_1 = require("../cache/cache-logger");
|
|
8
|
+
const cache_barrier_helper_1 = require("../helpers/cache-barrier.helper");
|
|
9
|
+
const cache_owner_helper_1 = require("../helpers/cache-owner.helper");
|
|
10
|
+
const cache_snapshot_helper_1 = require("../helpers/cache-snapshot.helper");
|
|
11
|
+
const cache_version_helper_1 = require("../helpers/cache-version.helper");
|
|
12
|
+
const defaultLogger = (0, cache_logger_1.consoleCacheLogger)('CacheDecorator');
|
|
13
|
+
function errorMessage(err) {
|
|
14
|
+
return err instanceof Error ? err.message : String(err);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Evicts one key using the strongest coordination the adapter supports.
|
|
18
|
+
*
|
|
19
|
+
* A {@link IVersionedCache} advances the key revision, which already fences any
|
|
20
|
+
* fill that started before this mutation, so no barrier value is stored: leaving
|
|
21
|
+
* one behind would make the next read hydrate a sentinel instead of a snapshot.
|
|
22
|
+
* Unversioned adapters have no revision to compare, so they receive a
|
|
23
|
+
* {@link CacheMutationBarrier} instead. {@link FromCache} bypasses such adapters,
|
|
24
|
+
* and wherever it meets a barrier it treats it as a miss, never as a snapshot.
|
|
25
|
+
*/
|
|
26
|
+
async function evictKey(cache, key, reason, entity, barrierTtl) {
|
|
27
|
+
if ((0, cache_port_1.isVersionedCache)(cache)) {
|
|
28
|
+
await cache.invalidate(key);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const barrier = (0, cache_barrier_helper_1.createCacheMutationBarrier)(reason, entity);
|
|
32
|
+
await cache.set(key, barrier, { ttl: barrierTtl });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Default barrier lifetime: long enough to outlive an in-flight read or write
|
|
36
|
+
* that started before the mutation, short enough that deleted aggregates do not
|
|
37
|
+
* accumulate keys indefinitely.
|
|
38
|
+
*/
|
|
39
|
+
exports.DEFAULT_BARRIER_TTL_MS = 60_000;
|
|
40
|
+
/**
|
|
41
|
+
* Write-through cache decorator for a {@link CommandRepository} `save` method.
|
|
42
|
+
*
|
|
43
|
+
* Provides declarative cache synchronization on mutating database operations:
|
|
44
|
+
* - **Write-through update**: When `save()` resolves with a non-nullish result, the result is cached
|
|
45
|
+
* under the key generated by `setKey` (after invalidating any secondary keys via `invalidateKeys`).
|
|
46
|
+
* - **Cache eviction on delete**: When `save()` resolves with `null` or `undefined` (e.g. entity deletion),
|
|
47
|
+
* all keys returned by `deleteKeys` are evicted from the cache.
|
|
48
|
+
* - **Best-effort safety**: Cache write and eviction operations swallow errors internally so that an
|
|
49
|
+
* already-committed database transaction is never converted into an application error.
|
|
50
|
+
* - **Explicit keys**: Requires at least one of `setKey`, `deleteKeys` or `invalidateKeys`;
|
|
51
|
+
* scoping the key (for example with `cacheKey`) is the key function's job.
|
|
52
|
+
*
|
|
53
|
+
* @example Positional syntax on creation / update
|
|
54
|
+
* ```typescript
|
|
55
|
+
* @Cache<User, UserSnapshot>(
|
|
56
|
+
* (user) => cacheKey(User.aggregateName, { id: user.id }),
|
|
57
|
+
* null,
|
|
58
|
+
* (user) => [cacheKey(User.aggregateName, { email: user.email })],
|
|
59
|
+
* )
|
|
60
|
+
* async save(user: User): Promise<UserSnapshot> { ... }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* Cache maintenance runs only after the wrapped persistence method succeeds and
|
|
64
|
+
* is best-effort: cache failures are logged rather than converting an already
|
|
65
|
+
* durable write into an application failure. Pair this decorator with
|
|
66
|
+
* `@AcknowledgePersisted` and `@MapPersistenceErrors` in the canonical order.
|
|
67
|
+
*
|
|
68
|
+
* @example Options object syntax on eviction / delete
|
|
69
|
+
* ```typescript
|
|
70
|
+
* @Cache<User, null>({
|
|
71
|
+
* deleteKeys: (user) => [
|
|
72
|
+
* cacheKey(User.aggregateName, { id: user.id }),
|
|
73
|
+
* cacheKey(User.aggregateName, { email: user.email }),
|
|
74
|
+
* ],
|
|
75
|
+
* })
|
|
76
|
+
* async save(user: User): Promise<null> { ... }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
function Cache(setKeyOrOptions, deleteKeysFn, invalidateKeysFn) {
|
|
80
|
+
const options = typeof setKeyOrOptions === 'function' || setKeyOrOptions === null
|
|
81
|
+
? {
|
|
82
|
+
setKey: setKeyOrOptions,
|
|
83
|
+
deleteKeys: deleteKeysFn,
|
|
84
|
+
invalidateKeys: invalidateKeysFn,
|
|
85
|
+
}
|
|
86
|
+
: setKeyOrOptions;
|
|
87
|
+
if (!options || typeof options !== 'object') {
|
|
88
|
+
throw new Error('@Cache decorator requires an explicit key derivation function or options object.');
|
|
89
|
+
}
|
|
90
|
+
const resolvedSetKey = options.setKey ?? null;
|
|
91
|
+
const resolvedDeleteKeys = options.deleteKeys ?? null;
|
|
92
|
+
const resolvedInvalidateKeys = options.invalidateKeys ?? null;
|
|
93
|
+
const resolvedTtl = options.ttl;
|
|
94
|
+
const resolvedBarrierTtl = options.barrierTtl ?? exports.DEFAULT_BARRIER_TTL_MS;
|
|
95
|
+
const logger = options.logger ?? defaultLogger;
|
|
96
|
+
const resolvedIsNewer = options.isNewer === null ? undefined : (options.isNewer ?? cache_version_helper_1.isCacheNewer);
|
|
97
|
+
if (!resolvedSetKey && !resolvedDeleteKeys && !resolvedInvalidateKeys) {
|
|
98
|
+
throw new Error('@Cache decorator requires at least one of setKey, deleteKeys, or invalidateKeys to be specified.');
|
|
99
|
+
}
|
|
100
|
+
return (_target, _propertyKey, descriptor) => {
|
|
101
|
+
const original = descriptor.value;
|
|
102
|
+
descriptor.value = async function (entity) {
|
|
103
|
+
// Observed before persistence so a write-through can tell whether anyone
|
|
104
|
+
// mutated the key while this write was in flight. A versioned adapter has
|
|
105
|
+
// no barrier value to outrank a late snapshot, so the revision it hands
|
|
106
|
+
// out here is what keeps a deleted aggregate from being resurrected.
|
|
107
|
+
let observedKey;
|
|
108
|
+
let observedRevision;
|
|
109
|
+
let observedValue;
|
|
110
|
+
if (this.cache && resolvedSetKey && (0, cache_port_1.isVersionedCache)(this.cache)) {
|
|
111
|
+
try {
|
|
112
|
+
observedKey = resolvedSetKey(entity);
|
|
113
|
+
const state = await this.cache.readState(observedKey);
|
|
114
|
+
observedRevision = state.revision;
|
|
115
|
+
observedValue = state.status === 'hit' ? state.value : undefined;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
observedKey = undefined;
|
|
119
|
+
observedRevision = undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const result = await original.call(this, entity);
|
|
123
|
+
if (!this.cache) {
|
|
124
|
+
(0, cache_owner_helper_1.reportMissingCacheProperty)(this, '@Cache', options.logger);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
// Cache maintenance is best-effort: the DB write already succeeded,
|
|
128
|
+
// so a cache failure must not turn a success into an error.
|
|
129
|
+
try {
|
|
130
|
+
const deleting = result === null || result === undefined;
|
|
131
|
+
const keysFn = deleting ? resolvedDeleteKeys : resolvedInvalidateKeys;
|
|
132
|
+
if (keysFn) {
|
|
133
|
+
let keys = [];
|
|
134
|
+
try {
|
|
135
|
+
keys = keysFn(entity) ?? [];
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const keyOption = deleting ? 'deleteKeys' : 'invalidateKeys';
|
|
139
|
+
const operation = deleting ? 'cache eviction' : 'cache maintenance';
|
|
140
|
+
(0, cache_logger_1.safeWarn)(logger, `Failed resolving ${keyOption} during ${operation}: ${errorMessage(err)}`);
|
|
141
|
+
}
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
try {
|
|
144
|
+
await evictKey(this.cache, key, deleting ? 'deleted' : 'invalidated', entity, resolvedBarrierTtl);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const action = deleting ? 'evicting deletion' : 'invalidating';
|
|
148
|
+
(0, cache_logger_1.safeWarn)(logger, `Failed ${action} key "${key}": ${errorMessage(err)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (deleting)
|
|
153
|
+
return result;
|
|
154
|
+
if (resolvedSetKey) {
|
|
155
|
+
let setKey;
|
|
156
|
+
try {
|
|
157
|
+
setKey = resolvedSetKey(entity);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
(0, cache_logger_1.safeWarn)(logger, `Failed resolving setKey during cache maintenance: ${errorMessage(err)}`);
|
|
161
|
+
}
|
|
162
|
+
if (setKey) {
|
|
163
|
+
try {
|
|
164
|
+
const snapshot = (0, cache_snapshot_helper_1.toCacheSnapshot)(result);
|
|
165
|
+
if ((0, cache_port_1.isVersionedCache)(this.cache)) {
|
|
166
|
+
if (observedRevision !== undefined && setKey === observedKey) {
|
|
167
|
+
// Skip the write-through when the snapshot observed before
|
|
168
|
+
// persistence is already newer; otherwise commit it only if
|
|
169
|
+
// the revision still matches, so a concurrent mutation wins.
|
|
170
|
+
if (!resolvedIsNewer?.(observedValue, snapshot)) {
|
|
171
|
+
await this.cache.tryFill(setKey, observedRevision, snapshot, { ttl: resolvedTtl });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// No revision was observed for this key before the write, so
|
|
176
|
+
// a fill cannot be fenced; drop the pre-write value instead.
|
|
177
|
+
await this.cache.invalidate(setKey);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
await this.cache.set(setKey, snapshot, {
|
|
182
|
+
ttl: resolvedTtl,
|
|
183
|
+
isNewer: resolvedIsNewer,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
(0, cache_logger_1.safeWarn)(logger, `Failed setting cache key "${setKey}": ${errorMessage(err)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
(0, cache_logger_1.safeWarn)(logger, `Unexpected error during cache maintenance: ${errorMessage(err)}`);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=cache.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.decorator.js","sourceRoot":"","sources":["../../../persistence/decorators/cache.decorator.ts"],"names":[],"mappings":";AAAA,sEAAsE;;;AAmJtE,sBAuKC;AAxTD,mEAG4C;AAE5C,wDAAqE;AAErE,0EAGyC;AACzC,sEAA2E;AAC3E,4EAAmE;AACnE,0EAA+D;AAE/D,MAAM,aAAa,GAAG,IAAA,iCAAkB,EAAC,gBAAgB,CAAC,CAAC;AAE3D,SAAS,YAAY,CAAC,GAAY;IAChC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,QAAQ,CACrB,KAAsB,EACtB,GAAW,EACX,MAA0B,EAC1B,MAAe,EACf,UAAkB;IAElB,IAAI,IAAA,6BAAgB,EAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,iDAA0B,EAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3D,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;AACrD,CAAC;AAsDD;;;;GAIG;AACU,QAAA,sBAAsB,GAAG,MAAM,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,SAAgB,KAAK,CACnB,eAA6E,EAC7E,YAAqD,EACrD,gBAAyD;IAEzD,MAAM,OAAO,GACX,OAAO,eAAe,KAAK,UAAU,IAAI,eAAe,KAAK,IAAI;QAC/D,CAAC,CAAC;YACE,MAAM,EAAE,eAAe;YACvB,UAAU,EAAE,YAAY;YACxB,cAAc,EAAE,gBAAgB;SACjC;QACH,CAAC,CAAC,eAAe,CAAC;IACtB,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,kFAAkF,CACnF,CAAC;IACJ,CAAC;IAED,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;IAC9C,MAAM,kBAAkB,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC;IACtD,MAAM,sBAAsB,GAAG,OAAO,CAAC,cAAc,IAAI,IAAI,CAAC;IAC9D,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAChC,MAAM,kBAAkB,GAAG,OAAO,CAAC,UAAU,IAAI,8BAAsB,CAAC;IACxE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC;IAC/C,MAAM,eAAe,GACnB,OAAO,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,mCAAY,CAAC,CAAC;IAE3E,IAAI,CAAC,cAAc,IAAI,CAAC,kBAAkB,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CACb,kGAAkG,CACnG,CAAC;IACJ,CAAC;IAED,OAAO,CACL,OAAe,EACf,YAA6B,EAC7B,UAA8B,EAC9B,EAAE;QACF,MAAM,QAAQ,GAAG,UAAU,CAAC,KAEA,CAAC;QAE7B,UAAU,CAAC,KAAK,GAAG,KAAK,WAEtB,MAAe;YAEf,yEAAyE;YACzE,0EAA0E;YAC1E,wEAAwE;YACxE,qEAAqE;YACrE,IAAI,WAA+B,CAAC;YACpC,IAAI,gBAAoC,CAAC;YACzC,IAAI,aAAsB,CAAC;YAE3B,IAAI,IAAI,CAAC,KAAK,IAAI,cAAc,IAAI,IAAA,6BAAgB,EAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACjE,IAAI,CAAC;oBACH,WAAW,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;oBACrC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;oBACtD,gBAAgB,GAAG,KAAK,CAAC,QAAQ,CAAC;oBAClC,aAAa,GAAG,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;gBACnE,CAAC;gBAAC,MAAM,CAAC;oBACP,WAAW,GAAG,SAAS,CAAC;oBACxB,gBAAgB,GAAG,SAAS,CAAC;gBAC/B,CAAC;YACH,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAEjD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;gBAChB,IAAA,+CAA0B,EAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;gBAC3D,OAAO,MAAM,CAAC;YAChB,CAAC;YAED,oEAAoE;YACpE,4DAA4D;YAC5D,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,CAAC;gBACzD,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,sBAAsB,CAAC;gBACtE,IAAI,MAAM,EAAE,CAAC;oBACX,IAAI,IAAI,GAAa,EAAE,CAAC;oBACxB,IAAI,CAAC;wBACH,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;oBAC9B,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,gBAAgB,CAAC;wBAC7D,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,CAAC;wBACpE,IAAA,uBAAQ,EACN,MAAM,EACN,oBAAoB,SAAS,WAAW,SAAS,KAAK,YAAY,CAAC,GAAG,CAAC,EAAE,CAC1E,CAAC;oBACJ,CAAC;oBACD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;wBACvB,IAAI,CAAC;4BACH,MAAM,QAAQ,CACZ,IAAI,CAAC,KAAK,EACV,GAAG,EACH,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,EACpC,MAAM,EACN,kBAAkB,CACnB,CAAC;wBACJ,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,cAAc,CAAC;4BAC/D,IAAA,uBAAQ,EACN,MAAM,EACN,UAAU,MAAM,SAAS,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,EAAE,CACtD,CAAC;wBACJ,CAAC;oBACH,CAAC;gBACH,CAAC;gBACD,IAAI,QAAQ;oBAAE,OAAO,MAAM,CAAC;gBAE5B,IAAI,cAAc,EAAE,CAAC;oBACnB,IAAI,MAA0B,CAAC;oBAC/B,IAAI,CAAC;wBACH,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;oBAClC,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,IAAA,uBAAQ,EACN,MAAM,EACN,qDAAqD,YAAY,CAAC,GAAG,CAAC,EAAE,CACzE,CAAC;oBACJ,CAAC;oBACD,IAAI,MAAM,EAAE,CAAC;wBACX,IAAI,CAAC;4BACH,MAAM,QAAQ,GAAG,IAAA,uCAAe,EAAC,MAAM,CAAC,CAAC;4BAEzC,IAAI,IAAA,6BAAgB,EAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gCACjC,IAAI,gBAAgB,KAAK,SAAS,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;oCAC7D,2DAA2D;oCAC3D,4DAA4D;oCAC5D,6DAA6D;oCAC7D,IAAI,CAAC,eAAe,EAAE,CAAC,aAAa,EAAE,QAAQ,CAAC,EAAE,CAAC;wCAChD,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CACtB,MAAM,EACN,gBAAgB,EAChB,QAAiB,EACjB,EAAE,GAAG,EAAE,WAAW,EAAE,CACrB,CAAC;oCACJ,CAAC;gCACH,CAAC;qCAAM,CAAC;oCACN,6DAA6D;oCAC7D,6DAA6D;oCAC7D,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;gCACtC,CAAC;4BACH,CAAC;iCAAM,CAAC;gCACN,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE;oCACrC,GAAG,EAAE,WAAW;oCAChB,OAAO,EAAE,eAAe;iCACzB,CAAC,CAAC;4BACL,CAAC;wBACH,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,IAAA,uBAAQ,EACN,MAAM,EACN,6BAA6B,MAAM,MAAM,YAAY,CAAC,GAAG,CAAC,EAAE,CAC7D,CAAC;wBACJ,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAA,uBAAQ,EACN,MAAM,EACN,8CAA8C,YAAY,CAAC,GAAG,CAAC,EAAE,CAClE,CAAC;YACJ,CAAC;YAED,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { IQueryOptions } from '../../application/query.options';
|
|
2
|
+
import type { ICacheLogger } from '../cache/cache-logger';
|
|
3
|
+
import { isCacheNewer } from '../helpers/cache-version.helper';
|
|
4
|
+
export { isCacheNewer };
|
|
5
|
+
/**
|
|
6
|
+
* Options for configuring read-through caching and rehydration via {@link FromCache}.
|
|
7
|
+
*/
|
|
8
|
+
export interface FromCacheOptions<TQuery = unknown, TResult = unknown> {
|
|
9
|
+
/**
|
|
10
|
+
* Function deriving the cache key from query options.
|
|
11
|
+
* When it returns `null`, caching is skipped for that call.
|
|
12
|
+
*/
|
|
13
|
+
keyFn?: ((query: TQuery) => string | null) | null;
|
|
14
|
+
/**
|
|
15
|
+
* Function to rehydrate a raw cached snapshot back into a domain entity.
|
|
16
|
+
* When omitted, the repository's {@link QueryRepositoryHydration} applies;
|
|
17
|
+
* `null` opts this method out of it.
|
|
18
|
+
*/
|
|
19
|
+
hydrateFn?: ((cached: unknown) => TResult) | null;
|
|
20
|
+
/**
|
|
21
|
+
* Optional serializer function to convert a domain entity or complex result into
|
|
22
|
+
* a pure, detached snapshot for storage in cache.
|
|
23
|
+
* If omitted, the repository's {@link QueryRepositoryHydration} serializer
|
|
24
|
+
* applies, and otherwise {@link toCacheSnapshot} extracts a snapshot via
|
|
25
|
+
* `result.toJSON()` or deep JSON cloning. `null` skips the repository default.
|
|
26
|
+
*/
|
|
27
|
+
serializeFn?: ((result: TResult) => unknown) | null;
|
|
28
|
+
/**
|
|
29
|
+
* Requires `hydrateFn` at decoration time. Every hit is rehydrated whenever a
|
|
30
|
+
* hydrator applies, so this flag does not change the result.
|
|
31
|
+
*/
|
|
32
|
+
alwaysHydrate?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Optional time-to-live in milliseconds for cached entries.
|
|
35
|
+
*/
|
|
36
|
+
ttl?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Optional version comparator for stale-read prevention during concurrent cache population.
|
|
39
|
+
* Defaults to {@link isCacheNewer}.
|
|
40
|
+
*/
|
|
41
|
+
isNewer?: (cached: unknown, incoming: unknown) => boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Receives read-through warnings (unversioned adapter, uncacheable result, missing
|
|
44
|
+
* `cache` property). Defaults to `console.warn` with a `[FromCacheDecorator]`
|
|
45
|
+
* prefix. See {@link ICacheLogger}.
|
|
46
|
+
*/
|
|
47
|
+
logger?: ICacheLogger;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read-through cache decorator for a {@link QueryRepository} `find` method.
|
|
51
|
+
*
|
|
52
|
+
* Provides declarative read-through caching with revision-fenced coordination:
|
|
53
|
+
* - **Key derivation**: Generates a cache key via `keyFn`. If `keyFn` returns `null`, caching is skipped.
|
|
54
|
+
* - **Snapshot storage contract**: Stores strictly serializable snapshots in the cache, never live domain aggregates.
|
|
55
|
+
* - **Consistent result shape**: When a method or repository `hydrateFn` applies, every hit is rehydrated, so hits
|
|
56
|
+
* and misses return the same type. Without a hydrator only plain-data results are cached; any other result
|
|
57
|
+
* (a class instance, or a result reshaped by `serializeFn`) is returned uncached and reported once.
|
|
58
|
+
* - **Revision-fenced consistency**: When coordinated via {@link IVersionedCache}, cache fill attempts verify the
|
|
59
|
+
* observed revision token atomically (`tryFill`). A fill started before an observed invalidation cannot
|
|
60
|
+
* repopulate the key afterwards.
|
|
61
|
+
* - **Bounded retry**: Contended fills inspect current cache state and boundedly retry authoritative reads before
|
|
62
|
+
* falling back to un-cached database results.
|
|
63
|
+
*/
|
|
64
|
+
export declare function FromCache<TQuery extends IQueryOptions = IQueryOptions, TResult = unknown>(keyFnOrOptions: ((query: TQuery) => string | null) | FromCacheOptions<TQuery, TResult>, hydrateFnOrOptions?: ((cached: unknown) => TResult) | FromCacheOptions<TQuery, TResult>, extraOptions?: FromCacheOptions<TQuery, TResult>): MethodDecorator;
|
|
65
|
+
//# sourceMappingURL=from-cache.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"from-cache.decorator.d.ts","sourceRoot":"","sources":["../../../persistence/decorators/from-cache.decorator.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAK1D,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAM/D,OAAO,EAAE,YAAY,EAAE,CAAC;AAsBxB;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,GAAG,OAAO;IACnE;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAElD;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IAElD;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IAEpD;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;IAE1D;;;;OAIG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AA8BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CACvB,MAAM,SAAS,aAAa,GAAG,aAAa,EAC5C,OAAO,GAAG,OAAO,EAEjB,cAAc,EACV,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC,GAClC,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,kBAAkB,CAAC,EACf,CAAC,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC,GAC9B,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,YAAY,CAAC,EAAE,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/C,eAAe,CAyIjB"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.isCacheNewer = void 0;
|
|
5
|
+
exports.FromCache = FromCache;
|
|
6
|
+
const cache_port_1 = require("../../application/ports/cache.port");
|
|
7
|
+
const cache_logger_1 = require("../cache/cache-logger");
|
|
8
|
+
const cache_barrier_helper_1 = require("../helpers/cache-barrier.helper");
|
|
9
|
+
const cache_owner_helper_1 = require("../helpers/cache-owner.helper");
|
|
10
|
+
const cache_snapshot_helper_1 = require("../helpers/cache-snapshot.helper");
|
|
11
|
+
const cache_version_helper_1 = require("../helpers/cache-version.helper");
|
|
12
|
+
Object.defineProperty(exports, "isCacheNewer", { enumerable: true, get: function () { return cache_version_helper_1.isCacheNewer; } });
|
|
13
|
+
const defaultLogger = (0, cache_logger_1.consoleCacheLogger)('FromCacheDecorator');
|
|
14
|
+
const warnedUnversionedAdapters = new WeakSet();
|
|
15
|
+
/**
|
|
16
|
+
* Reports an unversioned adapter once per instance: a silent loss of caching is
|
|
17
|
+
* harder to diagnose than the race the bypass avoids.
|
|
18
|
+
*/
|
|
19
|
+
function warnUnversionedAdapterOnce(cache, logger) {
|
|
20
|
+
if (warnedUnversionedAdapters.has(cache))
|
|
21
|
+
return;
|
|
22
|
+
warnedUnversionedAdapters.add(cache);
|
|
23
|
+
(0, cache_logger_1.safeWarn)(logger, `${cache.constructor?.name ?? 'Cache adapter'} implements only get/set/delete, ` +
|
|
24
|
+
'so @FromCache read-through is disabled for repositories using it. ' +
|
|
25
|
+
'Implement IVersionedCache (readState/invalidate/tryFill) to enable ' +
|
|
26
|
+
'revision-fenced caching.');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True for values whose cached JSON form equals the value itself: primitives,
|
|
30
|
+
* arrays and plain objects of such values. Class instances and `Date` are not.
|
|
31
|
+
*/
|
|
32
|
+
function isPlainData(value, ancestors = new WeakSet()) {
|
|
33
|
+
if (value === null || typeof value !== 'object') {
|
|
34
|
+
return typeof value !== 'function' && typeof value !== 'bigint';
|
|
35
|
+
}
|
|
36
|
+
const prototype = Object.getPrototypeOf(value);
|
|
37
|
+
if (ancestors.has(value) ||
|
|
38
|
+
(!Array.isArray(value) &&
|
|
39
|
+
prototype !== Object.prototype &&
|
|
40
|
+
prototype !== null)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
ancestors.add(value);
|
|
44
|
+
const plain = Object.values(value).every((item) => isPlainData(item, ancestors));
|
|
45
|
+
ancestors.delete(value);
|
|
46
|
+
return plain;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read-through cache decorator for a {@link QueryRepository} `find` method.
|
|
50
|
+
*
|
|
51
|
+
* Provides declarative read-through caching with revision-fenced coordination:
|
|
52
|
+
* - **Key derivation**: Generates a cache key via `keyFn`. If `keyFn` returns `null`, caching is skipped.
|
|
53
|
+
* - **Snapshot storage contract**: Stores strictly serializable snapshots in the cache, never live domain aggregates.
|
|
54
|
+
* - **Consistent result shape**: When a method or repository `hydrateFn` applies, every hit is rehydrated, so hits
|
|
55
|
+
* and misses return the same type. Without a hydrator only plain-data results are cached; any other result
|
|
56
|
+
* (a class instance, or a result reshaped by `serializeFn`) is returned uncached and reported once.
|
|
57
|
+
* - **Revision-fenced consistency**: When coordinated via {@link IVersionedCache}, cache fill attempts verify the
|
|
58
|
+
* observed revision token atomically (`tryFill`). A fill started before an observed invalidation cannot
|
|
59
|
+
* repopulate the key afterwards.
|
|
60
|
+
* - **Bounded retry**: Contended fills inspect current cache state and boundedly retry authoritative reads before
|
|
61
|
+
* falling back to un-cached database results.
|
|
62
|
+
*/
|
|
63
|
+
function FromCache(keyFnOrOptions, hydrateFnOrOptions, extraOptions) {
|
|
64
|
+
const positionalKey = typeof keyFnOrOptions === 'function';
|
|
65
|
+
const positionalHydrator = positionalKey && typeof hydrateFnOrOptions === 'function';
|
|
66
|
+
let resolvedOptions = positionalKey ? hydrateFnOrOptions : keyFnOrOptions;
|
|
67
|
+
if (typeof resolvedOptions === 'function')
|
|
68
|
+
resolvedOptions = extraOptions;
|
|
69
|
+
const resolvedKeyFn = positionalKey ? keyFnOrOptions : keyFnOrOptions.keyFn;
|
|
70
|
+
const resolvedHydrateFn = positionalHydrator
|
|
71
|
+
? hydrateFnOrOptions
|
|
72
|
+
: (resolvedOptions?.hydrateFn ?? undefined);
|
|
73
|
+
const declaresHydrateFn = positionalHydrator ||
|
|
74
|
+
(resolvedOptions !== undefined && 'hydrateFn' in resolvedOptions);
|
|
75
|
+
const declaresSerializeFn = resolvedOptions !== undefined && 'serializeFn' in resolvedOptions;
|
|
76
|
+
if (resolvedOptions?.alwaysHydrate && !resolvedHydrateFn) {
|
|
77
|
+
throw new TypeError('FromCache: alwaysHydrate requires a hydrateFn');
|
|
78
|
+
}
|
|
79
|
+
const logger = resolvedOptions?.logger ?? defaultLogger;
|
|
80
|
+
const MAX_FILL_RETRIES = 2;
|
|
81
|
+
let reportedUncacheable = false;
|
|
82
|
+
return (_target, _propertyKey, descriptor) => {
|
|
83
|
+
const original = descriptor.value;
|
|
84
|
+
descriptor.value = async function (query) {
|
|
85
|
+
if (!this.cache) {
|
|
86
|
+
(0, cache_owner_helper_1.reportMissingCacheProperty)(this, '@FromCache', resolvedOptions?.logger);
|
|
87
|
+
return original.call(this, query);
|
|
88
|
+
}
|
|
89
|
+
if (query.refresh)
|
|
90
|
+
return original.call(this, query);
|
|
91
|
+
if (!resolvedKeyFn)
|
|
92
|
+
throw new TypeError('FromCache requires a keyFn');
|
|
93
|
+
const key = resolvedKeyFn(query);
|
|
94
|
+
if (key === null) {
|
|
95
|
+
return original.call(this, query);
|
|
96
|
+
}
|
|
97
|
+
// Method-level options win; otherwise the repository policy applies,
|
|
98
|
+
// and a repository hydrator rehydrates every hit.
|
|
99
|
+
const repositoryHydration = this.hydration;
|
|
100
|
+
const hydrateFn = declaresHydrateFn
|
|
101
|
+
? resolvedHydrateFn
|
|
102
|
+
: repositoryHydration?.hydrateFn;
|
|
103
|
+
const serializeFn = declaresSerializeFn
|
|
104
|
+
? resolvedOptions?.serializeFn
|
|
105
|
+
: repositoryHydration?.serializeFn;
|
|
106
|
+
const hydrateCached = (cachedValue) => hydrateFn ? hydrateFn(cachedValue) : cachedValue;
|
|
107
|
+
// A revision-fenced key can still hold a barrier written by an
|
|
108
|
+
// unversioned writer against the same store; it is a sentinel, never a
|
|
109
|
+
// snapshot, so it counts as a miss rather than something to hydrate. The
|
|
110
|
+
// same holds for `null`: this decorator never fills one, so a stored null
|
|
111
|
+
// is a leftover from another writer, not a cached absence.
|
|
112
|
+
const isUsableSnapshot = (state) => state.status === 'hit' &&
|
|
113
|
+
state.value !== undefined &&
|
|
114
|
+
state.value !== null &&
|
|
115
|
+
!(0, cache_barrier_helper_1.isCacheMutationBarrier)(state.value);
|
|
116
|
+
// 1. Versioned coordination path (capable adapters)
|
|
117
|
+
if ((0, cache_port_1.isVersionedCache)(this.cache)) {
|
|
118
|
+
let observedState = await this.cache.readState(key);
|
|
119
|
+
if (isUsableSnapshot(observedState)) {
|
|
120
|
+
return hydrateCached(observedState.value);
|
|
121
|
+
}
|
|
122
|
+
for (let attempt = 0;; attempt++) {
|
|
123
|
+
const result = await original.call(this, query);
|
|
124
|
+
if (result === null || result === undefined) {
|
|
125
|
+
const postState = await this.cache.readState(key);
|
|
126
|
+
if (isUsableSnapshot(postState)) {
|
|
127
|
+
return hydrateCached(postState.value);
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
// A hit without a hydrator returns the cached data as is, so only a
|
|
132
|
+
// result that already is that data may be cached.
|
|
133
|
+
if (!hydrateFn && (serializeFn || !isPlainData(result))) {
|
|
134
|
+
if (!reportedUncacheable) {
|
|
135
|
+
reportedUncacheable = true;
|
|
136
|
+
(0, cache_logger_1.safeWarn)(logger, `${this.constructor.name} caches a result that a hit could not reproduce without a hydrateFn; ` +
|
|
137
|
+
'the result is returned uncached. Configure a hydrateFn or return plain data.');
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
const snapshot = (0, cache_snapshot_helper_1.toCacheSnapshot)(result, serializeFn);
|
|
142
|
+
const committed = await this.cache.tryFill(key, observedState.revision, snapshot, { ttl: resolvedOptions?.ttl });
|
|
143
|
+
if (committed) {
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
const currentState = await this.cache.readState(key);
|
|
147
|
+
if (isUsableSnapshot(currentState)) {
|
|
148
|
+
const newerCheck = resolvedOptions?.isNewer ?? cache_version_helper_1.isCacheNewer;
|
|
149
|
+
if (newerCheck(currentState.value, snapshot)) {
|
|
150
|
+
return hydrateCached(currentState.value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (attempt === MAX_FILL_RETRIES)
|
|
154
|
+
return result;
|
|
155
|
+
observedState = currentState;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 2. Unversioned adapter: with no revision to fence a fill against a
|
|
159
|
+
// concurrent invalidation, the cache is bypassed for both reads and fills.
|
|
160
|
+
warnUnversionedAdapterOnce(this.cache, logger);
|
|
161
|
+
return original.call(this, query);
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=from-cache.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"from-cache.decorator.js","sourceRoot":"","sources":["../../../persistence/decorators/from-cache.decorator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type IPersistenceDialect } from '../persistence-dialect';
|
|
2
|
+
/**
|
|
3
|
+
* Domain errors per violated unique constraint, keyed by the entity property the
|
|
4
|
+
* constraint covers. A multi-column constraint has no single property: key it by
|
|
5
|
+
* its declared name, passed as `TConstraint` from a constant declared beside the
|
|
6
|
+
* ORM mapping. No database name or column string appears in a repository.
|
|
7
|
+
*/
|
|
8
|
+
export type UniqueErrors<TEntity, TConstraint extends string = never> = {
|
|
9
|
+
readonly [K in Extract<keyof TEntity, string> | TConstraint]?: (entity: TEntity) => Error;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Method decorator that intercepts persistence failures from the wrapped method and
|
|
13
|
+
* translates identifiable database unique constraint violations into domain exceptions.
|
|
14
|
+
*
|
|
15
|
+
* ### Error Matching Mechanics
|
|
16
|
+
* - **Unique violations**: the persistence dialect ({@link IPersistenceDialect}) names the
|
|
17
|
+
* violated constraint by the entity property it covers; `unique[key]` builds the
|
|
18
|
+
* domain error. The dialect comes from `options.dialect`, else from
|
|
19
|
+
* {@link setPersistenceDialect}. A method that declares `unique` with no dialect
|
|
20
|
+
* available throws a `TypeError` before it runs, so a violation never escapes as a
|
|
21
|
+
* raw driver error.
|
|
22
|
+
* - **Residual translation**: Any error not matching a configured constraint is passed to the
|
|
23
|
+
* optional `otherwise` translator, which is the declarative place to convert driver/network
|
|
24
|
+
* failures into the neutral {@link TransientOperationError} retry signal.
|
|
25
|
+
* - **Passthrough**: Without `otherwise` — or when `otherwise` returns the error unchanged — the
|
|
26
|
+
* error is rethrown with its original identity, class, and stack trace completely preserved.
|
|
27
|
+
*
|
|
28
|
+
* Use `otherwise` for application-level translation of unmatched driver or
|
|
29
|
+
* network failures. Keep persistence translation in this decorator so the
|
|
30
|
+
* lifecycle order relative to `@AcknowledgePersisted` and `@Cache` remains
|
|
31
|
+
* explicit in the decorator stack.
|
|
32
|
+
*
|
|
33
|
+
* ### Canonical Decorator Ordering
|
|
34
|
+
* Always stack decorators in this outermost-to-innermost order:
|
|
35
|
+
* 1. `@Cache(...)` — Best-effort cache synchronization after acknowledgment.
|
|
36
|
+
* 2. `@AcknowledgePersisted(...)` — Acknowledges version baseline on successful write.
|
|
37
|
+
* 3. `@MapPersistenceErrors(...)` — Translates low-level DB driver errors to domain exceptions.
|
|
38
|
+
*
|
|
39
|
+
* @param options The `entity` extractor, the `unique` errors, an optional `dialect`
|
|
40
|
+
* overriding the registered one (for a repository on another store), and an
|
|
41
|
+
* optional `otherwise` translator.
|
|
42
|
+
*
|
|
43
|
+
* @example Mapping unique constraints in a create repository
|
|
44
|
+
* ```typescript
|
|
45
|
+
* @Injectable()
|
|
46
|
+
* export class CreateUserCommandRepository extends CommandRepository<User, UserSnapshot> {
|
|
47
|
+
* @Cache<User, UserSnapshot>((u) => `users:${u.id}`, null, (u) => [`users:email:${u.email}`])
|
|
48
|
+
* @AcknowledgePersisted<[User]>({ entity: ([user]) => user })
|
|
49
|
+
* @MapPersistenceErrors<[User], User>({
|
|
50
|
+
* entity: ([user]) => user,
|
|
51
|
+
* unique: { email: (user) => new UniqueEmailException(user) },
|
|
52
|
+
* })
|
|
53
|
+
* async save(user: User): Promise<UserSnapshot> {
|
|
54
|
+
* const created = this.store.em.create(User, user);
|
|
55
|
+
* this.store.em.persist(created);
|
|
56
|
+
* await this.store.em.flush();
|
|
57
|
+
* return created.toJSON();
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function MapPersistenceErrors<TArgs extends unknown[], TEntity, TConstraint extends string = never, TThis = unknown>(options: {
|
|
63
|
+
entity: (args: TArgs) => TEntity;
|
|
64
|
+
unique?: UniqueErrors<TEntity, TConstraint>;
|
|
65
|
+
dialect?: IPersistenceDialect;
|
|
66
|
+
/**
|
|
67
|
+
* Optional translator applied to any error that did not match a unique constraint
|
|
68
|
+
* mapping. Return the error unchanged to preserve its identity, or return a
|
|
69
|
+
* replacement to express it in application terms.
|
|
70
|
+
*
|
|
71
|
+
* The canonical use is transient-failure classification with
|
|
72
|
+
* {@link mapPersistenceError}, so that retry policies consume
|
|
73
|
+
* `TransientOperationError` rather than driver codes:
|
|
74
|
+
*
|
|
75
|
+
* ```typescript
|
|
76
|
+
* otherwise: (error, user) => mapPersistenceError(error, `deleting User ${user.id}`),
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* Deliberate domain errors thrown inside the method (`ConcurrencyConflictError`,
|
|
80
|
+
* `EntityNotFoundException`) also pass through this hook. `mapPersistenceError`
|
|
81
|
+
* returns non-transient errors unchanged, so no explicit re-throw guard is needed.
|
|
82
|
+
*
|
|
83
|
+
* Written as a method, it runs with `this` bound to the repository instance
|
|
84
|
+
* (typed by `TThis`), for a message that names instance state.
|
|
85
|
+
*/
|
|
86
|
+
otherwise?: (this: TThis, error: unknown, entity: TEntity) => unknown;
|
|
87
|
+
}): <TResult>(_target: object, _key: string | symbol, descriptor: TypedPropertyDescriptor<(...args: TArgs) => Promise<TResult>>) => void;
|
|
88
|
+
//# sourceMappingURL=map-persistence-errors.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-persistence-errors.decorator.d.ts","sourceRoot":"","sources":["../../../persistence/decorators/map-persistence-errors.decorator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,wBAAwB,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,MAAM,YAAY,CAAC,OAAO,EAAE,WAAW,SAAS,MAAM,GAAG,KAAK,IAAI;IACtE,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,MAAM,OAAO,EAAE,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,EAAE,CAC7D,MAAM,EAAE,OAAO,KACZ,KAAK;CACX,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,SAAS,OAAO,EAAE,EACvB,OAAO,EACP,WAAW,SAAS,MAAM,GAAG,KAAK,EAClC,KAAK,GAAG,OAAO,EACf,OAAO,EAAE;IACT,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC;IACjC,MAAM,CAAC,EAAE,YAAY,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CACvE,IACS,OAAO,EACb,SAAS,MAAM,EACf,MAAM,MAAM,GAAG,MAAM,EACrB,YAAY,uBAAuB,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,KACxE,IAAI,CA+BR"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MapPersistenceErrors = MapPersistenceErrors;
|
|
4
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
5
|
+
const persistence_dialect_1 = require("../persistence-dialect");
|
|
6
|
+
/**
|
|
7
|
+
* Method decorator that intercepts persistence failures from the wrapped method and
|
|
8
|
+
* translates identifiable database unique constraint violations into domain exceptions.
|
|
9
|
+
*
|
|
10
|
+
* ### Error Matching Mechanics
|
|
11
|
+
* - **Unique violations**: the persistence dialect ({@link IPersistenceDialect}) names the
|
|
12
|
+
* violated constraint by the entity property it covers; `unique[key]` builds the
|
|
13
|
+
* domain error. The dialect comes from `options.dialect`, else from
|
|
14
|
+
* {@link setPersistenceDialect}. A method that declares `unique` with no dialect
|
|
15
|
+
* available throws a `TypeError` before it runs, so a violation never escapes as a
|
|
16
|
+
* raw driver error.
|
|
17
|
+
* - **Residual translation**: Any error not matching a configured constraint is passed to the
|
|
18
|
+
* optional `otherwise` translator, which is the declarative place to convert driver/network
|
|
19
|
+
* failures into the neutral {@link TransientOperationError} retry signal.
|
|
20
|
+
* - **Passthrough**: Without `otherwise` — or when `otherwise` returns the error unchanged — the
|
|
21
|
+
* error is rethrown with its original identity, class, and stack trace completely preserved.
|
|
22
|
+
*
|
|
23
|
+
* Use `otherwise` for application-level translation of unmatched driver or
|
|
24
|
+
* network failures. Keep persistence translation in this decorator so the
|
|
25
|
+
* lifecycle order relative to `@AcknowledgePersisted` and `@Cache` remains
|
|
26
|
+
* explicit in the decorator stack.
|
|
27
|
+
*
|
|
28
|
+
* ### Canonical Decorator Ordering
|
|
29
|
+
* Always stack decorators in this outermost-to-innermost order:
|
|
30
|
+
* 1. `@Cache(...)` — Best-effort cache synchronization after acknowledgment.
|
|
31
|
+
* 2. `@AcknowledgePersisted(...)` — Acknowledges version baseline on successful write.
|
|
32
|
+
* 3. `@MapPersistenceErrors(...)` — Translates low-level DB driver errors to domain exceptions.
|
|
33
|
+
*
|
|
34
|
+
* @param options The `entity` extractor, the `unique` errors, an optional `dialect`
|
|
35
|
+
* overriding the registered one (for a repository on another store), and an
|
|
36
|
+
* optional `otherwise` translator.
|
|
37
|
+
*
|
|
38
|
+
* @example Mapping unique constraints in a create repository
|
|
39
|
+
* ```typescript
|
|
40
|
+
* @Injectable()
|
|
41
|
+
* export class CreateUserCommandRepository extends CommandRepository<User, UserSnapshot> {
|
|
42
|
+
* @Cache<User, UserSnapshot>((u) => `users:${u.id}`, null, (u) => [`users:email:${u.email}`])
|
|
43
|
+
* @AcknowledgePersisted<[User]>({ entity: ([user]) => user })
|
|
44
|
+
* @MapPersistenceErrors<[User], User>({
|
|
45
|
+
* entity: ([user]) => user,
|
|
46
|
+
* unique: { email: (user) => new UniqueEmailException(user) },
|
|
47
|
+
* })
|
|
48
|
+
* async save(user: User): Promise<UserSnapshot> {
|
|
49
|
+
* const created = this.store.em.create(User, user);
|
|
50
|
+
* this.store.em.persist(created);
|
|
51
|
+
* await this.store.em.flush();
|
|
52
|
+
* return created.toJSON();
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function MapPersistenceErrors(options) {
|
|
58
|
+
return (_target, _key, descriptor) => {
|
|
59
|
+
const original = descriptor.value;
|
|
60
|
+
if (typeof original !== 'function')
|
|
61
|
+
throw new TypeError('Persistence decorators require a method.');
|
|
62
|
+
const unique = options.unique ?? {};
|
|
63
|
+
const hasUnique = Object.keys(unique).length > 0;
|
|
64
|
+
descriptor.value = async function (...args) {
|
|
65
|
+
const dialect = options.dialect ?? (0, persistence_dialect_1.persistenceDialect)();
|
|
66
|
+
if (hasUnique && !dialect) {
|
|
67
|
+
throw new TypeError('Unique-constraint mapping needs a persistence dialect: call setPersistenceDialect() or pass { dialect }.');
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return await original.apply(this, args);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const entity = options.entity(args);
|
|
74
|
+
const key = hasUnique
|
|
75
|
+
? dialect?.uniqueViolation(error, entity)
|
|
76
|
+
: undefined;
|
|
77
|
+
const toError = key === undefined
|
|
78
|
+
? undefined
|
|
79
|
+
: unique[key];
|
|
80
|
+
if (toError)
|
|
81
|
+
throw toError(entity);
|
|
82
|
+
if (options.otherwise)
|
|
83
|
+
throw options.otherwise.call(this, error, entity);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=map-persistence-errors.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-persistence-errors.decorator.js","sourceRoot":"","sources":["../../../persistence/decorators/map-persistence-errors.decorator.ts"],"names":[],"mappings":";;AAqEA,oDAkEC;AAvID,sEAAsE;AACtE,gEAGgC;AAchC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,SAAgB,oBAAoB,CAKlC,OAyBD;IACC,OAAO,CACL,OAAe,EACf,IAAqB,EACrB,UAAyE,EACnE,EAAE;QACR,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC;QAClC,IAAI,OAAO,QAAQ,KAAK,UAAU;YAChC,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;QACpC,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QACjD,UAAU,CAAC,KAAK,GAAG,KAAK,WAAW,GAAG,IAAW;YAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,IAAA,wCAAkB,GAAE,CAAC;YACxD,IAAI,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC1B,MAAM,IAAI,SAAS,CACjB,0GAA0G,CAC3G,CAAC;YACJ,CAAC;YACD,IAAI,CAAC;gBACH,OAAO,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC1C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBACpC,MAAM,GAAG,GAAG,SAAS;oBACnB,CAAC,CAAC,OAAO,EAAE,eAAe,CAAC,KAAK,EAAE,MAAgB,CAAC;oBACnD,CAAC,CAAC,SAAS,CAAC;gBACd,MAAM,OAAO,GACX,GAAG,KAAK,SAAS;oBACf,CAAC,CAAC,SAAS;oBACX,CAAC,CAAE,MAAqD,CAAC,GAAG,CAAC,CAAC;gBAClE,IAAI,OAAO;oBAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;gBACnC,IAAI,OAAO,CAAC,SAAS;oBACnB,MAAM,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAa,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;gBAC7D,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|