@effect-app/infra 4.0.0-beta.302 → 4.0.0-beta.304

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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @effect-app/infra
2
2
 
3
+ ## 4.0.0-beta.304
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [412f08b]
8
+ - effect-app@4.0.0-beta.304
9
+
10
+ ## 4.0.0-beta.303
11
+
12
+ ### Patch Changes
13
+
14
+ - 33b0544: Replace repository codec child spans with parent-span timing attributes and bounded metrics, and record database operation timing for instrumented stores.
15
+ - Updated dependencies [33b0544]
16
+ - effect-app@4.0.0-beta.303
17
+
3
18
  ## 4.0.0-beta.302
4
19
 
5
20
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAE3C,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,GACT,OAAO,GACP,WAAW,GACX,QAAQ,GACR,MAAM,CAAA;AAEV,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;IACzB,mDAAmD;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;CACrD;AAYD;;;;GAIG;AACH,eAAO,MAAM,UAAU,MAAO,aAAa,4IAKxC,CAAA;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,MAAO,aAAa,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CACrC,CAAA;AAEzE,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,MAAO;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5C,sCAIG,CAAA;AAEJ,4CAA4C;AAC5C,eAAO,MAAM,sBAAsB,MAAO;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACzC,sCAMG,CAAA;AAEJ,MAAM,MAAM,eAAe,GACvB,YAAY,GACZ,UAAU,GACV,OAAO,GACP,QAAQ,GACR,KAAK,CAAA;AAET,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,CAAA;AAEZ,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAA;IACtC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;IAChC,0BAA0B;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;CACrD;AAYD,0DAA0D;AAC1D,eAAO,MAAM,iBAAiB,MACzB,oBAAoB,QACjB,UAAU,GAAG,UAAU,4IAM5B,CAAA;AAEH,yGAAyG;AACzG,eAAO,MAAM,iBAAiB,MACzB,oBAAoB,QACjB,UAAU,GAAG,UAAU;;;;CAMlB,CAAA"}
1
+ {"version":3,"file":"otel.d.ts","sourceRoot":"","sources":["../src/otel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAA;AAG3C,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,GACT,OAAO,GACP,WAAW,GACX,QAAQ,GACR,MAAM,CAAA;AAEV,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;IACzB,mDAAmD;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;CACrD;AAoCD;;;;GAIG;AACH,eAAO,MAAM,UAAU,MAAO,aAAa,4IAKxC,CAAA;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,MAAO,aAAa,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CACZ,CAAA;AAElG,sEAAsE;AACtE,eAAO,MAAM,kBAAkB,MAAO;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5C,sCAIG,CAAA;AAEJ,4CAA4C;AAC5C,eAAO,MAAM,sBAAsB,MAAO;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACzC,sCAMG,CAAA;AAEJ,MAAM,MAAM,eAAe,GACvB,YAAY,GACZ,UAAU,GACV,OAAO,GACP,QAAQ,GACR,KAAK,CAAA;AAET,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,CAAA;AAEZ,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAA;IACtC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;IAChC,0BAA0B;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC5C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;CACrD;AAYD,0DAA0D;AAC1D,eAAO,MAAM,iBAAiB,MACzB,oBAAoB,QACjB,UAAU,GAAG,UAAU,4IAM5B,CAAA;AAEH,yGAAyG;AACzG,eAAO,MAAM,iBAAiB,MACzB,oBAAoB,QACjB,UAAU,GAAG,UAAU;;;;CAMlB,CAAA"}
package/dist/otel.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * - Cosmos DB: https://opentelemetry.io/docs/specs/semconv/database/cosmosdb/
10
10
  */
11
11
  import * as Effect from "effect-app/Effect";
12
+ import * as Metric from "effect/Metric";
12
13
  const dbAttributes = (a) => ({
13
14
  "db.system.name": a.system,
14
15
  "db.operation.name": a.operation,
@@ -18,6 +19,24 @@ const dbAttributes = (a) => ({
18
19
  ...(a.entity !== undefined && { "app.entity": a.entity }),
19
20
  ...a.extra
20
21
  });
22
+ const dbOperationDuration = Metric.histogram("db.client.operation.duration", {
23
+ boundaries: [0.1, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000],
24
+ description: "Caller-observed database operation duration in milliseconds"
25
+ });
26
+ const timeDbOperation = (a) => (self) => Effect.clockWith((clock) => {
27
+ const startedAt = clock.currentTimeNanosUnsafe();
28
+ return Effect.onExit(self, () => {
29
+ const durationMs = Number(clock.currentTimeNanosUnsafe() - startedAt) / 1_000_000;
30
+ return Effect.all([
31
+ Effect.annotateCurrentSpan({ "db.operation.duration_ms": durationMs }),
32
+ Metric.update(Metric.withAttributes(dbOperationDuration, {
33
+ "db.system.name": a.system,
34
+ "db.operation.name": a.operation,
35
+ ...(a.entity !== undefined && { "app.entity": a.entity })
36
+ }), durationMs)
37
+ ], { discard: true });
38
+ });
39
+ });
21
40
  /**
22
41
  * Wrap an effect with an OTel-semconv database span.
23
42
  *
@@ -32,7 +51,7 @@ export const withDbSpan = (a) => Effect.withSpan(`${a.operation} ${a.collection}
32
51
  * Annotates before running so attrs persist even on failure.
33
52
  * No-op if there is no current span.
34
53
  */
35
- export const annotateDb = (a) => (self) => Effect.flatMap(Effect.annotateCurrentSpan(dbAttributes(a)), () => self);
54
+ export const annotateDb = (a) => (self) => Effect.flatMap(Effect.annotateCurrentSpan(dbAttributes(a)), () => self.pipe(timeDbOperation(a)));
36
55
  /** Annotate the current span with response metrics from a DB call. */
37
56
  export const annotateDbResponse = (m) => Effect.annotateCurrentSpan({
38
57
  ...(m.returnedRows !== undefined && { "db.response.returned_rows": m.returnedRows }),
@@ -62,4 +81,4 @@ export const messagingSpanArgs = (a, kind) => ({
62
81
  kind,
63
82
  attributes: messagingAttributes(a)
64
83
  });
65
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9vdGVsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7R0FTRztBQUVILE9BQU8sS0FBSyxNQUFNLE1BQU0sbUJBQW1CLENBQUE7QUE0QjNDLE1BQU0sWUFBWSxHQUFHLENBQUMsQ0FBZ0IsRUFBMkIsRUFBRSxDQUFDLENBQUM7SUFDbkUsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDLE1BQU07SUFDMUIsbUJBQW1CLEVBQUUsQ0FBQyxDQUFDLFNBQVM7SUFDaEMsb0JBQW9CLEVBQUUsQ0FBQyxDQUFDLFVBQVU7SUFDbEMsR0FBRyxDQUFDLENBQUMsQ0FBQyxTQUFTLEtBQUssU0FBUyxJQUFJLEVBQUUsY0FBYyxFQUFFLENBQUMsQ0FBQyxTQUFTLEVBQUUsQ0FBQztJQUNqRSxHQUFHLENBQUMsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLElBQUksRUFBRSxlQUFlLEVBQUUsQ0FBQyxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQzFELEdBQUcsQ0FBQyxDQUFDLENBQUMsTUFBTSxLQUFLLFNBQVMsSUFBSSxFQUFFLFlBQVksRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUM7SUFDekQsR0FBRyxDQUFDLENBQUMsS0FBSztDQUNYLENBQUMsQ0FBQTtBQUVGOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsTUFBTSxVQUFVLEdBQUcsQ0FBQyxDQUFnQixFQUFFLEVBQUUsQ0FDN0MsTUFBTSxDQUFDLFFBQVEsQ0FDYixHQUFHLENBQUMsQ0FBQyxTQUFTLElBQUksQ0FBQyxDQUFDLFVBQVUsRUFBRSxFQUNoQyxFQUFFLFVBQVUsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQUFDLEVBQUUsSUFBSSxFQUFFLFFBQWlCLEVBQUUsRUFDeEQsRUFBRSxpQkFBaUIsRUFBRSxLQUFLLEVBQUUsQ0FDN0IsQ0FBQTtBQUVIOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsTUFBTSxVQUFVLEdBQUcsQ0FB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
84
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9vdGVsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7R0FTRztBQUVILE9BQU8sS0FBSyxNQUFNLE1BQU0sbUJBQW1CLENBQUE7QUFDM0MsT0FBTyxLQUFLLE1BQU0sTUFBTSxlQUFlLENBQUE7QUE0QnZDLE1BQU0sWUFBWSxHQUFHLENBQUMsQ0FBZ0IsRUFBMkIsRUFBRSxDQUFDLENBQUM7SUFDbkUsZ0JBQWdCLEVBQUUsQ0FBQyxDQUFDLE1BQU07SUFDMUIsbUJBQW1CLEVBQUUsQ0FBQyxDQUFDLFNBQVM7SUFDaEMsb0JBQW9CLEVBQUUsQ0FBQyxDQUFDLFVBQVU7SUFDbEMsR0FBRyxDQUFDLENBQUMsQ0FBQyxTQUFTLEtBQUssU0FBUyxJQUFJLEVBQUUsY0FBYyxFQUFFLENBQUMsQ0FBQyxTQUFTLEVBQUUsQ0FBQztJQUNqRSxHQUFHLENBQUMsQ0FBQyxDQUFDLEtBQUssS0FBSyxTQUFTLElBQUksRUFBRSxlQUFlLEVBQUUsQ0FBQyxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQzFELEdBQUcsQ0FBQyxDQUFDLENBQUMsTUFBTSxLQUFLLFNBQVMsSUFBSSxFQUFFLFlBQVksRUFBRSxDQUFDLENBQUMsTUFBTSxFQUFFLENBQUM7SUFDekQsR0FBRyxDQUFDLENBQUMsS0FBSztDQUNYLENBQUMsQ0FBQTtBQUVGLE1BQU0sbUJBQW1CLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyw4QkFBOEIsRUFBRTtJQUMzRSxVQUFVLEVBQUUsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsRUFBRSxFQUFFLEVBQUUsRUFBRSxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQztJQUN2RixXQUFXLEVBQUUsNkRBQTZEO0NBQzNFLENBQUMs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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-app/infra",
3
- "version": "4.0.0-beta.302",
3
+ "version": "4.0.0-beta.304",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -15,7 +15,7 @@
15
15
  "proper-lockfile": "^4.1.2",
16
16
  "pure-rand": "8.4.0",
17
17
  "query-string": "^9.4.0",
18
- "effect-app": "4.0.0-beta.302"
18
+ "effect-app": "4.0.0-beta.304"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@azure/cosmos": "^4.9.3",
package/src/otel.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import * as Effect from "effect-app/Effect"
13
+ import * as Metric from "effect/Metric"
13
14
 
14
15
  export type DbSystem =
15
16
  | "postgresql"
@@ -47,6 +48,30 @@ const dbAttributes = (a: DbSpanOptions): Record<string, unknown> => ({
47
48
  ...a.extra
48
49
  })
49
50
 
51
+ const dbOperationDuration = Metric.histogram("db.client.operation.duration", {
52
+ boundaries: [0.1, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000],
53
+ description: "Caller-observed database operation duration in milliseconds"
54
+ })
55
+
56
+ const timeDbOperation = (a: DbSpanOptions) => <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
57
+ Effect.clockWith((clock) => {
58
+ const startedAt = clock.currentTimeNanosUnsafe()
59
+ return Effect.onExit(self, () => {
60
+ const durationMs = Number(clock.currentTimeNanosUnsafe() - startedAt) / 1_000_000
61
+ return Effect.all([
62
+ Effect.annotateCurrentSpan({ "db.operation.duration_ms": durationMs }),
63
+ Metric.update(
64
+ Metric.withAttributes(dbOperationDuration, {
65
+ "db.system.name": a.system,
66
+ "db.operation.name": a.operation,
67
+ ...(a.entity !== undefined && { "app.entity": a.entity })
68
+ }),
69
+ durationMs
70
+ )
71
+ ], { discard: true })
72
+ })
73
+ })
74
+
50
75
  /**
51
76
  * Wrap an effect with an OTel-semconv database span.
52
77
  *
@@ -68,7 +93,7 @@ export const withDbSpan = (a: DbSpanOptions) =>
68
93
  * No-op if there is no current span.
69
94
  */
70
95
  export const annotateDb = (a: DbSpanOptions) => <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
71
- Effect.flatMap(Effect.annotateCurrentSpan(dbAttributes(a)), () => self)
96
+ Effect.flatMap(Effect.annotateCurrentSpan(dbAttributes(a)), () => self.pipe(timeDbOperation(a)))
72
97
 
73
98
  /** Annotate the current span with response metrics from a DB call. */
74
99
  export const annotateDbResponse = (m: {
@@ -8,6 +8,7 @@ import { RepositoryRegistryLive } from "effect-app/Model/Repository/Registry"
8
8
  import * as S from "effect-app/Schema"
9
9
  import { setupRequestContextFromCurrent } from "effect-app/setupRequest"
10
10
  import * as Ref from "effect/Ref"
11
+ import * as Tracer from "effect/Tracer"
11
12
  import { MemoryStoreLive } from "../src/Store/Memory.js"
12
13
 
13
14
  class BatchItem extends S.Class<BatchItem>("BatchItem")({
@@ -169,7 +170,112 @@ describe("repository ext save/remove batching", () => {
169
170
  .pipe(Effect.provideService(DataDependencies.DataDependencyRecorder, recorder))
170
171
 
171
172
  expect(yield* Ref.get(readsRef)).toEqual(new Set([DataDependencies.repo("DependencyItem")]))
172
- expect(yield* Ref.get(writesRef)).toEqual(new Set([DataDependencies.repo("DependencyItem")]))
173
+ expect(yield* Ref.get(writesRef)).toEqual(new Set([DataDependencies.repo("DependencyItem", ["1"])]))
174
+ })
175
+ .pipe(
176
+ setupRequestContextFromCurrent(),
177
+ Effect.provide(TestStoreLive)
178
+ ))
179
+
180
+ it.effect("narrows direct repository reads to the requested entity", () =>
181
+ Effect
182
+ .gen(function*() {
183
+ const readsRef = yield* Ref.make(DataDependencies.empty())
184
+ const writesRef = yield* Ref.make(DataDependencies.empty())
185
+ const recorder = DataDependencies.makeDataDependencyRecorder(readsRef, writesRef)
186
+
187
+ yield* Effect
188
+ .gen(function*() {
189
+ const repo = yield* makeRepo("DependencyItem", BatchItem, {})
190
+ yield* repo.find("1")
191
+ })
192
+ .pipe(Effect.provideService(DataDependencies.DataDependencyRecorder, recorder))
193
+
194
+ expect(yield* Ref.get(readsRef)).toEqual(new Set([DataDependencies.repo("DependencyItem", ["1"])]))
195
+ })
196
+ .pipe(
197
+ setupRequestContextFromCurrent(),
198
+ Effect.provide(TestStoreLive)
199
+ ))
200
+
201
+ it("matches entity dependencies by overlapping ids with a coarse fallback", () => {
202
+ const item1 = new Set([DataDependencies.repo("DependencyItem", ["1"])])
203
+ const item2 = new Set([DataDependencies.repo("DependencyItem", ["2"])])
204
+ const collection = new Set([DataDependencies.repo("DependencyItem")])
205
+
206
+ expect(DataDependencies.intersects(item1, item2)).toBe(false)
207
+ expect(DataDependencies.intersects(item1, item1)).toBe(true)
208
+ expect(DataDependencies.intersects(collection, item2)).toBe(true)
209
+ })
210
+
211
+ it("decodes repository dependencies produced before entity ids were added", () => {
212
+ expect(
213
+ S.decodeUnknownSync(DataDependencies.DataDependency)({
214
+ type: "repo",
215
+ name: "DependencyItem"
216
+ })
217
+ )
218
+ .toEqual(DataDependencies.repo("DependencyItem"))
219
+ })
220
+
221
+ it.effect("matches an explicit query scope to a write alias", () =>
222
+ Effect
223
+ .gen(function*() {
224
+ const readsRef = yield* Ref.make(DataDependencies.empty())
225
+ const writesRef = yield* Ref.make(DataDependencies.empty())
226
+ const recorder = DataDependencies.makeDataDependencyRecorder(readsRef, writesRef)
227
+
228
+ yield* Effect
229
+ .gen(function*() {
230
+ const repo = yield* makeRepo("DependencyItem", BatchItem, {
231
+ dependencyIds: (item) => [item.id, `alias-${item.id}`]
232
+ })
233
+ yield* repo.save(new BatchItem({ id: "1", label: "one" }))
234
+ yield* repo.all.pipe(DataDependencies.withRepoReadScope("DependencyItem", ["alias-1"]))
235
+ })
236
+ .pipe(Effect.provideService(DataDependencies.DataDependencyRecorder, recorder))
237
+
238
+ expect(yield* Ref.get(readsRef)).toEqual(new Set([DataDependencies.repo("DependencyItem", ["alias-1"])]))
239
+ expect(yield* Ref.get(writesRef)).toEqual(
240
+ new Set([DataDependencies.repo("DependencyItem", ["1", "alias-1"])])
241
+ )
242
+ })
243
+ .pipe(
244
+ setupRequestContextFromCurrent(),
245
+ Effect.provide(TestStoreLive)
246
+ ))
247
+
248
+ it.effect("records schema timing on repository spans without codec child spans", () =>
249
+ Effect
250
+ .gen(function*() {
251
+ const spans: Tracer.NativeSpan[] = []
252
+ const tracer = Tracer.make({
253
+ span(options) {
254
+ const span = new Tracer.NativeSpan(options)
255
+ spans.push(span)
256
+ return span
257
+ }
258
+ })
259
+
260
+ yield* Effect
261
+ .gen(function*() {
262
+ const repo = yield* makeRepo("TelemetryItem", BatchItem, {})
263
+ yield* repo.save(new BatchItem({ id: "1", label: "one" }))
264
+ yield* repo.all
265
+ })
266
+ .pipe(Effect.provideService(Tracer.Tracer, tracer))
267
+
268
+ expect(spans.map((_) => _.name)).not.toContain("parseMany")
269
+ expect(spans.map((_) => _.name)).not.toContain("encodeMany")
270
+
271
+ const saveSpan = spans.find((_) => _.name === "Repository.saveAndPublish")
272
+ const allSpan = spans.find((_) => _.name === "Repository.all")
273
+ expect(saveSpan?.attributes.get("app.schema.encode.duration_ms")).toEqual(expect.any(Number))
274
+ expect(saveSpan?.attributes.get("app.schema.item_count")).toBe(1)
275
+ expect(saveSpan?.attributes.get("db.operation.duration_ms")).toEqual(expect.any(Number))
276
+ expect(allSpan?.attributes.get("app.schema.decode.duration_ms")).toEqual(expect.any(Number))
277
+ expect(allSpan?.attributes.get("app.schema.item_count")).toBe(1)
278
+ expect(allSpan?.attributes.get("db.operation.duration_ms")).toEqual(expect.any(Number))
173
279
  })
174
280
  .pipe(
175
281
  setupRequestContextFromCurrent(),
@@ -358,8 +358,8 @@ it.live(
358
358
  const writes = yield* Ref.get(writesRef)
359
359
  expect(values).toStrictEqual([1, 2, 3])
360
360
  // Each emitted value writes to RepoItem; routing drains the writes per chunk and the client
361
- // accumulates them the recorder dedupes, so a single RepoItem entry is recorded.
362
- expect(writes).toStrictEqual(new Set([DataDependencies.repo("RepoItem")]))
361
+ // accumulates their entity ids into one dependency.
362
+ expect(writes).toStrictEqual(new Set([DataDependencies.repo("RepoItem", ["1", "2", "3"])]))
363
363
  }, Effect.provide(TestLayer)),
364
364
  { timeout: 10_000 }
365
365
  )
@@ -377,7 +377,7 @@ it.live(
377
377
  const command = yield* withDependencyCapture(client.SaveRepoItem.handler({ id: "1", label: "one" }))
378
378
  expect(Exit.isSuccess(command.result)).toBe(true)
379
379
  expect(command.dependencies.reads).toStrictEqual(DataDependencies.empty())
380
- expect(command.dependencies.writes).toStrictEqual(new Set([DataDependencies.repo("RepoItem")]))
380
+ expect(command.dependencies.writes).toStrictEqual(new Set([DataDependencies.repo("RepoItem", ["1"])]))
381
381
  }, Effect.provide(TestLayer)),
382
382
  { timeout: 10_000 }
383
383
  )
@@ -411,7 +411,7 @@ it.live(
411
411
  // A command writing an UNRELATED repo => the query is NOT invalidated (negative control).
412
412
  const saveOther = yield* withDependencyCapture(client.SaveOtherItem.handler({ id: "2", label: "two" }))
413
413
  expect(Exit.isSuccess(saveOther.result)).toBe(true)
414
- expect(saveOther.dependencies.writes).toStrictEqual(new Set([DataDependencies.repo("OtherItem")]))
414
+ expect(saveOther.dependencies.writes).toStrictEqual(new Set([DataDependencies.repo("OtherItem", ["2"])]))
415
415
  expect(invalidatedBy(saveOther.dependencies.writes)).toStrictEqual([])
416
416
  }, Effect.provide(TestLayer)),
417
417
  { timeout: 10_000 }