@gaganref/confect-server 10.3.1-fork.36 → 10.3.1-fork.39

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.
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  type DefaultFunctionArgs,
13
13
  type FunctionVisibility,
14
+ type FunctionMetadata,
14
15
  type GenericActionCtx,
15
16
  type GenericDataModel,
16
17
  type StorageReader as ConvexStorageReader,
@@ -21,8 +22,12 @@ import {
21
22
  import type { Value } from "convex/values";
22
23
  import { ConvexError } from "convex/values";
23
24
  import { pipe } from "effect/Function";
25
+ import * as Clock from "effect/Clock";
26
+ import * as Cause from "effect/Cause";
24
27
  import * as Console from "effect/Console";
28
+ import * as Context from "effect/Context";
25
29
  import * as Effect from "effect/Effect";
30
+ import * as Exit from "effect/Exit";
26
31
  import * as Result from "effect/Result";
27
32
  import * as Layer from "effect/Layer";
28
33
  import * as Schema from "effect/Schema";
@@ -43,9 +48,16 @@ import * as Storage from "./Storage.js";
43
48
  import * as StorageActionWriter from "./StorageActionWriter.js";
44
49
  import type * as StorageReader from "./StorageReader.js";
45
50
  import * as StorageWriter from "./StorageWriter.js";
51
+ import * as Tracer from "effect/Tracer";
52
+ import * as ObservabilityRuntime from "./internal/observability.js";
46
53
  import * as VectorSearch from "./VectorSearch.js";
47
54
  import type * as Handler from "./Handler.js";
48
55
 
56
+ export const Observability = Context.Reference<Layer.Layer<never> | undefined>(
57
+ "@confect/server/Observability",
58
+ { defaultValue: () => undefined },
59
+ );
60
+
49
61
  export type Any =
50
62
  | RegisteredQuery<FunctionVisibility, DefaultFunctionArgs, any>
51
63
  | RegisteredMutation<FunctionVisibility, DefaultFunctionArgs, any>
@@ -185,67 +197,125 @@ export const combineErrorSchemas = (
185
197
  };
186
198
 
187
199
  /**
188
- * Run the `Effect` as a `Promise`. The error schema acts as an allowlist of
189
- * failures that may be surfaced to the client as a `ConvexError`:
200
+ * Run the `Effect` as a `Promise`. Inspect the complete `Exit` before encoding
201
+ * a declared error so a typed failure cannot hide a defect.
190
202
  *
191
- * - With a schema: typed errors are schema-encoded and wrapped in a
192
- * `ConvexError`, then thrown so Convex surfaces the data to the client.
193
- * `Effect.either` escapes the failure channel before `runPromise` so the
194
- * thrown `ConvexError` retains its `Symbol.for("ConvexError")` identity
195
- * instead of being wrapped in Effect's `FiberFailure`.
196
- * - Without a schema: every failure is converted to a defect via `Effect.orDie`,
197
- * so nothing—not even a `ConvexError` the handler placed in its error
198
- * channel—reaches the client as a `ConvexError`. The fiber dies and
199
- * `runPromise` rejects with a generic failure.
203
+ * Any `Die` reason takes the defect path. Without a `Die`, the first `Fail` in
204
+ * cause order is schema-encoded and thrown as a `ConvexError` outside the
205
+ * Effect runtime so Convex surfaces its data to the client. Other failures and
206
+ * interruptions alongside that `Fail` are ignored. Without an error schema, the
207
+ * selected failure is treated as a defect.
200
208
  *
201
- * Either way, a `ConvexError` _defect_—thrown imperatively rather than placed
202
- * in the error channel, e.g. the pagination protocol's `InvalidCursor` signal
203
- * from `QueryStream.paginate`—is rethrown bare so it retains its identity and
204
- * Convex serializes its `data` to the client, matching how a thrown
205
- * `ConvexError` behaves in a plain Convex handler.
209
+ * A lone `ConvexError` defect or a selected undeclared `ConvexError` failure is
210
+ * rethrown bare so it keeps Convex's meaning. Pagination and Workpool rely on
211
+ * its data for their control signals. Other causes with a `Die`, interrupt-only
212
+ * or empty causes, and error-encoding failures reject with a plain `Error`
213
+ * whose message renders the complete cause and whose `cause` holds it for
214
+ * server diagnostics.
206
215
  *
207
216
  * A `scheduler` in `runOptions` must be passed here as a run option rather than
208
217
  * provided via `Effect.provideService` inside `effect`: the run option lands in
209
218
  * the fiber's root context, while a service provided within `effect` pops
210
- * before the `orDie`/`catch`/`result` wrappers this function adds. The fiber's
211
- * op counter survives context pops, so a cooperative yield can fire inside
212
- * those wrappers—only a root-context scheduler covers them.
219
+ * before the exit/encoding wrappers this function adds. The fiber's op counter
220
+ * survives context pops, so a cooperative yield can fire inside those wrappers.
221
+ * Only a root-context scheduler covers them.
213
222
  */
214
223
  export const runHandlerPromise =
215
224
  (
216
225
  errorSchema: Schema.Codec<any, any> | undefined,
217
226
  runOptions?: {
218
227
  readonly scheduler?: EffectScheduler.Scheduler | undefined;
228
+ readonly clock?: Clock.Clock | undefined;
229
+ readonly invocation?: Effect.Effect<FunctionMetadata> | undefined;
230
+ readonly observability?: Layer.Layer<never> | undefined;
219
231
  },
220
232
  ) =>
221
233
  <A, E>(effect: Effect.Effect<A, E>): Promise<A> => {
222
- // A `ConvexError` defect escapes into the (escaped) failure channel so
223
- // the `throw` below rethrows it with its identity intact.
224
- const rethrowConvexErrorDefects = Effect.catchDefect(
225
- (defect: unknown): Effect.Effect<never, ConvexError<any>> =>
226
- defect instanceof ConvexError
227
- ? Effect.fail(defect)
228
- : Effect.die(defect),
234
+ const { observability, invocation, clock } = runOptions ?? {};
235
+ let encodedError: Exit.Exit<any, any> | undefined;
236
+ const observed = Effect.tapCause(effect, (cause) =>
237
+ Effect.gen(function* () {
238
+ const reason =
239
+ cause.reasons.find(Cause.isDieReason) ??
240
+ cause.reasons.find(Cause.isFailReason);
241
+ if (reason?._tag === "Fail" && errorSchema !== undefined) {
242
+ encodedError = yield* Effect.exit(
243
+ Schema.encodeEffect(errorSchema)(reason.error),
244
+ );
245
+ if (Exit.isFailure(encodedError))
246
+ return yield* Effect.orDie(encodedError);
247
+ }
248
+ }),
229
249
  );
230
-
231
- const withConvexError =
232
- errorSchema === undefined
233
- ? Effect.orDie(effect)
234
- : effect.pipe(
235
- Effect.catch((typedError) =>
236
- pipe(
237
- Schema.encodeEffect(errorSchema)(typedError),
238
- Effect.catchTag("SchemaError", Effect.die),
239
- Effect.andThen((encodedError) =>
240
- Effect.fail(new ConvexError(encodedError)),
250
+ const withConvexError: Effect.Effect<Result.Result<A, ConvexError<any>>> =
251
+ Effect.gen(function* () {
252
+ const exit = yield* Effect.exit(
253
+ observability === undefined || invocation === undefined
254
+ ? effect
255
+ : Effect.flatMap(invocation, ({ name, type, componentPath }) =>
256
+ Effect.useSpan(
257
+ name,
258
+ {
259
+ root: true,
260
+ attributes: { kind: type, componentPath },
261
+ },
262
+ (span) =>
263
+ Effect.provideService(observed, Tracer.ParentSpan, span),
241
264
  ),
242
265
  ),
243
- ),
266
+ );
267
+ if (Exit.isSuccess(exit)) {
268
+ return Result.succeed(exit.value);
269
+ }
270
+
271
+ const cause =
272
+ encodedError !== undefined && Exit.isFailure(encodedError)
273
+ ? encodedError.cause
274
+ : exit.cause;
275
+ const reason =
276
+ cause.reasons.find(Cause.isDieReason) ??
277
+ cause.reasons.find(Cause.isFailReason);
278
+ if (
279
+ encodedError === undefined &&
280
+ reason?._tag === "Die" &&
281
+ cause.reasons.length === 1 &&
282
+ reason.defect instanceof ConvexError
283
+ ) {
284
+ return Result.fail(reason.defect);
285
+ }
286
+
287
+ if (reason?._tag === "Fail") {
288
+ if (errorSchema === undefined) {
289
+ if (reason.error instanceof ConvexError) {
290
+ return Result.fail(reason.error);
291
+ }
292
+ return yield* Effect.die(reason.error);
293
+ }
294
+ const encoded =
295
+ encodedError ??
296
+ (yield* Effect.exit(
297
+ Schema.encodeEffect(errorSchema)(reason.error),
298
+ ));
299
+ if (Exit.isSuccess(encoded)) {
300
+ return Result.fail(new ConvexError(encoded.value));
301
+ }
302
+ return yield* Effect.die(
303
+ new Error(Cause.pretty(encoded.cause), { cause: encoded.cause }),
244
304
  );
305
+ }
306
+ return yield* Effect.die(new Error(Cause.pretty(cause), { cause }));
307
+ });
245
308
  return Effect.runPromise(
246
- Effect.result(rethrowConvexErrorDefects(withConvexError)).pipe(
247
- Effect.provide(ConvexLogger.layer),
309
+ withConvexError.pipe(
310
+ Effect.provide(
311
+ observability === undefined
312
+ ? ConvexLogger.layer
313
+ : ObservabilityRuntime.memoized(observability),
314
+ ),
248
315
  Effect.provideService(Console.Console, globalThis.console),
316
+ clock === undefined
317
+ ? (self) => self
318
+ : Effect.provideService(Clock.Clock, clock),
249
319
  ),
250
320
  runOptions,
251
321
  ).then(
@@ -276,6 +346,7 @@ export const actionFunctionBase = <
276
346
  resolvedMiddlewares = [],
277
347
  createLayer,
278
348
  scope = IdScope.app,
349
+ observability,
279
350
  }: {
280
351
  name: string;
281
352
  functionVisibility: FunctionVisibility;
@@ -288,6 +359,7 @@ export const actionFunctionBase = <
288
359
  ctx: GenericActionCtx<DataModel.ToConvex<DataModel.FromSchema<Schema>>>,
289
360
  ) => Layer.Layer<R>;
290
361
  scope?: IdScope.IdScope;
362
+ observability?: Layer.Layer<never> | undefined;
291
363
  }) => ({
292
364
  args: compileArgsSchema(args, scope),
293
365
  returns: compileReturnsSchema(returns, scope),
@@ -324,7 +396,12 @@ export const actionFunctionBase = <
324
396
  Schema.encodeEffect(returns),
325
397
  Effect.catchTag("SchemaError", Effect.die),
326
398
  );
327
- }).pipe(runHandlerPromise(combineErrorSchemas(error, resolvedMiddlewares))),
399
+ }).pipe(
400
+ runHandlerPromise(combineErrorSchemas(error, resolvedMiddlewares), {
401
+ invocation: Effect.promise(() => ctx.meta.getFunctionMetadata()),
402
+ observability,
403
+ }),
404
+ ),
328
405
  });
