effect-zero 0.5.0 → 0.6.0-beta.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.
Files changed (76) hide show
  1. package/README.md +56 -38
  2. package/dist/client-transaction.d.ts +11 -9
  3. package/dist/client-transaction.d.ts.map +1 -1
  4. package/dist/client-transaction.js +4 -1
  5. package/dist/client-transaction.js.map +1 -1
  6. package/dist/client.d.ts +68 -66
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +7 -10
  9. package/dist/client.js.map +1 -1
  10. package/dist/client.test.d.ts +2 -0
  11. package/dist/client.test.d.ts.map +1 -0
  12. package/dist/client.test.js +19 -0
  13. package/dist/client.test.js.map +1 -0
  14. package/dist/internal/server-synchronization-context.d.ts.map +1 -1
  15. package/dist/internal/server-synchronization-context.js +2 -2
  16. package/dist/internal/server-synchronization-context.js.map +1 -1
  17. package/dist/internal/server.d.ts +13 -28
  18. package/dist/internal/server.d.ts.map +1 -1
  19. package/dist/internal/server.js +5 -19
  20. package/dist/internal/server.js.map +1 -1
  21. package/dist/internal/server.test.js +5 -3
  22. package/dist/internal/server.test.js.map +1 -1
  23. package/dist/server-transaction.d.ts +82 -109
  24. package/dist/server-transaction.d.ts.map +1 -1
  25. package/dist/server-transaction.js +26 -74
  26. package/dist/server-transaction.js.map +1 -1
  27. package/dist/server.d.ts +207 -354
  28. package/dist/server.d.ts.map +1 -1
  29. package/dist/server.js +60 -80
  30. package/dist/server.js.map +1 -1
  31. package/dist/types/ast.d.ts +4 -0
  32. package/dist/types/ast.d.ts.map +1 -0
  33. package/dist/types/ast.js +4 -0
  34. package/dist/types/ast.js.map +1 -0
  35. package/dist/types/{queries.d.ts → custom-queries.d.ts} +19 -1
  36. package/dist/types/custom-queries.d.ts.map +1 -0
  37. package/dist/types/{queries.js → custom-queries.js} +5 -6
  38. package/dist/types/custom-queries.js.map +1 -0
  39. package/dist/types/mutate-server.d.ts +160 -0
  40. package/dist/types/mutate-server.d.ts.map +1 -0
  41. package/dist/types/mutate-server.js +29 -0
  42. package/dist/types/mutate-server.js.map +1 -0
  43. package/dist/types/mutation-id.d.ts +7 -0
  44. package/dist/types/mutation-id.d.ts.map +1 -0
  45. package/dist/types/mutation-id.js +8 -0
  46. package/dist/types/mutation-id.js.map +1 -0
  47. package/dist/types/mutation.d.ts +79 -0
  48. package/dist/types/mutation.d.ts.map +1 -0
  49. package/dist/types/mutation.js +125 -0
  50. package/dist/types/mutation.js.map +1 -0
  51. package/dist/types/push.d.ts +100 -85
  52. package/dist/types/push.d.ts.map +1 -1
  53. package/dist/types/push.js +7 -134
  54. package/dist/types/push.js.map +1 -1
  55. package/dist/types/query-server.d.ts +144 -0
  56. package/dist/types/query-server.d.ts.map +1 -0
  57. package/dist/types/query-server.js +19 -0
  58. package/dist/types/query-server.js.map +1 -0
  59. package/dist/types.test.js +4 -4
  60. package/dist/types.test.js.map +1 -1
  61. package/package.json +35 -5
  62. package/src/client-transaction.ts +5 -1
  63. package/src/client.ts +14 -15
  64. package/src/internal/server-synchronization-context.ts +37 -39
  65. package/src/internal/server.ts +8 -31
  66. package/src/server-transaction.ts +55 -101
  67. package/src/server.ts +147 -151
  68. package/src/types/ast.ts +4 -0
  69. package/src/types/{queries.ts → custom-queries.ts} +4 -6
  70. package/src/types/mutate-server.ts +37 -0
  71. package/src/types/mutation-id.ts +10 -0
  72. package/src/types/mutation.ts +155 -0
  73. package/src/types/push.ts +7 -167
  74. package/src/types/query-server.ts +27 -0
  75. package/dist/types/queries.d.ts.map +0 -1
  76. 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 { TransactionProviderHooks, ZQLDatabase } from "@rocicorp/zero/server";
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 type * as ClientTransaction from "./client-transaction.js";
14
- import { MutationAlreadyProcessedError, OutOfOrderMutationError, ServerTransactionInput } from "./internal/server.js";
15
- import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
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
- Context,
33
- { transaction: ZeroServerTransaction<TSchema, TTransaction>; transactionHooks: TransactionProviderHooks }
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
- const use = <A>(
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
- Effect.gen(function* () {
43
- const ctx = yield* Context;
44
- return yield* Effect.tryPromise({
45
- try: (signal) => fn(ctx.transaction, { signal }),
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 ctx = yield* Effect.context<
52
- ServerTransactionInput | Exclude<R, (typeof clientTransaction.Context)["Identifier"] | Context>
53
- >();
54
- const result = yield* Deferred.make<A, E | Effect.Error<typeof checkAndIncrementLastMutationId>>();
55
-
56
- const transactionInput = yield* ServerTransactionInput;
57
- yield* Effect.tryPromise({
58
- try: (signal) =>
59
- database.transaction(async (transaction, transactionHooks) => {
60
- const exit = await Effect.flatMap(checkAndIncrementLastMutationId, () => effect).pipe(
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 { Context, use, execute };
77
+ return {
78
+ use,
79
+ execute,
80
+ [DatabaseSymbol]: database,
81
+ [ServerTransactionCallbackSymbol]: ServerTransactionCallback,
82
+ };
119
83
  };
120
84
 
121
- class ServerTransactionError extends Data.TaggedError("ServerTransactionError")<{
85
+ class TransactionUseError extends Data.TaggedError("TransactionUseError")<{
122
86
  readonly cause: Cause.Cause<unknown>;
123
- }> {}
124
-
125
- class UpdateClientMutationIdError extends Data.TaggedError("UpdateClientMutationIdError")<{
126
- readonly cause: Cause.Cause<unknown>;
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 { ReadonlyJSONValue, Schema as ZeroSchema } from "@rocicorp/zero";
2
- import { handleQueryRequest } from "@rocicorp/zero/server";
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 { MutationAlreadyProcessedError, OutOfOrderMutationError, ServerTransactionInput } from "./internal/server.js";
23
+ import { TransactCallbackNotInvokedError, TransactInternalError } from "./internal/server.js";
19
24
  import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
20
- import { normalizeArgs, prefixId } from "./internal/utils.js";
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 * as Types from "./types/push.js";
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/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
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
- export const processPush = Effect.fn(function* <
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: ServerTransaction.Context<TSchema, TTransaction>,
40
- mutators: TMutators,
41
- params: Types.PushParams,
42
- request: Types.PushBody,
43
- ) {
44
- if (request.pushVersion !== 1) {
45
- return { error: "unsupportedPushVersion" as const };
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
- const responses = yield* Stream.fromIterable(request.mutations).pipe(
49
- Stream.mapEffect(
50
- Effect.fn(function* (mutation) {
51
- if (mutation.type !== "custom") {
52
- return yield* new CustomMutationExpectedError();
53
- }
54
-
55
- return yield* processMutation<Mutators.ExtractMutatorsRequirements<TMutators>>(mutators, mutation).pipe(
56
- Effect.catch((e) => processMutationError(transaction, e)),
57
- Effect.map((result) =>
58
- Types.MutationResponse.make({ id: { id: mutation.id, clientID: mutation.clientID }, result }),
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
- Effect.provideService(ServerTransactionInput, {
61
- clientID: mutation.clientID,
62
- mutationID: mutation.id,
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
- // We only stop processing if the mutation is out of order.
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 processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R>, mutation: Types.Mutation) {
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).pipe(
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 processMutationError = Effect.fn(function* <TSchema extends ZeroSchema, TTransaction>(
122
- transaction: ServerTransaction.Context<TSchema, TTransaction>,
123
- e: unknown,
124
- ) {
125
- const { clientID, mutationID } = yield* ServerTransactionInput;
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
- const err = Cause.squash(this.cause);
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
- export const handleQuery = Effect.fn(function* <E, R1, R2>(
184
- queries: MakeQueryResult<E, R1, R2>[],
185
- schema: ZeroSchema,
186
- payload: TransformRequestMessage,
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.runSyncExitWith(ctx),
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
- payload as ReadonlyJSONValue,
207
- ),
208
- catch: (e) =>
209
- Option.liftPredicate(e, QueryUserError.is<E>).pipe(
210
- Option.flatMap((e) => Cause.findErrorOption(e.cause)),
211
- Option.getOrElse(() => new QueryRequestError({ cause: Cause.fail(e) })),
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
- const QueryUserErrorTypeId = Symbol.for(prefixId("QueryUserError"));
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> }> {}
@@ -0,0 +1,4 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ /** TODO: Defined as unknown for now */
4
+ export const Ast = Schema.Unknown;
@@ -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/8e0f600fb3a9185facf60cfd4971d260b266690e/packages/zero-protocol/src/custom-queries.ts
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;