@confect/server 7.0.0 → 9.0.0-next.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/DatabaseSchema.d.ts +6 -6
  3. package/dist/DatabaseSchema.d.ts.map +1 -1
  4. package/dist/DatabaseSchema.js +3 -3
  5. package/dist/DatabaseSchema.js.map +1 -1
  6. package/dist/Document.d.ts.map +1 -1
  7. package/dist/Document.js +35 -23
  8. package/dist/Document.js.map +1 -1
  9. package/dist/FunctionImpl.d.ts +10 -7
  10. package/dist/FunctionImpl.d.ts.map +1 -1
  11. package/dist/FunctionImpl.js +8 -8
  12. package/dist/FunctionImpl.js.map +1 -1
  13. package/dist/GroupImpl.d.ts +51 -12
  14. package/dist/GroupImpl.d.ts.map +1 -1
  15. package/dist/GroupImpl.js +72 -4
  16. package/dist/GroupImpl.js.map +1 -1
  17. package/dist/GroupPath.d.ts +8 -0
  18. package/dist/GroupPath.d.ts.map +1 -0
  19. package/dist/GroupPath.js +10 -0
  20. package/dist/GroupPath.js.map +1 -0
  21. package/dist/RegisteredConvexFunction.d.ts +6 -6
  22. package/dist/RegisteredConvexFunction.d.ts.map +1 -1
  23. package/dist/RegisteredConvexFunction.js +18 -7
  24. package/dist/RegisteredConvexFunction.js.map +1 -1
  25. package/dist/RegisteredFunction.d.ts +3 -3
  26. package/dist/RegisteredFunction.d.ts.map +1 -1
  27. package/dist/RegisteredFunctions.d.ts +15 -4
  28. package/dist/RegisteredFunctions.d.ts.map +1 -1
  29. package/dist/RegisteredFunctions.js +20 -11
  30. package/dist/RegisteredFunctions.js.map +1 -1
  31. package/dist/SchemaToValidator.d.ts +8 -8
  32. package/dist/index.d.ts +1 -3
  33. package/dist/index.js +1 -3
  34. package/package.json +21 -19
  35. package/src/DatabaseSchema.ts +5 -5
  36. package/src/Document.ts +90 -58
  37. package/src/FunctionImpl.ts +27 -36
  38. package/src/GroupImpl.ts +168 -32
  39. package/src/GroupPath.ts +43 -0
  40. package/src/RegisteredConvexFunction.ts +18 -17
  41. package/src/RegisteredFunctions.ts +78 -28
  42. package/src/index.ts +0 -2
  43. package/dist/Impl.d.ts +0 -24
  44. package/dist/Impl.d.ts.map +0 -1
  45. package/dist/Impl.js +0 -28
  46. package/dist/Impl.js.map +0 -1
  47. package/dist/Registry.d.ts +0 -15
  48. package/dist/Registry.d.ts.map +0 -1
  49. package/dist/Registry.js +0 -10
  50. package/dist/Registry.js.map +0 -1
  51. package/src/Impl.ts +0 -59
  52. package/src/Registry.ts +0 -13
@@ -9,7 +9,11 @@ import * as Table from "./Table";
9
9
  export const TypeId = "@confect/server/DatabaseSchema";
10
10
  export type TypeId = typeof TypeId;
11
11
 
12
- export const isSchema = (u: unknown): u is Any =>
12
+ export interface Any {
13
+ readonly [TypeId]: TypeId;
14
+ }
15
+
16
+ export const isDatabaseSchema = (u: unknown): u is Any =>
13
17
  Predicate.hasProperty(u, TypeId);
14
18
 