329
406
 
330
407
  export type ActionServices<
@@ -23,7 +23,7 @@ export const buildForGroup = <Group extends GroupSpec.AnyWithProps>(
23
23
  databaseSchema: DatabaseSchema.AnyWithProps,
24
24
  groupLayer: Layer.Layer<GroupImpl.GroupImpl<"Finalized">>,
25
25
  ): RegisteredFunctions.RegisteredFunctionsForGroupSpec<Group> => {
26
- const { functionRegistryItems, middlewareRegistryItems } =
26
+ const { functionRegistryItems, middlewareRegistryItems, observability } =
27
27
  GroupRegistry.make(groupLayer);
28
28
  return mapLeaves<FunctionRegistryItem.AnyWithProps, RegisteredFunction.Any>(
29
29
  functionRegistryItems as {
@@ -38,6 +38,7 @@ export const buildForGroup = <Group extends GroupSpec.AnyWithProps>(
38
38
  databaseSchema,
39
39
  confect,
40
40
  ResolvedMiddleware.resolve(confect, middlewareRegistryItems),
41
+ observability,
41
42
  ),
42
43
  ),
43
44
  Match.exhaustive,
@@ -49,6 +50,7 @@ export const make = (
49
50
  databaseSchema: DatabaseSchema.AnyWithProps,
50
51
  item: FunctionRegistryItem.ConfectFunctionRegistryItem,
51
52
  resolvedMiddlewares: ReadonlyArray<ResolvedMiddleware.ResolvedMiddleware> = [],
53
+ observability?: Layer.Layer<never>,
52
54
  ): RegisteredFunction.Any => {
53
55
  const genericFunction = Match.value(item.functionVisibility).pipe(
54
56
  Match.when("public", () => actionGeneric),
@@ -59,6 +61,7 @@ export const make = (
59
61
  return genericFunction(
60
62
  nodeActionFunction(databaseSchema, {
61
63
  name: item.name,
64
+ observability,
62
65
  functionVisibility: item.functionVisibility,
63
66
  args: item.args,
64
67
  returns: item.returns,
@@ -86,6 +89,7 @@ const nodeActionFunction = <
86
89
  error,
87
90
  handler,
88
91
  resolvedMiddlewares,
92
+ observability,
89
93
  }: {
90
94
  name: string;
91
95
  functionVisibility: FunctionVisibility;
@@ -101,6 +105,7 @@ const nodeActionFunction = <
101
105
  | NodeServices.NodeServices
102
106
  >;
103
107
  resolvedMiddlewares: ReadonlyArray<ResolvedMiddleware.ResolvedMiddleware>;
108
+ observability?: Layer.Layer<never> | undefined;
104
109
  },
105
110
  ) =>
106
111
  RegisteredFunction.actionFunctionBase({
@@ -112,6 +117,7 @@ const nodeActionFunction = <
112
117
  error,
113
118
  handler,
114
119
  resolvedMiddlewares,
120
+ observability,
115
121
  createLayer: (ctx) =>
116
122
  Layer.mergeAll(
117
123
  RegisteredFunction.actionLayer(databaseSchema, ctx),
@@ -0,0 +1,27 @@
1
+ import * as Duration from "effect/Duration";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Exit from "effect/Exit";
4
+ import * as Layer from "effect/Layer";
5
+ import * as Scope from "effect/Scope";
6
+ import * as ConvexConfigProvider from "../ConvexConfigProvider.js";
7
+
8
+ const memoMap = Layer.makeMemoMapUnsafe();
9
+ export const scope = Scope.makeUnsafe();
10
+ const layers = new WeakMap<Layer.Layer<never>, Layer.Layer<never>>();
11
+
12
+ export const memoized = (layer: Layer.Layer<never>): Layer.Layer<never> => {
13
+ const cached = layers.get(layer);
14
+ if (cached !== undefined) return cached;
15
+ const built = Layer.buildWithMemoMap(layer, memoMap, scope).pipe(
16
+ Effect.provide(ConvexConfigProvider.layer),
17
+ // A failed first build is retried on the next invocation instead of failing every later one.
18
+ Effect.cachedWithTTL((exit) =>
19
+ Exit.isSuccess(exit) ? Duration.infinity : Duration.zero,
20
+ ),
21
+ Effect.runSync,
22
+ Effect.uninterruptible,
23
+ Layer.effectContext,
24
+ );
25
+ layers.set(layer, built);
26
+ return built;
27
+ };