effect-zero 0.1.0 → 0.2.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 (66) hide show
  1. package/README.md +138 -95
  2. package/dist/client-transaction.d.ts +22 -0
  3. package/dist/client-transaction.d.ts.map +1 -0
  4. package/dist/client-transaction.js +15 -0
  5. package/dist/client-transaction.js.map +1 -0
  6. package/dist/client.d.ts +10 -38
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +23 -50
  9. package/dist/client.js.map +1 -1
  10. package/dist/internal/server.d.ts +54 -0
  11. package/dist/internal/server.d.ts.map +1 -0
  12. package/dist/internal/server.js +64 -0
  13. package/dist/internal/server.js.map +1 -0
  14. package/dist/internal/utils.d.ts.map +1 -0
  15. package/dist/internal/utils.js.map +1 -0
  16. package/dist/mutators.d.ts +18 -23
  17. package/dist/mutators.d.ts.map +1 -1
  18. package/dist/mutators.js +9 -18
  19. package/dist/mutators.js.map +1 -1
  20. package/dist/query.d.ts +30 -0
  21. package/dist/query.d.ts.map +1 -0
  22. package/dist/query.js +54 -0
  23. package/dist/query.js.map +1 -0
  24. package/dist/server-transaction.d.ts +42 -0
  25. package/dist/server-transaction.d.ts.map +1 -0
  26. package/dist/server-transaction.js +88 -0
  27. package/dist/server-transaction.js.map +1 -0
  28. package/dist/server.d.ts +111 -168
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +80 -201
  31. package/dist/server.js.map +1 -1
  32. package/dist/snapshot.d.ts.map +1 -1
  33. package/dist/snapshot.js +3 -1
  34. package/dist/snapshot.js.map +1 -1
  35. package/dist/types/common.d.ts +4 -0
  36. package/dist/types/common.d.ts.map +1 -0
  37. package/dist/types/common.js +4 -0
  38. package/dist/types/common.js.map +1 -0
  39. package/dist/{types.d.ts → types/push.d.ts} +15 -15
  40. package/dist/types/push.d.ts.map +1 -0
  41. package/dist/{types.js → types/push.js} +54 -54
  42. package/dist/types/push.js.map +1 -0
  43. package/dist/types/queries.d.ts +29 -0
  44. package/dist/types/queries.d.ts.map +1 -0
  45. package/dist/types/queries.js +42 -0
  46. package/dist/types/queries.js.map +1 -0
  47. package/package.json +46 -21
  48. package/src/client-transaction.ts +30 -0
  49. package/src/client.ts +66 -116
  50. package/src/internal/server.ts +90 -0
  51. package/src/mutators.ts +42 -44
  52. package/src/query.ts +117 -0
  53. package/src/server-transaction.ts +141 -0
  54. package/src/server.ts +174 -320
  55. package/src/snapshot.ts +0 -2
  56. package/src/types/common.ts +4 -0
  57. package/src/types/push.ts +197 -0
  58. package/src/types/queries.ts +56 -0
  59. package/dist/types.d.ts.map +0 -1
  60. package/dist/types.js.map +0 -1
  61. package/dist/utils.d.ts.map +0 -1
  62. package/dist/utils.js.map +0 -1
  63. package/src/types.ts +0 -201
  64. /package/dist/{utils.d.ts → internal/utils.d.ts} +0 -0
  65. /package/dist/{utils.js → internal/utils.js} +0 -0
  66. /package/src/{utils.ts → internal/utils.ts} +0 -0
package/src/server.ts CHANGED
@@ -1,9 +1,9 @@
1
- import type { Database, TransactionProviderHooks, TransactionProviderInput } from "@rocicorp/zero/pg";
1
+ import type { ReadonlyJSONValue, Schema as ZeroSchema } from "@rocicorp/zero";
2
+ import { handleGetQueriesRequest } from "@rocicorp/zero/server";
3
+ import * as Arr from "effect/Array";
2
4
  import * as Cause from "effect/Cause";
