@entifix/mongo 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/adapters/make-mongo-facet-counter.d.ts.map +1 -1
- package/dist/adapters/make-mongo-facet-counter.js +3 -4
- package/dist/adapters/make-mongo-facet-counter.js.map +1 -1
- 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 +3 -3
- package/dist/adapters/make-mongo-repository.d.ts.map +1 -1
- package/dist/adapters/make-mongo-repository.js +11 -18
- package/dist/adapters/make-mongo-repository.js.map +1 -1
- package/dist/adapters/mongo-facet-counter-factory.d.ts.map +1 -1
- package/dist/adapters/mongo-facet-counter-factory.js +2 -4
- package/dist/adapters/mongo-facet-counter-factory.js.map +1 -1
- 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/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
|
@@ -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":"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"}
|
package/dist/outbox/relay.js
CHANGED
|
@@ -5,8 +5,17 @@ import { Context, Duration, Effect, Fiber, Result } from 'effect';
|
|
|
5
5
|
import { MongoDatabaseTag } from '../mongo-database/mongo-database.js';
|
|
6
6
|
import { recordOutboxStats } from './metrics.js';
|
|
7
7
|
import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
8
|
-
/** How often the sweep looks for entries the fast path did not carry. */
|
|
8
|
+
/** How often the sweep looks for entries the fast path did not carry. */ /**
|
|
9
|
+
* What an outbox entry records as the reason its publish failed: the failure's
|
|
10
|
+
* own message, or its code when it carries none — never empty, because an
|
|
11
|
+
* operator reading a quarantined entry has nothing else to go on.
|
|
12
|
+
*/ const failureText = (failure)=>failure.message === '' ? failure.code : failure.message;
|
|
13
|
+
export const SWEEP_INTERVAL = Duration.seconds(15);
|
|
9
14
|
/** Entries drained per pass, per database. */ export const BATCH = 100;
|
|
15
|
+
/**
|
|
16
|
+
* Hears nothing. The default only so a spec can start a relay without one —
|
|
17
|
+
* a host that passes no sink has a relay nobody can watch.
|
|
18
|
+
*/ export const ignoreRelayOutcome = ()=>Effect.void;
|
|
10
19
|
/**
|
|
11
20
|
* Publish attempts an outbox entry gets before it is quarantined, from
|
|
12
21
|
* config-service.
|
|
@@ -34,29 +43,44 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
|
34
43
|
* skip-so-the-head-moves: an entry that can never publish must stop blocking
|
|
35
44
|
* every entry behind it forever.
|
|
36
45
|
*/ export const drainOutbox = (outbox, bus, options)=>Effect.gen(function*() {
|
|
46
|
+
var _options_onOutcome;
|
|
37
47
|
const entries = yield* outbox.pending(BATCH);
|
|
38
48
|
let sent = 0;
|
|
49
|
+
const report = (_options_onOutcome = options.onOutcome) != null ? _options_onOutcome : ignoreRelayOutcome;
|
|
39
50
|
for (const entry of entries){
|
|
40
51
|
const outcome = yield* Effect.result(bus.publish(entry.event));
|
|
41
52
|
if (Result.isFailure(outcome)) {
|
|
53
|
+
var _options_onOutcome1;
|
|
42
54
|
const attempts = entry.attempts + 1;
|
|
43
55
|
const quarantine = attempts >= options.maxAttempts;
|
|
44
|
-
yield* outbox.recordFailure(entry, outcome.failure
|
|
56
|
+
yield* outbox.recordFailure(entry, failureText(outcome.failure), quarantine);
|
|
45
57
|
if (!quarantine) {
|
|
58
|
+
yield* report({
|
|
59
|
+
_tag: 'drained',
|
|
60
|
+
database: options.database,
|
|
61
|
+
sent
|
|
62
|
+
});
|
|
46
63
|
return sent;
|
|
47
64
|
}
|
|
48
|
-
yield*
|
|
65
|
+
yield* ((_options_onOutcome1 = options.onOutcome) != null ? _options_onOutcome1 : ignoreRelayOutcome)({
|
|
66
|
+
_tag: 'quarantined',
|
|
49
67
|
database: options.database,
|
|
50
68
|
eventId: entry.eventId,
|
|
51
69
|
eventName: entry.event.name,
|
|
52
70
|
attempts,
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
code: outcome.failure.code,
|
|
72
|
+
reason: failureText(outcome.failure)
|
|
73
|
+
});
|
|
55
74
|
continue;
|
|
56
75
|
}
|
|
57
76
|
yield* outbox.markSent(entry);
|
|
58
77
|
sent += 1;
|
|
59
78
|
}
|
|
79
|
+
yield* report({
|
|
80
|
+
_tag: 'drained',
|
|
81
|
+
database: options.database,
|
|
82
|
+
sent
|
|
83
|
+
});
|
|
60
84
|
return sent;
|
|
61
85
|
});
|
|
62
86
|
/**
|
|
@@ -70,18 +94,22 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
|
70
94
|
* Stats are sampled **after** the drain, so the gauge reports what is still
|
|
71
95
|
* waiting rather than what was waiting when the pass began.
|
|
72
96
|
*
|
|
73
|
-
* Every failure is caught and
|
|
74
|
-
* error that escapes ends the daemon silently — a
|
|
75
|
-
* indistinguishable from a relay with nothing to do.
|
|
97
|
+
* Every failure is caught and reported as `sweepFailed`. This runs inside
|
|
98
|
+
* `Effect.forever`, and an error that escapes ends the daemon silently — a
|
|
99
|
+
* relay that stopped is indistinguishable from a relay with nothing to do.
|
|
76
100
|
*/ export const sweepOutbox = (outbox, bus, options)=>Effect.gen(function*() {
|
|
77
101
|
var _options_onStats;
|
|
78
102
|
yield* drainOutbox(outbox, bus, options);
|
|
79
103
|
const sink = (_options_onStats = options.onStats) != null ? _options_onStats : recordOutboxStats;
|
|
80
104
|
yield* sink(options.database, (yield* outbox.stats()));
|
|
81
|
-
}).pipe(Effect.catch((
|
|
105
|
+
}).pipe(Effect.catch((failure)=>{
|
|
106
|
+
var _options_onOutcome;
|
|
107
|
+
return ((_options_onOutcome = options.onOutcome) != null ? _options_onOutcome : ignoreRelayOutcome)({
|
|
108
|
+
_tag: 'sweepFailed',
|
|
82
109
|
database: options.database,
|
|
83
|
-
|
|
84
|
-
})
|
|
110
|
+
failure
|
|
111
|
+
});
|
|
112
|
+
}));
|
|
85
113
|
/**
|
|
86
114
|
* Start the relay for a service that owns exactly one database.
|
|
87
115
|
*
|
|
@@ -106,6 +134,8 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
|
106
134
|
database: db.databaseName
|
|
107
135
|
}, (options == null ? void 0 : options.onStats) ? {
|
|
108
136
|
onStats: options.onStats
|
|
137
|
+
} : {}, (options == null ? void 0 : options.onOutcome) ? {
|
|
138
|
+
onOutcome: options.onOutcome
|
|
109
139
|
} : {})));
|
|
110
140
|
});
|
|
111
141
|
/**
|
|
@@ -135,7 +165,8 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
|
135
165
|
* pass: a sweep visits every tenant every interval, and the index build is the
|
|
136
166
|
* same request each time.
|
|
137
167
|
*/ export const sweepTenantOutboxes = (directory, bus, options)=>Effect.gen(function*() {
|
|
138
|
-
var _options_ensured;
|
|
168
|
+
var _options_onOutcome, _options_ensured;
|
|
169
|
+
const report = (_options_onOutcome = options.onOutcome) != null ? _options_onOutcome : ignoreRelayOutcome;
|
|
139
170
|
const ensured = (_options_ensured = options.ensured) != null ? _options_ensured : new Set();
|
|
140
171
|
const tenants = yield* directory.list();
|
|
141
172
|
for (const { handle: db } of tenants){
|
|
@@ -147,18 +178,24 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
|
147
178
|
}
|
|
148
179
|
yield* sweepOutbox(makeMongoOutbox(db), bus, _extends({
|
|
149
180
|
maxAttempts: options.maxAttempts,
|
|
150
|
-
database
|
|
181
|
+
database,
|
|
182
|
+
onOutcome: report
|
|
151
183
|
}, options.onStats ? {
|
|
152
184
|
onStats: options.onStats
|
|
153
185
|
} : {}));
|
|
154
|
-
}).pipe(Effect.catch((
|
|
186
|
+
}).pipe(Effect.catch((failure)=>report({
|
|
187
|
+
_tag: 'sweepFailed',
|
|
155
188
|
database,
|
|
156
|
-
|
|
157
|
-
})))
|
|
189
|
+
failure
|
|
190
|
+
})));
|
|
158
191
|
}
|
|
159
|
-
}).pipe(Effect.catch((
|
|
160
|
-
|
|
161
|
-
|
|
192
|
+
}).pipe(Effect.catch((failure)=>{
|
|
193
|
+
var _options_onOutcome;
|
|
194
|
+
return ((_options_onOutcome = options.onOutcome) != null ? _options_onOutcome : ignoreRelayOutcome)({
|
|
195
|
+
_tag: 'sweepFailed',
|
|
196
|
+
failure
|
|
197
|
+
});
|
|
198
|
+
}));
|
|
162
199
|
/**
|
|
163
200
|
* Start the relay for a store with one database per organization, listed by
|
|
164
201
|
* the {@link TenantDirectoryTag} the host provides.
|
|
@@ -176,6 +213,8 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
|
176
213
|
ensured
|
|
177
214
|
}, (options == null ? void 0 : options.onStats) ? {
|
|
178
215
|
onStats: options.onStats
|
|
216
|
+
} : {}, (options == null ? void 0 : options.onOutcome) ? {
|
|
217
|
+
onOutcome: options.onOutcome
|
|
179
218
|
} : {})));
|
|
180
219
|
});
|
|
181
220
|
/**
|
|
@@ -183,18 +222,20 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
|
|
|
183
222
|
*
|
|
184
223
|
* The fast path the sweep is the fallback for: an event drained here leaves as
|
|
185
224
|
* soon as its transaction commits, rather than up to {@link SWEEP_INTERVAL}
|
|
186
|
-
* later. Every failure is
|
|
187
|
-
* outbox, and the sweep will carry it.
|
|
188
|
-
*/ export const drainOutboxNow = (db)=>Effect.gen(function*() {
|
|
225
|
+
* later. Every failure is reported as `drainFailed` and swallowed — the entry
|
|
226
|
+
* is still in the outbox, and the sweep will carry it.
|
|
227
|
+
*/ export const drainOutboxNow = (db, onOutcome = ignoreRelayOutcome)=>Effect.gen(function*() {
|
|
189
228
|
const bus = yield* EventBusTag;
|
|
190
229
|
const maxAttempts = yield* OutboxMaxAttempts;
|
|
191
230
|
yield* drainOutbox(makeMongoOutbox(db), bus, {
|
|
192
231
|
maxAttempts,
|
|
193
|
-
database: db.databaseName
|
|
232
|
+
database: db.databaseName,
|
|
233
|
+
onOutcome
|
|
194
234
|
});
|
|
195
|
-
}).pipe(Effect.catch((
|
|
235
|
+
}).pipe(Effect.catch((failure)=>onOutcome({
|
|
236
|
+
_tag: 'drainFailed',
|
|
196
237
|
database: db.databaseName,
|
|
197
|
-
|
|
198
|
-
})))
|
|
238
|
+
failure
|
|
239
|
+
})));
|
|
199
240
|
|
|
200
241
|
//# sourceMappingURL=relay.js.map
|
package/dist/outbox/relay.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/outbox/relay.ts"],"sourcesContent":["import {\n ShutdownRegistryTag,\n type TenantDirectory,\n TenantDirectoryTag,\n} from '@entifix/business';\nimport {\n type EventBus,\n EventBusTag,\n type OutboxStats,\n type TransactionOutbox,\n} from '@entifix/transactions';\nimport { Context, Duration, Effect, Fiber, Result } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { MongoDatabaseTag } from '../mongo-database/mongo-database.js';\nimport { recordOutboxStats } from './metrics.js';\nimport { ensureOutboxIndexes, makeMongoOutbox } from './store.js';\n\n/** How often the sweep looks for entries the fast path did not carry. */\nexport const SWEEP_INTERVAL = Duration.seconds(15);\n/** Entries drained per pass, per database. */\nexport const BATCH = 100;\n\n/**\n * Publish attempts an outbox entry gets before it is quarantined, from\n * config-service.\n *\n * Configuration rather than a constant because it is a genuine operational\n * tunable: raising it while a flaky broker settles is a config edit, and the\n * next sweep reads the new value. That is the opposite of a subscription's\n * `maxAttempts`, which becomes an immutable queue argument and therefore stays\n * a literal beside its declaration.\n *\n * ⚠️ **Read it with `getNumber`, never a cast.** A value of `'five'` casts to\n * `NaN`, every `attempts >= maxAttempts` comparison goes false, and nothing is\n * ever quarantined — a relay that looks healthy while its head never moves.\n */\nexport class OutboxMaxAttempts extends Context.Service<\n OutboxMaxAttempts,\n number\n>()('OutboxMaxAttempts') {}\n\n/** What {@link drainOutbox} needs beyond the ports it publishes through. */\nexport interface DrainOptions {\n readonly maxAttempts: number;\n /** The database being drained; carried so a quarantine log names it. */\n readonly database: string;\n}\n\n/**\n * Publish one batch of pending entries, oldest first, and answer how many went.\n *\n * Two orderings are load-bearing here and neither is obvious:\n *\n * - A **still-deliverable** failure returns immediately rather than continuing.\n * The entries behind this one keep their order relative to it, which is what\n * stops a terminal event overtaking the `accepted` for the same transaction.\n * - A **quarantined** failure continues. That is the deliberate\n * skip-so-the-head-moves: an entry that can never publish must stop blocking\n * every entry behind it forever.\n */\nexport const drainOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions,\n) =>\n Effect.gen(function* () {\n const entries = yield* outbox.pending(BATCH);\n let sent = 0;\n for (const entry of entries) {\n const outcome = yield* Effect.result(bus.publish(entry.event));\n\n if (Result.isFailure(outcome)) {\n const attempts = entry.attempts + 1;\n const quarantine = attempts >= options.maxAttempts;\n\n yield* outbox.recordFailure(entry, outcome.failure.message, quarantine);\n\n if (!quarantine) {\n return sent;\n }\n\n yield* Effect.logError('outbox entry quarantined').pipe(\n Effect.annotateLogs({\n database: options.database,\n eventId: entry.eventId,\n eventName: entry.event.name,\n attempts,\n lastError: outcome.failure.message,\n }),\n );\n continue;\n }\n\n yield* outbox.markSent(entry);\n sent += 1;\n }\n return sent;\n });\n\n/**\n * Sample a sweep's depth and age.\n *\n * Defaults to {@link recordOutboxStats}, and a caller overrides it only to\n * assert what was sampled. There is no \"no sink\" option on purpose: an outbox\n * whose depth nothing reports is the failure the gauges exist to make visible.\n */\nexport type OutboxStatsSink = (\n database: string,\n stats: OutboxStats,\n) => Effect.Effect<void>;\n\n/**\n * Drain the one database this service owns, then sample its depth.\n *\n * The single-database counterpart to `startTenantOutboxRelay`. A\n * platform-plane store is one named database, so there is nothing to enumerate\n * and `listDatabases` — which needs a `MongoClient` and cluster-wide rights —\n * never comes into it.\n *\n * Stats are sampled **after** the drain, so the gauge reports what is still\n * waiting rather than what was waiting when the pass began.\n *\n * Every failure is caught and logged. This runs inside `Effect.forever`, and an\n * error that escapes ends the daemon silently — a relay that stopped is\n * indistinguishable from a relay with nothing to do.\n */\nexport const sweepOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions & { readonly onStats?: OutboxStatsSink },\n) =>\n Effect.gen(function* () {\n yield* drainOutbox(outbox, bus, options);\n const sink = options.onStats ?? recordOutboxStats;\n yield* sink(options.database, yield* outbox.stats());\n }).pipe(\n Effect.catch(error =>\n Effect.logError('outbox sweep failed').pipe(\n Effect.annotateLogs({\n database: options.database,\n error: String(error),\n }),\n ),\n ),\n );\n\n/**\n * Start the relay for a service that owns exactly one database.\n *\n * ⚠️ **The shutdown hook is `flush`, not `stop-intake`, and the phase is the\n * whole point.** `AmqpEventBusLayer` registers `stop-intake` to cancel consumers\n * and drain deliveries; this runs after that, so the last sweep publishes into a\n * connection that is still open. Registered the other way round it would try to\n * publish through a channel already closing, and the entries it was holding\n * would sit until the next boot.\n *\n * The daemon is `delay |> forever`, so the first sweep is one interval away\n * rather than at boot — the write path drains inline, and a sweep racing the\n * index bootstrap on a cold database only produces noise.\n */\nexport const startOutboxRelay = (options?: {\n readonly onStats?: OutboxStatsSink;\n}) =>\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n\n yield* ensureOutboxIndexes(db);\n const outbox = makeMongoOutbox(db);\n\n yield* runRelayDaemon(\n sweepOutbox(outbox, bus, {\n maxAttempts,\n database: db.databaseName,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n }),\n );\n });\n\n/**\n * Runs `sweepOnce` every {@link SWEEP_INTERVAL}, and once more on the way out.\n *\n * Interrupting the daemon before that last pass is what stops two sweeps\n * publishing the same entry; running the pass is what the hook is for — an\n * event committed a moment before shutdown would otherwise wait out the next\n * process's whole interval.\n */\nconst runRelayDaemon = (sweepOnce: Effect.Effect<void>) =>\n Effect.gen(function* () {\n const shutdown = yield* ShutdownRegistryTag;\n\n const daemon = yield* Effect.forkDetach(\n sweepOnce.pipe(Effect.delay(SWEEP_INTERVAL), Effect.forever),\n );\n\n yield* shutdown.register({\n name: 'outbox-relay',\n phase: 'flush',\n run: Fiber.interrupt(daemon).pipe(Effect.andThen(sweepOnce)),\n });\n });\n\n/**\n * One pass over every organization's outbox in a per-organization store.\n *\n * ⚠️ **Each tenant is caught on its own.** A failure that recurs — an index\n * conflict on one old database, say — would otherwise abandon every tenant\n * listed after it on every pass, while the relay went on looking healthy.\n *\n * Indexes are ensured once per database per process (`ensured`), not on every\n * pass: a sweep visits every tenant every interval, and the index build is the\n * same request each time.\n */\nexport const sweepTenantOutboxes = (\n directory: TenantDirectory<Db>,\n bus: EventBus,\n options: {\n readonly maxAttempts: number;\n readonly onStats?: OutboxStatsSink;\n readonly ensured?: Set<string>;\n },\n) =>\n Effect.gen(function* () {\n const ensured = options.ensured ?? new Set<string>();\n const tenants = yield* directory.list();\n\n for (const { handle: db } of tenants) {\n const database = db.databaseName;\n yield* Effect.gen(function* () {\n if (!ensured.has(database)) {\n yield* ensureOutboxIndexes(db);\n ensured.add(database);\n }\n yield* sweepOutbox(makeMongoOutbox(db), bus, {\n maxAttempts: options.maxAttempts,\n database,\n ...(options.onStats ? { onStats: options.onStats } : {}),\n });\n }).pipe(\n Effect.catch(error =>\n Effect.logError('draining a tenant outbox failed').pipe(\n Effect.annotateLogs({ database, error: String(error) }),\n ),\n ),\n );\n }\n }).pipe(\n Effect.catch(error =>\n Effect.logError('tenant outbox sweep failed').pipe(\n Effect.annotateLogs({ error: String(error) }),\n ),\n ),\n );\n\n/**\n * Start the relay for a store with one database per organization, listed by\n * the {@link TenantDirectoryTag} the host provides.\n *\n * The same two speeds as {@link startOutboxRelay}: the write path drains its\n * own tenant inline with {@link drainOutboxNow}, and this sweep carries\n * whatever that missed.\n */\nexport const startTenantOutboxRelay = (options?: {\n readonly onStats?: OutboxStatsSink;\n}) =>\n Effect.gen(function* () {\n const directory = (yield* TenantDirectoryTag) as TenantDirectory<Db>;\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n const ensured = new Set<string>();\n\n yield* runRelayDaemon(\n sweepTenantOutboxes(directory, bus, {\n maxAttempts,\n ensured,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n }),\n );\n });\n\n/**\n * Drain one database's outbox now, for a write path that has just committed.\n *\n * The fast path the sweep is the fallback for: an event drained here leaves as\n * soon as its transaction commits, rather than up to {@link SWEEP_INTERVAL}\n * later. Every failure is logged and swallowed — the entry is still in the\n * outbox, and the sweep will carry it.\n */\nexport const drainOutboxNow = (db: Db) =>\n Effect.gen(function* () {\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n yield* drainOutbox(makeMongoOutbox(db), bus, {\n maxAttempts,\n database: db.databaseName,\n });\n }).pipe(\n Effect.catch(error =>\n Effect.logWarning('inline outbox drain failed').pipe(\n Effect.annotateLogs({\n database: db.databaseName,\n error: String(error),\n }),\n ),\n ),\n );\n"],"names":["ShutdownRegistryTag","TenantDirectoryTag","EventBusTag","Context","Duration","Effect","Fiber","Result","MongoDatabaseTag","recordOutboxStats","ensureOutboxIndexes","makeMongoOutbox","SWEEP_INTERVAL","seconds","BATCH","OutboxMaxAttempts","Service","drainOutbox","outbox","bus","options","gen","entries","pending","sent","entry","outcome","result","publish","event","isFailure","attempts","quarantine","maxAttempts","recordFailure","failure","message","logError","pipe","annotateLogs","database","eventId","eventName","name","lastError","markSent","sweepOutbox","sink","onStats","stats","catch","error","String","startOutboxRelay","db","runRelayDaemon","databaseName","sweepOnce","shutdown","daemon","forkDetach","delay","forever","register","phase","run","interrupt","andThen","sweepTenantOutboxes","directory","ensured","Set","tenants","list","handle","has","add","startTenantOutboxRelay","drainOutboxNow","logWarning"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/outbox/relay.ts"],"sourcesContent":["import {\n ShutdownRegistryTag,\n type TenantDirectory,\n TenantDirectoryTag,\n} from '@entifix/business';\nimport {\n type EventBus,\n EventBusTag,\n type OutboxStats,\n type TransactionOutbox,\n} from '@entifix/transactions';\nimport { Context, Duration, Effect, Fiber, Result } from 'effect';\nimport type { Db } from 'mongodb';\n\nimport { MongoDatabaseTag } from '../mongo-database/mongo-database.js';\nimport { recordOutboxStats } from './metrics.js';\nimport { ensureOutboxIndexes, makeMongoOutbox } from './store.js';\n\n/** How often the sweep looks for entries the fast path did not carry. */\n/**\n * What an outbox entry records as the reason its publish failed: the failure's\n * own message, or its code when it carries none — never empty, because an\n * operator reading a quarantined entry has nothing else to go on.\n */\nconst failureText = (failure: {\n readonly message: string;\n readonly code: string;\n}) => (failure.message === '' ? failure.code : failure.message);\n\nexport const SWEEP_INTERVAL = Duration.seconds(15);\n/** Entries drained per pass, per database. */\nexport const BATCH = 100;\n\n/**\n * What a relay pass did, for the host to log\n * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).\n *\n * The relay decides nothing about level or sink: it says what happened and the\n * fork site that started it — the slice's shell — decides how loud that is. A\n * `failure` is whatever the store or the bus failed with; a typed one carries\n * its code and category for the log line.\n */\nexport type RelayOutcome =\n | {\n readonly _tag: 'drained';\n readonly database: string;\n readonly sent: number;\n }\n | {\n readonly _tag: 'quarantined';\n readonly database: string;\n readonly eventId: string;\n readonly eventName: string;\n readonly attempts: number;\n readonly code: string;\n readonly reason: string;\n }\n | {\n readonly _tag: 'sweepFailed';\n readonly database?: string;\n readonly failure: unknown;\n }\n | {\n readonly _tag: 'drainFailed';\n readonly database: string;\n readonly failure: unknown;\n };\n\n/** Where a relay's outcomes go. The host's fork site supplies it. */\nexport type RelayOutcomeSink = (outcome: RelayOutcome) => Effect.Effect<void>;\n\n/**\n * Hears nothing. The default only so a spec can start a relay without one —\n * a host that passes no sink has a relay nobody can watch.\n */\nexport const ignoreRelayOutcome: RelayOutcomeSink = () => Effect.void;\n\n/**\n * Publish attempts an outbox entry gets before it is quarantined, from\n * config-service.\n *\n * Configuration rather than a constant because it is a genuine operational\n * tunable: raising it while a flaky broker settles is a config edit, and the\n * next sweep reads the new value. That is the opposite of a subscription's\n * `maxAttempts`, which becomes an immutable queue argument and therefore stays\n * a literal beside its declaration.\n *\n * ⚠️ **Read it with `getNumber`, never a cast.** A value of `'five'` casts to\n * `NaN`, every `attempts >= maxAttempts` comparison goes false, and nothing is\n * ever quarantined — a relay that looks healthy while its head never moves.\n */\nexport class OutboxMaxAttempts extends Context.Service<\n OutboxMaxAttempts,\n number\n>()('OutboxMaxAttempts') {}\n\n/** What {@link drainOutbox} needs beyond the ports it publishes through. */\nexport interface DrainOptions {\n readonly maxAttempts: number;\n /** The database being drained; carried so a quarantine log names it. */\n readonly database: string;\n /** Where the pass reports what it did; see {@link RelayOutcome}. */\n readonly onOutcome?: RelayOutcomeSink;\n}\n\n/**\n * Publish one batch of pending entries, oldest first, and answer how many went.\n *\n * Two orderings are load-bearing here and neither is obvious:\n *\n * - A **still-deliverable** failure returns immediately rather than continuing.\n * The entries behind this one keep their order relative to it, which is what\n * stops a terminal event overtaking the `accepted` for the same transaction.\n * - A **quarantined** failure continues. That is the deliberate\n * skip-so-the-head-moves: an entry that can never publish must stop blocking\n * every entry behind it forever.\n */\nexport const drainOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions,\n) =>\n Effect.gen(function* () {\n const entries = yield* outbox.pending(BATCH);\n let sent = 0;\n const report = options.onOutcome ?? ignoreRelayOutcome;\n for (const entry of entries) {\n const outcome = yield* Effect.result(bus.publish(entry.event));\n\n if (Result.isFailure(outcome)) {\n const attempts = entry.attempts + 1;\n const quarantine = attempts >= options.maxAttempts;\n\n yield* outbox.recordFailure(\n entry,\n failureText(outcome.failure),\n quarantine,\n );\n\n if (!quarantine) {\n yield* report({ _tag: 'drained', database: options.database, sent });\n return sent;\n }\n\n yield* (options.onOutcome ?? ignoreRelayOutcome)({\n _tag: 'quarantined',\n database: options.database,\n eventId: entry.eventId,\n eventName: entry.event.name,\n attempts,\n code: outcome.failure.code,\n reason: failureText(outcome.failure),\n });\n continue;\n }\n\n yield* outbox.markSent(entry);\n sent += 1;\n }\n yield* report({ _tag: 'drained', database: options.database, sent });\n return sent;\n });\n\n/**\n * Sample a sweep's depth and age.\n *\n * Defaults to {@link recordOutboxStats}, and a caller overrides it only to\n * assert what was sampled. There is no \"no sink\" option on purpose: an outbox\n * whose depth nothing reports is the failure the gauges exist to make visible.\n */\nexport type OutboxStatsSink = (\n database: string,\n stats: OutboxStats,\n) => Effect.Effect<void>;\n\n/**\n * Drain the one database this service owns, then sample its depth.\n *\n * The single-database counterpart to `startTenantOutboxRelay`. A\n * platform-plane store is one named database, so there is nothing to enumerate\n * and `listDatabases` — which needs a `MongoClient` and cluster-wide rights —\n * never comes into it.\n *\n * Stats are sampled **after** the drain, so the gauge reports what is still\n * waiting rather than what was waiting when the pass began.\n *\n * Every failure is caught and reported as `sweepFailed`. This runs inside\n * `Effect.forever`, and an error that escapes ends the daemon silently — a\n * relay that stopped is indistinguishable from a relay with nothing to do.\n */\nexport const sweepOutbox = (\n outbox: TransactionOutbox,\n bus: EventBus,\n options: DrainOptions & { readonly onStats?: OutboxStatsSink },\n) =>\n Effect.gen(function* () {\n yield* drainOutbox(outbox, bus, options);\n const sink = options.onStats ?? recordOutboxStats;\n yield* sink(options.database, yield* outbox.stats());\n }).pipe(\n Effect.catch(failure =>\n (options.onOutcome ?? ignoreRelayOutcome)({\n _tag: 'sweepFailed',\n database: options.database,\n failure,\n }),\n ),\n );\n\n/**\n * Start the relay for a service that owns exactly one database.\n *\n * ⚠️ **The shutdown hook is `flush`, not `stop-intake`, and the phase is the\n * whole point.** `AmqpEventBusLayer` registers `stop-intake` to cancel consumers\n * and drain deliveries; this runs after that, so the last sweep publishes into a\n * connection that is still open. Registered the other way round it would try to\n * publish through a channel already closing, and the entries it was holding\n * would sit until the next boot.\n *\n * The daemon is `delay |> forever`, so the first sweep is one interval away\n * rather than at boot — the write path drains inline, and a sweep racing the\n * index bootstrap on a cold database only produces noise.\n */\nexport const startOutboxRelay = (options?: {\n readonly onStats?: OutboxStatsSink;\n readonly onOutcome?: RelayOutcomeSink;\n}) =>\n Effect.gen(function* () {\n const db = yield* MongoDatabaseTag;\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n\n yield* ensureOutboxIndexes(db);\n const outbox = makeMongoOutbox(db);\n\n yield* runRelayDaemon(\n sweepOutbox(outbox, bus, {\n maxAttempts,\n database: db.databaseName,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n ...(options?.onOutcome ? { onOutcome: options.onOutcome } : {}),\n }),\n );\n });\n\n/**\n * Runs `sweepOnce` every {@link SWEEP_INTERVAL}, and once more on the way out.\n *\n * Interrupting the daemon before that last pass is what stops two sweeps\n * publishing the same entry; running the pass is what the hook is for — an\n * event committed a moment before shutdown would otherwise wait out the next\n * process's whole interval.\n */\nconst runRelayDaemon = (sweepOnce: Effect.Effect<void>) =>\n Effect.gen(function* () {\n const shutdown = yield* ShutdownRegistryTag;\n\n const daemon = yield* Effect.forkDetach(\n sweepOnce.pipe(Effect.delay(SWEEP_INTERVAL), Effect.forever),\n );\n\n yield* shutdown.register({\n name: 'outbox-relay',\n phase: 'flush',\n run: Fiber.interrupt(daemon).pipe(Effect.andThen(sweepOnce)),\n });\n });\n\n/**\n * One pass over every organization's outbox in a per-organization store.\n *\n * ⚠️ **Each tenant is caught on its own.** A failure that recurs — an index\n * conflict on one old database, say — would otherwise abandon every tenant\n * listed after it on every pass, while the relay went on looking healthy.\n *\n * Indexes are ensured once per database per process (`ensured`), not on every\n * pass: a sweep visits every tenant every interval, and the index build is the\n * same request each time.\n */\nexport const sweepTenantOutboxes = (\n directory: TenantDirectory<Db>,\n bus: EventBus,\n options: {\n readonly maxAttempts: number;\n readonly onStats?: OutboxStatsSink;\n readonly onOutcome?: RelayOutcomeSink;\n readonly ensured?: Set<string>;\n },\n) =>\n Effect.gen(function* () {\n const report = options.onOutcome ?? ignoreRelayOutcome;\n const ensured = options.ensured ?? new Set<string>();\n const tenants = yield* directory.list();\n\n for (const { handle: db } of tenants) {\n const database = db.databaseName;\n yield* Effect.gen(function* () {\n if (!ensured.has(database)) {\n yield* ensureOutboxIndexes(db);\n ensured.add(database);\n }\n yield* sweepOutbox(makeMongoOutbox(db), bus, {\n maxAttempts: options.maxAttempts,\n database,\n onOutcome: report,\n ...(options.onStats ? { onStats: options.onStats } : {}),\n });\n }).pipe(\n Effect.catch(failure =>\n report({ _tag: 'sweepFailed', database, failure }),\n ),\n );\n }\n }).pipe(\n Effect.catch(failure =>\n (options.onOutcome ?? ignoreRelayOutcome)({\n _tag: 'sweepFailed',\n failure,\n }),\n ),\n );\n\n/**\n * Start the relay for a store with one database per organization, listed by\n * the {@link TenantDirectoryTag} the host provides.\n *\n * The same two speeds as {@link startOutboxRelay}: the write path drains its\n * own tenant inline with {@link drainOutboxNow}, and this sweep carries\n * whatever that missed.\n */\nexport const startTenantOutboxRelay = (options?: {\n readonly onStats?: OutboxStatsSink;\n readonly onOutcome?: RelayOutcomeSink;\n}) =>\n Effect.gen(function* () {\n const directory = (yield* TenantDirectoryTag) as TenantDirectory<Db>;\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n const ensured = new Set<string>();\n\n yield* runRelayDaemon(\n sweepTenantOutboxes(directory, bus, {\n maxAttempts,\n ensured,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n ...(options?.onOutcome ? { onOutcome: options.onOutcome } : {}),\n }),\n );\n });\n\n/**\n * Drain one database's outbox now, for a write path that has just committed.\n *\n * The fast path the sweep is the fallback for: an event drained here leaves as\n * soon as its transaction commits, rather than up to {@link SWEEP_INTERVAL}\n * later. Every failure is reported as `drainFailed` and swallowed — the entry\n * is still in the outbox, and the sweep will carry it.\n */\nexport const drainOutboxNow = (\n db: Db,\n onOutcome: RelayOutcomeSink = ignoreRelayOutcome,\n) =>\n Effect.gen(function* () {\n const bus = yield* EventBusTag;\n const maxAttempts = yield* OutboxMaxAttempts;\n yield* drainOutbox(makeMongoOutbox(db), bus, {\n maxAttempts,\n database: db.databaseName,\n onOutcome,\n });\n }).pipe(\n Effect.catch(failure =>\n onOutcome({ _tag: 'drainFailed', database: db.databaseName, failure }),\n ),\n );\n"],"names":["ShutdownRegistryTag","TenantDirectoryTag","EventBusTag","Context","Duration","Effect","Fiber","Result","MongoDatabaseTag","recordOutboxStats","ensureOutboxIndexes","makeMongoOutbox","failureText","failure","message","code","SWEEP_INTERVAL","seconds","BATCH","ignoreRelayOutcome","void","OutboxMaxAttempts","Service","drainOutbox","outbox","bus","options","gen","entries","pending","sent","report","onOutcome","entry","outcome","result","publish","event","isFailure","attempts","quarantine","maxAttempts","recordFailure","_tag","database","eventId","eventName","name","reason","markSent","sweepOutbox","sink","onStats","stats","pipe","catch","startOutboxRelay","db","runRelayDaemon","databaseName","sweepOnce","shutdown","daemon","forkDetach","delay","forever","register","phase","run","interrupt","andThen","sweepTenantOutboxes","directory","ensured","Set","tenants","list","handle","has","add","startTenantOutboxRelay","drainOutboxNow"],"mappings":"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"}
|
package/dist/outbox/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { DomainEvent } from '@entifix/core';
|
|
2
2
|
import type { OutboxEntry, TransactionOutbox } from '@entifix/transactions';
|
|
3
3
|
import { Effect } from 'effect';
|
|
4
4
|
import type { Db } from 'mongodb';
|
|
@@ -50,7 +50,7 @@ export declare const outboxDocument: (event: DomainEvent) => OutboxEntry;
|
|
|
50
50
|
*
|
|
51
51
|
* Answers whether an entry was actually revived.
|
|
52
52
|
*/
|
|
53
|
-
export declare const reviveQuarantined: (db: Db, eventId: string) => Effect.Effect<boolean,
|
|
53
|
+
export declare const reviveQuarantined: (db: Db, eventId: string) => Effect.Effect<boolean, import("@entifix/core").StoreFailure, never>;
|
|
54
54
|
/**
|
|
55
55
|
* Ensures the two indexes the outbox depends on.
|
|
56
56
|
*
|
|
@@ -70,7 +70,7 @@ export declare const reviveQuarantined: (db: Db, eventId: string) => Effect.Effe
|
|
|
70
70
|
* logs what it catches. The fix is `pnpm run <app>:dev:reset`; nothing runs in
|
|
71
71
|
* production, so there is no migration to write.
|
|
72
72
|
*/
|
|
73
|
-
export declare const ensureOutboxIndexes: (db: Db) => Effect.Effect<void,
|
|
73
|
+
export declare const ensureOutboxIndexes: (db: Db) => Effect.Effect<void, import("@entifix/core").StoreFailure, never>;
|
|
74
74
|
/**
|
|
75
75
|
* Mongo-backed {@link TransactionOutbox} over one database.
|
|
76
76
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/outbox/store.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/outbox/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAIlC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAatD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,WAAW,KAAG,WAWlD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,iBAAiB,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,wEAiBrD,CAAC;AAEL;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,GAAI,IAAI,EAAE,qEAWtC,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,EAAE,KAAG,iBA4FxC,CAAC"}
|