effect-zero 0.5.0 → 0.6.0-beta.1
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 +56 -38
- package/dist/client-transaction.d.ts +11 -9
- package/dist/client-transaction.d.ts.map +1 -1
- package/dist/client-transaction.js +4 -1
- package/dist/client-transaction.js.map +1 -1
- package/dist/client.d.ts +68 -66
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -10
- package/dist/client.js.map +1 -1
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +19 -0
- package/dist/client.test.js.map +1 -0
- package/dist/internal/server-synchronization-context.d.ts.map +1 -1
- package/dist/internal/server-synchronization-context.js +2 -2
- package/dist/internal/server-synchronization-context.js.map +1 -1
- package/dist/internal/server.d.ts +13 -28
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +5 -19
- package/dist/internal/server.js.map +1 -1
- package/dist/internal/server.test.js +5 -3
- package/dist/internal/server.test.js.map +1 -1
- package/dist/server-transaction.d.ts +82 -109
- package/dist/server-transaction.d.ts.map +1 -1
- package/dist/server-transaction.js +26 -74
- package/dist/server-transaction.js.map +1 -1
- package/dist/server.d.ts +207 -354
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +60 -80
- package/dist/server.js.map +1 -1
- package/dist/types/ast.d.ts +4 -0
- package/dist/types/ast.d.ts.map +1 -0
- package/dist/types/ast.js +4 -0
- package/dist/types/ast.js.map +1 -0
- package/dist/types/{queries.d.ts → custom-queries.d.ts} +19 -1
- package/dist/types/custom-queries.d.ts.map +1 -0
- package/dist/types/{queries.js → custom-queries.js} +5 -6
- package/dist/types/custom-queries.js.map +1 -0
- package/dist/types/mutate-server.d.ts +160 -0
- package/dist/types/mutate-server.d.ts.map +1 -0
- package/dist/types/mutate-server.js +29 -0
- package/dist/types/mutate-server.js.map +1 -0
- package/dist/types/mutation-id.d.ts +7 -0
- package/dist/types/mutation-id.d.ts.map +1 -0
- package/dist/types/mutation-id.js +8 -0
- package/dist/types/mutation-id.js.map +1 -0
- package/dist/types/mutation.d.ts +79 -0
- package/dist/types/mutation.d.ts.map +1 -0
- package/dist/types/mutation.js +125 -0
- package/dist/types/mutation.js.map +1 -0
- package/dist/types/push.d.ts +100 -85
- package/dist/types/push.d.ts.map +1 -1
- package/dist/types/push.js +7 -134
- package/dist/types/push.js.map +1 -1
- package/dist/types/query-server.d.ts +144 -0
- package/dist/types/query-server.d.ts.map +1 -0
- package/dist/types/query-server.js +19 -0
- package/dist/types/query-server.js.map +1 -0
- package/dist/types.test.js +4 -4
- package/dist/types.test.js.map +1 -1
- package/package.json +35 -5
- package/src/client-transaction.ts +5 -1
- package/src/client.ts +14 -15
- package/src/internal/server-synchronization-context.ts +37 -39
- package/src/internal/server.ts +8 -31
- package/src/server-transaction.ts +55 -101
- package/src/server.ts +147 -151
- package/src/types/ast.ts +4 -0
- package/src/types/{queries.ts → custom-queries.ts} +4 -6
- package/src/types/mutate-server.ts +37 -0
- package/src/types/mutation-id.ts +10 -0
- package/src/types/mutation.ts +155 -0
- package/src/types/push.ts +7 -167
- package/src/types/query-server.ts +27 -0
- package/dist/types/queries.d.ts.map +0 -1
- package/dist/types/queries.js.map +0 -1
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import type { Schema as ZeroSchema, ServerTransaction as ZeroServerTransaction } from "@rocicorp/zero";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ZQLDatabase } from "@rocicorp/zero/server";
|
|
3
3
|
import * as Cause from "effect/Cause";
|
|
4
4
|
import * as Ctx from "effect/Context";
|
|
5
5
|
import * as Data from "effect/Data";
|
|
6
|
-
import * as Deferred from "effect/Deferred";
|
|
7
6
|
import * as Effect from "effect/Effect";
|
|
8
|
-
import * as Exit from "effect/Exit";
|
|
9
7
|
import type { NodeInspectSymbol } from "effect/Inspectable";
|
|
10
8
|
import * as Layer from "effect/Layer";
|
|
11
9
|
import * as Predicate from "effect/Predicate";
|
|
12
10
|
import type * as Unify from "effect/Unify";
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
11
|
+
import * as ClientTransaction from "./client-transaction.js";
|
|
12
|
+
import type {
|
|
13
|
+
MultipleTransactionsError,
|
|
14
|
+
TransactCallbackNotInvokedError,
|
|
15
|
+
TransactInternalError,
|
|
16
|
+
} from "./internal/server.js";
|
|
17
|
+
import type * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
|
|
16
18
|
import { prefixId } from "./internal/utils.js";
|
|
17
19
|
|
|
18
|
-
// Updated to: https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
|
|
19
|
-
|
|
20
20
|
type _NodeInspectSymbol = NodeInspectSymbol;
|
|
21
21
|
type _Unify = Unify.typeSymbol | Unify.unifySymbol | Unify.ignoreSymbol;
|
|
22
22
|
|
|
23
|
+
export const DatabaseSymbol = Symbol.for(prefixId("Database"));
|
|
24
|
+
export const ServerTransactionCallbackSymbol = Symbol.for(prefixId("ServerTransactionCallback"));
|
|
25
|
+
|
|
23
26
|
// biome-ignore lint/suspicious/noExplicitAny: any client transaction (its Id literal is irrelevant here)
|
|
24
27
|
export type Context<TSchema extends ZeroSchema, TTransaction> = ReturnType<typeof make<any, TSchema, TTransaction>>;
|
|
25
28
|
|
|
@@ -28,111 +31,62 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
28
31
|
database: ZQLDatabase<TSchema, TTransaction>,
|
|
29
32
|
clientTransaction: ClientTransaction.Context<TSchema>,
|
|
30
33
|
) => {
|
|
31
|
-
class Context extends Ctx.Service<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
>()(`${id as string}/ServerTransactionContext` as const) {}
|
|
34
|
+
class Context extends Ctx.Service<Context, ZeroServerTransaction<TSchema, TTransaction>>()(
|
|
35
|
+
`${id as string}/ServerTransactionContext` as const,
|
|
36
|
+
) {}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
class ServerTransactionCallback extends Ctx.Service<
|
|
39
|
+
ServerTransactionCallback,
|
|
40
|
+
{
|
|
41
|
+
readonly transact: <A, E, R>(
|
|
42
|
+
fn: (transaction: ZeroServerTransaction<TSchema, TTransaction>) => Effect.Effect<A, E, R>,
|
|
43
|
+
) => Effect.Effect<
|
|
44
|
+
A,
|
|
45
|
+
E | TransactInternalError | TransactCallbackNotInvokedError | MultipleTransactionsError,
|
|
46
|
+
R | ServerSynchronizationContext.ServerSynchronizationContext
|
|
47
|
+
>;
|
|
48
|
+
}
|
|
49
|
+
>()(`${id as string}/ServerTransactionCallback` as const) {}
|
|
50
|
+
|
|
51
|
+
const use = Effect.fn(function* <A>(
|
|
37
52
|
fn: (
|
|
38
53
|
transaction: ZeroServerTransaction<TSchema, TTransaction>,
|
|
39
54
|
options: { readonly signal: AbortSignal },
|
|
40
55
|
) => PromiseLike<A>,
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
catch: (error) => new ServerTransactionError({ cause: Cause.fail(error) }),
|
|
47
|
-
});
|
|
56
|
+
) {
|
|
57
|
+
const transaction = yield* Context;
|
|
58
|
+
return yield* Effect.tryPromise({
|
|
59
|
+
try: (signal) => fn(transaction, { signal }),
|
|
60
|
+
catch: (error) => new TransactionUseError({ cause: Cause.fail(error) }),
|
|
48
61
|
});
|
|
62
|
+
});
|
|
49
63
|
|
|
50
64
|
const execute = Effect.fn(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Effect.provide([
|
|
62
|
-
Layer.succeed(Context, { transaction, transactionHooks }),
|
|
63
|
-
Layer.succeed(clientTransaction.Context, transaction),
|
|
64
|
-
]),
|
|
65
|
-
(effect) => Effect.runPromiseExitWith(ctx)(effect, { signal }),
|
|
66
|
-
);
|
|
67
|
-
Deferred.doneUnsafe(result, exit);
|
|
68
|
-
if (Exit.isFailure(exit)) {
|
|
69
|
-
// This error's purpose is to differentiate between "external" errors
|
|
70
|
-
// that originate from the user-defined mutator code and "internal" errors
|
|
71
|
-
// that originate from our own code and the Zero API.
|
|
72
|
-
// Both types are caught in the "catch" block below, but at this point we only need to handle
|
|
73
|
-
// the "internal" errors wrapping them in a `ZeroDatabaseError`, because "external" errors
|
|
74
|
-
// are already covered by passing the Exit result to the Deferred, which is why
|
|
75
|
-
// we have the ServerTransactionUserError silenced below in the pipe.
|
|
76
|
-
throw new ServerTransactionUserError();
|
|
77
|
-
}
|
|
78
|
-
return exit.value;
|
|
79
|
-
}, transactionInput),
|
|
80
|
-
catch: (error) => {
|
|
81
|
-
if (ServerTransactionUserError.is(error)) {
|
|
82
|
-
return error;
|
|
83
|
-
}
|
|
84
|
-
// This is for errors that occur when calling `database.transaction` despite the provided `effect` succeeding.
|
|
85
|
-
// This can be caused by e.g. the database connection timing out or other database-related issues.
|
|
86
|
-
return new ZeroDatabaseError({ cause: Cause.fail(error) });
|
|
87
|
-
},
|
|
88
|
-
}).pipe(Effect.catchTag("ServerTransactionUserError", () => Effect.void));
|
|
89
|
-
|
|
90
|
-
return yield* Deferred.await(result);
|
|
91
|
-
}, ServerSynchronizationContext.guard);
|
|
92
|
-
|
|
93
|
-
const checkAndIncrementLastMutationId = Effect.gen(function* () {
|
|
94
|
-
const { transactionHooks } = yield* Context;
|
|
95
|
-
const { clientID, mutationID: receivedMutationID } = yield* ServerTransactionInput;
|
|
96
|
-
|
|
97
|
-
const { lastMutationID } = yield* Effect.tryPromise({
|
|
98
|
-
try: () => transactionHooks.updateClientMutationID(),
|
|
99
|
-
catch: (error) => new UpdateClientMutationIdError({ cause: Cause.fail(error) }),
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (receivedMutationID < lastMutationID) {
|
|
103
|
-
return yield* new MutationAlreadyProcessedError({
|
|
104
|
-
clientID,
|
|
105
|
-
received: receivedMutationID,
|
|
106
|
-
actual: lastMutationID,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
if (receivedMutationID > lastMutationID) {
|
|
110
|
-
return yield* new OutOfOrderMutationError({
|
|
111
|
-
clientID,
|
|
112
|
-
receivedMutationID,
|
|
113
|
-
lastMutationID,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
65
|
+
const { transact } = yield* ServerTransactionCallback;
|
|
66
|
+
return yield* transact((transaction) =>
|
|
67
|
+
effect.pipe(
|
|
68
|
+
// Provide the live transaction under both the server- and client-transaction tags.
|
|
69
|
+
Effect.provide([
|
|
70
|
+
Layer.succeed(Context, transaction),
|
|
71
|
+
Layer.succeed(clientTransaction[ClientTransaction.ContextSymbol], transaction),
|
|
72
|
+
]),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
116
75
|
});
|
|
117
76
|
|
|
118
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
use,
|
|
79
|
+
execute,
|
|
80
|
+
[DatabaseSymbol]: database,
|
|
81
|
+
[ServerTransactionCallbackSymbol]: ServerTransactionCallback,
|
|
82
|
+
};
|
|
119
83
|
};
|
|
120
84
|
|
|
121
|
-
class
|
|
85
|
+
class TransactionUseError extends Data.TaggedError("TransactionUseError")<{
|
|
122
86
|
readonly cause: Cause.Cause<unknown>;
|
|
123
|
-
}> {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}> {}
|
|
128
|
-
class ZeroDatabaseError extends Data.TaggedError("ZeroDatabaseError")<{
|
|
129
|
-
readonly cause: Cause.Cause<unknown>;
|
|
130
|
-
}> {}
|
|
131
|
-
|
|
132
|
-
const ServerTransactionUserErrorTypeId = Symbol.for(prefixId("ServerTransactionUserError"));
|
|
133
|
-
class ServerTransactionUserError extends Data.TaggedError("ServerTransactionUserError") {
|
|
134
|
-
readonly [ServerTransactionUserErrorTypeId] = ServerTransactionUserErrorTypeId;
|
|
135
|
-
static is(e: unknown): e is ServerTransactionUserError {
|
|
136
|
-
return Predicate.hasProperty(e, ServerTransactionUserErrorTypeId);
|
|
87
|
+
}> {
|
|
88
|
+
override get message() {
|
|
89
|
+
const error = Cause.squash(this.cause);
|
|
90
|
+
return Predicate.isError(error) ? error.message : "Internal error";
|
|
137
91
|
}
|
|
138
92
|
}
|
package/src/server.ts
CHANGED
|
@@ -1,83 +1,148 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ReadonlyJSONValue,
|
|
3
|
+
Schema as ZeroSchema,
|
|
4
|
+
ServerTransaction as ZeroServerTransaction,
|
|
5
|
+
} from "@rocicorp/zero";
|
|
6
|
+
import { ApplicationError, handleMutateRequest, handleQueryRequest, OutOfOrderMutation } from "@rocicorp/zero/server";
|
|
3
7
|
import * as Arr from "effect/Array";
|
|
4
8
|
import * as Cause from "effect/Cause";
|
|
5
9
|
import * as Data from "effect/Data";
|
|
10
|
+
import * as Deferred from "effect/Deferred";
|
|
6
11
|
import * as Effect from "effect/Effect";
|
|
7
12
|
import * as Exit from "effect/Exit";
|
|
8
13
|
import * as Fn from "effect/Function";
|
|
9
14
|
import type { NodeInspectSymbol } from "effect/Inspectable";
|
|
15
|
+
import * as Layer from "effect/Layer";
|
|
10
16
|
import * as Match from "effect/Match";
|
|
11
17
|
import * as Option from "effect/Option";
|
|
12
18
|
import * as Predicate from "effect/Predicate";
|
|
13
19
|
import * as Rec from "effect/Record";
|
|
14
20
|
import * as Schema from "effect/Schema";
|
|
15
|
-
import * as Stream from "effect/Stream";
|
|
16
21
|
import * as Str from "effect/String";
|
|
17
22
|
import type * as Unify from "effect/Unify";
|
|
18
|
-
import {
|
|
23
|
+
import { TransactCallbackNotInvokedError, TransactInternalError } from "./internal/server.js";
|
|
19
24
|
import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
|
|
20
|
-
import { normalizeArgs
|
|
25
|
+
import { normalizeArgs } from "./internal/utils.js";
|
|
21
26
|
import * as Mutators from "./mutators.js";
|
|
22
27
|
import { type MakeQueryResult, QueryNameSymbol, RunQuerySymbol } from "./query.js";
|
|
23
28
|
import type * as ServerTransaction from "./server-transaction.js";
|
|
24
|
-
import
|
|
25
|
-
import type { TransformRequestMessage } from "./types/queries.js";
|
|
29
|
+
import { DatabaseSymbol, ServerTransactionCallbackSymbol } from "./server-transaction.js";
|
|
30
|
+
import type { TransformRequestMessage } from "./types/custom-queries.js";
|
|
31
|
+
import type { MutateParams } from "./types/mutate-server.js";
|
|
32
|
+
import type { Mutation } from "./types/mutation.js";
|
|
33
|
+
import type { PushBody } from "./types/push.js";
|
|
26
34
|
|
|
27
35
|
// Updated to:
|
|
28
|
-
// https://github.com/rocicorp/mono/blob/
|
|
29
|
-
// https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/process-mutations.ts
|
|
36
|
+
// https://github.com/rocicorp/mono/blob/0eeabd495a26d0d67b9a5a81c424d8a76ef004b7/packages/zero-server/src/process-mutations.ts#L214
|
|
30
37
|
|
|
31
38
|
type _NodeInspectSymbol = NodeInspectSymbol;
|
|
32
39
|
type _Unify = Unify.typeSymbol | Unify.unifySymbol | Unify.ignoreSymbol;
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
// The userId type, pulled from the upstream handlers. We read it off the response type rather than
|
|
42
|
+
// `Parameters<...>[0]` because the handlers are overloaded (so `Parameters` resolves to the wrong
|
|
43
|
+
// overload) and their argument type isn't exported.
|
|
44
|
+
type MutateUserId = Extract<Awaited<ReturnType<typeof handleMutateRequest>>, { kind: "MutateResponse" }>["userID"];
|
|
45
|
+
type QueryUserId = Extract<Awaited<ReturnType<typeof handleQueryRequest>>, { kind: "QueryResponse" }>["userID"];
|
|
46
|
+
|
|
47
|
+
export const handleMutate = Effect.fn(function* <
|
|
35
48
|
TSchema extends ZeroSchema,
|
|
36
49
|
TTransaction,
|
|
37
50
|
TMutators extends Mutators.AnyMutators,
|
|
38
|
-
>(
|
|
39
|
-
transaction
|
|
40
|
-
mutators
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
>({
|
|
52
|
+
transaction,
|
|
53
|
+
mutators,
|
|
54
|
+
query,
|
|
55
|
+
body,
|
|
56
|
+
userId,
|
|
57
|
+
}: {
|
|
58
|
+
transaction: ServerTransaction.Context<TSchema, TTransaction>;
|
|
59
|
+
mutators: TMutators;
|
|
60
|
+
query: MutateParams;
|
|
61
|
+
body: PushBody;
|
|
62
|
+
userId: MutateUserId;
|
|
63
|
+
}) {
|
|
64
|
+
const ctx =
|
|
65
|
+
yield* Effect.context<
|
|
66
|
+
Exclude<
|
|
67
|
+
Mutators.ExtractMutatorsRequirements<TMutators>,
|
|
68
|
+
| (typeof transaction)[typeof ServerTransactionCallbackSymbol]["Identifier"]
|
|
69
|
+
| ServerSynchronizationContext.ServerSynchronizationContext
|
|
70
|
+
>
|
|
71
|
+
>();
|
|
47
72
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
return yield* Effect.tryPromise({
|
|
74
|
+
try: () =>
|
|
75
|
+
handleMutateRequest({
|
|
76
|
+
dbProvider: transaction[DatabaseSymbol],
|
|
77
|
+
handler: (transact_, mutation) =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const response = yield* Deferred.make<Awaited<ReturnType<typeof transact_>>>();
|
|
80
|
+
|
|
81
|
+
const transact = Effect.fn(function* <A, E, R>(
|
|
82
|
+
fn: (transaction: ZeroServerTransaction<TSchema, TTransaction>) => Effect.Effect<A, E, R>,
|
|
83
|
+
) {
|
|
84
|
+
const innerCtx = yield* Effect.context<R>();
|
|
85
|
+
const exitDeferred = yield* Deferred.make<A, E>();
|
|
86
|
+
|
|
87
|
+
yield* Effect.tryPromise({
|
|
88
|
+
try: (signal) =>
|
|
89
|
+
transact_((transaction) =>
|
|
90
|
+
fn(transaction).pipe(
|
|
91
|
+
Effect.onExit((exit) => Deferred.done(exitDeferred, exit)),
|
|
92
|
+
Effect.catchCause((cause) => Effect.die(makeApplicationError(cause))),
|
|
93
|
+
Effect.asVoid,
|
|
94
|
+
Effect.provide(innerCtx),
|
|
95
|
+
(effect) => Effect.runPromise(effect, { signal }),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
catch: (error) => new TransactInternalError({ cause: Cause.fail(error) }),
|
|
99
|
+
}).pipe(Effect.flatMap((value) => Deferred.succeed(response, value)));
|
|
100
|
+
|
|
101
|
+
return yield* Deferred.poll(exitDeferred).pipe(
|
|
102
|
+
Effect.flatMap(Effect.fromOption),
|
|
103
|
+
Effect.catchTag("NoSuchElementError", () => Effect.fail(new TransactCallbackNotInvokedError())),
|
|
104
|
+
Effect.flatten,
|
|
105
|
+
);
|
|
106
|
+
}, ServerSynchronizationContext.guard);
|
|
107
|
+
|
|
108
|
+
const exit: Exit.Exit<unknown, unknown> = yield* runMutation<
|
|
109
|
+
Mutators.ExtractMutatorsRequirements<TMutators>
|
|
110
|
+
>(mutators, mutation).pipe(
|
|
111
|
+
ServerSynchronizationContext.finalize,
|
|
112
|
+
Effect.provide([
|
|
113
|
+
Layer.succeed(transaction[ServerTransactionCallbackSymbol], { transact }),
|
|
114
|
+
ServerSynchronizationContext.layer,
|
|
115
|
+
]),
|
|
116
|
+
Effect.exit,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return yield* Deferred.poll(response).pipe(
|
|
120
|
+
Effect.flatMap(Effect.fromOption),
|
|
121
|
+
Effect.catchTag("NoSuchElementError", () =>
|
|
122
|
+
Effect.failCause(Exit.isFailure(exit) ? exit.cause : Cause.empty),
|
|
123
|
+
),
|
|
124
|
+
Effect.flatten,
|
|
125
|
+
);
|
|
126
|
+
}).pipe(
|
|
127
|
+
Effect.catchCause((cause) => {
|
|
128
|
+
const error = cause.pipe(Cause.squash, (squashed) =>
|
|
129
|
+
TransactInternalError.is(squashed) ? Cause.squash(squashed.cause) : squashed,
|
|
130
|
+
);
|
|
131
|
+
// Split out-of-order mutations from application errors, as `handleMutateRequest` expects.
|
|
132
|
+
return Effect.fail(error instanceof OutOfOrderMutation ? error : makeApplicationError(Cause.fail(error)));
|
|
133
|
+
}),
|
|
134
|
+
Effect.provide(ctx),
|
|
135
|
+
Effect.runPromise,
|
|
59
136
|
),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
clientGroupID: request.clientGroupID,
|
|
64
|
-
upstreamSchema: params.schema,
|
|
65
|
-
}),
|
|
66
|
-
Effect.provide(ServerSynchronizationContext.layer),
|
|
67
|
-
);
|
|
137
|
+
query,
|
|
138
|
+
body: body as ReadonlyJSONValue,
|
|
139
|
+
userID: userId,
|
|
68
140
|
}),
|
|
69
|
-
),
|
|
70
|
-
|
|
71
|
-
// If the mutation has already been processed or if it returns an application error,
|
|
72
|
-
// we continue processing the next mutation.
|
|
73
|
-
Stream.takeUntil(({ result }) => Predicate.hasProperty(result, "error") && result.error === "oooMutation"),
|
|
74
|
-
Stream.runCollect,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
return { mutations: responses } satisfies Types.PushResponse;
|
|
141
|
+
catch: (e) => new HandleMutateError({ cause: Cause.fail(e) }),
|
|
142
|
+
});
|
|
78
143
|
});
|
|
79
144
|
|
|
80
|
-
const
|
|
145
|
+
const runMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R>, mutation: Mutation) {
|
|
81
146
|
// Support both "namespace|name" and "namespace.name" formats, and single-segment names.
|
|
82
147
|
const [namespace, name] = mutation.name.includes("|") ? Str.split(mutation.name, "|") : Str.split(mutation.name, ".");
|
|
83
148
|
|
|
@@ -99,117 +164,58 @@ const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R
|
|
|
99
164
|
normalizeArgs(mutation.args[0]),
|
|
100
165
|
).pipe(Effect.catchTag("SchemaError", (e) => Effect.fail(new ServerArgsParseError({ cause: Cause.fail(e) }))));
|
|
101
166
|
|
|
102
|
-
return yield* mutator(args)
|
|
103
|
-
ServerSynchronizationContext.finalize,
|
|
104
|
-
Effect.as<Types.MutationResult>({}),
|
|
105
|
-
Effect.catchIf(OutOfOrderMutationError.is, (e) =>
|
|
106
|
-
Effect.logError(e.message).pipe(
|
|
107
|
-
Effect.as({ error: "oooMutation", details: e.message } satisfies Types.ZeroError),
|
|
108
|
-
),
|
|
109
|
-
),
|
|
110
|
-
Effect.catchIf(MutationAlreadyProcessedError.is, (e) =>
|
|
111
|
-
Effect.logWarning(e.message).pipe(
|
|
112
|
-
Effect.as({ error: "alreadyProcessed", details: e.message } satisfies Types.ZeroError),
|
|
113
|
-
),
|
|
114
|
-
),
|
|
115
|
-
// Case #5 "Zero transactions then fail" / #6 "Fail before transaction"
|
|
116
|
-
// Catches all errors that are produced before the transaction is executed
|
|
117
|
-
Effect.catchCause((cause) => Effect.fail(new MutationUserError({ cause }))),
|
|
118
|
-
);
|
|
167
|
+
return yield* mutator(args);
|
|
119
168
|
});
|
|
120
169
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
yield* Effect.logError(`Unexpected error processing mutation ${mutationID} for client ${clientID}`, Cause.fail(e));
|
|
128
|
-
|
|
129
|
-
const errorMessage = Match.value(e).pipe(
|
|
130
|
-
Match.when(MutationUserError.is, (e) => e.message),
|
|
131
|
-
Match.orElse(() => "Internal error"),
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const appError = {
|
|
135
|
-
error: "app",
|
|
136
|
-
details: errorMessage,
|
|
137
|
-
} satisfies Types.AppError;
|
|
138
|
-
|
|
139
|
-
yield* transaction
|
|
140
|
-
.execute(
|
|
141
|
-
Effect.gen(function* () {
|
|
142
|
-
const { transactionHooks } = yield* transaction.Context;
|
|
143
|
-
return yield* Effect.tryPromise({
|
|
144
|
-
try: () => transactionHooks.writeMutationResult({ id: { id: mutationID, clientID }, result: appError }),
|
|
145
|
-
catch: (error) => new WriteMutationResultError({ cause: Cause.fail(error) }),
|
|
146
|
-
});
|
|
147
|
-
}),
|
|
148
|
-
)
|
|
149
|
-
.pipe(Effect.catchCause(Effect.logError));
|
|
150
|
-
|
|
151
|
-
yield* Effect.logWarning(`Mutation ${mutationID} for client ${clientID} was retried after an error: ${e}`);
|
|
152
|
-
|
|
153
|
-
return appError;
|
|
154
|
-
});
|
|
170
|
+
export const makeApplicationError = (cause: Cause.Cause<unknown>) => {
|
|
171
|
+
const error = Cause.squash(cause);
|
|
172
|
+
const message = Predicate.isError(error) ? error.message : "Internal error";
|
|
173
|
+
return new ApplicationError(message, { cause: error });
|
|
174
|
+
};
|
|
155
175
|
|
|
156
|
-
class WriteMutationResultError extends Data.TaggedError("WriteMutationResultError")<{
|
|
157
|
-
readonly cause: Cause.Cause<unknown>;
|
|
158
|
-
}> {}
|
|
159
|
-
class CustomMutationExpectedError extends Data.TaggedError("CustomMutationExpectedError") {}
|
|
160
176
|
class MutatorNotFoundError extends Data.TaggedError("MutatorNotFoundError")<{
|
|
161
177
|
readonly name: string;
|
|
162
|
-
}> {}
|
|
163
|
-
|
|
164
|
-
class ServerArgsParseError extends Data.TaggedError("ServerArgsParseError")<{
|
|
165
|
-
readonly cause: Cause.Cause<Schema.SchemaError>;
|
|
166
|
-
}> {}
|
|
167
|
-
|
|
168
|
-
const MutationUserErrorTypeId = Symbol.for(prefixId("MutationUserError"));
|
|
169
|
-
class MutationUserError extends Data.TaggedError("MutationUserError")<{
|
|
170
|
-
readonly cause: Cause.Cause<unknown>;
|
|
171
178
|
}> {
|
|
172
|
-
readonly [MutationUserErrorTypeId] = MutationUserErrorTypeId;
|
|
173
|
-
static is(e: unknown): e is MutationUserError {
|
|
174
|
-
return Predicate.hasProperty(e, MutationUserErrorTypeId);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
179
|
override get message() {
|
|
178
|
-
|
|
179
|
-
return err instanceof Error ? err.message : "exception was not of type `Error`";
|
|
180
|
+
return `Mutator not found: ${this.name}`;
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
) {
|
|
184
|
+
class ServerArgsParseError extends Data.TaggedError("ServerArgsParseError")<{
|
|
185
|
+
readonly cause: Cause.Cause<unknown>;
|
|
186
|
+
}> {}
|
|
187
|
+
|
|
188
|
+
class HandleMutateError extends Data.TaggedError("HandleMutateError")<{ readonly cause: Cause.Cause<unknown> }> {}
|
|
189
|
+
|
|
190
|
+
export const handleQuery = Effect.fn(function* <E, R1, R2>({
|
|
191
|
+
queries,
|
|
192
|
+
schema,
|
|
193
|
+
body,
|
|
194
|
+
userId,
|
|
195
|
+
}: {
|
|
196
|
+
queries: MakeQueryResult<E, R1, R2>[];
|
|
197
|
+
schema: ZeroSchema;
|
|
198
|
+
body: TransformRequestMessage;
|
|
199
|
+
userId: QueryUserId;
|
|
200
|
+
}) {
|
|
188
201
|
const ctx = yield* Effect.context<R1 | R2>();
|
|
189
202
|
return yield* Effect.tryPromise({
|
|
190
203
|
try: () =>
|
|
191
|
-
handleQueryRequest(
|
|
192
|
-
(name, args) =>
|
|
204
|
+
handleQueryRequest({
|
|
205
|
+
handler: (name, args) =>
|
|
193
206
|
Arr.findFirst(queries, (q) => q[QueryNameSymbol] === name).pipe(
|
|
194
207
|
Effect.fromOption,
|
|
195
208
|
Effect.catchTag("NoSuchElementError", () => Effect.fail(new QueryNotFound({ name }))),
|
|
196
209
|
Effect.flatMap((query) => query[RunQuerySymbol]({ _tag: "Encoded", args })),
|
|
197
|
-
Effect.
|
|
198
|
-
(exit) => {
|
|
199
|
-
if (Exit.isFailure(exit)) {
|
|
200
|
-
throw new QueryUserError<E>({ cause: exit.cause });
|
|
201
|
-
}
|
|
202
|
-
return exit.value;
|
|
203
|
-
},
|
|
210
|
+
Effect.runSyncWith(ctx),
|
|
204
211
|
),
|
|
205
212
|
schema,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
),
|
|
213
|
+
// `query` params are unused by the query endpoint at runtime but required by the options type.
|
|
214
|
+
query: {},
|
|
215
|
+
body: body as ReadonlyJSONValue,
|
|
216
|
+
userID: userId,
|
|
217
|
+
}),
|
|
218
|
+
catch: (e) => new HandleQueryError({ cause: Cause.fail(e) }),
|
|
213
219
|
});
|
|
214
220
|
});
|
|
215
221
|
|
|
@@ -219,14 +225,4 @@ class QueryNotFound extends Data.TaggedError("QueryNotFound")<{ name: string }>
|
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
|
|
222
|
-
|
|
223
|
-
class QueryUserError<E> extends Data.TaggedError("QueryUserError")<{
|
|
224
|
-
cause: Cause.Cause<E | Schema.SchemaError | QueryNotFound>;
|
|
225
|
-
}> {
|
|
226
|
-
readonly [QueryUserErrorTypeId] = QueryUserErrorTypeId;
|
|
227
|
-
static is<E>(e: unknown): e is QueryUserError<E> {
|
|
228
|
-
return Predicate.hasProperty(e, QueryUserErrorTypeId);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
class QueryRequestError extends Data.TaggedError("QueryRequestError")<{ cause: Cause.Cause<unknown> }> {}
|
|
228
|
+
class HandleQueryError extends Data.TaggedError("HandleQueryError")<{ cause: Cause.Cause<unknown> }> {}
|
package/src/types/ast.ts
ADDED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
|
+
import { Ast } from "./ast.js";
|
|
2
3
|
import { JsonSchema } from "./common.js";
|
|
3
4
|
import { TransformFailedBody } from "./error.js";
|
|
4
5
|
|
|
5
6
|
// The code below was converted to Effect Schema from:
|
|
6
|
-
// https://github.com/rocicorp/mono/blob/
|
|
7
|
-
|
|
8
|
-
/** TODO: Defined as unknown for now */
|
|
9
|
-
const Ast = Schema.Unknown;
|
|
7
|
+
// https://github.com/rocicorp/mono/blob/74273167af5d15ed07045e04165cb04d3983d44f/packages/zero-protocol/src/custom-queries.ts
|
|
10
8
|
|
|
11
9
|
const TransformRequestBody = Schema.Array(
|
|
12
10
|
Schema.Struct({
|
|
@@ -16,7 +14,7 @@ const TransformRequestBody = Schema.Array(
|
|
|
16
14
|
}),
|
|
17
15
|
);
|
|
18
16
|
|
|
19
|
-
const TransformedQuery = Schema.Struct({
|
|
17
|
+
export const TransformedQuery = Schema.Struct({
|
|
20
18
|
id: Schema.String,
|
|
21
19
|
name: Schema.String,
|
|
22
20
|
ast: Ast,
|
|
@@ -39,7 +37,7 @@ const ParseErroredQuery = Schema.Struct({
|
|
|
39
37
|
details: Schema.optional(JsonSchema),
|
|
40
38
|
});
|
|
41
39
|
|
|
42
|
-
const ErroredQuery = Schema.Union([AppErroredQuery, ParseErroredQuery]);
|
|
40
|
+
export const ErroredQuery = Schema.Union([AppErroredQuery, ParseErroredQuery]);
|
|
43
41
|
|
|
44
42
|
const TransformResponseBody = Schema.Array(Schema.Union([TransformedQuery, ErroredQuery]));
|
|
45
43
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { PushFailedBody } from "./error.js";
|
|
3
|
+
import { MutationResponse } from "./mutation.js";
|
|
4
|
+
import { PushError } from "./push.js";
|
|
5
|
+
|
|
6
|
+
// The code below was converted to Effect Schema from:
|
|
7
|
+
// https://github.com/rocicorp/mono/blob/74273167af5d15ed07045e04165cb04d3983d44f/packages/zero-protocol/src/mutate-server.ts
|
|
8
|
+
|
|
9
|
+
const LegacyPushSuccess = Schema.Struct({
|
|
10
|
+
mutations: Schema.Array(MutationResponse),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const LegacyPushResponse = Schema.Union([LegacyPushSuccess, PushError, PushFailedBody]);
|
|
14
|
+
|
|
15
|
+
export const MutateSuccess = Schema.Struct({
|
|
16
|
+
kind: Schema.Literal("MutateResponse"),
|
|
17
|
+
userID: Schema.optional(Schema.NullOr(Schema.String)),
|
|
18
|
+
mutations: Schema.Array(MutationResponse),
|
|
19
|
+
});
|
|
20
|
+
export type MutateSuccess = typeof MutateSuccess.Type;
|
|
21
|
+
|
|
22
|
+
export const MutateResponse = Schema.Union([
|
|
23
|
+
MutateSuccess,
|
|
24
|
+
PushFailedBody,
|
|
25
|
+
// for backwards compatibility
|
|
26
|
+
LegacyPushResponse,
|
|
27
|
+
]);
|
|
28
|
+
export type MutateResponse = typeof MutateResponse.Type;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The schema for the querystring parameters of the custom mutate endpoint.
|
|
32
|
+
*/
|
|
33
|
+
export const MutateParams = Schema.Struct({
|
|
34
|
+
schema: Schema.String,
|
|
35
|
+
appID: Schema.String,
|
|
36
|
+
});
|
|
37
|
+
export type MutateParams = typeof MutateParams.Type;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
// The code below was converted to Effect Schema from:
|
|
4
|
+
// https://github.com/rocicorp/mono/blob/74273167af5d15ed07045e04165cb04d3983d44f/packages/zero-protocol/src/mutation-id.ts
|
|
5
|
+
|
|
6
|
+
export const MutationId = Schema.Struct({
|
|
7
|
+
id: Schema.Number,
|
|
8
|
+
clientID: Schema.String,
|
|
9
|
+
});
|
|
10
|
+
export type MutationId = typeof MutationId.Type;
|