@entifix/core 0.9.0 → 0.10.1
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/dist/deadline/deadline.d.ts +33 -0
- package/dist/deadline/deadline.d.ts.map +1 -0
- package/dist/deadline/deadline.js +33 -0
- package/dist/deadline/deadline.js.map +1 -0
- package/dist/entity-definition/links/entity-collection-link/entity-collection-link.d.ts +3 -3
- package/dist/entity-definition/links/entity-collection-link/entity-collection-link.d.ts.map +1 -1
- package/dist/entity-definition/links/entity-collection-link/entity-collection-link.js.map +1 -1
- package/dist/entity-definition/links/entity-link/entity-link.d.ts +4 -4
- package/dist/entity-definition/links/entity-link/entity-link.d.ts.map +1 -1
- package/dist/entity-definition/links/entity-link/entity-link.js.map +1 -1
- package/dist/entity-definition/links/entity-link-source/entity-link-source.d.ts +3 -3
- package/dist/entity-definition/links/entity-link-source/entity-link-source.d.ts.map +1 -1
- package/dist/entity-definition/links/entity-link-source/entity-link-source.js.map +1 -1
- package/dist/entity-definition/validate/validate-records.d.ts +23 -6
- package/dist/entity-definition/validate/validate-records.d.ts.map +1 -1
- package/dist/entity-definition/validate/validate-records.js +25 -10
- package/dist/entity-definition/validate/validate-records.js.map +1 -1
- package/dist/envelope/read-envelope.d.ts +8 -8
- package/dist/envelope/read-envelope.d.ts.map +1 -1
- package/dist/envelope/read-envelope.js +18 -27
- package/dist/envelope/read-envelope.js.map +1 -1
- package/dist/failure/define.d.ts +39 -0
- package/dist/failure/define.d.ts.map +1 -0
- package/dist/failure/define.js +70 -0
- package/dist/failure/define.js.map +1 -0
- package/dist/failure/failure.d.ts +79 -0
- package/dist/failure/failure.d.ts.map +1 -0
- package/dist/failure/failure.js +39 -0
- package/dist/failure/failure.js.map +1 -0
- package/dist/failure/generic.d.ts +141 -0
- package/dist/failure/generic.d.ts.map +1 -0
- package/dist/failure/generic.js +101 -0
- package/dist/failure/generic.js.map +1 -0
- package/dist/failure/index.d.ts +50 -0
- package/dist/failure/index.d.ts.map +1 -0
- package/dist/failure/index.js +47 -0
- package/dist/failure/index.js.map +1 -0
- package/dist/failure/predicates.d.ts +56 -0
- package/dist/failure/predicates.d.ts.map +1 -0
- package/dist/failure/predicates.js +22 -0
- package/dist/failure/predicates.js.map +1 -0
- package/dist/failure/problem.d.ts +49 -0
- package/dist/failure/problem.d.ts.map +1 -0
- package/dist/failure/problem.js +87 -0
- package/dist/failure/problem.js.map +1 -0
- package/dist/failure/registry.d.ts +28 -0
- package/dist/failure/registry.d.ts.map +1 -0
- package/dist/failure/registry.js +36 -0
- package/dist/failure/registry.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
|
-
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
4
3
|
import { deserializeEntityCollection, deserializeSingleEntity } from '../entity-definition/serializer/index.js';
|
|
4
|
+
import { InvalidEnvelope } from '../failure/generic.js';
|
|
5
5
|
import { envelopeEntityName } from './make-envelope.js';
|
|
6
6
|
/**
|
|
7
7
|
* Structural check only — `meta.type` is validated separately so a mismatch
|
|
@@ -15,22 +15,18 @@ import { envelopeEntityName } from './make-envelope.js';
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Narrows an arbitrary body to an envelope of the expected `meta.type`, failing
|
|
18
|
-
* with an {@link
|
|
18
|
+
* with an {@link InvalidEnvelope} otherwise. A wrong shape must never
|
|
19
19
|
* silently deserialize into a half-populated entity.
|
|
20
20
|
*/ function assertEnvelope(entityConstructor, body, expected) {
|
|
21
21
|
const entity = envelopeEntityName(entityConstructor);
|
|
22
22
|
if (!isEntifixEnvelope(body)) {
|
|
23
|
-
return Effect.fail(new
|
|
24
|
-
entity
|
|
25
|
-
expected,
|
|
26
|
-
body
|
|
23
|
+
return Effect.fail(new InvalidEnvelope({
|
|
24
|
+
reason: `Expected an EntifixEnvelope for "${entity}" but the payload carried no meta.type`
|
|
27
25
|
}));
|
|
28
26
|
}
|
|
29
27
|
if (body.meta.type !== expected) {
|
|
30
|
-
return Effect.fail(new
|
|
31
|
-
entity
|
|
32
|
-
expected,
|
|
33
|
-
actual: body.meta.type
|
|
28
|
+
return Effect.fail(new InvalidEnvelope({
|
|
29
|
+
reason: `Expected an EntifixEnvelope of type "${expected}" for "${entity}" but got "${body.meta.type}"`
|
|
34
30
|
}));
|
|
35
31
|
}
|
|
36
32
|
return Effect.succeed(body);
|
|
@@ -41,17 +37,13 @@ import { envelopeEntityName } from './make-envelope.js';
|
|
|
41
37
|
* whose `data` is not a serialized entity. `label` only sharpens error text.
|
|
42
38
|
*/ export function readEnvelope(body, expected, label = 'message') {
|
|
43
39
|
if (!isEntifixEnvelope(body)) {
|
|
44
|
-
return Effect.fail(new
|
|
45
|
-
label
|
|
46
|
-
expected,
|
|
47
|
-
body
|
|
40
|
+
return Effect.fail(new InvalidEnvelope({
|
|
41
|
+
reason: `Expected an EntifixEnvelope for "${label}" but the payload carried no meta.type`
|
|
48
42
|
}));
|
|
49
43
|
}
|
|
50
44
|
if (body.meta.type !== expected) {
|
|
51
|
-
return Effect.fail(new
|
|
52
|
-
label
|
|
53
|
-
expected,
|
|
54
|
-
actual: body.meta.type
|
|
45
|
+
return Effect.fail(new InvalidEnvelope({
|
|
46
|
+
reason: `Expected an EntifixEnvelope of type "${expected}" for "${label}" but got "${body.meta.type}"`
|
|
55
47
|
}));
|
|
56
48
|
}
|
|
57
49
|
return Effect.succeed(body);
|
|
@@ -60,8 +52,8 @@ export const readEntityEnvelope = (entityConstructor, body)=>Effect.gen(function
|
|
|
60
52
|
const envelope = yield* assertEnvelope(entityConstructor, body, 'entity');
|
|
61
53
|
const entity = yield* deserializeSingleEntity(entityConstructor, envelope.data);
|
|
62
54
|
if (entity === undefined) {
|
|
63
|
-
return yield* Effect.fail(new
|
|
64
|
-
|
|
55
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
56
|
+
reason: `EntifixEnvelope for "${envelopeEntityName(entityConstructor)}" carried no data`
|
|
65
57
|
}));
|
|
66
58
|
}
|
|
67
59
|
return entity;
|
|
@@ -93,8 +85,8 @@ export const readEntityPageEnvelope = (entityConstructor, body)=>Effect.gen(func
|
|
|
93
85
|
const envelope = yield* assertEnvelope(entityConstructor, body, 'entityMetadata');
|
|
94
86
|
const document = envelope.data;
|
|
95
87
|
if (!Array.isArray(document == null ? void 0 : document.actions) || !Array.isArray(document == null ? void 0 : document.useCases)) {
|
|
96
|
-
return yield* Effect.fail(new
|
|
97
|
-
|
|
88
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
89
|
+
reason: `EntifixEnvelope for "${envelopeEntityName(entityConstructor)}" carried no metadata document`
|
|
98
90
|
}));
|
|
99
91
|
}
|
|
100
92
|
return document;
|
|
@@ -113,8 +105,8 @@ const isFacetValueRow = (row)=>row != null && typeof row === 'object' && [
|
|
|
113
105
|
const counts = envelope.data;
|
|
114
106
|
const wellFormed = counts != null && typeof counts === 'object' && Object.values(counts).every((rows)=>Array.isArray(rows) && rows.every(isFacetValueRow));
|
|
115
107
|
if (!wellFormed) {
|
|
116
|
-
return yield* Effect.fail(new
|
|
117
|
-
|
|
108
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
109
|
+
reason: `EntifixEnvelope for "${envelopeEntityName(entityConstructor)}" carried no facet counts`
|
|
118
110
|
}));
|
|
119
111
|
}
|
|
120
112
|
return counts;
|
|
@@ -144,9 +136,8 @@ const isFacetValueRow = (row)=>row != null && typeof row === 'object' && [
|
|
|
144
136
|
const event = envelope.meta.event;
|
|
145
137
|
const missing = REQUIRED_EVENT_META.filter((member)=>typeof (event == null ? void 0 : event[member]) !== 'string' || event[member] === '');
|
|
146
138
|
if (missing.length > 0) {
|
|
147
|
-
return yield* Effect.fail(new
|
|
148
|
-
missing,
|
|
149
|
-
body
|
|
139
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
140
|
+
reason: `event envelope carried incomplete meta.event (missing ${missing.join(', ')})`
|
|
150
141
|
}));
|
|
151
142
|
}
|
|
152
143
|
return _extends({}, event, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/envelope/read-envelope.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n} from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type {\n EntityFacetCounts,\n EntityFacetValue,\n} from '../types/EntityFacets.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport { envelopeEntityName } from './make-envelope.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeType,\n EntifixEventMeta,\n SerializedEntityPage,\n} from './types.js';\n\n/**\n * Structural check only — `meta.type` is validated separately so a mismatch\n * reports the expected/actual types rather than a generic \"not an envelope\".\n */\nexport function isEntifixEnvelope(body: unknown): body is EntifixEnvelope {\n if (body == null || typeof body !== 'object') {\n return false;\n }\n const meta = (body as EntifixEnvelope).meta;\n return (\n meta != null && typeof meta === 'object' && typeof meta.type === 'string'\n );\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type`, failing\n * with an {@link EntifixBuildError} otherwise. A wrong shape must never\n * silently deserialize into a half-populated entity.\n */\nfunction assertEnvelope<TEntity extends Entity, TData>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n expected: EntifixEnvelopeType,\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n const entity = envelopeEntityName(entityConstructor);\n\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${entity}\" but the payload carried no meta.type`,\n undefined,\n { entity, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${entity}\" but got \"${body.meta.type}\"`,\n undefined,\n { entity, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type` without\n * requiring an entity constructor — for `command`/`transactionRecord` messages\n * whose `data` is not a serialized entity. `label` only sharpens error text.\n */\nexport function readEnvelope<TData>(\n body: unknown,\n expected: EntifixEnvelopeType,\n label = 'message',\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${label}\" but the payload carried no meta.type`,\n undefined,\n { label, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${label}\" but got \"${body.meta.type}\"`,\n undefined,\n { label, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\nexport const readEntityEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entity',\n );\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n envelope.data,\n );\n if (entity === undefined) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no data`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return entity;\n });\n\nexport const readEntityCollectionEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entityCollection',\n );\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data,\n );\n return items as TEntity[];\n });\n\nexport const readEntityPageEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<\n TEntity,\n SerializedEntityPage<TEntity>\n >(entityConstructor, body, 'entityPage');\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data?.items,\n );\n return {\n items: items as TEntity[],\n total: envelope.data?.total ?? 0,\n request: envelope.data?.request ?? {},\n } satisfies EntityPage<TEntity>;\n });\n\n/**\n * Reads a `$metadata` response back into its document.\n *\n * No deserialization step, and no defaulting of the members either: an envelope\n * whose `data` is missing a list would leave a form guessing whether \"no\n * actions\" meant \"denied\" or \"malformed\", so a broken payload fails loudly here\n * the way a half-populated entity does.\n */\nexport const readEntityMetadataEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityMetadataDocument>(\n entityConstructor,\n body,\n 'entityMetadata',\n );\n const document = envelope.data;\n if (\n !Array.isArray(document?.actions) ||\n !Array.isArray(document?.useCases)\n ) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no metadata document`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return document;\n });\n\nconst isFacetValueRow = (row: unknown): row is EntityFacetValue =>\n row != null &&\n typeof row === 'object' &&\n ['string', 'number', 'boolean'].includes(\n typeof (row as EntityFacetValue).value,\n ) &&\n typeof (row as EntityFacetValue).count === 'number';\n\n/**\n * Reads a `$facets` response back into its counts, refusing a body whose lists\n * are not `{ value, count }` rows — a panel that rendered a malformed count\n * would show a number nobody computed.\n */\nexport const readEntityFacetsEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityFacetCounts>(\n entityConstructor,\n body,\n 'entityFacets',\n );\n const counts = envelope.data;\n const wellFormed =\n counts != null &&\n typeof counts === 'object' &&\n Object.values(counts).every(\n rows => Array.isArray(rows) && rows.every(isFacetValueRow),\n );\n if (!wellFormed) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no facet counts`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return counts;\n });\n\n/** Every member `meta.event` must carry for a message to be routable at all. */\nconst REQUIRED_EVENT_META = ['name', 'id', 'source', 'at'] as const;\n\n/**\n * Reads a bus message back into a {@link DomainEvent}.\n *\n * The metadata is validated rather than defaulted, and that is the point: a\n * message missing `id` cannot be deduplicated and a message missing `name`\n * cannot be routed, so both are failures a consumer must see rather than\n * absences it silently works around. It is the same call\n * {@link readEntityMetadataEnvelope} makes — a broken payload fails loudly\n * instead of arriving half-populated.\n *\n * Note this does not validate `data`. The transport has no idea what a\n * `catalog.published` payload should look like, and inventing an opinion here\n * is how the bus would start knowing about domains.\n */\nexport function readEventEnvelope<TData = unknown>(\n body: unknown,\n): Effect.Effect<DomainEvent<TData>, EntifixBuildError> {\n return Effect.gen(function* () {\n const envelope = yield* readEnvelope<TData>(body, 'event', 'event');\n const event = envelope.meta.event;\n const missing = REQUIRED_EVENT_META.filter(\n member => typeof event?.[member] !== 'string' || event[member] === '',\n );\n if (missing.length > 0) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `event envelope carried incomplete meta.event (missing ${missing.join(\n ', ',\n )})`,\n undefined,\n { missing, body },\n ),\n );\n }\n return { ...(event as EntifixEventMeta), data: envelope.data };\n });\n}\n"],"names":["Effect","EntifixBuildError","deserializeEntityCollection","deserializeSingleEntity","envelopeEntityName","isEntifixEnvelope","body","meta","type","assertEnvelope","entityConstructor","expected","entity","fail","undefined","actual","succeed","readEnvelope","label","readEntityEnvelope","gen","envelope","data","readEntityCollectionEnvelope","items","readEntityPageEnvelope","total","request","readEntityMetadataEnvelope","document","Array","isArray","actions","useCases","isFacetValueRow","row","includes","value","count","readEntityFacetsEnvelope","counts","wellFormed","Object","values","every","rows","REQUIRED_EVENT_META","readEventEnvelope","event","missing","filter","member","length","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/envelope/read-envelope.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n} from '../entity-definition/serializer/index.js';\nimport { InvalidEnvelope } from '../failure/generic.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type {\n EntityFacetCounts,\n EntityFacetValue,\n} from '../types/EntityFacets.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport { envelopeEntityName } from './make-envelope.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeType,\n EntifixEventMeta,\n SerializedEntityPage,\n} from './types.js';\n\n/**\n * Structural check only — `meta.type` is validated separately so a mismatch\n * reports the expected/actual types rather than a generic \"not an envelope\".\n */\nexport function isEntifixEnvelope(body: unknown): body is EntifixEnvelope {\n if (body == null || typeof body !== 'object') {\n return false;\n }\n const meta = (body as EntifixEnvelope).meta;\n return (\n meta != null && typeof meta === 'object' && typeof meta.type === 'string'\n );\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type`, failing\n * with an {@link InvalidEnvelope} otherwise. A wrong shape must never\n * silently deserialize into a half-populated entity.\n */\nfunction assertEnvelope<TEntity extends Entity, TData>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n expected: EntifixEnvelopeType,\n): Effect.Effect<EntifixEnvelope<TData>, InvalidEnvelope> {\n const entity = envelopeEntityName(entityConstructor);\n\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope for \"${entity}\" but the payload carried no meta.type`,\n }),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope of type \"${expected}\" for \"${entity}\" but got \"${body.meta.type}\"`,\n }),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type` without\n * requiring an entity constructor — for `command`/`transactionRecord` messages\n * whose `data` is not a serialized entity. `label` only sharpens error text.\n */\nexport function readEnvelope<TData>(\n body: unknown,\n expected: EntifixEnvelopeType,\n label = 'message',\n): Effect.Effect<EntifixEnvelope<TData>, InvalidEnvelope> {\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope for \"${label}\" but the payload carried no meta.type`,\n }),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope of type \"${expected}\" for \"${label}\" but got \"${body.meta.type}\"`,\n }),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\nexport const readEntityEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entity',\n );\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n envelope.data,\n );\n if (entity === undefined) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no data`,\n }),\n );\n }\n return entity;\n });\n\nexport const readEntityCollectionEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entityCollection',\n );\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data,\n );\n return items as TEntity[];\n });\n\nexport const readEntityPageEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<\n TEntity,\n SerializedEntityPage<TEntity>\n >(entityConstructor, body, 'entityPage');\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data?.items,\n );\n return {\n items: items as TEntity[],\n total: envelope.data?.total ?? 0,\n request: envelope.data?.request ?? {},\n } satisfies EntityPage<TEntity>;\n });\n\n/**\n * Reads a `$metadata` response back into its document.\n *\n * No deserialization step, and no defaulting of the members either: an envelope\n * whose `data` is missing a list would leave a form guessing whether \"no\n * actions\" meant \"denied\" or \"malformed\", so a broken payload fails loudly here\n * the way a half-populated entity does.\n */\nexport const readEntityMetadataEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityMetadataDocument>(\n entityConstructor,\n body,\n 'entityMetadata',\n );\n const document = envelope.data;\n if (\n !Array.isArray(document?.actions) ||\n !Array.isArray(document?.useCases)\n ) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no metadata document`,\n }),\n );\n }\n return document;\n });\n\nconst isFacetValueRow = (row: unknown): row is EntityFacetValue =>\n row != null &&\n typeof row === 'object' &&\n ['string', 'number', 'boolean'].includes(\n typeof (row as EntityFacetValue).value,\n ) &&\n typeof (row as EntityFacetValue).count === 'number';\n\n/**\n * Reads a `$facets` response back into its counts, refusing a body whose lists\n * are not `{ value, count }` rows — a panel that rendered a malformed count\n * would show a number nobody computed.\n */\nexport const readEntityFacetsEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityFacetCounts>(\n entityConstructor,\n body,\n 'entityFacets',\n );\n const counts = envelope.data;\n const wellFormed =\n counts != null &&\n typeof counts === 'object' &&\n Object.values(counts).every(\n rows => Array.isArray(rows) && rows.every(isFacetValueRow),\n );\n if (!wellFormed) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no facet counts`,\n }),\n );\n }\n return counts;\n });\n\n/** Every member `meta.event` must carry for a message to be routable at all. */\nconst REQUIRED_EVENT_META = ['name', 'id', 'source', 'at'] as const;\n\n/**\n * Reads a bus message back into a {@link DomainEvent}.\n *\n * The metadata is validated rather than defaulted, and that is the point: a\n * message missing `id` cannot be deduplicated and a message missing `name`\n * cannot be routed, so both are failures a consumer must see rather than\n * absences it silently works around. It is the same call\n * {@link readEntityMetadataEnvelope} makes — a broken payload fails loudly\n * instead of arriving half-populated.\n *\n * Note this does not validate `data`. The transport has no idea what a\n * `catalog.published` payload should look like, and inventing an opinion here\n * is how the bus would start knowing about domains.\n */\nexport function readEventEnvelope<TData = unknown>(\n body: unknown,\n): Effect.Effect<DomainEvent<TData>, InvalidEnvelope> {\n return Effect.gen(function* () {\n const envelope = yield* readEnvelope<TData>(body, 'event', 'event');\n const event = envelope.meta.event;\n const missing = REQUIRED_EVENT_META.filter(\n member => typeof event?.[member] !== 'string' || event[member] === '',\n );\n if (missing.length > 0) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `event envelope carried incomplete meta.event (missing ${missing.join(\n ', ',\n )})`,\n }),\n );\n }\n return { ...(event as EntifixEventMeta), data: envelope.data };\n });\n}\n"],"names":["Effect","deserializeEntityCollection","deserializeSingleEntity","InvalidEnvelope","envelopeEntityName","isEntifixEnvelope","body","meta","type","assertEnvelope","entityConstructor","expected","entity","fail","reason","succeed","readEnvelope","label","readEntityEnvelope","gen","envelope","data","undefined","readEntityCollectionEnvelope","items","readEntityPageEnvelope","total","request","readEntityMetadataEnvelope","document","Array","isArray","actions","useCases","isFacetValueRow","row","includes","value","count","readEntityFacetsEnvelope","counts","wellFormed","Object","values","every","rows","REQUIRED_EVENT_META","readEventEnvelope","event","missing","filter","member","length","join"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import { type FailureCategory, type FailureFacts } from './failure.js';
|
|
3
|
+
export interface FailureOptions<Fields> {
|
|
4
|
+
/**
|
|
5
|
+
* The failure's `message`, from its fields — what a terminal shows when it is
|
|
6
|
+
* thrown and what a problem body carries as `detail`. Written for a reader:
|
|
7
|
+
* never a stack, never `String(error)`.
|
|
8
|
+
*/
|
|
9
|
+
readonly message?: (fields: Fields) => string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The definer for one category.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* class DuplicateSlug extends Failure.conflict<DuplicateSlug>()(
|
|
16
|
+
* 'DuplicateSlug',
|
|
17
|
+
* 'duplicateSlug',
|
|
18
|
+
* { slug: Schema.String },
|
|
19
|
+
* ) {}
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The curried `<Self>()` is `Schema.TaggedError`'s own shape, kept because the
|
|
23
|
+
* class has to name itself for its instances to type as itself. `code`,
|
|
24
|
+
* `category` and `family` ride on the prototype — they are facts about the
|
|
25
|
+
* class, not fields an instance is constructed with — and enter the instance
|
|
26
|
+
* type through `TaggedError`'s brand parameter, as literals, so a predicate on
|
|
27
|
+
* them narrows a union.
|
|
28
|
+
*
|
|
29
|
+
* The code is registered here, at definition. A second tag claiming it throws
|
|
30
|
+
* before anything runs.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ The registry holds the class this returns, not the subclass that extends
|
|
33
|
+
* it, so a failure decoded from a wire is an instance of the base. That is why
|
|
34
|
+
* a failure class carries fields and no behaviour — a `message` written as a
|
|
35
|
+
* getter on the subclass would vanish on decode, so it is passed here and put
|
|
36
|
+
* on the base — and why nothing matches a failure with `instanceof`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const defineFailure: <C extends FailureCategory>(category: C) => <Self = never>() => <const Tag extends string, const Code extends string, const Fields extends Schema.Struct.Fields = Record<never, never>>(tag: Tag, code: Code, fields?: Fields, options?: FailureOptions<Schema.Struct.Type<Fields>>) => [Self] extends [never] ? "Missing `Self` generic - use `class Self extends Schema.TaggedError<Self>(...)`" : Schema.Class<Self, Schema.TaggedStruct<Tag, Fields>, import("effect/Cause").YieldableError & FailureFacts<Code, C>>;
|
|
39
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/failure/define.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,YAAY,EAGlB,MAAM,cAAc,CAAC;AAGtB,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,GACvB,CAAC,SAAS,eAAe,EAAE,UAAU,CAAC,MACtC,IAAI,GAAG,KAAK,QAEX,KAAK,CAAC,GAAG,SAAS,MAAM,EACxB,KAAK,CAAC,IAAI,SAAS,MAAM,EACzB,KAAK,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAEhE,KAAK,GAAG,EACR,MAAM,IAAI,EACV,SAAS,MAAM,EACf,UAAS,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAM,qOA0CzD,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { Schema } from 'effect';
|
|
3
|
+
import { familyOf, RESERVED_FIELDS } from './failure.js';
|
|
4
|
+
import { failureTypeUri, registerFailure } from './registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* The definer for one category.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* class DuplicateSlug extends Failure.conflict<DuplicateSlug>()(
|
|
10
|
+
* 'DuplicateSlug',
|
|
11
|
+
* 'duplicateSlug',
|
|
12
|
+
* { slug: Schema.String },
|
|
13
|
+
* ) {}
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The curried `<Self>()` is `Schema.TaggedError`'s own shape, kept because the
|
|
17
|
+
* class has to name itself for its instances to type as itself. `code`,
|
|
18
|
+
* `category` and `family` ride on the prototype — they are facts about the
|
|
19
|
+
* class, not fields an instance is constructed with — and enter the instance
|
|
20
|
+
* type through `TaggedError`'s brand parameter, as literals, so a predicate on
|
|
21
|
+
* them narrows a union.
|
|
22
|
+
*
|
|
23
|
+
* The code is registered here, at definition. A second tag claiming it throws
|
|
24
|
+
* before anything runs.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ The registry holds the class this returns, not the subclass that extends
|
|
27
|
+
* it, so a failure decoded from a wire is an instance of the base. That is why
|
|
28
|
+
* a failure class carries fields and no behaviour — a `message` written as a
|
|
29
|
+
* getter on the subclass would vanish on decode, so it is passed here and put
|
|
30
|
+
* on the base — and why nothing matches a failure with `instanceof`.
|
|
31
|
+
*/ export const defineFailure = (category)=>()=>(tag, code, fields, options = {})=>{
|
|
32
|
+
const own = fields != null ? fields : {};
|
|
33
|
+
const shadowed = Object.keys(own).filter((key)=>RESERVED_FIELDS.has(key));
|
|
34
|
+
if (shadowed.length > 0) throw new Error(`failure '${tag}' declares ${shadowed.map((key)=>`'${key}'`).join(', ')}, ` + 'which the problem body reserves for itself. Rename the field.');
|
|
35
|
+
const Base = Schema.TaggedError()(tag, own);
|
|
36
|
+
// `Base` is still `TaggedError`'s conditional type here — `Self` resolves
|
|
37
|
+
// only at the `extends` site — so the prototype is reached through a cast.
|
|
38
|
+
Object.defineProperties(Base.prototype, _extends({
|
|
39
|
+
code: {
|
|
40
|
+
value: code,
|
|
41
|
+
enumerable: false
|
|
42
|
+
},
|
|
43
|
+
category: {
|
|
44
|
+
value: category,
|
|
45
|
+
enumerable: false
|
|
46
|
+
},
|
|
47
|
+
family: {
|
|
48
|
+
value: familyOf(category),
|
|
49
|
+
enumerable: false
|
|
50
|
+
}
|
|
51
|
+
}, options.message === undefined ? {} : {
|
|
52
|
+
message: {
|
|
53
|
+
get () {
|
|
54
|
+
return options.message == null ? void 0 : options.message.call(options, this);
|
|
55
|
+
},
|
|
56
|
+
enumerable: false
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
registerFailure({
|
|
60
|
+
code,
|
|
61
|
+
tag,
|
|
62
|
+
category,
|
|
63
|
+
family: familyOf(category),
|
|
64
|
+
type: failureTypeUri(code),
|
|
65
|
+
failureClass: Base
|
|
66
|
+
});
|
|
67
|
+
return Base;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=define.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/failure/define.ts"],"sourcesContent":["import { Schema } from 'effect';\n\nimport {\n type FailureCategory,\n type FailureClass,\n type FailureFacts,\n familyOf,\n RESERVED_FIELDS,\n} from './failure.js';\nimport { failureTypeUri, registerFailure } from './registry.js';\n\nexport interface FailureOptions<Fields> {\n /**\n * The failure's `message`, from its fields — what a terminal shows when it is\n * thrown and what a problem body carries as `detail`. Written for a reader:\n * never a stack, never `String(error)`.\n */\n readonly message?: (fields: Fields) => string;\n}\n\n/**\n * The definer for one category.\n *\n * ```ts\n * class DuplicateSlug extends Failure.conflict<DuplicateSlug>()(\n * 'DuplicateSlug',\n * 'duplicateSlug',\n * { slug: Schema.String },\n * ) {}\n * ```\n *\n * The curried `<Self>()` is `Schema.TaggedError`'s own shape, kept because the\n * class has to name itself for its instances to type as itself. `code`,\n * `category` and `family` ride on the prototype — they are facts about the\n * class, not fields an instance is constructed with — and enter the instance\n * type through `TaggedError`'s brand parameter, as literals, so a predicate on\n * them narrows a union.\n *\n * The code is registered here, at definition. A second tag claiming it throws\n * before anything runs.\n *\n * ⚠️ The registry holds the class this returns, not the subclass that extends\n * it, so a failure decoded from a wire is an instance of the base. That is why\n * a failure class carries fields and no behaviour — a `message` written as a\n * getter on the subclass would vanish on decode, so it is passed here and put\n * on the base — and why nothing matches a failure with `instanceof`.\n */\nexport const defineFailure =\n <C extends FailureCategory>(category: C) =>\n <Self = never>() =>\n <\n const Tag extends string,\n const Code extends string,\n const Fields extends Schema.Struct.Fields = Record<never, never>,\n >(\n tag: Tag,\n code: Code,\n fields?: Fields,\n options: FailureOptions<Schema.Struct.Type<Fields>> = {},\n ) => {\n const own = fields ?? ({} as Fields);\n const shadowed = Object.keys(own).filter(key => RESERVED_FIELDS.has(key));\n if (shadowed.length > 0)\n throw new Error(\n `failure '${tag}' declares ${shadowed.map(key => `'${key}'`).join(', ')}, ` +\n 'which the problem body reserves for itself. Rename the field.',\n );\n\n const Base = Schema.TaggedError<Self, FailureFacts<Code, C>>()(tag, own);\n // `Base` is still `TaggedError`'s conditional type here — `Self` resolves\n // only at the `extends` site — so the prototype is reached through a cast.\n Object.defineProperties(\n (Base as unknown as { prototype: object }).prototype,\n {\n code: { value: code, enumerable: false },\n category: { value: category, enumerable: false },\n family: { value: familyOf(category), enumerable: false },\n ...(options.message === undefined\n ? {}\n : {\n message: {\n get(this: Schema.Struct.Type<Fields>) {\n return options.message?.(this);\n },\n enumerable: false,\n },\n }),\n },\n );\n\n registerFailure({\n code,\n tag,\n category,\n family: familyOf(category),\n type: failureTypeUri(code),\n failureClass: Base as unknown as FailureClass,\n });\n\n return Base;\n };\n"],"names":["Schema","familyOf","RESERVED_FIELDS","failureTypeUri","registerFailure","defineFailure","category","tag","code","fields","options","own","shadowed","Object","keys","filter","key","has","length","Error","map","join","Base","TaggedError","defineProperties","prototype","value","enumerable","family","message","undefined","get","type","failureClass"],"mappings":";AAAA,SAASA,MAAM,QAAQ,SAAS;AAEhC,SAIEC,QAAQ,EACRC,eAAe,QACV,eAAe;AACtB,SAASC,cAAc,EAAEC,eAAe,QAAQ,gBAAgB;AAWhE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMC,gBACX,CAA4BC,WAC5B,IACA,CAKEC,KACAC,MACAC,QACAC,UAAsD,CAAC,CAAC;YAExD,MAAMC,MAAMF,iBAAAA,SAAW,CAAC;YACxB,MAAMG,WAAWC,OAAOC,IAAI,CAACH,KAAKI,MAAM,CAACC,CAAAA,MAAOd,gBAAgBe,GAAG,CAACD;YACpE,IAAIJ,SAASM,MAAM,GAAG,GACpB,MAAM,IAAIC,MACR,CAAC,SAAS,EAAEZ,IAAI,WAAW,EAAEK,SAASQ,GAAG,CAACJ,CAAAA,MAAO,CAAC,CAAC,EAAEA,IAAI,CAAC,CAAC,EAAEK,IAAI,CAAC,MAAM,EAAE,CAAC,GACzE;YAGN,MAAMC,OAAOtB,OAAOuB,WAAW,GAAgChB,KAAKI;YACpE,0EAA0E;YAC1E,2EAA2E;YAC3EE,OAAOW,gBAAgB,CACrB,AAACF,KAA0CG,SAAS,EACpD;gBACEjB,MAAM;oBAAEkB,OAAOlB;oBAAMmB,YAAY;gBAAM;gBACvCrB,UAAU;oBAAEoB,OAAOpB;oBAAUqB,YAAY;gBAAM;gBAC/CC,QAAQ;oBAAEF,OAAOzB,SAASK;oBAAWqB,YAAY;gBAAM;eACnDjB,QAAQmB,OAAO,KAAKC,YACpB,CAAC,IACD;gBACED,SAAS;oBACPE;wBACE,OAAOrB,QAAQmB,OAAO,oBAAfnB,QAAQmB,OAAO,MAAfnB,SAAkB,IAAI;oBAC/B;oBACAiB,YAAY;gBACd;YACF;YAIRvB,gBAAgB;gBACdI;gBACAD;gBACAD;gBACAsB,QAAQ3B,SAASK;gBACjB0B,MAAM7B,eAAeK;gBACrByB,cAAcX;YAChB;YAEA,OAAOA;QACT,EAAE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Schema } from 'effect';
|
|
2
|
+
import type { EntifixError } from '../base-entities/entifix-error/entifix-error.js';
|
|
3
|
+
/**
|
|
4
|
+
* The two families every failure belongs to
|
|
5
|
+
* ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).
|
|
6
|
+
*
|
|
7
|
+
* - `business` — an answer. The request was understood and the answer is no.
|
|
8
|
+
* Never retried, never trips a breaker.
|
|
9
|
+
* - `technical` — a non-answer. Something stopped the request being answered,
|
|
10
|
+
* and the layer directly above may retry it.
|
|
11
|
+
*/
|
|
12
|
+
export type FailureFamily = 'business' | 'technical';
|
|
13
|
+
/** The business categories, each with the status it answers by default. */
|
|
14
|
+
export declare const BUSINESS_CATEGORIES: {
|
|
15
|
+
readonly invalid: 400;
|
|
16
|
+
readonly unauthenticated: 401;
|
|
17
|
+
readonly forbidden: 403;
|
|
18
|
+
readonly notFound: 404;
|
|
19
|
+
readonly conflict: 409;
|
|
20
|
+
readonly declined: 422;
|
|
21
|
+
};
|
|
22
|
+
/** The technical categories, each with the status it answers by default. */
|
|
23
|
+
export declare const TECHNICAL_CATEGORIES: {
|
|
24
|
+
readonly contention: 409;
|
|
25
|
+
readonly exhausted: 429;
|
|
26
|
+
readonly unavailable: 503;
|
|
27
|
+
readonly timeout: 504;
|
|
28
|
+
readonly protocol: 502;
|
|
29
|
+
readonly internal: 500;
|
|
30
|
+
};
|
|
31
|
+
export type BusinessCategory = keyof typeof BUSINESS_CATEGORIES;
|
|
32
|
+
export type TechnicalCategory = keyof typeof TECHNICAL_CATEGORIES;
|
|
33
|
+
/**
|
|
34
|
+
* The closed set. Every layer branches on it — the shell's status and log
|
|
35
|
+
* tables, the bus's poison rule, the client's inline-versus-toast choice — so
|
|
36
|
+
* it stays small enough for each of them to be exhaustive over. Tags stay open.
|
|
37
|
+
*/
|
|
38
|
+
export type FailureCategory = BusinessCategory | TechnicalCategory;
|
|
39
|
+
export type FamilyOf<C extends FailureCategory> = C extends BusinessCategory ? 'business' : 'technical';
|
|
40
|
+
export declare const FAILURE_CATEGORIES: readonly FailureCategory[];
|
|
41
|
+
export declare const familyOf: <C extends FailureCategory>(category: C) => FamilyOf<C>;
|
|
42
|
+
/** The status a category answers with unless a host overrides its tag. */
|
|
43
|
+
export declare const defaultStatus: (category: FailureCategory) => number;
|
|
44
|
+
/**
|
|
45
|
+
* What every failure carries beyond its own fields. `code` is the permanent,
|
|
46
|
+
* wire-visible key a client translates; `category` and `family` are what every
|
|
47
|
+
* layer above decides by.
|
|
48
|
+
*/
|
|
49
|
+
export interface FailureFacts<Code extends string = string, C extends FailureCategory = FailureCategory> {
|
|
50
|
+
readonly code: Code;
|
|
51
|
+
readonly category: C;
|
|
52
|
+
readonly family: FamilyOf<C>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Any failure the framework or a host raises: a `Schema.TaggedError` carrying
|
|
56
|
+
* its code, category and family.
|
|
57
|
+
*/
|
|
58
|
+
export interface EntifixFailure<Tag extends string = string, Code extends string = string, C extends FailureCategory = FailureCategory> extends FailureFacts<Code, C> {
|
|
59
|
+
readonly _tag: Tag;
|
|
60
|
+
readonly message: string;
|
|
61
|
+
}
|
|
62
|
+
/** A failure class, as the registry and the problem codec hold it. */
|
|
63
|
+
export interface FailureClass {
|
|
64
|
+
new (...args: never[]): EntifixFailure;
|
|
65
|
+
readonly identifier: string;
|
|
66
|
+
readonly fields: Schema.Struct.Fields;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Members of the problem body that a failure's own fields must not shadow,
|
|
70
|
+
* because the decoder reads them back as the envelope rather than as fields.
|
|
71
|
+
*/
|
|
72
|
+
export declare const RESERVED_FIELDS: ReadonlySet<string>;
|
|
73
|
+
/**
|
|
74
|
+
* What a port or a hook fails with while the framework moves from the
|
|
75
|
+
* `EntifixError` class tree to typed failures: either one. Deleted with the
|
|
76
|
+
* class tree, in the release that removes it.
|
|
77
|
+
*/
|
|
78
|
+
export type TransitionalFailure = EntifixError | EntifixFailure;
|
|
79
|
+
//# sourceMappingURL=failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure.d.ts","sourceRoot":"","sources":["../../src/failure/failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iDAAiD,CAAC;AAEpF;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,WAAW,CAAC;AAErD,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB;;;;;;;CAOtB,CAAC;AAEX,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB;;;;;;;CAOvB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAChE,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAElE;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAEnE,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,eAAe,IAAI,CAAC,SAAS,gBAAgB,GACxE,UAAU,GACV,WAAW,CAAC;AAEhB,eAAO,MAAM,kBAAkB,EAAE,SAAS,eAAe,EAGxD,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,eAAe,EAAE,UAAU,CAAC,KAAG,QAAQ,CAAC,CAAC,CACC,CAAC;AAE9E,0EAA0E;AAC1E,eAAO,MAAM,aAAa,GAAI,UAAU,eAAe,KAAG,MAGD,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAC3B,IAAI,SAAS,MAAM,GAAG,MAAM,EAC5B,CAAC,SAAS,eAAe,GAAG,eAAe;IAE3C,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc,CAC7B,GAAG,SAAS,MAAM,GAAG,MAAM,EAC3B,IAAI,SAAS,MAAM,GAAG,MAAM,EAC5B,CAAC,SAAS,eAAe,GAAG,eAAe,CAC3C,SAAQ,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;CACvC;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAW9C,CAAC;AAEH;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,GAAG,cAAc,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The business categories, each with the status it answers by default. */ export const BUSINESS_CATEGORIES = {
|
|
2
|
+
invalid: 400,
|
|
3
|
+
unauthenticated: 401,
|
|
4
|
+
forbidden: 403,
|
|
5
|
+
notFound: 404,
|
|
6
|
+
conflict: 409,
|
|
7
|
+
declined: 422
|
|
8
|
+
};
|
|
9
|
+
/** The technical categories, each with the status it answers by default. */ export const TECHNICAL_CATEGORIES = {
|
|
10
|
+
contention: 409,
|
|
11
|
+
exhausted: 429,
|
|
12
|
+
unavailable: 503,
|
|
13
|
+
timeout: 504,
|
|
14
|
+
protocol: 502,
|
|
15
|
+
internal: 500
|
|
16
|
+
};
|
|
17
|
+
export const FAILURE_CATEGORIES = [
|
|
18
|
+
...Object.keys(BUSINESS_CATEGORIES),
|
|
19
|
+
...Object.keys(TECHNICAL_CATEGORIES)
|
|
20
|
+
];
|
|
21
|
+
export const familyOf = (category)=>category in BUSINESS_CATEGORIES ? 'business' : 'technical';
|
|
22
|
+
/** The status a category answers with unless a host overrides its tag. */ export const defaultStatus = (category)=>category in BUSINESS_CATEGORIES ? BUSINESS_CATEGORIES[category] : TECHNICAL_CATEGORIES[category];
|
|
23
|
+
/**
|
|
24
|
+
* Members of the problem body that a failure's own fields must not shadow,
|
|
25
|
+
* because the decoder reads them back as the envelope rather than as fields.
|
|
26
|
+
*/ export const RESERVED_FIELDS = new Set([
|
|
27
|
+
'type',
|
|
28
|
+
'title',
|
|
29
|
+
'status',
|
|
30
|
+
'detail',
|
|
31
|
+
'instance',
|
|
32
|
+
'code',
|
|
33
|
+
'category',
|
|
34
|
+
'family',
|
|
35
|
+
'traceId',
|
|
36
|
+
'retryAfter'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/failure/failure.ts"],"sourcesContent":["import type { Schema } from 'effect';\n\nimport type { EntifixError } from '../base-entities/entifix-error/entifix-error.js';\n\n/**\n * The two families every failure belongs to\n * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).\n *\n * - `business` — an answer. The request was understood and the answer is no.\n * Never retried, never trips a breaker.\n * - `technical` — a non-answer. Something stopped the request being answered,\n * and the layer directly above may retry it.\n */\nexport type FailureFamily = 'business' | 'technical';\n\n/** The business categories, each with the status it answers by default. */\nexport const BUSINESS_CATEGORIES = {\n invalid: 400,\n unauthenticated: 401,\n forbidden: 403,\n notFound: 404,\n conflict: 409,\n declined: 422,\n} as const;\n\n/** The technical categories, each with the status it answers by default. */\nexport const TECHNICAL_CATEGORIES = {\n contention: 409,\n exhausted: 429,\n unavailable: 503,\n timeout: 504,\n protocol: 502,\n internal: 500,\n} as const;\n\nexport type BusinessCategory = keyof typeof BUSINESS_CATEGORIES;\nexport type TechnicalCategory = keyof typeof TECHNICAL_CATEGORIES;\n\n/**\n * The closed set. Every layer branches on it — the shell's status and log\n * tables, the bus's poison rule, the client's inline-versus-toast choice — so\n * it stays small enough for each of them to be exhaustive over. Tags stay open.\n */\nexport type FailureCategory = BusinessCategory | TechnicalCategory;\n\nexport type FamilyOf<C extends FailureCategory> = C extends BusinessCategory\n ? 'business'\n : 'technical';\n\nexport const FAILURE_CATEGORIES: readonly FailureCategory[] = [\n ...(Object.keys(BUSINESS_CATEGORIES) as BusinessCategory[]),\n ...(Object.keys(TECHNICAL_CATEGORIES) as TechnicalCategory[]),\n];\n\nexport const familyOf = <C extends FailureCategory>(category: C): FamilyOf<C> =>\n (category in BUSINESS_CATEGORIES ? 'business' : 'technical') as FamilyOf<C>;\n\n/** The status a category answers with unless a host overrides its tag. */\nexport const defaultStatus = (category: FailureCategory): number =>\n category in BUSINESS_CATEGORIES\n ? BUSINESS_CATEGORIES[category as BusinessCategory]\n : TECHNICAL_CATEGORIES[category as TechnicalCategory];\n\n/**\n * What every failure carries beyond its own fields. `code` is the permanent,\n * wire-visible key a client translates; `category` and `family` are what every\n * layer above decides by.\n */\nexport interface FailureFacts<\n Code extends string = string,\n C extends FailureCategory = FailureCategory,\n> {\n readonly code: Code;\n readonly category: C;\n readonly family: FamilyOf<C>;\n}\n\n/**\n * Any failure the framework or a host raises: a `Schema.TaggedError` carrying\n * its code, category and family.\n */\nexport interface EntifixFailure<\n Tag extends string = string,\n Code extends string = string,\n C extends FailureCategory = FailureCategory,\n> extends FailureFacts<Code, C> {\n readonly _tag: Tag;\n readonly message: string;\n}\n\n/** A failure class, as the registry and the problem codec hold it. */\nexport interface FailureClass {\n new (...args: never[]): EntifixFailure;\n readonly identifier: string;\n readonly fields: Schema.Struct.Fields;\n}\n\n/**\n * Members of the problem body that a failure's own fields must not shadow,\n * because the decoder reads them back as the envelope rather than as fields.\n */\nexport const RESERVED_FIELDS: ReadonlySet<string> = new Set([\n 'type',\n 'title',\n 'status',\n 'detail',\n 'instance',\n 'code',\n 'category',\n 'family',\n 'traceId',\n 'retryAfter',\n]);\n\n/**\n * What a port or a hook fails with while the framework moves from the\n * `EntifixError` class tree to typed failures: either one. Deleted with the\n * class tree, in the release that removes it.\n */\nexport type TransitionalFailure = EntifixError | EntifixFailure;\n"],"names":["BUSINESS_CATEGORIES","invalid","unauthenticated","forbidden","notFound","conflict","declined","TECHNICAL_CATEGORIES","contention","exhausted","unavailable","timeout","protocol","internal","FAILURE_CATEGORIES","Object","keys","familyOf","category","defaultStatus","RESERVED_FIELDS","Set"],"mappings":"AAeA,yEAAyE,GACzE,OAAO,MAAMA,sBAAsB;IACjCC,SAAS;IACTC,iBAAiB;IACjBC,WAAW;IACXC,UAAU;IACVC,UAAU;IACVC,UAAU;AACZ,EAAW;AAEX,0EAA0E,GAC1E,OAAO,MAAMC,uBAAuB;IAClCC,YAAY;IACZC,WAAW;IACXC,aAAa;IACbC,SAAS;IACTC,UAAU;IACVC,UAAU;AACZ,EAAW;AAgBX,OAAO,MAAMC,qBAAiD;OACxDC,OAAOC,IAAI,CAAChB;OACZe,OAAOC,IAAI,CAACT;CACjB,CAAC;AAEF,OAAO,MAAMU,WAAW,CAA4BC,WACjDA,YAAYlB,sBAAsB,aAAa,YAA4B;AAE9E,wEAAwE,GACxE,OAAO,MAAMmB,gBAAgB,CAACD,WAC5BA,YAAYlB,sBACRA,mBAAmB,CAACkB,SAA6B,GACjDX,oBAAoB,CAACW,SAA8B,CAAC;AAoC1D;;;CAGC,GACD,OAAO,MAAME,kBAAuC,IAAIC,IAAI;IAC1D;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,EAAE"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The failures every host shares — the ones a framework route, an adapter or a
|
|
4
|
+
* transport raises without knowing the domain it serves. A domain names its
|
|
5
|
+
* own refusals beside these, through the same definers.
|
|
6
|
+
*
|
|
7
|
+
* Each code is permanent: a host's message catalog keys on it.
|
|
8
|
+
*/
|
|
9
|
+
/** One field a request got wrong. `pointer` locates it, `code` names the rule. */
|
|
10
|
+
export declare const FieldProblem: Schema.Struct<{
|
|
11
|
+
readonly pointer: Schema.String;
|
|
12
|
+
readonly detail: Schema.String;
|
|
13
|
+
readonly code: Schema.String;
|
|
14
|
+
}>;
|
|
15
|
+
export type FieldProblem = typeof FieldProblem.Type;
|
|
16
|
+
declare const Invalid_base: Schema.Class<Invalid, Schema.TaggedStruct<"Invalid", {
|
|
17
|
+
readonly errors: Schema.$Array<Schema.Struct<{
|
|
18
|
+
readonly pointer: Schema.String;
|
|
19
|
+
readonly detail: Schema.String;
|
|
20
|
+
readonly code: Schema.String;
|
|
21
|
+
}>>;
|
|
22
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"invalid", "invalid">>;
|
|
23
|
+
/** The request breaks rules on its own; every broken one is listed. */
|
|
24
|
+
export declare class Invalid extends Invalid_base {
|
|
25
|
+
}
|
|
26
|
+
declare const InvalidQuery_base: Schema.Class<InvalidQuery, Schema.TaggedStruct<"InvalidQuery", {
|
|
27
|
+
readonly reason: Schema.String;
|
|
28
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"invalidQuery", "invalid">>;
|
|
29
|
+
/** A list, filter or sort the query language cannot accept. */
|
|
30
|
+
export declare class InvalidQuery extends InvalidQuery_base {
|
|
31
|
+
}
|
|
32
|
+
declare const InvalidEnvelope_base: Schema.Class<InvalidEnvelope, Schema.TaggedStruct<"InvalidEnvelope", {
|
|
33
|
+
readonly reason: Schema.String;
|
|
34
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"invalidEnvelope", "invalid">>;
|
|
35
|
+
/**
|
|
36
|
+
* A payload that is not the envelope its reader expected. The sender's fault —
|
|
37
|
+
* a `400` at an HTTP edge and poison on a bus, where redelivering the same
|
|
38
|
+
* bytes produces the same refusal.
|
|
39
|
+
*/
|
|
40
|
+
export declare class InvalidEnvelope extends InvalidEnvelope_base {
|
|
41
|
+
}
|
|
42
|
+
declare const Unauthenticated_base: Schema.Class<Unauthenticated, Schema.TaggedStruct<"Unauthenticated", Record<never, never>>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"unauthenticated", "unauthenticated">>;
|
|
43
|
+
/** No credential, or one that does not verify. */
|
|
44
|
+
export declare class Unauthenticated extends Unauthenticated_base {
|
|
45
|
+
}
|
|
46
|
+
declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
|
|
47
|
+
readonly permission: Schema.optionalKey<Schema.String>;
|
|
48
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"forbidden", "forbidden">>;
|
|
49
|
+
/** A verified caller without the grant. */
|
|
50
|
+
export declare class Forbidden extends Forbidden_base {
|
|
51
|
+
}
|
|
52
|
+
declare const NotFound_base: Schema.Class<NotFound, Schema.TaggedStruct<"NotFound", {
|
|
53
|
+
readonly entity: Schema.optionalKey<Schema.String>;
|
|
54
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"notFound", "notFound">>;
|
|
55
|
+
/** The addressed record does not exist. */
|
|
56
|
+
export declare class NotFound extends NotFound_base {
|
|
57
|
+
}
|
|
58
|
+
declare const DuplicateKey_base: Schema.Class<DuplicateKey, Schema.TaggedStruct<"DuplicateKey", {
|
|
59
|
+
readonly key: Schema.optionalKey<Schema.String>;
|
|
60
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"duplicateKey", "conflict">>;
|
|
61
|
+
/** A write that would duplicate a value the store declares unique. */
|
|
62
|
+
export declare class DuplicateKey extends DuplicateKey_base {
|
|
63
|
+
}
|
|
64
|
+
declare const Declined_base: Schema.Class<Declined, Schema.TaggedStruct<"Declined", {
|
|
65
|
+
readonly reason: Schema.optionalKey<Schema.String>;
|
|
66
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"declined", "declined">>;
|
|
67
|
+
/** A third party, or a rule over other records, said no. */
|
|
68
|
+
export declare class Declined extends Declined_base {
|
|
69
|
+
}
|
|
70
|
+
declare const Contention_base: Schema.Class<Contention, Schema.TaggedStruct<"Contention", Record<never, never>>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"contention", "contention">>;
|
|
71
|
+
/** Lost a race that a retry can win — a claim taken, a transaction aborted. */
|
|
72
|
+
export declare class Contention extends Contention_base {
|
|
73
|
+
}
|
|
74
|
+
declare const Overloaded_base: Schema.Class<Overloaded, Schema.TaggedStruct<"Overloaded", {
|
|
75
|
+
readonly retryAfterSeconds: Schema.optionalKey<Schema.Number>;
|
|
76
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"overloaded", "exhausted">>;
|
|
77
|
+
/** The dependency asked for less load. Stops retries wherever it appears. */
|
|
78
|
+
export declare class Overloaded extends Overloaded_base {
|
|
79
|
+
}
|
|
80
|
+
declare const Unavailable_base: Schema.Class<Unavailable, Schema.TaggedStruct<"Unavailable", {
|
|
81
|
+
readonly dependency: Schema.optionalKey<Schema.String>;
|
|
82
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"unavailable", "unavailable">>;
|
|
83
|
+
/** A dependency could not be reached. */
|
|
84
|
+
export declare class Unavailable extends Unavailable_base {
|
|
85
|
+
}
|
|
86
|
+
declare const UpstreamUnavailable_base: Schema.Class<UpstreamUnavailable, Schema.TaggedStruct<"UpstreamUnavailable", {
|
|
87
|
+
readonly upstream: Schema.optionalKey<Schema.String>;
|
|
88
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"upstreamUnavailable", "unavailable">>;
|
|
89
|
+
/** The service a proxy forwards to could not be reached. */
|
|
90
|
+
export declare class UpstreamUnavailable extends UpstreamUnavailable_base {
|
|
91
|
+
}
|
|
92
|
+
declare const Timeout_base: Schema.Class<Timeout, Schema.TaggedStruct<"Timeout", {
|
|
93
|
+
readonly dependency: Schema.optionalKey<Schema.String>;
|
|
94
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"timeout", "timeout">>;
|
|
95
|
+
/** A dependency did not answer in time. */
|
|
96
|
+
export declare class Timeout extends Timeout_base {
|
|
97
|
+
}
|
|
98
|
+
declare const ProtocolFailure_base: Schema.Class<ProtocolFailure, Schema.TaggedStruct<"ProtocolFailure", {
|
|
99
|
+
readonly reason: Schema.String;
|
|
100
|
+
readonly upstreamStatus: Schema.optionalKey<Schema.Number>;
|
|
101
|
+
}>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"protocol", "protocol">>;
|
|
102
|
+
/**
|
|
103
|
+
* A peer answered something that does not decode — a body that is not a
|
|
104
|
+
* problem, a code nothing registered, a document that does not match its
|
|
105
|
+
* schema.
|
|
106
|
+
*/
|
|
107
|
+
export declare class ProtocolFailure extends ProtocolFailure_base {
|
|
108
|
+
}
|
|
109
|
+
declare const Internal_base: Schema.Class<Internal, Schema.TaggedStruct<"Internal", Record<never, never>>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"internal", "internal">>;
|
|
110
|
+
/** A bug, surfaced as a value. Never carries the bug's text onto a wire. */
|
|
111
|
+
export declare class Internal extends Internal_base {
|
|
112
|
+
}
|
|
113
|
+
declare const IdempotencyKeyMissing_base: Schema.Class<IdempotencyKeyMissing, Schema.TaggedStruct<"IdempotencyKeyMissing", Record<never, never>>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"idempotencyKeyMissing", "invalid">>;
|
|
114
|
+
/** An idempotent route was called without its `Idempotency-Key`. */
|
|
115
|
+
export declare class IdempotencyKeyMissing extends IdempotencyKeyMissing_base {
|
|
116
|
+
}
|
|
117
|
+
declare const IdempotencyKeyReused_base: Schema.Class<IdempotencyKeyReused, Schema.TaggedStruct<"IdempotencyKeyReused", Record<never, never>>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"idempotencyKeyReused", "declined">>;
|
|
118
|
+
/** One `Idempotency-Key` arrived with two different payloads. */
|
|
119
|
+
export declare class IdempotencyKeyReused extends IdempotencyKeyReused_base {
|
|
120
|
+
}
|
|
121
|
+
declare const IdempotencyKeyInFlight_base: Schema.Class<IdempotencyKeyInFlight, Schema.TaggedStruct<"IdempotencyKeyInFlight", Record<never, never>>, import("effect/Cause").YieldableError & import("./failure.js").FailureFacts<"idempotencyKeyInFlight", "contention">>;
|
|
122
|
+
/** The first request under this `Idempotency-Key` has not finished. */
|
|
123
|
+
export declare class IdempotencyKeyInFlight extends IdempotencyKeyInFlight_base {
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What a store adapter fails with — a datastore, a cache, a bus: the value it
|
|
127
|
+
* guarded already exists, someone else got there first, it could not be
|
|
128
|
+
* reached, it did not answer in time, or it answered something unreadable.
|
|
129
|
+
* A port that talks to storage names this as its failure.
|
|
130
|
+
*/
|
|
131
|
+
export type StoreFailure = DuplicateKey | Contention | Unavailable | Timeout | ProtocolFailure;
|
|
132
|
+
/**
|
|
133
|
+
* The technical part of {@link StoreFailure}: the store failing to answer, as
|
|
134
|
+
* opposed to answering no. A unit of work rolls back and surfaces these; a
|
|
135
|
+
* business failure — a `DuplicateKey` included — is its abort.
|
|
136
|
+
*/
|
|
137
|
+
export type StoreFault = Exclude<StoreFailure, DuplicateKey>;
|
|
138
|
+
/** Whether `value` is a {@link StoreFault}, read by tag. */
|
|
139
|
+
export declare const isStoreFault: (value: unknown) => value is StoreFault;
|
|
140
|
+
export {};
|
|
141
|
+
//# sourceMappingURL=generic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generic.d.ts","sourceRoot":"","sources":["../../src/failure/generic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC;;;;;;GAMG;AAEH,kFAAkF;AAClF,eAAO,MAAM,YAAY;;;;EAIvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAC;;;;;;;;AAEpD,uEAAuE;AACvE,qBAAa,OAAQ,SAAQ,YAI5B;CAAG;;;;AAEJ,+DAA+D;AAC/D,qBAAa,YAAa,SAAQ,iBAKjC;CAAG;;;;AAEJ;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,oBAOpC;CAAG;;AAEJ,kDAAkD;AAClD,qBAAa,eAAgB,SAAQ,oBAEqB;CAAG;;;;AAE7D,2CAA2C;AAC3C,qBAAa,SAAU,SAAQ,cAM9B;CAAG;;;;AAEJ,2CAA2C;AAC3C,qBAAa,QAAS,SAAQ,aAI7B;CAAG;;;;AAEJ,sEAAsE;AACtE,qBAAa,YAAa,SAAQ,iBAIjC;CAAG;;;;AAEJ,4DAA4D;AAC5D,qBAAa,QAAS,SAAQ,aAK7B;CAAG;;AAEJ,+EAA+E;AAC/E,qBAAa,UAAW,SAAQ,eAG/B;CAAG;;;;AAEJ,6EAA6E;AAC7E,qBAAa,UAAW,SAAQ,eAI/B;CAAG;;;;AAEJ,yCAAyC;AACzC,qBAAa,WAAY,SAAQ,gBAIhC;CAAG;;;;AAEJ,4DAA4D;AAC5D,qBAAa,mBAAoB,SAAQ,wBAIvC;CAAG;;;;AAEL,2CAA2C;AAC3C,qBAAa,OAAQ,SAAQ,YAI5B;CAAG;;;;;AAEJ;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,oBAUpC;CAAG;;AAEJ,4EAA4E;AAC5E,qBAAa,QAAS,SAAQ,aAG7B;CAAG;;AAEJ,oEAAoE;AACpE,qBAAa,qBAAsB,SAAQ,0BAEiC;CAAG;;AAE/E,iEAAiE;AACjE,qBAAa,oBAAqB,SAAQ,yBAE+B;CAAG;;AAE5E,uEAAuE;AACvE,qBAAa,sBAAuB,SAAQ,2BAK3C;CAAG;AAEJ;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GACtB,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,GAAG,eAAe,CAAC;AAEtE;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;AAS7D,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,UAGsB,CAAC"}
|