effect-app 4.0.0-beta.3 → 4.0.0-beta.31

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 (73) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/dist/Effect.d.ts.map +1 -1
  3. package/dist/Effect.js +3 -2
  4. package/dist/Operations.d.ts +48 -12
  5. package/dist/Operations.d.ts.map +1 -1
  6. package/dist/Pure.d.ts.map +1 -1
  7. package/dist/Pure.js +11 -11
  8. package/dist/Schema/Class.d.ts.map +1 -1
  9. package/dist/Schema/Class.js +1 -7
  10. package/dist/Schema/brand.d.ts +8 -5
  11. package/dist/Schema/brand.d.ts.map +1 -1
  12. package/dist/Schema/brand.js +1 -1
  13. package/dist/Schema/email.d.ts.map +1 -1
  14. package/dist/Schema/email.js +4 -3
  15. package/dist/Schema/ext.d.ts +26 -25
  16. package/dist/Schema/ext.d.ts.map +1 -1
  17. package/dist/Schema/ext.js +13 -20
  18. package/dist/Schema/moreStrings.d.ts +6 -6
  19. package/dist/Schema/moreStrings.d.ts.map +1 -1
  20. package/dist/Schema/moreStrings.js +6 -4
  21. package/dist/Schema/phoneNumber.d.ts.map +1 -1
  22. package/dist/Schema/phoneNumber.js +3 -2
  23. package/dist/Schema.d.ts +18 -52
  24. package/dist/Schema.d.ts.map +1 -1
  25. package/dist/Schema.js +40 -61
  26. package/dist/ServiceMap.d.ts +3 -3
  27. package/dist/ServiceMap.d.ts.map +1 -1
  28. package/dist/ServiceMap.js +1 -1
  29. package/dist/client/apiClientFactory.d.ts.map +1 -1
  30. package/dist/client/apiClientFactory.js +8 -9
  31. package/dist/client/errors.d.ts.map +1 -1
  32. package/dist/client/errors.js +1 -1
  33. package/dist/client/makeClient.d.ts +8 -4
  34. package/dist/client/makeClient.d.ts.map +1 -1
  35. package/dist/client/makeClient.js +13 -15
  36. package/dist/http/Request.d.ts.map +1 -1
  37. package/dist/http/Request.js +5 -5
  38. package/dist/ids.d.ts +6 -6
  39. package/dist/ids.d.ts.map +1 -1
  40. package/dist/ids.js +1 -1
  41. package/dist/index.d.ts +0 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -2
  44. package/dist/utils.d.ts +18 -0
  45. package/dist/utils.d.ts.map +1 -1
  46. package/dist/utils.js +24 -5
  47. package/package.json +3 -7
  48. package/src/Effect.ts +3 -2
  49. package/src/Pure.ts +12 -13
  50. package/src/Schema/Class.ts +0 -6
  51. package/src/Schema/brand.ts +13 -7
  52. package/src/Schema/email.ts +4 -2
  53. package/src/Schema/ext.ts +46 -38
  54. package/src/Schema/moreStrings.ts +16 -13
  55. package/src/Schema/phoneNumber.ts +3 -1
  56. package/src/Schema.ts +76 -100
  57. package/src/ServiceMap.ts +7 -6
  58. package/src/client/apiClientFactory.ts +12 -15
  59. package/src/client/errors.ts +12 -3
  60. package/src/client/makeClient.ts +25 -20
  61. package/src/http/Request.ts +7 -4
  62. package/src/ids.ts +1 -1
  63. package/src/index.ts +0 -1
  64. package/src/utils.ts +26 -4
  65. package/test/dist/moreStrings.test.d.ts.map +1 -0
  66. package/test/dist/rpc.test.d.ts.map +1 -1
  67. package/test/moreStrings.test.ts +17 -0
  68. package/test/rpc.test.ts +10 -0
  69. package/test/schema.test.ts +178 -1
  70. package/dist/Struct.d.ts +0 -44
  71. package/dist/Struct.d.ts.map +0 -1
  72. package/dist/Struct.js +0 -29
  73. package/src/Struct.ts +0 -54
package/src/Schema.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { Array, Option, pipe, SchemaAST, type Tracer } from "effect"
1
+ import { SchemaAST, type Tracer } from "effect"
2
2
  import * as S from "effect/Schema"
