@gaganref/confect-server 10.3.1-fork.8 → 10.3.1-fork.80

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.
Files changed (180) hide show
  1. package/dist/ActionCall.d.ts +30 -0
  2. package/dist/ActionCall.d.ts.map +1 -0
  3. package/dist/ActionCall.js +41 -0
  4. package/dist/ActionCall.js.map +1 -0
  5. package/dist/ActionRunner.d.ts +1 -1
  6. package/dist/ActionRunner.d.ts.map +1 -1
  7. package/dist/ActionRunner.js +3 -3
  8. package/dist/ActionRunner.js.map +1 -1
  9. package/dist/AiGatewayDecisionClient.js +1 -1
  10. package/dist/AiGatewayLanguageClient.js +1 -1
  11. package/dist/Auth.d.ts +31 -2
  12. package/dist/Auth.d.ts.map +1 -1
  13. package/dist/Auth.js +23 -3
  14. package/dist/Auth.js.map +1 -1
  15. package/dist/Callback.d.ts +1 -1
  16. package/dist/Callback.d.ts.map +1 -1
  17. package/dist/Callback.js +6 -6
  18. package/dist/Callback.js.map +1 -1
  19. package/dist/ConvexConfigProvider.js +2 -2
  20. package/dist/ConvexFunctionDefinitions.d.ts.map +1 -1
  21. package/dist/ConvexFunctionDefinitions.js +4 -4
  22. package/dist/ConvexFunctionDefinitions.js.map +1 -1
  23. package/dist/ConvexLogger.js +1 -1
  24. package/dist/CronJobs.d.ts.map +1 -1
  25. package/dist/CronJobs.js +7 -6
  26. package/dist/CronJobs.js.map +1 -1
  27. package/dist/DatabaseSchema.js +2 -2
  28. package/dist/DatabaseWriter.d.ts +36 -2
  29. package/dist/DatabaseWriter.d.ts.map +1 -1
  30. package/dist/DatabaseWriter.js +55 -8
  31. package/dist/DatabaseWriter.js.map +1 -1
  32. package/dist/ExecutionMetadata.js +1 -1
  33. package/dist/FunctionImpl.js +2 -2
  34. package/dist/FunctionRegistryItem.d.ts +1 -0
  35. package/dist/FunctionRegistryItem.d.ts.map +1 -1
  36. package/dist/FunctionRegistryItem.js +2 -1
  37. package/dist/FunctionRegistryItem.js.map +1 -1
  38. package/dist/GroupImpl.d.ts +21 -2
  39. package/dist/GroupImpl.d.ts.map +1 -1
  40. package/dist/GroupImpl.js +21 -22
  41. package/dist/GroupImpl.js.map +1 -1
  42. package/dist/GroupRegistry.d.ts +1 -0
  43. package/dist/GroupRegistry.d.ts.map +1 -1
  44. package/dist/GroupRegistry.js +10 -3
  45. package/dist/GroupRegistry.js.map +1 -1
  46. package/dist/Handler.d.ts +8 -4
  47. package/dist/Handler.d.ts.map +1 -1
  48. package/dist/Handler.js.map +1 -1
  49. package/dist/HttpRouter.d.ts +3 -0
  50. package/dist/HttpRouter.d.ts.map +1 -1
  51. package/dist/HttpRouter.js +29 -10
  52. package/dist/HttpRouter.js.map +1 -1
  53. package/dist/MiddlewareImpl.js +1 -1
  54. package/dist/MountInit.d.ts +31 -0
  55. package/dist/MountInit.d.ts.map +1 -0
  56. package/dist/MountInit.js +49 -0
  57. package/dist/MountInit.js.map +1 -0
  58. package/dist/MutationRunner.d.ts +1 -1
  59. package/dist/MutationRunner.d.ts.map +1 -1
  60. package/dist/MutationRunner.js +3 -3
  61. package/dist/MutationRunner.js.map +1 -1
  62. package/dist/MutationTransactionContext.d.ts.map +1 -1
  63. package/dist/MutationTransactionContext.js +4 -3
  64. package/dist/MutationTransactionContext.js.map +1 -1
  65. package/dist/OfficialComponent.d.ts +60 -0
  66. package/dist/OfficialComponent.d.ts.map +1 -0
  67. package/dist/OfficialComponent.js +72 -0
  68. package/dist/OfficialComponent.js.map +1 -0
  69. package/dist/OrderedQuery.d.ts +4 -5
  70. package/dist/OrderedQuery.d.ts.map +1 -1
  71. package/dist/OrderedQuery.js.map +1 -1
  72. package/dist/QueryInitializer.js +2 -2
  73. package/dist/QueryRunner.js +2 -2
  74. package/dist/QueryStream.d.ts +120 -12
  75. package/dist/QueryStream.d.ts.map +1 -1
  76. package/dist/QueryStream.js +143 -41
  77. package/dist/QueryStream.js.map +1 -1
  78. package/dist/QueryStreamCursor.js +1 -1
  79. package/dist/QueryStreamIndexPrefix.js +1 -1
  80. package/dist/QueryStreamIndexRange.js +2 -2
  81. package/dist/QueryStreamKey.js +1 -1
  82. package/dist/QueryStreamKeyBounds.js +2 -2
  83. package/dist/QueryStreamKeyLayout.js +2 -2
  84. package/dist/QueryStreamKeyValues.js +2 -2
  85. package/dist/QueryStreamPagination.js +1 -1
  86. package/dist/QueryStreamReadBudget.d.ts +3 -2
  87. package/dist/QueryStreamReadBudget.d.ts.map +1 -1
  88. package/dist/QueryStreamReadBudget.js +8 -5
  89. package/dist/QueryStreamReadBudget.js.map +1 -1
  90. package/dist/QueryTransactionContext.js +2 -2
  91. package/dist/RegisteredConvexFunction.d.ts +10 -5
  92. package/dist/RegisteredConvexFunction.d.ts.map +1 -1
  93. package/dist/RegisteredConvexFunction.js +45 -29
  94. package/dist/RegisteredConvexFunction.js.map +1 -1
  95. package/dist/RegisteredFunction.d.ts +39 -22
  96. package/dist/RegisteredFunction.d.ts.map +1 -1
  97. package/dist/RegisteredFunction.js +155 -42
  98. package/dist/RegisteredFunction.js.map +1 -1
  99. package/dist/RegisteredNodeFunction.d.ts +1 -1
  100. package/dist/RegisteredNodeFunction.d.ts.map +1 -1
  101. package/dist/RegisteredNodeFunction.js +10 -6
  102. package/dist/RegisteredNodeFunction.js.map +1 -1
  103. package/dist/RegistryItems.d.ts +7 -0
  104. package/dist/RegistryItems.d.ts.map +1 -1
  105. package/dist/RegistryItems.js +8 -3
  106. package/dist/RegistryItems.js.map +1 -1
  107. package/dist/RequestMetadata.js +1 -1
  108. package/dist/Scheduler.d.ts +1 -1
  109. package/dist/Scheduler.d.ts.map +1 -1
  110. package/dist/Scheduler.js +14 -13
  111. package/dist/Scheduler.js.map +1 -1
  112. package/dist/Storage.js +1 -1
  113. package/dist/StorageActionWriter.js +1 -1
  114. package/dist/StorageReader.js +1 -1
  115. package/dist/StorageWriter.js +1 -1
  116. package/dist/Table.d.ts +6 -6
  117. package/dist/Table.d.ts.map +1 -1
  118. package/dist/TableInfo.d.ts +1 -1
  119. package/dist/TableInfo.d.ts.map +1 -1
  120. package/dist/TableInfo.js.map +1 -1
  121. package/dist/TransactionMetadata.js +1 -1
  122. package/dist/VectorSearch.js +1 -1
  123. package/dist/index.d.ts +3 -0
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +21 -18
  126. package/dist/internal/AiGatewayDecisionClient.js +1 -1
  127. package/dist/internal/AiGatewayLanguageClient.js +1 -1
  128. package/dist/internal/AiGatewayServiceToken.js +1 -1
  129. package/dist/internal/DocumentPatch.d.ts +16 -0
  130. package/dist/internal/DocumentPatch.d.ts.map +1 -0
  131. package/dist/internal/DocumentPatch.js +36 -0
  132. package/dist/internal/DocumentPatch.js.map +1 -0
  133. package/dist/internal/httpBody.d.ts +13 -0
  134. package/dist/internal/httpBody.d.ts.map +1 -0
  135. package/dist/internal/httpBody.js +47 -0
  136. package/dist/internal/httpBody.js.map +1 -0
  137. package/dist/internal/httpOptions.d.ts +4 -0
  138. package/dist/internal/httpOptions.d.ts.map +1 -0
  139. package/dist/internal/httpOptions.js +83 -0
  140. package/dist/internal/httpOptions.js.map +1 -0
  141. package/dist/internal/observability.d.ts +5 -0
  142. package/dist/internal/observability.d.ts.map +1 -0
  143. package/dist/internal/observability.js +21 -0
  144. package/dist/internal/observability.js.map +1 -0
  145. package/dist/internal/reservedArgument.d.ts +8 -0
  146. package/dist/internal/reservedArgument.d.ts.map +1 -0
  147. package/dist/internal/reservedArgument.js +30 -0
  148. package/dist/internal/reservedArgument.js.map +1 -0
  149. package/package.json +5 -5
  150. package/src/ActionCall.ts +83 -0
  151. package/src/ActionRunner.ts +3 -2
  152. package/src/Auth.ts +49 -1
  153. package/src/Callback.ts +3 -2
  154. package/src/ConvexFunctionDefinitions.ts +2 -1
  155. package/src/CronJobs.ts +2 -1
  156. package/src/DatabaseWriter.ts +183 -25
  157. package/src/FunctionRegistryItem.ts +4 -0
  158. package/src/GroupImpl.ts +61 -8
  159. package/src/GroupRegistry.ts +7 -3
  160. package/src/Handler.ts +21 -5
  161. package/src/HttpRouter.ts +69 -5
  162. package/src/MountInit.ts +80 -0
  163. package/src/MutationRunner.ts +3 -2
  164. package/src/MutationTransactionContext.ts +2 -1
  165. package/src/OfficialComponent.ts +168 -0
  166. package/src/OrderedQuery.ts +4 -5
  167. package/src/QueryStream.ts +391 -61
  168. package/src/QueryStreamReadBudget.ts +19 -6
  169. package/src/RegisteredConvexFunction.ts +35 -2
  170. package/src/RegisteredFunction.ts +251 -45
  171. package/src/RegisteredNodeFunction.ts +11 -1
  172. package/src/RegistryItems.ts +31 -0
  173. package/src/Scheduler.ts +17 -19
  174. package/src/TableInfo.ts +2 -1
  175. package/src/index.ts +3 -0
  176. package/src/internal/DocumentPatch.ts +87 -0
  177. package/src/internal/httpBody.ts +72 -0
  178. package/src/internal/httpOptions.ts +154 -0
  179. package/src/internal/observability.ts +27 -0
  180. package/src/internal/reservedArgument.ts +56 -0
