@entifix/testing-unit 0.1.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/builders/entity-builder.d.ts +26 -0
  4. package/dist/builders/entity-builder.d.ts.map +1 -0
  5. package/dist/builders/entity-builder.js +30 -0
  6. package/dist/builders/entity-builder.js.map +1 -0
  7. package/dist/contracts/entity-repository.contract.d.ts +36 -0
  8. package/dist/contracts/entity-repository.contract.d.ts.map +1 -0
  9. package/dist/contracts/entity-repository.contract.js +273 -0
  10. package/dist/contracts/entity-repository.contract.js.map +1 -0
  11. package/dist/contracts/index.d.ts +3 -0
  12. package/dist/contracts/index.d.ts.map +1 -0
  13. package/dist/contracts/index.js +4 -0
  14. package/dist/contracts/index.js.map +1 -0
  15. package/dist/contracts/transaction-ports.contract.d.ts +44 -0
  16. package/dist/contracts/transaction-ports.contract.d.ts.map +1 -0
  17. package/dist/contracts/transaction-ports.contract.js +203 -0
  18. package/dist/contracts/transaction-ports.contract.js.map +1 -0
  19. package/dist/doubles/configuration.d.ts +36 -0
  20. package/dist/doubles/configuration.d.ts.map +1 -0
  21. package/dist/doubles/configuration.js +31 -0
  22. package/dist/doubles/configuration.js.map +1 -0
  23. package/dist/doubles/entity-repository.d.ts +24 -0
  24. package/dist/doubles/entity-repository.d.ts.map +1 -0
  25. package/dist/doubles/entity-repository.js +159 -0
  26. package/dist/doubles/entity-repository.js.map +1 -0
  27. package/dist/doubles/link-resolver.d.ts +17 -0
  28. package/dist/doubles/link-resolver.d.ts.map +1 -0
  29. package/dist/doubles/link-resolver.js +34 -0
  30. package/dist/doubles/link-resolver.js.map +1 -0
  31. package/dist/doubles/transaction-ports.d.ts +70 -0
  32. package/dist/doubles/transaction-ports.d.ts.map +1 -0
  33. package/dist/doubles/transaction-ports.js +145 -0
  34. package/dist/doubles/transaction-ports.js.map +1 -0
  35. package/dist/drivers/fake-amqp.d.ts +91 -0
  36. package/dist/drivers/fake-amqp.d.ts.map +1 -0
  37. package/dist/drivers/fake-amqp.js +164 -0
  38. package/dist/drivers/fake-amqp.js.map +1 -0
  39. package/dist/drivers/fake-mongo.d.ts +59 -0
  40. package/dist/drivers/fake-mongo.d.ts.map +1 -0
  41. package/dist/drivers/fake-mongo.js +603 -0
  42. package/dist/drivers/fake-mongo.js.map +1 -0
  43. package/dist/drivers/fake-redis.d.ts +32 -0
  44. package/dist/drivers/fake-redis.d.ts.map +1 -0
  45. package/dist/drivers/fake-redis.js +164 -0
  46. package/dist/drivers/fake-redis.js.map +1 -0
  47. package/dist/drivers/fake-sql.d.ts +58 -0
  48. package/dist/drivers/fake-sql.d.ts.map +1 -0
  49. package/dist/drivers/fake-sql.js +102 -0
  50. package/dist/drivers/fake-sql.js.map +1 -0
  51. package/dist/drivers/index.d.ts +5 -0
  52. package/dist/drivers/index.d.ts.map +1 -0
  53. package/dist/drivers/index.js +6 -0
  54. package/dist/drivers/index.js.map +1 -0
  55. package/dist/effect/run.d.ts +34 -0
  56. package/dist/effect/run.d.ts.map +1 -0
  57. package/dist/effect/run.js +48 -0
  58. package/dist/effect/run.js.map +1 -0
  59. package/dist/http/entity-handlers.d.ts +37 -0
  60. package/dist/http/entity-handlers.d.ts.map +1 -0
  61. package/dist/http/entity-handlers.js +92 -0
  62. package/dist/http/entity-handlers.js.map +1 -0
  63. package/dist/http/index.d.ts +3 -0
  64. package/dist/http/index.d.ts.map +1 -0
  65. package/dist/http/index.js +4 -0
  66. package/dist/http/index.js.map +1 -0
  67. package/dist/http/server.d.ts +19 -0
  68. package/dist/http/server.d.ts.map +1 -0
  69. package/dist/http/server.js +31 -0
  70. package/dist/http/server.js.map +1 -0
  71. package/dist/index.d.ts +7 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/react/index.d.ts +3 -0
  76. package/dist/react/index.d.ts.map +1 -0
  77. package/dist/react/index.js +4 -0
  78. package/dist/react/index.js.map +1 -0
  79. package/dist/react/render.d.ts +30 -0
  80. package/dist/react/render.d.ts.map +1 -0
  81. package/dist/react/render.js +38 -0
  82. package/dist/react/render.js.map +1 -0
  83. package/dist/react/ui-preferences.d.ts +17 -0
  84. package/dist/react/ui-preferences.d.ts.map +1 -0
  85. package/dist/react/ui-preferences.js +29 -0
  86. package/dist/react/ui-preferences.js.map +1 -0
  87. package/package.json +122 -0