3
3
  import type { NonEmptyReadonlyArray } from "./Array.js"
4
4
  import { fakerArb } from "./faker.js"
5
- import { Email as EmailT } from "./Schema/email.js"
5
+ import { Email as EmailT, type Email as EmailType } from "./Schema/email.js"
6
6
  import { withDefaultMake } from "./Schema/ext.js"
7
- import { PhoneNumber as PhoneNumberT } from "./Schema/phoneNumber.js"
8
- import type { AST } from "./Schema/schema.js"
7
+ import { PhoneNumber as PhoneNumberT, type PhoneNumber as PhoneNumberType } from "./Schema/phoneNumber.js"
9
8
  import { extendM } from "./utils.js"
10
9
 
11
10
  export * from "effect/Schema"
@@ -40,127 +39,104 @@ export interface WithOptionalSpan {
40
39
  [SpanId]?: Tracer.Span
41
40
  }
42
41
 
42
+ const makeEmail = S.decodeSync(EmailT as any) as (value: string) => EmailType
43
+ const makePhoneNumber = S.decodeSync(PhoneNumberT as any) as (value: string) => PhoneNumberType
44
+
43
45
  export const Email = EmailT
44
46
  .pipe(
45
47
  S.annotate({
46
48
  // eslint-disable-next-line @typescript-eslint/unbound-method
47
- arbitrary: (): any => (fc: any) => fakerArb((faker) => faker.internet.exampleEmail)(fc).map(Email)
49
+ toArbitrary: () => (fc) => fakerArb((faker) => faker.internet.exampleEmail)(fc).map(makeEmail)
48
50
  }),
49
51
  withDefaultMake
50
52
  )
51
53
 
52
- export type Email = EmailT
54
+ export type Email = EmailType
53
55
 
54
56
  export const PhoneNumber = PhoneNumberT
55
57
  .pipe(
56
58
  S.annotate({
57
- arbitrary: (): any => (fc: any) =>
59
+ toArbitrary: () => (fc) =>
58
60
  // eslint-disable-next-line @typescript-eslint/unbound-method
59
- fakerArb((faker) => faker.phone.number)(fc).map(PhoneNumber)
61
+ fakerArb((faker) => faker.phone.number)(fc).map(makePhoneNumber)
60
62
  }),
61
63
  withDefaultMake
62
64
  )
63
65
 
64
- export const makeIs = <A extends { _tag: string }, I, R>(
65
- schema: S.Codec<A, I, R>
66
- ) => {
67
- // In v4, transformations are stored as encoding on nodes, not as wrapper nodes.
68
- // Union member ASTs are directly Objects (TypeLiteral equivalent).
69
- if (SchemaAST.isUnion(schema.ast)) {
70
- return schema.ast.types.reduce((acc: any, t: AST.AST) => {
71
- if (!SchemaAST.isObjects(t)) return acc
72
- const tag = Array.findFirst(t.propertySignatures, (_: any) => {
73
- if (_.name === "_tag" && SchemaAST.isLiteral(_.type)) {
74
- return Option.some(_.type)
75
- }
76
- return Option.none()
77
- })
78
- const ast = Option.getOrUndefined(tag)
79
- if (!ast) {
80
- return acc
81
- }
82
- return {
83
- ...acc,
84
- [String((ast as SchemaAST.Literal).literal)]: (x: { _tag: string }) =>
85
- x._tag === (ast as SchemaAST.Literal).literal
86
- }
87
- }, {} as Is<A>)
88
- }
89
- throw new Error("Unsupported")
90
- }
66
+ export type PhoneNumber = PhoneNumberType
91
67
 
