effect 4.0.0-beta.59 → 4.0.0-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Duration.ts CHANGED
@@ -87,6 +87,9 @@ export type Unit =
87
87
  /**
88
88
  * Valid input types that can be converted to a Duration.
89
89
  *
90
+ * String inputs accept values like `"10 seconds"`, `"500 millis"`,
91
+ * `"Infinity"`, and `"-Infinity"`.
92
+ *
90
93
  * @since 2.0.0
91
94
  * @category models
92
95
  */
@@ -96,6 +99,8 @@ export type Input =
96
99
  | bigint // nanos
97
100
  | readonly [seconds: number, nanos: number]
98
101
  | `${number} ${Unit}`
102
+ | "Infinity"
103
+ | "-Infinity"
99
104
  | DurationObject
100
105
 
101
106
  /**
@@ -140,7 +145,8 @@ const DURATION_REGEXP = /^(-?\d+(?:\.\d+)?)\s+(nanos?|micros?|millis?|seconds?|m
140
145
  *
141
146
  * const duration1 = Duration.fromInputUnsafe(1000) // 1000 milliseconds
142
147
  * const duration2 = Duration.fromInputUnsafe("5 seconds")
143
- * const duration3 = Duration.fromInputUnsafe([2, 500_000_000]) // 2 seconds and 500ms
148
+ * const duration3 = Duration.fromInputUnsafe("Infinity")
149
+ * const duration4 = Duration.fromInputUnsafe([2, 500_000_000]) // 2 seconds and 500ms
144
150
  * ```
145
151
  *
146
152
  * @since 2.0.0
@@ -153,6 +159,12 @@ export const fromInputUnsafe = (input: Input): Duration => {
153
159
  case "bigint":
154
160
  return nanos(input)
155
161
  case "string": {
162
+ if (input === "Infinity") {
163
+ return infinity
164
+ }
165
+ if (input === "-Infinity") {
166
+ return negativeInfinity
167
+ }
156
168
  const match = DURATION_REGEXP.exec(input)
157
169
  if (!match) break
158
170
  const [_, valueStr, unit] = match
@@ -666,8 +678,8 @@ export const weeks = (weeks: number): Duration => make(weeks * 604_800_000)
666
678
  * @since 2.0.0
667
679
  * @category getters
668
680
  */
