@entifix/mongo 0.1.4 → 0.3.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.
@@ -1,11 +1,11 @@
1
1
  import { EntifixConnError } from '@entifix/core';
2
2
  import { Context, Layer } from 'effect';
3
3
  import { Db, MongoClient } from 'mongodb';
4
- declare const MongoDatabaseTag_base: Context.TagClass<MongoDatabaseTag, "MongoDatabaseTag", Db>;
4
+ declare const MongoDatabaseTag_base: Context.ServiceClass<MongoDatabaseTag, "MongoDatabaseTag", Db>;
5
5
  /** DI tag carrying a connected mongodb {@link Db} handle. */
6
6
  export declare class MongoDatabaseTag extends MongoDatabaseTag_base {
7
7
  }
8
- declare const MongoClientTag_base: Context.TagClass<MongoClientTag, "MongoClientTag", MongoClient>;
8
+ declare const MongoClientTag_base: Context.ServiceClass<MongoClientTag, "MongoClientTag", MongoClient>;
9
9
  /**
10
10
  * DI tag carrying the connected {@link MongoClient} itself — the **pool**.
11
11
  *
@@ -1 +1 @@
1
- {"version":3,"file":"mongo-database.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAU,KAAK,EAAY,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;;AAE1C,6DAA6D;AAC7D,qBAAa,gBAAiB,SAAQ,qBAGnC;CAAG;;AAEN;;;;;;;;;GASG;AACH,qBAAa,cAAe,SAAQ,mBAGjC;CAAG;AAEN,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,gBAAgB,CAyB5C,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,qBAAqB,KAC9B,KAAK,CAAC,KAAK,CAAC,gBAAgB,GAAG,cAAc,EAAE,gBAAgB,CAO/D,CAAC"}
1
+ {"version":3,"file":"mongo-database.d.ts","sourceRoot":"","sources":["../../src/mongo-database/mongo-database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAU,KAAK,EAAY,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;;AAE1C,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,gBAAgB,CA2B5C,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,qBAAqB,KAC9B,KAAK,CAAC,KAAK,CAAC,gBAAgB,GAAG,cAAc,EAAE,gBAAgB,CAO/D,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { EntifixConnError } from '@entifix/core';
2
2
  import { Context, Effect, Layer, Schedule } from 'effect';
3
3
  import { MongoClient } from 'mongodb';
4
- /** DI tag carrying a connected mongodb {@link Db} handle. */ export class MongoDatabaseTag extends Context.Tag('MongoDatabaseTag')() {
4
+ /** DI tag carrying a connected mongodb {@link Db} handle. */ export class MongoDatabaseTag extends Context.Service()('MongoDatabaseTag') {
5
5
  }
6
6
  /**
7
7
  * DI tag carrying the connected {@link MongoClient} itself — the **pool**.
@@ -12,7 +12,7 @@ import { MongoClient } from 'mongodb';
12
12
  * layers over the same URI would mean two pools.
13
13
  *
14
14
  * Reach for {@link MongoDatabaseTag} unless you are resolving tenant storage.
15
- */ export class MongoClientTag extends Context.Tag('MongoClientTag')() {
15
+ */ export class MongoClientTag extends Context.Service()('MongoClientTag') {
16
16
  }
17
17
  /**
18
18
  * A scoped {@link Layer} that opens a {@link MongoClient} on acquire and closes
@@ -27,7 +27,7 @@ import { MongoClient } from 'mongodb';
27
27
  *
28
28
  * {@link MongoDatabaseLayer} builds on this rather than connecting again: two
29
29
  * layers over the same URI would mean two pools.
30
- */ export const MongoClientLayer = (settings)=>Layer.scoped(MongoClientTag, Effect.acquireRelease(Effect.tryPromise({
30
+ */ export const MongoClientLayer = (settings)=>Layer.effect(MongoClientTag, Effect.acquireRelease(Effect.tryPromise({
31
31
  try: ()=>MongoClient.connect(settings.uri, {
32
32
  // The 30s default means a readiness probe against a missing
33
33
  // primary sits for 30s; the registry would time it out first and
@@ -39,7 +39,9 @@ import { MongoClient } from 'mongodb';
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.
42
- Effect.retry(Schedule.exponential('250 millis').pipe(Schedule.upTo('30 seconds')))), (client)=>Effect.promise(()=>client.close())));
42
+ Effect.retry(Schedule.exponential('250 millis').pipe(Schedule.upTo({
43
+ duration: '30 seconds'
44
+ })))), (client)=>Effect.promise(()=>client.close())));
43
45
  /**
44
46
  * {@link MongoClientLayer} plus the named {@link Db} resolved from it, exposed
45
47
  * under {@link MongoDatabaseTag}. One acquire, one pool, both tags — which is
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/mongo-database/mongo-database.ts"],"sourcesContent":["import { EntifixConnError } from '@entifix/core';\nimport { Context, Effect, Layer, Schedule } from 'effect';\nimport { Db, MongoClient } from 'mongodb';\n\n/** DI tag carrying a connected mongodb {@link Db} handle. */\nexport class MongoDatabaseTag extends Context.Tag('MongoDatabaseTag')<\n MongoDatabaseTag,\n Db\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.Tag('MongoClientTag')<\n MongoClientTag,\n MongoClient\n>() {}\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 register, which is the thing ADR 0020 forbids.\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, EntifixConnError> =>\n Layer.scoped(\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 =>\n new EntifixConnError('Failed to connect to MongoDB', 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(Schedule.upTo('30 seconds')),\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, EntifixConnError> =>\n Layer.provideMerge(\n Layer.effect(\n MongoDatabaseTag,\n Effect.map(MongoClientTag, client => client.db(settings.dbName)),\n ),\n MongoClientLayer(settings),\n );\n"],"names":["EntifixConnError","Context","Effect","Layer","Schedule","MongoClient","MongoDatabaseTag","Tag","MongoClientTag","MongoClientLayer","settings","scoped","acquireRelease","tryPromise","try","connect","uri","serverSelectionTimeoutMS","catch","error","pipe","retry","exponential","upTo","client","promise","close","MongoDatabaseLayer","provideMerge","effect","map","db","dbName"],"mappings":"AAAA,SAASA,gBAAgB,QAAQ,gBAAgB;AACjD,SAASC,OAAO,EAAEC,MAAM,EAAEC,KAAK,EAAEC,QAAQ,QAAQ,SAAS;AAC1D,SAAaC,WAAW,QAAQ,UAAU;AAE1C,2DAA2D,GAC3D,OAAO,MAAMC,yBAAyBL,QAAQM,GAAG,CAAC;AAG7C;AAEL;;;;;;;;;CASC,GACD,OAAO,MAAMC,uBAAuBP,QAAQM,GAAG,CAAC;AAG3C;AAUL;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAME,mBAAmB,CAC9BC,WAEAP,MAAMQ,MAAM,CACVH,gBACAN,OAAOU,cAAc,CACnBV,OAAOW,UAAU,CAAC;QAChBC,KAAK,IACHT,YAAYU,OAAO,CAACL,SAASM,GAAG,EAAE;gBAChC,4DAA4D;gBAC5D,iEAAiE;gBACjE,iEAAiE;gBACjE,sCAAsC;gBACtCC,0BAA0B;YAC5B;QACFC,OAAOC,CAAAA,QACL,IAAInB,iBAAiB,gCAAgCmB;IACzD,GAAGC,IAAI,CACL,oEAAoE;IACpE,sEAAsE;IACtE,mEAAmE;IACnElB,OAAOmB,KAAK,CACVjB,SAASkB,WAAW,CAAC,cAAcF,IAAI,CAAChB,SAASmB,IAAI,CAAC,kBAG1DC,CAAAA,SAAUtB,OAAOuB,OAAO,CAAC,IAAMD,OAAOE,KAAK,MAE7C;AAEJ;;;;;;;CAOC,GACD,OAAO,MAAMC,qBAAqB,CAChCjB,WAEAP,MAAMyB,YAAY,CAChBzB,MAAM0B,MAAM,CACVvB,kBACAJ,OAAO4B,GAAG,CAACtB,gBAAgBgB,CAAAA,SAAUA,OAAOO,EAAE,CAACrB,SAASsB,MAAM,KAEhEvB,iBAAiBC,WACjB"}
1
+ {"version":3,"sources":["../../src/mongo-database/mongo-database.ts"],"sourcesContent":["import { EntifixConnError } from '@entifix/core';\nimport { Context, Effect, Layer, Schedule } from 'effect';\nimport { Db, MongoClient } from 'mongodb';\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 register, which is the thing ADR 0020 forbids.\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, EntifixConnError> =>\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 =>\n new EntifixConnError('Failed to connect to MongoDB', 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, EntifixConnError> =>\n Layer.provideMerge(\n Layer.effect(\n MongoDatabaseTag,\n Effect.map(MongoClientTag, client => client.db(settings.dbName)),\n ),\n MongoClientLayer(settings),\n );\n"],"names":["EntifixConnError","Context","Effect","Layer","Schedule","MongoClient","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":"AAAA,SAASA,gBAAgB,QAAQ,gBAAgB;AACjD,SAASC,OAAO,EAAEC,MAAM,EAAEC,KAAK,EAAEC,QAAQ,QAAQ,SAAS;AAC1D,SAAaC,WAAW,QAAQ,UAAU;AAE1C,2DAA2D,GAC3D,OAAO,MAAMC,yBAAyBL,QAAQM,OAAO,GACnD;AACC;AAEH;;;;;;;;;CASC,GACD,OAAO,MAAMC,uBAAuBP,QAAQM,OAAO,GAG/C;AAAmB;AAUvB;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAME,mBAAmB,CAC9BC,WAEAP,MAAMQ,MAAM,CACVH,gBACAN,OAAOU,cAAc,CACnBV,OAAOW,UAAU,CAAC;QAChBC,KAAK,IACHT,YAAYU,OAAO,CAACL,SAASM,GAAG,EAAE;gBAChC,4DAA4D;gBAC5D,iEAAiE;gBACjE,iEAAiE;gBACjE,sCAAsC;gBACtCC,0BAA0B;YAC5B;QACFC,OAAOC,CAAAA,QACL,IAAInB,iBAAiB,gCAAgCmB;IACzD,GAAGC,IAAI,CACL,oEAAoE;IACpE,sEAAsE;IACtE,mEAAmE;IACnElB,OAAOmB,KAAK,CACVjB,SAASkB,WAAW,CAAC,cAAcF,IAAI,CACrChB,SAASmB,IAAI,CAAC;QAAEC,UAAU;IAAa,OAI7CC,CAAAA,SAAUvB,OAAOwB,OAAO,CAAC,IAAMD,OAAOE,KAAK,MAE7C;AAEJ;;;;;;;CAOC,GACD,OAAO,MAAMC,qBAAqB,CAChClB,WAEAP,MAAM0B,YAAY,CAChB1B,MAAMQ,MAAM,CACVL,kBACAJ,OAAO4B,GAAG,CAACtB,gBAAgBiB,CAAAA,SAAUA,OAAOM,EAAE,CAACrB,SAASsB,MAAM,KAEhEvB,iBAAiBC,WACjB"}
@@ -32,7 +32,7 @@ import { MongoClientTag } from './mongo-database.js';
32
32
  check: Effect.tryPromise(()=>client.db('admin').command({
33
33
  ping: 1
34
34
  })).pipe(Effect.as(true), // Unreachable is a `false`, never a failed endpoint.
35
- Effect.catchAll(()=>Effect.succeed(false)))
35
+ Effect.catch(()=>Effect.succeed(false)))
36
36
  });
37
37
  }));
38
38
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/mongo-database/mongo-health-probe.ts"],"sourcesContent":["import { HealthRegistryTag } from '@entifix/business';\nimport { Effect, Layer } from 'effect';\n\nimport { MongoClientTag } from './mongo-database.js';\n\n/** Probe name reported by `/api/health/ready` when Mongo is unreachable. */\nexport const MONGO_PROBE_NAME = 'mongo';\n\n/**\n * Registers a readiness probe for the Mongo **connection**: `admin().ping()`,\n * which is the cheapest round trip the driver offers and, unlike \"is the client\n * object non-null\", actually crosses the wire.\n *\n * It depends on {@link MongoClientTag}, not on a named database, because the\n * ping targets the `admin` database and never touches the connected one — so a\n * service whose handles are all per-request (tenant storage) does not have to\n * invent a database name to become observable. Naming one it never writes is\n * exactly the phantom store ADR 0020 rules out.\n *\n * Merge it beside {@link MongoClientLayer} in a service's `AppLayer` and the\n * service's readiness endpoint starts reporting Mongo — nothing in the service\n * describes the probe, so it cannot drift from the dependency it describes.\n *\n * `stores` names the Stores this connection backs, by their register name in\n * `tools/slices/` — `['catalog', 'saga']`, never a database name and never a\n * URI. It is the composition root's to supply because a client package cannot\n * know it, and it is a list because one connection routinely backs several\n * Stores (ADR 0031).\n */\nexport const MongoHealthProbeLayer = (\n stores: readonly string[],\n): Layer.Layer<never, never, MongoClientTag | HealthRegistryTag> =>\n Layer.effectDiscard(\n Effect.gen(function* () {\n const client = yield* MongoClientTag;\n const registry = yield* HealthRegistryTag;\n\n yield* registry.register({\n name: MONGO_PROBE_NAME,\n kind: 'datastore',\n targets: stores,\n check: Effect.tryPromise(() =>\n client.db('admin').command({ ping: 1 }),\n ).pipe(\n Effect.as(true),\n // Unreachable is a `false`, never a failed endpoint.\n Effect.catchAll(() => Effect.succeed(false)),\n ),\n });\n }),\n );\n"],"names":["HealthRegistryTag","Effect","Layer","MongoClientTag","MONGO_PROBE_NAME","MongoHealthProbeLayer","stores","effectDiscard","gen","client","registry","register","name","kind","targets","check","tryPromise","db","command","ping","pipe","as","catchAll","succeed"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,oBAAoB;AACtD,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAEvC,SAASC,cAAc,QAAQ,sBAAsB;AAErD,0EAA0E,GAC1E,OAAO,MAAMC,mBAAmB,QAAQ;AAExC;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMC,wBAAwB,CACnCC,SAEAJ,MAAMK,aAAa,CACjBN,OAAOO,GAAG,CAAC;QACT,MAAMC,SAAS,OAAON;QACtB,MAAMO,WAAW,OAAOV;QAExB,OAAOU,SAASC,QAAQ,CAAC;YACvBC,MAAMR;YACNS,MAAM;YACNC,SAASR;YACTS,OAAOd,OAAOe,UAAU,CAAC,IACvBP,OAAOQ,EAAE,CAAC,SAASC,OAAO,CAAC;oBAAEC,MAAM;gBAAE,IACrCC,IAAI,CACJnB,OAAOoB,EAAE,CAAC,OACV,qDAAqD;YACrDpB,OAAOqB,QAAQ,CAAC,IAAMrB,OAAOsB,OAAO,CAAC;QAEzC;IACF,IACA"}
1
+ {"version":3,"sources":["../../src/mongo-database/mongo-health-probe.ts"],"sourcesContent":["import { HealthRegistryTag } from '@entifix/business';\nimport { Effect, Layer } from 'effect';\n\nimport { MongoClientTag } from './mongo-database.js';\n\n/** Probe name reported by `/api/health/ready` when Mongo is unreachable. */\nexport const MONGO_PROBE_NAME = 'mongo';\n\n/**\n * Registers a readiness probe for the Mongo **connection**: `admin().ping()`,\n * which is the cheapest round trip the driver offers and, unlike \"is the client\n * object non-null\", actually crosses the wire.\n *\n * It depends on {@link MongoClientTag}, not on a named database, because the\n * ping targets the `admin` database and never touches the connected one — so a\n * service whose handles are all per-request (tenant storage) does not have to\n * invent a database name to become observable. Naming one it never writes is\n * exactly the phantom store ADR 0020 rules out.\n *\n * Merge it beside {@link MongoClientLayer} in a service's `AppLayer` and the\n * service's readiness endpoint starts reporting Mongo — nothing in the service\n * describes the probe, so it cannot drift from the dependency it describes.\n *\n * `stores` names the Stores this connection backs, by their register name in\n * `tools/slices/` — `['catalog', 'saga']`, never a database name and never a\n * URI. It is the composition root's to supply because a client package cannot\n * know it, and it is a list because one connection routinely backs several\n * Stores (ADR 0031).\n */\nexport const MongoHealthProbeLayer = (\n stores: readonly string[],\n): Layer.Layer<never, never, MongoClientTag | HealthRegistryTag> =>\n Layer.effectDiscard(\n Effect.gen(function* () {\n const client = yield* MongoClientTag;\n const registry = yield* HealthRegistryTag;\n\n yield* registry.register({\n name: MONGO_PROBE_NAME,\n kind: 'datastore',\n targets: stores,\n check: Effect.tryPromise(() =>\n client.db('admin').command({ ping: 1 }),\n ).pipe(\n Effect.as(true),\n // Unreachable is a `false`, never a failed endpoint.\n Effect.catch(() => Effect.succeed(false)),\n ),\n });\n }),\n );\n"],"names":["HealthRegistryTag","Effect","Layer","MongoClientTag","MONGO_PROBE_NAME","MongoHealthProbeLayer","stores","effectDiscard","gen","client","registry","register","name","kind","targets","check","tryPromise","db","command","ping","pipe","as","catch","succeed"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,oBAAoB;AACtD,SAASC,MAAM,EAAEC,KAAK,QAAQ,SAAS;AAEvC,SAASC,cAAc,QAAQ,sBAAsB;AAErD,0EAA0E,GAC1E,OAAO,MAAMC,mBAAmB,QAAQ;AAExC;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMC,wBAAwB,CACnCC,SAEAJ,MAAMK,aAAa,CACjBN,OAAOO,GAAG,CAAC;QACT,MAAMC,SAAS,OAAON;QACtB,MAAMO,WAAW,OAAOV;QAExB,OAAOU,SAASC,QAAQ,CAAC;YACvBC,MAAMR;YACNS,MAAM;YACNC,SAASR;YACTS,OAAOd,OAAOe,UAAU,CAAC,IACvBP,OAAOQ,EAAE,CAAC,SAASC,OAAO,CAAC;oBAAEC,MAAM;gBAAE,IACrCC,IAAI,CACJnB,OAAOoB,EAAE,CAAC,OACV,qDAAqD;YACrDpB,OAAOqB,KAAK,CAAC,IAAMrB,OAAOsB,OAAO,CAAC;QAEtC;IACF,IACA"}
@@ -27,7 +27,7 @@ import { Effect, Metric } from 'effect';
27
27
  * {@link outboxOldestPendingAge}.
28
28
  */
29
29
  /** Unsent, un-quarantined entries, per database. */
30
- export declare const outboxPending: Metric.Metric.Gauge<number>;
30
+ export declare const outboxPending: Metric.Gauge<number>;
31
31
  /**
32
32
  * Age in seconds of the oldest unsent entry, per database.
33
33
  *
@@ -38,9 +38,9 @@ export declare const outboxPending: Metric.Metric.Gauge<number>;
38
38
  * A depth that is flat and an age that climbs is the signature of a relay whose
39
39
  * head has stopped moving.
40
40
  */
41
- export declare const outboxOldestPendingAge: Metric.Metric.Gauge<number>;
41
+ export declare const outboxOldestPendingAge: Metric.Gauge<number>;
42
42
  /** Entries past the ceiling, which nothing retries and nothing deletes. */
43
- export declare const outboxQuarantined: Metric.Metric.Gauge<number>;
43
+ export declare const outboxQuarantined: Metric.Gauge<number>;
44
44
  /** Record one outbox's depth and age. */
45
45
  export declare const recordOutboxStats: (database: string, stats: OutboxStats) => Effect.Effect<void, never, never>;
46
46
  //# sourceMappingURL=metrics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/outbox/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,oDAAoD;AACpD,eAAO,MAAM,aAAa,6BAExB,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAkB9D,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,6BAE5B,CAAC;AAEH,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,EAAE,OAAO,WAAW,sCAgBrE,CAAC"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/outbox/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,oDAAoD;AACpD,eAAO,MAAM,aAAa,sBAExB,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAkBvD,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,sBAE5B,CAAC;AAEH,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,EAAE,OAAO,WAAW,sCAgBrE,CAAC"}
@@ -47,17 +47,21 @@ import { Effect, Metric } from 'effect';
47
47
  // it `_seconds`, which is why the name above no longer carries that suffix
48
48
  // itself. The cost is a constant `unit` label, because the producer builds the
49
49
  // datapoint attributes from the same tag set it reads the unit from.
50
- Metric.tagged('unit', 's'));
50
+ Metric.withAttributes({
51
+ unit: 's'
52
+ }));
51
53
  /** Entries past the ceiling, which nothing retries and nothing deletes. */ export const outboxQuarantined = Metric.gauge('outbox_quarantined_entries', {
52
54
  description: 'Quarantined outbox entries, by database.'
53
55
  });
54
56
  /** Record one outbox's depth and age. */ export const recordOutboxStats = (database, stats)=>{
55
- const tagged = (metric)=>Metric.tagged(metric, 'database', database);
57
+ const tagged = (metric)=>Metric.withAttributes(metric, {
58
+ database
59
+ });
56
60
  // Zero when nothing is waiting. The alternative — not reporting the series —
57
61
  // reads on most dashboards as "no data", which is indistinguishable from the
58
62
  // exporter having broken at exactly the moment a healthy relay looks idle.
59
63
  const ageSeconds = stats.oldestPendingAt === undefined ? 0 : Math.max(0, (Date.now() - Date.parse(stats.oldestPendingAt)) / 1000);
60
- return Metric.set(tagged(outboxPending), stats.pending).pipe(Effect.andThen(Metric.set(tagged(outboxOldestPendingAge), ageSeconds)), Effect.andThen(Metric.set(tagged(outboxQuarantined), stats.quarantined)));
64
+ return Metric.update(tagged(outboxPending), stats.pending).pipe(Effect.andThen(Metric.update(tagged(outboxOldestPendingAge), ageSeconds)), Effect.andThen(Metric.update(tagged(outboxQuarantined), stats.quarantined)));
61
65
  };
62
66
 
63
67
  //# sourceMappingURL=metrics.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/outbox/metrics.ts"],"sourcesContent":["import type { OutboxStats } from '@entifix/transactions';\nimport { Effect, Metric } from 'effect';\n\n/**\n * The outbox half of ADR 0001's first metric set.\n *\n * It lives beside the relay that samples it rather than in any one service,\n * because three slices now own an outbox — `catalog`, `order` and `payment` —\n * and the warning below is exactly why a copy per service is not an option: an\n * Effect metric is keyed on its **description**, so three hand-maintained\n * definitions of `outbox_pending_entries` are three series the moment one\n * wording drifts, and a dashboard would go quiet without failing anything.\n *\n * ⚠️ The metric objects are **exported so a reader uses the same instance**. An\n * Effect metric's registry key includes its description, so rebuilding one by\n * name without the description addresses a different series that is permanently\n * zero — a silent way to write a query against nothing.\n *\n * These are gauges rather than counters because they describe a *backlog*: the\n * question is how many are waiting right now, not how many there have ever\n * been. They are sampled by the two daemons that already run on an interval, so\n * nothing new is scheduled to produce them.\n *\n * ⚠️ **A dimensionless gauge reaches Prometheus with a `_ratio` suffix.** That\n * is the OTel exporter's convention for unit `1`, not a mistake here, so\n * `outbox_pending_entries` is queried as `outbox_pending_entries_ratio`.\n * Counters are unaffected. Where a metric has a real unit, tag it — see\n * {@link outboxOldestPendingAge}.\n */\n\n/** Unsent, un-quarantined entries, per database. */\nexport const outboxPending = Metric.gauge('outbox_pending_entries', {\n description: 'Unsent, un-quarantined outbox entries, by database.',\n});\n\n/**\n * Age in seconds of the oldest unsent entry, per database.\n *\n * **The metric that makes a stuck relay visible.** #179 gave the relay a\n * ceiling, so an entry that can never publish is quarantined and skipped rather\n * than blocking the head of the line forever — but that is reported only as a\n * log line, so how far behind the outbox has fallen is otherwise unanswerable.\n * A depth that is flat and an age that climbs is the signature of a relay whose\n * head has stopped moving.\n */\nexport const outboxOldestPendingAge: Metric.Metric.Gauge<number> = Metric.gauge(\n 'outbox_oldest_pending_age',\n {\n description:\n 'Age of the oldest unsent outbox entry, by database. Zero when ' +\n 'the outbox is empty.',\n },\n).pipe(\n // ⚠️ **The unit is carried as a tag, and that is the only way to set it.**\n // `@effect/opentelemetry`'s producer reads `tags.unit ?? tags.time_unit ??\n // '1'`, so a gauge with no `unit` tag is exported as dimensionless — and the\n // Prometheus exporter then appends `_ratio` to a dimensionless gauge's name.\n // Measured: this series arrived as `outbox_oldest_pending_age_seconds_ratio`,\n // a duration announcing itself as a ratio. Tagging it makes the exporter name\n // it `_seconds`, which is why the name above no longer carries that suffix\n // itself. The cost is a constant `unit` label, because the producer builds the\n // datapoint attributes from the same tag set it reads the unit from.\n Metric.tagged('unit', 's'),\n);\n\n/** Entries past the ceiling, which nothing retries and nothing deletes. */\nexport const outboxQuarantined = Metric.gauge('outbox_quarantined_entries', {\n description: 'Quarantined outbox entries, by database.',\n});\n\n/** Record one outbox's depth and age. */\nexport const recordOutboxStats = (database: string, stats: OutboxStats) => {\n const tagged = (metric: Metric.Metric.Gauge<number>) =>\n Metric.tagged(metric, 'database', database);\n\n // Zero when nothing is waiting. The alternative — not reporting the series —\n // reads on most dashboards as \"no data\", which is indistinguishable from the\n // exporter having broken at exactly the moment a healthy relay looks idle.\n const ageSeconds =\n stats.oldestPendingAt === undefined\n ? 0\n : Math.max(0, (Date.now() - Date.parse(stats.oldestPendingAt)) / 1000);\n\n return Metric.set(tagged(outboxPending), stats.pending).pipe(\n Effect.andThen(Metric.set(tagged(outboxOldestPendingAge), ageSeconds)),\n Effect.andThen(Metric.set(tagged(outboxQuarantined), stats.quarantined)),\n );\n};\n"],"names":["Effect","Metric","outboxPending","gauge","description","outboxOldestPendingAge","pipe","tagged","outboxQuarantined","recordOutboxStats","database","stats","metric","ageSeconds","oldestPendingAt","undefined","Math","max","Date","now","parse","set","pending","andThen","quarantined"],"mappings":"AACA,SAASA,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GAED,kDAAkD,GAClD,OAAO,MAAMC,gBAAgBD,OAAOE,KAAK,CAAC,0BAA0B;IAClEC,aAAa;AACf,GAAG;AAEH;;;;;;;;;CASC,GACD,OAAO,MAAMC,yBAAsDJ,OAAOE,KAAK,CAC7E,6BACA;IACEC,aACE,mEACA;AACJ,GACAE,IAAI,CACJ,2EAA2E;AAC3E,2EAA2E;AAC3E,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,2EAA2E;AAC3E,+EAA+E;AAC/E,qEAAqE;AACrEL,OAAOM,MAAM,CAAC,QAAQ,MACtB;AAEF,yEAAyE,GACzE,OAAO,MAAMC,oBAAoBP,OAAOE,KAAK,CAAC,8BAA8B;IAC1EC,aAAa;AACf,GAAG;AAEH,uCAAuC,GACvC,OAAO,MAAMK,oBAAoB,CAACC,UAAkBC;IAClD,MAAMJ,SAAS,CAACK,SACdX,OAAOM,MAAM,CAACK,QAAQ,YAAYF;IAEpC,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,MAAMG,aACJF,MAAMG,eAAe,KAAKC,YACtB,IACAC,KAAKC,GAAG,CAAC,GAAG,AAACC,CAAAA,KAAKC,GAAG,KAAKD,KAAKE,KAAK,CAACT,MAAMG,eAAe,CAAA,IAAK;IAErE,OAAOb,OAAOoB,GAAG,CAACd,OAAOL,gBAAgBS,MAAMW,OAAO,EAAEhB,IAAI,CAC1DN,OAAOuB,OAAO,CAACtB,OAAOoB,GAAG,CAACd,OAAOF,yBAAyBQ,cAC1Db,OAAOuB,OAAO,CAACtB,OAAOoB,GAAG,CAACd,OAAOC,oBAAoBG,MAAMa,WAAW;AAE1E,EAAE"}
1
+ {"version":3,"sources":["../../src/outbox/metrics.ts"],"sourcesContent":["import type { OutboxStats } from '@entifix/transactions';\nimport { Effect, Metric } from 'effect';\n\n/**\n * The outbox half of ADR 0001's first metric set.\n *\n * It lives beside the relay that samples it rather than in any one service,\n * because three slices now own an outbox — `catalog`, `order` and `payment` —\n * and the warning below is exactly why a copy per service is not an option: an\n * Effect metric is keyed on its **description**, so three hand-maintained\n * definitions of `outbox_pending_entries` are three series the moment one\n * wording drifts, and a dashboard would go quiet without failing anything.\n *\n * ⚠️ The metric objects are **exported so a reader uses the same instance**. An\n * Effect metric's registry key includes its description, so rebuilding one by\n * name without the description addresses a different series that is permanently\n * zero — a silent way to write a query against nothing.\n *\n * These are gauges rather than counters because they describe a *backlog*: the\n * question is how many are waiting right now, not how many there have ever\n * been. They are sampled by the two daemons that already run on an interval, so\n * nothing new is scheduled to produce them.\n *\n * ⚠️ **A dimensionless gauge reaches Prometheus with a `_ratio` suffix.** That\n * is the OTel exporter's convention for unit `1`, not a mistake here, so\n * `outbox_pending_entries` is queried as `outbox_pending_entries_ratio`.\n * Counters are unaffected. Where a metric has a real unit, tag it — see\n * {@link outboxOldestPendingAge}.\n */\n\n/** Unsent, un-quarantined entries, per database. */\nexport const outboxPending = Metric.gauge('outbox_pending_entries', {\n description: 'Unsent, un-quarantined outbox entries, by database.',\n});\n\n/**\n * Age in seconds of the oldest unsent entry, per database.\n *\n * **The metric that makes a stuck relay visible.** #179 gave the relay a\n * ceiling, so an entry that can never publish is quarantined and skipped rather\n * than blocking the head of the line forever — but that is reported only as a\n * log line, so how far behind the outbox has fallen is otherwise unanswerable.\n * A depth that is flat and an age that climbs is the signature of a relay whose\n * head has stopped moving.\n */\nexport const outboxOldestPendingAge: Metric.Gauge<number> = Metric.gauge(\n 'outbox_oldest_pending_age',\n {\n description:\n 'Age of the oldest unsent outbox entry, by database. Zero when ' +\n 'the outbox is empty.',\n },\n).pipe(\n // ⚠️ **The unit is carried as a tag, and that is the only way to set it.**\n // `@effect/opentelemetry`'s producer reads `tags.unit ?? tags.time_unit ??\n // '1'`, so a gauge with no `unit` tag is exported as dimensionless — and the\n // Prometheus exporter then appends `_ratio` to a dimensionless gauge's name.\n // Measured: this series arrived as `outbox_oldest_pending_age_seconds_ratio`,\n // a duration announcing itself as a ratio. Tagging it makes the exporter name\n // it `_seconds`, which is why the name above no longer carries that suffix\n // itself. The cost is a constant `unit` label, because the producer builds the\n // datapoint attributes from the same tag set it reads the unit from.\n Metric.withAttributes({ unit: 's' }),\n);\n\n/** Entries past the ceiling, which nothing retries and nothing deletes. */\nexport const outboxQuarantined = Metric.gauge('outbox_quarantined_entries', {\n description: 'Quarantined outbox entries, by database.',\n});\n\n/** Record one outbox's depth and age. */\nexport const recordOutboxStats = (database: string, stats: OutboxStats) => {\n const tagged = (metric: Metric.Gauge<number>) =>\n Metric.withAttributes(metric, { database });\n\n // Zero when nothing is waiting. The alternative — not reporting the series —\n // reads on most dashboards as \"no data\", which is indistinguishable from the\n // exporter having broken at exactly the moment a healthy relay looks idle.\n const ageSeconds =\n stats.oldestPendingAt === undefined\n ? 0\n : Math.max(0, (Date.now() - Date.parse(stats.oldestPendingAt)) / 1000);\n\n return Metric.update(tagged(outboxPending), stats.pending).pipe(\n Effect.andThen(Metric.update(tagged(outboxOldestPendingAge), ageSeconds)),\n Effect.andThen(Metric.update(tagged(outboxQuarantined), stats.quarantined)),\n );\n};\n"],"names":["Effect","Metric","outboxPending","gauge","description","outboxOldestPendingAge","pipe","withAttributes","unit","outboxQuarantined","recordOutboxStats","database","stats","tagged","metric","ageSeconds","oldestPendingAt","undefined","Math","max","Date","now","parse","update","pending","andThen","quarantined"],"mappings":"AACA,SAASA,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;CAyBC,GAED,kDAAkD,GAClD,OAAO,MAAMC,gBAAgBD,OAAOE,KAAK,CAAC,0BAA0B;IAClEC,aAAa;AACf,GAAG;AAEH;;;;;;;;;CASC,GACD,OAAO,MAAMC,yBAA+CJ,OAAOE,KAAK,CACtE,6BACA;IACEC,aACE,mEACA;AACJ,GACAE,IAAI,CACJ,2EAA2E;AAC3E,2EAA2E;AAC3E,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,2EAA2E;AAC3E,+EAA+E;AAC/E,qEAAqE;AACrEL,OAAOM,cAAc,CAAC;IAAEC,MAAM;AAAI,IAClC;AAEF,yEAAyE,GACzE,OAAO,MAAMC,oBAAoBR,OAAOE,KAAK,CAAC,8BAA8B;IAC1EC,aAAa;AACf,GAAG;AAEH,uCAAuC,GACvC,OAAO,MAAMM,oBAAoB,CAACC,UAAkBC;IAClD,MAAMC,SAAS,CAACC,SACdb,OAAOM,cAAc,CAACO,QAAQ;YAAEH;QAAS;IAE3C,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,MAAMI,aACJH,MAAMI,eAAe,KAAKC,YACtB,IACAC,KAAKC,GAAG,CAAC,GAAG,AAACC,CAAAA,KAAKC,GAAG,KAAKD,KAAKE,KAAK,CAACV,MAAMI,eAAe,CAAA,IAAK;IAErE,OAAOf,OAAOsB,MAAM,CAACV,OAAOX,gBAAgBU,MAAMY,OAAO,EAAElB,IAAI,CAC7DN,OAAOyB,OAAO,CAACxB,OAAOsB,MAAM,CAACV,OAAOR,yBAAyBU,cAC7Df,OAAOyB,OAAO,CAACxB,OAAOsB,MAAM,CAACV,OAAOJ,oBAAoBG,MAAMc,WAAW;AAE7E,EAAE"}
@@ -6,7 +6,7 @@ import { MongoDatabaseTag } from '../mongo-database/mongo-database.js';
6
6
  export declare const SWEEP_INTERVAL: Duration.Duration;
7
7
  /** Entries drained per pass, per database. */
8
8
  export declare const BATCH = 100;
9
- declare const OutboxMaxAttempts_base: Context.TagClass<OutboxMaxAttempts, "OutboxMaxAttempts", number>;
9
+ declare const OutboxMaxAttempts_base: Context.ServiceClass<OutboxMaxAttempts, "OutboxMaxAttempts", number>;
10
10
  /**
11
11
  * Publish attempts an outbox entry gets before it is quarantined, from
12
12
  * config-service.
@@ -1 +1 @@
1
- {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/outbox/relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,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;AAElE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAIvE,yEAAyE;AACzE,eAAO,MAAM,cAAc,mBAAuB,CAAC;AACnD,8CAA8C;AAC9C,eAAO,MAAM,KAAK,MAAM,CAAC;;AAEzB;;;;;;;;;;;;;GAaG;AACH,qBAAa,iBAAkB,SAAQ,sBAGpC;CAAG;AAEN,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,iBAAiB,EACzB,KAAK,QAAQ,EACb,SAAS,YAAY,2EAkCnB,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,sCAe7D,CAAC;AAEJ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAU;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;CACpC,4IAyBG,CAAC"}
1
+ {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/outbox/relay.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,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;AAElE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAIvE,yEAAyE;AACzE,eAAO,MAAM,cAAc,mBAAuB,CAAC;AACnD,8CAA8C;AAC9C,eAAO,MAAM,KAAK,MAAM,CAAC;;AAEzB;;;;;;;;;;;;;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;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,iBAAiB,EACzB,KAAK,QAAQ,EACb,SAAS,YAAY,2EAkCnB,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,sCAe7D,CAAC;AAEJ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAU;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;CACpC,4IAyBG,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
2
  import { ShutdownRegistryTag } from '@entifix/business';
3
3
  import { EventBusTag } from '@entifix/transactions';
4
- import { Context, Duration, Effect, Either, Fiber } from 'effect';
4
+ 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';
@@ -20,7 +20,7 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
20
20
  * ⚠️ **Read it with `getNumber`, never a cast.** A value of `'five'` casts to
21
21
  * `NaN`, every `attempts >= maxAttempts` comparison goes false, and nothing is
22
22
  * ever quarantined — a relay that looks healthy while its head never moves.
23
- */ export class OutboxMaxAttempts extends Context.Tag('OutboxMaxAttempts')() {
23
+ */ export class OutboxMaxAttempts extends Context.Service()('OutboxMaxAttempts') {
24
24
  }
25
25
  /**
26
26
  * Publish one batch of pending entries, oldest first, and answer how many went.
@@ -37,11 +37,11 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
37
37
  const entries = yield* outbox.pending(BATCH);
38
38
  let sent = 0;
39
39
  for (const entry of entries){
40
- const outcome = yield* Effect.either(bus.publish(entry.event));
41
- if (Either.isLeft(outcome)) {
40
+ const outcome = yield* Effect.result(bus.publish(entry.event));
41
+ if (Result.isFailure(outcome)) {
42
42
  const attempts = entry.attempts + 1;
43
43
  const quarantine = attempts >= options.maxAttempts;
44
- yield* outbox.recordFailure(entry, outcome.left.message, quarantine);
44
+ yield* outbox.recordFailure(entry, outcome.failure.message, quarantine);
45
45
  if (!quarantine) {
46
46
  return sent;
47
47
  }
@@ -50,7 +50,7 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
50
50
  eventId: entry.eventId,
51
51
  eventName: entry.event.name,
52
52
  attempts,
53
- lastError: outcome.left.message
53
+ lastError: outcome.failure.message
54
54
  }));
55
55
  continue;
56
56
  }
@@ -78,7 +78,7 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
78
78
  yield* drainOutbox(outbox, bus, options);
79
79
  const sink = (_options_onStats = options.onStats) != null ? _options_onStats : recordOutboxStats;
80
80
  yield* sink(options.database, (yield* outbox.stats()));
81
- }).pipe(Effect.catchAll((error)=>Effect.logError('outbox sweep failed').pipe(Effect.annotateLogs({
81
+ }).pipe(Effect.catch((error)=>Effect.logError('outbox sweep failed').pipe(Effect.annotateLogs({
82
82
  database: options.database,
83
83
  error: String(error)
84
84
  }))));
@@ -108,7 +108,7 @@ import { ensureOutboxIndexes, makeMongoOutbox } from './store.js';
108
108
  }, (options == null ? void 0 : options.onStats) ? {
109
109
  onStats: options.onStats
110
110
  } : {}));
111
- const daemon = yield* Effect.forkDaemon(sweepOnce.pipe(Effect.delay(SWEEP_INTERVAL), Effect.forever));
111
+ const daemon = yield* Effect.forkDetach(sweepOnce.pipe(Effect.delay(SWEEP_INTERVAL), Effect.forever));
112
112
  yield* shutdown.register({
113
113
  name: 'outbox-relay',
114
114
  phase: 'flush',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/outbox/relay.ts"],"sourcesContent":["import { ShutdownRegistryTag } from '@entifix/business';\nimport {\n type EventBus,\n EventBusTag,\n type OutboxStats,\n type TransactionOutbox,\n} from '@entifix/transactions';\nimport { Context, Duration, Effect, Either, Fiber } from 'effect';\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.Tag('OutboxMaxAttempts')<\n OutboxMaxAttempts,\n number\n>() {}\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 ADR 0030's 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.either(bus.publish(entry.event));\n\n if (Either.isLeft(outcome)) {\n const attempts = entry.attempts + 1;\n const quarantine = attempts >= options.maxAttempts;\n\n yield* outbox.recordFailure(entry, outcome.left.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.left.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 marketplace-admin's tenant walk. 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.catchAll(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 const shutdown = yield* ShutdownRegistryTag;\n\n yield* ensureOutboxIndexes(db);\n const outbox = makeMongoOutbox(db);\n\n const sweepOnce = sweepOutbox(outbox, bus, {\n maxAttempts,\n database: db.databaseName,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n });\n\n const daemon = yield* Effect.forkDaemon(\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"],"names":["ShutdownRegistryTag","EventBusTag","Context","Duration","Effect","Either","Fiber","MongoDatabaseTag","recordOutboxStats","ensureOutboxIndexes","makeMongoOutbox","SWEEP_INTERVAL","seconds","BATCH","OutboxMaxAttempts","Tag","drainOutbox","outbox","bus","options","gen","entries","pending","sent","entry","outcome","either","publish","event","isLeft","attempts","quarantine","maxAttempts","recordFailure","left","message","logError","pipe","annotateLogs","database","eventId","eventName","name","lastError","markSent","sweepOutbox","sink","onStats","stats","catchAll","error","String","startOutboxRelay","db","shutdown","sweepOnce","databaseName","daemon","forkDaemon","delay","forever","register","phase","run","interrupt","andThen"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/outbox/relay.ts"],"sourcesContent":["import { ShutdownRegistryTag } 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';\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 ADR 0030's 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 marketplace-admin's tenant walk. 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 const shutdown = yield* ShutdownRegistryTag;\n\n yield* ensureOutboxIndexes(db);\n const outbox = makeMongoOutbox(db);\n\n const sweepOnce = sweepOutbox(outbox, bus, {\n maxAttempts,\n database: db.databaseName,\n ...(options?.onStats ? { onStats: options.onStats } : {}),\n });\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"],"names":["ShutdownRegistryTag","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","shutdown","sweepOnce","databaseName","daemon","forkDetach","delay","forever","register","phase","run","interrupt","andThen"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/mongo",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "description": "MongoDB adapter: an entity repository, RSQL-to-Mongo query translation, per-tenant databases, and an optional transactional outbox.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,13 +18,11 @@
18
18
  "exports": {
19
19
  "./package.json": "./package.json",
20
20
  ".": {
21
- "@entifix/source": "./src/index.ts",
22
21
  "types": "./dist/index.d.ts",
23
22
  "import": "./dist/index.js",
24
23
  "default": "./dist/index.js"
25
24
  },
26
25
  "./transactions": {
27
- "@entifix/source": "./src/transactions/index.ts",
28
26
  "types": "./dist/transactions/index.d.ts",
29
27
  "import": "./dist/transactions/index.js",
30
28
  "default": "./dist/transactions/index.js"
@@ -59,17 +57,17 @@
59
57
  "dependencies": {
60
58
  "@swc/helpers": "0.5.23",
61
59
  "mongodb": "7.7.0",
62
- "@entifix/core": "0.1.4",
63
- "@entifix/business": "0.1.4"
60
+ "@entifix/core": "0.3.0",
61
+ "@entifix/business": "0.3.0"
64
62
  },
65
63
  "devDependencies": {
66
- "effect": "3.22.2",
67
- "@entifix/transactions": "0.1.4",
68
- "@entifix/testing-unit": "0.1.4"
64
+ "effect": "4.0.5",
65
+ "@entifix/transactions": "0.3.0",
66
+ "@entifix/testing-unit": "0.3.0"
69
67
  },
70
68
  "peerDependencies": {
71
- "effect": ">=3.22.2 <4",
72
- "@entifix/transactions": "0.1.4"
69
+ "effect": "^4.0.5",
70
+ "@entifix/transactions": "0.3.0"
73
71
  },
74
72
  "peerDependenciesMeta": {
75
73
  "@entifix/transactions": {