package/src/Scheduler.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import * as ReservedArgument from "./internal/reservedArgument.js";
1
2
  import type * as IdScope from "@confect/core/IdScope";
2
- import { Ref } from "@confect/core";
3
+ import type { Ref } from "@confect/core";
4
+ import { getFunctionReference } from "@confect/core/RefInternal";
3
5
  import type { GenericId } from "@confect/core/GenericId";
4
6
  import type { Scheduler as ConvexScheduler } from "convex/server";
5
7
  import type { GenericId as ConvexGenericId } from "convex/values";
@@ -12,38 +14,34 @@ import * as Layer from "effect/Layer";
12
14
  const make = (scheduler: ConvexScheduler) => ({
13
15
  cancel: (id: ConvexGenericId<"_scheduled_functions">) =>
14
16
  Effect.promise(() => scheduler.cancel(id)),
15
- runAfter: <Ref_ extends Ref.AnyMutation | Ref.AnyAction>(
16
- delay: Duration.Duration,
17
- ref: Ref_,
18
- ...args: Ref.OptionalArgs<Ref_>
19
- ) => {
17
+ runAfter: Effect.fn("Scheduler.runAfter")(function* <
18
+ Ref_ extends Ref.AnyMutation | Ref.AnyAction,
19
+ >(delay: Duration.Duration, ref: Ref_, ...args: Ref.OptionalArgs<Ref_>) {
20
20
  const delayMs = Duration.toMillis(delay);
21
- const functionReference = Ref.getFunctionReference(ref);
22
- const encodedArgs = Ref.encodeArgsSync(
21
+ const functionReference = getFunctionReference(ref);
22
+ const encodedArgs = yield* ReservedArgument.encodeArgs(
23
23
  ref,
24
24
  (args[0] ?? {}) as Ref.Args<Ref_>,
25
25
  );
26
26
 
27
- return Effect.promise(() =>
27
+ return yield* Effect.promise(() =>
28
28
  scheduler.runAfter(delayMs, functionReference, encodedArgs),
29
29
  );
30
- },
31
- runAt: <Ref_ extends Ref.AnyMutation | Ref.AnyAction>(
32
- dateTime: DateTime.DateTime,
33
- ref: Ref_,
34
- ...args: Ref.OptionalArgs<Ref_>
35
- ) => {
30
+ }),
31
+ runAt: Effect.fn("Scheduler.runAt")(function* <
32
+ Ref_ extends Ref.AnyMutation | Ref.AnyAction,
33
+ >(dateTime: DateTime.DateTime, ref: Ref_, ...args: Ref.OptionalArgs<Ref_>) {
36
34
  const timestamp = DateTime.toEpochMillis(dateTime);
37
- const functionReference = Ref.getFunctionReference(ref);
38
- const encodedArgs = Ref.encodeArgsSync(
35
+ const functionReference = getFunctionReference(ref);
36
+ const encodedArgs = yield* ReservedArgument.encodeArgs(
39
37
  ref,
40
38
  (args[0] ?? {}) as Ref.Args<Ref_>,
41
39
  );
42
40
 
43
- return Effect.promise(() =>
41
+ return yield* Effect.promise(() =>
44
42
  scheduler.runAt(timestamp, functionReference, encodedArgs),
45
43
  );
46
- },
44
+ }),
47
45
  });
48
46
 
49
47
  export const Scheduler = Context.Service<ReturnType<typeof make>>(
package/src/TableInfo.ts CHANGED
@@ -26,7 +26,8 @@ export type TableInfo<Table_ extends Table.AnyWithProps> =
26
26
  infer Indexes,
27
27
  infer SearchIndexes,
28
28
  infer VectorIndexes,
29
- infer Scope
29
+ infer Scope,
30
+ any
30
31
  >
31
32
  ? {
32
33
  readonly [TypeId]: TypeId;
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * as ActionCall from "./ActionCall.js";
1
2
  export * as AiGatewayDecisionClient from "./AiGatewayDecisionClient.js";
2
3
  export * as AiGatewayDecisionModel from "./AiGatewayDecisionModel.js";
3
4
  export * as AiGatewayLanguageClient from "./AiGatewayLanguageClient.js";
@@ -25,9 +26,11 @@ export * as Handler from "./Handler.js";
25
26
  export * as HttpRouter from "./HttpRouter.js";
26
27
  export * as MiddlewareImpl from "./MiddlewareImpl.js";
27
28
  export * as MiddlewareRegistryItem from "./MiddlewareRegistryItem.js";
29
+ export * as MountInit from "./MountInit.js";
28
30
  export * as MutationCtx from "./MutationCtx.js";
29
31
  export * as MutationRunner from "./MutationRunner.js";
30
32
  export * as MutationTransactionContext from "./MutationTransactionContext.js";
33
+ export * as OfficialComponent from "./OfficialComponent.js";
31
34
  export * as OrderedQuery from "./OrderedQuery.js";
32
35
  export * as QueryCtx from "./QueryCtx.js";
33
36
  export * as QueryInitializer from "./QueryInitializer.js";
@@ -0,0 +1,87 @@
1
+ import type { ReadonlyValue } from "@confect/core/SchemaToValidator";
2
+ import { pipe } from "effect/Function";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Record from "effect/Record";
5
+ import * as Schema from "effect/Schema";
6
+ import * as SchemaAST from "effect/SchemaAST";
7
+ import type * as Table from "../Table.js";
8
+
9
+ export type EncodedPatch = Record.ReadonlyRecord<
10
+ string,
11
+ ReadonlyValue | undefined
12
+ >;
13
+
14
+ export type PatchEncoder = (
15
+ patch: Record.ReadonlyRecord<string, unknown>,
16
+ ) => Effect.Effect<EncodedPatch, Schema.SchemaError>;
17
+
18
+ const patchEncoders = new WeakMap<Table.AnyWithProps, PatchEncoder | null>();
19
+
20
+ /**
21
+ * Returns the encoder for patches to the table's documents, or `undefined` when
22
+ * the table's codec does not encode each field independently. The encoder maps
23
+ * every patched field of the table to its encoded value, or to `undefined` when
24
+ * the patch unsets the field or the field's codec encodes the value as an
25
+ * absent key. Keys outside the table's fields are left out.
26
+ */
27
+ export const encoder = (
28
+ table: Table.AnyWithProps,
29
+ ): PatchEncoder | undefined => {
30
+ let cached = patchEncoders.get(table);
31
+ if (cached === undefined) {
32
+ cached = makePatchEncoder(table);
33
+ patchEncoders.set(table, cached);
34
+ }
35
+ return cached ?? undefined;
36
+ };
37
+
38
+ // A struct with no encoding of its own encodes each field through that field's
39
+ // codec alone, so a patch encodes field by field. A union or a transformation
40
+ // over the whole struct can make one field's encoding depend on another field.
41
+ // A struct-level check must see the decoded fields before they are encoded,
42
+ // and keys under an index signature have no field codec of their own.
43
+ const makePatchEncoder = (table: Table.AnyWithProps): PatchEncoder | null => {
44
+ const ast = table.Fields.ast;
45
+ if (
46
+ !SchemaAST.isObjects(ast) ||
47
+ ast.encoding !== undefined ||
48
+ ast.checks !== undefined ||
49
+ ast.indexSignatures.length > 0
50
+ ) {
51
+ return null;
52
+ }
53
+ // A field's codec runs only when the patch gives the field a value, since a
54
+ // codec can handle a missing key. Each comes from the table, whose codecs
55
+ // need no services.
56
+ const fieldEncoders: Record.ReadonlyRecord<
57
+ string,
58
+ (
59
+ value: unknown,
60
+ ) => Effect.Effect<ReadonlyValue | undefined, Schema.SchemaError>
61
+ > = Object.fromEntries(
62
+ ast.propertySignatures.map(({ name, type }) => {
63
+ const key = String(name);
64
+ const encode = Schema.encodeUnknownEffect(
65
+ Schema.Struct({
66
+ [key]: Schema.optionalKey(
67
+ Schema.make<Schema.Codec<unknown, ReadonlyValue>>(type),
68
+ ),
69
+ }),
70
+ );
71
+ return [
72
+ key,
73
+ (value: unknown) =>
74
+ Effect.map(encode({ [key]: value }), (encoded) => encoded[key]),
75
+ ];
76
+ }),
77
+ );
78
+ return (patch) =>
79
+ pipe(
80
+ fieldEncoders,
81
+ Record.filter((_, name) => Object.hasOwn(patch, name)),
82
+ Record.map((encode, name) =>
83
+ patch[name] === undefined ? Effect.undefined : encode(patch[name]),
84
+ ),
85
+ Effect.all,
86
+ );
87
+ };
@@ -0,0 +1,72 @@
1
+ import type * as ByteSize from "effect/ByteSize";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Schema from "effect/Schema";
4
+ import * as Stream from "effect/Stream";
5
+ import * as HttpServerRequest from "effect/http/HttpServerRequest";
6
+ import * as HttpServerResponse from "effect/http/HttpServerResponse";
7
+
8
+ class BodyTooLarge extends Schema.TaggedError<BodyTooLarge>()(
9
+ "BodyTooLarge",
10
+ {},
11
+ ) {}
12
+
13
+ /**
14
+ * Count the body before downstream middleware or decoding. Configure maxBytes
15
+ * from the endpoint's policy. Non-identity Content-Encoding receives 415. Read
16
+ * arrayBuffer in the wrapped effect to verify the same bytes the decoder uses.
17
+ */
18
+ export const withBodyLimit = <A, E, R>(
19
+ effect: Effect.Effect<A, E, R>,
20
+ options: { readonly maxBytes: ByteSize.ByteSize },
21
+ ) =>
22
+ Effect.gen(function* () {
23
+ const request = yield* HttpServerRequest.HttpServerRequest;
24
+ const encoding = request.headers["content-encoding"]?.trim().toLowerCase();
25
+ if (encoding !== undefined && encoding !== "identity")
26
+ return HttpServerResponse.empty({ status: 415 });
27
+ let size = 0;
28
+ const chunks: Array<Uint8Array> = [];
29
+ const empty =
30
+ request.source instanceof Request && request.source.body === null;
31
+ const read = empty
32
+ ? Effect.void
33
+ : Stream.runForEach(request.stream, (chunk) => {
34
+ size += chunk.byteLength;
35
+ if (BigInt(size) > options.maxBytes)
36
+ return Effect.fail(new BodyTooLarge());
37
+ chunks.push(chunk);
38
+ return Effect.void;
39
+ });
40
+ const limited = yield* read.pipe(
41
+ Effect.as(false),
42
+ Effect.catchTag("BodyTooLarge", () => Effect.succeed(true)),
43
+ Effect.orDie,
44
+ );
45
+ if (limited) return HttpServerResponse.empty({ status: 413 });
46
+ const bytes = new Uint8Array(size);
47
+ let offset = 0;
48
+ for (const chunk of chunks) {
49
+ bytes.set(chunk, offset);
50
+ offset += chunk.byteLength;
51
+ }
52
+ const source = new Request(request.originalUrl, {
53
+ method: request.method,
54
+ headers: request.headers,
55
+ ...(request.method === "GET" || request.method === "HEAD"
56
+ ? {}
57
+ : { body: bytes }),
58
+ });
59
+ // WORKAROUND: Effect 4.0.4 caches text and bytes independently. Remove these
60
+ // overrides when the minimum Effect version shares text and byte reads.
61
+ source.arrayBuffer = () => Promise.resolve(bytes.buffer);
62
+ source.text = () => Promise.resolve(new TextDecoder().decode(bytes));
63
+ const buffered = HttpServerRequest.fromWeb(source).modify({
64
+ url: request.url,
65
+ remoteAddress: request.remoteAddress,
66
+ });
67
+ return yield* Effect.provideService(
68
+ effect,
69
+ HttpServerRequest.HttpServerRequest,
70
+ buffered,
71
+ );
72
+ });
@@ -0,0 +1,154 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Exit from "effect/Exit";
4
+ import * as Layer from "effect/Layer";
5
+ import * as HttpRouter from "effect/http/HttpRouter";
6
+ import * as HttpServerError from "effect/http/HttpServerError";
7
+ import * as HttpServerRequest from "effect/http/HttpServerRequest";
8
+
9
+ /* oxlint-disable effecttsgo/any-unknown-in-error-context -- Preserve the router's erased request errors at the routing boundary. */
10
+
11
+ type Route = Parameters<HttpRouter.HttpRouter["addAll"]>[0][number];
12
+
13
+ const notFound = Effect.flatMap(
14
+ HttpServerRequest.HttpServerRequest,
15
+ (request) =>
16
+ Effect.fail(
17
+ new HttpServerError.HttpServerError({
18
+ reason: new HttpServerError.RouteNotFound({ request }),
19
+ }),
20
+ ),
21
+ );
22
+
23
+ // Effect's router adds `/a/:b` and `/a` for `/a/:b?` in one call, so an earlier
24
+ // `/a/:id` rejects both. Split the same way, each form claims `first` alone.
25
+ const optional = /(\/:[^/()]*?)\?(\/?)/;
26
+ const forms = (path: Route["path"]) =>
27
+ (optional.test(path)
28
+ ? [path.replace(optional, "$1$2"), path.replace(optional, "$2") || "/"]
29
+ : [path]) as ReadonlyArray<Route["path"]>;
30
+
31
+ const missingRouteContext = `Service not found: ${HttpRouter.RouteContext.key}`;
32
+
33
+ // WORKAROUND: Effect 4.0.4 compiles endpoint middleware, decoding and the
34
+ // handler into one route handler. Its endpoint body reads RouteContext before
35
+ // decoding or calling the handler, so without RouteContext a preflight that
36
+ // middleware passes on stops there. Remove when Effect can run an endpoint's
37
+ // middleware around another handler.
38
+ const preflight = (handler: Route["handler"]) =>
39
+ handler.pipe(
40
+ Effect.updateContext(Context.omit(HttpRouter.RouteContext)),
41
+ Effect.catchDefect((defect) =>
42
+ defect instanceof Error && defect.message === missingRouteContext
43
+ ? notFound
44
+ : Effect.die(defect),
45
+ ),
46
+ );
47
+
48
+ export const withOptions = <A, E, R>(
49
+ routes: Layer.Layer<A, E, R>,
50
+ endpoints: ReadonlySet<string>,
51
+ ) =>
52
+ Layer.unwrap(
53
+ Effect.gen(function* () {
54
+ const router = yield* HttpRouter.HttpRouter;
55
+ const registered: Array<{
56
+ readonly route: Route;
57
+ readonly prefix: string;
58
+ readonly context: Context.Context<never>;
59
+ }> = [];
60
+ const wrap = (
61
+ target: HttpRouter.HttpRouter,
62
+ prefix: string,
63
+ ): HttpRouter.HttpRouter => {
64
+ const addAll: HttpRouter.HttpRouter["addAll"] = (entries) =>
65
+ Effect.contextWith((context: Context.Context<never>) => {
66
+ for (const route of entries)
67
+ registered.push({ route, prefix, context });
68
+ return target.addAll(entries);
69
+ });
70
+ return HttpRouter.HttpRouter.of({
71
+ ...target,
72
+ addAll,
73
+ add: (method, path, handler, options) =>
74
+ addAll([HttpRouter.route(method, path, handler, options)]),
75
+ prefixed: (next) =>
76
+ wrap(target.prefixed(next), `${prefix}${next.replace(/\/$/, "")}`),
77
+ });
78
+ };
79
+ const register = Effect.gen(function* () {
80
+ // Effect's routers decide which paths match: one per declared method,
81
+ // and `first`, which refuses a path equivalent to an earlier one.
82
+ const byMethod = new Map<string, HttpRouter.HttpRouter>();
83
+ const first = yield* HttpRouter.make;
84
+ const explicit = yield* HttpRouter.make;
85
+ for (const { route, prefix, context } of registered) {
86
+ const at = (target: HttpRouter.HttpRouter) =>
87
+ prefix === "" ? target : target.prefixed(prefix);
88
+ if (route.method === "OPTIONS" || route.method === "*") {
89
+ yield* at(explicit).addAll([{ ...route, handler: notFound }]);
90
+ continue;
91
+ }
92
+ const copy: Route = {
93
+ ...route,
94
+ method: "OPTIONS",
95
+ handler: endpoints.has(`${route.method} ${route.path}`)
96
+ ? preflight(route.handler)
97
+ : notFound,
98
+ };
99
+ const candidate =
100
+ byMethod.get(route.method) ?? (yield* HttpRouter.make);
101
+ byMethod.set(route.method, candidate);
102
+ // Route registration erases request requirements, as in Effect's router.
103
+ yield* Effect.provideContext(at(candidate).addAll([copy]), context);
104
+ // Split the prefixed path: an optional parameter may sit in the prefix.
105
+ const placed =
106
+ prefix === "" ? copy : HttpRouter.prefixRoute(copy, prefix);
107
+ for (const path of forms(placed.path)) {
108
+ const form = first.addAll([{ ...placed, path }]);
109
+ yield* Effect.exit(Effect.provideContext(form, context));
110
+ }
111
+ }
112
+ // Effect's router answers HEAD from GET where no HEAD route matches. The
113
+ // root wildcard matches last, unless a HEAD route already claims it.
114
+ const get = byMethod.get("GET");
115
+ if (get !== undefined) {
116
+ const head = byMethod.get("HEAD") ?? (yield* HttpRouter.make);
117
+ byMethod.set("HEAD", head);
118
+ yield* Effect.exit(head.add("OPTIONS", "*", get.asHttpEffect()));
119
+ }
120
+ // An explicit OPTIONS route on the root wildcard answers every path.
121
+ const claimed = yield* Effect.exit(
122
+ explicit.add("OPTIONS", "*", notFound),
123
+ );
124
+ if (Exit.isFailure(claimed)) return;
125
+ const candidates = new Map(
126
+ Array.from(byMethod, ([method, candidate]) => [
127
+ method,
128
+ candidate.asHttpEffect(),
129
+ ]),
130
+ );
131
+ const fallback = first.asHttpEffect();
132
+ // The root wildcard matches last, so explicit OPTIONS routes still win.
133
+ yield* router.add(
134
+ "OPTIONS",
135
+ "*",
136
+ Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {
137
+ const method = request.headers["access-control-request-method"];
138
+ return method === undefined
139
+ ? fallback
140
+ : (candidates.get(method) ?? notFound);
141
+ }),
142
+ );
143
+ });
144
+ return Layer.effectDiscard(register).pipe(
145
+ Layer.provideMerge(
146
+ routes.pipe(
147
+ Layer.provide(
148
+ Layer.succeed(HttpRouter.HttpRouter, wrap(router, "")),
149
+ ),
150
+ ),
151
+ ),
152
+ );
153
+ }),
154
+ );
@@ -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
+ };
@@ -0,0 +1,56 @@
1
+ import { convexToJson, type Value } from "convex/values";
2
+ import * as Ref from "@confect/core/Ref";
3
+ import * as Context from "effect/Context";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Tracer from "effect/Tracer";
6
+
7
+ export const Forward = Context.Reference<
8
+ ((span: Tracer.Span) => unknown) | undefined
9
+ >("@confect/server/RegisteredFunction/Forward", {
10
+ defaultValue: () => undefined,
11
+ });
12
+
13
+ const inject = (
14
+ ref: Ref.Any,
15
+ encoded: unknown,
16
+ context: Context.Context<never>,
17
+ ): unknown => {
18
+ if (ref.reservedArgument === undefined || ref.functionType === "query")
19
+ return encoded;
20
+ const forward = Context.get(context, Forward);
21
+ const span = Context.getOrUndefined(context, Tracer.ParentSpan);
22
+ if (forward === undefined || span?._tag !== "Span") return encoded;
23
+ try {
24
+ const value = forward(span);
25
+ if (value === undefined) return encoded;
26
+ convexToJson(value as Value);
27
+ return {
28
+ ...(encoded as Record<string, unknown>),
29
+ [ref.reservedArgument]: value,
30
+ };
31
+ } catch {
32
+ return encoded;
33
+ }
34
+ };
35
+
36
+ export const runWithCodec = <Ref_ extends Ref.Any>(
37
+ ref: Ref_,
38
+ args: Ref.Args<Ref_>,
39
+ call: (
40
+ native: Ref.FunctionReference<Ref_>,
41
+ encoded: unknown,
42
+ ) => PromiseLike<unknown>,
43
+ ) =>
44
+ Effect.withFiber((fiber) =>
45
+ Ref.runWithCodec(ref, args, (native, encoded) =>
46
+ call(native, inject(ref, encoded, fiber.context)),
47
+ ),
48
+ );
49
+
50
+ export const encodeArgs = <Ref_ extends Ref.Any>(
51
+ ref: Ref_,
52
+ args: Ref.Args<Ref_>,
53
+ ) =>
54
+ Effect.withFiberSucceed((fiber) =>
55
+ inject(ref, Ref.encodeArgsSync(ref, args), fiber.context),
56
+ );