15
19
  /**
@@ -31,10 +35,6 @@ export interface DatabaseSchema<Tables_ extends Table.AnyWithProps = never> {
31
35
  ): DatabaseSchema<Tables_ | TableDef>;
32
36
  }
33
37
 
34
- export interface Any {
35
- readonly [TypeId]: TypeId;
36
- }
37
-
38
38
  export interface AnyWithProps {
39
39
  readonly [TypeId]: TypeId;
40
40
  readonly tables: Record<string, Table.AnyWithProps>;
package/src/Document.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import * as SystemFields from "@confect/core/SystemFields";
1
2
  import { Effect, Function, ParseResult, pipe, Schema } from "effect";
2
3
  import type { ReadonlyRecord } from "effect/Record";
3
- import * as SystemFields from "@confect/core/SystemFields";
4
4
  import type * as DataModel from "./DataModel";
5
5
  import type { ReadonlyValue } from "./SchemaToValidator";
6
6
  import type * as TableInfo from "./TableInfo";
@@ -10,6 +10,37 @@ export type WithoutSystemFields<Doc> = Omit<Doc, "_creationTime" | "_id">;
10
10
  export type Any = any;
11
11
  export type AnyEncoded = ReadonlyRecord<string, ReadonlyValue>;
12
12
 
13
+ type Decode = (doc: unknown) => Effect.Effect<unknown, ParseResult.ParseError>;
14
+
15
+ const decoderCache = new WeakMap<
16
+ Schema.Schema.AnyNoContext,
17
+ Map<string, Decode>
18
+ >();
19
+
20
+ const getDecoder = (
21
+ tableName: string,
22
+ tableSchema: Schema.Schema.AnyNoContext,
23
+ ): Decode => {
24
+ const byTable =
25
+ decoderCache.get(tableSchema) ??
26
+ (() => {
27
+ const map = new Map<string, Decode>();
28
+ decoderCache.set(tableSchema, map);
29
+ return map;
30
+ })();
31
+
32
+ return (
33
+ byTable.get(tableName) ??
34
+ (() => {
35
+ const decoder = Schema.decode(
36
+ SystemFields.extendWithSystemFields(tableName, tableSchema),
37
+ ) as Decode;
38
+ byTable.set(tableName, decoder);
39
+ return decoder;
40
+ })()
41
+ );
42
+ };
43
+
13
44
  export const decode = Function.dual<
14
45
  <
15
46
  DataModel_ extends DataModel.AnyWithProps,
@@ -53,36 +84,43 @@ export const decode = Function.dual<
53
84
  DataModel.TableInfoWithName_<DataModel_, TableName>["document"],
54
85
  DocumentDecodeError
55
86
  > =>
56
- Effect.gen(function* () {
57
- const TableSchemaWithSystemFields = SystemFields.extendWithSystemFields(
58
- tableName,
59
- tableSchema,
60
- );
61
-
62
- const encodedDoc =
63
- self as (typeof TableSchemaWithSystemFields)["Encoded"];
64
-
65
- const decodedDoc = yield* pipe(
66
- encodedDoc,
67
- Schema.decode(TableSchemaWithSystemFields),
68
- Effect.catchTag("ParseError", (parseError) =>
69
- Effect.gen(function* () {
70
- const formattedParseError =
71
- yield* ParseResult.TreeFormatter.formatError(parseError);
72
-
73
- return yield* new DocumentDecodeError({
74
- tableName,
75
- id: encodedDoc._id,
76
- parseError: formattedParseError,
77
- });
78
- }),
79
- ),
80
- );
81
-
82
- return decodedDoc;
83
- }),
87
+ pipe(
88
+ self,
89
+ getDecoder(tableName, tableSchema),
90
+ Effect.catchIf(ParseResult.isParseError, (parseError) =>
91
+ Effect.gen(function* () {
92
+ const formattedParseError =
93
+ yield* ParseResult.TreeFormatter.formatError(parseError);
94
+
95
+ return yield* new DocumentDecodeError({
96
+ tableName,
97
+ id: self._id,
98
+ parseError: formattedParseError,
99
+ });
100
+ }),
101
+ ),
102
+ Effect.map(
103
+ (decodedDoc) =>
104
+ decodedDoc as DataModel.TableInfoWithName_<
105
+ DataModel_,
106
+ TableName
107
+ >["document"],
108
+ ),
109
+ ),
84
110
  );
85
111
 
112
+ type Encode = (doc: unknown) => Effect.Effect<unknown, ParseResult.ParseError>;
113
+
114
+ const encoderCache = new WeakMap<Schema.Schema.AnyNoContext, Encode>();
115
+
116
+ const getEncoder = (tableSchema: Schema.Schema.AnyNoContext): Encode =>
117
+ encoderCache.get(tableSchema) ??
118
+ (() => {
119
+ const encoder = Schema.encode(tableSchema) as Encode;
120
+ encoderCache.set(tableSchema, encoder);
121
+ return encoder;
122
+ })();
123
+
86
124
  export const encode = Function.dual<
87
125
  <
88
126
  DataModel_ extends DataModel.AnyWithProps,
@@ -126,35 +164,29 @@ export const encode = Function.dual<
126
164
  DataModel.TableInfoWithName_<DataModel_, TableName>["encodedDocument"],
127
165
  DocumentEncodeError
128
166
  > =>
129
- Effect.gen(function* () {
130
- type TableSchemaWithSystemFields = SystemFields.ExtendWithSystemFields<
131
- TableName,
132
- TableInfo.TableSchema<
133
- DataModel.TableInfoWithName_<DataModel_, TableName>
134
- >
135
- >;
136
-
137
- const decodedDoc = self as TableSchemaWithSystemFields["Type"];
138
-
139
- const encodedDoc = yield* pipe(
140
- decodedDoc,
141
- Schema.encode(tableSchema),
142
- Effect.catchTag("ParseError", (parseError) =>
143
- Effect.gen(function* () {
144
- const formattedParseError =
145
- yield* ParseResult.TreeFormatter.formatError(parseError);
146
-
147
- return yield* new DocumentEncodeError({
148
- tableName,
149
- id: decodedDoc._id,
150
- parseError: formattedParseError,
151
- });
152
- }),
153
- ),
154
- );
155
-
156
- return encodedDoc;
157
- }),
167
+ pipe(
168
+ self,
169
+ getEncoder(tableSchema),
170
+ Effect.catchIf(ParseResult.isParseError, (parseError) =>
171
+ Effect.gen(function* () {
172
+ const formattedParseError =
173
+ yield* ParseResult.TreeFormatter.formatError(parseError);
174
+
175
+ return yield* new DocumentEncodeError({
176
+ tableName,
177
+ id: self._id,
178
+ parseError: formattedParseError,
179
+ });
180
+ }),
181
+ ),
182
+ Effect.map(
183
+ (encodedDoc) =>
184
+ encodedDoc as DataModel.TableInfoWithName_<
185
+ DataModel_,
186
+ TableName
187
+ >["encodedDocument"],
188
+ ),
189
+ ),
158
190
  );
159
191
 
160
192
  export class DocumentDecodeError extends Schema.TaggedError<DocumentDecodeError>()(
@@ -1,11 +1,11 @@
1
1
  import type * as FunctionSpec from "@confect/core/FunctionSpec";
2
- import type * as GroupPath from "@confect/core/GroupPath";
3
2
  import type * as GroupSpec from "@confect/core/GroupSpec";
4
- import { Array, Context, Effect, Layer, Ref, String } from "effect";
3
+ import * as Registry from "@confect/core/Registry";
4
+ import { Context, Effect, Layer, Ref, String } from "effect";
5
5
  import type * as Api from "./Api";
6
+ import { resolveGroupPathUnsafe } from "./GroupPath";
6
7
  import type * as Handler from "./Handler";
7
8
  import { setNestedProperty } from "./internal/utils";
8
- import * as Registry from "./Registry";
9
9
  import * as RegistryItem from "./RegistryItem";
10
10
 
11
11
  export interface FunctionImpl<
@@ -32,34 +32,23 @@ export const FunctionImpl = <
32
32
 
33
33
  export const make = <
34
34
  Api_ extends Api.AnyWithProps,
35
- const GroupPath_ extends GroupPath.All<Api.Groups<Api_>>,
36
- const FunctionName extends FunctionSpec.Name<
37
- GroupSpec.Functions<GroupPath.GroupAt<Api.Groups<Api_>, GroupPath_>>
38
- >,
35
+ Group extends GroupSpec.AnyWithProps,
36
+ const FunctionName extends FunctionSpec.Name<GroupSpec.Functions<Group>>,
39
37
  >(
40
38
  api: Api_,
41
- groupPath: GroupPath_,
39
+ group: Group,
42
40
  functionName: FunctionName,
43
41
  handler: Handler.WithName<
44
42
  Api.Schema<Api_>,
45
- GroupSpec.Functions<GroupPath.GroupAt<Api.Groups<Api_>, GroupPath_>>,
43
+ GroupSpec.Functions<Group>,
46
44
  FunctionName
47
45
  >,
48
- ): Layer.Layer<FunctionImpl<GroupPath_, FunctionName>> => {
49
- const groupPathParts = String.split(groupPath, ".");
50
- const [firstGroupPathPart, ...restGroupPathParts] = groupPathParts;
51
-
52
- const group_: GroupSpec.AnyWithProps = Array.reduce(
53
- restGroupPathParts,
54
- (api as any).spec.groups[firstGroupPathPart as any]!,
55
- (currentGroup: any, groupPathPart: any) =>
56
- currentGroup.groups[groupPathPart],
57
- );
58
-
59
- const functionSpec = group_.functions[functionName]!;
46
+ ): Layer.Layer<FunctionImpl<string, FunctionName>> => {
47
+ const groupPath = resolveGroupPathUnsafe(api.spec, group);
48
+ const functionSpec = group.functions[functionName]!;
60
49
 
61
50
  return Layer.effect(
62
- FunctionImpl<GroupPath_, FunctionName>({
51
+ FunctionImpl<string, FunctionName>({
63
52
  groupPath,
64
53
  functionName,
65
54
  }),
@@ -69,7 +58,7 @@ export const make = <
69
58
  yield* Ref.update(registry, (registryItems) =>
70
59
  setNestedProperty(
71
60
  registryItems,
72
- [...groupPathParts, functionName],
61
+ [...String.split(groupPath, "."), functionName],
73
62
  RegistryItem.make({
74
63
  functionSpec,
75
64
  handler,
@@ -94,19 +83,21 @@ export type ForGroupPathAndFunction<
94
83
  > = FunctionImpl<GroupPath_, FunctionName>;
95
84
 
96
85
  /**
97
- * Get all function implementation services required for a group at a given path.
86
+ * Get all function implementation services required for a group spec.
98
87
  */