92
- export const makeIsAnyOf = <A extends { _tag: string }, I, R>(
93
- schema: S.Codec<A, I, R>
94
- ): IsAny<A> => {
95
- if (SchemaAST.isUnion(schema.ast)) {
96
- return <Keys extends A["_tag"][]>(...keys: Keys) => (a: A): a is ExtractUnion<A, ElemType<Keys>> =>
97
- keys.includes(a._tag)
98
- }
99
- throw new Error("Unsupported")
68
+ // Copied from SchemaAST.collectSentinels (marked @internal in effect).
69
+ // Returns all { key, literal } pairs that can discriminate a union member.
70
+ const getTagFromAST = (schema: S.Top): string => {
71
+ const sentinels = collectSentinelsFromAST(schema.ast)
72
+ const sentinel = sentinels.find((s) => s.key === "_tag")
73
+ if (sentinel !== undefined && typeof sentinel.literal === "string") return sentinel.literal
74
+ throw new Error("No _tag literal found on schema member")
100
75
  }
101
76
 
102
- export type ExtractUnion<A extends { _tag: string }, Tags extends A["_tag"]> = Extract<A, Record<"_tag", Tags>>
103
- export type Is<A extends { _tag: string }> = { [K in A as K["_tag"]]: (a: A) => a is K }
104
- export type ElemType<A> = A extends Array<infer E> ? E : never
105
- export interface IsAny<A extends { _tag: string }> {
106
- <Keys extends A["_tag"][]>(...keys: Keys): (a: A) => a is ExtractUnion<A, ElemType<Keys>>
77
+ function collectSentinelsFromAST(
78
+ ast: SchemaAST.AST
79
+ ): Array<{ key: PropertyKey; literal: SchemaAST.LiteralValue | symbol }> {
80
+ switch (ast._tag) {
81
+ case "Declaration": {
82
+ const s = ast.annotations?.["~sentinels"]
83
+ return Array.isArray(s) ? s : []
84
+ }
85
+ case "Objects":
86
+ return ast.propertySignatures.flatMap(
87
+ (ps): Array<{ key: PropertyKey; literal: SchemaAST.LiteralValue | symbol }> => {
88
+ const type = ps.type
89
+ if (!SchemaAST.isOptional(type)) {
90
+ if (SchemaAST.isLiteral(type)) return [{ key: ps.name, literal: type.literal }]
91
+ if (SchemaAST.isUniqueSymbol(type)) return [{ key: ps.name, literal: type.symbol }]
92
+ }
93
+ return []
94
+ }
95
+ )
96
+ case "Suspend":
97
+ return collectSentinelsFromAST(ast.thunk())
98
+ default:
99
+ return []
100
+ }
107
101
  }
108
102
 
109
- export const taggedUnionMap = <
110
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
- Members extends readonly (S.Top & { fields: { _tag: S.tag<string> } })[]
112
- >(
113
- self: Members
114
- ) =>
115
- self.reduce((acc, key) => {
116
- // TODO: v4 migration — PropertySignatureDeclaration removed, need v4 AST traversal
117
- const ast = key.fields._tag.ast as any
118
- const tag = ((ast.type ?? ast) as SchemaAST.Literal).literal as string // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
119
- ;(acc as any)[tag] = key as any
120
- return acc
121
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
- }, {} as any)
123
-
124
103
  export const tags = <
125
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
- Members extends NonEmptyReadonlyArray<(S.Top & { fields: { _tag: S.tag<string> } })>
104
+ Members extends NonEmptyReadonlyArray<(S.Top & { readonly Type: { readonly _tag: string } })>
127
105
  >(
128
106
  self: Members
129
107
  ) =>
130
- S.Literals(self.map((key) => {
131
- // TODO: v4 migration — PropertySignatureDeclaration removed, need v4 AST traversal
132
- const ast = key.fields._tag.ast as any
133
- const tag = ((ast.type ?? ast) as SchemaAST.Literal).literal
134
- return tag
135
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
- })) as any
137
-
138
- export const ExtendTaggedUnion = <A extends { _tag: string }, I, R>(
139
- schema: S.Codec<A, I, R>
140
- ) =>
141
- extendM(
142
- schema,
143
- (_) => ({
144
- is: S.is(schema as any),
145
- isA: makeIs(_ as any),
146
- isAnyOf: makeIsAnyOf(_ as any) /*, map: taggedUnionMap(a) */
147
- })
148
- )
108
+ S.Literals(
109
+ self.map(getTagFromAST) as {
110
+ [Index in keyof Members]: Members[Index]["Type"]["_tag"]
111
+ }
112
+ ) as S.Literals<
113
+ {
114
+ [Index in keyof Members]: Members[Index]["Type"]["_tag"]
115
+ }
116
+ >
117
+
118
+ type TaggedUnionMembers = NonEmptyReadonlyArray<
119
+ S.Top & { readonly Type: { readonly _tag: string } }
120
+ >
121
+
122
+ type TaggedUnionTags<Members extends TaggedUnionMembers> = S.Literals<
123
+ {
124
+ [Index in keyof Members]: Members[Index]["Type"]["_tag"]
125
+ }
126
+ >
149
127
 
150
- export const TaggedUnion = <
151
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
- Members extends readonly (S.Top & { fields: { _tag: S.tag<any> } })[]
153
- >(...a: Members) =>
154
- pipe(
155
- S.Union(a),
156
- (_) =>
157
- extendM(_, (_) => ({
158
- is: S.is(_ as any),
159
- isA: makeIs(_ as any),
160
- isAnyOf: makeIsAnyOf(_ as any),
161
- tagMap: taggedUnionMap(a),
162
- tags: tags(a as any)
163
- }))
164
- )
128
+ type TaggedUnionWithTags<Members extends TaggedUnionMembers> = S.toTaggedUnion<"_tag", Members> & {
129
+ readonly tags: TaggedUnionTags<Members>
130
+ }
131
+
132
+ const extendTaggedUnionWithTags = <Members extends TaggedUnionMembers>(
133
+ schema: S.Union<Members>
134
+ ): TaggedUnionWithTags<Members> => extendM(schema.pipe(S.toTaggedUnion("_tag")), () => ({ tags: tags(schema.members) }))
165
135
 
166
- export type PhoneNumber = PhoneNumberT
136
+ export const ExtendTaggedUnion = <Members extends TaggedUnionMembers>(
137
+ schema: S.Union<Members>
138
+ ): TaggedUnionWithTags<Members> => extendTaggedUnionWithTags(schema)
139
+
140
+ export const TaggedUnion = <
141
+ Members extends TaggedUnionMembers
142
+ >(...a: Members): TaggedUnionWithTags<Members> => extendTaggedUnionWithTags(S.Union(a))
package/src/ServiceMap.ts CHANGED
@@ -7,13 +7,14 @@
7
7
 
8
8
  import { type Effect, Layer, type Scope, type Types } from "effect"
9
9
  import * as ServiceMap from "effect/ServiceMap"
10
- import { Yieldable } from "./Effect.js"
10
+ import { type Yieldable } from "./Effect.js"
11
11
 
12
12
  export * from "effect/ServiceMap"
13
13
 
14
- export interface Opaque<Self extends object, in out Shape extends object> extends ServiceMap.Key<Self, Self>, Yieldable<Opaque<Self, Shape>, Self, never, Self> {
15
- // temp while sorting out https://github.com/Effect-TS/effect-smol/pull/1534
16
- of(self: Shape): Self
14
+ export interface Opaque<Self extends object, in out Shape extends object>
15
+ extends ServiceMap.Key<Self, Self>, Yieldable<Opaque<Self, Shape>, Self, never, Self>
16
+ {
17
+ of(this: void, self: Shape): Self
17
18
  serviceMap(self: Shape): ServiceMap.ServiceMap<Self>
18
19
  // a version that leverages the Shape -> Self conversion
19
20
  toLayer: <E, R>(
@@ -151,7 +152,7 @@ export const Opaque: {
151
152
  id: Identifier,
152
153
  options?: {
153
154
  readonly make: ((...args: Args) => Effect.Effect<Shape, E, R>) | Effect.Effect<Shape, E, R> | undefined
154
- } | undefined
155
+ }
155
156
  ) =>
156
157
  & OpaqueClass<Self, Identifier, Shape>
157
158
  & ([Types.unassigned] extends [R] ? unknown
@@ -181,7 +182,7 @@ export const Opaque: {
181
182
  const svc = ServiceMap.Service()(id, options) as any
182
183
  return Object.assign(svc, {
183
184
  toLayer: (eff: Effect.Effect<any, any, any>) => {
184
- return Layer.effect(svc as any, eff)
185
+ return Layer.effect(svc, eff)
185
186
  }
186
187
  })
187
188
  }
@@ -50,17 +50,17 @@ export const HttpClientLayer = (config: ApiConfig) =>
50
50
  Effect
51
51
  .gen(function*() {
52
52
  const baseClient = yield* HttpClient.HttpClient
53
- const ctx = yield* RequestName
54
53
  const client = baseClient.pipe(
55
54
  HttpClient.mapRequest(HttpClientRequest.prependUrl(config.url + "/rpc")),
56
55
  HttpClient.mapRequest(
57
56
  HttpClientRequest.setHeaders(config.headers.pipe(Option.getOrElse(() => ({}))))
58
57
  ),
59
- HttpClient.mapRequest((req) =>
60
- flow(
61
- HttpClientRequest.appendUrlParam("action", ctx.requestName),
62
- HttpClientRequest.appendUrl("/" + ctx.moduleName)
63
- )(req)
58
+ HttpClient.mapRequestEffect((req) =>
59
+ Effect.map(RequestName.asEffect(), (ctx) =>
60
+ flow(
61
+ HttpClientRequest.appendUrlParam("action", ctx.requestName),
62
+ HttpClientRequest.appendUrl("/" + ctx.moduleName)
63
+ )(req))
64
64
  )
65
65
  )
66
66
  return client
@@ -76,7 +76,7 @@ export const HttpClientFromConfigLayer = Layer.unwrap(
76
76
 
77
77
  export const RpcSerializationLayer = (config: ApiConfig) =>
78
78
  Layer.mergeAll(
79
- RpcSerialization.layerJson,
79
+ RpcSerialization.layerNdjson,
80
80
  HttpClientLayer(config)
81
81
  )
82
82
 
@@ -91,7 +91,7 @@ const getFiltered = <M extends Requests>(resource: M) => {
91
91
  // TODO: Record.filter
92
92
  const filtered = typedKeysOf(resource).reduce((acc, cur) => {
93
93
  if (
94
- Predicate.isObject(resource[cur])
94
+ Predicate.isObjectKeyword(resource[cur])
95
95
  && (resource[cur].success)
96
96
  ) {
97
97
  acc[cur as keyof Filtered] = resource[cur] as any
@@ -146,10 +146,7 @@ const makeRpcTag = <M extends Requests>(resource: M) => {
146
146
  // Use Layer.effect directly (not TheClient.toLayer) so TypeScript properly excludes Scope
147
147
  const layer = Layer.effect(
148
148
  TheClient,
149
- Effect.map(
150
- RpcClient.make(rpcs, { spanPrefix: "RpcClient." + meta.moduleName }),
151
- (cl) => (cl as any)[meta.moduleName]
152
- )
149
+ RpcClient.make(rpcs, { spanPrefix: "RpcClient." + meta.moduleName })
153
150
  )
154
151
  return Object.assign(TheClient, { layer })
155
152
  }
@@ -188,7 +185,7 @@ const makeApiClientFactory = Effect
188
185
  const filtered = getFiltered(resource)
189
186
  return {
190
187
  mr,
191
- client: (typedKeysOf(filtered)
188
+ client: typedKeysOf(filtered)
192
189
  .reduce((prev, cur) => {
193
190
  const h = filtered[cur]!
194
191
 
@@ -211,7 +208,7 @@ const makeApiClientFactory = Effect
211
208
  const layers = requestLevelLayers.pipe(Layer.provideMerge(requestNameLayer))
212
209
 
213
210
  const fields = Struct.omit(Request.fields, ["_tag"] as const)
214
- const requestAttr = h._tag
211
+ const requestAttr = `${meta.moduleName}.${h._tag}`
215
212
  // @ts-expect-error doc
216
213
  prev[cur] = Object.keys(fields).length === 0
217
214
  ? {
@@ -246,7 +243,7 @@ const makeApiClientFactory = Effect
246
243
  }
247
244
 
248
245
  return prev
249
- }, {} as Client<M, M["meta"]["moduleName"]>))
246
+ }, {} as Client<M, M["meta"]["moduleName"]>)
250
247
  }
251
248
  })
252
249
 
@@ -43,7 +43,10 @@ export class InvalidStateError extends TaggedError<InvalidStateError>()("Invalid
43
43
  message: S.String
44
44
  }) {
45
45
  constructor(messageOrObject: string | { message: string; cause?: unknown }, disableValidation?: boolean) {
46
- super(typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject } as any, disableValidation as any)
46
+ super(
47
+ typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject } as any,
48
+ disableValidation as any
49
+ )
47
50
  }
48
51
  }
49
52
 
@@ -51,7 +54,10 @@ export class ServiceUnavailableError extends TaggedError<ServiceUnavailableError
51
54
  message: S.String
52
55
  }) {
53
56
  constructor(messageOrObject: string | { message: string; cause?: unknown }, disableValidation?: boolean) {
54
- super(typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject } as any, disableValidation as any)
57
+ super(
58
+ typeof messageOrObject === "object" ? messageOrObject : { message: messageOrObject } as any,
59
+ disableValidation as any
60
+ )
55
61
  }
56
62
  }
57
63
 
@@ -116,7 +122,10 @@ export class OptimisticConcurrencyException extends TaggedError<OptimisticConcur
116
122
  | ({ message: string; cause?: unknown; raw?: unknown }),
117
123
  disableValidation?: boolean
118
124
  ) {
119
- super("message" in args ? args : { message: `Existing ${args.type} ${args.id} record changed` } as any, disableValidation as any)
125
+ super(
126
+ "message" in args ? args : { message: `Existing ${args.type} ${args.id} record changed` } as any,
127
+ disableValidation as any
128
+ )
120
129
  if (!("message" in args)) {
121
130
  this.details = args
122
131
  }
@@ -1,14 +1,32 @@
1
- import { GetEffectError, type GetContextConfig, type RequestContextMapTagAny } from "../rpc/RpcContextMap.js"
1
+ import { SchemaGetter, SchemaTransformation } from "effect"
2
+ import { Link } from "effect/SchemaAST"
3
+ import { Transformation } from "effect/SchemaTransformation"
4
+ import { type GetContextConfig, type GetEffectError, type RequestContextMapTagAny } from "../rpc/RpcContextMap.js"
2
5
  import * as S from "../Schema.js"
3
6
  import { AST } from "../Schema.js"
4
7
 
5
8
  const merge = (a: any, b: Array<any>) =>
6
9
  a !== undefined && b.length ? S.Union([a, ...b]) : a !== undefined ? a : b.length ? S.Union(b) : S.Never
7
10
 
11
+ const undefinedToNull = new Link(
12
+ S.AST.null,
13
+ new Transformation(
14
+ SchemaGetter.transform(() => undefined),
15
+ SchemaGetter.transform(() => null)
16
+ )
17
+ )
18
+
8
19
  /**
9
20
  * Whatever the input, we will only decode or encode to void
10
21
  */
11
- const ForceVoid: S.Codec<void> = S.Void as any
22
+ export const ForceVoid = S
23
+ .Any
24
+ .pipe(
25
+ S.decodeTo(S.Any, SchemaTransformation.transform({ decode: () => void 0 as void, encode: () => void 0 }))
26
+ )
27
+ .annotate({
28
+ toCodecJson: () => undefinedToNull
29
+ })
12
30
 
13
31
  type SchemaOrFields<T> = T extends S.Top ? T : T extends S.Struct.Fields ? S.Struct<T> : S.Void
14
32
 
@@ -62,16 +80,16 @@ export const makeRpcClient = <
62
80
  tag: Tag,
63
81
  fields: Payload,
64
82
  config: RequestConfig & C
65
- ): TaggedRequestResult<Tag, Payload, S.Codec<void>, ErrorResult<C>, Omit<C, "success" | "error">>
83
+ ): TaggedRequestResult<Tag, Payload, typeof ForceVoid, ErrorResult<C>, Omit<C, "success" | "error">>
66
84
  <Tag extends string, Payload extends S.Struct.Fields, C extends Record<string, any>>(
67
85
  tag: Tag,
68
86
  fields: Payload,
69
87
  config: C & RequestConfig
70
- ): TaggedRequestResult<Tag, Payload, S.Codec<void>, ErrorResult<C>, Omit<C, "success" | "error">>
88
+ ): TaggedRequestResult<Tag, Payload, typeof ForceVoid, ErrorResult<C>, Omit<C, "success" | "error">>
71
89
  <Tag extends string, Payload extends S.Struct.Fields>(
72
90
  tag: Tag,
73
91
  fields: Payload
74
- ): TaggedRequestResult<Tag, Payload, S.Codec<void>, ErrorResult<never>, Record<string, never>>
92
+ ): TaggedRequestResult<Tag, Payload, typeof ForceVoid, ErrorResult<{}>, Record<string, never>>
75
93
  } {
76
94
  // TODO: filter errors based on config + take care of inversion
77
95
  const errorSchemas = Object.values(rcs.config).map((_) => _.error)
@@ -92,22 +110,9 @@ export const makeRpcClient = <
92
110
  : S.Struct(config.success)
93
111
  : ForceVoid
94
112
 
95
- const payloadSchema = S.Struct({ _tag: S.tag(tag), ...fields })
96
-
97
- const taggedFields = { _tag: S.tag(tag), ...fields }
98
-
99
- const RequestClass = class {
100
- constructor(payload?: any) {
101
- if (payload) {
102
- Object.assign(this, payload)
103
- }
104
- ;(this as any)._tag = tag
105
- }
106
- }
107
-
108
- Object.assign(RequestClass, payloadSchema, {
113
+ const RequestClass = S.TaggedClass<any>()(tag, fields)
114
+ Object.assign(RequestClass, {
109
115
  _tag: tag,
110
- fields: taggedFields,
111
116
  success: successSchema,
112
117
  error: failureSchema,
113
118
  config
@@ -1,3 +1,4 @@
1
+ import { Option } from "effect"
1
2
  import type { HttpClientResponse } from "effect/unstable/http/HttpClientResponse"
2
3
  import * as Effect from "../Effect.js"
3
4
  import { HttpClient, HttpClientError, HttpClientRequest, HttpHeaders } from "./internal/lib.js"
@@ -24,16 +25,18 @@ export const demandJson = (client: HttpClient.HttpClient) =>
24
25
  .mapRequest(client, (_) => HttpClientRequest.acceptJson(_))
25
26
  .pipe(HttpClient.transform((r, request) =>
26
27
  Effect.tap(r, (response) =>
27
- HttpHeaders
28
- .get(response.headers, "Content-Type")
29
- ?.startsWith("application/json")
28
+ Option
29
+ .exists(
30
+ HttpHeaders.get(response.headers, "Content-Type"),
31
+ (_) => _.startsWith("application/json")
32
+ )
30
33
  ? Effect.void
31
34
  : Effect.fail(
32
35
  new HttpClientError.DecodeError({
33
36
  request,
34
37
  response,
35
38
  description: "not json response: "
36
- + HttpHeaders.get(response.headers, "Content-Type")
39
+ + Option.getOrElse(HttpHeaders.get(response.headers, "Content-Type"), () => "<missing>")
37
40
  })
38
41
  ))
39
42
  ))
package/src/ids.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { brandedStringId, NonEmptyString255, StringId, type StringIdBrand, withDefaultMake, Codec } from "effect-app/Schema"
1
+ import { brandedStringId, type Codec, NonEmptyString255, StringId, type StringIdBrand, withDefaultMake } from "effect-app/Schema"
2
2
  import type { B } from "effect-app/Schema/schema"
3
3
  import type { Simplify } from "effect/Types"
4
4
  import { S } from "./index.js"
package/src/index.ts CHANGED
@@ -23,7 +23,6 @@ export { type NonEmptyArray, type NonEmptyReadonlyArray } from "./Array.js"
23
23
 
24
24
  export * from "effect"
25
25
 
26
- export * as Struct from "./Struct.js"
27
26
  export type * as Types from "./Types.js"
28
27
 
29
28
  export * as SecretURL from "./Config/SecretURL.js"
package/src/utils.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-function-type */
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  /* eslint-disable @typescript-eslint/no-redundant-type-constituents */
4
- import { Effect, Exit, Fiber, Option, Record } from "effect"
4
+ import { Cause, Effect, Exit, Fiber, Option, Record } from "effect"
5
5
  import { dual } from "effect/Function"
6
- import { isFunction } from "effect/Predicate"
6
+ import { isFunction, isObject } from "effect/Predicate"
7
7
  import * as Result from "effect/Result"
8
8
  import type { GetFieldType, NumericDictionary, PropertyPath } from "lodash"
9
9
  import { identity, pipe } from "./Function.js"
@@ -924,8 +924,8 @@ export const runtimeFiberAsPromise = <A, E>(fiber: Fiber.Fiber<A, E>, signal?: A
924
924
  if (Exit.isSuccess(exit)) {
925
925
  resolve(exit.value)
926
926
  } else {
927
- // errors really should be of type Error, so we wrap in FiberFailure just as default Effect
928
- reject(exit.cause)
927
+ // eslint-disable-next-line
928
+ reject(Cause.squash(exit.cause))
929
929
  }
930
930
  })
931
931
  )
@@ -950,3 +950,25 @@ export type UnionToTuples<T, U = T> = [T] extends [never] ? []
950
950
  | [T, ...UnionToTuples<Exclude<U, T>>]
951
951
  | UnionToTuples<Exclude<U, T>>
952
952
  : []
953
+
954
+ const genConstructor = (function*() {}).constructor
955
+
956
+ /**
957
+ * @example
958
+ * ```ts
959
+ * import { Utils } from "effect"
960
+ *
961
+ * function* generatorFn() {
962
+ * yield 1
963
+ * yield 2
964
+ * }
965
+ *
966
+ * console.log(Utils.isGeneratorFunction(generatorFn)) // true
967
+ * console.log(Utils.isGeneratorFunction(() => {})) // false
968
+ * ```
969
+ *
970
+ * @category predicates
971
+ * @since 3.11.0
972
+ */
973
+ export const isGeneratorFunction = (u: unknown): u is (...args: Array<any>) => Generator<any, any, any> =>
974
+ isObject(u) && u.constructor === genConstructor
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moreStrings.test.d.ts","sourceRoot":"","sources":["../moreStrings.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"rpc.test.d.ts","sourceRoot":"","sources":["../rpc.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACrF,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAA;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;AAE7C,qBAAa,iBAAkB,SAAQ,sBAIrC;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIL,qBAAa,KAAM,SAAQ,UAQzB;CAAG"}
1
+ {"version":3,"file":"rpc.test.d.ts","sourceRoot":"","sources":["../rpc.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAErF,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAA;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;AAE7C,qBAAa,iBAAkB,SAAQ,sBAIrC;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIL,qBAAa,KAAM,SAAQ,UAQzB;CAAG"}
@@ -0,0 +1,17 @@
1
+ import { S } from "effect-app"
2
+ import * as fc from "fast-check"
3
+ import { urlAlphabet } from "nanoid"
4
+ import { test } from "vitest"
5
+
6
+ const nanoidAlphabet = new Set(urlAlphabet)
7
+
8
+ const isNanoId = (value: string) => value.length === 21 && Array.from(value).every((char) => nanoidAlphabet.has(char))
9
+
10
+ test("StringId arbitrary generates nanoid-shaped values", () => {
11
+ fc.assert(
12
+ fc.property(S.toArbitrary(S.StringId), (value) => {
13
+ expect(isNanoId(value)).toBe(true)
14
+ expect(S.is(S.StringId)(value)).toBe(true)
15
+ })
16
+ )
17
+ })
package/test/rpc.test.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { expect, test } from "vitest"
1
2
  import { makeRpcClient, NotLoggedInError, UnauthorizedError } from "../src/client.js"
3
+ import { ForceVoid } from "../src/client/makeClient.js"
2
4
  import { S } from "../src/index.js"
3
5
  import { RpcContextMap } from "../src/rpc.js"
4
6
 
@@ -21,3 +23,11 @@ export class Stats extends TaggedRequest<Stats>()("Stats", {}, {
21
23
  }) {}
22
24
 
23
25
  declare const _stats: typeof Stats.success.Type
26
+
27
+ test("ForceVoid decodes and encodes as void", () => {
28
+ expect(S.decodeUnknownSync(ForceVoid)(undefined)).toBe(undefined)
29
+ expect(S.is(ForceVoid)(undefined)).toBe(true)
30
+ expect(S.decodeUnknownSync(ForceVoid)("test")).toBe(undefined)
31
+ expect(S.is(ForceVoid)("test")).toBe(true)
32
+ expect(S.encodeUnknownSync(ForceVoid)("test")).toBe(undefined)
33
+ })