@entifix/mongo 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/adapters/filter-translator.d.ts +1 -1
- package/dist/adapters/filter-translator.d.ts.map +1 -1
- package/dist/adapters/filter-translator.js +6 -5
- package/dist/adapters/filter-translator.js.map +1 -1
- package/dist/adapters/make-mongo-facet-counter.d.ts +18 -0
- package/dist/adapters/make-mongo-facet-counter.d.ts.map +1 -0
- package/dist/adapters/make-mongo-facet-counter.js +76 -0
- package/dist/adapters/make-mongo-facet-counter.js.map +1 -0
- package/dist/adapters/make-mongo-link-resolver.d.ts.map +1 -1
- package/dist/adapters/make-mongo-link-resolver.js +2 -4
- package/dist/adapters/make-mongo-link-resolver.js.map +1 -1
- package/dist/adapters/make-mongo-repository.d.ts +5 -3
- package/dist/adapters/make-mongo-repository.d.ts.map +1 -1
- package/dist/adapters/make-mongo-repository.js +13 -20
- package/dist/adapters/make-mongo-repository.js.map +1 -1
- package/dist/adapters/mongo-facet-counter-factory.d.ts +18 -0
- package/dist/adapters/mongo-facet-counter-factory.d.ts.map +1 -0
- package/dist/adapters/mongo-facet-counter-factory.js +29 -0
- package/dist/adapters/mongo-facet-counter-factory.js.map +1 -0
- package/dist/adapters/mongo-repository-factory.d.ts.map +1 -1
- package/dist/adapters/mongo-repository-factory.js +2 -4
- package/dist/adapters/mongo-repository-factory.js.map +1 -1
- package/dist/errors/translate-mongo-error.d.ts +30 -0
- package/dist/errors/translate-mongo-error.d.ts.map +1 -0
- package/dist/errors/translate-mongo-error.js +74 -0
- package/dist/errors/translate-mongo-error.js.map +1 -0
- package/dist/inbox/store.d.ts +1 -2
- package/dist/inbox/store.d.ts.map +1 -1
- package/dist/inbox/store.js +3 -8
- package/dist/inbox/store.js.map +1 -1
- 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/dist/mongo-database/mongo-database.d.ts +3 -3
- package/dist/mongo-database/mongo-database.d.ts.map +1 -1
- package/dist/mongo-database/mongo-database.js +2 -2
- package/dist/mongo-database/mongo-database.js.map +1 -1
- package/dist/mongo-database/mongo-tenant-directory.d.ts.map +1 -1
- package/dist/mongo-database/mongo-tenant-directory.js +2 -4
- package/dist/mongo-database/mongo-tenant-directory.js.map +1 -1
- package/dist/mongo-database/mongo-tenant-resolver.d.ts.map +1 -1
- package/dist/mongo-database/mongo-tenant-resolver.js +18 -7
- package/dist/mongo-database/mongo-tenant-resolver.js.map +1 -1
- package/dist/outbox/relay.d.ts +50 -9
- package/dist/outbox/relay.d.ts.map +1 -1
- package/dist/outbox/relay.js +67 -26
- package/dist/outbox/relay.js.map +1 -1
- package/dist/outbox/store.d.ts +3 -3
- package/dist/outbox/store.d.ts.map +1 -1
- package/dist/outbox/store.js +8 -22
- package/dist/outbox/store.js.map +1 -1
- package/dist/saga/store.d.ts +3 -4
- package/dist/saga/store.d.ts.map +1 -1
- package/dist/saga/store.js +10 -15
- package/dist/saga/store.js.map +1 -1
- package/dist/unit-of-work/command-inbox.d.ts +2 -3
- package/dist/unit-of-work/command-inbox.d.ts.map +1 -1
- package/dist/unit-of-work/command-inbox.js +3 -7
- package/dist/unit-of-work/command-inbox.js.map +1 -1
- package/dist/unit-of-work/mongo-unit-of-work.d.ts.map +1 -1
- package/dist/unit-of-work/mongo-unit-of-work.js +30 -26
- package/dist/unit-of-work/mongo-unit-of-work.js.map +1 -1
- package/package.json +8 -8
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { Contention, DuplicateKey, isFailure, isStoreFault, ProtocolFailure, Timeout, Unavailable } from '@entifix/core';
|
|
3
|
+
const shape = (error)=>typeof error === 'object' && error !== null ? error : {};
|
|
4
|
+
const labelled = (error, label)=>{
|
|
5
|
+
var _ref, _ref1;
|
|
6
|
+
var _error_errorLabels;
|
|
7
|
+
return (_ref = (_ref1 = error.hasErrorLabel == null ? void 0 : error.hasErrorLabel.call(error, label)) != null ? _ref1 : (_error_errorLabels = error.errorLabels) == null ? void 0 : _error_errorLabels.includes(label)) != null ? _ref : false;
|
|
8
|
+
};
|
|
9
|
+
/** Whether a server label is on `error` or in its cause chain. */ export const mongoErrorLabelled = (error, label)=>{
|
|
10
|
+
let current = error;
|
|
11
|
+
for(let depth = 0; current != null && depth < 5; depth += 1){
|
|
12
|
+
if (labelled(shape(current), label)) return true;
|
|
13
|
+
current = shape(current).cause;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
const DUPLICATE_KEY = 11000;
|
|
18
|
+
const WRITE_CONFLICT = 112;
|
|
19
|
+
const MAX_TIME_EXPIRED = 50;
|
|
20
|
+
const UNREACHABLE = new Set([
|
|
21
|
+
'MongoNetworkError',
|
|
22
|
+
'MongoServerSelectionError',
|
|
23
|
+
'MongoNotConnectedError',
|
|
24
|
+
'MongoTopologyClosedError',
|
|
25
|
+
'MongoServerClosedError',
|
|
26
|
+
'MongoPoolClosedError'
|
|
27
|
+
]);
|
|
28
|
+
const isDuplicate = (error)=>{
|
|
29
|
+
var _ref;
|
|
30
|
+
var _error_writeErrors;
|
|
31
|
+
return error.code === DUPLICATE_KEY || ((_ref = (_error_writeErrors = error.writeErrors) == null ? void 0 : _error_writeErrors.some((write)=>write.code === DUPLICATE_KEY)) != null ? _ref : false);
|
|
32
|
+
};
|
|
33
|
+
const duplicatedKey = (error)=>typeof error.keyPattern === 'object' && error.keyPattern !== null ? Object.keys(error.keyPattern).join(',') : undefined;
|
|
34
|
+
export const translateMongoError = (error, context = {})=>{
|
|
35
|
+
var _context_dependency;
|
|
36
|
+
// Already translated, by an inner call: pass it through as it is.
|
|
37
|
+
if (isFailure(error)) return error;
|
|
38
|
+
const dependency = (_context_dependency = context.dependency) != null ? _context_dependency : 'mongo';
|
|
39
|
+
const driver = shape(error);
|
|
40
|
+
if (isDuplicate(driver)) {
|
|
41
|
+
if (context.duplicate === 'contention') return new Contention();
|
|
42
|
+
const key = duplicatedKey(driver);
|
|
43
|
+
return new DuplicateKey(key === undefined ? {} : {
|
|
44
|
+
key
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (driver.code === WRITE_CONFLICT || mongoErrorLabelled(error, 'TransientTransactionError')) return new Contention();
|
|
48
|
+
if (driver.code === MAX_TIME_EXPIRED || driver.name === 'MongoNetworkTimeoutError' || driver.name === 'MongoOperationTimeoutError') return new Timeout({
|
|
49
|
+
dependency
|
|
50
|
+
});
|
|
51
|
+
if (driver.name === 'BSONError') return new ProtocolFailure({
|
|
52
|
+
reason: `${dependency} answered a document that does not decode`
|
|
53
|
+
});
|
|
54
|
+
if (typeof driver.name === 'string' && UNREACHABLE.has(driver.name)) return new Unavailable({
|
|
55
|
+
dependency
|
|
56
|
+
});
|
|
57
|
+
// Anything else the driver raised is still the store failing to answer,
|
|
58
|
+
// which is what the class tree called every one of these.
|
|
59
|
+
return new Unavailable({
|
|
60
|
+
dependency
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* A driver error as the store's own fault, for the bookkeeping a mechanism
|
|
65
|
+
* does around a caller's work — a claim, a commit, a remembered answer. A
|
|
66
|
+
* duplicate key there means a race lost, never an answer for the caller.
|
|
67
|
+
*/ export const translateMongoFault = (error, context = {})=>{
|
|
68
|
+
const failure = translateMongoError(error, _extends({}, context, {
|
|
69
|
+
duplicate: 'contention'
|
|
70
|
+
}));
|
|
71
|
+
return isStoreFault(failure) ? failure : new Contention();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=translate-mongo-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors/translate-mongo-error.ts"],"sourcesContent":["import {\n Contention,\n DuplicateKey,\n isFailure,\n isStoreFault,\n ProtocolFailure,\n type StoreFailure,\n type StoreFault,\n Timeout,\n Unavailable,\n} from '@entifix/core';\n\n/**\n * What a Mongo driver error means, as a categorised failure\n * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).\n *\n * Read by **name, code and label**, never by class: the driver's error classes\n * are not what a consumer can match on, and a second copy of the driver in one\n * process would answer `instanceof` with `false`.\n */\nexport interface MongoErrorContext {\n /** What the failure names as its dependency; `mongo` when absent. */\n readonly dependency?: string;\n /**\n * What a duplicate key means here. On a declared uniqueness it is a\n * `conflict` — the caller asked for something that already exists. On a\n * claim or a race (an inbox, a command claim, a saga claim) it is\n * `contention` — someone else got there first, and retrying wins or replays.\n */\n readonly duplicate?: 'conflict' | 'contention';\n}\n\ninterface DriverErrorShape {\n readonly name?: unknown;\n readonly code?: unknown;\n readonly codeName?: unknown;\n readonly keyPattern?: unknown;\n readonly writeErrors?: ReadonlyArray<{ readonly code?: unknown }>;\n readonly errorLabels?: readonly string[];\n readonly hasErrorLabel?: (label: string) => boolean;\n readonly cause?: unknown;\n}\n\nconst shape = (error: unknown): DriverErrorShape =>\n typeof error === 'object' && error !== null\n ? (error as DriverErrorShape)\n : {};\n\nconst labelled = (error: DriverErrorShape, label: string): boolean =>\n error.hasErrorLabel?.(label) ?? error.errorLabels?.includes(label) ?? false;\n\n/** Whether a server label is on `error` or in its cause chain. */\nexport const mongoErrorLabelled = (error: unknown, label: string): boolean => {\n let current: unknown = error;\n for (let depth = 0; current != null && depth < 5; depth += 1) {\n if (labelled(shape(current), label)) return true;\n current = shape(current).cause;\n }\n return false;\n};\n\nconst DUPLICATE_KEY = 11000;\nconst WRITE_CONFLICT = 112;\nconst MAX_TIME_EXPIRED = 50;\n\nconst UNREACHABLE = new Set([\n 'MongoNetworkError',\n 'MongoServerSelectionError',\n 'MongoNotConnectedError',\n 'MongoTopologyClosedError',\n 'MongoServerClosedError',\n 'MongoPoolClosedError',\n]);\n\nconst isDuplicate = (error: DriverErrorShape): boolean =>\n error.code === DUPLICATE_KEY ||\n (error.writeErrors?.some(write => write.code === DUPLICATE_KEY) ?? false);\n\nconst duplicatedKey = (error: DriverErrorShape): string | undefined =>\n typeof error.keyPattern === 'object' && error.keyPattern !== null\n ? Object.keys(error.keyPattern).join(',')\n : undefined;\n\nexport const translateMongoError = (\n error: unknown,\n context: MongoErrorContext = {},\n): StoreFailure => {\n // Already translated, by an inner call: pass it through as it is.\n if (isFailure(error)) return error as StoreFailure;\n const dependency = context.dependency ?? 'mongo';\n const driver = shape(error);\n\n if (isDuplicate(driver)) {\n if (context.duplicate === 'contention') return new Contention();\n const key = duplicatedKey(driver);\n return new DuplicateKey(key === undefined ? {} : { key });\n }\n if (\n driver.code === WRITE_CONFLICT ||\n mongoErrorLabelled(error, 'TransientTransactionError')\n )\n return new Contention();\n if (\n driver.code === MAX_TIME_EXPIRED ||\n driver.name === 'MongoNetworkTimeoutError' ||\n driver.name === 'MongoOperationTimeoutError'\n )\n return new Timeout({ dependency });\n if (driver.name === 'BSONError')\n return new ProtocolFailure({\n reason: `${dependency} answered a document that does not decode`,\n });\n if (typeof driver.name === 'string' && UNREACHABLE.has(driver.name))\n return new Unavailable({ dependency });\n // Anything else the driver raised is still the store failing to answer,\n // which is what the class tree called every one of these.\n return new Unavailable({ dependency });\n};\n\n/**\n * A driver error as the store's own fault, for the bookkeeping a mechanism\n * does around a caller's work — a claim, a commit, a remembered answer. A\n * duplicate key there means a race lost, never an answer for the caller.\n */\nexport const translateMongoFault = (\n error: unknown,\n context: Omit<MongoErrorContext, 'duplicate'> = {},\n): StoreFault => {\n const failure = translateMongoError(error, {\n ...context,\n duplicate: 'contention',\n });\n return isStoreFault(failure) ? failure : new Contention();\n};\n"],"names":["Contention","DuplicateKey","isFailure","isStoreFault","ProtocolFailure","Timeout","Unavailable","shape","error","labelled","label","hasErrorLabel","errorLabels","includes","mongoErrorLabelled","current","depth","cause","DUPLICATE_KEY","WRITE_CONFLICT","MAX_TIME_EXPIRED","UNREACHABLE","Set","isDuplicate","code","writeErrors","some","write","duplicatedKey","keyPattern","Object","keys","join","undefined","translateMongoError","context","dependency","driver","duplicate","key","name","reason","has","translateMongoFault","failure"],"mappings":";AAAA,SACEA,UAAU,EACVC,YAAY,EACZC,SAAS,EACTC,YAAY,EACZC,eAAe,EAGfC,OAAO,EACPC,WAAW,QACN,gBAAgB;AAiCvB,MAAMC,QAAQ,CAACC,QACb,OAAOA,UAAU,YAAYA,UAAU,OAClCA,QACD,CAAC;AAEP,MAAMC,WAAW,CAACD,OAAyBE;QACzCF;QAAgCA;YAAhCA,gBAAAA,MAAMG,aAAa,oBAAnBH,MAAMG,aAAa,MAAnBH,OAAsBE,2BAAUF,qBAAAA,MAAMI,WAAW,qBAAjBJ,mBAAmBK,QAAQ,CAACH,kBAA5DF,OAAsE;;AAExE,gEAAgE,GAChE,OAAO,MAAMM,qBAAqB,CAACN,OAAgBE;IACjD,IAAIK,UAAmBP;IACvB,IAAK,IAAIQ,QAAQ,GAAGD,WAAW,QAAQC,QAAQ,GAAGA,SAAS,EAAG;QAC5D,IAAIP,SAASF,MAAMQ,UAAUL,QAAQ,OAAO;QAC5CK,UAAUR,MAAMQ,SAASE,KAAK;IAChC;IACA,OAAO;AACT,EAAE;AAEF,MAAMC,gBAAgB;AACtB,MAAMC,iBAAiB;AACvB,MAAMC,mBAAmB;AAEzB,MAAMC,cAAc,IAAIC,IAAI;IAC1B;IACA;IACA;IACA;IACA;IACA;CACD;AAED,MAAMC,cAAc,CAACf;;QAElBA;WADDA,MAAMgB,IAAI,KAAKN,2BACdV,qBAAAA,MAAMiB,WAAW,qBAAjBjB,mBAAmBkB,IAAI,CAACC,CAAAA,QAASA,MAAMH,IAAI,KAAKN,iCAAkB;;AAErE,MAAMU,gBAAgB,CAACpB,QACrB,OAAOA,MAAMqB,UAAU,KAAK,YAAYrB,MAAMqB,UAAU,KAAK,OACzDC,OAAOC,IAAI,CAACvB,MAAMqB,UAAU,EAAEG,IAAI,CAAC,OACnCC;AAEN,OAAO,MAAMC,sBAAsB,CACjC1B,OACA2B,UAA6B,CAAC,CAAC;QAIZA;IAFnB,kEAAkE;IAClE,IAAIjC,UAAUM,QAAQ,OAAOA;IAC7B,MAAM4B,cAAaD,sBAAAA,QAAQC,UAAU,YAAlBD,sBAAsB;IACzC,MAAME,SAAS9B,MAAMC;IAErB,IAAIe,YAAYc,SAAS;QACvB,IAAIF,QAAQG,SAAS,KAAK,cAAc,OAAO,IAAItC;QACnD,MAAMuC,MAAMX,cAAcS;QAC1B,OAAO,IAAIpC,aAAasC,QAAQN,YAAY,CAAC,IAAI;YAAEM;QAAI;IACzD;IACA,IACEF,OAAOb,IAAI,KAAKL,kBAChBL,mBAAmBN,OAAO,8BAE1B,OAAO,IAAIR;IACb,IACEqC,OAAOb,IAAI,KAAKJ,oBAChBiB,OAAOG,IAAI,KAAK,8BAChBH,OAAOG,IAAI,KAAK,8BAEhB,OAAO,IAAInC,QAAQ;QAAE+B;IAAW;IAClC,IAAIC,OAAOG,IAAI,KAAK,aAClB,OAAO,IAAIpC,gBAAgB;QACzBqC,QAAQ,GAAGL,WAAW,yCAAyC,CAAC;IAClE;IACF,IAAI,OAAOC,OAAOG,IAAI,KAAK,YAAYnB,YAAYqB,GAAG,CAACL,OAAOG,IAAI,GAChE,OAAO,IAAIlC,YAAY;QAAE8B;IAAW;IACtC,wEAAwE;IACxE,0DAA0D;IAC1D,OAAO,IAAI9B,YAAY;QAAE8B;IAAW;AACtC,EAAE;AAEF;;;;CAIC,GACD,OAAO,MAAMO,sBAAsB,CACjCnC,OACA2B,UAAgD,CAAC,CAAC;IAElD,MAAMS,UAAUV,oBAAoB1B,OAAO,aACtC2B;QACHG,WAAW;;IAEb,OAAOnC,aAAayC,WAAWA,UAAU,IAAI5C;AAC/C,EAAE"}
|
package/dist/inbox/store.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { EntifixConnError } from '@entifix/core';
|
|
2
1
|
import type { InboxClaim, TransactionInbox } from '@entifix/transactions';
|
|
3
2
|
import { Effect } from 'effect';
|
|
4
3
|
import type { Db } from 'mongodb';
|
|
@@ -37,7 +36,7 @@ export declare const inboxDocument: (consumer: string, eventId: string) => Inbox
|
|
|
37
36
|
* the index must exist before the first claim, not eventually, or two
|
|
38
37
|
* concurrent first deliveries both insert.
|
|
39
38
|
*/
|
|
40
|
-
export declare const ensureInboxIndexes: (db: Db) => Effect.Effect<void,
|
|
39
|
+
export declare const ensureInboxIndexes: (db: Db) => Effect.Effect<void, import("@entifix/core").StoreFailure, never>;
|
|
41
40
|
/**
|
|
42
41
|
* A {@link TransactionInbox} over one database, for one consumer.
|
|
43
42
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/inbox/store.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/inbox/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAIlC;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAKpD,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,OAGM,CAAC;AAEvD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACxB,UAAU,MAAM,EAChB,SAAS,MAAM,KACd,UAID,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,EAAE,qEAQrC,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,IAAI,EAAE,EAAE,UAAU,MAAM,KAAG,gBAuBzD,CAAC"}
|
package/dist/inbox/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EntifixConnError } from '@entifix/core';
|
|
2
1
|
import { Effect } from 'effect';
|
|
2
|
+
import { translateMongoError } from '../errors/translate-mongo-error.js';
|
|
3
3
|
/**
|
|
4
4
|
* The inbox collection, and it lives in **whichever database the side effect it
|
|
5
5
|
* guards writes to** — not in one central place.
|
|
@@ -46,9 +46,7 @@ export const isDuplicateKey = (error)=>typeof error === 'object' && error !== nu
|
|
|
46
46
|
unique: true
|
|
47
47
|
});
|
|
48
48
|
},
|
|
49
|
-
catch: (error)=>
|
|
50
|
-
database: db.databaseName
|
|
51
|
-
})
|
|
49
|
+
catch: (error)=>translateMongoError(error)
|
|
52
50
|
});
|
|
53
51
|
/**
|
|
54
52
|
* A {@link TransactionInbox} over one database, for one consumer.
|
|
@@ -75,10 +73,7 @@ export const isDuplicateKey = (error)=>typeof error === 'object' && error !== nu
|
|
|
75
73
|
throw error;
|
|
76
74
|
}
|
|
77
75
|
},
|
|
78
|
-
catch: (error)=>
|
|
79
|
-
consumer,
|
|
80
|
-
eventId
|
|
81
|
-
})
|
|
76
|
+
catch: (error)=>translateMongoError(error)
|
|
82
77
|
})
|
|
83
78
|
};
|
|
84
79
|
};
|
package/dist/inbox/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/inbox/store.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../src/inbox/store.ts"],"sourcesContent":["import type { InboxClaim, TransactionInbox } from '@entifix/transactions';\nimport { Effect } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { translateMongoError } from '../errors/translate-mongo-error.js';\n\n/**\n * The inbox collection, and it lives in **whichever database the side effect it\n * guards writes to** — not in one central place.\n *\n * That is the whole mechanism, not a deployment preference: a claim is only\n * worth anything if it commits in the same storage transaction as the effect,\n * and two databases cannot be one transaction. So the saga tracker's inbox is in\n * the `saga` database because its fold writes there, exactly as the outbox is\n * per-tenant because the entity is.\n */\nexport const INBOX_COLLECTION = 'transaction_inbox';\n\n/** Mongo's duplicate-key error, which is how a redelivery is recognised. */\nconst DUPLICATE_KEY = 11000;\n\nexport const isDuplicateKey = (error: unknown): boolean =>\n typeof error === 'object' &&\n error !== null &&\n (error as { code?: unknown }).code === DUPLICATE_KEY;\n\n/**\n * The stored shape of a claim.\n *\n * Exported for the same reason `outboxDocument` is: the claim that matters is\n * **not** written through the port. It has to join the side effect's session to\n * be atomic with it, and a session cannot live in a framework-free contract, so\n * the consumer inserts this document itself. {@link makeMongoInbox} is for a\n * consumer that can claim standalone.\n */\nexport const inboxDocument = (\n consumer: string,\n eventId: string,\n): InboxClaim => ({\n consumer,\n eventId,\n claimedAt: new Date().toISOString(),\n});\n\n/**\n * The unique index the claim rides, on **`(consumer, eventId)`**.\n *\n * Compound, and that is the half easy to get wrong. Two consumers legitimately\n * process the same event — the tracker's fold and the SSE hub both bind\n * `transaction.*` — so a unique index on `eventId` alone would let whichever\n * claimed first starve every other consumer of every message, while the broker,\n * the queue and the handler all went on reporting success.\n *\n * Called per handle rather than once at boot, matching `ensureOutboxIndexes`:\n * the index must exist before the first claim, not eventually, or two\n * concurrent first deliveries both insert.\n */\nexport const ensureInboxIndexes = (db: Db) =>\n Effect.tryPromise({\n try: async () => {\n await db\n .collection<InboxClaim>(INBOX_COLLECTION)\n .createIndex({ consumer: 1, eventId: 1 }, { unique: true });\n },\n catch: error => translateMongoError(error),\n });\n\n/**\n * A {@link TransactionInbox} over one database, for one consumer.\n *\n * `consumer` is the subscription's durable work-queue name\n * (`<slice>.<pattern>`) — deterministic, and stable across restarts, which an\n * anonymous broadcast queue's server-generated name is not. That is also why\n * `dedupe: 'inbox'` is only legal on a `work` subscription.\n */\nexport const makeMongoInbox = (db: Db, consumer: string): TransactionInbox => {\n const collection = db.collection<InboxClaim>(INBOX_COLLECTION);\n\n return {\n claim: eventId =>\n Effect.tryPromise({\n try: async () => {\n try {\n await collection.insertOne(inboxDocument(consumer, eventId));\n return 'claimed' as const;\n } catch (error) {\n // Not a failure: the unique index rejected a second claim for this\n // consumer and message, which is precisely how a redelivery is\n // identified.\n if (isDuplicateKey(error)) {\n return 'duplicate' as const;\n }\n throw error;\n }\n },\n catch: error => translateMongoError(error),\n }),\n };\n};\n"],"names":["Effect","translateMongoError","INBOX_COLLECTION","DUPLICATE_KEY","isDuplicateKey","error","code","inboxDocument","consumer","eventId","claimedAt","Date","toISOString","ensureInboxIndexes","db","tryPromise","try","collection","createIndex","unique","catch","makeMongoInbox","claim","insertOne"],"mappings":"AACA,SAASA,MAAM,QAAQ,SAAS;AAGhC,SAASC,mBAAmB,QAAQ,qCAAqC;AAEzE;;;;;;;;;CASC,GACD,OAAO,MAAMC,mBAAmB,oBAAoB;AAEpD,0EAA0E,GAC1E,MAAMC,gBAAgB;AAEtB,OAAO,MAAMC,iBAAiB,CAACC,QAC7B,OAAOA,UAAU,YACjBA,UAAU,QACV,AAACA,MAA6BC,IAAI,KAAKH,cAAc;AAEvD;;;;;;;;CAQC,GACD,OAAO,MAAMI,gBAAgB,CAC3BC,UACAC,UACgB,CAAA;QAChBD;QACAC;QACAC,WAAW,IAAIC,OAAOC,WAAW;IACnC,CAAA,EAAG;AAEH;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,qBAAqB,CAACC,KACjCd,OAAOe,UAAU,CAAC;QAChBC,KAAK;YACH,MAAMF,GACHG,UAAU,CAAaf,kBACvBgB,WAAW,CAAC;gBAAEV,UAAU;gBAAGC,SAAS;YAAE,GAAG;gBAAEU,QAAQ;YAAK;QAC7D;QACAC,OAAOf,CAAAA,QAASJ,oBAAoBI;IACtC,GAAG;AAEL;;;;;;;CAOC,GACD,OAAO,MAAMgB,iBAAiB,CAACP,IAAQN;IACrC,MAAMS,aAAaH,GAAGG,UAAU,CAAaf;IAE7C,OAAO;QACLoB,OAAOb,CAAAA,UACLT,OAAOe,UAAU,CAAC;gBAChBC,KAAK;oBACH,IAAI;wBACF,MAAMC,WAAWM,SAAS,CAAChB,cAAcC,UAAUC;wBACnD,OAAO;oBACT,EAAE,OAAOJ,OAAO;wBACd,mEAAmE;wBACnE,+DAA+D;wBAC/D,cAAc;wBACd,IAAID,eAAeC,QAAQ;4BACzB,OAAO;wBACT;wBACA,MAAMA;oBACR;gBACF;gBACAe,OAAOf,CAAAA,QAASJ,oBAAoBI;YACtC;IACJ;AACF,EAAE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './adapters/filter-translator.js';
|
|
2
|
+
export * from './adapters/make-mongo-facet-counter.js';
|
|
2
3
|
export * from './adapters/make-mongo-link-resolver.js';
|
|
3
4
|
export * from './adapters/make-mongo-repository.js';
|
|
5
|
+
export * from './adapters/mongo-facet-counter-factory.js';
|
|
4
6
|
export * from './adapters/mongo-repository-factory.js';
|
|
5
7
|
export * from './mongo-database/mongo-database.js';
|
|
6
8
|
export * from './mongo-database/mongo-health-probe.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,wCAAwC,CAAC;AACvD,cAAc,qCAAqC,CAAC;AACpD,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,wCAAwC,CAAC;AACvD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,2CAA2C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,wCAAwC,CAAC;AACvD,cAAc,wCAAwC,CAAC;AACvD,cAAc,qCAAqC,CAAC;AACpD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,wCAAwC,CAAC;AACvD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,2CAA2C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './adapters/filter-translator.js';
|
|
2
|
+
export * from './adapters/make-mongo-facet-counter.js';
|
|
2
3
|
export * from './adapters/make-mongo-link-resolver.js';
|
|
3
4
|
export * from './adapters/make-mongo-repository.js';
|
|
5
|
+
export * from './adapters/mongo-facet-counter-factory.js';
|
|
4
6
|
export * from './adapters/mongo-repository-factory.js';
|
|
5
7
|
export * from './mongo-database/mongo-database.js';
|
|
6
8
|
export * from './mongo-database/mongo-health-probe.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './adapters/filter-translator.js';\nexport * from './adapters/make-mongo-link-resolver.js';\nexport * from './adapters/make-mongo-repository.js';\nexport * from './adapters/mongo-repository-factory.js';\nexport * from './mongo-database/mongo-database.js';\nexport * from './mongo-database/mongo-health-probe.js';\nexport * from './mongo-database/mongo-tenant-directory.js';\nexport * from './mongo-database/mongo-tenant-resolver.js';\n"],"names":[],"mappings":"AAAA,cAAc,kCAAkC;AAChD,cAAc,yCAAyC;AACvD,cAAc,sCAAsC;AACpD,cAAc,yCAAyC;AACvD,cAAc,qCAAqC;AACnD,cAAc,yCAAyC;AACvD,cAAc,6CAA6C;AAC3D,cAAc,4CAA4C"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './adapters/filter-translator.js';\nexport * from './adapters/make-mongo-facet-counter.js';\nexport * from './adapters/make-mongo-link-resolver.js';\nexport * from './adapters/make-mongo-repository.js';\nexport * from './adapters/mongo-facet-counter-factory.js';\nexport * from './adapters/mongo-repository-factory.js';\nexport * from './mongo-database/mongo-database.js';\nexport * from './mongo-database/mongo-health-probe.js';\nexport * from './mongo-database/mongo-tenant-directory.js';\nexport * from './mongo-database/mongo-tenant-resolver.js';\n"],"names":[],"mappings":"AAAA,cAAc,kCAAkC;AAChD,cAAc,yCAAyC;AACvD,cAAc,yCAAyC;AACvD,cAAc,sCAAsC;AACpD,cAAc,4CAA4C;AAC1D,cAAc,yCAAyC;AACvD,cAAc,qCAAqC;AACnD,cAAc,yCAAyC;AACvD,cAAc,6CAA6C;AAC3D,cAAc,4CAA4C"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { EntifixFailure } from '@entifix/core';
|
|
2
2
|
import { Context, Layer } from 'effect';
|
|
3
3
|
import { Db, MongoClient } from 'mongodb';
|
|
4
4
|
declare const MongoDatabaseTag_base: Context.ServiceClass<MongoDatabaseTag, "MongoDatabaseTag", Db>;
|
|
@@ -38,7 +38,7 @@ export interface MongoDatabaseSettings extends MongoClientSettings {
|
|
|
38
38
|
* {@link MongoDatabaseLayer} builds on this rather than connecting again: two
|
|
39
39
|
* layers over the same URI would mean two pools.
|
|
40
40
|
*/
|
|
41
|
-
export declare const MongoClientLayer: (settings: MongoClientSettings) => Layer.Layer<MongoClientTag,
|
|
41
|
+
export declare const MongoClientLayer: (settings: MongoClientSettings) => Layer.Layer<MongoClientTag, EntifixFailure>;
|
|
42
42
|
/**
|
|
43
43
|
* {@link MongoClientLayer} plus the named {@link Db} resolved from it, exposed
|
|
44
44
|
* under {@link MongoDatabaseTag}. One acquire, one pool, both tags — which is
|
|
@@ -47,6 +47,6 @@ export declare const MongoClientLayer: (settings: MongoClientSettings) => Layer.
|
|
|
47
47
|
* Use it when the service owns exactly one named store (`auth-service`). A
|
|
48
48
|
* service whose handles are all per-request wants {@link MongoClientLayer}.
|
|
49
49
|
*/
|
|
50
|
-
export declare const MongoDatabaseLayer: (settings: MongoDatabaseSettings) => Layer.Layer<MongoDatabaseTag | MongoClientTag,
|
|
50
|
+
export declare const MongoDatabaseLayer: (settings: MongoDatabaseSettings) => Layer.Layer<MongoDatabaseTag | MongoClientTag, EntifixFailure>;
|
|
51
51
|
export {};
|
|
52
52
|
//# sourceMappingURL=mongo-database.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mongo-database.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"mongo-database.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,OAAO,EAAU,KAAK,EAAY,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;;AAI1C,6DAA6D;AAC7D,qBAAa,gBAAiB,SAAQ,qBAErC;CAAG;;AAEJ;;;;;;;;;GASG;AACH,qBAAa,cAAe,SAAQ,mBAGf;CAAG;AAExB,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAsB,SAAQ,mBAAmB;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAC3B,UAAU,mBAAmB,KAC5B,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,cAAc,CA0B1C,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,qBAAqB,KAC9B,KAAK,CAAC,KAAK,CAAC,gBAAgB,GAAG,cAAc,EAAE,cAAc,CAO7D,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { EntifixConnError } from '@entifix/core';
|
|
2
1
|
import { Context, Effect, Layer, Schedule } from 'effect';
|
|
3
2
|
import { MongoClient } from 'mongodb';
|
|
3
|
+
import { translateMongoError } from '../errors/translate-mongo-error.js';
|
|
4
4
|
/** DI tag carrying a connected mongodb {@link Db} handle. */ export class MongoDatabaseTag extends Context.Service()('MongoDatabaseTag') {
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
@@ -35,7 +35,7 @@ import { MongoClient } from 'mongodb';
|
|
|
35
35
|
// alive behind it. Fail fast instead.
|
|
36
36
|
serverSelectionTimeoutMS: 5000
|
|
37
37
|
}),
|
|
38
|
-
catch: (error)=>
|
|
38
|
+
catch: (error)=>translateMongoError(error)
|
|
39
39
|
}).pipe(// Boot order is not guaranteed — a service may start while Mongo is
|
|
40
40
|
// still rolling out. Keep trying for a window instead of dying on the
|
|
41
41
|
// first refusal, which leaves a dead process nothing will restart.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mongo-database/mongo-database.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../../src/mongo-database/mongo-database.ts"],"sourcesContent":["import type { EntifixFailure } from '@entifix/core';\nimport { Context, Effect, Layer, Schedule } from 'effect';\nimport { Db, MongoClient } from 'mongodb';\n\nimport { translateMongoError } from '../errors/translate-mongo-error.js';\n\n/** DI tag carrying a connected mongodb {@link Db} handle. */\nexport class MongoDatabaseTag extends Context.Service<MongoDatabaseTag, Db>()(\n 'MongoDatabaseTag',\n) {}\n\n/**\n * DI tag carrying the connected {@link MongoClient} itself — the **pool**.\n *\n * Exposed alongside {@link MongoDatabaseTag} so a per-organization handle can be\n * resolved with `client.db(name)` inside a request without opening a second\n * connection. Both tags come from one acquire for exactly that reason: two\n * layers over the same URI would mean two pools.\n *\n * Reach for {@link MongoDatabaseTag} unless you are resolving tenant storage.\n */\nexport class MongoClientTag extends Context.Service<\n MongoClientTag,\n MongoClient\n>()('MongoClientTag') {}\n\nexport interface MongoClientSettings {\n readonly uri: string;\n}\n\nexport interface MongoDatabaseSettings extends MongoClientSettings {\n readonly dbName: string;\n}\n\n/**\n * A scoped {@link Layer} that opens a {@link MongoClient} on acquire and closes\n * it on release, exposing it under {@link MongoClientTag}. Because it is scoped,\n * the connection is torn down deterministically when the service's root layer is\n * interrupted (the `makeService` graceful-shutdown path).\n *\n * This is the layer to reach for when a service names **no** database at boot —\n * one that resolves every handle per request (tenant storage) or holds several\n * named stores in one process. Naming a database it never writes would put a\n * phantom store in the host's register — a store nothing writes.\n *\n * {@link MongoDatabaseLayer} builds on this rather than connecting again: two\n * layers over the same URI would mean two pools.\n */\nexport const MongoClientLayer = (\n settings: MongoClientSettings,\n): Layer.Layer<MongoClientTag, EntifixFailure> =>\n Layer.effect(\n MongoClientTag,\n Effect.acquireRelease(\n Effect.tryPromise({\n try: () =>\n MongoClient.connect(settings.uri, {\n // The 30s default means a readiness probe against a missing\n // primary sits for 30s; the registry would time it out first and\n // report a truthful `false`, but the driver would keep the fiber\n // alive behind it. Fail fast instead.\n serverSelectionTimeoutMS: 5_000,\n }),\n catch: error => translateMongoError(error),\n }).pipe(\n // Boot order is not guaranteed — a service may start while Mongo is\n // still rolling out. Keep trying for a window instead of dying on the\n // first refusal, which leaves a dead process nothing will restart.\n Effect.retry(\n Schedule.exponential('250 millis').pipe(\n Schedule.upTo({ duration: '30 seconds' }),\n ),\n ),\n ),\n client => Effect.promise(() => client.close()),\n ),\n );\n\n/**\n * {@link MongoClientLayer} plus the named {@link Db} resolved from it, exposed\n * under {@link MongoDatabaseTag}. One acquire, one pool, both tags — which is\n * why a service that wants both must not merge the two layers.\n *\n * Use it when the service owns exactly one named store (`auth-service`). A\n * service whose handles are all per-request wants {@link MongoClientLayer}.\n */\nexport const MongoDatabaseLayer = (\n settings: MongoDatabaseSettings,\n): Layer.Layer<MongoDatabaseTag | MongoClientTag, EntifixFailure> =>\n Layer.provideMerge(\n Layer.effect(\n MongoDatabaseTag,\n Effect.map(MongoClientTag, client => client.db(settings.dbName)),\n ),\n MongoClientLayer(settings),\n );\n"],"names":["Context","Effect","Layer","Schedule","MongoClient","translateMongoError","MongoDatabaseTag","Service","MongoClientTag","MongoClientLayer","settings","effect","acquireRelease","tryPromise","try","connect","uri","serverSelectionTimeoutMS","catch","error","pipe","retry","exponential","upTo","duration","client","promise","close","MongoDatabaseLayer","provideMerge","map","db","dbName"],"mappings":"AACA,SAASA,OAAO,EAAEC,MAAM,EAAEC,KAAK,EAAEC,QAAQ,QAAQ,SAAS;AAC1D,SAAaC,WAAW,QAAQ,UAAU;AAE1C,SAASC,mBAAmB,QAAQ,qCAAqC;AAEzE,2DAA2D,GAC3D,OAAO,MAAMC,yBAAyBN,QAAQO,OAAO,GACnD;AACC;AAEH;;;;;;;;;CASC,GACD,OAAO,MAAMC,uBAAuBR,QAAQO,OAAO,GAG/C;AAAmB;AAUvB;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAME,mBAAmB,CAC9BC,WAEAR,MAAMS,MAAM,CACVH,gBACAP,OAAOW,cAAc,CACnBX,OAAOY,UAAU,CAAC;QAChBC,KAAK,IACHV,YAAYW,OAAO,CAACL,SAASM,GAAG,EAAE;gBAChC,4DAA4D;gBAC5D,iEAAiE;gBACjE,iEAAiE;gBACjE,sCAAsC;gBACtCC,0BAA0B;YAC5B;QACFC,OAAOC,CAAAA,QAASd,oBAAoBc;IACtC,GAAGC,IAAI,CACL,oEAAoE;IACpE,sEAAsE;IACtE,mEAAmE;IACnEnB,OAAOoB,KAAK,CACVlB,SAASmB,WAAW,CAAC,cAAcF,IAAI,CACrCjB,SAASoB,IAAI,CAAC;QAAEC,UAAU;IAAa,OAI7CC,CAAAA,SAAUxB,OAAOyB,OAAO,CAAC,IAAMD,OAAOE,KAAK,MAE7C;AAEJ;;;;;;;CAOC,GACD,OAAO,MAAMC,qBAAqB,CAChClB,WAEAR,MAAM2B,YAAY,CAChB3B,MAAMS,MAAM,CACVL,kBACAL,OAAO6B,GAAG,CAACtB,gBAAgBiB,CAAAA,SAAUA,OAAOM,EAAE,CAACrB,SAASsB,MAAM,KAEhEvB,iBAAiBC,WACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mongo-tenant-directory.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-tenant-directory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"mongo-tenant-directory.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-tenant-directory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,WAAW,EACnB,QAAQ,MAAM,KACb,eAAe,CAAC,EAAE,CAoBnB,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EntifixConnError } from '@entifix/core';
|
|
2
1
|
import { Effect } from 'effect';
|
|
2
|
+
import { translateMongoError } from '../errors/translate-mongo-error.js';
|
|
3
3
|
/**
|
|
4
4
|
* Lists every database named `<prefix><organizationId>` on one client — the
|
|
5
5
|
* same naming {@link makeMongoTenantResolver} resolves one organization by.
|
|
@@ -22,9 +22,7 @@ import { Effect } from 'effect';
|
|
|
22
22
|
handle: client.db(name)
|
|
23
23
|
}));
|
|
24
24
|
},
|
|
25
|
-
catch: (error)=>
|
|
26
|
-
prefix
|
|
27
|
-
})
|
|
25
|
+
catch: (error)=>translateMongoError(error)
|
|
28
26
|
})
|
|
29
27
|
});
|
|
30
28
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mongo-database/mongo-tenant-directory.ts"],"sourcesContent":["import type { TenantDirectory } from '@entifix/business';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/mongo-database/mongo-tenant-directory.ts"],"sourcesContent":["import type { TenantDirectory } from '@entifix/business';\nimport { Effect } from 'effect';\nimport type { Db, MongoClient } from 'mongodb';\n\nimport { translateMongoError } from '../errors/translate-mongo-error.js';\n\n/**\n * Lists every database named `<prefix><organizationId>` on one client — the\n * same naming {@link makeMongoTenantResolver} resolves one organization by.\n *\n * `listDatabases` needs cluster-wide rights and a round trip, which is why it\n * is a directory for background work and never a request's path: a request\n * already knows its organization and resolves it directly.\n *\n * Only a database that exists is listed, and Mongo creates one lazily on its\n * first write. An organization that has never written to this store is not\n * here, which is right for every sweep: it has nothing to sweep.\n */\nexport const makeMongoTenantDirectory = (\n client: MongoClient,\n prefix: string,\n): TenantDirectory<Db> => ({\n list: () =>\n Effect.tryPromise({\n try: async () => {\n const { databases } = await client.db('admin').admin().listDatabases({\n nameOnly: true,\n });\n return databases\n .map(database => database.name)\n .filter(\n name => name.startsWith(prefix) && name.length > prefix.length,\n )\n .sort()\n .map(name => ({\n organizationId: name.slice(prefix.length),\n handle: client.db(name),\n }));\n },\n catch: error => translateMongoError(error),\n }),\n});\n"],"names":["Effect","translateMongoError","makeMongoTenantDirectory","client","prefix","list","tryPromise","try","databases","db","admin","listDatabases","nameOnly","map","database","name","filter","startsWith","length","sort","organizationId","slice","handle","catch","error"],"mappings":"AACA,SAASA,MAAM,QAAQ,SAAS;AAGhC,SAASC,mBAAmB,QAAQ,qCAAqC;AAEzE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,2BAA2B,CACtCC,QACAC,SACyB,CAAA;QACzBC,MAAM,IACJL,OAAOM,UAAU,CAAC;gBAChBC,KAAK;oBACH,MAAM,EAAEC,SAAS,EAAE,GAAG,MAAML,OAAOM,EAAE,CAAC,SAASC,KAAK,GAAGC,aAAa,CAAC;wBACnEC,UAAU;oBACZ;oBACA,OAAOJ,UACJK,GAAG,CAACC,CAAAA,WAAYA,SAASC,IAAI,EAC7BC,MAAM,CACLD,CAAAA,OAAQA,KAAKE,UAAU,CAACb,WAAWW,KAAKG,MAAM,GAAGd,OAAOc,MAAM,EAE/DC,IAAI,GACJN,GAAG,CAACE,CAAAA,OAAS,CAAA;4BACZK,gBAAgBL,KAAKM,KAAK,CAACjB,OAAOc,MAAM;4BACxCI,QAAQnB,OAAOM,EAAE,CAACM;wBACpB,CAAA;gBACJ;gBACAQ,OAAOC,CAAAA,QAASvB,oBAAoBuB;YACtC;IACJ,CAAA,EAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mongo-tenant-resolver.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-tenant-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGhE,OAAO,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAa/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,WAAW,EACnB,QAAQ,MAAM,KACb,sBAAsB,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"mongo-tenant-resolver.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-tenant-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGhE,OAAO,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAa/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,WAAW,EACnB,QAAQ,MAAM,KACb,sBAAsB,CAAC,EAAE,CAmC1B,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Invalid } from '@entifix/core';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
3
|
/** Mongo's own limit on a database name, minus room for a prefix. */ const MAX_DB_NAME_LENGTH = 63;
|
|
4
4
|
/**
|
|
@@ -21,16 +21,27 @@ import { Effect } from 'effect';
|
|
|
21
21
|
*/ export const makeMongoTenantResolver = (client, prefix)=>({
|
|
22
22
|
forOrganization: (organizationId)=>Effect.gen(function*() {
|
|
23
23
|
if (!SAFE_ORGANIZATION_ID.test(organizationId)) {
|
|
24
|
-
return yield* Effect.fail(new
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return yield* Effect.fail(new Invalid({
|
|
25
|
+
errors: [
|
|
26
|
+
{
|
|
27
|
+
pointer: '/organizationId',
|
|
28
|
+
// The id itself is deliberately not echoed: it arrived unvalidated.
|
|
29
|
+
detail: 'not a safe organization id',
|
|
30
|
+
code: 'unsafeOrganizationId'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
27
33
|
}));
|
|
28
34
|
}
|
|
29
35
|
const name = `${prefix}${organizationId}`;
|
|
30
36
|
if (name.length > MAX_DB_NAME_LENGTH) {
|
|
31
|
-
return yield* Effect.fail(new
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
return yield* Effect.fail(new Invalid({
|
|
38
|
+
errors: [
|
|
39
|
+
{
|
|
40
|
+
pointer: '/organizationId',
|
|
41
|
+
detail: `the tenant database name exceeds ${MAX_DB_NAME_LENGTH} characters`,
|
|
42
|
+
code: 'organizationIdTooLong'
|
|
43
|
+
}
|
|
44
|
+
]
|
|
34
45
|
}));
|
|
35
46
|
}
|
|
36
47
|
return client.db(name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mongo-database/mongo-tenant-resolver.ts"],"sourcesContent":["import type { TenantDatabaseResolver } from '@entifix/business';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/mongo-database/mongo-tenant-resolver.ts"],"sourcesContent":["import type { TenantDatabaseResolver } from '@entifix/business';\nimport { Invalid } from '@entifix/core';\nimport { Effect } from 'effect';\nimport type { Db, MongoClient } from 'mongodb';\n\n/** Mongo's own limit on a database name, minus room for a prefix. */\nconst MAX_DB_NAME_LENGTH = 63;\n\n/**\n * Mongo names a database with a small character set. An organization id is a\n * hex ObjectId today, but this guards the shape rather than trusting it: a name\n * assembled from an unvalidated id is a place where a crafted value could reach\n * another tenant's database.\n */\nconst SAFE_ORGANIZATION_ID = /^[a-zA-Z0-9_-]+$/;\n\n/**\n * Resolves one organization's Mongo database from a single connected client.\n *\n * Cheap by construction: `client.db(name)` returns a **handle**, not a\n * connection, so N organizations share one pool and one socket set, and Mongo\n * creates the database lazily on first write — which is why provisioning an\n * organization is a control-plane record plus this naming convention, with no\n * `CREATE DATABASE` step to fail halfway.\n *\n * Call it inside the request. The pool is the boot-time `MongoDatabaseLayer`;\n * wrapping this in a per-request `Layer` would rebuild the pool per request.\n */\nexport const makeMongoTenantResolver = (\n client: MongoClient,\n prefix: string,\n): TenantDatabaseResolver<Db> => ({\n forOrganization: (organizationId: string) =>\n Effect.gen(function* () {\n if (!SAFE_ORGANIZATION_ID.test(organizationId)) {\n return yield* Effect.fail(\n new Invalid({\n errors: [\n {\n pointer: '/organizationId',\n // The id itself is deliberately not echoed: it arrived unvalidated.\n detail: 'not a safe organization id',\n code: 'unsafeOrganizationId',\n },\n ],\n }),\n );\n }\n\n const name = `${prefix}${organizationId}`;\n if (name.length > MAX_DB_NAME_LENGTH) {\n return yield* Effect.fail(\n new Invalid({\n errors: [\n {\n pointer: '/organizationId',\n detail: `the tenant database name exceeds ${MAX_DB_NAME_LENGTH} characters`,\n code: 'organizationIdTooLong',\n },\n ],\n }),\n );\n }\n\n return client.db(name);\n }),\n});\n"],"names":["Invalid","Effect","MAX_DB_NAME_LENGTH","SAFE_ORGANIZATION_ID","makeMongoTenantResolver","client","prefix","forOrganization","organizationId","gen","test","fail","errors","pointer","detail","code","name","length","db"],"mappings":"AACA,SAASA,OAAO,QAAQ,gBAAgB;AACxC,SAASC,MAAM,QAAQ,SAAS;AAGhC,mEAAmE,GACnE,MAAMC,qBAAqB;AAE3B;;;;;CAKC,GACD,MAAMC,uBAAuB;AAE7B;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,0BAA0B,CACrCC,QACAC,SACgC,CAAA;QAChCC,iBAAiB,CAACC,iBAChBP,OAAOQ,GAAG,CAAC;gBACT,IAAI,CAACN,qBAAqBO,IAAI,CAACF,iBAAiB;oBAC9C,OAAO,OAAOP,OAAOU,IAAI,CACvB,IAAIX,QAAQ;wBACVY,QAAQ;4BACN;gCACEC,SAAS;gCACT,oEAAoE;gCACpEC,QAAQ;gCACRC,MAAM;4BACR;yBACD;oBACH;gBAEJ;gBAEA,MAAMC,OAAO,GAAGV,SAASE,gBAAgB;gBACzC,IAAIQ,KAAKC,MAAM,GAAGf,oBAAoB;oBACpC,OAAO,OAAOD,OAAOU,IAAI,CACvB,IAAIX,QAAQ;wBACVY,QAAQ;4BACN;gCACEC,SAAS;gCACTC,QAAQ,CAAC,iCAAiC,EAAEZ,mBAAmB,WAAW,CAAC;gCAC3Ea,MAAM;4BACR;yBACD;oBACH;gBAEJ;gBAEA,OAAOV,OAAOa,EAAE,CAACF;YACnB;IACJ,CAAA,EAAG"}
|
package/dist/outbox/relay.d.ts
CHANGED
|
@@ -3,10 +3,46 @@ import { type EventBus, EventBusTag, type OutboxStats, type TransactionOutbox }
|
|
|
3
3
|
import { Context, Duration, Effect } from 'effect';
|
|
4
4
|
import type { Db } from 'mongodb';
|
|
5
5
|
import { MongoDatabaseTag } from '../mongo-database/mongo-database.js';
|
|
6
|
-
/** How often the sweep looks for entries the fast path did not carry. */
|
|
7
6
|
export declare const SWEEP_INTERVAL: Duration.Duration;
|
|
8
7
|
/** Entries drained per pass, per database. */
|
|
9
8
|
export declare const BATCH = 100;
|
|
9
|
+
/**
|
|
10
|
+
* What a relay pass did, for the host to log
|
|
11
|
+
* ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).
|
|
12
|
+
*
|
|
13
|
+
* The relay decides nothing about level or sink: it says what happened and the
|
|
14
|
+
* fork site that started it — the slice's shell — decides how loud that is. A
|
|
15
|
+
* `failure` is whatever the store or the bus failed with; a typed one carries
|
|
16
|
+
* its code and category for the log line.
|
|
17
|
+
*/
|
|
18
|
+
export type RelayOutcome = {
|
|
19
|
+
readonly _tag: 'drained';
|
|
20
|
+
readonly database: string;
|
|
21
|
+
readonly sent: number;
|
|
22
|
+
} | {
|
|
23
|
+
readonly _tag: 'quarantined';
|
|
24
|
+
readonly database: string;
|
|
25
|
+
readonly eventId: string;
|
|
26
|
+
readonly eventName: string;
|
|
27
|
+
readonly attempts: number;
|
|
28
|
+
readonly code: string;
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly _tag: 'sweepFailed';
|
|
32
|
+
readonly database?: string;
|
|
33
|
+
readonly failure: unknown;
|
|
34
|
+
} | {
|
|
35
|
+
readonly _tag: 'drainFailed';
|
|
36
|
+
readonly database: string;
|
|
37
|
+
readonly failure: unknown;
|
|
38
|
+
};
|
|
39
|
+
/** Where a relay's outcomes go. The host's fork site supplies it. */
|
|
40
|
+
export type RelayOutcomeSink = (outcome: RelayOutcome) => Effect.Effect<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Hears nothing. The default only so a spec can start a relay without one —
|
|
43
|
+
* a host that passes no sink has a relay nobody can watch.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ignoreRelayOutcome: RelayOutcomeSink;
|
|
10
46
|
declare const OutboxMaxAttempts_base: Context.ServiceClass<OutboxMaxAttempts, "OutboxMaxAttempts", number>;
|
|
11
47
|
/**
|
|
12
48
|
* Publish attempts an outbox entry gets before it is quarantined, from
|
|
@@ -29,6 +65,8 @@ export interface DrainOptions {
|
|
|
29
65
|
readonly maxAttempts: number;
|
|
30
66
|
/** The database being drained; carried so a quarantine log names it. */
|
|
31
67
|
readonly database: string;
|
|
68
|
+
/** Where the pass reports what it did; see {@link RelayOutcome}. */
|
|
69
|
+
readonly onOutcome?: RelayOutcomeSink;
|
|
32
70
|
}
|
|
33
71
|
/**
|
|
34
72
|
* Publish one batch of pending entries, oldest first, and answer how many went.
|
|
@@ -42,7 +80,7 @@ export interface DrainOptions {
|
|
|
42
80
|
* skip-so-the-head-moves: an entry that can never publish must stop blocking
|
|
43
81
|
* every entry behind it forever.
|
|
44
82
|
*/
|
|
45
|
-
export declare const drainOutbox: (outbox: TransactionOutbox, bus: EventBus, options: DrainOptions) => Effect.Effect<number, import("@entifix/core").
|
|
83
|
+
export declare const drainOutbox: (outbox: TransactionOutbox, bus: EventBus, options: DrainOptions) => Effect.Effect<number, import("@entifix/core").StoreFailure, never>;
|
|
46
84
|
/**
|
|
47
85
|
* Sample a sweep's depth and age.
|
|
48
86
|
*
|
|
@@ -62,9 +100,9 @@ export type OutboxStatsSink = (database: string, stats: OutboxStats) => Effect.E
|
|
|
62
100
|
* Stats are sampled **after** the drain, so the gauge reports what is still
|
|
63
101
|
* waiting rather than what was waiting when the pass began.
|
|
64
102
|
*
|
|
65
|
-
* Every failure is caught and
|
|
66
|
-
* error that escapes ends the daemon silently — a
|
|
67
|
-
* indistinguishable from a relay with nothing to do.
|
|
103
|
+
* Every failure is caught and reported as `sweepFailed`. This runs inside
|
|
104
|
+
* `Effect.forever`, and an error that escapes ends the daemon silently — a
|
|
105
|
+
* relay that stopped is indistinguishable from a relay with nothing to do.
|
|
68
106
|
*/
|
|
69
107
|
export declare const sweepOutbox: (outbox: TransactionOutbox, bus: EventBus, options: DrainOptions & {
|
|
70
108
|
readonly onStats?: OutboxStatsSink;
|
|
@@ -85,7 +123,8 @@ export declare const sweepOutbox: (outbox: TransactionOutbox, bus: EventBus, opt
|
|
|
85
123
|
*/
|
|
86
124
|
export declare const startOutboxRelay: (options?: {
|
|
87
125
|
readonly onStats?: OutboxStatsSink;
|
|
88
|
-
|
|
126
|
+
readonly onOutcome?: RelayOutcomeSink;
|
|
127
|
+
}) => Effect.Effect<void, import("@entifix/core").StoreFailure, ShutdownRegistryTag | MongoDatabaseTag | EventBusTag | OutboxMaxAttempts>;
|
|
89
128
|
/**
|
|
90
129
|
* One pass over every organization's outbox in a per-organization store.
|
|
91
130
|
*
|
|
@@ -100,6 +139,7 @@ export declare const startOutboxRelay: (options?: {
|
|
|
100
139
|
export declare const sweepTenantOutboxes: (directory: TenantDirectory<Db>, bus: EventBus, options: {
|
|
101
140
|
readonly maxAttempts: number;
|
|
102
141
|
readonly onStats?: OutboxStatsSink;
|
|
142
|
+
readonly onOutcome?: RelayOutcomeSink;
|
|
103
143
|
readonly ensured?: Set<string>;
|
|
104
144
|
}) => Effect.Effect<void, never, never>;
|
|
105
145
|
/**
|
|
@@ -112,15 +152,16 @@ export declare const sweepTenantOutboxes: (directory: TenantDirectory<Db>, bus:
|
|
|
112
152
|
*/
|
|
113
153
|
export declare const startTenantOutboxRelay: (options?: {
|
|
114
154
|
readonly onStats?: OutboxStatsSink;
|
|
155
|
+
readonly onOutcome?: RelayOutcomeSink;
|
|
115
156
|
}) => Effect.Effect<void, never, ShutdownRegistryTag | TenantDirectoryTag | EventBusTag | OutboxMaxAttempts>;
|
|
116
157
|
/**
|
|
117
158
|
* Drain one database's outbox now, for a write path that has just committed.
|
|
118
159
|
*
|
|
119
160
|
* The fast path the sweep is the fallback for: an event drained here leaves as
|
|
120
161
|
* soon as its transaction commits, rather than up to {@link SWEEP_INTERVAL}
|
|
121
|
-
* later. Every failure is
|
|
122
|
-
* outbox, and the sweep will carry it.
|
|
162
|
+
* later. Every failure is reported as `drainFailed` and swallowed — the entry
|
|
163
|
+
* is still in the outbox, and the sweep will carry it.
|
|
123
164
|
*/
|
|
124
|
-
export declare const drainOutboxNow: (db: Db) => Effect.Effect<void, never, EventBusTag | OutboxMaxAttempts>;
|
|
165
|
+
export declare const drainOutboxNow: (db: Db, onOutcome?: RelayOutcomeSink) => Effect.Effect<void, never, EventBusTag | OutboxMaxAttempts>;
|
|
125
166
|
export {};
|
|
126
167
|
//# sourceMappingURL=relay.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/outbox/relay.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,EACpB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,QAAQ,EACb,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAClE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/outbox/relay.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,EACpB,kBAAkB,EACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,QAAQ,EACb,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAClE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAevE,eAAO,MAAM,cAAc,mBAAuB,CAAC;AACnD,8CAA8C;AAC9C,eAAO,MAAM,KAAK,MAAM,CAAC;AAEzB;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GACpB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEN,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAE9E;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,gBAAoC,CAAC;;AAEtE;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAkB,SAAQ,sBAGf;CAAG;AAE3B,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,iBAAiB,EACzB,KAAK,QAAQ,EACb,SAAS,YAAY,uEAyCnB,CAAC;AAEL;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,WAAW,KACf,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAEzB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,iBAAiB,EACzB,KAAK,QAAQ,EACb,SAAS,YAAY,GAAG;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAA;CAAE,sCAc7D,CAAC;AAEJ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAU;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC,wIAiBG,CAAC;AAyBL;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,eAAe,CAAC,EAAE,CAAC,EAC9B,KAAK,QAAQ,EACb,SAAS;IACP,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAChC,sCAiCA,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,UAAU;IAC/C,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC,2GAeG,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GACzB,IAAI,EAAE,EACN,YAAW,gBAAqC,gEAc/C,CAAC"}
|