99
- export type FromGroupAtPath<
100
- GroupPath_ extends string,
101
- Group extends GroupSpec.AnyWithProps,
102
- > =
103
- GroupPath.GroupAt<Group, GroupPath_> extends infer GroupAtPath extends
104
- GroupSpec.AnyWithProps
105
- ? FunctionSpec.Name<
106
- GroupSpec.Functions<GroupAtPath>
107
- > extends infer FunctionNames extends string
108
- ? FunctionNames extends string
109
- ? FunctionImpl<GroupPath_, FunctionNames>
110
- : never
88
+ export type FromGroupSpec<Group extends GroupSpec.AnyWithProps> =
89
+ FunctionSpec.Name<
90
+ GroupSpec.Functions<Group>
91
+ > extends infer FunctionNames extends string
92
+ ? FunctionNames extends string
93
+ ? FunctionImpl<string, FunctionNames>
111
94
  : never
112
95
  : never;
96
+
97
+ /**
98
+ * @deprecated Use {@link FromGroupSpec} instead.
99
+ */
100
+ export type FromGroupAtPath<
101
+ _GroupPath extends string,
102
+ Group extends GroupSpec.AnyWithProps,
103
+ > = FromGroupSpec<Group>;
package/src/GroupImpl.ts CHANGED
@@ -1,60 +1,196 @@
1
- import type * as GroupPath from "@confect/core/GroupPath";
2
1
  import type * as GroupSpec from "@confect/core/GroupSpec";