3
5
  import * as Chunk from "effect/Chunk";
4
- import * as Context from "effect/Context";
5
6
  import * as Data from "effect/Data";
6
- import * as Deferred from "effect/Deferred";
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Exit from "effect/Exit";
9
9
  import * as Fn from "effect/Function";
@@ -16,362 +16,216 @@ import * as Runtime from "effect/Runtime";
16
16
  import * as Schema from "effect/Schema";
17
17
  import * as Stream from "effect/Stream";
18
18
  import * as Str from "effect/String";
19
- import * as SynchronizedRef from "effect/SynchronizedRef";
20
- import { type AnyMutators, type ExtractMutatorsRequirements, MutatorArgsSchemaSym } from "./mutators.js";
21
19
  import {
22
- type ZeroAppError,
23
- type ZeroError,
24
- type ZeroMutation,
25
- ZeroMutationResponse,
26
- type ZeroMutationResult,
27
- type ZeroPushBody,
28
- type ZeroPushParams,
29
- type ZeroPushResponse,
30
- } from "./types.js";
31
- import { prefixId } from "./utils.js";
32
-
33
- // Updated to: https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
34
-
35
- export interface ZeroServerTransactionContext {
36
- readonly _tag: unique symbol;
37
- }
38
-
39
- // NOTE(zero): Using the "Lifting the Generic" technique.
40
- // NOTE(zero): Could choose to not accept `Database<T>` as a param and instead create some `ZeroDatabase` service inside the
41
- // `make` function, which various other functions require. However, this means that the user will have to manually
42
- // specify the `T` type parameter which seemed a bit clunky.
43
- export const makeServer = <T, I = never>(options: { database: Database<T>; clientTransaction?: Context.Tag<I, T> }) => {
44
- const ZeroServerTransactionContext = Context.Tag(prefixId("ZeroServerTransactionContext"))<
45
- ZeroServerTransactionContext,
46
- { transaction: T; transactionHooks: TransactionProviderHooks }
47
- >();
48
-
49
- // TODO: Maybe prefix this / suffix this with a tag passed in to `make`?
50
- const execute = Effect.fn(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
51
- const runtime =
52
- // TODO: Is there a cleaner way to write this?
53
- yield* Effect.runtime<Exclude<Exclude<R | ZeroTransactionInput, ZeroServerTransactionContext>, I>>();
54
- const result = yield* Deferred.make<A, E | Effect.Effect.Error<typeof checkAndIncrementLastMutationID>>();
20
+ MutationAlreadyProcessedError,
21
+ OutOfOrderMutationError,
22
+ ServerSynchronizationContext,
23
+ ServerTransactionInput,
24
+ } from "./internal/server.js";
25
+ import { prefixId } from "./internal/utils.js";
26
+ import * as Mutators from "./mutators.js";
27
+ import { type MakeQueryResult, QueryNameSymbol, RunQuerySymbol } from "./query.js";
28
+ import type * as ServerTransaction from "./server-transaction.js";
29
+ import * as Types from "./types/push.js";
30
+ import type { TransformRequestMessage } from "./types/queries.js";
31
+
32
+ type ServerTransactionContext<TSchema extends ZeroSchema, TTransaction> = Omit<
33
+ ReturnType<typeof ServerTransaction.make<string, TSchema, TTransaction>>,
34
+ "use"
35
+ >;
36
+
37
+ export const processPush = Effect.fn(function* <
38
+ TSchema extends ZeroSchema,
39
+ TTransaction,
40
+ TMutators extends Mutators.AnyMutators,
41
+ >(
42
+ transaction: ServerTransactionContext<TSchema, TTransaction>,
43
+ mutators: TMutators,
44
+ params: Types.PushParams,
45
+ request: Types.PushBody,
46
+ ) {
47
+ if (request.pushVersion !== 1) {
48
+ return { error: "unsupportedPushVersion" as const };
49
+ }
55
50
 
56
- const transactionInput = yield* ZeroTransactionInput;
57
- yield* Effect.tryPromise({
58
- try: (signal) =>
59
- options.database.transaction(async (transaction, transactionHooks) => {
60
- const exit = await Effect.zipRight(checkAndIncrementLastMutationID, effect).pipe(
61
- Effect.provideService(ZeroServerTransactionContext, { transaction, transactionHooks }),
62
- options.clientTransaction
63
- ? Effect.provideService(options.clientTransaction, transaction)
64
- : // TODO: Is there a cleaner way to write this?
65
- <A, E, R>(effect: Effect.Effect<A, E, R>) => effect as Effect.Effect<A, E, Exclude<R, I>>,
66
- (effect) => Runtime.runPromiseExit(runtime, effect, { signal }),
67
- );
68
- Deferred.unsafeDone(result, exit);
69
- return Exit.getOrElse(exit, () => {
70
- // This error's purpose is to differentiate between "external" errors
71
- // that originate from the user-defined mutator code and "internal" errors
72
- // that originate from our own code and the Zero API.
73
- // Both types are caught in the "catch" block below, but at this point we only need to handle
74
- // the "internal" errors wrapping them in a `ZeroDatabaseError`, because "external" errors
75
- // are already covered by passing the Exit result to the Deferred, which is why
76
- // we have the ZeroTransactionUserError silenced below in the pipe.
77
- throw new ZeroTransactionUserError();
78
- });
79
- }, transactionInput),
80
- catch: (error) => {
81
- if (ZeroTransactionUserError.is(error)) {
82
- return error;
51
+ const responses = yield* Stream.fromIterable(request.mutations).pipe(
52
+ Stream.mapEffect(
53
+ Effect.fn(function* (mutation) {
54
+ if (mutation.type !== "custom") {
55
+ return yield* new CustomMutationExpectedError();
83
56
  }
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("ZeroTransactionUserError", () => Effect.void));
89
-
90
- return yield* result;
91
- }, ZeroServerMutationContext.guard);
92
-
93
- const use = <A>(fn: (transaction: T, options: { readonly signal: AbortSignal }) => PromiseLike<A>) =>
94
- Effect.flatMap(ZeroServerTransactionContext, (ctx) =>
95
- Effect.tryPromise({
96
- try: (signal) => fn(ctx.transaction, { signal }),
97
- catch: Fn.identity,
57
+ return yield* processMutation<Mutators.ExtractMutatorsRequirements<TMutators>>(mutators, mutation).pipe(
58
+ Effect.catchAll((e) => processMutationError(transaction, e)),
59
+ Effect.map((result) =>
60
+ Types.MutationResponse.make({ id: { id: mutation.id, clientID: mutation.clientID }, result }),
61
+ ),
62
+ Effect.provideService(ServerTransactionInput, {
63
+ clientID: mutation.clientID,
64
+ mutationID: mutation.id,
65
+ clientGroupID: request.clientGroupID,
66
+ upstreamSchema: params.schema,
67
+ }),
68
+ );
98
69
  }),
99
- );
100
-
101
- const checkAndIncrementLastMutationID = Effect.gen(function* () {
102
- const { transactionHooks } = yield* ZeroServerTransactionContext;
103
- const { clientID, mutationID: receivedMutationID } = yield* ZeroTransactionInput;
104
-
105
- const { lastMutationID } = yield* Effect.tryPromise({
106
- try: () => transactionHooks.updateClientMutationID(),
107
- catch: (error) => new UpdateClientMutationIDError({ cause: Cause.fail(error) }),
108
- });
109
-
110
- if (receivedMutationID < lastMutationID) {
111
- return yield* new MutationAlreadyProcessedError({
112
- clientID,
113
- received: receivedMutationID,
114
- actual: lastMutationID,
115
- });
116
- }
117
- if (receivedMutationID > lastMutationID) {
118
- return yield* new OutOfOrderMutationError({
119
- clientID,
120
- receivedMutationID,
121
- lastMutationID,
122
- });
123
- }
124
- });
125
-
126
- const processPush = Effect.fn(function* <T extends AnyMutators>(
127
- mutators: T,
128
- params: ZeroPushParams,
129
- request: ZeroPushBody,
130
- ) {
131
- if (request.pushVersion !== 1) {
132
- return { error: "unsupportedPushVersion" as const };
133
- }
70
+ ),
71
+ // We only stop processing if the mutation is out of order.
72
+ // If the mutation has already been processed or if it returns an application error,
73
+ // we continue processing the next mutation.
74
+ Stream.takeUntil(({ result }) => Predicate.hasProperty(result, "error") && result.error === "oooMutation"),
75
+ Stream.runCollect,
76
+ );
134
77
 
135
- const responses = yield* Stream.fromIterable(request.mutations).pipe(
136
- Stream.mapEffect(
137
- Effect.fn(function* (mutation) {
138
- if (mutation.type !== "custom") {
139
- return yield* new CustomMutationExpectedError({});
140
- }
141
- return yield* processMutation<ExtractMutatorsRequirements<T>>(mutators, mutation).pipe(
142
- Effect.map((result) =>
143
- ZeroMutationResponse.make({ id: { id: mutation.id, clientID: mutation.clientID }, result }),
144
- ),
145
- Effect.provideService(ZeroTransactionInput, {
146
- clientID: mutation.clientID,
147
- mutationID: mutation.id,
148
- clientGroupID: request.clientGroupID,
149
- upstreamSchema: params.schema,
150
- }),
151
- );
152
- }),
78
+ return { mutations: Chunk.toArray(responses) } satisfies Types.PushResponse;
79
+ }, Effect.provide(ServerSynchronizationContext.Default));
80
+
81
+ const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R>, mutation: Types.Mutation) {
82
+ // Support both "namespace|name" and "namespace.name" formats, and single-segment names.
83
+ const [namespace, name] = mutation.name.includes("|") ? Str.split(mutation.name, "|") : Str.split(mutation.name, ".");
84
+
85
+ const mutator = yield* Fn.pipe(
86
+ mutators,
87
+ Rec.get<string>(namespace),
88
+ Option.flatMap((mutator) =>
89
+ Match.value([mutator, name]).pipe(
90
+ Match.when([Predicate.isRecord, Predicate.isString], ([mutator, name]) => Rec.get<string>(name)(mutator)),
91
+ Match.when([Predicate.isFunction, Predicate.isUndefined], ([mutator]) => Option.some(mutator)),
92
+ Match.orElse(() => Option.none()),
153
93
  ),
154
- // We only stop processing if the mutation is out of order.
155
- // If the mutation has already been processed or if it returns an application error,
156
- // we continue processing the next mutation.
157
- Stream.takeUntil(({ result }) => Predicate.hasProperty(result, "error") && result.error === "oooMutation"),
158
- Stream.runCollect,
159
- Effect.annotateLogs({ module: prefixId("ZeroServer"), method: "processPush" }),
160
- );
161
-
162
- return { mutations: Chunk.toArray(responses) } satisfies ZeroPushResponse;
163
- });
164
-
165
- const processMutation = Effect.fn(
166
- function* <R>(mutators: AnyMutators<R>, mutation: ZeroMutation) {
167
- // Support both "namespace|name" and "namespace.name" formats, and single-segment names.
168
- const [namespace, name] = mutation.name.includes("|")
169
- ? Str.split(mutation.name, "|")
170
- : Str.split(mutation.name, ".");
171
-
172
- const mutator = yield* Fn.pipe(
173
- mutators,
174
- Rec.get<string>(namespace),
175
- Option.flatMap((mutator) =>
176
- Match.value([mutator, name]).pipe(
177
- Match.when([Predicate.isRecord, Predicate.isString], ([mutator, name]) => Rec.get<string>(name)(mutator)),
178
- Match.when([Predicate.isFunction, Predicate.isUndefined], ([mutator]) => Option.some(mutator)),
179
- Match.orElse(() => Option.none()),
180
- ),
181
- ),
182
- Effect.catchTag("NoSuchElementException", () => new MutatorNotFoundError({ mutationName: mutation.name })),
183
- );
94
+ ),
95
+ Effect.catchTag("NoSuchElementException", () => new MutatorNotFoundError({ name: mutation.name })),
96
+ );
184
97
 
185
- const args = yield* Schema.decode(mutator[MutatorArgsSchemaSym])(mutation.args[0]).pipe(
186
- Effect.catchTag("ParseError", (e) => new ZeroServerArgsParseError({ cause: Cause.fail(e) })),
187
- );
98
+ const args = yield* Schema.decode(mutator[Mutators.MutatorSchemaSymbol])(mutation.args[0]).pipe(
99
+ Effect.catchTag("ParseError", (e) => new ServerArgsParseError({ cause: Cause.fail(e) })),
100
+ );
188
101
 
189
- return yield* mutator(args).pipe(
190
- // Case #2 "One transaction then fail"
191
- // If the transaction was executed successfully, swallow the error and just log it, otherwise re-throw it
192
- Effect.catchAllCause(
193
- Effect.fn(function* (e) {
194
- const wasTransactionExecuted = yield* ZeroServerMutationContext.wasTransactionExecuted;
195
- if (wasTransactionExecuted) {
196
- return yield* Effect.logError("Error occurred after transaction execution completed", e);
197
- }
198
- return yield* Effect.failCause(e);
199
- }),
200
- ),
201
- // Case #4 "Zero transactions then succeed"
202
- // Check that the transaction was executed during the mutation
203
- Effect.tap(
204
- Effect.gen(function* () {
205
- const wasTransactionExecuted = yield* ZeroServerMutationContext.wasTransactionExecuted;
206
- if (!wasTransactionExecuted) {
207
- return yield* new NoTransactionError();
208
- }
209
- }),
210
- ),
211
- Effect.as<ZeroMutationResult>({}),
212
- Effect.catchIf(OutOfOrderMutationError.is, (e) =>
213
- Effect.logError(e.message).pipe(Effect.as({ error: "oooMutation", details: e.message } satisfies ZeroError)),
214
- ),
215
- Effect.catchIf(MutationAlreadyProcessedError.is, (e) =>
216
- Effect.logWarning(e.message).pipe(
217
- Effect.as({ error: "alreadyProcessed", details: e.message } satisfies ZeroError),
218
- ),
219
- ),
220
- // Case #5 "Zero transactions then fail" / #6 "Fail before transaction"
221
- // Catches all errors that are produced before the transaction is executed
222
- Effect.catchAllCause((cause) => new ZeroMutationUserError({ cause })),
223
- );
224
- },
225
- Effect.catchAll((e) => processMutationError(e)),
226
- Effect.provide(ZeroServerMutationContext.Default),
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.catchAllCause((cause) => new MutationUserError({ cause })),
227
118
  );
119
+ });
228
120
 
229
- const processMutationError = Effect.fn(function* (e: unknown) {
230
- const { clientID, mutationID } = yield* ZeroTransactionInput;
121
+ const processMutationError = Effect.fn(function* <TSchema extends ZeroSchema, TTransaction>(
122
+ transaction: ServerTransactionContext<TSchema, TTransaction>,
123
+ e: unknown,
124
+ ) {
125
+ const { clientID, mutationID } = yield* ServerTransactionInput;
231
126
 
232
- yield* Effect.logError(`Unexpected error processing mutation ${mutationID} for client ${clientID}`, Cause.fail(e));
127
+ yield* Effect.logError(`Unexpected error processing mutation ${mutationID} for client ${clientID}`, Cause.fail(e));
233
128
 
234
- const errorMessage = Match.value(e).pipe(
235
- Match.when(ZeroMutationUserError.is, (e) => e.message),
236
- Match.orElse(() => "Internal error"),
237
- );
129
+ const errorMessage = Match.value(e).pipe(
130
+ Match.when(MutationUserError.is, (e) => e.message),
131
+ Match.orElse(() => "Internal error"),
132
+ );
238
133
 
239
- const appError = {
240
- error: "app",
241
- details: errorMessage,
242
- } satisfies ZeroAppError;
134
+ const appError = {
135
+ error: "app",
136
+ details: errorMessage,
137
+ } satisfies Types.AppError;
243
138
 
244
- yield* execute(
245
- Effect.flatMap(ZeroServerTransactionContext, ({ transactionHooks }) => {
139
+ yield* transaction
140
+ .execute(
141
+ Effect.flatMap(transaction, ({ transactionHooks }) => {
246
142
  return Effect.tryPromise({
247
143
  try: () => transactionHooks.writeMutationResult({ id: { id: mutationID, clientID }, result: appError }),
248
144
  catch: (error) => new WriteMutationResultError({ cause: Cause.fail(error) }),
249
145
  });
250
146
  }),
251
- ).pipe(Effect.catchAllCause(Effect.logError));
147
+ )
148
+ .pipe(Effect.catchAllCause(Effect.logError));
252
149
 
253
- yield* Effect.logWarning(`Mutation ${mutationID} for client ${clientID} was retried after an error: ${e}`);
254
-
255
- return appError;
256
- });
150
+ yield* Effect.logWarning(`Mutation ${mutationID} for client ${clientID} was retried after an error: ${e}`);
257
151
 
258
- return {
259
- Transaction: Object.assign(ZeroServerTransactionContext, { execute, use }),
260
- processPush,
261
- };
262
- };
152
+ return appError;
153
+ });
263
154
 
264
- class ZeroTransactionInput extends Context.Tag(prefixId("ZeroTransactionInput"))<
265
- ZeroTransactionInput,
266
- TransactionProviderInput
267
- >() {}
268
-
269
- class ZeroServerMutationContext extends Effect.Service<ZeroServerMutationContext>()(
270
- prefixId("ZeroServerMutationContext"),
271
- {
272
- effect: Effect.gen(function* () {
273
- return { wasTransactionExecuted: yield* SynchronizedRef.make(false) };
274
- }),
275
- },
276
- ) {
277
- static wasTransactionExecuted = Effect.flatMap(ZeroServerMutationContext, (ctx) => ctx.wasTransactionExecuted);
278
-
279
- // Ensures that only one transaction is executed at a time and checks that another transaction wasn't already executed.
280
- static guard = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
281
- Effect.flatMap(this, ({ wasTransactionExecuted }) =>
282
- SynchronizedRef.modifyEffect(
283
- wasTransactionExecuted,
284
- Effect.fn(function* (wasTransactionExecuted) {
285
- if (wasTransactionExecuted) {
286
- return yield* new MultipleTransactionsError();
287
- }
288
- const result = yield* effect;
289
- return [result, true];
290
- }),
291
- ),
292
- );
293
- }
294
-
295
- class ZeroDatabaseError extends Data.TaggedError("ZeroDatabaseError")<{
296
- cause: Cause.Cause<unknown>;
297
- }> {}
298
-
299
- class UpdateClientMutationIDError extends Data.TaggedError("UpdateClientMutationIDError")<{
155
+ class WriteMutationResultError extends Data.TaggedError("WriteMutationResultError")<{
300
156
  readonly cause: Cause.Cause<unknown>;
301
157
  }> {}
302
-
158
+ class CustomMutationExpectedError extends Data.TaggedError("CustomMutationExpectedError") {}
303
159
  class MutatorNotFoundError extends Data.TaggedError("MutatorNotFoundError")<{
304
- mutationName: string;
305
- }> {
306
- override message = `Mutator not found for mutation ${this.mutationName}`;
307
- }
308
-
309
- class WriteMutationResultError extends Data.TaggedError("WriteMutationResultError")<{
310
- readonly cause: Cause.Cause<unknown>;
160
+ readonly name: string;
311
161
  }> {}
312
- class CustomMutationExpectedError extends Data.TaggedError("CustomMutationExpectedError")<object> {}
313
162
 
314
- export class MultipleTransactionsError extends Data.TaggedError("MultipleTransactionsError") {
315
- override message = "Multiple transactions detected in a mutation, only one transaction is allowed.";
316
- }
317
-
318
- export class NoTransactionError extends Data.TaggedError("NoTransactionError") {
319
- override message = "No transaction detected in a mutation, a transaction is required.";
320
- }
163
+ class ServerArgsParseError extends Data.TaggedError("ServerArgsParseError")<{
164
+ readonly cause: Cause.Cause<ParseResult.ParseError>;
165
+ }> {}
321
166
 
322
- const OutOfOrderMutationErrorTypeId = Symbol.for(prefixId("OutOfOrderMutationError"));
323
- export class OutOfOrderMutationError extends Data.TaggedError("OutOfOrderMutationError")<{
324
- readonly clientID: string;
325
- readonly receivedMutationID: number;
326
- readonly lastMutationID: number | bigint;
167
+ const MutationUserErrorTypeId = Symbol.for(prefixId("MutationUserError"));
168
+ class MutationUserError extends Data.TaggedError("MutationUserError")<{
169
+ readonly cause: Cause.Cause<unknown>;
327
170
  }> {
328
- readonly [OutOfOrderMutationErrorTypeId] = OutOfOrderMutationErrorTypeId;
329
- override get message() {
330
- return `Client ${this.clientID} sent mutation ID ${this.receivedMutationID} but expected ${this.lastMutationID}`;
331
- }
332
- static is(e: unknown): e is OutOfOrderMutationError {
333
- return Predicate.hasProperty(e, OutOfOrderMutationErrorTypeId);
171
+ readonly [MutationUserErrorTypeId] = MutationUserErrorTypeId;
172
+ static is(e: unknown): e is MutationUserError {
173
+ return Predicate.hasProperty(e, MutationUserErrorTypeId);
334
174
  }
335
- }
336
175
 
337
- const MutationAlreadyProcessedErrorTypeId = Symbol.for(prefixId("MutationAlreadyProcessedError"));
338
- export class MutationAlreadyProcessedError extends Data.TaggedError("MutationAlreadyProcessedError")<{
339
- readonly clientID: string;
340
- readonly received: number;
341
- readonly actual: number | bigint;
342
- }> {
343
- readonly [MutationAlreadyProcessedErrorTypeId] = MutationAlreadyProcessedErrorTypeId;
344
176
  override get message() {
345
- return `Ignoring mutation from ${this.clientID} with ID ${this.received} as it was already processed. Expected: ${this.actual}`;
346
- }
347
- static is(e: unknown): e is MutationAlreadyProcessedError {
348
- return Predicate.hasProperty(e, MutationAlreadyProcessedErrorTypeId);
177
+ const err = Cause.squash(this.cause);
178
+ return err instanceof Error ? err.message : "exception was not of type `Error`";
349
179
  }
350
180
  }
351
181
 
352
- const ZeroTransactionUserErrorTypeId = Symbol.for(prefixId("ZeroTransactionUserError"));
353
- export class ZeroTransactionUserError extends Data.TaggedError("ZeroTransactionUserError") {
354
- readonly [ZeroTransactionUserErrorTypeId] = ZeroTransactionUserErrorTypeId;
355
- static is(e: unknown): e is ZeroTransactionUserError {
356
- return Predicate.hasProperty(e, ZeroTransactionUserErrorTypeId);
182
+ export const handleGetQueries = Effect.fn(function* <E, R1, R2>(
183
+ queries: MakeQueryResult<E, R1, R2>[],
184
+ schema: ZeroSchema,
185
+ payload: TransformRequestMessage,
186
+ ) {
187
+ const runtime = yield* Effect.runtime<R1 | R2>();
188
+ return yield* Effect.tryPromise({
189
+ try: () =>
190
+ handleGetQueriesRequest(
191
+ (name, args) =>
192
+ Arr.findFirst(queries, (q) => q[QueryNameSymbol] === name).pipe(
193
+ Effect.catchTag("NoSuchElementException", () => new QueryNotFound({ name })),
194
+ Effect.flatMap((query) => query[RunQuerySymbol]({ _tag: "Encoded", args })),
195
+ Effect.map((query) => ({ query })),
196
+ (effect) => Runtime.runPromiseExit(runtime, effect),
197
+ (exit) =>
198
+ exit.then(
199
+ Exit.getOrElse((cause) => {
200
+ throw new QueryUserError<E>({ cause });
201
+ }),
202
+ ),
203
+ ),
204
+ schema,
205
+ payload as ReadonlyJSONValue,
206
+ ),
207
+ catch: (e) =>
208
+ Option.liftPredicate(e, QueryUserError.is<E>).pipe(
209
+ Option.flatMap((e) => Cause.failureOption(e.cause)),
210
+ Option.getOrElse(() => new GetQueriesError({ cause: Cause.fail(e) })),
211
+ ),
212
+ });
213
+ });
214
+
215
+ class QueryNotFound extends Data.TaggedError("QueryNotFound")<{ name: string }> {
216
+ override get message() {
217
+ return `Query not found: ${this.name}`;
357
218
  }
358
219
  }
359
220
 
360
- const ZeroMutationUserErrorTypeId = Symbol.for(prefixId("ZeroMutationUserError"));
361
- export class ZeroMutationUserError extends Data.TaggedError("ZeroMutationUserError")<{
362
- cause: Cause.Cause<unknown>;
221
+ const QueryUserErrorTypeId = Symbol.for(prefixId("QueryUserError"));
222
+ class QueryUserError<E> extends Data.TaggedError("QueryUserError")<{
223
+ cause: Cause.Cause<E | ParseResult.ParseError | QueryNotFound>;
363
224
  }> {
364
- readonly [ZeroMutationUserErrorTypeId] = ZeroMutationUserErrorTypeId;
365
- static is(e: unknown): e is ZeroMutationUserError {
366
- return Predicate.hasProperty(e, ZeroMutationUserErrorTypeId);
367
- }
368
-
369
- override get message() {
370
- const err = Cause.squash(this.cause);
371
- return err instanceof Error ? err.message : "exception was not of type `Error`";
225
+ readonly [QueryUserErrorTypeId] = QueryUserErrorTypeId;
226
+ static is<E>(e: unknown): e is QueryUserError<E> {
227
+ return Predicate.hasProperty(e, QueryUserErrorTypeId);
372
228
  }
373
229
  }
374
230
 
375
- class ZeroServerArgsParseError extends Data.TaggedError("ZeroServerArgsParseError")<{
376
- cause: Cause.Cause<ParseResult.ParseError>;
377
- }> {}
231
+ class GetQueriesError extends Data.TaggedError("GetQueriesError")<{ cause: Cause.Cause<unknown> }> {}
package/src/snapshot.ts CHANGED
@@ -1,5 +1,3 @@
1
- // TODO(zero): clean this up
2
-
3
1
  // Code borrowed from:
4
2
  // https://github.com/rocicorp/mono/blob/30f209f2946b4cf2cd2dee459849351498f11308/packages/zero-react/src/use-query.tsx
5
3
  // https://github.com/rocicorp/mono/blob/30f209f2946b4cf2cd2dee459849351498f11308/packages/shared/src/deep-clone.ts#L4
@@ -0,0 +1,4 @@
1
+ import * as Schema from "effect/Schema";
2
+
3
+ /** TODO: Defined as unknown for now */
4
+ export const JsonSchema = Schema.Unknown;