@@ -0,0 +1,70 @@
1
+ import { type DomainEvent, type EntifixError } from '@entifix/core';
2
+ import type { EventBus, LockService, SequenceService, TransactionInbox } from '@entifix/transactions';
3
+ import { Effect } from 'effect';
4
+ export interface InMemoryLockService extends LockService {
5
+ /** Keys currently held, in acquisition order. */
6
+ readonly held: string[];
7
+ /** Every acquire/release in the order it happened. */
8
+ readonly log: ReadonlyArray<{
9
+ action: 'acquire' | 'release';
10
+ key: string;
11
+ }>;
12
+ /**
13
+ * Marks `key` as already taken by someone else, so the next `acquire` fails
14
+ * with {@link EntifixLockError} — the 409 path of the transaction facade.
15
+ */
16
+ contendOn(key: string): void;
17
+ }
18
+ /**
19
+ * In-memory {@link LockService}. Acquisition is immediate and uncontended
20
+ * unless {@link InMemoryLockService.contendOn} says otherwise, and `release`
21
+ * only frees a lock whose token matches — the compare-and-delete rule the Redis
22
+ * adapter enforces with a Lua script.
23
+ */
24
+ export declare const makeInMemoryLockService: () => InMemoryLockService;
25
+ export interface InMemorySequenceService extends SequenceService {
26
+ /** The current value of every sequence drawn from so far. */
27
+ readonly values: Readonly<Record<string, number>>;
28
+ }
29
+ /**
30
+ * In-memory {@link SequenceService} — a plain counter per name. Atomicity is
31
+ * what the Redis `INCR` adapter buys; single-threaded JS gives it for free here.
32
+ */
33
+ export declare const makeInMemorySequenceService: () => InMemorySequenceService;
34
+ export interface RecordingEventBus extends EventBus {
35
+ /** Every event published, in order. */
36
+ readonly published: DomainEvent[];
37
+ /**
38
+ * Pushes an event to the subscribers whose pattern matches it, as a topic
39
+ * exchange would. Deliberately not "to all subscribers": routing is the
40
+ * broker's job in production, so a double that ignored it would let a
41
+ * consumer pass its tests while receiving traffic it never bound to.
42
+ */
43
+ deliver(event: DomainEvent): Effect.Effect<void, EntifixError>;
44
+ /** Makes the next `publish` fail, for the broker-unavailable branch. */
45
+ failNextPublish(): void;
46
+ }
47
+ /**
48
+ * In-memory {@link EventBus} that records what was published rather than
49
+ * asserting on it. Prefer reading `bus.published` in a test over a mock's call
50
+ * assertions: the expectation then reads as state, not as a spy protocol.
51
+ */
52
+ export declare const makeRecordingEventBus: () => RecordingEventBus;
53
+ /**
54
+ * An in-memory {@link TransactionInbox}, one per consumer, over a shared claim
55
+ * set.
56
+ *
57
+ * Sharing the set across the inboxes one factory hands out is what makes the
58
+ * contract's "one consumer cannot consume another's claim" case a real
59
+ * assertion, rather than one the double satisfies by only ever having seen a
60
+ * single consumer.
61
+ */
62
+ export declare const makeInMemoryInboxes: () => {
63
+ /** An inbox for `consumer`, over the shared claim set. */
64
+ for: (consumer: string) => TransactionInbox;
65
+ readonly claims: readonly {
66
+ consumer: string;
67
+ eventId: string;
68
+ }[];
69
+ };
70
+ //# sourceMappingURL=transaction-ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-ports.d.ts","sourceRoot":"","sources":["../../src/doubles/transaction-ports.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,QAAQ,EAER,WAAW,EACX,eAAe,EACf,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E;;;OAGG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,mBAsC1C,CAAC;AAEF,MAAM,WAAW,uBAAwB,SAAQ,eAAe;IAC9D,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAED;;;GAGG;AACH,eAAO,MAAM,2BAA2B,QAAO,uBAc9C,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,QAAQ;IACjD,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC;IAClC;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAC/D,wEAAwE;IACxE,eAAe,IAAI,IAAI,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAO,iBA4CxC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB;IAI5B,0DAA0D;oBAC1C,MAAM,KAAG,gBAAgB;qBAS3B,SAAS;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE;CAOjE,CAAC"}
@@ -0,0 +1,145 @@
1
+ import { EntifixConnError, EntifixLockError, matchesEventPattern } from '@entifix/core';
2
+ import { Effect } from 'effect';
3
+ /**
4
+ * In-memory {@link LockService}. Acquisition is immediate and uncontended
5
+ * unless {@link InMemoryLockService.contendOn} says otherwise, and `release`
6
+ * only frees a lock whose token matches — the compare-and-delete rule the Redis
7
+ * adapter enforces with a Lua script.
8
+ */ export const makeInMemoryLockService = ()=>{
9
+ const tokens = new Map();
10
+ const contended = new Set();
11
+ const log = [];
12
+ let counter = 0;
13
+ return {
14
+ acquire: (key)=>Effect.suspend(()=>{
15
+ if (contended.has(key) || tokens.has(key)) {
16
+ return Effect.fail(new EntifixLockError(`Could not acquire lock "${key}"`, undefined, {
17
+ key
18
+ }));
19
+ }
20
+ const token = `token-${counter += 1}`;
21
+ tokens.set(key, token);
22
+ log.push({
23
+ action: 'acquire',
24
+ key
25
+ });
26
+ return Effect.succeed({
27
+ key,
28
+ token
29
+ });
30
+ }),
31
+ release: (handle)=>Effect.sync(()=>{
32
+ if (tokens.get(handle.key) === handle.token) {
33
+ tokens.delete(handle.key);
34
+ log.push({
35
+ action: 'release',
36
+ key: handle.key
37
+ });
38
+ }
39
+ }),
40
+ get held () {
41
+ return [
42
+ ...tokens.keys()
43
+ ];
44
+ },
45
+ get log () {
46
+ return log;
47
+ },
48
+ contendOn: (key)=>{
49
+ contended.add(key);
50
+ }
51
+ };
52
+ };
53
+ /**
54
+ * In-memory {@link SequenceService} — a plain counter per name. Atomicity is
55
+ * what the Redis `INCR` adapter buys; single-threaded JS gives it for free here.
56
+ */ export const makeInMemorySequenceService = ()=>{
57
+ const sequences = new Map();
58
+ return {
59
+ next: (name)=>Effect.sync(()=>{
60
+ var _sequences_get;
61
+ const next = ((_sequences_get = sequences.get(name)) != null ? _sequences_get : 0) + 1;
62
+ sequences.set(name, next);
63
+ return next;
64
+ }),
65
+ get values () {
66
+ return Object.fromEntries(sequences);
67
+ }
68
+ };
69
+ };
70
+ /**
71
+ * In-memory {@link EventBus} that records what was published rather than
72
+ * asserting on it. Prefer reading `bus.published` in a test over a mock's call
73
+ * assertions: the expectation then reads as state, not as a spy protocol.
74
+ */ export const makeRecordingEventBus = ()=>{
75
+ const published = [];
76
+ const handlers = [];
77
+ let failNext = false;
78
+ return {
79
+ publish: (event)=>Effect.suspend(()=>{
80
+ if (failNext) {
81
+ failNext = false;
82
+ return Effect.fail(new EntifixConnError('Event bus unavailable', undefined, {
83
+ eventId: event.id
84
+ }));
85
+ }
86
+ published.push(event);
87
+ return Effect.void;
88
+ }),
89
+ subscribe: (subscription, handle)=>Effect.sync(()=>{
90
+ // Only the pattern is retained: queue durability and the delivery
91
+ // ceiling are broker facts with no in-memory analogue, and faking them
92
+ // would assert against the double rather than against a broker.
93
+ handlers.push({
94
+ pattern: subscription.pattern,
95
+ handle
96
+ });
97
+ }),
98
+ deliver: (event)=>Effect.forEach(handlers.filter(({ pattern })=>matchesEventPattern(pattern, event.name)), ({ handle })=>handle(event), {
99
+ discard: true
100
+ }),
101
+ get published () {
102
+ return published;
103
+ },
104
+ failNextPublish: ()=>{
105
+ failNext = true;
106
+ }
107
+ };
108
+ };
109
+ /**
110
+ * An in-memory {@link TransactionInbox}, one per consumer, over a shared claim
111
+ * set.
112
+ *
113
+ * Sharing the set across the inboxes one factory hands out is what makes the
114
+ * contract's "one consumer cannot consume another's claim" case a real
115
+ * assertion, rather than one the double satisfies by only ever having seen a
116
+ * single consumer.
117
+ */ export const makeInMemoryInboxes = ()=>{
118
+ const claimed = new Set();
119
+ return {
120
+ /** An inbox for `consumer`, over the shared claim set. */ for: (consumer)=>({
121
+ claim: (eventId)=>Effect.sync(()=>{
122
+ const key = JSON.stringify([
123
+ consumer,
124
+ eventId
125
+ ]);
126
+ if (claimed.has(key)) return 'duplicate';
127
+ claimed.add(key);
128
+ return 'claimed';
129
+ })
130
+ }),
131
+ get claims () {
132
+ return [
133
+ ...claimed
134
+ ].map((key)=>{
135
+ const [consumer, eventId] = JSON.parse(key);
136
+ return {
137
+ consumer,
138
+ eventId
139
+ };
140
+ });
141
+ }
142
+ };
143
+ };
144
+
145
+ //# sourceMappingURL=transaction-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/doubles/transaction-ports.ts"],"sourcesContent":["import {\n type DomainEvent,\n EntifixConnError,\n type EntifixError,\n EntifixLockError,\n matchesEventPattern,\n} from '@entifix/core';\nimport type {\n EventBus,\n LockHandle,\n LockService,\n SequenceService,\n TransactionInbox,\n} from '@entifix/transactions';\nimport { Effect } from 'effect';\n\nexport interface InMemoryLockService extends LockService {\n /** Keys currently held, in acquisition order. */\n readonly held: string[];\n /** Every acquire/release in the order it happened. */\n readonly log: ReadonlyArray<{ action: 'acquire' | 'release'; key: string }>;\n /**\n * Marks `key` as already taken by someone else, so the next `acquire` fails\n * with {@link EntifixLockError} — the 409 path of the transaction facade.\n */\n contendOn(key: string): void;\n}\n\n/**\n * In-memory {@link LockService}. Acquisition is immediate and uncontended\n * unless {@link InMemoryLockService.contendOn} says otherwise, and `release`\n * only frees a lock whose token matches — the compare-and-delete rule the Redis\n * adapter enforces with a Lua script.\n */\nexport const makeInMemoryLockService = (): InMemoryLockService => {\n const tokens = new Map<string, string>();\n const contended = new Set<string>();\n const log: Array<{ action: 'acquire' | 'release'; key: string }> = [];\n let counter = 0;\n\n return {\n acquire: key =>\n Effect.suspend(() => {\n if (contended.has(key) || tokens.has(key)) {\n return Effect.fail(\n new EntifixLockError(`Could not acquire lock \"${key}\"`, undefined, {\n key,\n }),\n );\n }\n const token = `token-${(counter += 1)}`;\n tokens.set(key, token);\n log.push({ action: 'acquire', key });\n return Effect.succeed({ key, token });\n }),\n release: (handle: LockHandle) =>\n Effect.sync(() => {\n if (tokens.get(handle.key) === handle.token) {\n tokens.delete(handle.key);\n log.push({ action: 'release', key: handle.key });\n }\n }),\n get held() {\n return [...tokens.keys()];\n },\n get log() {\n return log;\n },\n contendOn: key => {\n contended.add(key);\n },\n };\n};\n\nexport interface InMemorySequenceService extends SequenceService {\n /** The current value of every sequence drawn from so far. */\n readonly values: Readonly<Record<string, number>>;\n}\n\n/**\n * In-memory {@link SequenceService} — a plain counter per name. Atomicity is\n * what the Redis `INCR` adapter buys; single-threaded JS gives it for free here.\n */\nexport const makeInMemorySequenceService = (): InMemorySequenceService => {\n const sequences = new Map<string, number>();\n\n return {\n next: name =>\n Effect.sync(() => {\n const next = (sequences.get(name) ?? 0) + 1;\n sequences.set(name, next);\n return next;\n }),\n get values() {\n return Object.fromEntries(sequences);\n },\n };\n};\n\nexport interface RecordingEventBus extends EventBus {\n /** Every event published, in order. */\n readonly published: DomainEvent[];\n /**\n * Pushes an event to the subscribers whose pattern matches it, as a topic\n * exchange would. Deliberately not \"to all subscribers\": routing is the\n * broker's job in production, so a double that ignored it would let a\n * consumer pass its tests while receiving traffic it never bound to.\n */\n deliver(event: DomainEvent): Effect.Effect<void, EntifixError>;\n /** Makes the next `publish` fail, for the broker-unavailable branch. */\n failNextPublish(): void;\n}\n\n/**\n * In-memory {@link EventBus} that records what was published rather than\n * asserting on it. Prefer reading `bus.published` in a test over a mock's call\n * assertions: the expectation then reads as state, not as a spy protocol.\n */\nexport const makeRecordingEventBus = (): RecordingEventBus => {\n const published: DomainEvent[] = [];\n const handlers: Array<{\n pattern: string;\n handle: (event: DomainEvent) => Effect.Effect<void, EntifixError>;\n }> = [];\n let failNext = false;\n\n return {\n publish: event =>\n Effect.suspend(() => {\n if (failNext) {\n failNext = false;\n return Effect.fail(\n new EntifixConnError('Event bus unavailable', undefined, {\n eventId: event.id,\n }),\n );\n }\n published.push(event);\n return Effect.void;\n }),\n subscribe: (subscription, handle) =>\n Effect.sync(() => {\n // Only the pattern is retained: queue durability and the delivery\n // ceiling are broker facts with no in-memory analogue, and faking them\n // would assert against the double rather than against a broker.\n handlers.push({ pattern: subscription.pattern, handle });\n }),\n deliver: event =>\n Effect.forEach(\n handlers.filter(({ pattern }) =>\n matchesEventPattern(pattern, event.name),\n ),\n ({ handle }) => handle(event),\n { discard: true },\n ),\n get published() {\n return published;\n },\n failNextPublish: () => {\n failNext = true;\n },\n };\n};\n\n/**\n * An in-memory {@link TransactionInbox}, one per consumer, over a shared claim\n * set.\n *\n * Sharing the set across the inboxes one factory hands out is what makes the\n * contract's \"one consumer cannot consume another's claim\" case a real\n * assertion, rather than one the double satisfies by only ever having seen a\n * single consumer.\n */\nexport const makeInMemoryInboxes = () => {\n const claimed = new Set<string>();\n\n return {\n /** An inbox for `consumer`, over the shared claim set. */\n for: (consumer: string): TransactionInbox => ({\n claim: eventId =>\n Effect.sync(() => {\n const key = JSON.stringify([consumer, eventId]);\n if (claimed.has(key)) return 'duplicate' as const;\n claimed.add(key);\n return 'claimed' as const;\n }),\n }),\n get claims(): readonly { consumer: string; eventId: string }[] {\n return [...claimed].map(key => {\n const [consumer, eventId] = JSON.parse(key) as [string, string];\n return { consumer, eventId };\n });\n },\n };\n};\n"],"names":["EntifixConnError","EntifixLockError","matchesEventPattern","Effect","makeInMemoryLockService","tokens","Map","contended","Set","log","counter","acquire","key","suspend","has","fail","undefined","token","set","push","action","succeed","release","handle","sync","get","delete","held","keys","contendOn","add","makeInMemorySequenceService","sequences","next","name","values","Object","fromEntries","makeRecordingEventBus","published","handlers","failNext","publish","event","eventId","id","void","subscribe","subscription","pattern","deliver","forEach","filter","discard","failNextPublish","makeInMemoryInboxes","claimed","for","consumer","claim","JSON","stringify","claims","map","parse"],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A fake of the amqplib channel, not of the {@link EventBus} port.
3
+ *
4
+ * `makeAmqpEventBus` runs on top of it, so the envelope framing, the
5
+ * `prefetch(1)` the saga tracker depends on, and the ack/nack policy are
6
+ * all exercised for real.
7
+ */
8
+ export interface FakeAmqpMessage {
9
+ content: Buffer;
10
+ }
11
+ export interface FakeAmqpChannel {
12
+ /** Everything published, decoded from the wire. */
13
+ readonly published: ReadonlyArray<{
14
+ exchange: string;
15
+ routingKey: string;
16
+ body: unknown;
17
+ }>;
18
+ /**
19
+ * Every queue binding the adapter asked for. Recorded because the exchange is
20
+ * a topic now: the pattern a subscriber binds is the whole of its routing, so
21
+ * a fake that swallowed it would let a wrong binding pass every test.
22
+ */
23
+ readonly bindings: ReadonlyArray<{
24
+ queue: string;
25
+ exchange: string;
26
+ pattern: string;
27
+ }>;
28
+ /**
29
+ * Every exchange the adapter declared, with its type. Recorded because the
30
+ * dead-letter exchange is `direct` while the event exchange is `topic`, and a
31
+ * broker will not retype an existing exchange — declaring the wrong type is a
32
+ * failure that only appears against a real broker.
33
+ */
34
+ readonly exchanges: ReadonlyArray<{
35
+ exchange: string;
36
+ type: string;
37
+ options: Record<string, unknown> | undefined;
38
+ }>;
39
+ /**
40
+ * Every queue the adapter declared, with the arguments it declared it with.
41
+ * The arguments *are* the delivery policy — `x-queue-type`,
42
+ * `x-delivery-limit`, `x-dead-letter-exchange` — so a fake that swallowed
43
+ * them would let a queue with no retry ceiling and no dead-letter path pass
44
+ * every test, which is the exact defect ADR 0030 exists to close.
45
+ */
46
+ readonly queues: ReadonlyArray<{
47
+ queue: string;
48
+ options: Record<string, unknown> | undefined;
49
+ }>;
50
+ /** Messages acked, so the success policy can be asserted. */
51
+ readonly acked: FakeAmqpMessage[];
52
+ /**
53
+ * Messages nacked, **with their flags**. `requeue` is the whole distinction
54
+ * between a transient failure the broker should retry and a poison message it
55
+ * should quarantine at once, so recording only that a nack happened is how
56
+ * "dead-letters a failed message" stayed a passing test for a bus that
57
+ * discarded it.
58
+ */
59
+ readonly nacked: ReadonlyArray<{
60
+ message: FakeAmqpMessage;
61
+ allUpTo: boolean;
62
+ requeue: boolean;
63
+ }>;
64
+ /** The prefetch the adapter asked for — 1, or the fold races. */
65
+ readonly prefetchCount: number | undefined;
66
+ /**
67
+ * The consumer tags cancelled, in order.
68
+ *
69
+ * Recorded rather than merely accepted because a graceful shutdown's first
70
+ * phase *is* the cancel (ADR 0030): a connector that dropped the tag on the
71
+ * floor would pass every test while leaving the broker delivering into a
72
+ * process that is closing its connection.
73
+ */
74
+ readonly cancelled: readonly string[];
75
+ /**
76
+ * Pushes a message to the consumers whose binding matches it, as the broker
77
+ * would. The routing key defaults to the envelope's own `meta.event.name`,
78
+ * which is what the adapter publishes with.
79
+ */
80
+ deliver(body: unknown, routingKey?: string): Promise<void>;
81
+ /** Pushes a raw payload, for malformed-message paths. */
82
+ deliverRaw(payload: string): Promise<void>;
83
+ /** Simulates the broker cancelling the consumer (amqplib delivers `null`). */
84
+ deliverCancellation(): Promise<void>;
85
+ /** Makes every subsequent channel call throw/reject with `error`. */
86
+ failWith(error: unknown): void;
87
+ /** The object to pass where an amqplib `Channel` is expected. */
88
+ readonly channel: unknown;
89
+ }
90
+ export declare const makeFakeAmqpChannel: () => FakeAmqpChannel;
91
+ //# sourceMappingURL=fake-amqp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-amqp.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-amqp.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;KACf,CAAC,CAAC;IACH;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;QAC/B,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;QAChC,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC,CAAC;IACH,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,eAAe,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;QAC7B,OAAO,EAAE,eAAe,CAAC;QACzB,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC,CAAC;IACH,iEAAiE;IACjE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,yDAAyD;IACzD,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AASD,eAAO,MAAM,mBAAmB,QAAO,eA0KtC,CAAC"}
@@ -0,0 +1,164 @@
1
+ import { matchesEventPattern } from '@entifix/core';
2
+ /** The routing key a body would have been published with. */ const routingKeyOf = (body)=>{
3
+ var _body_meta;
4
+ const event = body == null ? void 0 : (_body_meta = body.meta) == null ? void 0 : _body_meta.event;
5
+ return typeof (event == null ? void 0 : event.name) === 'string' ? event.name : '';
6
+ };
7
+ export const makeFakeAmqpChannel = ()=>{
8
+ const published = [];
9
+ const bindings = [];
10
+ const exchanges = [];
11
+ const queues = [];
12
+ const acked = [];
13
+ const nacked = [];
14
+ let consumer;
15
+ const cancelled = [];
16
+ let consumerTags = 0;
17
+ let boundQueue;
18
+ let prefetchCount;
19
+ let failure;
20
+ const guard = ()=>{
21
+ if (failure !== undefined) throw failure;
22
+ };
23
+ const channel = {
24
+ publish: (exchange, routingKey, content)=>{
25
+ guard();
26
+ published.push({
27
+ exchange,
28
+ routingKey,
29
+ body: JSON.parse(content.toString())
30
+ });
31
+ return true;
32
+ },
33
+ prefetch: async (count)=>{
34
+ guard();
35
+ prefetchCount = count;
36
+ },
37
+ assertExchange: async (exchange, type, options)=>{
38
+ guard();
39
+ exchanges.push({
40
+ exchange,
41
+ type,
42
+ options
43
+ });
44
+ return {
45
+ exchange
46
+ };
47
+ },
48
+ assertQueue: async (queue, options)=>{
49
+ guard();
50
+ const name = queue === '' ? 'amq.gen-fake' : queue;
51
+ queues.push({
52
+ queue: name,
53
+ options
54
+ });
55
+ return {
56
+ queue: name
57
+ };
58
+ },
59
+ bindQueue: async (queue, exchange, pattern)=>{
60
+ guard();
61
+ bindings.push({
62
+ queue,
63
+ exchange,
64
+ pattern
65
+ });
66
+ },
67
+ consume: async (queue, handler)=>{
68
+ guard();
69
+ consumer = handler;
70
+ boundQueue = queue;
71
+ // A fresh tag per `consume`, so a reconnect's re-bind is distinguishable
72
+ // from the binding it replaced — the connector cancels what it currently
73
+ // holds, and a constant tag would hide it cancelling a stale one.
74
+ consumerTags += 1;
75
+ return {
76
+ consumerTag: `fake-consumer-${consumerTags}`
77
+ };
78
+ },
79
+ cancel: async (consumerTag)=>{
80
+ guard();
81
+ cancelled.push(consumerTag);
82
+ consumer = undefined;
83
+ },
84
+ ack: (message)=>{
85
+ acked.push(message);
86
+ },
87
+ nack: (message, allUpTo, requeue)=>{
88
+ nacked.push({
89
+ message,
90
+ allUpTo,
91
+ requeue
92
+ });
93
+ },
94
+ close: async ()=>{
95
+ guard();
96
+ }
97
+ };
98
+ /**
99
+ * The adapter acks/nacks from inside a floating promise chain, so a delivery
100
+ * is only settled once the microtask queue drains.
101
+ */ const settle = ()=>new Promise((resolve)=>setTimeout(resolve, 0));
102
+ /**
103
+ * Whether the consumer's queue is bound to a pattern matching `routingKey`.
104
+ * The broker decides this in production, so the fake has to as well — without
105
+ * it every subscriber sees every message and the topic exchange is untested.
106
+ */ const routes = (routingKey)=>bindings.some((binding)=>binding.queue === boundQueue && matchesEventPattern(binding.pattern, routingKey));
107
+ /**
108
+ * `routingKey === undefined` means "the broker already routed this" — used by
109
+ * the malformed-payload path, where the message reached the queue and it is
110
+ * the *content* that is wrong.
111
+ */ const push = async (payload, routingKey)=>{
112
+ if (consumer === undefined) {
113
+ throw new Error('fake amqp: nothing subscribed yet');
114
+ }
115
+ if (routingKey !== undefined && !routes(routingKey)) {
116
+ return;
117
+ }
118
+ consumer({
119
+ content: Buffer.from(payload)
120
+ });
121
+ await settle();
122
+ };
123
+ return {
124
+ get published () {
125
+ return published;
126
+ },
127
+ get bindings () {
128
+ return bindings;
129
+ },
130
+ get exchanges () {
131
+ return exchanges;
132
+ },
133
+ get queues () {
134
+ return queues;
135
+ },
136
+ get acked () {
137
+ return acked;
138
+ },
139
+ get nacked () {
140
+ return nacked;
141
+ },
142
+ get prefetchCount () {
143
+ return prefetchCount;
144
+ },
145
+ get cancelled () {
146
+ return cancelled;
147
+ },
148
+ deliver: (body, routingKey)=>push(JSON.stringify(body), routingKey != null ? routingKey : routingKeyOf(body)),
149
+ deliverRaw: (payload)=>push(payload, undefined),
150
+ deliverCancellation: async ()=>{
151
+ if (consumer === undefined) {
152
+ throw new Error('fake amqp: nothing subscribed yet');
153
+ }
154
+ consumer(null);
155
+ await settle();
156
+ },
157
+ failWith: (error)=>{
158
+ failure = error;
159
+ },
160
+ channel
161
+ };
162
+ };
163
+
164
+ //# sourceMappingURL=fake-amqp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/drivers/fake-amqp.ts"],"sourcesContent":["import { matchesEventPattern } from '@entifix/core';\n\n/**\n * A fake of the amqplib channel, not of the {@link EventBus} port.\n *\n * `makeAmqpEventBus` runs on top of it, so the envelope framing, the\n * `prefetch(1)` the saga tracker depends on, and the ack/nack policy are\n * all exercised for real.\n */\n\nexport interface FakeAmqpMessage {\n content: Buffer;\n}\n\nexport interface FakeAmqpChannel {\n /** Everything published, decoded from the wire. */\n readonly published: ReadonlyArray<{\n exchange: string;\n routingKey: string;\n body: unknown;\n }>;\n /**\n * Every queue binding the adapter asked for. Recorded because the exchange is\n * a topic now: the pattern a subscriber binds is the whole of its routing, so\n * a fake that swallowed it would let a wrong binding pass every test.\n */\n readonly bindings: ReadonlyArray<{\n queue: string;\n exchange: string;\n pattern: string;\n }>;\n /**\n * Every exchange the adapter declared, with its type. Recorded because the\n * dead-letter exchange is `direct` while the event exchange is `topic`, and a\n * broker will not retype an existing exchange — declaring the wrong type is a\n * failure that only appears against a real broker.\n */\n readonly exchanges: ReadonlyArray<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }>;\n /**\n * Every queue the adapter declared, with the arguments it declared it with.\n * The arguments *are* the delivery policy — `x-queue-type`,\n * `x-delivery-limit`, `x-dead-letter-exchange` — so a fake that swallowed\n * them would let a queue with no retry ceiling and no dead-letter path pass\n * every test, which is the exact defect ADR 0030 exists to close.\n */\n readonly queues: ReadonlyArray<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }>;\n /** Messages acked, so the success policy can be asserted. */\n readonly acked: FakeAmqpMessage[];\n /**\n * Messages nacked, **with their flags**. `requeue` is the whole distinction\n * between a transient failure the broker should retry and a poison message it\n * should quarantine at once, so recording only that a nack happened is how\n * \"dead-letters a failed message\" stayed a passing test for a bus that\n * discarded it.\n */\n readonly nacked: ReadonlyArray<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }>;\n /** The prefetch the adapter asked for — 1, or the fold races. */\n readonly prefetchCount: number | undefined;\n /**\n * The consumer tags cancelled, in order.\n *\n * Recorded rather than merely accepted because a graceful shutdown's first\n * phase *is* the cancel (ADR 0030): a connector that dropped the tag on the\n * floor would pass every test while leaving the broker delivering into a\n * process that is closing its connection.\n */\n readonly cancelled: readonly string[];\n /**\n * Pushes a message to the consumers whose binding matches it, as the broker\n * would. The routing key defaults to the envelope's own `meta.event.name`,\n * which is what the adapter publishes with.\n */\n deliver(body: unknown, routingKey?: string): Promise<void>;\n /** Pushes a raw payload, for malformed-message paths. */\n deliverRaw(payload: string): Promise<void>;\n /** Simulates the broker cancelling the consumer (amqplib delivers `null`). */\n deliverCancellation(): Promise<void>;\n /** Makes every subsequent channel call throw/reject with `error`. */\n failWith(error: unknown): void;\n /** The object to pass where an amqplib `Channel` is expected. */\n readonly channel: unknown;\n}\n\n/** The routing key a body would have been published with. */\nconst routingKeyOf = (body: unknown): string => {\n const event = (body as { meta?: { event?: { name?: unknown } } })?.meta\n ?.event;\n return typeof event?.name === 'string' ? event.name : '';\n};\n\nexport const makeFakeAmqpChannel = (): FakeAmqpChannel => {\n const published: Array<{\n exchange: string;\n routingKey: string;\n body: unknown;\n }> = [];\n const bindings: Array<{ queue: string; exchange: string; pattern: string }> =\n [];\n const exchanges: Array<{\n exchange: string;\n type: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const queues: Array<{\n queue: string;\n options: Record<string, unknown> | undefined;\n }> = [];\n const acked: FakeAmqpMessage[] = [];\n const nacked: Array<{\n message: FakeAmqpMessage;\n allUpTo: boolean;\n requeue: boolean;\n }> = [];\n let consumer: ((message: FakeAmqpMessage | null) => void) | undefined;\n const cancelled: string[] = [];\n let consumerTags = 0;\n let boundQueue: string | undefined;\n let prefetchCount: number | undefined;\n let failure: unknown;\n\n const guard = () => {\n if (failure !== undefined) throw failure;\n };\n\n const channel = {\n publish: (exchange: string, routingKey: string, content: Buffer) => {\n guard();\n published.push({\n exchange,\n routingKey,\n body: JSON.parse(content.toString()) as unknown,\n });\n return true;\n },\n prefetch: async (count: number) => {\n guard();\n prefetchCount = count;\n },\n assertExchange: async (\n exchange: string,\n type: string,\n options?: Record<string, unknown>,\n ) => {\n guard();\n exchanges.push({ exchange, type, options });\n return { exchange };\n },\n assertQueue: async (queue: string, options?: Record<string, unknown>) => {\n guard();\n const name = queue === '' ? 'amq.gen-fake' : queue;\n queues.push({ queue: name, options });\n return { queue: name };\n },\n bindQueue: async (queue: string, exchange: string, pattern: string) => {\n guard();\n bindings.push({ queue, exchange, pattern });\n },\n consume: async (\n queue: string,\n handler: (message: FakeAmqpMessage | null) => void,\n ) => {\n guard();\n consumer = handler;\n boundQueue = queue;\n // A fresh tag per `consume`, so a reconnect's re-bind is distinguishable\n // from the binding it replaced — the connector cancels what it currently\n // holds, and a constant tag would hide it cancelling a stale one.\n consumerTags += 1;\n return { consumerTag: `fake-consumer-${consumerTags}` };\n },\n cancel: async (consumerTag: string) => {\n guard();\n cancelled.push(consumerTag);\n consumer = undefined;\n },\n ack: (message: FakeAmqpMessage) => {\n acked.push(message);\n },\n nack: (message: FakeAmqpMessage, allUpTo: boolean, requeue: boolean) => {\n nacked.push({ message, allUpTo, requeue });\n },\n close: async () => {\n guard();\n },\n };\n\n /**\n * The adapter acks/nacks from inside a floating promise chain, so a delivery\n * is only settled once the microtask queue drains.\n */\n const settle = () => new Promise<void>(resolve => setTimeout(resolve, 0));\n\n /**\n * Whether the consumer's queue is bound to a pattern matching `routingKey`.\n * The broker decides this in production, so the fake has to as well — without\n * it every subscriber sees every message and the topic exchange is untested.\n */\n const routes = (routingKey: string) =>\n bindings.some(\n binding =>\n binding.queue === boundQueue &&\n matchesEventPattern(binding.pattern, routingKey),\n );\n\n /**\n * `routingKey === undefined` means \"the broker already routed this\" — used by\n * the malformed-payload path, where the message reached the queue and it is\n * the *content* that is wrong.\n */\n const push = async (payload: string, routingKey: string | undefined) => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n if (routingKey !== undefined && !routes(routingKey)) {\n return;\n }\n consumer({ content: Buffer.from(payload) });\n await settle();\n };\n\n return {\n get published() {\n return published;\n },\n get bindings() {\n return bindings;\n },\n get exchanges() {\n return exchanges;\n },\n get queues() {\n return queues;\n },\n get acked() {\n return acked;\n },\n get nacked() {\n return nacked;\n },\n get prefetchCount() {\n return prefetchCount;\n },\n get cancelled() {\n return cancelled;\n },\n deliver: (body, routingKey) =>\n push(JSON.stringify(body), routingKey ?? routingKeyOf(body)),\n deliverRaw: payload => push(payload, undefined),\n deliverCancellation: async () => {\n if (consumer === undefined) {\n throw new Error('fake amqp: nothing subscribed yet');\n }\n consumer(null);\n await settle();\n },\n failWith: error => {\n failure = error;\n },\n channel,\n };\n};\n"],"names":["matchesEventPattern","routingKeyOf","body","event","meta","name","makeFakeAmqpChannel","published","bindings","exchanges","queues","acked","nacked","consumer","cancelled","consumerTags","boundQueue","prefetchCount","failure","guard","undefined","channel","publish","exchange","routingKey","content","push","JSON","parse","toString","prefetch","count","assertExchange","type","options","assertQueue","queue","bindQueue","pattern","consume","handler","consumerTag","cancel","ack","message","nack","allUpTo","requeue","close","settle","Promise","resolve","setTimeout","routes","some","binding","payload","Error","Buffer","from","deliver","stringify","deliverRaw","deliverCancellation","failWith","error"],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A fake of the mongodb driver, not of the repository port.
3
+ *
4
+ * It sits one level *below* `makeMongoRepository`, so the adapter's own code —
5
+ * the filter translation, the projection, the upsert rule, the error mapping —
6
+ * actually runs and is measured by coverage. A fake at the port level would
7
+ * skip all of it.
8
+ *
9
+ * Only the surface the adapter uses is implemented; anything else is a
10
+ * deliberate `TypeError` rather than a silent no-op.
11
+ */
12
+ type Document = Record<string, unknown>;
13
+ export interface FakeMongoCollection {
14
+ /** The documents currently stored, as the driver would return them. */
15
+ readonly documents: Document[];
16
+ }
17
+ export interface FakeMongoDb {
18
+ /** Replaces a collection's contents. */
19
+ seed(collection: string, documents: Document[]): void;
20
+ /** Reads a collection's current contents. */
21
+ read(collection: string): Document[];
22
+ /**
23
+ * Makes every subsequent driver call reject with `error`, so the adapter's
24
+ * `EntifixConnError` mapping is reachable. Pass `undefined` to restore.
25
+ */
26
+ failWith(error: unknown): void;
27
+ /**
28
+ * Makes only `operation` reject, leaving the rest working — the way a read
29
+ * that succeeds and a count that then fails reaches a distinct error branch.
30
+ */
31
+ failOn(operation: string, error: unknown): void;
32
+ /** Every operation performed, for assertions about what the adapter did. */
33
+ readonly operations: ReadonlyArray<{
34
+ collection: string;
35
+ op: string;
36
+ }>;
37
+ /**
38
+ * Opens a session whose `withTransaction` rolls the whole store back when its
39
+ * body throws. What a service hands to `client.startSession()`; see the
40
+ * implementation for why it is atomicity and not isolation.
41
+ */
42
+ startSession(): {
43
+ withTransaction(body: () => Promise<unknown>): Promise<unknown>;
44
+ endSession(): Promise<void>;
45
+ readonly hasEnded: boolean;
46
+ };
47
+ /** The object to hand to `makeMongoRepository` in place of a real `Db`. */
48
+ readonly db: unknown;
49
+ }
50
+ /**
51
+ * Builds an in-memory stand-in for a mongodb `Db`.
52
+ *
53
+ * `db` is typed `unknown` on purpose: casting it at the call site keeps this
54
+ * package free of a hard dependency on the `mongodb` types while remaining
55
+ * honest that it is not a real driver.
56
+ */
57
+ export declare const makeFakeMongoDb: (seed?: Record<string, Document[]>) => FakeMongoDb;
58
+ export {};
59
+ //# sourceMappingURL=fake-mongo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-mongo.d.ts","sourceRoot":"","sources":["../../src/drivers/fake-mongo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuMxC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACtD,6CAA6C;IAC7C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvE;;;;OAIG;IACH,YAAY,IAAI;QACd,eAAe,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAM,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAM,KACpC,WA0dF,CAAC"}