3
- import { Context, Layer } from "effect";
2
+ import * as Registry from "@confect/core/Registry";
3
+ import {
4
+ Array,
5
+ Context,
6
+ Effect,
7
+ Layer,
8
+ Option,
9
+ pipe,
10
+ Predicate,
11
+ Record,
12
+ Ref,
13
+ String,
14
+ } from "effect";
4
15
  import type * as Api from "./Api";
5
16
  import type * as FunctionImpl from "./FunctionImpl";
17
+ import { resolveGroupPathUnsafe } from "./GroupPath";
6
18
 
7
- export interface GroupImpl<GroupPath_ extends string> {
19
+ export const TypeId = "@confect/server/GroupImpl";
20
+ export type TypeId = typeof TypeId;
21
+
22
+ export type FinalizationStatus = "Unfinalized" | "Finalized";
23
+
24
+ export interface GroupImpl<
25
+ GroupPath_ extends string,
26
+ FinalizationStatus_ extends FinalizationStatus = "Unfinalized",
27
+ > {
28
+ readonly [TypeId]: TypeId;
8
29
  readonly groupPath: GroupPath_;
30
+ readonly finalizationStatus: FinalizationStatus_;
31
+ /**
32
+ * Names of every function registered into this group's layer scope by
33
+ * `FunctionImpl.make`. Authoritative only when `finalizationStatus` is
34
+ * `"Finalized"`; the `"Unfinalized"` value is set to `[]` at `make`-time
35
+ * since the list is only known once `finalize` snapshots the registry.
36
+ */
37
+ readonly registeredFunctionNames: ReadonlyArray<string>;
9
38
  }
10
39
 
11
- export const GroupImpl = <GroupPath_ extends string>({
40
+ export interface Any extends GroupImpl<string, FinalizationStatus> {}
41
+
42
+ export const isGroupImpl = (u: unknown): u is Any =>
43
+ Predicate.hasProperty(u, TypeId);
44
+
45
+ export interface AnyFinalized extends GroupImpl<string, "Finalized"> {}
46
+ export interface AnyUnfinalized extends GroupImpl<string, "Unfinalized"> {}
47
+
48
+ export const isFinalizedGroupImpl = (u: unknown): u is AnyFinalized =>
49
+ isGroupImpl(u) && u.finalizationStatus === "Finalized";
50
+
51
+ export const isUnfinalizedGroupImpl = (u: unknown): u is AnyUnfinalized =>
52
+ isGroupImpl(u) && u.finalizationStatus === "Unfinalized";
53
+
54
+ /**
55
+ * Build the runtime tag for a `GroupImpl` service. The finalization status is
56
+ * embedded in the tag string so that `Unfinalized` and `Finalized` are distinct
57
+ * services at runtime; consumers of a finalized layer (the server's
58
+ * `RegisteredFunctions.buildForGroup` and the CLI's `implValidation`) retrieve
59
+ * the typed `Finalized` service directly rather than scanning the context.
60
+ */
61
+ export const GroupImpl = <
62
+ GroupPath_ extends string,
63
+ FinalizationStatus_ extends FinalizationStatus,
64
+ >({
12
65
  groupPath,
66
+ finalizationStatus,
13
67
  }: {
14
68
  groupPath: GroupPath_;
69
+ finalizationStatus: FinalizationStatus_;
15
70
  }) =>
16
- Context.GenericTag<GroupImpl<GroupPath_>>(
17
- `@confect/server/GroupImpl/${groupPath}`,
71
+ Context.GenericTag<GroupImpl<GroupPath_, FinalizationStatus_>>(
72
+ `@confect/server/GroupImpl/${finalizationStatus}/${groupPath}`,
18
73
  );
19
74
 
20
75
  export const make = <
21
76
  Api_ extends Api.AnyWithProps,
22
- const GroupPath_ extends GroupPath.All<Api.Groups<Api_>>,
77
+ Group extends GroupSpec.AnyWithProps,
23
78
  >(
24
- _api: Api_,
25
- groupPath: GroupPath_,
79
+ api: Api_,
80
+ group: Group,
26
81
  ): Layer.Layer<
27
- GroupImpl<GroupPath_>,
82
+ GroupImpl<string, "Unfinalized">,
28
83
  never,
29
- | FromGroupWithPath<GroupPath_, Api.Groups<Api_>>
30
- | FunctionImpl.FromGroupAtPath<GroupPath_, Api.Groups<Api_>>
31
- > =>
32
- Layer.succeed(
33
- GroupImpl<GroupPath_>({
84
+ FunctionImpl.FromGroupSpec<Group>
85
+ > => {
86
+ const groupPath = resolveGroupPathUnsafe(api.spec, group);
87
+
88
+ return Layer.succeed(
89
+ GroupImpl<string, "Unfinalized">({
34
90
  groupPath,
91
+ finalizationStatus: "Unfinalized",
35
92
  }),
36
93
  {
94
+ [TypeId]: TypeId,
37
95
  groupPath,
96
+ finalizationStatus: "Unfinalized" as const,
97
+ registeredFunctionNames: [],
38
98
  },
39
99
  ) as Layer.Layer<
40
- GroupImpl<GroupPath_>,
100
+ GroupImpl<string, "Unfinalized">,
41
101
  never,
42
- | FromGroupWithPath<GroupPath_, Api.Groups<Api_>>
43
- | FunctionImpl.FromGroupAtPath<GroupPath_, Api.Groups<Api_>>
102
+ FunctionImpl.FromGroupSpec<Group>
44
103
  >;
104
+ };
45
105
 
46
- export type FromGroups<Groups extends GroupSpec.Any> = Groups extends never
47
- ? never
48
- : Groups extends GroupSpec.AnyWithProps
49
- ? GroupImpl<GroupSpec.Name<Groups>>
50
- : never;
106
+ const isFunctionShaped = (value: unknown): boolean =>
107
+ Predicate.isRecord(value) && "functionSpec" in value;
51
108
 
52
- export type FromGroupWithPath<
53
- GroupPath_ extends string,
54
- Group extends GroupSpec.AnyWithProps,
55
- > =
56
- GroupPath.SubGroupsAt<Group, GroupPath_> extends infer SubGroupPaths
57
- ? SubGroupPaths extends string
58
- ? GroupImpl<SubGroupPaths>
59
- : never
60
- : never;
109
+ /**
110
+ * Walk a `RegistryItems` tree to the entries at `groupPath` and return the
111
+ * names of the function-shaped leaves directly underneath.
112
+ */
113
+ const collectFunctionNamesAtPath = (
114
+ items: Registry.RegistryItems,
115
+ groupPath: string,
116
+ ): ReadonlyArray<string> =>
117
+ pipe(
118
+ String.split(groupPath, "."),
119
+ Array.reduce(Option.some<unknown>(items), (acc, segment) =>
120
+ acc.pipe(
121
+ Option.filter(Predicate.isRecord),
122
+ Option.flatMap((node) =>
123
+ segment in node ? Option.some(node[segment]) : Option.none(),
124
+ ),
125
+ ),
126
+ ),
127
+ Option.filter(Predicate.isRecord),
128
+ Option.map(Record.toEntries),
129
+ Option.map(
130
+ Array.filterMap(([name, value]) =>
131
+ isFunctionShaped(value) ? Option.some(name) : Option.none(),
132
+ ),
133
+ ),
134
+ Option.getOrElse((): ReadonlyArray<string> => []),
135
+ );
136
+
137
+ const findUnfinalizedGroupImpl = <S>(
138
+ context: Context.Context<S>,
139
+ ): Option.Option<AnyUnfinalized> =>
140
+ Array.findFirst(context.unsafeMap.values(), isUnfinalizedGroupImpl);
141
+
142
+ /**
143
+ * Mark a `GroupImpl` layer as fully implemented. The parameter type defaults
144
+ * `RIn = never`, so passing a layer that still requires any `FunctionImpl`
145
+ * service produces a type error at the impl author's site. The codegen
146
+ * boundary requires the resulting `"Finalized"` brand, so omitting this call
147
+ * is also rejected downstream.
148
+ *
149
+ * As a side effect of finalization, the names of every `FunctionImpl` that
150
+ * registered into this group's scope are snapshotted onto the produced
151
+ * service value's `registeredFunctionNames` field, so consumers can verify
152
+ * impl completeness against a `GroupSpec`'s expected functions without
153
+ * having to inspect the `Registry` themselves.
154
+ */
155
+ export const finalize = <GroupPath_ extends string>(
156
+ group: Layer.Layer<GroupImpl<GroupPath_, "Unfinalized">>,
157
+ ): Layer.Layer<GroupImpl<GroupPath_, "Finalized">> =>
158
+ Layer.flatMap(
159
+ group,
160
+ (context): Layer.Layer<GroupImpl<GroupPath_, "Finalized">> =>
161
+ findUnfinalizedGroupImpl(context).pipe(
162
+ Option.match({
163
+ onNone: () =>
164
+ Layer.die(
165
+ new Error(
166
+ "GroupImpl.finalize: no Unfinalized GroupImpl service was found in the layer's context.",
167
+ ),
168
+ ),
169
+ onSome: (unfinalized) => {
170
+ const groupPath = unfinalized.groupPath as GroupPath_;
171
+ return Layer.effect(
172
+ GroupImpl<GroupPath_, "Finalized">({
173
+ groupPath,
174
+ finalizationStatus: "Finalized",
175
+ }),
176
+ Effect.gen(function* () {
177
+ const registry = yield* Registry.Registry;
178
+ const items = yield* Ref.get(registry);
179
+ return {
180
+ [TypeId]: TypeId,
181
+ groupPath,
182
+ finalizationStatus: "Finalized" as const,
183
+ registeredFunctionNames: collectFunctionNamesAtPath(
184
+ items,
185
+ groupPath,
186
+ ),
187
+ };
188
+ }),
189
+ );
190
+ },
191
+ }),
192
+ ),
193
+ );
194
+
195
+ export type FromGroupSpec<Group extends GroupSpec.AnyWithProps> =
196
+ FunctionImpl.FromGroupSpec<Group>;
@@ -0,0 +1,43 @@
1
+ import type * as GroupSpec from "@confect/core/GroupSpec";
2
+ import type * as Spec from "@confect/core/Spec";
3
+ import { Array, Option, pipe, Record } from "effect";
4
+
5
+ const resolveGroupPathInGroup = (
6
+ group: GroupSpec.AnyWithProps,
7
+ target: GroupSpec.AnyWithProps,
8
+ pathSegments: ReadonlyArray<string>,
9
+ ): Option.Option<string> =>
10
+ pipe(
11
+ Record.toEntries(group.groups),
12
+ Array.findFirst(([name, child]) =>
13
+ child === target
14
+ ? Option.some(Array.join([...pathSegments, name], "."))
15
+ : resolveGroupPathInGroup(child, target, [...pathSegments, name]),
16
+ ),
17
+ );
18
+
19
+ const resolveGroupPath = (
20
+ spec: Spec.AnyWithProps,
21
+ target: GroupSpec.AnyWithProps,
22
+ ): Option.Option<string> =>
23
+ pipe(
24
+ Record.toEntries(spec.groups),
25
+ Array.findFirst(([name, group]) =>
26
+ group === target
27
+ ? Option.some(name)
28
+ : resolveGroupPathInGroup(group, target, [name]),
29
+ ),
30
+ );
31
+
32
+ export const resolveGroupPathUnsafe = (
33
+ spec: Spec.AnyWithProps,
34
+ target: GroupSpec.AnyWithProps,
35
+ ): string =>
36
+ resolveGroupPath(spec, target).pipe(
37
+ Option.getOrThrowWith(
38
+ () =>
39
+ new Error(
40
+ "Could not resolve group path for the provided GroupSpec. Ensure the spec is part of the assembled API spec tree.",
41
+ ),
42
+ ),
43
+ );
@@ -98,27 +98,28 @@ export const make = <Api_ extends Api.AnyWithPropsWithRuntime<"Convex">>(
98
98
  Match.exhaustive,
99
99
  );
100
100
 
101
- // Convex's query cache is invalidated by any Date.now() call during handler
102
- // execution. Effect's unsafeFork calls Date.now() when constructing a
103
- // FiberId.Runtime, which trips the cache for every confect-wrapped query. We
104
- // stub Date.now to 0 for the span of the handler; queries are forbidden from
105
- // relying on real time for correctness anyway.
106
- //
107
- // Users who explicitly want the real timestamp can still reach it via Effect's
108
- // Clock service (Clock.currentTimeMillis/Clock.currentTimeNanos). We provide
109
- // a Clock layer whose methods close over the *original* Date.now, so opting in
110
- // to Clock is an opt-in to worse caching—but caching is not broken by default.
101
+ /**
102
+ * Convex's query cache is invalidated by any Date.now() call during handler
103
+ * execution. Effect's unsafeFork calls Date.now() when constructing a
104
+ * FiberId.Runtime, which trips the cache for every confect-wrapped query. We
105
+ * stub Date.now to 0 for the span of the handler; queries are forbidden from
106
+ * relying on real time for correctness anyway.
107
+ *
108
+ * Users who explicitly want the real timestamp can still reach it via Effect's
109
+ * Clock service (Clock.currentTimeMillis/Clock.currentTimeNanos). We provide a
110
+ * Clock whose user-facing Effects call realDateNow (Convex's tracker) directly,
111
+ * making Clock an explicit opt-in to cache invalidation. The unsafe methods
112
+ * used internally by Effect (logging, span events, scheduler) return constants
113
+ * so they never touch the tracker—caching is not broken by default.
114
+ */
111
115
  const unpatchedClock = (realDateNow: () => number): Clock.Clock => {
112
- const bigint1e6 = BigInt(1_000_000);
113
- const unsafeCurrentTimeMillis = () => realDateNow();
114
- const unsafeCurrentTimeNanos = () => BigInt(realDateNow()) * bigint1e6;
115
116
  const defaultClock = Clock.make();
116
117
  return {
117
118
  ...defaultClock,
118
- unsafeCurrentTimeMillis,
119
- unsafeCurrentTimeNanos,
120
- currentTimeMillis: Effect.sync(unsafeCurrentTimeMillis),
121
- currentTimeNanos: Effect.sync(unsafeCurrentTimeNanos),
119
+ unsafeCurrentTimeMillis: () => 0,
120
+ unsafeCurrentTimeNanos: () => 0n,
121
+ currentTimeMillis: Effect.sync(() => realDateNow()),
122
+ currentTimeNanos: Effect.sync(() => BigInt(realDateNow()) * 1_000_000n),
122
123
  };
123
124
  };
124
125