effect-zero 0.7.0 → 0.9.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/README.md +86 -77
- package/dist/client-transaction.d.ts +0 -1
- package/dist/client-transaction.d.ts.map +1 -1
- package/dist/client.d.ts +38 -138
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +75 -24
- package/dist/client.js.map +1 -1
- package/dist/internal/utils.d.ts +0 -7
- package/dist/internal/utils.d.ts.map +1 -1
- package/dist/internal/utils.js +0 -7
- package/dist/internal/utils.js.map +1 -1
- package/dist/mutator.d.ts +115 -0
- package/dist/mutator.d.ts.map +1 -0
- package/dist/mutator.js +67 -0
- package/dist/mutator.js.map +1 -0
- package/dist/mutator.test.d.ts +2 -0
- package/dist/mutator.test.d.ts.map +1 -0
- package/dist/{client.test.js → mutator.test.js} +5 -5
- package/dist/mutator.test.js.map +1 -0
- package/dist/query.d.ts +2 -2
- package/dist/result.d.ts +1 -1
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +1 -1
- package/dist/result.js.map +1 -1
- package/dist/server-transaction.d.ts +0 -1
- package/dist/server-transaction.d.ts.map +1 -1
- package/dist/server.d.ts +7 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +30 -15
- package/dist/server.js.map +1 -1
- package/dist/types.test.js +85 -120
- package/dist/types.test.js.map +1 -1
- package/package.json +49 -48
- package/src/client.ts +159 -56
- package/src/internal/utils.ts +0 -8
- package/src/mutator.ts +232 -0
- package/src/result.ts +2 -2
- package/src/server.ts +49 -34
- package/dist/client.test.d.ts +0 -2
- package/dist/client.test.d.ts.map +0 -1
- package/dist/client.test.js.map +0 -1
- package/dist/mutators.d.ts +0 -30
- package/dist/mutators.d.ts.map +0 -1
- package/dist/mutators.js +0 -17
- package/dist/mutators.js.map +0 -1
- package/src/mutators.ts +0 -80
package/src/client.ts
CHANGED
|
@@ -1,79 +1,182 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type MutatorResultDetails,
|
|
3
|
+
type MutatorResultErrorDetails,
|
|
4
|
+
type MutatorResultSuccessDetails,
|
|
5
|
+
type ReadonlyJSONValue,
|
|
6
|
+
Zero,
|
|
7
|
+
type ZeroOptions,
|
|
8
|
+
} from "@rocicorp/zero";
|
|
3
9
|
import * as Data from "effect/Data";
|
|
4
10
|
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Exit from "effect/Exit";
|
|
12
|
+
import * as Fiber from "effect/Fiber";
|
|
13
|
+
import * as Fn from "effect/Function";
|
|
5
14
|
import type { NodeInspectSymbol } from "effect/Inspectable";
|
|
15
|
+
import type * as LogLevel from "effect/LogLevel";
|
|
6
16
|
import * as Match from "effect/Match";
|
|
17
|
+
import * as Option from "effect/Option";
|
|
7
18
|
import * as Predicate from "effect/Predicate";
|
|
8
19
|
import * as Rec from "effect/Record";
|
|
9
|
-
import * as
|
|
20
|
+
import * as Scope from "effect/Scope";
|
|
21
|
+
import type * as Tracer from "effect/Tracer";
|
|
10
22
|
import type * as Unify from "effect/Unify";
|
|
11
|
-
import type
|
|
12
|
-
import
|
|
13
|
-
import { normalizeArgs } from "./internal/utils.js";
|
|
14
|
-
import * as Mutators from "./mutators.js";
|
|
23
|
+
import { type ContextSymbol, SchemaSymbol } from "./client-transaction.js";
|
|
24
|
+
import * as Mutator from "./mutator.js";
|
|
15
25
|
|
|
16
26
|
type _NodeInspectSymbol = NodeInspectSymbol;
|
|
17
27
|
type _Unify = Unify.typeSymbol | Unify.unifySymbol | Unify.ignoreSymbol;
|
|
18
28
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
mutators
|
|
22
|
-
|
|
29
|
+
export type Options<TRegistry extends Mutator.AnyRegistry> = Omit<
|
|
30
|
+
ZeroOptions<Mutator.ZeroSchemaOf<TRegistry>>,
|
|
31
|
+
"schema" | "mutators" | "context"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Makes a Zero client that runs the registry's mutators with the services in scope here.
|
|
36
|
+
* Run mutations with {@link mutate}.
|
|
37
|
+
*/
|
|
38
|
+
export const make = Effect.fn(function* <TRegistry extends Mutator.AnyRegistry>(
|
|
39
|
+
mutators: TRegistry,
|
|
40
|
+
options: Options<TRegistry>,
|
|
23
41
|
) {
|
|
24
|
-
const
|
|
42
|
+
const context =
|
|
25
43
|
yield* Effect.context<
|
|
26
|
-
Exclude<
|
|
44
|
+
Exclude<Mutator.Requirements<TRegistry>, Mutator.Transaction<TRegistry>[typeof ContextSymbol]["Identifier"]>
|
|
27
45
|
>();
|
|
28
46
|
|
|
29
|
-
function unwrapMutator<E>(mutator: Mutators.AnyMutator<Mutators.ExtractMutatorsRequirements<TMutators>, E>) {
|
|
30
|
-
return (tx: ZeroTransaction<TSchema>, args: unknown, _ctx: unknown) =>
|
|
31
|
-
Schema.decodeUnknownEffect(mutator[Mutators.MutatorSchemaSymbol])(normalizeArgs(args)).pipe(
|
|
32
|
-
Effect.catchTag("SchemaError", (e) => Effect.fail(new ClientArgsParseError({ cause: Cause.fail(e) }))),
|
|
33
|
-
Effect.flatMap(mutator),
|
|
34
|
-
Effect.provideService(transaction[ContextSymbol], tx),
|
|
35
|
-
Effect.runPromiseWith(ctx),
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const unwrappedMutators = Rec.map(mutators, (v) =>
|
|
40
|
-
Match.value(v).pipe(Match.when(Predicate.isFunction, unwrapMutator), Match.orElse(Rec.map(unwrapMutator))),
|
|
41
|
-
) as ZeroOptions<TSchema, UnwrapMutators<TSchema, TMutators>>["mutators"];
|
|
42
|
-
|
|
43
47
|
return yield* Effect.acquireRelease(
|
|
44
|
-
Effect.sync(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
Effect.sync(
|
|
49
|
+
() =>
|
|
50
|
+
new Zero<Mutator.ZeroSchemaOf<TRegistry>>({
|
|
51
|
+
// Zero's own logs (at its `logLevel`, "warn" by default) go through Effect's logger, unless `options` sets
|
|
52
|
+
// a `logSink`.
|
|
53
|
+
logSink: {
|
|
54
|
+
log: (level, logContext, ...args) =>
|
|
55
|
+
Match.value(level).pipe(
|
|
56
|
+
Match.withReturnType<LogLevel.Severity>(),
|
|
57
|
+
Match.when("debug", () => "Debug"),
|
|
58
|
+
Match.when("info", () => "Info"),
|
|
59
|
+
Match.when("warn", () => "Warn"),
|
|
60
|
+
Match.when("error", () => "Error"),
|
|
61
|
+
Match.exhaustive,
|
|
62
|
+
(severity) => Effect.logWithLevel(severity)(...args),
|
|
63
|
+
Effect.annotateLogs({ ...logContext, logger: "zero" }),
|
|
64
|
+
Effect.runForkWith(context),
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
...options,
|
|
68
|
+
schema: mutators[Mutator.RegistryTypeId].transaction[SchemaSymbol],
|
|
69
|
+
mutators,
|
|
70
|
+
// The mutators run with these services.
|
|
71
|
+
context,
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
51
74
|
(zero) => Effect.promise(() => zero.close()),
|
|
52
75
|
);
|
|
53
76
|
});
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
/** A mutation sent with {@link mutate}. Its client and server outcomes settle independently. */
|
|
79
|
+
export interface Mutation {
|
|
80
|
+
/** Completes once the mutation is applied locally; fails if Zero or the client mutator rejected it. */
|
|
81
|
+
readonly client: Effect.Effect<void, MutationError>;
|
|
82
|
+
/** Completes once the server applied the mutation; fails if it was rejected anywhere. */
|
|
83
|
+
readonly server: Effect.Effect<void, MutationError>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sends a mutation with `zero.mutate(request)` and traces it. Awaiting `client` or `server` is optional; both
|
|
88
|
+
* outcomes are always recorded on the `zero.mutation` span, as the `zero.mutation.client` and
|
|
89
|
+
* `zero.mutation.server` child spans.
|
|
90
|
+
*/
|
|
91
|
+
export const mutate = Effect.fn(function* <Type>(
|
|
92
|
+
// biome-ignore lint/suspicious/noExplicitAny: any Zero instance made with `make`
|
|
93
|
+
zero: Zero<any, any, any>,
|
|
94
|
+
request: Mutator.MutateRequest<Type>,
|
|
95
|
+
) {
|
|
96
|
+
const mutator = request.mutator.mutatorName;
|
|
97
|
+
const connection = zero.connection.state.current;
|
|
98
|
+
// The span outlives this call: it ends when this scope closes, once both outcomes are known.
|
|
99
|
+
const scope = yield* Scope.make();
|
|
100
|
+
const span = yield* Effect.makeSpanScoped("zero.mutation", {
|
|
101
|
+
attributes: {
|
|
102
|
+
"zero.mutator": mutator,
|
|
103
|
+
"zero.clientID": zero.clientID,
|
|
104
|
+
"zero.connection.state": connection.name,
|
|
105
|
+
...Rec.getSomes({
|
|
106
|
+
"zero.connection.reason": Option.fromNullishOr("reason" in connection ? connection.reason : undefined).pipe(
|
|
107
|
+
Option.map((reason) => (Predicate.isString(reason) ? reason : reason.type)),
|
|
108
|
+
),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
}).pipe(Scope.provide(scope));
|
|
112
|
+
|
|
113
|
+
const result = yield* Effect.sync(() => zero.mutate(request)).pipe(
|
|
114
|
+
Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const client = yield* Effect.promise(() => result.client).pipe(
|
|
118
|
+
Effect.filterOrElse(isApplied, (details) => new MutationError({ mutator, origin: "client", ...details.error })),
|
|
119
|
+
Effect.asVoid,
|
|
120
|
+
traceOutcome({ origin: "client", parent: span, mutator }),
|
|
121
|
+
Effect.forkDetach,
|
|
122
|
+
);
|
|
123
|
+
const server = yield* Effect.promise(() => result.server).pipe(
|
|
124
|
+
Effect.filterOrElse(
|
|
125
|
+
isApplied,
|
|
126
|
+
Effect.fnUntraced(function* (details) {
|
|
127
|
+
// A write the client rejected is never pushed; Zero reports the client's error for the server too.
|
|
128
|
+
const origin = Exit.isFailure(yield* Fiber.await(client)) ? "client" : "server";
|
|
129
|
+
return yield* new MutationError({ mutator, origin, ...details.error });
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
Effect.asVoid,
|
|
133
|
+
traceOutcome({ origin: "server", parent: span, mutator }),
|
|
134
|
+
Effect.forkDetach,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
yield* Effect.gen(function* () {
|
|
138
|
+
span.attribute("zero.clientGroupID", yield* Effect.promise(() => zero.clientGroupID));
|
|
139
|
+
const clientExit = yield* Fiber.await(client);
|
|
140
|
+
const serverExit = yield* Fiber.await(server);
|
|
141
|
+
yield* Scope.close(scope, Exit.isFailure(clientExit) ? clientExit : serverExit);
|
|
142
|
+
}).pipe(Effect.forkDetach);
|
|
143
|
+
|
|
144
|
+
// Joining a fiber never re-runs it, so either outcome can be awaited any number of times.
|
|
145
|
+
return { client: Fiber.join(client), server: Fiber.join(server) };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const isApplied = (details: MutatorResultDetails): details is MutatorResultSuccessDetails => details.type === "success";
|
|
61
149
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
150
|
+
/** Runs a mutation's outcome in its `zero.mutation.client` or `zero.mutation.server` span. */
|
|
151
|
+
const traceOutcome: {
|
|
152
|
+
(options: TraceOutcomeOptions): (self: Effect.Effect<void, MutationError>) => Effect.Effect<void, MutationError>;
|
|
153
|
+
(self: Effect.Effect<void, MutationError>, options: TraceOutcomeOptions): Effect.Effect<void, MutationError>;
|
|
154
|
+
} = Fn.dual(2, (self: Effect.Effect<void, MutationError>, { origin, parent, mutator }: TraceOutcomeOptions) =>
|
|
155
|
+
self.pipe(
|
|
156
|
+
Effect.tapError((error) =>
|
|
157
|
+
Effect.annotateCurrentSpan({
|
|
158
|
+
"zero.mutation.error.type": error.type,
|
|
159
|
+
"zero.mutation.error.origin": error.origin,
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
Effect.withSpan(`zero.mutation.${origin}`, { parent, attributes: { "zero.mutator": mutator } }),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
71
165
|
|
|
72
|
-
|
|
73
|
-
readonly
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const error = Cause.squash(this.cause);
|
|
77
|
-
return Predicate.isError(error) ? error.message : "Internal error";
|
|
78
|
-
}
|
|
166
|
+
interface TraceOutcomeOptions {
|
|
167
|
+
readonly origin: "client" | "server";
|
|
168
|
+
readonly parent: Tracer.Span;
|
|
169
|
+
readonly mutator: string;
|
|
79
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Zero rejected a mutation, with Zero's own error details: `type` is `"zero"` when Zero itself refused it
|
|
174
|
+
* (e.g. offline), and `"app"` when a mutator failed. `origin` is where it failed.
|
|
175
|
+
*/
|
|
176
|
+
export class MutationError extends Data.TaggedError("MutationError")<{
|
|
177
|
+
readonly mutator: string;
|
|
178
|
+
readonly origin: "client" | "server";
|
|
179
|
+
readonly type: MutatorResultErrorDetails["error"]["type"];
|
|
180
|
+
readonly message: string;
|
|
181
|
+
readonly details?: ReadonlyJSONValue | undefined;
|
|
182
|
+
}> {}
|
package/src/internal/utils.ts
CHANGED
|
@@ -2,11 +2,3 @@
|
|
|
2
2
|
* Prefix a string with the package name.
|
|
3
3
|
*/
|
|
4
4
|
export const prefixId = <S extends string>(name: S) => `effect-zero/${name}` as const;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Zero unconditionally serializes arg-less mutator calls as `null` on the wire
|
|
8
|
-
* (https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-client/src/client/custom.ts).
|
|
9
|
-
* v3 `Schema.Void` happened to accept any input; v4 `Schema.Void` is strict
|
|
10
|
-
* `=== undefined`, so we renormalize before decoding.
|
|
11
|
-
*/
|
|
12
|
-
export const normalizeArgs = (args: unknown): unknown => (args === null ? undefined : args);
|
package/src/mutator.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyMutatorRegistry,
|
|
3
|
+
MutateRequest as ZeroMutateRequest,
|
|
4
|
+
Schema as ZeroSchema,
|
|
5
|
+
Transaction as ZeroTransaction,
|
|
6
|
+
} from "@rocicorp/zero";
|
|
7
|
+
import * as Cause from "effect/Cause";
|
|
8
|
+
import * as Context from "effect/Context";
|
|
9
|
+
import * as Data from "effect/Data";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import type { NodeInspectSymbol } from "effect/Inspectable";
|
|
12
|
+
import * as Predicate from "effect/Predicate";
|
|
13
|
+
import * as Rec from "effect/Record";
|
|
14
|
+
import * as Schema from "effect/Schema";
|
|
15
|
+
import type * as Unify from "effect/Unify";
|
|
16
|
+
import type * as ClientTransaction from "./client-transaction.js";
|
|
17
|
+
import { ContextSymbol } from "./client-transaction.js";
|
|
18
|
+
import { prefixId } from "./internal/utils.js";
|
|
19
|
+
|
|
20
|
+
type _NodeInspectSymbol = NodeInspectSymbol;
|
|
21
|
+
type _Unify = Unify.typeSymbol | Unify.unifySymbol | Unify.ignoreSymbol;
|
|
22
|
+
|
|
23
|
+
export const RegistryTypeId = Symbol.for(prefixId("MutatorRegistry"));
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A mutator that isn't part of a registry yet, made with {@link make}. Like `defineMutator`, it gets its name
|
|
27
|
+
* from its path in the {@link registry}.
|
|
28
|
+
*/
|
|
29
|
+
export interface Definition<Type, Encoded, E, R> {
|
|
30
|
+
readonly payload: Schema.Codec<Type, Encoded, R>;
|
|
31
|
+
readonly mutator: (args: Type) => Effect.Effect<unknown, E, R>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
35
|
+
export type AnyDefinition = Definition<any, any, any, any>;
|
|
36
|
+
|
|
37
|
+
export type Definitions = { readonly [key: string]: AnyDefinition | Definitions };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A registered mutator. Calling it encodes `args` and returns a request for `ZfxClient.mutate(zero, request)`, or
|
|
41
|
+
* for Zero's own `zero.mutate(request)`. It is also a Zero `Mutator`, so the registry can be passed to Zero.
|
|
42
|
+
*/
|
|
43
|
+
export interface Mutator<Type, Encoded, E, R> extends Definition<Type, Encoded, E, R> {
|
|
44
|
+
(args: Type): MutateRequest<Type>;
|
|
45
|
+
readonly mutatorName: string;
|
|
46
|
+
/** Runs the mutator for Zero, which calls it with the transaction and the `context` from `ZfxClient.make`. */
|
|
47
|
+
readonly fn: (options: {
|
|
48
|
+
readonly args: unknown;
|
|
49
|
+
readonly ctx: unknown;
|
|
50
|
+
readonly tx: ZeroTransaction<ZeroSchema>;
|
|
51
|
+
}) => Promise<void>;
|
|
52
|
+
readonly "~": "Mutator";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
56
|
+
export type AnyMutator = Mutator<any, any, any, any>;
|
|
57
|
+
|
|
58
|
+
/** A request to run a mutator, for `ZfxClient.mutate` or Zero's own `zero.mutate`. */
|
|
59
|
+
// biome-ignore lint/suspicious/noExplicitAny: compatible with any Zero instance's `zero.mutate`
|
|
60
|
+
export type MutateRequest<Type> = ZeroMutateRequest<any, any, any, any> & {
|
|
61
|
+
// biome-ignore lint/suspicious/noExplicitAny: the encoded args, errors and services don't matter to the caller
|
|
62
|
+
readonly mutator: Mutator<Type, any, any, any>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type Tree<D> = {
|
|
66
|
+
readonly [K in keyof D]: D[K] extends Definition<infer Type, infer Encoded, infer E, infer R>
|
|
67
|
+
? Mutator<Type, Encoded, E, R>
|
|
68
|
+
: Tree<D[K]>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** A tree of registered mutators, made with {@link registry}. Pass it to `ZfxClient.make` and the server. */
|
|
72
|
+
export type Registry<D extends Definitions, TTransaction extends AnyClientTransaction> = Tree<D> & {
|
|
73
|
+
readonly "~": AnyMutatorRegistry["~"];
|
|
74
|
+
readonly [RegistryTypeId]: { readonly transaction: TTransaction };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
78
|
+
export type AnyRegistry = Registry<any, AnyClientTransaction>;
|
|
79
|
+
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: any client transaction
|
|
81
|
+
type AnyClientTransaction = ClientTransaction.Context<any>;
|
|
82
|
+
|
|
83
|
+
type Keys<T> = Exclude<Extract<keyof T, string>, "~">;
|
|
84
|
+
|
|
85
|
+
/** The services a registry's mutators require. */
|
|
86
|
+
// biome-ignore lint/suspicious/noExplicitAny: matches any mutator
|
|
87
|
+
export type Requirements<T> = T extends Mutator<any, any, any, infer R>
|
|
88
|
+
? R
|
|
89
|
+
: { [K in Keys<T>]: Requirements<T[K]> }[Keys<T>];
|
|
90
|
+
|
|
91
|
+
/** The client transaction a registry's mutators run in. */
|
|
92
|
+
export type Transaction<T extends AnyRegistry> = T[typeof RegistryTypeId]["transaction"];
|
|
93
|
+
|
|
94
|
+
/** The Zero schema a registry's mutators run against. */
|
|
95
|
+
export type ZeroSchemaOf<T extends AnyRegistry> = Transaction<T> extends ClientTransaction.Context<infer S> ? S : never;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Defines a mutator. Its `payload` codec decodes the args on both the client and the server; it must encode without
|
|
99
|
+
* services, since calling a registered mutator encodes its args synchronously.
|
|
100
|
+
*/
|
|
101
|
+
export const make = <Type, Encoded, RD = never, A = unknown, E = never, R = never>(options: {
|
|
102
|
+
readonly payload: Schema.Codec<Type, Encoded, RD, never>;
|
|
103
|
+
readonly mutator: (args: NoInfer<Type>) => Effect.Effect<A, E, R>;
|
|
104
|
+
}): Definition<Type, Encoded, E, R | RD> => options;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Registers mutators under their path, like Zero's `defineMutators`: `{ todo: { create } }` registers
|
|
108
|
+
* `"todo.create"`. The result is also a Zero mutator registry, so it can be passed to `new Zero({ mutators })`.
|
|
109
|
+
*/
|
|
110
|
+
export const registry = <TTransaction extends AnyClientTransaction, const D extends Definitions>(
|
|
111
|
+
transaction: TTransaction,
|
|
112
|
+
definitions: D,
|
|
113
|
+
): Registry<D, TTransaction> => {
|
|
114
|
+
const register = (definitions: Definitions, path: ReadonlyArray<string>): Record<string, unknown> =>
|
|
115
|
+
Rec.map(definitions, (value, key) =>
|
|
116
|
+
isDefinition(value) ? makeMutator(transaction, [...path, key].join("."), value) : register(value, [...path, key]),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// The registry's shape is only known from `D`.
|
|
120
|
+
return Object.assign(register(definitions, []), {
|
|
121
|
+
"~": "MutatorRegistry",
|
|
122
|
+
[RegistryTypeId]: { transaction },
|
|
123
|
+
}) as unknown as Registry<D, TTransaction>;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const isDefinition = (u: unknown): u is AnyDefinition =>
|
|
127
|
+
Predicate.hasProperty(u, "payload") && Schema.isSchema(u.payload);
|
|
128
|
+
|
|
129
|
+
const makeMutator = <Type, Encoded, E, R>(
|
|
130
|
+
transaction: AnyClientTransaction,
|
|
131
|
+
name: string,
|
|
132
|
+
definition: Definition<Type, Encoded, E, R>,
|
|
133
|
+
): Mutator<Type, Encoded, E, R> => {
|
|
134
|
+
const run = (args: unknown, tx: ZeroTransaction<ZeroSchema>) =>
|
|
135
|
+
Schema.decodeUnknownEffect(definition.payload)(args).pipe(
|
|
136
|
+
Effect.catchTag("SchemaError", (error) => new ClientArgsParseError({ cause: Cause.fail(error) })),
|
|
137
|
+
Effect.flatMap(definition.mutator),
|
|
138
|
+
Effect.withSpan("zero.mutator", {
|
|
139
|
+
attributes: {
|
|
140
|
+
"zero.mutator": name,
|
|
141
|
+
"zero.clientID": tx.clientID,
|
|
142
|
+
"zero.mutationID": tx.mutationID,
|
|
143
|
+
"zero.mutation.reason": tx.reason,
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
Effect.provideService(transaction[ContextSymbol], tx),
|
|
147
|
+
Effect.asVoid,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const mutator: Mutator<Type, Encoded, E, R> = Object.assign(
|
|
151
|
+
(args: Type) =>
|
|
152
|
+
// A Zero `MutateRequest`, whose brand only exists at the type level.
|
|
153
|
+
({ mutator, args: Schema.encodeSync(definition.payload)(args), "~": "MutateRequest" }) as MutateRequest<Type>,
|
|
154
|
+
{
|
|
155
|
+
...definition,
|
|
156
|
+
mutatorName: name,
|
|
157
|
+
"~": "Mutator" as const,
|
|
158
|
+
fn: ({
|
|
159
|
+
args,
|
|
160
|
+
ctx,
|
|
161
|
+
tx,
|
|
162
|
+
}: {
|
|
163
|
+
readonly args: unknown;
|
|
164
|
+
readonly ctx: unknown;
|
|
165
|
+
readonly tx: ZeroTransaction<ZeroSchema>;
|
|
166
|
+
}) =>
|
|
167
|
+
Context.isContext(ctx)
|
|
168
|
+
? // `ZfxClient.make` passes the services it captured, which cover every mutator's requirements.
|
|
169
|
+
run(args, tx).pipe(Effect.runPromiseWith(ctx as Context.Context<R>))
|
|
170
|
+
: Promise.reject(new MutatorContextNotFound({ mutator: name })),
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
return mutator;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
type Overrides<T> = {
|
|
177
|
+
readonly [K in Keys<T>]?: T[K] extends Mutator<infer Type, infer _Encoded, infer _E, infer _R>
|
|
178
|
+
? // biome-ignore lint/suspicious/noExplicitAny: any server implementation
|
|
179
|
+
(args: Type) => Effect.Effect<unknown, any, any>
|
|
180
|
+
: Overrides<T[K]>;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** The server's view of a registry, made with {@link server}. */
|
|
184
|
+
export interface ServerMutators<TRegistry extends AnyRegistry, O> {
|
|
185
|
+
readonly registry: TRegistry;
|
|
186
|
+
readonly overrides: O;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type AnyServerMutators = ServerMutators<AnyRegistry, unknown>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The registry's mutators as the server runs them, for `ZfxServer.handleMutate`. `overrides` replaces some mutators'
|
|
193
|
+
* bodies, like Zero's `defineMutators(base, overrides)`; every other mutator runs its shared body in a server
|
|
194
|
+
* transaction, so pass `{}` if nothing differs. An override runs outside the transaction, so it can do work before
|
|
195
|
+
* and after `serverTransaction.execute`, e.g. `registry.todo.create.mutator(args).pipe(serverTransaction.execute)`.
|
|
196
|
+
*/
|
|
197
|
+
export const server = <TRegistry extends AnyRegistry, const O extends Overrides<TRegistry>>(
|
|
198
|
+
registry: TRegistry,
|
|
199
|
+
overrides: O,
|
|
200
|
+
): ServerMutators<TRegistry, O> => ({ registry, overrides });
|
|
201
|
+
|
|
202
|
+
type ServerTreeRequirements<T, O, ClientTransactionId> = {
|
|
203
|
+
[K in Keys<T>]: T[K] extends AnyMutator
|
|
204
|
+
? K extends keyof O
|
|
205
|
+
? O[K] extends (args: never) => Effect.Effect<infer _A, infer _E, infer R>
|
|
206
|
+
? R
|
|
207
|
+
: never
|
|
208
|
+
: Exclude<Requirements<T[K]>, ClientTransactionId>
|
|
209
|
+
: ServerTreeRequirements<T[K], K extends keyof O ? O[K] : unknown, ClientTransactionId>;
|
|
210
|
+
}[Keys<T>];
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The services the server needs to run these mutators. Mutators that aren't overridden run in a server transaction,
|
|
214
|
+
* which provides their client transaction.
|
|
215
|
+
*/
|
|
216
|
+
export type ServerRequirements<T extends AnyServerMutators> = T extends ServerMutators<infer TRegistry, infer O>
|
|
217
|
+
? ServerTreeRequirements<TRegistry, O, Transaction<TRegistry>[typeof ContextSymbol]["Identifier"]>
|
|
218
|
+
: never;
|
|
219
|
+
|
|
220
|
+
export class ClientArgsParseError extends Data.TaggedError("ClientArgsParseError")<{
|
|
221
|
+
readonly cause: Cause.Cause<unknown>;
|
|
222
|
+
}> {
|
|
223
|
+
override get message() {
|
|
224
|
+
const error = Cause.squash(this.cause);
|
|
225
|
+
return Predicate.isError(error) && !Schema.isSchemaError(error) ? error.message : "Internal error";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** A mutator ran on a Zero instance that wasn't made with `ZfxClient.make`. */
|
|
230
|
+
export class MutatorContextNotFound extends Data.TaggedError("MutatorContextNotFound")<{
|
|
231
|
+
readonly mutator: string;
|
|
232
|
+
}> {}
|
package/src/result.ts
CHANGED
|
@@ -5,14 +5,14 @@ import * as Fn from "effect/Function";
|
|
|
5
5
|
import * as Match from "effect/Match";
|
|
6
6
|
import * as Option from "effect/Option";
|
|
7
7
|
import * as Predicate from "effect/Predicate";
|
|
8
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
8
9
|
import * as Struct from "effect/Struct";
|
|
9
|
-
import { AsyncResult } from "effect/unstable/reactivity";
|
|
10
10
|
import type { QueryResult } from "./internal/query-result.js";
|
|
11
11
|
|
|
12
12
|
/*
|
|
13
13
|
### Introduction
|
|
14
14
|
|
|
15
|
-
When using `effect/
|
|
15
|
+
When using `effect/reactivity` (atoms) with effect-zero, you will often have atoms which contain a type in the form of
|
|
16
16
|
|
|
17
17
|
`AsyncResult<QueryResult<A>, E>`
|
|
18
18
|
|
package/src/server.ts
CHANGED
|
@@ -10,10 +10,8 @@ import * as Data from "effect/Data";
|
|
|
10
10
|
import * as Deferred from "effect/Deferred";
|
|
11
11
|
import * as Effect from "effect/Effect";
|
|
12
12
|
import * as Exit from "effect/Exit";
|
|
13
|
-
import * as Fn from "effect/Function";
|
|
14
13
|
import type { NodeInspectSymbol } from "effect/Inspectable";
|
|
15
14
|
import * as Layer from "effect/Layer";
|
|
16
|
-
import * as Match from "effect/Match";
|
|
17
15
|
import * as Option from "effect/Option";
|
|
18
16
|
import * as Predicate from "effect/Predicate";
|
|
19
17
|
import * as Rec from "effect/Record";
|
|
@@ -22,8 +20,7 @@ import * as Str from "effect/String";
|
|
|
22
20
|
import type * as Unify from "effect/Unify";
|
|
23
21
|
import { TransactCallbackNotInvokedError, TransactInternalError } from "./internal/server.js";
|
|
24
22
|
import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
|
|
25
|
-
import
|
|
26
|
-
import * as Mutators from "./mutators.js";
|
|
23
|
+
import type * as Mutator from "./mutator.js";
|
|
27
24
|
import { type MakeQueryResult, QueryNameSymbol, RunQuerySymbol } from "./query.js";
|
|
28
25
|
import type * as ServerTransaction from "./server-transaction.js";
|
|
29
26
|
import { DatabaseSymbol, ServerTransactionCallbackSymbol } from "./server-transaction.js";
|
|
@@ -44,10 +41,14 @@ type _Unify = Unify.typeSymbol | Unify.unifySymbol | Unify.ignoreSymbol;
|
|
|
44
41
|
type MutateUserId = Extract<Awaited<ReturnType<typeof handleMutateRequest>>, { kind: "MutateResponse" }>["userID"];
|
|
45
42
|
type QueryUserId = Extract<Awaited<ReturnType<typeof handleQueryRequest>>, { kind: "QueryResponse" }>["userID"];
|
|
46
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Handles Zero's push endpoint, running each mutation's server implementation: its override from
|
|
46
|
+
* `ZfxMutator.server`, or else its shared body in a server transaction.
|
|
47
|
+
*/
|
|
47
48
|
export const handleMutate = Effect.fn(function* <
|
|
48
49
|
TSchema extends ZeroSchema,
|
|
49
50
|
TTransaction,
|
|
50
|
-
TMutators extends
|
|
51
|
+
TMutators extends Mutator.AnyServerMutators,
|
|
51
52
|
>({
|
|
52
53
|
transaction,
|
|
53
54
|
mutators,
|
|
@@ -64,7 +65,7 @@ export const handleMutate = Effect.fn(function* <
|
|
|
64
65
|
const ctx =
|
|
65
66
|
yield* Effect.context<
|
|
66
67
|
Exclude<
|
|
67
|
-
|
|
68
|
+
Mutator.ServerRequirements<TMutators>,
|
|
68
69
|
| (typeof transaction)[typeof ServerTransactionCallbackSymbol]["Identifier"]
|
|
69
70
|
| ServerSynchronizationContext.ServerSynchronizationContext
|
|
70
71
|
>
|
|
@@ -91,28 +92,31 @@ export const handleMutate = Effect.fn(function* <
|
|
|
91
92
|
Effect.onExit((exit) => Deferred.done(exitDeferred, exit)),
|
|
92
93
|
Effect.catchCause((cause) => Effect.die(makeApplicationError(cause))),
|
|
93
94
|
Effect.asVoid,
|
|
94
|
-
Effect.
|
|
95
|
-
(effect) => Effect.runPromise(effect, { signal }),
|
|
95
|
+
(effect) => Effect.runPromiseWith(innerCtx)(effect, { signal }),
|
|
96
96
|
),
|
|
97
97
|
),
|
|
98
98
|
catch: (error) => new TransactInternalError({ cause: Cause.fail(error) }),
|
|
99
99
|
}).pipe(Effect.flatMap((value) => Deferred.succeed(response, value)));
|
|
100
100
|
|
|
101
101
|
return yield* Deferred.poll(exitDeferred).pipe(
|
|
102
|
-
Effect.flatMap(Effect.fromOption),
|
|
103
|
-
Effect.catchTag("NoSuchElementError", () => Effect.fail(new TransactCallbackNotInvokedError())),
|
|
102
|
+
Effect.flatMap(Effect.fromOption(() => new TransactCallbackNotInvokedError())),
|
|
104
103
|
Effect.flatten,
|
|
105
104
|
);
|
|
106
105
|
}, ServerSynchronizationContext.guard);
|
|
107
106
|
|
|
108
|
-
const exit: Exit.Exit<unknown, unknown> = yield* runMutation
|
|
109
|
-
Mutators.ExtractMutatorsRequirements<TMutators>
|
|
110
|
-
>(mutators, mutation).pipe(
|
|
107
|
+
const exit: Exit.Exit<unknown, unknown> = yield* runMutation(mutators, transaction, mutation).pipe(
|
|
111
108
|
ServerSynchronizationContext.finalize,
|
|
112
109
|
Effect.provide([
|
|
113
110
|
Layer.succeed(transaction[ServerTransactionCallbackSymbol], { transact }),
|
|
114
111
|
ServerSynchronizationContext.layer,
|
|
115
112
|
]),
|
|
113
|
+
Effect.withSpan("zero.mutation.handle", {
|
|
114
|
+
attributes: {
|
|
115
|
+
"zero.mutator": mutation.name,
|
|
116
|
+
"zero.clientID": mutation.clientID,
|
|
117
|
+
"zero.mutationID": mutation.id,
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
116
120
|
Effect.exit,
|
|
117
121
|
);
|
|
118
122
|
|
|
@@ -142,29 +146,41 @@ export const handleMutate = Effect.fn(function* <
|
|
|
142
146
|
});
|
|
143
147
|
});
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Runs a mutation: its override if it has one, or else its shared body in a server transaction. Like upstream, names
|
|
151
|
+
* are split on "." or "|" (the separator of Zero's legacy custom mutators), at any depth. Their services aren't tracked
|
|
152
|
+
* from here on: `handleMutate` already requires every implementation's.
|
|
153
|
+
*/
|
|
154
|
+
const runMutation = Effect.fn(function* <TSchema extends ZeroSchema, TTransaction>(
|
|
155
|
+
mutators: Mutator.AnyServerMutators,
|
|
156
|
+
transaction: ServerTransaction.Context<TSchema, TTransaction>,
|
|
157
|
+
mutation: Mutation,
|
|
158
|
+
) {
|
|
159
|
+
const { registry, overrides } = Rec.map(mutators, (root) =>
|
|
160
|
+
Arr.reduce(Str.split(mutation.name, /[.|]/), Option.some(root), (tree, key) =>
|
|
161
|
+
// Only an object has keys to walk into; the checks below make sure the walk ended at a mutator.
|
|
162
|
+
tree.pipe(Option.filter(Predicate.isObject), Option.flatMap(Rec.get(key))),
|
|
158
163
|
),
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
);
|
|
165
|
+
const mutator = yield* registry.pipe(
|
|
166
|
+
Option.filter(
|
|
167
|
+
(u): u is Mutator.Mutator<unknown, unknown, unknown, never> =>
|
|
168
|
+
Predicate.isFunction(u) && Predicate.hasProperty(u, "mutatorName"),
|
|
169
|
+
),
|
|
170
|
+
Effect.fromOption(() => new MutatorNotFoundError({ name: mutation.name })),
|
|
161
171
|
);
|
|
162
172
|
|
|
163
|
-
const args = yield* Schema.decodeUnknownEffect(mutator[
|
|
164
|
-
|
|
165
|
-
)
|
|
173
|
+
const args = yield* Schema.decodeUnknownEffect(mutator.payload)(mutation.args[0]).pipe(
|
|
174
|
+
Effect.catchTag("SchemaError", (e) => Effect.fail(new ServerArgsParseError({ cause: Cause.fail(e) }))),
|
|
175
|
+
);
|
|
166
176
|
|
|
167
|
-
return yield*
|
|
177
|
+
return yield* overrides.pipe(
|
|
178
|
+
Option.filter((u): u is (args: unknown) => Effect.Effect<unknown, unknown> => Predicate.isFunction(u)),
|
|
179
|
+
Option.match({
|
|
180
|
+
onNone: () => transaction.execute(mutator.mutator(args)),
|
|
181
|
+
onSome: (run) => run(args),
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
168
184
|
});
|
|
169
185
|
|
|
170
186
|
export const makeApplicationError = (cause: Cause.Cause<unknown>) => {
|
|
@@ -204,8 +220,7 @@ export const handleQuery = Effect.fn(function* <E, R1, R2>({
|
|
|
204
220
|
handleQueryRequest({
|
|
205
221
|
handler: (name, args) =>
|
|
206
222
|
Arr.findFirst(queries, (q) => q[QueryNameSymbol] === name).pipe(
|
|
207
|
-
Effect.fromOption,
|
|
208
|
-
Effect.catchTag("NoSuchElementError", () => Effect.fail(new QueryNotFound({ name }))),
|
|
223
|
+
Effect.fromOption(() => new QueryNotFound({ name })),
|
|
209
224
|
Effect.flatMap((query) => query[RunQuerySymbol]({ _tag: "Encoded", args })),
|
|
210
225
|
Effect.runSyncWith(ctx),
|
|
211
226
|
),
|
package/dist/client.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../src/client.test.ts"],"names":[],"mappings":""}
|