diff --git a/.changeset/native-effect-duplicates.md b/.changeset/native-effect-duplicates.md new file mode 100644 index 0000000000..5261cbe49c --- /dev/null +++ b/.changeset/native-effect-duplicates.md @@ -0,0 +1,16 @@ +--- +"effect-app": patch +"@effect-app/infra": patch +"@effect-app/vue": patch +"@effect-app/vue-components": patch +--- + +Remove effect-app helpers that Effect already provides, and add `migrateEffectAppSource` to rewrite call sites. + +Deleted aliases and wrappers now live in Effect: `String.capitalize` / `uncapitalize`, `Struct.keys`, `Record.values`, `absurd`, `Tuple.make`, `Option.isSome`, `Option.liftPredicate` for non-empty arrays, `Array.chunksOf`, `Chunk.findFirst` / `findLast` / `partition` / `containsWith`, `Array.dedupeWith`, `Effect.annotateLogsScoped`, `Deferred.await`, `Effect.fromOption`, `Result.fromOption`, and `DateTime.add` / `subtract`. `Schema.DateValid` was an alias of `Schema.Date`. The date-fns calendar wrappers and the `date-fns` dependency are gone. + +`effect-app/migration/nativeEffect` exports the replacement table and `migrateEffectAppSource(source)`, which rewrites those imports in one file. `dropUndefined` and `fromBool` stay, along with `groupByT` (any key, entry list), `randomElement`, `findFirstMap`, and the schema constructors that add construction defaults. + +`effect-app/DateTime` adds `makeZonedLocal` and `pipeZonedLocal`. `makeZonedLocal` attaches the process timezone to a `Date` without moving the instant. `pipeZonedLocal` runs the functions passed to it and returns that instant with `DateTime.toDateUtc`. `DateAdd*` and `DateSub*` migrate to `pipeZonedLocal(date, DateTime.add({ days }))`, which keeps the local calendar. + +`Chunk.containsWith` calls the equivalence as `(needle, element)`. The old `elem` called it as `(element, needle)`, so only a symmetric equivalence is unchanged. `Record.values` expects one value type, so a mapped type whose value depends on its key should use `Object.values`. diff --git a/package.json b/package.json index 9ba85d25ba..90f2cb16ee 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "subtree:effect": "node scripts/sync-effect-subtree.js" }, "dependencies": { - "date-fns": "^4.4.0", "fast-check": "^4.9.0", "vue": "^3.5.35" }, diff --git a/packages/e2e/test/repoInvalidation.e2e.test.ts b/packages/e2e/test/repoInvalidation.e2e.test.ts index f65566c6ce..5ca7ef1bd4 100644 --- a/packages/e2e/test/repoInvalidation.e2e.test.ts +++ b/packages/e2e/test/repoInvalidation.e2e.test.ts @@ -8,7 +8,6 @@ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query" import { ApiClientFactory, InvalidStateError, makeRpcClient, NotLoggedInError, OptimisticConcurrencyException } from "effect-app/client" import * as Context from "effect-app/Context" import * as Effect from "effect-app/Effect" -import { tuple } from "effect-app/Function" import { HttpRouter, HttpServer } from "effect-app/http" import * as Layer from "effect-app/Layer" import { DefaultGenericMiddlewares } from "effect-app/middleware" @@ -22,6 +21,7 @@ import * as Option from "effect/Option" import * as Reactivity from "effect/reactivity/Reactivity" import { RpcSerialization } from "effect/rpc" import * as Scope from "effect/Scope" +import * as Tuple from "effect/Tuple" import { createServer } from "http" import { createApp, effectScope, ref } from "vue" import { RequestContextMiddleware } from "../../infra/src/internal/RequestContextMiddleware.ts" @@ -167,7 +167,7 @@ const setup = () => { Effect.context() ] as const ) - .pipe(Effect.map(([client, context]) => tuple(runtime, client, context)), runtime.runPromise) + .pipe(Effect.map(([client, context]) => Tuple.make(runtime, client, context)), runtime.runPromise) } it("atom engine: rpc repo write invalidates and refetches; unrelated rpc write does not", async () => { diff --git a/packages/effect-app/package.json b/packages/effect-app/package.json index 0b83b52d3a..1a902a0aa6 100644 --- a/packages/effect-app/package.json +++ b/packages/effect-app/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@tsconfig/strictest": "^2.0.8", - "date-fns": "^4.4.0", "nanoid": "^5.1.11", "path-parser": "^6.1.0", "query-string": "^9.4.0", diff --git a/packages/effect-app/src/Array.ts b/packages/effect-app/src/Array.ts index a36c992c8d..3ec7dca366 100644 --- a/packages/effect-app/src/Array.ts +++ b/packages/effect-app/src/Array.ts @@ -1,26 +1,9 @@ import type { NonEmptyArray, NonEmptyReadonlyArray } from "effect/Array" import * as Array from "effect/Array" -import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import { dual, type Predicate } from "./Function.ts" import * as Option from "./Option.ts" -export const toNonEmptyArray = Option.liftPredicate(Array.isReadonlyArrayNonEmpty) - -export const isArray: { - // uses ReadonlyArray here because otherwise the second overload don't work when ROA is involved. - (self: unknown): self is ReadonlyArray - (self: T): self is Extract> -} = Array.isArray - -export function NEAFromArray(ar: Array) { - return ar.length ? Option.some(ar as NonEmptyArray) : Option.none() -} - -export function NEROArrayFromArray(ar: ReadonlyArray) { - return ar.length ? Option.some(ar as NonEmptyReadonlyArray) : Option.none() -} - export const groupByT = dual< ( f: (a: NoInfer) => Key @@ -85,26 +68,11 @@ export function filterWith(self: ReadonlyArray, predicates: ReadonlyArray< return self.filter((_) => predicates.every((f) => f(_))) } -/** - * Split the `items` array into multiple, smaller chunks of the given `size`. - */ -export function* _chunk_(items_: Iterable, size: number) { - const items = [...items_] - - while (items.length) { - yield items.splice(0, size) - } -} - -/** - * Split the `items` array into multiple, smaller chunks of the given `size`. - */ -export function chunk_(items_: Iterable, size: number) { - return Chunk.fromIterable(_chunk_(items_, size)) -} - export function forEachEffectNA(as: NonEmptyReadonlyArray, f: (a: A) => Effect.Effect) { - return Effect.map(Effect.forEach(as, f), (_) => Option.getOrNull(toNonEmptyArray(_))) + return Effect.map( + Effect.forEach(as, f), + (_) => Option.getOrNull(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(_)) + ) } export * from "effect/Array" diff --git a/packages/effect-app/src/Chunk.ts b/packages/effect-app/src/Chunk.ts index 1ac48650d5..bde5c8a491 100644 --- a/packages/effect-app/src/Chunk.ts +++ b/packages/effect-app/src/Chunk.ts @@ -1,20 +1,19 @@ -import { pipe, type Predicate, type Refinement } from "./Function.ts" +import { pipe } from "./Function.ts" import * as Option from "./Option.ts" import * as Chunk from "effect/Chunk" -import { type Equivalence } from "effect/Equivalence" import * as Array from "./Array.ts" export * from "effect/Chunk" -export const fromIterable = Chunk.fromIterable - export function groupByTChunk_(c: Chunk.Chunk, f: (a: A) => Key) { return pipe(Chunk.toReadonlyArray(c), Array.groupByT(f), Chunk.fromIterable) } /** - * Returns the first element that satisfies the predicate. + * Returns the first `Some` produced by `f`. + * + * Effect's `Chunk.findFirst` takes a predicate, not an `Option`-returning function. */ export function findFirstMap( f: (a: A) => Option.Option @@ -31,51 +30,3 @@ export function findFirstMap( return Option.none() } } - -export function toArray(c: Chunk.Chunk): T[] { - return Chunk.toReadonlyArray(c) as T[] -} - -/** - * Remove duplicates from an array, keeping the first occurrence of an element. - */ -export function uniq(E: Equivalence) { - return (self: Chunk.Chunk): Chunk.Chunk => { - let out = Chunk.fromIterable([] as A[]) - for (let i = 0; i < self.length; i++) { - const a = Chunk.getUnsafe(self, i) - if (!elem(E, a)(out)) { - out = Chunk.append(out, a) - } - } - return self.length === out.length ? self : out - } -} - -/** - * Test if a value is a member of an array. Takes a `Equivalence` as a single - * argument which returns the function to use to search for a value of type `A` - * in an array of type `Chunk`. - */ -export function elem(E: Equivalence, value: A) { - return (self: Chunk.Chunk): boolean => { - for (let i = 0; i < self.length; i++) { - if (E(Chunk.getUnsafe(self, i), value)) { - return true - } - } - return false - } -} - -export const ChunkPartition = Chunk.partition - -export const findFirstSimple: { - (self: Chunk.Chunk, refinement: Refinement): Option.Option - (self: Chunk.Chunk, predicate: Predicate): Option.Option -} = Chunk.findFirst - -export const findLastSimple: { - (self: Chunk.Chunk, refinement: Refinement): Option.Option - (self: Chunk.Chunk, predicate: Predicate): Option.Option -} = Chunk.findLast diff --git a/packages/effect-app/src/DateTime.ts b/packages/effect-app/src/DateTime.ts new file mode 100644 index 0000000000..9b0bdad7b4 --- /dev/null +++ b/packages/effect-app/src/DateTime.ts @@ -0,0 +1,473 @@ +import * as DateTime from "effect/DateTime" + +/** + * The same instant as `date`, zoned with the process timezone. + * + * A JavaScript `Date` stores no zone. This attaches `DateTime.zoneMakeLocal()` without moving the instant. + */ +export const makeZonedLocal = (date: Date): DateTime.Zoned => + DateTime.makeZonedUnsafe(date, { timeZone: DateTime.zoneMakeLocal() }) + +/** + * Zones `date` with the process timezone, runs the functions, then returns the instant with `DateTime.toDateUtc`. + * + * Accepts as many functions as `pipe`. The last function must return a `DateTime`. Calendar parts follow the process timezone, as `Date#setDate` does. + */ +export function pipeZonedLocal(date: Date): Date +export function pipeZonedLocal( + date: Date, + ab: (self: DateTime.Zoned) => A +): Date +export function pipeZonedLocal( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B +): Date +export function pipeZonedLocal( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L, + mn: (l: L) => M +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L, + mn: (l: L) => M, + no: (m: M) => N +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L, + mn: (l: L) => M, + no: (m: M) => N, + op: (n: N) => O +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L, + mn: (l: L) => M, + no: (m: M) => N, + op: (n: N) => O, + pq: (o: O) => P +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L, + mn: (l: L) => M, + no: (m: M) => N, + op: (n: N) => O, + pq: (o: O) => P, + qr: (p: P) => Q +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L, + mn: (l: L) => M, + no: (m: M) => N, + op: (n: N) => O, + pq: (o: O) => P, + qr: (p: P) => Q, + rs: (q: Q) => R +): Date +export function pipeZonedLocal< + A = never, + B = never, + C = never, + D = never, + E = never, + F = never, + G = never, + H = never, + I = never, + J = never, + K = never, + L = never, + M = never, + N = never, + O = never, + P = never, + Q = never, + R = never, + S extends DateTime.DateTime = never +>( + date: Date, + ab: (self: DateTime.Zoned) => A, + bc: (a: A) => B, + cd: (b: B) => C, + de: (c: C) => D, + ef: (d: D) => E, + fg: (e: E) => F, + gh: (f: F) => G, + hi: (g: G) => H, + ij: (h: H) => I, + jk: (i: I) => J, + kl: (j: J) => K, + lm: (k: K) => L, + mn: (l: L) => M, + no: (m: M) => N, + op: (n: N) => O, + pq: (o: O) => P, + qr: (p: P) => Q, + rs: (q: Q) => R, + st: (r: R) => S +): Date +export function pipeZonedLocal(date: Date, ...steps: ReadonlyArray<(value: any) => any>): Date { + let current: unknown = makeZonedLocal(date) + for (const step of steps) { + current = step(current) + } + if (!DateTime.isDateTime(current)) { + throw new TypeError("pipeZonedLocal must finish on a DateTime") + } + return DateTime.toDateUtc(current) +} diff --git a/packages/effect-app/src/Effect.ts b/packages/effect-app/src/Effect.ts index 0685cdbebb..b840560951 100644 --- a/packages/effect-app/src/Effect.ts +++ b/packages/effect-app/src/Effect.ts @@ -2,15 +2,13 @@ /* eslint-disable prefer-destructuring */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import * as Def from "effect/Deferred" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as Ref from "effect/Ref" -import type { Scope } from "effect/Scope" import type { Semaphore } from "effect/Semaphore" +import * as Struct from "effect/Struct" import type * as Context from "./Context.ts" import { curry } from "./Function.ts" -import { typedKeysOf } from "./utils.ts" export * from "effect/Effect" // v4: Effect interface not re-exported by `export *` due to local binding collision @@ -94,9 +92,6 @@ export function ifDiff(n: I, orig: I) { return (f: (i: I) => Effect.Effect) => ifDiff_(n, orig, f) } -// NOTE: await extension doesnt work via tsplus somehow -export const await_ = Def.await - /** * Ref has atomic modify support if synchronous, for Effect we need a Semaphore. */ @@ -151,9 +146,9 @@ export function allLower | Ef services: T ) { return Effect.all( - typedKeysOf(services).reduce((prev, cur) => { + Struct.keys(services).reduce((prev, cur) => { const svc = services[cur]! - prev[((cur as string)[0]!.toLowerCase() + (cur as string).slice(1)) as unknown as LowerFirst] = svc + prev[(cur[0]!.toLowerCase() + cur.slice(1)) as unknown as LowerFirst] = svc return prev }, {} as any), { concurrency: "unbounded" } @@ -186,17 +181,3 @@ export function catchAllMap(f: (e: E) => A2) { return (self: Effect.Effect): Effect.Effect => Effect.catch(self, (err: E) => Effect.sync(() => f(err))) } - -/** - * Annotates each log in this scope with the specified log annotation. - */ -export function annotateLogscoped(key: string, value: string): Effect.Effect { - return Effect.annotateLogsScoped(key, value) -} - -/** - * Annotates each log in this scope with the specified log annotations. - */ -export function annotateLogsScoped(kvps: Record): Effect.Effect { - return Effect.annotateLogsScoped(kvps) -} diff --git a/packages/effect-app/src/Function.ts b/packages/effect-app/src/Function.ts index 1f56364fef..567367d23b 100644 --- a/packages/effect-app/src/Function.ts +++ b/packages/effect-app/src/Function.ts @@ -1,13 +1,6 @@ export * from "effect/Function" export type { Predicate, Refinement } from "effect/Predicate" -/** - * Construct tuples - */ -export function tuple>(...t: T): Readonly { - return t -} - /* eslint-disable @typescript-eslint/no-explicit-any */ export function tupledCurry(f: (b: B) => (a: A) => C) { diff --git a/packages/effect-app/src/Model/Repository/internal/internal.ts b/packages/effect-app/src/Model/Repository/internal/internal.ts index 07bc29355a..99bdf2b4d6 100644 --- a/packages/effect-app/src/Model/Repository/internal/internal.ts +++ b/packages/effect-app/src/Model/Repository/internal/internal.ts @@ -12,7 +12,6 @@ import type * as Scope from "effect/Scope" import * as Unify from "effect/Unify" import * as Array from "../../../Array.ts" import type { NonEmptyReadonlyArray } from "../../../Array.ts" -import { toNonEmptyArray } from "../../../Array.ts" import * as Chunk from "../../../Chunk.ts" import { NotFoundError } from "../../../client/errors.ts" import * as Context from "../../../Context.ts" @@ -407,7 +406,7 @@ export function makeRepoInternal< const saveAllE = (a: Iterable) => flatMapOption( Effect - .sync(() => toNonEmptyArray([...a])), + .sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)([...a])), (a) => Effect.gen(function*() { const { get, set } = yield* cms @@ -442,7 +441,7 @@ export function makeRepoInternal< }) return yield* saveAll(it) .pipe( - Effect.andThen(Effect.sync(() => toNonEmptyArray(evts))), + Effect.andThen(Effect.sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(evts))), // TODO: for full consistency the events should be stored within the same database transaction, and then picked up. (_) => flatMapOption(_, pub), Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"] as [T[], "save" | "remove"])), @@ -473,7 +472,7 @@ export function makeRepoInternal< set(e[idKey], undefined) } yield* Effect - .sync(() => toNonEmptyArray(evts)) + .sync(() => Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(evts)) // TODO: for full consistency the events should be stored within the same database transaction, and then picked up. .pipe((_) => flatMapOption(_, pub)) diff --git a/packages/effect-app/src/Model/dsl.ts b/packages/effect-app/src/Model/dsl.ts index a99abfd715..0cfef0a68d 100644 --- a/packages/effect-app/src/Model/dsl.ts +++ b/packages/effect-app/src/Model/dsl.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { NonEmptyReadonlyArray } from "../Array.ts" -import { toNonEmptyArray } from "../Array.ts" +import { isReadonlyArrayNonEmpty, type NonEmptyReadonlyArray } from "../Array.ts" import * as Effect from "../Effect.ts" +import * as Option from "../Option.ts" import { type FixEnv, get, logMany, type Pure, type PureEnvEnv, type PureLogT, set } from "../Pure.ts" export interface PureDSL { @@ -118,12 +118,13 @@ export function makeDSL() { export interface DSLExt extends ReturnType> {} export function ifAny(fn: (items: NonEmptyReadonlyArray) => Effect.Effect) { - return (items: Iterable) => Effect.flatMapOption(Effect.sync(() => toNonEmptyArray([...items])), fn) + return (items: Iterable) => + Effect.flatMapOption(Effect.sync(() => Option.liftPredicate(isReadonlyArrayNonEmpty)([...items])), fn) } export function ifAny_( items: Iterable, fn: (items: NonEmptyReadonlyArray) => Effect.Effect ) { - return Effect.flatMapOption(Effect.sync(() => toNonEmptyArray([...items])), fn) + return Effect.flatMapOption(Effect.sync(() => Option.liftPredicate(isReadonlyArrayNonEmpty)([...items])), fn) } diff --git a/packages/effect-app/src/Model/query/new-kid-interpreter.ts b/packages/effect-app/src/Model/query/new-kid-interpreter.ts index e3c6804446..a0c38e24b0 100644 --- a/packages/effect-app/src/Model/query/new-kid-interpreter.ts +++ b/packages/effect-app/src/Model/query/new-kid-interpreter.ts @@ -2,7 +2,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { identity } from "effect/Function" import * as Array from "../../Array.ts" -import { toNonEmptyArray } from "../../Array.ts" import * as Option from "../../Option.ts" import * as S from "../../Schema.ts" import * as SchemaAST from "../../SchemaAST.ts" @@ -439,10 +438,10 @@ export const toFilter = < t: null as unknown as TFieldValues, limit: a.limit, skip: a.skip, - select: Option.getOrUndefined(toNonEmptyArray(aggSelect)) as any, + select: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(aggSelect)) as any, schema: a.schema, computed: undefined, - order: Option.getOrUndefined(toNonEmptyArray(a.order)), + order: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(a.order)), ttype: a.ttype, mode: "aggregate" as const, filter: a.filter.length ? a.filter : undefined @@ -519,10 +518,10 @@ export const toFilter = < t: null as unknown as TFieldValues, limit: a.limit, skip: a.skip, - select: Option.getOrUndefined(toNonEmptyArray(select)), + select: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(select)), schema, computed, - order: Option.getOrUndefined(toNonEmptyArray(a.order)), + order: Option.getOrUndefined(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(a.order)), ttype: a.ttype, mode: a.mode ?? "transform", filter: a.filter.length diff --git a/packages/effect-app/src/Option.ts b/packages/effect-app/src/Option.ts index 5e0934ddc9..3daac9dfef 100644 --- a/packages/effect-app/src/Option.ts +++ b/packages/effect-app/src/Option.ts @@ -4,22 +4,15 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ // eslint-disable-next-line import/no-unassigned-import import "./builtin.ts" -import { getOrUndefined as value, type Some } from "effect/Option" +import { type Some } from "effect/Option" import * as Option from "effect/Option" export * from "effect/Option" -export const getOrUndefined = value - export function omitableToNullable(om: Option.Option | undefined) { return om ?? Option.fromNullishOr(om) } -export const toBool = Option.match({ - onNone: () => false, - onSome: () => true -}) - export const fromBool = (b: boolean) => (b ? Option.some(true) : Option.none()) /** diff --git a/packages/effect-app/src/Pure.ts b/packages/effect-app/src/Pure.ts index c21bcf9dbb..41315bc14c 100644 --- a/packages/effect-app/src/Pure.ts +++ b/packages/effect-app/src/Pure.ts @@ -3,8 +3,8 @@ import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as Result from "effect/Result" +import * as Tuple from "effect/Tuple" import * as Context from "./Context.ts" -import { tuple } from "./Function.ts" const S1 = Symbol() const S2 = Symbol() @@ -159,10 +159,10 @@ export function runAll( ) .pipe( Effect.map( - ({ log, state }) => tuple(log, Result.succeed(tuple(state, x))) + ({ log, state }) => Tuple.make(log, Result.succeed(Tuple.make(state, x))) ) )) - .pipe(Effect.catch((err: any) => tagg.useSync((env) => tuple(env.env.log, Result.fail(err))))) + .pipe(Effect.catch((err: any) => tagg.useSync((env) => Tuple.make(env.env.log, Result.fail(err))))) return Effect.provide(a, Layer.succeed(tagg, { env: makePureEnv(s) as any }) as any) as any } @@ -170,7 +170,7 @@ export function runResult( self: Effect.Effect>, s: S4 ) { - return Effect.map(runAll(self, s), ([log, r]) => tuple(log, Result.map(r, ([s]) => s))) + return Effect.map(runAll(self, s), ([log, r]) => Tuple.make(log, Result.map(r, ([s]) => s))) } export function runTerm( @@ -179,7 +179,7 @@ export function runTerm( ) { return Effect.flatMap( runAll(self, s), - ([evts, r]) => Effect.map(Effect.fromResult(r), ([s3, a]) => tuple(s3, Chunk.toArray(evts), a)) + ([evts, r]) => Effect.map(Effect.fromResult(r), ([s3, a]) => Tuple.make(s3, Chunk.toArray(evts), a)) ) } @@ -187,14 +187,14 @@ export function runTermDiscard( self: Effect.Effect>, s: S4 ) { - return Effect.map(runTerm(self, s), ([s3, w3]) => tuple(s3, w3)) + return Effect.map(runTerm(self, s), ([s3, w3]) => Tuple.make(s3, w3)) } export function runA( self: Effect.Effect>, s: S4 ) { - return Effect.map(runAll(self, s), ([log, r]) => tuple(log, Result.map(r, ([, a]) => a))) + return Effect.map(runAll(self, s), ([log, r]) => Tuple.make(log, Result.map(r, ([, a]) => a))) } export function modify( @@ -224,14 +224,14 @@ export function modifyM( export function updateWith(upd: (s: S2) => S3) { return modify((_: S2) => { const r = upd(_) - return tuple(r, r) + return Tuple.make(r, r) }) } export function updateWithEffect( upd: (s: S2, log: (evt: W) => PureLogT) => Effect.Effect> ): Effect.Effect> { - return modifyM((_: S2) => Effect.map(upd(_, log), (_) => tuple(_, _))) + return modifyM((_: S2) => Effect.map(upd(_, log), (_) => Tuple.make(_, _))) } export type FixEnv = @@ -301,11 +301,11 @@ export type FixEnv = // } // function update(upd: (s: S2) => S3) { -// return modify(_ => tuple(upd(_), void 0 as void)) +// return modify(_ => Tuple.make(upd(_), void 0 as void)) // } // function updateM(upd: (s: S2) => Effect.Effect) { -// return modifyM(_ => upd(_).map(_ => tuple(_, void 0 as void))) +// return modifyM(_ => upd(_).map(_ => Tuple.make(_, void 0 as void))) // } // const accessLog = Effect.serviceWithEffect(tag, _ => _.env.log.get) @@ -321,12 +321,12 @@ export type FixEnv = // ).map( // ( // { log, state } -// ) => tuple(log, Either.right(tuple(state, x)) as Either.Either) +// ) => Tuple.make(log, Either.right(Tuple.make(state, x)) as Either.Either) // ) // ).catchAll( // err => // accessLog.map(log => -// tuple(log, Either.left(err) as Either.Either) +// Tuple.make(log, Either.left(err) as Either.Either) // ) // ).provideService(tag, { env: makePureEnv(s) as any }) as any // } @@ -335,14 +335,14 @@ export type FixEnv = // self: Effect.Effect, E, A>, // s: S4 // ) { -// return runAll(self, s).map(([log, r]) => tuple(log, r.map(([s]) => s))) +// return runAll(self, s).map(([log, r]) => Tuple.make(log, r.map(([s]) => s))) // } // function runA( // self: Effect.Effect, E, A>, // s: S4 // ) { -// return runAll(self, s).map(([log, r]) => tuple(log, r.map(([, a]) => a))) +// return runAll(self, s).map(([log, r]) => Tuple.make(log, r.map(([, a]) => a))) // } // return { diff --git a/packages/effect-app/src/Schema.ts b/packages/effect-app/src/Schema.ts index 606f9cdd2d..71afd00561 100644 --- a/packages/effect-app/src/Schema.ts +++ b/packages/effect-app/src/Schema.ts @@ -115,7 +115,7 @@ export * from "./Schema/Class.ts" export { Class, ErrorClass, Opaque, OpaqueErrorFacadeClass, OpaqueFacade, OpaqueShape, OpaqueType, TaggedClass, TaggedErrorClass } from "./Schema/Class.ts" export { fromBrand, nominal } from "./Schema/brand.ts" -export { Array, Boolean, Date, DateFromString, DateValid, Finite, Literals, NullOr, Number, ReadonlyMap, ReadonlySet } from "./Schema/ext.ts" +export { Array, Boolean, Date, DateFromString, Finite, Literals, NullOr, Number, ReadonlyMap, ReadonlySet } from "./Schema/ext.ts" export { Int, NonNegativeInt } from "./Schema/numbers.ts" export * from "./Schema/email.ts" diff --git a/packages/effect-app/src/Schema/ext.ts b/packages/effect-app/src/Schema/ext.ts index e19242ee80..788f966890 100644 --- a/packages/effect-app/src/Schema/ext.ts +++ b/packages/effect-app/src/Schema/ext.ts @@ -43,7 +43,7 @@ import * as Struct from "effect/Struct" import { type NonEmptyReadonlyArray } from "../Array.ts" import * as Context from "../Context.ts" import * as SchemaAST from "../SchemaAST.ts" -import { extendM, typedKeysOf } from "../utils.ts" +import { extendM } from "../utils.ts" import { type AST } from "./schema.ts" type ProvidedCodec = S.Codec< @@ -134,13 +134,6 @@ const dateHelpers = (s: S.Date) => ({ /** Like the default Schema `Date` (Encoded is `Date`) with default helpers. */ export const Date = extendM(S.Date, dateHelpers) -/** - * Alias of {@link Date}. Core `Schema.Date` already rejects invalid Dates. - * - * @deprecated Use {@link Date}. - */ -export const DateValid = Date - /** Like the default Schema `Boolean` but with default helpers. */ export const Boolean = Object.assign(S.Boolean, { /** @@ -522,7 +515,7 @@ export function makeOptional( ): { [K in keyof NER]: NER[K] extends S.Top ? ReturnType> : any } { - return typedKeysOf(t).reduce((prev, cur) => { + return Struct.keys(t).reduce((prev, cur) => { prev[cur] = S.optional(t[cur] as any) return prev }, {} as any) @@ -601,7 +594,7 @@ export function makeExactOptional( [K in keyof NER]: NER[K] extends S.Top ? DropConstructorDefault>> : any } { - return typedKeysOf(t).reduce((prev, cur) => { + return Struct.keys(t).reduce((prev, cur) => { prev[cur] = dropConstructorDefault(S.optionalKey(t[cur] as any)) return prev }, {} as any) diff --git a/packages/effect-app/src/Set.ts b/packages/effect-app/src/Set.ts index f03b8138d2..23af735d22 100644 --- a/packages/effect-app/src/Set.ts +++ b/packages/effect-app/src/Set.ts @@ -6,7 +6,8 @@ import * as Option from "effect/Option" import type * as Order from "effect/Order" import { not } from "effect/Predicate" import type * as Result from "effect/Result" -import { identity, pipe, type Predicate, type Refinement, tuple } from "./Function.ts" +import * as Tuple from "effect/Tuple" +import { identity, pipe, type Predicate, type Refinement } from "./Function.ts" export function find_( as: ReadonlySet, @@ -309,7 +310,7 @@ export function partition_( left.add(value) } } - return tuple(left, right) + return Tuple.make(left, right) } /** @@ -375,7 +376,7 @@ export function partitionMap_( break } } - return tuple(left, right) + return Tuple.make(left, right) } } @@ -552,7 +553,7 @@ export function separate( break } }) - return tuple(left, right) + return Tuple.make(left, right) } } diff --git a/packages/effect-app/src/_ext/date.ts b/packages/effect-app/src/_ext/date.ts deleted file mode 100644 index 60169bea55..0000000000 --- a/packages/effect-app/src/_ext/date.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { addDays, addHours, addMinutes, addMonths, addSeconds, addWeeks, addYears, subDays, subHours, subMinutes, subMonths, subSeconds, subWeeks, subYears } from "date-fns" - -export const DateAddDays: (date: Date, amount: number) => Date = addDays -export const DateSubDays: (date: Date, amount: number) => Date = subDays - -export const DateAddHours: (date: Date, amount: number) => Date = addHours -export const DateSubHours: (date: Date, amount: number) => Date = subHours - -export const DateAddMinutes: (date: Date, amount: number) => Date = addMinutes - -export const DateSubMinutes: (date: Date, amount: number) => Date = subMinutes - -export const DateAddSeconds: (date: Date, amount: number) => Date = addSeconds - -export const DateSubSeconds: (date: Date, amount: number) => Date = subSeconds - -export const DateAddYears: (date: Date, amount: number) => Date = addYears - -export const DateSubYears: (date: Date, amount: number) => Date = subYears - -export const DateAddMonths: (date: Date, amount: number) => Date = addMonths - -export const DateSubMonths: (date: Date, amount: number) => Date = subMonths - -export const DateAddWeeks: (date: Date, amount: number) => Date = addWeeks - -export const DateSubWeeks: (date: Date, amount: number) => Date = subWeeks diff --git a/packages/effect-app/src/_ext/misc.ts b/packages/effect-app/src/_ext/misc.ts deleted file mode 100644 index 0d4c2a7368..0000000000 --- a/packages/effect-app/src/_ext/misc.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { LazyArg } from "effect-app/Function" -import * as Effect from "effect/Effect" -import * as Option from "effect/Option" -import * as Result from "effect/Result" -import type * as Scope from "effect/Scope" - -export type _R> = [T] extends [ - Effect.Effect -] ? R - : never - -export type _E> = [T] extends [ - Effect.Effect -] ? E - : never - -export function encaseMaybeInEffect_( - o: Option.Option, - onError: LazyArg -): Effect.Effect { - return Option.match(o, { onNone: () => Effect.fail(onError()), onSome: Effect.succeed }) -} - -export function encaseMaybeEither_( - o: Option.Option, - onError: LazyArg -): Result.Result { - return Option.match(o, { onNone: () => Result.fail(onError()), onSome: Result.succeed }) -} - -export function toNullable( - self: Effect.Effect, E, R> -) { - return Effect.map(self, (_) => Option.getOrNull(_)) -} - -export function scope( - scopedEffect: Effect.Effect, - effect: Effect.Effect -): Effect.Effect> { - return Effect.andThen(scopedEffect, effect).pipe(Effect.scoped) -} - -export function flatMapScoped( - scopedEffect: Effect.Effect, - effect: (a: A) => Effect.Effect -): Effect.Effect> { - return scopedEffect.pipe(Effect.flatMap(effect), Effect.scoped) -} diff --git a/packages/effect-app/src/client/apiClientFactory.ts b/packages/effect-app/src/client/apiClientFactory.ts index c55d8d562a..b957831084 100644 --- a/packages/effect-app/src/client/apiClientFactory.ts +++ b/packages/effect-app/src/client/apiClientFactory.ts @@ -4,6 +4,7 @@ import * as Layer from "effect/Layer" import * as ManagedRuntime from "effect/ManagedRuntime" import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" import { Rpc, RpcClient, RpcGroup, RpcSerialization } from "effect/rpc" import * as Schema from "effect/Schema" import * as Stream from "effect/Stream" @@ -15,7 +16,6 @@ import * as Effect from "../Effect.ts" import { HttpClient, HttpClientRequest } from "../http.ts" import { Invalidation } from "../rpc.ts" import type * as S from "../Schema.ts" -import { typedKeysOf, typedValuesOf } from "../utils.ts" import type { Client, ClientForOptions, ExtractModuleName, RequestsAny } from "./clientFor.ts" import { InvalidationKeysFromServer } from "./InvalidationKeys.ts" @@ -94,7 +94,7 @@ const getFiltered = (resource: M) => { [K in keyof M as M[K] extends Req ? K : never]: M[K] extends Req ? M[K] : never } // TODO: Record.filter - const filtered = typedKeysOf(resource).reduce((acc, cur) => { + const filtered = Struct.keys(resource).reduce((acc, cur) => { if ( Predicate.isObjectKeyword(resource[cur]) && (resource[cur].success) @@ -110,7 +110,7 @@ const getFiltered = (resource: M) => { export const getMeta = ( resource: M ): { moduleName: ExtractModuleName; middleware?: unknown } => { - const first = typedValuesOf(getFiltered(resource))[0] + const first = Record.values(getFiltered(resource))[0] if (first && "moduleName" in first) return { moduleName: first.moduleName, middleware: (first as any).middleware } throw new Error("No moduleName on requests!") } @@ -125,7 +125,7 @@ export const makeRpcGroupFromRequestsAndModuleName = { + ...Record.values(filtered).map((_) => { const r = _ as any const isStream = r.stream const isCommand = r.type === "command" @@ -262,13 +262,14 @@ const makeApiClientFactory = Effect return { mr, - client: typedKeysOf(filtered) + client: Struct + .keys(filtered) .reduce((prev, cur) => { const h = filtered[cur]! const Request = h - const id = `${meta.moduleName}.${cur as string}` + const id = `${meta.moduleName}.${cur}` .replaceAll(".js", "") const requestMeta = { @@ -281,7 +282,7 @@ const makeApiClientFactory = Effect } const requestNameLayer = Layer.succeed(RequestName, { - requestName: cur as string, + requestName: cur, moduleName: meta.moduleName }) diff --git a/packages/effect-app/src/migration/nativeEffect.ts b/packages/effect-app/src/migration/nativeEffect.ts new file mode 100644 index 0000000000..59dc42e431 --- /dev/null +++ b/packages/effect-app/src/migration/nativeEffect.ts @@ -0,0 +1,645 @@ +/** + * Rewrites source that imported effect-app helpers now provided by Effect. + * + * The rewrite is textual and keeps the rest of the file. It understands named + * imports, aliases, and `import * as` namespaces from the effect-app entry points + * listed in {@link nativeEffectReplacements}. + */ + +export interface NativeEffectReplacement { + readonly module: string + readonly name: string + /** Expression that replaces a reference to `name`. */ + readonly expression: string + readonly note: string + readonly imports: ReadonlyArray + /** When set, a call `name(...args)` is rewritten instead of the bare identifier. */ + readonly call?: "date-add" | "date-sub" | "chunk" +} + +const struct = [["Struct", "effect/Struct"]] as const +const record = [["Record", "effect/Record"]] as const +const str = [["String", "effect/String"]] as const +const option = [["Option", "effect/Option"]] as const +const array = [["Array", "effect/Array"]] as const +const chunk = [["Chunk", "effect/Chunk"]] as const +const tuple = [["Tuple", "effect/Tuple"]] as const +const effect = [["Effect", "effect/Effect"]] as const +const dateTime = [["DateTime", "effect/DateTime"], ["pipeZonedLocal", "effect-app/DateTime"]] as const +const deferred = [["Deferred", "effect/Deferred"]] as const +const result = [["Result", "effect/Result"]] as const +const absurd = [["absurd", "effect/Function"]] as const + +export const nativeEffectReplacements: ReadonlyArray = [ + { + module: "effect-app/utils", + name: "typedKeysOf", + expression: "Struct.keys", + imports: struct, + note: "Struct.keys returns `(keyof T & string)[]`." + }, + { + module: "effect-app/utils", + name: "typedValuesOf", + expression: "Record.values", + imports: record, + note: + "Record.values reads a record with one value type. When each value's type depends on its key, use Object.values." + }, + { + module: "effect-app/utils", + name: "capitalize", + expression: "String.capitalize", + imports: str, + note: "Same first-character capitalization, including the empty string." + }, + { + module: "effect-app/utils", + name: "uncapitalize", + expression: "String.uncapitalize", + imports: str, + note: "Same first-character uncapitalization, including the empty string." + }, + { + module: "effect-app/utils", + name: "assertUnreachable", + expression: "absurd", + imports: absurd, + note: "absurd throws when an impossible value is reached. The error text no longer includes the value." + }, + { + module: "effect-app/Function", + name: "tuple", + expression: "Tuple.make", + imports: tuple, + note: "Tuple.make returns a mutable tuple. It is assignable wherever a readonly tuple was expected." + }, + { + module: "effect-app/Array", + name: "toNonEmptyArray", + expression: "Option.liftPredicate(Array.isReadonlyArrayNonEmpty)", + imports: [...option, ...array], + note: "Option of the same readonly array when it is non-empty." + }, + { + module: "effect-app/Array", + name: "NEROArrayFromArray", + expression: "Option.liftPredicate(Array.isReadonlyArrayNonEmpty)", + imports: [...option, ...array], + note: "Same as toNonEmptyArray." + }, + { + module: "effect-app/Array", + name: "NEAFromArray", + expression: "Option.liftPredicate(Array.isArrayNonEmpty)", + imports: [...option, ...array], + note: "Option of the same mutable array when it is non-empty." + }, + { + module: "effect-app/Array", + name: "chunk_", + expression: "Array.chunksOf", + imports: [...array, ...chunk], + call: "chunk", + note: "Was a Chunk of mutable slices. Now a Chunk of Array.chunksOf groups. An empty input is an empty Chunk." + }, + { + module: "effect-app/Array", + name: "_chunk_", + expression: "Array.chunksOf", + imports: array, + note: "The generator form is gone. Array.chunksOf returns the groups directly." + }, + { + module: "effect-app/Option", + name: "toBool", + expression: "Option.isSome", + imports: option, + note: "isSome is the boolean test and also narrows to Some." + }, + { + module: "effect-app/Effect", + name: "annotateLogscoped", + expression: "Effect.annotateLogsScoped", + imports: effect, + note: "The old name was a typo of Effect.annotateLogsScoped." + }, + { + module: "effect-app/Effect", + name: "await_", + expression: "Deferred.await", + imports: deferred, + note: "Await a Deferred. Effect.await_ was Deferred.await." + }, + { + module: "effect-app/Chunk", + name: "findFirstSimple", + expression: "Chunk.findFirst", + imports: chunk, + note: "The alias passed the chunk first. Chunk.findFirst is dual, so the data-first call still typechecks." + }, + { + module: "effect-app/Chunk", + name: "findLastSimple", + expression: "Chunk.findLast", + imports: chunk, + note: "Alias of Chunk.findLast." + }, + { + module: "effect-app/Chunk", + name: "ChunkPartition", + expression: "Chunk.partition", + imports: chunk, + note: "Alias of Chunk.partition." + }, + { + module: "effect-app/Chunk", + name: "uniq", + expression: + "(equivalence) => (self) => Chunk.fromIterable(Array.dedupeWith(Chunk.toReadonlyArray(self), equivalence))", + imports: [...chunk, ...array], + note: "Keeps the first match under a custom equivalence. Equal values can use Chunk.dedupe instead." + }, + { + module: "effect-app/Chunk", + name: "elem", + expression: "(equivalence, value) => Chunk.containsWith(equivalence)(value)", + imports: chunk, + note: "Equivalence is treated as symmetric. containsWith applies it as equivalence(needle, element)." + }, + { + module: "effect-app/Schema", + name: "DateValid", + expression: "Date", + imports: [["Date", "effect-app/Schema"]], + note: "DateValid was an alias of effect-app Schema.Date. Schema.Date already rejects invalid dates." + }, + { + module: "effect-app/_ext/misc", + name: "encaseMaybeInEffect_", + expression: "Effect.fromOption", + imports: effect, + note: "Effect.fromOption(option, onNone) fails with the lazy error on None." + }, + { + module: "effect-app/_ext/misc", + name: "encaseMaybeEither_", + expression: "Result.fromOption", + imports: result, + note: "Result.fromOption(option, onNone)." + }, + { + module: "effect-app/_ext/misc", + name: "toNullable", + expression: "(effect) => Effect.map(effect, Option.getOrNull)", + imports: [...effect, ...option], + note: "Maps a successful Option to its value or null." + }, + { + module: "effect-app/_ext/misc", + name: "flatMapScoped", + expression: "(scoped, f) => Effect.scoped(Effect.flatMap(scoped, f))", + imports: effect, + note: "Closes the scope after the continuation." + }, + { + module: "effect-app/_ext/misc", + name: "scope", + expression: "(scoped, effect) => Effect.scoped(Effect.andThen(scoped, effect))", + imports: effect, + note: "Runs the second effect inside the first effect's scope, then closes it." + }, + ...([ + ["DateAddDays", "days", "date-add"], + ["DateAddHours", "hours", "date-add"], + ["DateAddMinutes", "minutes", "date-add"], + ["DateAddSeconds", "seconds", "date-add"], + ["DateAddWeeks", "weeks", "date-add"], + ["DateAddMonths", "months", "date-add"], + ["DateAddYears", "years", "date-add"], + ["DateSubDays", "days", "date-sub"], + ["DateSubHours", "hours", "date-sub"], + ["DateSubMinutes", "minutes", "date-sub"], + ["DateSubSeconds", "seconds", "date-sub"], + ["DateSubWeeks", "weeks", "date-sub"], + ["DateSubMonths", "months", "date-sub"], + ["DateSubYears", "years", "date-sub"] + ] as const) + .map(([name, unit, call]): NativeEffectReplacement => ({ + module: "effect-app/_ext/date", + name, + expression: unit, + imports: dateTime, + call, + note: + "pipeZonedLocal attaches the process timezone, runs DateTime.add or DateTime.subtract, and returns the instant with toDateUtc." + })) +] + +interface Binding { + readonly replacement: NativeEffectReplacement + readonly local: string +} + +const byModule = new Map>() +for (const replacement of nativeEffectReplacements) { + const existing = byModule.get(replacement.module) ?? new Map() + existing.set(replacement.name, replacement) + byModule.set(replacement.module, existing) +} + +const moduleNames = new Set(byModule.keys()) + +interface Rendered { + readonly text: string + readonly end: number +} + +/** Rewrite one source file. `source` is the file text. */ +export const migrateEffectAppSource = (source: string): string => { + const imports = findImports(source) + const bindings = new Map() + const namespaceBindings = new Map>() + const edits: Array<{ start: number; end: number; text: string }> = [] + const needed = new Map>() + const maskedSpans: Array<{ start: number; end: number }> = [] + + for (const statement of imports) { + const table = byModule.get(statement.module) + if (table === undefined) continue + maskedSpans.push({ start: statement.start, end: statement.end }) + + if (statement.namespace !== undefined) { + namespaceBindings.set(statement.namespace, table) + continue + } + + const removed: Array = [] + for (const specifier of statement.specifiers) { + const replacement = table.get(specifier.imported) + if (replacement === undefined) continue + bindings.set(specifier.local, { replacement, local: specifier.local }) + removed.push(specifier) + rememberImports(needed, replacement) + } + if (removed.length === 0) continue + if (removed.length === statement.specifiers.length) { + edits.push({ start: statement.start, end: statement.end, text: "" }) + } else { + const kept = statement.specifiers.filter((specifier) => !removed.includes(specifier)) + edits.push({ + start: statement.clauseStart, + end: statement.clauseEnd, + text: `{ ${kept.map((specifier) => specifier.text).join(", ")} }` + }) + } + } + + const mask = maskSpans(maskNonCode(source), maskedSpans) + + for (const [local, binding] of bindings) { + for (const match of identifierSpans(mask, local)) { + const rendered = render(binding.replacement, source, match + local.length) + edits.push({ start: match, end: rendered.end, text: rendered.text }) + } + } + + for (const [namespace, table] of namespaceBindings) { + let used = false + for (const match of identifierSpans(mask, namespace)) { + const after = skipSpaces(source, match + namespace.length) + if (source[after] !== ".") continue + const propertyStart = after + 1 + const property = readIdentifier(source, propertyStart) + if (property === undefined) continue + const replacement = table.get(property.name) + if (replacement === undefined) { + used = true + continue + } + rememberImports(needed, replacement) + const rendered = render(replacement, source, propertyStart + property.name.length) + edits.push({ start: match, end: rendered.end, text: rendered.text }) + } + if (!used) { + const statement = imports.find((item) => item.namespace === namespace && byModule.has(item.module)) + if (statement !== undefined) edits.push({ start: statement.start, end: statement.end, text: "" }) + } + } + + return insertImports(applyEdits(source, edits), needed) +} + +const render = (replacement: NativeEffectReplacement, source: string, afterName: number): Rendered => { + const bare = { text: replacement.expression, end: afterName } + if (replacement.call === undefined) return bare + const open = skipSpaces(source, afterName) + if (source[open] !== "(") return bare + const args = splitArgs(source, open) + if (args === undefined) return bare + if (replacement.call === "chunk" && args.args.length === 2) { + return { + text: `Chunk.fromIterable(Array.chunksOf(${args.args[0]}, ${args.args[1]}))`, + end: args.end + } + } + if ((replacement.call === "date-add" || replacement.call === "date-sub") && args.args.length === 2) { + const method = replacement.call === "date-add" ? "add" : "subtract" + return { + text: `pipeZonedLocal(${args.args[0]}, DateTime.${method}({ ${replacement.expression}: ${args.args[1]} }))`, + end: args.end + } + } + return bare +} + +const rememberImports = (needed: Map>, replacement: NativeEffectReplacement) => { + for (const [local, from] of replacement.imports) { + const names = needed.get(from) ?? new Set() + names.add(local) + needed.set(from, names) + } +} + +interface Specifier { + readonly imported: string + readonly local: string + readonly text: string +} + +interface ImportStatement { + readonly start: number + readonly end: number + readonly clauseStart: number + readonly clauseEnd: number + readonly module: string + readonly namespace?: string + readonly specifiers: ReadonlyArray +} + +const findImports = (source: string): ReadonlyArray => { + const found: Array = [] + const pattern = /\bimport\s+(type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["']/g + for (const match of source.matchAll(pattern)) { + const clause = match[2] ?? "" + const moduleName = match[3] ?? "" + if (!moduleNames.has(moduleName)) continue + const start = lineStart(source, match.index ?? 0) + const end = lineEnd(source, (match.index ?? 0) + match[0].length) + const clauseStart = (match.index ?? 0) + match[0].indexOf(clause) + const namespace = /^\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause.trim()) + found.push({ + start, + end, + clauseStart, + clauseEnd: clauseStart + clause.length, + module: moduleName, + ...(namespace?.[1] === undefined ? {} : { namespace: namespace[1] }), + specifiers: namespace === null ? parseSpecifiers(clause) : [] + }) + } + return found +} + +const parseSpecifiers = (clause: string): ReadonlyArray => { + const brace = clause.indexOf("{") + if (brace < 0) return [] + const end = clause.lastIndexOf("}") + const body = clause.slice(brace + 1, end) + return body.split(",").flatMap((part) => { + const text = part.trim() + if (text === "" || text.startsWith("type ")) return [] + const alias = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(text) + if (alias === null) return [] + return [{ imported: alias[1]!, local: alias[2] ?? alias[1]!, text }] + }) +} + +const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + +const identifierSpans = (mask: string, name: string): ReadonlyArray => { + const spans: Array = [] + const pattern = new RegExp(`(? { + const match = /^[A-Za-z_$][\w$]*/.exec(source.slice(index)) + return match === null ? undefined : { name: match[0] } +} + +const splitArgs = ( + source: string, + open: number +): { readonly args: ReadonlyArray; readonly end: number } | undefined => { + const balanced = readBalanced(source, open, "(", ")") + if (balanced === undefined) return undefined + const args: Array = [] + let depth = 0 + let quote: "'" | "\"" | "`" | undefined + let start = 0 + const body = balanced.body + for (let i = 0; i < body.length; i++) { + const char = body[i] + if (quote !== undefined) { + if (char === "\\") { + i++ + continue + } + if (char === quote) quote = undefined + continue + } + if (char === "'" || char === "\"" || char === "`") { + quote = char + continue + } + if (char === "(" || char === "[" || char === "{") depth++ + if (char === ")" || char === "]" || char === "}") depth-- + if (char === "," && depth === 0) { + args.push(body.slice(start, i).trim()) + start = i + 1 + } + } + const last = body.slice(start).trim() + if (last !== "") args.push(last) + return { args, end: balanced.end } +} + +const readBalanced = ( + source: string, + open: number, + left: string, + right: string +): { readonly body: string; readonly end: number } | undefined => { + if (source[open] !== left) return undefined + let depth = 0 + let quote: "'" | "\"" | "`" | undefined + for (let i = open; i < source.length; i++) { + const char = source[i] + if (quote !== undefined) { + if (char === "\\") { + i++ + continue + } + if (char === quote) quote = undefined + continue + } + if (char === "'" || char === "\"" || char === "`") { + quote = char + continue + } + if (char === left) depth++ + if (char === right) { + depth-- + if (depth === 0) return { body: source.slice(open + 1, i), end: i + 1 } + } + } + return undefined +} + +const maskSpans = (mask: string, spans: ReadonlyArray<{ start: number; end: number }>): string => { + const chars = [...mask] + for (const span of spans) { + for (let i = span.start; i < span.end && i < chars.length; i++) { + if (chars[i] !== "\n") chars[i] = " " + } + } + return chars.join("") +} + +const skipSpaces = (source: string, index: number): number => { + let i = index + while (i < source.length && (source[i] === " " || source[i] === "\n" || source[i] === "\t")) i++ + return i +} + +const lineStart = (source: string, index: number): number => { + const start = source.lastIndexOf("\n", index - 1) + return start < 0 ? 0 : start + 1 +} + +const lineEnd = (source: string, index: number): number => { + const end = source.indexOf("\n", index) + return end < 0 ? source.length : end + 1 +} + +const applyEdits = (source: string, edits: ReadonlyArray<{ start: number; end: number; text: string }>): string => { + const ordered = [...edits].sort((a, b) => b.start - a.start) + let next = source + for (const edit of ordered) next = next.slice(0, edit.start) + edit.text + next.slice(edit.end) + return next +} + +const insertImports = (source: string, needed: ReadonlyMap>): string => { + const lines: Array = [] + for (const [from, names] of needed) { + for (const name of names) { + if (hasImport(source, name, from)) continue + lines.push( + namedImportNames.has(name) + ? `import { ${name} } from "${from}"` + : `import * as ${name} from "${from}"` + ) + } + } + if (lines.length === 0) return source.replace(/^\n+/, "") + const lastImport = lastImportEnd(source) + const block = `${lines.sort().join("\n")}\n` + if (lastImport === 0) return `${block}${source.replace(/^\n+/, "")}` + return `${source.slice(0, lastImport)}${block}${source.slice(lastImport)}`.replace(/^\n+/, "") +} + +const namedImportNames = new Set(["absurd", "Date", "pipeZonedLocal"]) + +const hasImport = (source: string, name: string, from: string): boolean => { + const fromPattern = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + const namespace = new RegExp(`import\\s+\\*\\s+as\\s+${name}\\s+from\\s+["']${fromPattern}["']`) + const named = new RegExp(`import\\s+(type\\s+)?\\{[^}]*\\b${name}\\b[^}]*}\\s+from\\s+["']${fromPattern}["']`) + const barrel = name === name[0]?.toUpperCase() + ? new RegExp(`import\\s+(type\\s+)?\\{[^}]*\\b${name}\\b[^}]*}\\s+from\\s+["']effect["']`) + : undefined + return namespace.test(source) || named.test(source) || barrel?.test(source) === true +} + +const lastImportEnd = (source: string): number => { + const pattern = /\bimport\s+(type\s+)?[\s\S]*?\s+from\s+["'][^"']+["']/g + let end = 0 + for (const match of source.matchAll(pattern)) { + end = lineEnd(source, (match.index ?? 0) + match[0].length) + } + return end +} + +const maskNonCode = (source: string): string => { + const chars = [...source] + let i = 0 + const blank = (from: number, to: number) => { + for (let j = from; j < to; j++) { + if (chars[j] !== "\n") chars[j] = " " + } + } + while (i < source.length) { + const two = source.slice(i, i + 2) + if (two === "//") { + const end = source.indexOf("\n", i) + blank(i, end < 0 ? source.length : end) + i = end < 0 ? source.length : end + continue + } + if (two === "/*") { + const end = source.indexOf("*/", i + 2) + const stop = end < 0 ? source.length : end + 2 + blank(i, stop) + i = stop + continue + } + const char = source[i] + if (char === "'" || char === "\"") { + const stop = scanQuoted(source, i, char) + blank(i, stop) + i = stop + continue + } + if (char === "`") { + const stop = scanTemplate(source, i) + blank(i, stop) + i = stop + continue + } + i++ + } + return chars.join("") +} + +const scanQuoted = (source: string, start: number, quote: "'" | "\""): number => { + for (let i = start + 1; i < source.length; i++) { + if (source[i] === "\\") { + i++ + continue + } + if (source[i] === quote) return i + 1 + } + return source.length +} + +const scanTemplate = (source: string, start: number): number => { + for (let i = start + 1; i < source.length; i++) { + if (source[i] === "\\") { + i++ + continue + } + if (source[i] === "`") return i + 1 + if (source.slice(i, i + 2) === "${") { + let depth = 1 + i += 2 + while (i < source.length && depth > 0) { + if (source[i] === "{") depth++ + else if (source[i] === "}") depth-- + i++ + } + i-- + } + } + return source.length +} diff --git a/packages/effect-app/src/utils.ts b/packages/effect-app/src/utils.ts index ed46138208..6c96e8f55a 100644 --- a/packages/effect-app/src/utils.ts +++ b/packages/effect-app/src/utils.ts @@ -47,11 +47,6 @@ export function toString(v: unknown) { return `${v}` } -export const typedKeysOf = (obj: T) => Object.keys(obj) as (keyof T)[] -// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -export const typedValuesOf = (obj: T) => Object.values(obj) as ValueOf[] -type ValueOf = T[keyof T] - export type Constructor = { new(...args: any[]): T } export type ThenArg = T extends Promise ? U : T extends (...args: any[]) => Promise ? V @@ -133,14 +128,6 @@ export function dropUndefined( type GetTag = T extends { _tag: infer K } ? K : never export const isOfType = (tag: GetTag) => (e: { _tag: string }): e is T => e._tag === tag -export function capitalize(string: T): Capitalize { - return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize -} - -export function uncapitalize(string: T): Uncapitalize { - return (string.charAt(0).toLowerCase() + string.slice(1)) as Uncapitalize -} - export function pretty(o: unknown): string { return JSON.stringify(o, undefined, 2) ?? "undefined" } @@ -565,10 +552,6 @@ export function exhaustiveMatch_(t: T) { return any>>(handlers: Out): ReturnType => handlers[t](t) } -export function assertUnreachable(x: never): never { - throw new Error("Unknown case " + x) -} - export type OptPromise any> = ( ...args: Parameters ) => Promise> | ReturnType diff --git a/packages/effect-app/test/dateTimeLocal.test.ts b/packages/effect-app/test/dateTimeLocal.test.ts new file mode 100644 index 0000000000..924cd9f527 --- /dev/null +++ b/packages/effect-app/test/dateTimeLocal.test.ts @@ -0,0 +1,55 @@ +import { makeZonedLocal, pipeZonedLocal } from "effect-app/DateTime" +import * as DateTime from "effect/DateTime" +import { describe, expect, test } from "vitest" + +describe("local Date helpers", () => { + const start = new Date("2024-03-09T17:00:00.000Z") + + test("makeZonedLocal keeps the instant and uses the process zone", () => { + const zoned = makeZonedLocal(start) + expect(zoned.epochMilliseconds).toBe(start.getTime()) + expect(DateTime.zoneToString(zoned.zone)).toBe(DateTime.zoneToString(DateTime.zoneMakeLocal())) + }) + + test("pipeZonedLocal adds a local calendar day and returns the instant", () => { + const expected = new Date(start.getTime()) + expected.setDate(expected.getDate() + 1) + expect(pipeZonedLocal(start, DateTime.add({ days: 1 })).getTime()).toBe(expected.getTime()) + }) + + test("pipeZonedLocal runs every step before toDateUtc", () => { + const expected = new Date(start.getTime()) + expected.setDate(expected.getDate() + 1) + expected.setMonth(expected.getMonth() + 1) + const result = pipeZonedLocal(start, DateTime.add({ days: 1 }), DateTime.add({ months: 1 })) + expect(result.getTime()).toBe(expected.getTime()) + }) + + test("pipeZonedLocal accepts as many functions as pipe", () => { + const keep = (self: A) => self + const expected = new Date(start.getTime()) + expected.setDate(expected.getDate() + 1) + const result = pipeZonedLocal( + start, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + keep, + DateTime.add({ days: 1 }) + ) + expect(result.getTime()).toBe(expected.getTime()) + }) +}) diff --git a/packages/effect-app/test/nativeEffectMigration.test.ts b/packages/effect-app/test/nativeEffectMigration.test.ts new file mode 100644 index 0000000000..27e3c4eefa --- /dev/null +++ b/packages/effect-app/test/nativeEffectMigration.test.ts @@ -0,0 +1,77 @@ +import { migrateEffectAppSource } from "effect-app/migration/nativeEffect" +import { describe, expect, test } from "vitest" + +describe("migrateEffectAppSource", () => { + test("rewrites named helpers and keeps the other imports", () => { + const source = ` +import { typedKeysOf, pretty } from "effect-app/utils" +import { tuple } from "effect-app/Function" +import { toBool, fromBool } from "effect-app/Option" + +const keys = typedKeysOf(value) +const pair = tuple(keys, pretty(value)) +const present = toBool(option) +const flag = fromBool(enabled) +` + const migrated = migrateEffectAppSource(source) + expect(migrated).toContain(`import { pretty } from "effect-app/utils"`) + expect(migrated).toContain(`import { fromBool } from "effect-app/Option"`) + expect(migrated).toContain(`import * as Struct from "effect/Struct"`) + expect(migrated).toContain(`import * as Tuple from "effect/Tuple"`) + expect(migrated).toContain(`import * as Option from "effect/Option"`) + expect(migrated).toContain("const keys = Struct.keys(value)") + expect(migrated).toContain("const pair = Tuple.make(keys, pretty(value))") + expect(migrated).toContain("const present = Option.isSome(option)") + expect(migrated).toContain("const flag = fromBool(enabled)") + expect(migrated).not.toContain("typedKeysOf") + expect(migrated).not.toContain("effect-app/Function") + }) + + test("rewrites aliases, namespaces, calls, and leaves strings and comments", () => { + const source = ` +import { dropUndefined as compact, assertUnreachable as unreachable } from "effect-app/utils" +import * as Dates from "effect-app/_ext/date" +import { chunk_, toNonEmptyArray } from "effect-app/Array" +import { DateValid } from "effect-app/Schema" + +// typedKeysOf stays in this comment +const label = "tuple(typedKeysOf)" +const fields = compact({ a: 1, b: undefined }) +const next = Dates.DateAddDays(today, 2) +const parts = chunk_(items, 10) +const some = toNonEmptyArray(items) +const Day = DateValid +const missing = unreachable(value) +` + const migrated = migrateEffectAppSource(source) + expect(migrated).toContain("// typedKeysOf stays in this comment") + expect(migrated).toContain(`const label = "tuple(typedKeysOf)"`) + expect(migrated).toContain("const fields = compact({ a: 1, b: undefined })") + expect(migrated).toContain("dropUndefined as compact") + expect(migrated).toContain( + "const next = pipeZonedLocal(today, DateTime.add({ days: 2 }))" + ) + expect(migrated).toContain("const parts = Chunk.fromIterable(Array.chunksOf(items, 10))") + expect(migrated).toContain("const some = Option.liftPredicate(Array.isReadonlyArrayNonEmpty)(items)") + expect(migrated).toContain(`import { Date } from "effect-app/Schema"`) + expect(migrated).toContain("const Day = Date") + expect(migrated).toContain("const missing = absurd(value)") + expect(migrated).toContain(`import { pipeZonedLocal } from "effect-app/DateTime"`) + expect(migrated).toContain(`import * as DateTime from "effect/DateTime"`) + expect(migrated).not.toContain("effect-app/_ext/date") + expect(migrated).not.toContain("DateValid") + }) + + test("rewrites a namespace member and keeps the namespace when it still has other members", () => { + const source = ` +import * as Chunk from "effect-app/Chunk" + +const first = Chunk.findFirstSimple(chunk, predicate) +const grouped = Chunk.groupByTChunk_(chunk, key) +` + const migrated = migrateEffectAppSource(source) + expect(migrated).toContain(`import * as Chunk from "effect-app/Chunk"`) + expect(migrated).toContain("const first = Chunk.findFirst(chunk, predicate)") + expect(migrated).toContain("const grouped = Chunk.groupByTChunk_(chunk, key)") + }) +}) diff --git a/packages/infra/src/Store/Cosmos.ts b/packages/infra/src/Store/Cosmos.ts index a3f9fa9457..16e9211056 100644 --- a/packages/infra/src/Store/Cosmos.ts +++ b/packages/infra/src/Store/Cosmos.ts @@ -2,7 +2,6 @@ import * as Array from "effect-app/Array" import type { NonEmptyReadonlyArray } from "effect-app/Array" -import { toNonEmptyArray } from "effect-app/Array" import * as Effect from "effect-app/Effect" import * as Layer from "effect-app/Layer" import type { FieldValues } from "effect-app/Model/filter/types" @@ -165,7 +164,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ autoscaleMaxThroughput, pr Effect.andThen(seed!), Effect.flatMap((m) => Effect.flatMapOption( - Effect.succeed(toNonEmptyArray([...m])), + Effect.succeed(Option.liftPredicate(Array.isReadonlyArrayNonEmpty)([...m])), (a) => bulkSetInternal(a, ns).pipe(Effect.orDie) ) ), diff --git a/packages/infra/src/Store/Cosmos/query.ts b/packages/infra/src/Store/Cosmos/query.ts index 4c4f7e9eb3..78597438b0 100644 --- a/packages/infra/src/Store/Cosmos/query.ts +++ b/packages/infra/src/Store/Cosmos/query.ts @@ -6,7 +6,7 @@ import * as Effect from "effect-app/Effect" import type { FilterR, FilterResult, Ops } from "effect-app/Model/filter/filterApi" import type { AggregateIrExpression, ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "effect-app/Model/query" import type { SupportedValues } from "effect-app/Store" -import { assertUnreachable } from "effect-app/utils" +import { absurd } from "effect/Function" import { InfraLogger } from "../../logger.ts" import { isRelationCheck } from "../codeFilter.ts" import { jsonifyFilter, type JsonLower } from "../utils.ts" @@ -178,7 +178,7 @@ export function buildWhereCosmosQuery3( ? `IS_NULL(${k}) = true` : `${k} = ${v}` default: { - return assertUnreachable(x.op) + return absurd(x.op) } } } @@ -389,7 +389,7 @@ export function buildWhereCosmosQuery3( return `MAX(${fieldRef}) AS ${key}` } default: - return assertUnreachable(agg) + return absurd(agg) } } @@ -404,7 +404,7 @@ export function buildWhereCosmosQuery3( case "mul": return `(${compileExpr(expression.left)} * ${compileExpr(expression.right)})` default: - return assertUnreachable(expression) + return absurd(expression) } } const factorExpr = (unitExpr: string, toBase: string, factors: Readonly>) => { diff --git a/packages/infra/src/Store/Memory.ts b/packages/infra/src/Store/Memory.ts index 37c5233930..7a42160f65 100644 --- a/packages/infra/src/Store/Memory.ts +++ b/packages/infra/src/Store/Memory.ts @@ -8,8 +8,7 @@ import type { FieldValues } from "effect-app/Model/filter/types" import type { AggregateIrExpression, ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "effect-app/Model/query" import * as Option from "effect-app/Option" import { type FilterArgs, type PersistenceModelType, type Store, type StoreConfig, storeId, StoreMaker } from "effect-app/Store" -import { assertUnreachable } from "effect-app/utils" -import { flow, pipe } from "effect/Function" +import { absurd, flow, pipe } from "effect/Function" import * as Order from "effect/Order" import * as Ref from "effect/Ref" import * as Result from "effect/Result" @@ -59,7 +58,7 @@ const emptyValueFor = (tag: ComputedProjectionIrExpression["_tag"]) => { case "relation-length": return 0 default: - return assertUnreachable(tag) + return absurd(tag) } } @@ -89,7 +88,7 @@ const computeProjectionValue = ( case "mul": return evalExpr(value, expression.left) * evalExpr(value, expression.right) default: - return assertUnreachable(expression) + return absurd(expression) } } switch (computed._tag) { @@ -166,7 +165,7 @@ const computeProjectionValue = ( return out } default: - return assertUnreachable(computed) + return absurd(computed) } } @@ -203,7 +202,7 @@ const computeAggregateValue = (rows: readonly T[], agg: A return max ?? null } default: - return assertUnreachable(agg) + return absurd(agg) } } diff --git a/packages/infra/src/Store/SQL.ts b/packages/infra/src/Store/SQL.ts index ab3ae95f3a..b14ce615c3 100644 --- a/packages/infra/src/Store/SQL.ts +++ b/packages/infra/src/Store/SQL.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { NonEmptyReadonlyArray } from "effect-app/Array" -import { toNonEmptyArray } from "effect-app/Array" +import { isReadonlyArrayNonEmpty, type NonEmptyReadonlyArray } from "effect-app/Array" import * as Effect from "effect-app/Effect" import type { FieldValues } from "effect-app/Model/filter/types" import type { ComputedProjectionIrExpression } from "effect-app/Model/query" @@ -196,7 +195,7 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType: if ((existing as any[]).length > 0) return yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`) const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie) - const ne = toNonEmptyArray([...items]) + const ne = Option.liftPredicate(isReadonlyArrayNonEmpty)([...items]) // Seed inserts are not concurrent; OCC here is a programming defect. if (Option.isSome(ne)) { yield* bulkSetInternal(ne.value, ns).pipe( @@ -550,7 +549,7 @@ function makeSQLiteStorePerNs( if ((existing as any[]).length > 0) return yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`) const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie) - const ne = toNonEmptyArray([...items]) + const ne = Option.liftPredicate(isReadonlyArrayNonEmpty)([...items]) // Seed inserts are not concurrent; OCC here is a programming defect. if (Option.isSome(ne)) { yield* bulkSetInternal(ne.value, ns).pipe( diff --git a/packages/infra/src/Store/SQL/Pg.ts b/packages/infra/src/Store/SQL/Pg.ts index 98d9e045d2..0648c486f2 100644 --- a/packages/infra/src/Store/SQL/Pg.ts +++ b/packages/infra/src/Store/SQL/Pg.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { NonEmptyReadonlyArray } from "effect-app/Array" -import { toNonEmptyArray } from "effect-app/Array" +import { isReadonlyArrayNonEmpty, type NonEmptyReadonlyArray } from "effect-app/Array" import * as Effect from "effect-app/Effect" import type { FieldValues } from "effect-app/Model/filter/types" import type { ComputedProjectionIrExpression } from "effect-app/Model/query" @@ -178,7 +177,7 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) { if ((existing as any[]).length > 0) return yield* InfraLogger.logInfo(`Seeding data for ${name} (namespace: ${ns})`) const items = yield* seed.pipe(Effect.provide(ctx), Effect.orDie) - const ne = toNonEmptyArray([...items]) + const ne = Option.liftPredicate(isReadonlyArrayNonEmpty)([...items]) // Seed inserts are not concurrent; OCC here is a programming defect. if (Option.isSome(ne)) { yield* bulkSetInternal(ne.value, ns).pipe( diff --git a/packages/infra/src/Store/SQL/query.ts b/packages/infra/src/Store/SQL/query.ts index 00117a3502..cd97d04d02 100644 --- a/packages/infra/src/Store/SQL/query.ts +++ b/packages/infra/src/Store/SQL/query.ts @@ -3,7 +3,7 @@ import type { NonEmptyReadonlyArray } from "effect-app/Array" import * as Effect from "effect-app/Effect" import type { FilterR, FilterResult } from "effect-app/Model/filter/filterApi" import type { AggregateIrExpression, ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "effect-app/Model/query" -import { assertUnreachable } from "effect-app/utils" +import { absurd } from "effect/Function" import { InfraLogger } from "../../logger.ts" import { isRelationCheck } from "../codeFilter.ts" import { jsonifyFilter, type JsonLower } from "../utils.ts" @@ -473,7 +473,7 @@ export function buildWhereSQLQuery( return `${k} = ${v}` } default: - return assertUnreachable(x.op) + return absurd(x.op) } } @@ -555,7 +555,7 @@ export function buildWhereSQLQuery( case "mul": return `(${compileExpr(expression.left)} * ${compileExpr(expression.right)})` default: - return assertUnreachable(expression) + return absurd(expression) } } const factorCaseExpr = (unitExpr: string, toBase: string, factors: Readonly>) => { @@ -644,7 +644,7 @@ export function buildWhereSQLQuery( return `(SELECT COALESCE(jsonb_agg(__v), '[]'::jsonb) FROM (${unionQuery}) t) AS "${key}"` } default: - return assertUnreachable(computed) + return absurd(computed) } } @@ -664,7 +664,7 @@ export function buildWhereSQLQuery( case "agg-max": return `MAX(${fieldExpr(agg.field)}) AS "${key}"` default: - return assertUnreachable(agg) + return absurd(agg) } } diff --git a/packages/infra/src/Store/codeFilter.ts b/packages/infra/src/Store/codeFilter.ts index 5de521c4bc..5141dd4903 100644 --- a/packages/infra/src/Store/codeFilter.ts +++ b/packages/infra/src/Store/codeFilter.ts @@ -5,7 +5,7 @@ import type { FilterR, FilterResult } from "effect-app/Model/filter/filterApi" import type { FieldValues } from "effect-app/Model/filter/types" import * as Option from "effect-app/Option" import type { Filter } from "effect-app/Store" -import { assertUnreachable } from "effect-app/utils" +import { absurd } from "effect/Function" import { compare, get, greaterThan, greaterThanExclusive, lowerThan, lowerThanExclusive, toJsonQueryValue } from "./utils.ts" const vAsArr = (v: unknown) => toJsonQueryValue(v) as any[] @@ -103,7 +103,7 @@ const filterStatement = (x: any, p: FilterR) => { case undefined: return compare(k, v) default: { - return assertUnreachable(p.op) + return absurd(p.op) } } } diff --git a/packages/infra/src/errorReporter.ts b/packages/infra/src/errorReporter.ts index 2afb3be412..ab7838df91 100644 --- a/packages/infra/src/errorReporter.ts +++ b/packages/infra/src/errorReporter.ts @@ -74,7 +74,9 @@ export function logError(name: string) { return Effect.fnUntraced( function*(cause: Cause.Cause, extras?: Record) { if (Cause.hasInterruptsOnly(cause)) { - yield* InfraLogger.logDebug("Interrupted").pipe(Effect.annotateLogs(dropUndefined({ extras }))) + yield* InfraLogger.logDebug("Interrupted").pipe( + Effect.annotateLogs(dropUndefined({ extras })) + ) return } yield* InfraLogger diff --git a/packages/infra/src/routing.ts b/packages/infra/src/routing.ts index 555683449f..a3034e9459 100644 --- a/packages/infra/src/routing.ts +++ b/packages/infra/src/routing.ts @@ -14,12 +14,13 @@ import { Invalidation } from "effect-app/rpc" import { type GetEffectContext, type GetEffectError, type RpcContextMap } from "effect-app/rpc/RpcContextMap" import * as S from "effect-app/Schema" import { type TypeTestId } from "effect-app/TypeTest" -import { typedKeysOf, typedValuesOf } from "effect-app/utils" import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" import * as Ref from "effect/Ref" import { Rpc, RpcGroup, type RpcSerialization, RpcServer } from "effect/rpc" import type * as Scope from "effect/Scope" import * as Stream from "effect/Stream" +import * as Struct from "effect/Struct" import { type LayerUtils } from "./layerUtils.ts" import { RequestType as RequestTypeAnnotation } from "./routing/middleware.ts" @@ -321,14 +322,14 @@ export const makeRouter = = Layer.Layer const meta = getMeta(rsc) type RequestModules = FilterRequestModules - const requestModules = typedKeysOf(rsc).reduce((acc, cur) => { + const requestModules = Struct.keys(rsc).reduce((acc, cur) => { if (Predicate.isObjectKeyword(rsc[cur]) && rsc[cur]["success"]) { acc[cur as keyof RequestModules] = rsc[cur] } return acc }, {} as RequestModules) - const routeMatcher = typedKeysOf(requestModules).reduce( + const routeMatcher = Struct.keys(requestModules).reduce( (prev, cur) => { ;(prev as any)[cur] = Object.assign((handlerImpl: any) => { // handlerImpl is the actual handler implementation @@ -398,7 +399,7 @@ export const makeRouter = = Layer.Layer > > } = (impl: Record) => - typedKeysOf(impl).reduce((acc, cur) => { + Struct.keys(impl).reduce((acc, cur) => { acc[cur] = "raw" in impl[cur] ? routeMatcher[cur].raw(impl[cur].raw) : routeMatcher[cur](impl[cur]) return acc }, {} as any) @@ -437,7 +438,7 @@ export const makeRouter = = Layer.Layer const mw = meta.middleware as any // return make.pipe(Effect.map((c) => controllers(c, dependencies))) - const mapped = typedKeysOf(requestModules).reduce((acc, cur) => { + const mapped = Struct.keys(requestModules).reduce((acc, cur) => { const handler = controllers[cur as keyof typeof controllers] const resource = rsc[cur] @@ -623,7 +624,7 @@ export const makeRouter = = Layer.Layer const rpcs = RpcGroup .make( - ...typedValuesOf(mapped).map(([resource]) => { + ...(Object.values(mapped) as Array<(typeof mapped)[keyof typeof mapped]>).map(([resource]) => { const isStream = resource.stream const isCommand = resource.type === "command" return (isCommand @@ -654,10 +655,13 @@ export const makeRouter = = Layer.Layer const rpc = rpcs .toLayer(Effect.gen(function*() { - return typedValuesOf(mapped).reduce((acc, [resource, handler]) => { - acc[`${meta.moduleName}.${resource._tag}`] = handler - return acc - }, {} as Record) as any // TODO + return (Object.values(mapped) as Array<(typeof mapped)[keyof typeof mapped]>).reduce( + (acc, [resource, handler]) => { + acc[`${meta.moduleName}.${resource._tag}`] = handler + return acc + }, + {} as Record + ) as any // TODO })) as unknown as Layer.Layer< { [K in keyof RequestModules]: Rpc.Handler }, MakeE, @@ -774,7 +778,7 @@ export const makeRouter = = Layer.Layer >( handlers: T ) { - const routers = typedValuesOf(handlers) + const routers = Record.values(handlers) return Layer.mergeAll(...routers as [any]) as unknown as Layer.Layer< never, diff --git a/packages/vue/src/errorReporter.ts b/packages/vue/src/errorReporter.ts index 01e95ad98b..cabd05214b 100644 --- a/packages/vue/src/errorReporter.ts +++ b/packages/vue/src/errorReporter.ts @@ -71,7 +71,9 @@ export function logError(name: string) { return Effect.fnUntraced( function*(cause: Cause.Cause, extras?: Record) { if (Cause.hasInterruptsOnly(cause)) { - yield* Effect.logDebug("Interrupted").pipe(Effect.annotateLogs(dropUndefined({ extras }))) + yield* Effect.logDebug("Interrupted").pipe( + Effect.annotateLogs(dropUndefined({ extras })) + ) return } yield* Effect diff --git a/packages/vue/src/makeIntl.ts b/packages/vue/src/makeIntl.ts index 7d612bb65f..085b714ab0 100644 --- a/packages/vue/src/makeIntl.ts +++ b/packages/vue/src/makeIntl.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-object-type */ import { createIntl, createIntlCache, type Formatters, type IntlFormatters, type MessageDescriptor, type ResolvedIntlConfig } from "@formatjs/intl" -import { typedKeysOf } from "effect-app/utils" +import * as Struct from "effect/Struct" import { type FormatXMLElementFn, type Options as IntlMessageFormatOptions, type PrimitiveType } from "intl-messageformat" import { type Ref, watch } from "vue" import { translate } from "./form.ts" @@ -20,7 +20,7 @@ export const makeIntl = ( ) => { const intlCache = createIntlCache() - const intls = typedKeysOf(messages).reduce( + const intls = Struct.keys(messages).reduce( (acc, cur) => { acc[cur] = createIntl( { diff --git a/packages/vue/src/mutate.ts b/packages/vue/src/mutate.ts index b966954df4..a632b536e1 100644 --- a/packages/vue/src/mutate.ts +++ b/packages/vue/src/mutate.ts @@ -3,7 +3,6 @@ import { DataDependencies, type InvalidationKey, InvalidationKeysFromServer, mak import type { ClientForOptions, RequestHandlerWithInput } from "effect-app/client/clientFor" import type { InvalidateQueryInstruction } from "effect-app/client/makeClient" import * as Effect from "effect-app/Effect" -import { tuple } from "effect-app/Function" import * as Option from "effect-app/Option" import { isReadonlyArrayNonEmpty } from "effect/Array" import type * as Cause from "effect/Cause" @@ -12,6 +11,7 @@ import * as AsyncResult from "effect/reactivity/AsyncResult" import type * as Reactivity from "effect/reactivity/Reactivity" import * as Ref from "effect/Ref" import * as Stream from "effect/Stream" +import * as Tuple from "effect/Tuple" import { computed, type ComputedRef, shallowRef } from "vue" import { invalidateAndAwait, invalidateSoft } from "./atomQuery.ts" import { getDerivedInvalidationKeys, partitionInvalidationKeys } from "./dependencyMetadata.ts" @@ -102,7 +102,7 @@ export function make(self: Effect.Effect) { const latestSuccess = computed(() => Option.getOrUndefined(AsyncResult.value(result.value))) - return tuple(result, latestSuccess, execute) + return Tuple.make(result, latestSuccess, execute) } /** @@ -221,7 +221,7 @@ export const asResult = ( )) ) - return tuple(computed(() => state.value), act) + return Tuple.make(computed(() => state.value), act) } /** @@ -268,7 +268,7 @@ export const asStreamResult = ( const act = (...args: Args) => runStream(handler(...args)) - return tuple(computed(() => state.value), act) + return Tuple.make(computed(() => state.value), act) } const buildInvalidateCache = ( diff --git a/packages/vue/src/routeParams.ts b/packages/vue/src/routeParams.ts index 0575f25aff..67341996b8 100644 --- a/packages/vue/src/routeParams.ts +++ b/packages/vue/src/routeParams.ts @@ -1,7 +1,7 @@ import * as Option from "effect-app/Option" import * as S from "effect-app/Schema" -import { typedKeysOf } from "effect-app/utils" import { flow } from "effect/Function" +import * as Struct from "effect/Struct" import type { ParsedQuery } from "query-string" export function getQueryParam(search: ParsedQuery, param: string) { @@ -18,10 +18,10 @@ export function parseRouteParamsOption } { - return typedKeysOf(t).reduce( + return Struct.keys(t).reduce( (prev, cur) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - prev[cur] = getQueryParamO(query, cur as string).pipe( + prev[cur] = getQueryParamO(query, cur).pipe( Option.flatMap(S.decodeUnknownOption(t[cur]!)) ) @@ -39,7 +39,7 @@ export function parseRouteParams>>( ): { [K in keyof NER]: NER[K]["Type"] } { - return typedKeysOf(t).reduce( + return Struct.keys(t).reduce( (prev, cur) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion prev[cur] = S.decodeUnknownSync(t[cur]!)( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25a5739577..0079d4d2c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,9 +19,6 @@ patchedDependencies: importers: .: dependencies: - date-fns: - specifier: ^4.4.0 - version: 4.4.0 fast-check: specifier: ^4.9.0 version: 4.9.0 @@ -188,9 +185,6 @@ importers: "@tsconfig/strictest": specifier: ^2.0.8 version: 2.0.8 - date-fns: - specifier: ^4.4.0 - version: 4.4.0 effect: specifier: ^4.0.1 version: 4.0.1 @@ -5383,11 +5377,6 @@ packages: } engines: { node: ">= 0.4" } - date-fns@4.4.0: - resolution: { - integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==, - } - de-indent@1.0.2: resolution: { integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==, @@ -12637,8 +12626,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - date-fns@4.4.0: {} - de-indent@1.0.2: {} debug@3.2.7(supports-color@8.1.1):