669
- export const toMillis = (self: Duration): number =>
670
- match(self, {
681
+ export const toMillis = (self: Input): number =>
682
+ match(fromInputUnsafe(self), {
671
683
  onMillis: identity,
672
684
  onNanos: (nanos) => Number(nanos) / 1_000_000,
673
685
  onInfinity: () => Infinity,
@@ -688,8 +700,8 @@ export const toMillis = (self: Duration): number =>
688
700
  * @since 2.0.0
689
701
  * @category getters
690
702
  */
691
- export const toSeconds = (self: Duration): number =>
692
- match(self, {
703
+ export const toSeconds = (self: Input): number =>
704
+ match(fromInputUnsafe(self), {
693
705
  onMillis: (millis) => millis / 1_000,
694
706
  onNanos: (nanos) => Number(nanos) / 1_000_000_000,
695
707
  onInfinity: () => Infinity,
@@ -710,8 +722,8 @@ export const toSeconds = (self: Duration): number =>
710
722
  * @since 3.8.0
711
723
  * @category getters
712
724
  */
713
- export const toMinutes = (self: Duration): number =>
714
- match(self, {
725
+ export const toMinutes = (self: Input): number =>
726
+ match(fromInputUnsafe(self), {
715
727
  onMillis: (millis) => millis / 60_000,
716
728
  onNanos: (nanos) => Number(nanos) / 60_000_000_000,
717
729
  onInfinity: () => Infinity,
@@ -732,8 +744,8 @@ export const toMinutes = (self: Duration): number =>
732
744
  * @since 3.8.0
733
745
  * @category getters
734
746
  */
735
- export const toHours = (self: Duration): number =>
736
- match(self, {
747
+ export const toHours = (self: Input): number =>
748
+ match(fromInputUnsafe(self), {
737
749
  onMillis: (millis) => millis / 3_600_000,
738
750
  onNanos: (nanos) => Number(nanos) / 3_600_000_000_000,
739
751
  onInfinity: () => Infinity,
@@ -754,8 +766,8 @@ export const toHours = (self: Duration): number =>
754
766
  * @since 3.8.0
755
767
  * @category getters
756
768
  */
757
- export const toDays = (self: Duration): number =>
758
- match(self, {
769
+ export const toDays = (self: Input): number =>
770
+ match(fromInputUnsafe(self), {
759
771
  onMillis: (millis) => millis / 86_400_000,
760
772
  onNanos: (nanos) => Number(nanos) / 86_400_000_000_000,
761
773
  onInfinity: () => Infinity,
@@ -776,8 +788,8 @@ export const toDays = (self: Duration): number =>
776
788
  * @since 3.8.0
777
789
  * @category getters
778
790
  */
779
- export const toWeeks = (self: Duration): number =>
780
- match(self, {
791
+ export const toWeeks = (self: Input): number =>
792
+ match(fromInputUnsafe(self), {
781
793
  onMillis: (millis) => millis / 604_800_000,
782
794
  onNanos: (nanos) => Number(nanos) / 604_800_000_000_000,
783
795
  onInfinity: () => Infinity,
@@ -808,7 +820,8 @@ export const toWeeks = (self: Duration): number =>
808
820
  * @since 2.0.0
809
821
  * @category getters
810
822
  */
811
- export const toNanosUnsafe = (self: Duration): bigint => {
823
+ export const toNanosUnsafe = (input: Input): bigint => {
824
+ const self = fromInputUnsafe(input)
812
825
  switch (self.value._tag) {
813
826
  case "Infinity":
814
827
  case "NegativeInfinity":
@@ -839,7 +852,7 @@ export const toNanosUnsafe = (self: Duration): bigint => {
839
852
  * @category getters
840
853
  * @since 4.0.0
841
854
  */
842
- export const toNanos: (self: Duration) => Option.Option<bigint> = Option.liftThrowable(toNanosUnsafe)
855
+ export const toNanos: (self: Input) => Option.Option<bigint> = Option.liftThrowable(toNanosUnsafe)
843
856
 
844
857
  /**
845
858
  * Converts a Duration to high-resolution time format [seconds, nanoseconds].
@@ -856,7 +869,8 @@ export const toNanos: (self: Duration) => Option.Option<bigint> = Option.liftThr
856
869
  * @since 2.0.0
857
870
  * @category getters
858
871
  */
859
- export const toHrTime = (self: Duration): [seconds: number, nanos: number] => {
872
+ export const toHrTime = (input: Input): [seconds: number, nanos: number] => {
873
+ const self = fromInputUnsafe(input)
860
874
  switch (self.value._tag) {
861
875
  case "Infinity":
862
876
  return [Infinity, 0]
package/src/Formatter.ts CHANGED
@@ -362,8 +362,8 @@ function safeToString(input: any): string {
362
362
  *
363
363
  * Behavior:
364
364
  * - Does not mutate input.
365
- * - Uses `JSON.stringify` internally with a replacer that tracks seen
366
- * objects.
365
+ * - Uses `JSON.stringify` internally with a replacer that tracks the
366
+ * current object ancestry.
367
367
  * - Circular references are replaced with `undefined` (omitted from
368
368
  * output).
369
369
  * - `Redactable` values are automatically redacted before serialization.
@@ -414,17 +414,23 @@ function safeToString(input: any): string {
414
414
  export function formatJson(input: unknown, options?: {
415
415
  readonly space?: number | string | undefined
416
416
  }): string {
417
- let cache: Array<unknown> = []
418
- const out = JSON.stringify(
417
+ const ancestors: Array<object> = []
418
+ return JSON.stringify(
419
419
  input,
420
- (_key, value) =>
421
- typeof value === "object" && value !== null
422
- ? cache.includes(value)
423
- ? undefined // circular reference
424
- : cache.push(value) && redact(value)
425
- : value,
420
+ function(this: unknown, _key: string, value: unknown) {
421
+ const redacted = redact(value)
422
+ if (typeof redacted !== "object" || redacted === null) {
423
+ return redacted
424
+ }
425
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
426
+ ancestors.pop()
427
+ }
428
+ if (ancestors.includes(redacted)) {
429
+ return undefined // circular reference
430
+ }
431
+ ancestors.push(redacted)
432
+ return redacted
433
+ },
426
434
  options?.space
427
435
  )
428
- ;(cache as any) = undefined
429
- return out
430
436
  }
@@ -37,9 +37,8 @@
37
37
  *
38
38
  * @since 2.0.0
39
39
  */
40
- import { format } from "./Formatter.ts"
40
+ import { format, formatJson } from "./Formatter.ts"
41
41
  import * as Predicate from "./Predicate.ts"
42
- import * as Redactable from "./Redactable.ts"
43
42
  import { redact } from "./Redactable.ts"
44
43
 
45
44
  /**
@@ -176,31 +175,12 @@ export const toStringUnknown = (u: unknown, whitespace: number | string | undefi
176
175
  return u
177
176
  }
178
177
  try {
179
- return typeof u === "object" ? stringifyCircular(u, whitespace) : String(u)
178
+ return typeof u === "object" ? formatJson(u, { space: whitespace }) : String(u)
180
179
  } catch {
181
180
  return String(u)
182
181
  }
183
182
  }
184
183
 
185
- /**
186
- * @since 2.0.0
187
- */
188
- export const stringifyCircular = (obj: unknown, whitespace?: number | string | undefined): string => {
189
- let cache: Array<unknown> = []
190
- const retVal = JSON.stringify(
191
- obj,
192
- (_key, value) =>
193
- typeof value === "object" && value !== null
194
- ? cache.includes(value)
195
- ? undefined // circular reference
196
- : cache.push(value) && Redactable.redact(value)
197
- : value,
198
- whitespace
199
- )
200
- ;(cache as any) = undefined
201
- return retVal
202
- }
203
-
204
184
  /**
205
185
  * A base prototype object that implements the {@link Inspectable} interface.
206
186
  *
package/src/Schema.ts CHANGED
@@ -8862,6 +8862,35 @@ export const Duration: Duration = declare(
8862
8862
  }
8863
8863
  )
8864
8864
 
8865
+ const DurationString = String.annotate({ expected: "a string that will be decoded as a Duration" })
8866
+
8867
+ /**
8868
+ * Companion type for {@link DurationFromString}.
8869
+ *
8870
+ * @category Duration
8871
+ * @since 4.0.0
8872
+ */
8873
+ export interface DurationFromString extends decodeTo<Duration, String> {
8874
+ readonly "Rebuild": DurationFromString
8875
+ }
8876
+
8877
+ /**
8878
+ * A transformation schema that parses a string into a `Duration`.
8879
+ *
8880
+ * Decoding:
8881
+ * - A `string` is decoded as a `Duration`, accepting any format that
8882
+ * `Duration.fromInput` can parse.
8883
+ *
8884
+ * Encoding:
8885
+ * - A `Duration` is encoded as a parseable `string`.
8886
+ *
8887
+ * @category Duration
8888
+ * @since 4.0.0
8889
+ */
8890
+ export const DurationFromString: DurationFromString = DurationString.pipe(
8891
+ decodeTo(Duration, Transformation.durationFromString)
8892
+ )
8893
+
8865
8894
  /**
8866
8895
  * Companion type for {@link DurationFromNanos}.
8867
8896
  *
@@ -35,7 +35,7 @@
35
35
  * - Trim/case strings → {@link trim}, {@link toLowerCase}, {@link toUpperCase}, {@link capitalize}, {@link uncapitalize}, {@link snakeToCamel}
36
36
  * - Parse key-value strings → {@link splitKeyValue}
37
37
  * - Coerce string ↔ number/bigint → {@link numberFromString}, {@link bigintFromString}
38
- * - Coerce string ↔ Date → {@link dateFromString}
38
+ * - Coerce string ↔ Date/Duration → {@link dateFromString}, {@link durationFromString}
39
39
  * - Decode durations → {@link durationFromNanos}, {@link durationFromMillis}
40
40
  * - Wrap nullable/optional as Option → {@link optionFromNullOr}, {@link optionFromOptionalKey}, {@link optionFromOptional}
41
41
  * - Parse URLs → {@link urlFromString}
@@ -915,6 +915,47 @@ export const dateFromString: Transformation<globalThis.Date, string> = new Trans
915
915
  Getter.transform(formatDate)
916
916
  )
917
917
 
918
+ /**
919
+ * Decodes a `string` into a `Duration` and encodes a `Duration` back to a
920
+ * parseable `string`.
921
+ *
922
+ * When to use this:
923
+ * - Parsing human-readable duration strings from APIs, config, or user input.
924
+ *
925
+ * Behavior:
926
+ * - Decode: accepts any string that `Duration.fromInput` can parse, including
927
+ * `"Infinity"` and `"-Infinity"`.
928
+ * - Encode: returns `String(duration)`, producing strings like `"2000 millis"`
929
+ * or `"10 nanos"` that round-trip through the parser.
930
+ *
931
+ * **Example** (Duration from string)
932
+ *
933
+ * ```ts
934
+ * import { Schema, SchemaTransformation } from "effect"
935
+ *
936
+ * const schema = Schema.String.pipe(
937
+ * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromString)
938
+ * )
939
+ * ```
940
+ *
941
+ * See also:
942
+ * - {@link durationFromNanos}
943
+ * - {@link durationFromMillis}
944
+ *
945
+ * @since 4.0.0
946
+ */
947
+ export const durationFromString: Transformation<Duration.Duration, string> = transformOrFail<
948
+ Duration.Duration,
949
+ string
950
+ >({
951
+ decode: (s) =>
952
+ Option.match(Duration.fromInput(s as Duration.Input), {
953
+ onNone: () => Effect.fail(new Issue.InvalidValue(Option.some(s), { message: `Invalid Duration string: ${s}` })),
954
+ onSome: Effect.succeed
955
+ }),
956
+ encode: (duration) => Effect.succeed(globalThis.String(duration))
957
+ })
958
+
918
959
  /**
919
960
  * Decodes a `bigint` (nanoseconds) into a `Duration` and encodes a
920
961
  * `Duration` back to `bigint` nanoseconds.
@@ -1250,7 +1291,7 @@ export const urlFromString: Transformation<URL, string> = transformOrFail<URL, s
1250
1291
  decode: (s) =>
1251
1292
  Effect.try({
1252
1293
  try: () => new URL(s),
1253
- catch: (e) => new Issue.InvalidValue(Option.some(s), { message: globalThis.String(e) })
1294
+ catch: () => new Issue.InvalidValue(Option.some(s), { message: `Invalid URL string: ${s}` })
1254
1295
  }),
1255
1296
  encode: (url) => Effect.succeed(url.href)
1256
1297
  })
@@ -1592,7 +1633,8 @@ export const dateTimeUtcFromString: Transformation<DateTime.Utc, string> = trans
1592
1633
  >({
1593
1634
  decode: (s) => {
1594
1635
  return Option.match(DateTime.make(s), {
1595
- onNone: () => Effect.fail(new Issue.InvalidValue(Option.some(s), { message: "Invalid DateTime input" })),
1636
+ onNone: () =>
1637
+ Effect.fail(new Issue.InvalidValue(Option.some(s), { message: `Invalid UTC DateTime string: ${s}` })),
1596
1638
  onSome: (result) => Effect.succeed(DateTime.toUtc(result))
1597
1639
  })
1598
1640
  },
@@ -1609,7 +1651,7 @@ export const dateTimeZonedFromString: Transformation<DateTime.Zoned, string> = t
1609
1651
  decode: (s) => {
1610
1652
  return Option.match(DateTime.makeZonedFromString(s), {
1611
1653
  onNone: () =>
1612
- Effect.fail(new Issue.InvalidValue(Option.some(s), { message: `Invalid zoned DateTime string: ${s}` })),
1654
+ Effect.fail(new Issue.InvalidValue(Option.some(s), { message: `Invalid Zoned DateTime string: ${s}` })),
1613
1655
  onSome: Effect.succeed
1614
1656
  })
1615
1657
  },
@@ -785,6 +785,141 @@ export const make = <
785
785
  }) as any
786
786
  }
787
787
 
788
+ /**
789
+ * Create a custom Rpc constructor, that can transform the output schemas.
790
+ *
791
+ * ```typescript
792
+ * import { Schema } from "effect"
793
+ * import { Rpc } from "effect/unstable/rpc"
794
+ *
795
+ * // Create a custom Rpc wrapper definition by transforming the success and error
796
+ * // schemas.
797
+ * export interface RpcWithPagination extends Rpc.Custom {
798
+ * readonly out: Rpc.Custom.Out<
799
+ * Paginated<this["success"]>,
800
+ * this["error"]
801
+ * >
802
+ * }
803
+ *
804
+ * // The type definition for the transformed success schema.
805
+ * export interface Paginated<S extends Schema.Top> extends
806
+ * Schema.Struct<{
807
+ * readonly offset: Schema.Number
808
+ * readonly total: Schema.Number
809
+ * readonly results: Schema.$Array<S>
810
+ * }>
811
+ * {}
812
+ *
813
+ * // You can then implement the schema transformation using `Rpc.custom`
814
+ * export const makePaginated = Rpc.custom<RpcWithPagination>((schemas) => ({
815
+ * ...schemas,
816
+ * success: Schema.Struct({
817
+ * offset: Schema.Number,
818
+ * total: Schema.Number,
819
+ * results: Schema.Array(schemas.success)
820
+ * })
821
+ * }))
822
+ *
823
+ * // You can then use the custom constructor in the same way `Rpc.make` is used.
824
+ * export const listAllRpc = makePaginated("listAll", {
825
+ * success: Schema.String
826
+ * })
827
+ * ```
828
+ *
829
+ * @since 4.0.0
830
+ * @category Custom constructors
831
+ */
832
+ export const custom = <Def extends Custom>(
833
+ f: (options: Custom.OutDefault) => (Def & Custom.OutDefault)["out"]
834
+ ) =>
835
+ <
836
+ const Tag extends string,
837
+ Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
838
+ Success extends Schema.Top = Schema.Void,
839
+ Error extends Schema.Top = Schema.Never,
840
+ const Stream extends boolean = false,
841
+ Out extends Custom.OutDefault = Custom.Kind<Def, Success, Error>
842
+ >(tag: Tag, options?: {
843
+ readonly payload?: Payload
844
+ readonly success?: Success
845
+ readonly error?: Error
846
+ readonly defect?: DefectSchema
847
+ readonly stream?: Stream
848
+ readonly primaryKey?: [Payload] extends [Schema.Struct.Fields] ? ((
849
+ payload: Payload extends Schema.Struct.Fields ? Struct.Simplify<Schema.Struct<Payload>["Type"]> : Payload["Type"]
850
+ ) => string) :
851
+ never
852
+ }): Rpc<
853
+ Tag,
854
+ Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> : Payload,
855
+ Stream extends true ? RpcSchema.Stream<Out["success"], Out["error"]> : Out["success"],
856
+ Stream extends true ? typeof Schema.Never : Out["error"]
857
+ > => {
858
+ const success = options?.success ?? Schema.Void
859
+ const error = options?.error ?? Schema.Never
860
+ const defect = options?.defect ?? Schema.Defect
861
+ const out = f({
862
+ success,
863
+ error,
864
+ defect
865
+ })
866
+ return make(tag, {
867
+ ...out,
868
+ primaryKey: options?.primaryKey,
869
+ payload: options?.payload,
870
+ stream: options?.stream
871
+ }) as any
872
+ }
873
+
874
+ /**
875
+ * @since 4.0.0
876
+ * @category Custom constructors
877
+ */
878
+ export interface Custom {
879
+ readonly out: Custom.OutDefault
880
+ readonly success: Schema.Top
881
+ readonly error: Schema.Top
882
+ readonly defect: DefectSchema
883
+ }
884
+
885
+ /**
886
+ * @since 4.0.0
887
+ * @category Custom constructors
888
+ */
889
+ export declare namespace Custom {
890
+ /**
891
+ * @since 4.0.0
892
+ * @category Custom constructors
893
+ */
894
+ export interface Out<
895
+ Success extends Schema.Top,
896
+ Error extends Schema.Top
897
+ > {
898
+ readonly success: Success
899
+ readonly error: Error
900
+ readonly defect: DefectSchema
901
+ }
902
+
903
+ /**
904
+ * @since 4.0.0
905
+ * @category Custom constructors
906
+ */
907
+ export type OutDefault = Out<Schema.Top, Schema.Top>
908
+
909
+ /**
910
+ * @since 4.0.0
911
+ * @category Custom constructors
912
+ */
913
+ export type Kind<
914
+ Def extends Custom,
915
+ Success extends Schema.Top,
916
+ Error extends Schema.Top
917
+ > = (Def & {
918
+ readonly success: Success
919
+ readonly error: Error
920
+ })["out"]
921
+ }
922
+
788
923
  const exitSchemaCache = new WeakMap<Any, Schema.Exit<Schema.Top, Schema.Top, DefectSchema>>()
789
924
 
790
925
  /**