@timothyrusso/effect-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/config/makeConfig.d.ts +43 -0
- package/dist/config/makeConfig.d.ts.map +1 -0
- package/dist/config/makeConfig.js +44 -0
- package/dist/config/makeConfig.js.map +1 -0
- package/dist/errors/appError.d.ts +55 -0
- package/dist/errors/appError.d.ts.map +1 -0
- package/dist/errors/appError.js +31 -0
- package/dist/errors/appError.js.map +1 -0
- package/dist/errors/messageKeys.d.ts +36 -0
- package/dist/errors/messageKeys.d.ts.map +1 -0
- package/dist/errors/messageKeys.js +26 -0
- package/dist/errors/messageKeys.js.map +1 -0
- package/dist/errors/unexpected.d.ts +26 -0
- package/dist/errors/unexpected.d.ts.map +1 -0
- package/dist/errors/unexpected.js +16 -0
- package/dist/errors/unexpected.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/logger.d.ts +25 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +27 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/react/EffectRuntimeProvider.d.ts +22 -0
- package/dist/react/EffectRuntimeProvider.d.ts.map +1 -0
- package/dist/react/EffectRuntimeProvider.js +17 -0
- package/dist/react/EffectRuntimeProvider.js.map +1 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +9 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/register.d.ts +30 -0
- package/dist/react/register.d.ts.map +1 -0
- package/dist/react/register.js +2 -0
- package/dist/react/register.js.map +1 -0
- package/dist/react/runAtBoundary.d.ts +13 -0
- package/dist/react/runAtBoundary.d.ts.map +1 -0
- package/dist/react/runAtBoundary.js +23 -0
- package/dist/react/runAtBoundary.js.map +1 -0
- package/dist/react/useEffectMutation.d.ts +19 -0
- package/dist/react/useEffectMutation.d.ts.map +1 -0
- package/dist/react/useEffectMutation.js +21 -0
- package/dist/react/useEffectMutation.js.map +1 -0
- package/dist/react/useEffectQuery.d.ts +20 -0
- package/dist/react/useEffectQuery.d.ts.map +1 -0
- package/dist/react/useEffectQuery.js +22 -0
- package/dist/react/useEffectQuery.js.map +1 -0
- package/dist/react/useErrorMessage.d.ts +13 -0
- package/dist/react/useErrorMessage.d.ts.map +1 -0
- package/dist/react/useErrorMessage.js +15 -0
- package/dist/react/useErrorMessage.js.map +1 -0
- package/dist/runtime/makeAppRuntime.d.ts +38 -0
- package/dist/runtime/makeAppRuntime.d.ts.map +1 -0
- package/dist/runtime/makeAppRuntime.js +26 -0
- package/dist/runtime/makeAppRuntime.js.map +1 -0
- package/dist/sqlite/migrations.d.ts +33 -0
- package/dist/sqlite/migrations.d.ts.map +1 -0
- package/dist/sqlite/migrations.js +49 -0
- package/dist/sqlite/migrations.js.map +1 -0
- package/dist/sqlite/sqliteClient.d.ts +63 -0
- package/dist/sqlite/sqliteClient.d.ts.map +1 -0
- package/dist/sqlite/sqliteClient.js +37 -0
- package/dist/sqlite/sqliteClient.js.map +1 -0
- package/dist/testing/clock.d.ts +16 -0
- package/dist/testing/clock.d.ts.map +1 -0
- package/dist/testing/clock.js +18 -0
- package/dist/testing/clock.js.map +1 -0
- package/dist/testing/collectLogs.d.ts +32 -0
- package/dist/testing/collectLogs.d.ts.map +1 -0
- package/dist/testing/collectLogs.js +31 -0
- package/dist/testing/collectLogs.js.map +1 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/nodeSqlite.d.ts +27 -0
- package/dist/testing/nodeSqlite.d.ts.map +1 -0
- package/dist/testing/nodeSqlite.js +74 -0
- package/dist/testing/nodeSqlite.js.map +1 -0
- package/dist/testing/runTest.d.ts +25 -0
- package/dist/testing/runTest.d.ts.map +1 -0
- package/dist/testing/runTest.js +18 -0
- package/dist/testing/runTest.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AnyAppError } from '../errors/appError.js';
|
|
2
|
+
import type { UnexpectedError } from '../errors/unexpected.js';
|
|
3
|
+
/**
|
|
4
|
+
* The app registers its runtime's services and its closed error union here, once, so
|
|
5
|
+
* `useEffectQuery` and `useEffectMutation` accept only `Effect<A, AppError, AppServices>` and
|
|
6
|
+
* type `error` as `AppError`:
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* declare module '@timothyrusso/effect-core/react' {
|
|
11
|
+
* interface Register {
|
|
12
|
+
* services: AppServices;
|
|
13
|
+
* error: AppError;
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export interface Register {
|
|
19
|
+
}
|
|
20
|
+
/** The registered `AppServices`, or `never` until the app registers them. */
|
|
21
|
+
export type RegisteredServices = Register extends {
|
|
22
|
+
readonly services: infer R;
|
|
23
|
+
} ? R : never;
|
|
24
|
+
/** The registered `AppError` union, or any app error until the app registers one. */
|
|
25
|
+
export type RegisteredError = Register extends {
|
|
26
|
+
readonly error: infer E extends AnyAppError;
|
|
27
|
+
} ? E : AnyAppError;
|
|
28
|
+
/** The `error` a hook reports: the registered union, plus the `UnexpectedError` a defect becomes. */
|
|
29
|
+
export type EffectHookError = RegisteredError | UnexpectedError;
|
|
30
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/react/register.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,QAAQ;CAAG;AAE5B,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAAG,QAAQ,SAAS;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAC;AAE7F,qFAAqF;AACrF,MAAM,MAAM,eAAe,GAAG,QAAQ,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,WAAW,CAAA;CAAE,GAAG,CAAC,GAAG,WAAW,CAAC;AAEjH,qGAAqG;AACrG,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/react/register.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { type LogContext } from '../logger/logger.js';
|
|
3
|
+
import type { ProvidedRuntime } from './EffectRuntimeProvider.js';
|
|
4
|
+
import type { RegisteredError, RegisteredServices } from './register.js';
|
|
5
|
+
/**
|
|
6
|
+
* Runs a hook's Effect on the app runtime and settles the promise TanStack Query expects. A defect
|
|
7
|
+
* becomes an `UnexpectedError` with the defect as `cause`. Each failure is logged once: an
|
|
8
|
+
* `UnexpectedError` through `Logger.error`, a typed failure through `Logger.warn` with its tag.
|
|
9
|
+
* The promise rejects with the error itself, not a `FiberFailure`. Aborting `signal` interrupts
|
|
10
|
+
* the fiber.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runAtBoundary<A>(runtime: ProvidedRuntime, effect: Effect.Effect<A, RegisteredError, RegisteredServices>, context: LogContext, signal?: AbortSignal): Promise<A>;
|
|
13
|
+
//# sourceMappingURL=runAtBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runAtBoundary.d.ts","sourceRoot":"","sources":["../../src/react/runAtBoundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAErD,OAAO,EAAE,KAAK,UAAU,EAAU,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEzE;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,eAAe,EAAE,kBAAkB,CAAC,EAC7D,OAAO,EAAE,UAAU,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC,CAgBZ"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Option } from 'effect';
|
|
2
|
+
import { UnexpectedError } from '../errors/unexpected.js';
|
|
3
|
+
import { Logger } from '../logger/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Runs a hook's Effect on the app runtime and settles the promise TanStack Query expects. A defect
|
|
6
|
+
* becomes an `UnexpectedError` with the defect as `cause`. Each failure is logged once: an
|
|
7
|
+
* `UnexpectedError` through `Logger.error`, a typed failure through `Logger.warn` with its tag.
|
|
8
|
+
* The promise rejects with the error itself, not a `FiberFailure`. Aborting `signal` interrupts
|
|
9
|
+
* the fiber.
|
|
10
|
+
*/
|
|
11
|
+
export async function runAtBoundary(runtime, effect, context, signal) {
|
|
12
|
+
const program = effect.pipe(Effect.catchAllDefect(defect => Effect.fail(new UnexpectedError({ cause: defect }))), Effect.tapError(error => Effect.flatMap(Logger, logger => error instanceof UnexpectedError
|
|
13
|
+
? logger.error(error, context)
|
|
14
|
+
: logger.warn(error._tag, { ...context, error }))));
|
|
15
|
+
const exit = await runtime.runPromiseExit(program, signal === undefined ? {} : { signal });
|
|
16
|
+
if (Exit.isSuccess(exit))
|
|
17
|
+
return exit.value;
|
|
18
|
+
const failure = Cause.failureOption(exit.cause);
|
|
19
|
+
if (Option.isSome(failure))
|
|
20
|
+
throw failure.value;
|
|
21
|
+
throw Cause.squash(exit.cause);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=runAtBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runAtBoundary.js","sourceRoot":"","sources":["../../src/react/runAtBoundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAmB,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAI9D;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAwB,EACxB,MAA6D,EAC7D,OAAmB,EACnB,MAAoB;IAEpB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CACzB,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EACpF,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CACtB,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAC9B,KAAK,YAAY,eAAe;QAC9B,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC;QAC9B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,CAAC,CACnD,CACF,CACF,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3F,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC;IAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChD,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,MAAM,OAAO,CAAC,KAAK,CAAC;IAChD,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type UseMutationOptions, type UseMutationResult } from '@tanstack/react-query';
|
|
2
|
+
import type { Effect } from 'effect';
|
|
3
|
+
import type { EffectHookError, RegisteredError, RegisteredServices } from './register.js';
|
|
4
|
+
/** `useMutation` options, with `mutationFn` returning an Effect instead of a promise. */
|
|
5
|
+
export interface UseEffectMutationOptions<A, TVariables = void, TOnMutateResult = unknown> extends Omit<UseMutationOptions<A, EffectHookError, TVariables, TOnMutateResult>, 'mutationFn'> {
|
|
6
|
+
/** The use case to run for `variables`. It may need only the registered `AppServices`. */
|
|
7
|
+
readonly mutationFn: (variables: TVariables) => Effect.Effect<A, RegisteredError, RegisteredServices>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `useMutation` for an Effect. Failures reach `error` and `onError` as an app error, a defect as
|
|
11
|
+
* an `UnexpectedError`, and each is logged once through `Logger`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const rename = useEffectMutation({ mutationFn: (name: string) => renameTrip(id, name) });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function useEffectMutation<A, TVariables = void, TOnMutateResult = unknown>(options: UseEffectMutationOptions<A, TVariables, TOnMutateResult>): UseMutationResult<A, EffectHookError, TVariables, TOnMutateResult>;
|
|
19
|
+
//# sourceMappingURL=useEffectMutation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectMutation.d.ts","sourceRoot":"","sources":["../../src/react/useEffectMutation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAe,MAAM,uBAAuB,CAAC;AACrG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAG1F,yFAAyF;AACzF,MAAM,WAAW,wBAAwB,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,EAAE,eAAe,GAAG,OAAO,CACvF,SAAQ,IAAI,CAAC,kBAAkB,CAAC,CAAC,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,CAAC,EAAE,YAAY,CAAC;IAC/F,0FAA0F;IAC1F,QAAQ,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,eAAe,EAAE,kBAAkB,CAAC,CAAC;CACvG;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,EAAE,eAAe,GAAG,OAAO,EAC/E,OAAO,EAAE,wBAAwB,CAAC,CAAC,EAAE,UAAU,EAAE,eAAe,CAAC,GAChE,iBAAiB,CAAC,CAAC,EAAE,eAAe,EAAE,UAAU,EAAE,eAAe,CAAC,CAOpE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { useAppRuntime } from './EffectRuntimeProvider.js';
|
|
3
|
+
import { runAtBoundary } from './runAtBoundary.js';
|
|
4
|
+
/**
|
|
5
|
+
* `useMutation` for an Effect. Failures reach `error` and `onError` as an app error, a defect as
|
|
6
|
+
* an `UnexpectedError`, and each is logged once through `Logger`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const rename = useEffectMutation({ mutationFn: (name: string) => renameTrip(id, name) });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function useEffectMutation(options) {
|
|
14
|
+
const runtime = useAppRuntime();
|
|
15
|
+
const { mutationFn, ...rest } = options;
|
|
16
|
+
return useMutation({
|
|
17
|
+
...rest,
|
|
18
|
+
mutationFn: variables => runAtBoundary(runtime, mutationFn(variables), { mutationKey: rest.mutationKey }),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=useEffectMutation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectMutation.js","sourceRoot":"","sources":["../../src/react/useEffectMutation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmD,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAErG,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AASnD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAiE;IAEjE,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IACxC,OAAO,WAAW,CAAkD;QAClE,GAAG,IAAI;QACP,UAAU,EAAE,SAAS,CAAC,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;KAC1G,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type QueryKey, type UseQueryOptions, type UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import type { Effect } from 'effect';
|
|
3
|
+
import type { EffectHookError, RegisteredError, RegisteredServices } from './register.js';
|
|
4
|
+
/** `useQuery` options, with `queryFn` an Effect instead of a function returning a promise. */
|
|
5
|
+
export interface UseEffectQueryOptions<A, TData = A, TQueryKey extends QueryKey = QueryKey> extends Omit<UseQueryOptions<A, EffectHookError, TData, TQueryKey>, 'queryFn'> {
|
|
6
|
+
/** The use case to run. It may need only the registered `AppServices`. */
|
|
7
|
+
readonly queryFn: Effect.Effect<A, RegisteredError, RegisteredServices>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `useQuery` for an Effect. The Effect runs on the app runtime; a typed failure is `error` with
|
|
11
|
+
* its own `_tag`, a defect is an `UnexpectedError` with the defect as `cause`, and each failure is
|
|
12
|
+
* logged once through `Logger`. Unmounting or changing the key interrupts the running fiber.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const trip = useEffectQuery({ queryKey: ['trip', id], queryFn: getTrip(id) });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function useEffectQuery<A, TData = A, TQueryKey extends QueryKey = QueryKey>(options: UseEffectQueryOptions<A, TData, TQueryKey>): UseQueryResult<TData, EffectHookError>;
|
|
20
|
+
//# sourceMappingURL=useEffectQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectQuery.d.ts","sourceRoot":"","sources":["../../src/react/useEffectQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAY,MAAM,uBAAuB,CAAC;AAC3G,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAG1F,8FAA8F;AAC9F,MAAM,WAAW,qBAAqB,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,SAAS,SAAS,QAAQ,GAAG,QAAQ,CACxF,SAAQ,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,SAAS,CAAC;IAC9E,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,eAAe,EAAE,kBAAkB,CAAC,CAAC;CACzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,SAAS,SAAS,QAAQ,GAAG,QAAQ,EAChF,OAAO,EAAE,qBAAqB,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,GAClD,cAAc,CAAC,KAAK,EAAE,eAAe,CAAC,CAOxC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useAppRuntime } from './EffectRuntimeProvider.js';
|
|
3
|
+
import { runAtBoundary } from './runAtBoundary.js';
|
|
4
|
+
/**
|
|
5
|
+
* `useQuery` for an Effect. The Effect runs on the app runtime; a typed failure is `error` with
|
|
6
|
+
* its own `_tag`, a defect is an `UnexpectedError` with the defect as `cause`, and each failure is
|
|
7
|
+
* logged once through `Logger`. Unmounting or changing the key interrupts the running fiber.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const trip = useEffectQuery({ queryKey: ['trip', id], queryFn: getTrip(id) });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function useEffectQuery(options) {
|
|
15
|
+
const runtime = useAppRuntime();
|
|
16
|
+
const { queryFn: effect, ...rest } = options;
|
|
17
|
+
return useQuery({
|
|
18
|
+
...rest,
|
|
19
|
+
queryFn: ({ queryKey, signal }) => runAtBoundary(runtime, effect, { queryKey }, signal),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=useEffectQuery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectQuery.js","sourceRoot":"","sources":["../../src/react/useEffectQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4D,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAE3G,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AASnD;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAmD;IAEnD,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAC7C,OAAO,QAAQ,CAAuC;QACpD,GAAG,IAAI;QACP,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,CAAC;KACxF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnyAppError } from '../errors/appError.js';
|
|
2
|
+
import { type MessageKeyMapper } from '../errors/messageKeys.js';
|
|
3
|
+
/**
|
|
4
|
+
* The translated message for `error`, or `undefined` when there is none: looks the tag up in the
|
|
5
|
+
* app's exhaustive `mapper` and passes the key to `t`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const message = useErrorMessage(trip.error, errorTagToMessageKey, t);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function useErrorMessage<E extends AnyAppError, K extends string>(error: E | null | undefined, mapper: MessageKeyMapper<E, K>, t: (key: K) => string): string | undefined;
|
|
13
|
+
//# sourceMappingURL=useErrorMessage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useErrorMessage.d.ts","sourceRoot":"","sources":["../../src/react/useErrorMessage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,0BAA0B,CAAC;AAEpF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,MAAM,EACrE,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3B,MAAM,EAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,EAC9B,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,GACpB,MAAM,GAAG,SAAS,CAEpB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { resolveMessageKey } from '../errors/messageKeys.js';
|
|
3
|
+
/**
|
|
4
|
+
* The translated message for `error`, or `undefined` when there is none: looks the tag up in the
|
|
5
|
+
* app's exhaustive `mapper` and passes the key to `t`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const message = useErrorMessage(trip.error, errorTagToMessageKey, t);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export function useErrorMessage(error, mapper, t) {
|
|
13
|
+
return useMemo(() => (error ? t(resolveMessageKey(error, mapper)) : undefined), [error, mapper, t]);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=useErrorMessage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useErrorMessage.js","sourceRoot":"","sources":["../../src/react/useErrorMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAEhC,OAAO,EAAyB,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAEpF;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAC7B,KAA2B,EAC3B,MAA8B,EAC9B,CAAqB;IAErB,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AACtG,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Exit, type Fiber, type Layer } from 'effect';
|
|
2
|
+
/** Options every `run*` method takes: an `AbortSignal` interrupts the fiber when it aborts. */
|
|
3
|
+
export interface RunOptions {
|
|
4
|
+
readonly signal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The app's single runtime, built from the Layer that provides `AppServices`. `R` is what it
|
|
8
|
+
* provides and `ER` how building it can fail (a `ConfigError`, for example). Running an
|
|
9
|
+
* Effect that needs a service outside `R` is a compile error.
|
|
10
|
+
*/
|
|
11
|
+
export interface AppRuntime<R, ER = never> {
|
|
12
|
+
/** Builds the Layer now instead of on first use, and rejects with its error if it fails. */
|
|
13
|
+
readonly boot: () => Promise<void>;
|
|
14
|
+
/** Runs an Effect; rejects with a `FiberFailure` carrying the cause, as `Effect.runPromise`. */
|
|
15
|
+
readonly runPromise: <A, E>(effect: Effect.Effect<A, E, R>, options?: RunOptions) => Promise<A>;
|
|
16
|
+
/** Runs an Effect and resolves with its `Exit`, never rejecting. */
|
|
17
|
+
readonly runPromiseExit: <A, E>(effect: Effect.Effect<A, E, R>, options?: RunOptions) => Promise<Exit.Exit<A, E | ER>>;
|
|
18
|
+
/** Starts an Effect on a new fiber and returns the fiber. */
|
|
19
|
+
readonly runFork: <A, E>(effect: Effect.Effect<A, E, R>) => Fiber.RuntimeFiber<A, E | ER>;
|
|
20
|
+
/** Releases every resource the Layer acquired. */
|
|
21
|
+
readonly dispose: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The services an {@link AppRuntime} provides: `type AppServices = AppServicesOf<typeof runtime>`.
|
|
25
|
+
*/
|
|
26
|
+
export type AppServicesOf<T> = T extends AppRuntime<infer R, infer _ER> ? R : never;
|
|
27
|
+
/**
|
|
28
|
+
* Builds the app's one runtime (a `ManagedRuntime`) from the Layer of every service. Create it
|
|
29
|
+
* once, in `features/core/runtime`, and hand it to `EffectRuntimeProvider`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* export const runtime = makeAppRuntime(Layer.mergeAll(ConsoleLogger, ConfigLive, TripsLive));
|
|
34
|
+
* export type AppServices = AppServicesOf<typeof runtime>;
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function makeAppRuntime<R, ER>(layer: Layer.Layer<R, ER, never>): AppRuntime<R, ER>;
|
|
38
|
+
//# sourceMappingURL=makeAppRuntime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"makeAppRuntime.d.ts","sourceRoot":"","sources":["../../src/runtime/makeAppRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,IAAI,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAErF,+FAA+F;AAC/F,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC,EAAE,EAAE,GAAG,KAAK;IACvC,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,gGAAgG;IAChG,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAChG,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,CAAC,EAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,UAAU,KACjB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACnC,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1F,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,SAAS,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEpF;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAYzF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, ManagedRuntime } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the app's one runtime (a `ManagedRuntime`) from the Layer of every service. Create it
|
|
4
|
+
* once, in `features/core/runtime`, and hand it to `EffectRuntimeProvider`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* export const runtime = makeAppRuntime(Layer.mergeAll(ConsoleLogger, ConfigLive, TripsLive));
|
|
9
|
+
* export type AppServices = AppServicesOf<typeof runtime>;
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export function makeAppRuntime(layer) {
|
|
13
|
+
const managed = ManagedRuntime.make(layer);
|
|
14
|
+
return {
|
|
15
|
+
boot: async () => {
|
|
16
|
+
const exit = await managed.runPromiseExit(Effect.void);
|
|
17
|
+
if (Exit.isFailure(exit))
|
|
18
|
+
throw Cause.squash(exit.cause);
|
|
19
|
+
},
|
|
20
|
+
runPromise: (effect, options) => managed.runPromise(effect, options),
|
|
21
|
+
runPromiseExit: (effect, options) => managed.runPromiseExit(effect, options),
|
|
22
|
+
runFork: effect => managed.runFork(effect),
|
|
23
|
+
dispose: () => managed.dispose(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=makeAppRuntime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"makeAppRuntime.js","sourceRoot":"","sources":["../../src/runtime/makeAppRuntime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAA0B,cAAc,EAAE,MAAM,QAAQ,CAAC;AAiCrF;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAQ,KAAgC;IACpE,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3C,OAAO;QACL,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACvD,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAAE,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3D,CAAC;QACD,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,OAAO,CAAC;QACpE,cAAc,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC;QAC5E,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;QAC1C,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;KACjC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { type SqlError, type SqliteDatabase } from './sqliteClient.js';
|
|
3
|
+
/**
|
|
4
|
+
* One schema step. `version` is a positive integer; `up` is SQL for `execAsync` or a function
|
|
5
|
+
* that runs its statements on the transaction it receives.
|
|
6
|
+
*/
|
|
7
|
+
export interface Migration {
|
|
8
|
+
readonly version: number;
|
|
9
|
+
readonly up: string | ((txn: SqliteDatabase) => Promise<void>);
|
|
10
|
+
}
|
|
11
|
+
/** What {@link runMigrations} did: the version before and after, and each version it applied. */
|
|
12
|
+
export interface MigrationReport {
|
|
13
|
+
readonly from: number;
|
|
14
|
+
readonly to: number;
|
|
15
|
+
readonly applied: readonly number[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Brings the database to the highest version in `migrations`, keyed by `PRAGMA user_version`.
|
|
19
|
+
* Each pending migration runs, in version order, in its own exclusive transaction together with
|
|
20
|
+
* the `user_version` bump, so a failing step rolls back entirely and leaves `user_version` at the
|
|
21
|
+
* last step that succeeded. Running it again is a no-op. A duplicate or non-integer version is a
|
|
22
|
+
* defect.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* yield* runMigrations(db, [
|
|
27
|
+
* { version: 1, up: 'CREATE TABLE trips (id TEXT PRIMARY KEY, name TEXT NOT NULL);' },
|
|
28
|
+
* { version: 2, up: 'ALTER TABLE trips ADD COLUMN starts_at TEXT;' },
|
|
29
|
+
* ]);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function runMigrations(client: SqliteDatabase, migrations: readonly Migration[]): Effect.Effect<MigrationReport, SqlError>;
|
|
33
|
+
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/sqlite/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAU,MAAM,mBAAmB,CAAC;AAE/E;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CAChE;AAED,iGAAiG;AACjG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAgBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,EACtB,UAAU,EAAE,SAAS,SAAS,EAAE,GAC/B,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,QAAQ,CAAC,CAmB1C"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { trySql } from './sqliteClient.js';
|
|
3
|
+
function validate(migrations) {
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
for (const migration of migrations) {
|
|
6
|
+
if (!Number.isInteger(migration.version) || migration.version < 1) {
|
|
7
|
+
return Effect.dieMessage(`Migration version ${migration.version} is not a positive integer`);
|
|
8
|
+
}
|
|
9
|
+
if (seen.has(migration.version)) {
|
|
10
|
+
return Effect.dieMessage(`Migration version ${migration.version} is declared twice`);
|
|
11
|
+
}
|
|
12
|
+
seen.add(migration.version);
|
|
13
|
+
}
|
|
14
|
+
return Effect.succeed([...migrations].sort((a, b) => a.version - b.version));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Brings the database to the highest version in `migrations`, keyed by `PRAGMA user_version`.
|
|
18
|
+
* Each pending migration runs, in version order, in its own exclusive transaction together with
|
|
19
|
+
* the `user_version` bump, so a failing step rolls back entirely and leaves `user_version` at the
|
|
20
|
+
* last step that succeeded. Running it again is a no-op. A duplicate or non-integer version is a
|
|
21
|
+
* defect.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* yield* runMigrations(db, [
|
|
26
|
+
* { version: 1, up: 'CREATE TABLE trips (id TEXT PRIMARY KEY, name TEXT NOT NULL);' },
|
|
27
|
+
* { version: 2, up: 'ALTER TABLE trips ADD COLUMN starts_at TEXT;' },
|
|
28
|
+
* ]);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function runMigrations(client, migrations) {
|
|
32
|
+
return Effect.gen(function* () {
|
|
33
|
+
const ordered = yield* validate(migrations);
|
|
34
|
+
const row = yield* trySql('read user_version', () => client.getFirstAsync('PRAGMA user_version'));
|
|
35
|
+
const from = row?.user_version ?? 0;
|
|
36
|
+
const pending = ordered.filter(migration => migration.version > from);
|
|
37
|
+
for (const migration of pending) {
|
|
38
|
+
yield* trySql(`migration ${migration.version}`, () => client.withExclusiveTransactionAsync(async (txn) => {
|
|
39
|
+
if (typeof migration.up === 'string')
|
|
40
|
+
await txn.execAsync(migration.up);
|
|
41
|
+
else
|
|
42
|
+
await migration.up(txn);
|
|
43
|
+
await txn.execAsync(`PRAGMA user_version = ${migration.version}`);
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
return { from, to: pending.at(-1)?.version ?? from, applied: pending.map(migration => migration.version) };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/sqlite/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAsC,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAkB/E,SAAS,QAAQ,CAAC,UAAgC;IAChD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;YAClE,OAAO,MAAM,CAAC,UAAU,CAAC,qBAAqB,SAAS,CAAC,OAAO,4BAA4B,CAAC,CAAC;QAC/F,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,OAAO,MAAM,CAAC,UAAU,CAAC,qBAAqB,SAAS,CAAC,OAAO,oBAAoB,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAsB,EACtB,UAAgC;IAEhC,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACzB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,mBAAmB,EAAE,GAAG,EAAE,CAClD,MAAM,CAAC,aAAa,CAA2B,qBAAqB,CAAC,CACtE,CAAC;QACF,MAAM,IAAI,GAAG,GAAG,EAAE,YAAY,IAAI,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;QACtE,KAAK,MAAM,SAAS,IAAI,OAAO,EAAE,CAAC;YAChC,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,SAAS,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,CACnD,MAAM,CAAC,6BAA6B,CAAC,KAAK,EAAC,GAAG,EAAC,EAAE;gBAC/C,IAAI,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ;oBAAE,MAAM,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;;oBACnE,MAAM,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;gBAC7B,MAAM,GAAG,CAAC,SAAS,CAAC,yBAAyB,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;YACpE,CAAC,CAAC,CACH,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7G,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Context, Effect } from 'effect';
|
|
2
|
+
/** A value SQLite can bind: the same set expo-sqlite accepts. */
|
|
3
|
+
export type SqlValue = string | number | null | boolean | Uint8Array | ArrayBuffer;
|
|
4
|
+
/** Positional (`?`) or named (`$name`) parameters. */
|
|
5
|
+
export type SqlParams = SqlValue[] | Record<string, SqlValue>;
|
|
6
|
+
/** What a write reports. */
|
|
7
|
+
export interface SqlRunResult {
|
|
8
|
+
readonly lastInsertRowId: number;
|
|
9
|
+
readonly changes: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The slice of expo-sqlite's async `SQLiteDatabase` API the kit relies on. An expo-sqlite
|
|
13
|
+
* database satisfies it as it is, so the app's Live Layer is one line:
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* export const SqliteLive = Layer.effect(
|
|
18
|
+
* SqliteClient,
|
|
19
|
+
* Effect.promise(() => SQLite.openDatabaseAsync('app.db')),
|
|
20
|
+
* );
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface SqliteDatabase {
|
|
24
|
+
/** Runs one or more statements without parameters (DDL, pragmas). */
|
|
25
|
+
execAsync(source: string): Promise<void>;
|
|
26
|
+
/** Runs a write. */
|
|
27
|
+
runAsync(source: string, params?: SqlParams): Promise<SqlRunResult>;
|
|
28
|
+
/** Every row of a query. */
|
|
29
|
+
getAllAsync<T>(source: string, params?: SqlParams): Promise<T[]>;
|
|
30
|
+
/** The first row of a query, or `null`. */
|
|
31
|
+
getFirstAsync<T>(source: string, params?: SqlParams): Promise<T | null>;
|
|
32
|
+
/** Runs `task` in a transaction; other queries on the connection may interleave. */
|
|
33
|
+
withTransactionAsync(task: () => Promise<void>): Promise<void>;
|
|
34
|
+
/** Runs `task` in an exclusive transaction; use `txn` for every statement inside it. */
|
|
35
|
+
withExclusiveTransactionAsync(task: (txn: SqliteDatabase) => Promise<void>): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
declare const SqliteClient_base: Context.TagClass<SqliteClient, "@timothyrusso/effect-core/SqliteClient", SqliteDatabase>;
|
|
38
|
+
/**
|
|
39
|
+
* The app's database connection. The kit ships the interface and the Node test Layer
|
|
40
|
+
* (`makeNodeSqliteLayer` in `/testing`); the expo-sqlite Live Layer lives in the app.
|
|
41
|
+
*/
|
|
42
|
+
export declare class SqliteClient extends SqliteClient_base {
|
|
43
|
+
}
|
|
44
|
+
declare const SqlError_base: import("../errors/appError.js").AppErrorClass<"SqlError", "errors.sql">;
|
|
45
|
+
/** A statement or a transaction failed. `cause` is the driver's error. */
|
|
46
|
+
export declare class SqlError extends SqlError_base<{
|
|
47
|
+
readonly message: string;
|
|
48
|
+
}> {
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Runs `query` against the connection in the context, failing with a {@link SqlError} when its
|
|
52
|
+
* promise rejects. `operation` names the call in the error message.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const trips = withSqlite('list trips', db => db.getAllAsync<TripRow>('SELECT * FROM trips'));
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function withSqlite<A>(operation: string, query: (db: SqliteDatabase) => Promise<A>): Effect.Effect<A, SqlError, SqliteClient>;
|
|
60
|
+
/** `Effect.tryPromise` with the rejection mapped to a {@link SqlError}. */
|
|
61
|
+
export declare function trySql<A>(operation: string, run: () => Promise<A>): Effect.Effect<A, SqlError>;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=sqliteClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqliteClient.d.ts","sourceRoot":"","sources":["../../src/sqlite/sqliteClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGzC,iEAAiE;AACjE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,CAAC;AAEnF,sDAAsD;AACtD,MAAM,MAAM,SAAS,GAAG,QAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAE9D,4BAA4B;AAC5B,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,oBAAoB;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACpE,4BAA4B;IAC5B,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACjE,2CAA2C;IAC3C,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACxE,oFAAoF;IACpF,oBAAoB,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,wFAAwF;IACxF,6BAA6B,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5F;;AAED;;;GAGG;AACH,qBAAa,YAAa,SAAQ,iBAG/B;CAAG;;AAEN,0EAA0E;AAC1E,qBAAa,QAAS,SAAQ,cAAuC;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;CAAG;AASrG;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAC1B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,EAAE,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,YAAY,CAAC,CAE1C;AAED,2EAA2E;AAC3E,wBAAgB,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,CAK9F"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Context, Effect } from 'effect';
|
|
2
|
+
import { AppErrorBase } from '../errors/appError.js';
|
|
3
|
+
/**
|
|
4
|
+
* The app's database connection. The kit ships the interface and the Node test Layer
|
|
5
|
+
* (`makeNodeSqliteLayer` in `/testing`); the expo-sqlite Live Layer lives in the app.
|
|
6
|
+
*/
|
|
7
|
+
export class SqliteClient extends Context.Tag('@timothyrusso/effect-core/SqliteClient')() {
|
|
8
|
+
}
|
|
9
|
+
/** A statement or a transaction failed. `cause` is the driver's error. */
|
|
10
|
+
export class SqlError extends AppErrorBase('SqlError', 'errors.sql') {
|
|
11
|
+
}
|
|
12
|
+
function messageOf(cause) {
|
|
13
|
+
if (typeof cause === 'object' && cause !== null && 'message' in cause && typeof cause.message === 'string') {
|
|
14
|
+
return cause.message;
|
|
15
|
+
}
|
|
16
|
+
return String(cause);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Runs `query` against the connection in the context, failing with a {@link SqlError} when its
|
|
20
|
+
* promise rejects. `operation` names the call in the error message.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const trips = withSqlite('list trips', db => db.getAllAsync<TripRow>('SELECT * FROM trips'));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function withSqlite(operation, query) {
|
|
28
|
+
return Effect.flatMap(SqliteClient, db => trySql(operation, () => query(db)));
|
|
29
|
+
}
|
|
30
|
+
/** `Effect.tryPromise` with the rejection mapped to a {@link SqlError}. */
|
|
31
|
+
export function trySql(operation, run) {
|
|
32
|
+
return Effect.tryPromise({
|
|
33
|
+
try: run,
|
|
34
|
+
catch: cause => new SqlError({ message: `${operation}: ${messageOf(cause)}`, cause }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=sqliteClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqliteClient.js","sourceRoot":"","sources":["../../src/sqlite/sqliteClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAyCrD;;;GAGG;AACH,MAAM,OAAO,YAAa,SAAQ,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,EAGpF;CAAG;AAEN,0EAA0E;AAC1E,MAAM,OAAO,QAAS,SAAQ,YAAY,CAAC,UAAU,EAAE,YAAY,CAA+B;CAAG;AAErG,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC3G,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CACxB,SAAiB,EACjB,KAAyC;IAEzC,OAAO,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,MAAM,CAAI,SAAiB,EAAE,GAAqB;IAChE,OAAO,MAAM,CAAC,UAAU,CAAC;QACvB,GAAG,EAAE,GAAG;QACR,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,SAAS,KAAK,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC;KACtF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Duration, type Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Moves the `TestClock` that {@link runTest} and {@link itEffect} install forward by `duration`,
|
|
4
|
+
* running every sleep and schedule that falls due. Fork the Effect that waits first, then advance.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* itEffect('expires the cache after an hour', Effect.gen(function* () {
|
|
9
|
+
* const fiber = yield* Effect.fork(expireAfter(Duration.hours(1)));
|
|
10
|
+
* yield* advanceClock(Duration.hours(1));
|
|
11
|
+
* expect(yield* Fiber.join(fiber)).toBe('expired');
|
|
12
|
+
* }));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function advanceClock(duration: Duration.DurationInput): Effect.Effect<void>;
|
|
16
|
+
//# sourceMappingURL=clock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../../src/testing/clock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAa,MAAM,QAAQ,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAElF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TestClock } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Moves the `TestClock` that {@link runTest} and {@link itEffect} install forward by `duration`,
|
|
4
|
+
* running every sleep and schedule that falls due. Fork the Effect that waits first, then advance.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* itEffect('expires the cache after an hour', Effect.gen(function* () {
|
|
9
|
+
* const fiber = yield* Effect.fork(expireAfter(Duration.hours(1)));
|
|
10
|
+
* yield* advanceClock(Duration.hours(1));
|
|
11
|
+
* expect(yield* Fiber.join(fiber)).toBe('expired');
|
|
12
|
+
* }));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function advanceClock(duration) {
|
|
16
|
+
return TestClock.adjust(duration);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../../src/testing/clock.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgC;IAC3D,OAAO,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Layer } from 'effect';
|
|
2
|
+
import { type LogContext, Logger } from '../logger/logger.js';
|
|
3
|
+
/** One call a collecting logger received. */
|
|
4
|
+
export interface LogEntry {
|
|
5
|
+
readonly level: 'error' | 'warn' | 'info' | 'debug';
|
|
6
|
+
/** The message, or for `error` the error's `_tag` (or its string form). */
|
|
7
|
+
readonly message: string;
|
|
8
|
+
/** The value passed to `error`; `undefined` for the other levels. */
|
|
9
|
+
readonly error: unknown;
|
|
10
|
+
readonly context: LogContext;
|
|
11
|
+
}
|
|
12
|
+
/** What {@link collectLogs} returns. */
|
|
13
|
+
export interface CollectedLogs {
|
|
14
|
+
/** A `Logger` Layer that records every call into `entries`. */
|
|
15
|
+
readonly layer: Layer.Layer<Logger>;
|
|
16
|
+
/** Every call so far, oldest first. */
|
|
17
|
+
readonly entries: readonly LogEntry[];
|
|
18
|
+
/** Empties `entries`. */
|
|
19
|
+
clear(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A `Logger` that records instead of printing, to assert what the boundary logged.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const logs = collectLogs();
|
|
27
|
+
* const runtime = makeAppRuntime(Layer.merge(logs.layer, TripsTestLayer));
|
|
28
|
+
* expect(logs.entries).toHaveLength(1);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function collectLogs(): CollectedLogs;
|
|
32
|
+
//# sourceMappingURL=collectLogs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collectLogs.d.ts","sourceRoot":"","sources":["../../src/testing/collectLogs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE9D,6CAA6C;AAC7C,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACpD,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;CAC9B;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,QAAQ,EAAE,CAAC;IACtC,yBAAyB;IACzB,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,aAAa,CAiB3C"}
|