effect-zero 0.4.2 → 0.5.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.
- package/dist/client-transaction.d.ts +77 -7
- package/dist/client-transaction.d.ts.map +1 -1
- package/dist/client-transaction.js +11 -7
- package/dist/client-transaction.js.map +1 -1
- package/dist/client.d.ts +119 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -6
- package/dist/client.js.map +1 -1
- package/dist/internal/server-synchronization-context.d.ts +19 -0
- package/dist/internal/server-synchronization-context.d.ts.map +1 -0
- package/dist/internal/server-synchronization-context.js +46 -0
- package/dist/internal/server-synchronization-context.js.map +1 -0
- package/dist/internal/server.d.ts +5 -16
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +1 -38
- package/dist/internal/server.js.map +1 -1
- package/dist/internal/server.test.js +7 -8
- package/dist/internal/server.test.js.map +1 -1
- package/dist/internal/utils.d.ts +7 -0
- package/dist/internal/utils.d.ts.map +1 -1
- package/dist/internal/utils.js +7 -0
- package/dist/internal/utils.js.map +1 -1
- package/dist/mutators.d.ts +1 -1
- package/dist/mutators.d.ts.map +1 -1
- package/dist/mutators.js +1 -1
- package/dist/query.d.ts +3 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +6 -12
- package/dist/query.js.map +1 -1
- package/dist/result.d.ts +15 -15
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +11 -11
- package/dist/result.js.map +1 -1
- package/dist/server-transaction.d.ts +142 -15
- package/dist/server-transaction.d.ts.map +1 -1
- package/dist/server-transaction.js +24 -19
- package/dist/server-transaction.js.map +1 -1
- package/dist/server.d.ts +247 -12
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -18
- package/dist/server.js.map +1 -1
- package/dist/types/common.d.ts +1 -1
- package/dist/types/common.d.ts.map +1 -1
- package/dist/types/error.d.ts +69 -48
- package/dist/types/error.d.ts.map +1 -1
- package/dist/types/error.js +38 -30
- package/dist/types/error.js.map +1 -1
- package/dist/types/push.d.ts +149 -149
- package/dist/types/push.d.ts.map +1 -1
- package/dist/types/push.js +45 -40
- package/dist/types/push.js.map +1 -1
- package/dist/types/queries.d.ts +42 -42
- package/dist/types/queries.d.ts.map +1 -1
- package/dist/types/queries.js +7 -7
- package/dist/types/queries.js.map +1 -1
- package/dist/types.test.js +13 -15
- package/dist/types.test.js.map +1 -1
- package/package.json +5 -7
- package/src/client-transaction.ts +13 -11
- package/src/client.ts +17 -19
- package/src/internal/server-synchronization-context.ts +64 -0
- package/src/internal/server.ts +3 -59
- package/src/internal/utils.ts +8 -0
- package/src/mutators.ts +2 -2
- package/src/query.ts +8 -20
- package/src/result.ts +40 -37
- package/src/server-transaction.ts +32 -37
- package/src/server.ts +34 -37
- package/src/types/error.ts +8 -8
- package/src/types/push.ts +22 -22
- package/src/types/queries.ts +7 -7
package/src/query.ts
CHANGED
|
@@ -4,9 +4,9 @@ import * as Effect from "effect/Effect";
|
|
|
4
4
|
import * as Equal from "effect/Equal";
|
|
5
5
|
import * as Hash from "effect/Hash";
|
|
6
6
|
import * as Match from "effect/Match";
|
|
7
|
+
import * as Queue from "effect/Queue";
|
|
7
8
|
import * as Schema from "effect/Schema";
|
|
8
9
|
import * as Stream from "effect/Stream";
|
|
9
|
-
import * as Subscribable from "effect/Subscribable";
|
|
10
10
|
import * as QueryResult from "./internal/query-result.js";
|
|
11
11
|
import { prefixId } from "./internal/utils.js";
|
|
12
12
|
import { deepClone, getDefaultSnapshot, getSnapshot } from "./snapshot.js";
|
|
@@ -32,15 +32,15 @@ export const make = <
|
|
|
32
32
|
R2,
|
|
33
33
|
>(options: {
|
|
34
34
|
name: N;
|
|
35
|
-
payload: Schema.
|
|
35
|
+
payload: Schema.Codec<B, A, R1, R1>;
|
|
36
36
|
query: (args: NoInfer<B>) => Effect.Effect<ZeroQuery<T, S, R>, E, R2>;
|
|
37
37
|
}) => {
|
|
38
38
|
const runQuery = Effect.fn(function* (args: QueryArgs<A, B>) {
|
|
39
39
|
const { encoded, decoded } = yield* Match.valueTags(args, {
|
|
40
40
|
Encoded: ({ args: encoded }) =>
|
|
41
|
-
Effect.map(Schema.
|
|
41
|
+
Effect.map(Schema.decodeEffect(options.payload)(encoded), (decoded) => ({ encoded, decoded })),
|
|
42
42
|
Decoded: ({ args: decoded }) =>
|
|
43
|
-
Effect.map(Schema.
|
|
43
|
+
Effect.map(Schema.encodeEffect(options.payload)(decoded), (encoded) => ({ encoded, decoded })),
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
return yield* options.query(decoded).pipe(
|
|
@@ -88,8 +88,8 @@ export const stream = <T extends keyof S["tables"] & string, S extends ZeroSchem
|
|
|
88
88
|
(view) => Effect.sync(() => view.destroy()),
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
-
return Stream.
|
|
92
|
-
Effect.sync(() => view.addListener((...args) =>
|
|
91
|
+
return Stream.callback<Parameters<Parameters<(typeof view)["addListener"]>[0]>>((queue) =>
|
|
92
|
+
Effect.sync(() => view.addListener((...args) => Queue.offerUnsafe(queue, args))),
|
|
93
93
|
).pipe(
|
|
94
94
|
Stream.mapEffect(([data, resultType, error]) =>
|
|
95
95
|
Effect.sync(() => {
|
|
@@ -102,27 +102,15 @@ export const stream = <T extends keyof S["tables"] & string, S extends ZeroSchem
|
|
|
102
102
|
// TODO: Look into this. It seems like this is difficult but not impossible to model in the Effect paradigm.
|
|
103
103
|
// Likely need some kind of stateful piece between the publisher and and subscriber, allowing the subscriber to swap out
|
|
104
104
|
// the publisher when this function is called.
|
|
105
|
-
() => Effect.
|
|
105
|
+
() => Effect.die("retry not available in `effect-zero`"),
|
|
106
106
|
error,
|
|
107
107
|
);
|
|
108
108
|
}),
|
|
109
109
|
),
|
|
110
110
|
Stream.map(QueryResult.make),
|
|
111
111
|
);
|
|
112
|
-
}).pipe(Stream.
|
|
112
|
+
}).pipe(Stream.unwrap);
|
|
113
113
|
|
|
114
114
|
export const initialValue = <T extends keyof S["tables"] & string, S extends ZeroSchema, R>(
|
|
115
115
|
query: ZeroQuery<T, S, R>,
|
|
116
116
|
) => QueryResult.make<R>(getDefaultSnapshot(asQueryInternals(query).format.singular));
|
|
117
|
-
|
|
118
|
-
export const subscribable = <T extends keyof S["tables"] & string, S extends ZeroSchema, R>(
|
|
119
|
-
zero: Zero<S>,
|
|
120
|
-
query: ZeroQuery<T, S, R>,
|
|
121
|
-
) => {
|
|
122
|
-
return Subscribable.make({
|
|
123
|
-
get: Effect.promise(() => zero.run(query, { type: "unknown" })).pipe(
|
|
124
|
-
Effect.map((value) => ({ _tag: "Partial", value }) satisfies QueryResult.QueryResult.Partial<R>),
|
|
125
|
-
),
|
|
126
|
-
changes: stream(zero, query),
|
|
127
|
-
});
|
|
128
|
-
};
|
package/src/result.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Result as AtomResult } from "@effect-atom/atom";
|
|
2
1
|
import type { HumanReadable } from "@rocicorp/zero";
|
|
3
2
|
import * as Cause from "effect/Cause";
|
|
4
3
|
import * as Data from "effect/Data";
|
|
@@ -7,14 +6,15 @@ import * as Match from "effect/Match";
|
|
|
7
6
|
import * as Option from "effect/Option";
|
|
8
7
|
import * as Predicate from "effect/Predicate";
|
|
9
8
|
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/unstable/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
|
|
|
19
19
|
However, such a type is not easy to work with.
|
|
20
20
|
The purpose of `effect-zero`.Result is to provide a more convenient type for this use case.
|
|
@@ -22,8 +22,8 @@ The purpose of `effect-zero`.Result is to provide a more convenient type for thi
|
|
|
22
22
|
### Background
|
|
23
23
|
|
|
24
24
|
To start, let's analyze the possible states of the original type:
|
|
25
|
-
- The outer
|
|
26
|
-
- The inner
|
|
25
|
+
- The outer `AsyncResult` may be `Initial<_, _>`, `Failure<_, E>`, or `Success<QueryResult, _>`.
|
|
26
|
+
- The inner `QueryResult` may be `unknown<A>`, `complete<A>`, or `error`.
|
|
27
27
|
|
|
28
28
|
This means that there are 5 total possible states:
|
|
29
29
|
- Initial, representing the state before the query has been executed on either the client or the server.
|
|
@@ -43,45 +43,48 @@ On the other hand, `effect-zero`.Result has the following states:
|
|
|
43
43
|
|
|
44
44
|
This has a few key benefits:
|
|
45
45
|
- It allows you to deal with errors in a more streamlined way, as now all error states are represented as a `Failure(E | QueryError)`,
|
|
46
|
-
thus you can use the builtin facilities of `
|
|
46
|
+
thus you can use the builtin facilities of `AsyncResult` to work with the possible error states.
|
|
47
47
|
- We provide a method `acceptPartialMode` which allows for simplifying the type further by specifying the situations in which you want to accept
|
|
48
48
|
partial results, resulting a further simplified type of `Initial | Success(A) | Failure(E | QueryError)`.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
-
export type Result<A, E = never> =
|
|
51
|
+
export type Result<A, E = never> = AsyncResult.AsyncResult<
|
|
52
|
+
QueryResult.Partial<A> | QueryResult.Complete<A>,
|
|
53
|
+
E | QueryError
|
|
54
|
+
>;
|
|
52
55
|
export namespace Result {
|
|
53
|
-
export type Initial<A, E = never> =
|
|
56
|
+
export type Initial<A, E = never> = AsyncResult.Initial<
|
|
54
57
|
QueryResult.Partial<A> | QueryResult.Complete<A>,
|
|
55
58
|
E | QueryError
|
|
56
59
|
>;
|
|
57
|
-
export type Failure<A, E = never> =
|
|
60
|
+
export type Failure<A, E = never> = AsyncResult.Failure<
|
|
58
61
|
QueryResult.Partial<A> | QueryResult.Complete<A>,
|
|
59
62
|
E | QueryError
|
|
60
63
|
>;
|
|
61
|
-
export type Success<A, E = never> =
|
|
64
|
+
export type Success<A, E = never> = AsyncResult.Success<
|
|
62
65
|
QueryResult.Partial<A> | QueryResult.Complete<A>,
|
|
63
66
|
E | QueryError
|
|
64
67
|
>;
|
|
65
68
|
|
|
66
69
|
// More granular Partial and Complete types
|
|
67
|
-
export type Partial<A, E = never> =
|
|
68
|
-
export type Complete<A, E = never> =
|
|
70
|
+
export type Partial<A, E = never> = AsyncResult.Success<QueryResult.Partial<A>, E | QueryError>;
|
|
71
|
+
export type Complete<A, E = never> = AsyncResult.Success<QueryResult.Complete<A>, E | QueryError>;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export class QueryError extends Data.TaggedError("QueryError")<{
|
|
72
75
|
readonly details: QueryResult.Error["details"];
|
|
73
76
|
}> {}
|
|
74
77
|
|
|
75
|
-
export const make = <A, E>(value:
|
|
78
|
+
export const make = <A, E>(value: AsyncResult.AsyncResult<QueryResult<A>, E>): Result<A, E> =>
|
|
76
79
|
Match.valueTags(value, {
|
|
77
|
-
Initial: () =>
|
|
80
|
+
Initial: () => AsyncResult.initial(),
|
|
78
81
|
Success: ({ value }) =>
|
|
79
82
|
Match.valueTags(value, {
|
|
80
|
-
Partial: (value) =>
|
|
81
|
-
Complete: (value) =>
|
|
82
|
-
Error: (e) =>
|
|
83
|
+
Partial: (value) => AsyncResult.success(value),
|
|
84
|
+
Complete: (value) => AsyncResult.success(value),
|
|
85
|
+
Error: (e): Result<A, E> => AsyncResult.failure(Cause.fail(new QueryError(e))),
|
|
83
86
|
}),
|
|
84
|
-
Failure: (e) =>
|
|
87
|
+
Failure: (e): Result<A, E> => AsyncResult.failure(e.cause),
|
|
85
88
|
});
|
|
86
89
|
|
|
87
90
|
// based on: https://github.com/tim-smart/effect-atom/blob/04c15cacda42dd230782f52c0e978400793a502c/packages/atom/src/Result.ts#L424
|
|
@@ -124,34 +127,34 @@ export const match: {
|
|
|
124
127
|
);
|
|
125
128
|
|
|
126
129
|
export const mapSuccess: {
|
|
127
|
-
<A, B>(f: (value: A) => B): <E = never>(self:
|
|
128
|
-
<A, B, E = never>(self:
|
|
129
|
-
} = Fn.dual(2, <A, B, E = never>(self:
|
|
130
|
+
<A, B>(f: (value: A) => B): <E = never>(self: AsyncResult.Success<A, E>) => AsyncResult.Success<B, E>;
|
|
131
|
+
<A, B, E = never>(self: AsyncResult.Success<A, E>, f: (value: A) => B): AsyncResult.Success<B, E>;
|
|
132
|
+
} = Fn.dual(2, <A, B, E = never>(self: AsyncResult.Success<A, E>, f: (value: A) => B) => AsyncResult.map(self, f));
|
|
130
133
|
|
|
131
134
|
export const acceptPartialWith: {
|
|
132
135
|
<A, E = never>(
|
|
133
136
|
self: Result<A, E>,
|
|
134
137
|
predicate: (value: HumanReadable<A>) => boolean,
|
|
135
138
|
):
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
+
| AsyncResult.Initial<HumanReadable<A>, E>
|
|
140
|
+
| AsyncResult.Success<HumanReadable<A>, QueryError | E>
|
|
141
|
+
| AsyncResult.Failure<HumanReadable<A>, QueryError | E>;
|
|
139
142
|
<A, E = never>(
|
|
140
143
|
self: Result<A, E>,
|
|
141
144
|
): (
|
|
142
145
|
predicate: (value: HumanReadable<A>) => boolean,
|
|
143
146
|
) =>
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
+
| AsyncResult.Initial<HumanReadable<A>, E>
|
|
148
|
+
| AsyncResult.Success<HumanReadable<A>, QueryError | E>
|
|
149
|
+
| AsyncResult.Failure<HumanReadable<A>, QueryError | E>;
|
|
147
150
|
} = Fn.dual(2, <A, E = never>(self: Result<A, E>, predicate: (value: HumanReadable<A>) => boolean) =>
|
|
148
|
-
|
|
151
|
+
AsyncResult.match(self, {
|
|
149
152
|
onSuccess: (success) =>
|
|
150
153
|
Predicate.isTagged(success.value, "Partial") && !predicate(success.value.value)
|
|
151
|
-
?
|
|
154
|
+
? AsyncResult.initial(success.waiting)
|
|
152
155
|
: mapSuccess(success, Struct.get("value")),
|
|
153
156
|
onFailure: (failure) =>
|
|
154
|
-
|
|
157
|
+
AsyncResult.failure(failure.cause, {
|
|
155
158
|
waiting: failure.waiting,
|
|
156
159
|
previousSuccess: failure.previousSuccess.pipe(
|
|
157
160
|
Option.flatMap((success) =>
|
|
@@ -161,7 +164,7 @@ export const acceptPartialWith: {
|
|
|
161
164
|
),
|
|
162
165
|
),
|
|
163
166
|
}),
|
|
164
|
-
onInitial: (initial) =>
|
|
167
|
+
onInitial: (initial) => AsyncResult.initial(initial.waiting),
|
|
165
168
|
}),
|
|
166
169
|
);
|
|
167
170
|
|
|
@@ -182,9 +185,9 @@ export const acceptPartialMode: {
|
|
|
182
185
|
**/
|
|
183
186
|
mode: AcceptPartialMode,
|
|
184
187
|
):
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
+
| AsyncResult.Initial<HumanReadable<A>, E>
|
|
189
|
+
| AsyncResult.Success<HumanReadable<A>, QueryError | E>
|
|
190
|
+
| AsyncResult.Failure<HumanReadable<A>, QueryError | E>;
|
|
188
191
|
<A, E>(
|
|
189
192
|
/**
|
|
190
193
|
* - `acceptCached` -> Will resolve to the partial result if it is available
|
|
@@ -200,9 +203,9 @@ export const acceptPartialMode: {
|
|
|
200
203
|
): (
|
|
201
204
|
self: Result<A, E>,
|
|
202
205
|
) =>
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
206
|
+
| AsyncResult.Initial<HumanReadable<A>, E>
|
|
207
|
+
| AsyncResult.Success<HumanReadable<A>, QueryError | E>
|
|
208
|
+
| AsyncResult.Failure<HumanReadable<A>, QueryError | E>;
|
|
206
209
|
} = Fn.dual(2, <A, E>(self: Result<A, E>, mode: AcceptPartialMode) =>
|
|
207
210
|
Match.value(mode).pipe(
|
|
208
211
|
Match.when("waitForServer", () => acceptPartialWith(self, () => false)),
|
|
@@ -1,42 +1,35 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Schema as ZeroSchema,
|
|
3
|
-
ServerTransaction as ZeroServerTransaction,
|
|
4
|
-
Transaction as ZeroTransaction,
|
|
5
|
-
} from "@rocicorp/zero";
|
|
1
|
+
import type { Schema as ZeroSchema, ServerTransaction as ZeroServerTransaction } from "@rocicorp/zero";
|
|
6
2
|
import type { TransactionProviderHooks, ZQLDatabase } from "@rocicorp/zero/server";
|
|
7
3
|
import * as Cause from "effect/Cause";
|
|
8
|
-
import * as
|
|
4
|
+
import * as Ctx from "effect/Context";
|
|
9
5
|
import * as Data from "effect/Data";
|
|
10
6
|
import * as Deferred from "effect/Deferred";
|
|
11
7
|
import * as Effect from "effect/Effect";
|
|
12
8
|
import * as Exit from "effect/Exit";
|
|
9
|
+
import type { NodeInspectSymbol } from "effect/Inspectable";
|
|
13
10
|
import * as Layer from "effect/Layer";
|
|
14
11
|
import * as Predicate from "effect/Predicate";
|
|
15
|
-
import * as Runtime from "effect/Runtime";
|
|
16
12
|
import type * as ClientTransaction from "./client-transaction.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
OutOfOrderMutationError,
|
|
20
|
-
ServerSynchronizationContext,
|
|
21
|
-
ServerTransactionInput,
|
|
22
|
-
} from "./internal/server.js";
|
|
13
|
+
import { MutationAlreadyProcessedError, OutOfOrderMutationError, ServerTransactionInput } from "./internal/server.js";
|
|
14
|
+
import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
|
|
23
15
|
import { prefixId } from "./internal/utils.js";
|
|
24
16
|
|
|
25
17
|
// Updated to: https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
type _ = NodeInspectSymbol;
|
|
20
|
+
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: any client transaction (its Id literal is irrelevant here)
|
|
22
|
+
export type Context<TSchema extends ZeroSchema, TTransaction> = ReturnType<typeof make<any, TSchema, TTransaction>>;
|
|
30
23
|
|
|
31
24
|
export const make = <const Id extends string, TSchema extends ZeroSchema, TTransaction>(
|
|
32
25
|
id: Id,
|
|
33
26
|
database: ZQLDatabase<TSchema, TTransaction>,
|
|
34
|
-
clientTransaction:
|
|
27
|
+
clientTransaction: ClientTransaction.Context<TSchema>,
|
|
35
28
|
) => {
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
class Context extends Ctx.Service<
|
|
30
|
+
Context,
|
|
38
31
|
{ transaction: ZeroServerTransaction<TSchema, TTransaction>; transactionHooks: TransactionProviderHooks }
|
|
39
|
-
>()
|
|
32
|
+
>()(`${id as string}/ServerTransactionContext` as const) {}
|
|
40
33
|
|
|
41
34
|
const use = <A>(
|
|
42
35
|
fn: (
|
|
@@ -44,32 +37,33 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
44
37
|
options: { readonly signal: AbortSignal },
|
|
45
38
|
) => PromiseLike<A>,
|
|
46
39
|
) =>
|
|
47
|
-
Effect.
|
|
48
|
-
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const ctx = yield* Context;
|
|
42
|
+
return yield* Effect.tryPromise({
|
|
49
43
|
try: (signal) => fn(ctx.transaction, { signal }),
|
|
50
44
|
catch: (error) => new ServerTransactionError({ cause: Cause.fail(error) }),
|
|
51
|
-
})
|
|
52
|
-
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
53
47
|
|
|
54
48
|
const execute = Effect.fn(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
55
|
-
const
|
|
56
|
-
ServerTransactionInput | Exclude<R,
|
|
49
|
+
const ctx = yield* Effect.context<
|
|
50
|
+
ServerTransactionInput | Exclude<R, (typeof clientTransaction.Context)["Identifier"] | Context>
|
|
57
51
|
>();
|
|
58
|
-
const result = yield* Deferred.make<A, E | Effect.
|
|
52
|
+
const result = yield* Deferred.make<A, E | Effect.Error<typeof checkAndIncrementLastMutationId>>();
|
|
59
53
|
|
|
60
54
|
const transactionInput = yield* ServerTransactionInput;
|
|
61
55
|
yield* Effect.tryPromise({
|
|
62
56
|
try: (signal) =>
|
|
63
57
|
database.transaction(async (transaction, transactionHooks) => {
|
|
64
|
-
const exit = await Effect.
|
|
58
|
+
const exit = await Effect.flatMap(checkAndIncrementLastMutationId, () => effect).pipe(
|
|
65
59
|
Effect.provide([
|
|
66
|
-
Layer.succeed(
|
|
67
|
-
Layer.succeed(clientTransaction, transaction),
|
|
60
|
+
Layer.succeed(Context, { transaction, transactionHooks }),
|
|
61
|
+
Layer.succeed(clientTransaction.Context, transaction),
|
|
68
62
|
]),
|
|
69
|
-
(effect) =>
|
|
63
|
+
(effect) => Effect.runPromiseExitWith(ctx)(effect, { signal }),
|
|
70
64
|
);
|
|
71
|
-
Deferred.
|
|
72
|
-
|
|
65
|
+
Deferred.doneUnsafe(result, exit);
|
|
66
|
+
if (Exit.isFailure(exit)) {
|
|
73
67
|
// This error's purpose is to differentiate between "external" errors
|
|
74
68
|
// that originate from the user-defined mutator code and "internal" errors
|
|
75
69
|
// that originate from our own code and the Zero API.
|
|
@@ -78,7 +72,8 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
78
72
|
// are already covered by passing the Exit result to the Deferred, which is why
|
|
79
73
|
// we have the ServerTransactionUserError silenced below in the pipe.
|
|
80
74
|
throw new ServerTransactionUserError();
|
|
81
|
-
}
|
|
75
|
+
}
|
|
76
|
+
return exit.value;
|
|
82
77
|
}, transactionInput),
|
|
83
78
|
catch: (error) => {
|
|
84
79
|
if (ServerTransactionUserError.is(error)) {
|
|
@@ -90,11 +85,11 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
90
85
|
},
|
|
91
86
|
}).pipe(Effect.catchTag("ServerTransactionUserError", () => Effect.void));
|
|
92
87
|
|
|
93
|
-
return yield* result;
|
|
88
|
+
return yield* Deferred.await(result);
|
|
94
89
|
}, ServerSynchronizationContext.guard);
|
|
95
90
|
|
|
96
91
|
const checkAndIncrementLastMutationId = Effect.gen(function* () {
|
|
97
|
-
const { transactionHooks } = yield*
|
|
92
|
+
const { transactionHooks } = yield* Context;
|
|
98
93
|
const { clientID, mutationID: receivedMutationID } = yield* ServerTransactionInput;
|
|
99
94
|
|
|
100
95
|
const { lastMutationID } = yield* Effect.tryPromise({
|
|
@@ -118,7 +113,7 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
118
113
|
}
|
|
119
114
|
});
|
|
120
115
|
|
|
121
|
-
return
|
|
116
|
+
return { Context, use, execute };
|
|
122
117
|
};
|
|
123
118
|
|
|
124
119
|
class ServerTransactionError extends Data.TaggedError("ServerTransactionError")<{
|
package/src/server.ts
CHANGED
|
@@ -2,27 +2,21 @@ import type { ReadonlyJSONValue, Schema as ZeroSchema } from "@rocicorp/zero";
|
|
|
2
2
|
import { handleQueryRequest } from "@rocicorp/zero/server";
|
|
3
3
|
import * as Arr from "effect/Array";
|
|
4
4
|
import * as Cause from "effect/Cause";
|
|
5
|
-
import * as Chunk from "effect/Chunk";
|
|
6
5
|
import * as Data from "effect/Data";
|
|
7
6
|
import * as Effect from "effect/Effect";
|
|
8
7
|
import * as Exit from "effect/Exit";
|
|
9
8
|
import * as Fn from "effect/Function";
|
|
9
|
+
import type { NodeInspectSymbol } from "effect/Inspectable";
|
|
10
10
|
import * as Match from "effect/Match";
|
|
11
11
|
import * as Option from "effect/Option";
|
|
12
|
-
import type * as ParseResult from "effect/ParseResult";
|
|
13
12
|
import * as Predicate from "effect/Predicate";
|
|
14
13
|
import * as Rec from "effect/Record";
|
|
15
|
-
import * as Runtime from "effect/Runtime";
|
|
16
14
|
import * as Schema from "effect/Schema";
|
|
17
15
|
import * as Stream from "effect/Stream";
|
|
18
16
|
import * as Str from "effect/String";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ServerSynchronizationContext,
|
|
23
|
-
ServerTransactionInput,
|
|
24
|
-
} from "./internal/server.js";
|
|
25
|
-
import { prefixId } from "./internal/utils.js";
|
|
17
|
+
import { MutationAlreadyProcessedError, OutOfOrderMutationError, ServerTransactionInput } from "./internal/server.js";
|
|
18
|
+
import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
|
|
19
|
+
import { normalizeArgs, prefixId } from "./internal/utils.js";
|
|
26
20
|
import * as Mutators from "./mutators.js";
|
|
27
21
|
import { type MakeQueryResult, QueryNameSymbol, RunQuerySymbol } from "./query.js";
|
|
28
22
|
import type * as ServerTransaction from "./server-transaction.js";
|
|
@@ -33,17 +27,14 @@ import type { TransformRequestMessage } from "./types/queries.js";
|
|
|
33
27
|
// https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
|
|
34
28
|
// https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/process-mutations.ts
|
|
35
29
|
|
|
36
|
-
type
|
|
37
|
-
ReturnType<typeof ServerTransaction.make<string, TSchema, TTransaction>>,
|
|
38
|
-
"use"
|
|
39
|
-
>;
|
|
30
|
+
type _ = NodeInspectSymbol;
|
|
40
31
|
|
|
41
32
|
export const processPush = Effect.fn(function* <
|
|
42
33
|
TSchema extends ZeroSchema,
|
|
43
34
|
TTransaction,
|
|
44
35
|
TMutators extends Mutators.AnyMutators,
|
|
45
36
|
>(
|
|
46
|
-
transaction:
|
|
37
|
+
transaction: ServerTransaction.Context<TSchema, TTransaction>,
|
|
47
38
|
mutators: TMutators,
|
|
48
39
|
params: Types.PushParams,
|
|
49
40
|
request: Types.PushBody,
|
|
@@ -60,7 +51,7 @@ export const processPush = Effect.fn(function* <
|
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
return yield* processMutation<Mutators.ExtractMutatorsRequirements<TMutators>>(mutators, mutation).pipe(
|
|
63
|
-
Effect.
|
|
54
|
+
Effect.catch((e) => processMutationError(transaction, e)),
|
|
64
55
|
Effect.map((result) =>
|
|
65
56
|
Types.MutationResponse.make({ id: { id: mutation.id, clientID: mutation.clientID }, result }),
|
|
66
57
|
),
|
|
@@ -70,7 +61,7 @@ export const processPush = Effect.fn(function* <
|
|
|
70
61
|
clientGroupID: request.clientGroupID,
|
|
71
62
|
upstreamSchema: params.schema,
|
|
72
63
|
}),
|
|
73
|
-
Effect.provide(ServerSynchronizationContext.
|
|
64
|
+
Effect.provide(ServerSynchronizationContext.layer),
|
|
74
65
|
);
|
|
75
66
|
}),
|
|
76
67
|
),
|
|
@@ -81,7 +72,7 @@ export const processPush = Effect.fn(function* <
|
|
|
81
72
|
Stream.runCollect,
|
|
82
73
|
);
|
|
83
74
|
|
|
84
|
-
return { mutations:
|
|
75
|
+
return { mutations: responses } satisfies Types.PushResponse;
|
|
85
76
|
});
|
|
86
77
|
|
|
87
78
|
const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R>, mutation: Types.Mutation) {
|
|
@@ -93,17 +84,18 @@ const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R
|
|
|
93
84
|
Rec.get<string>(namespace),
|
|
94
85
|
Option.flatMap((mutator) =>
|
|
95
86
|
Match.value([mutator, name]).pipe(
|
|
96
|
-
Match.when([Predicate.
|
|
87
|
+
Match.when([Predicate.isObject, Predicate.isString], ([mutator, name]) => Rec.get<string>(name)(mutator)),
|
|
97
88
|
Match.when([Predicate.isFunction, Predicate.isUndefined], ([mutator]) => Option.some(mutator)),
|
|
98
89
|
Match.orElse(() => Option.none()),
|
|
99
90
|
),
|
|
100
91
|
),
|
|
101
|
-
Effect.
|
|
92
|
+
Effect.fromOption,
|
|
93
|
+
Effect.catchTag("NoSuchElementError", () => Effect.fail(new MutatorNotFoundError({ name: mutation.name }))),
|
|
102
94
|
);
|
|
103
95
|
|
|
104
|
-
const args = yield* Schema.
|
|
105
|
-
|
|
106
|
-
);
|
|
96
|
+
const args = yield* Schema.decodeUnknownEffect(mutator[Mutators.MutatorSchemaSymbol])(
|
|
97
|
+
normalizeArgs(mutation.args[0]),
|
|
98
|
+
).pipe(Effect.catchTag("SchemaError", (e) => Effect.fail(new ServerArgsParseError({ cause: Cause.fail(e) }))));
|
|
107
99
|
|
|
108
100
|
return yield* mutator(args).pipe(
|
|
109
101
|
ServerSynchronizationContext.finalize,
|
|
@@ -120,12 +112,12 @@ const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R
|
|
|
120
112
|
),
|
|
121
113
|
// Case #5 "Zero transactions then fail" / #6 "Fail before transaction"
|
|
122
114
|
// Catches all errors that are produced before the transaction is executed
|
|
123
|
-
Effect.
|
|
115
|
+
Effect.catchCause((cause) => Effect.fail(new MutationUserError({ cause }))),
|
|
124
116
|
);
|
|
125
117
|
});
|
|
126
118
|
|
|
127
119
|
const processMutationError = Effect.fn(function* <TSchema extends ZeroSchema, TTransaction>(
|
|
128
|
-
transaction:
|
|
120
|
+
transaction: ServerTransaction.Context<TSchema, TTransaction>,
|
|
129
121
|
e: unknown,
|
|
130
122
|
) {
|
|
131
123
|
const { clientID, mutationID } = yield* ServerTransactionInput;
|
|
@@ -144,14 +136,15 @@ const processMutationError = Effect.fn(function* <TSchema extends ZeroSchema, TT
|
|
|
144
136
|
|
|
145
137
|
yield* transaction
|
|
146
138
|
.execute(
|
|
147
|
-
Effect.
|
|
148
|
-
|
|
139
|
+
Effect.gen(function* () {
|
|
140
|
+
const { transactionHooks } = yield* transaction.Context;
|
|
141
|
+
return yield* Effect.tryPromise({
|
|
149
142
|
try: () => transactionHooks.writeMutationResult({ id: { id: mutationID, clientID }, result: appError }),
|
|
150
143
|
catch: (error) => new WriteMutationResultError({ cause: Cause.fail(error) }),
|
|
151
144
|
});
|
|
152
145
|
}),
|
|
153
146
|
)
|
|
154
|
-
.pipe(Effect.
|
|
147
|
+
.pipe(Effect.catchCause(Effect.logError));
|
|
155
148
|
|
|
156
149
|
yield* Effect.logWarning(`Mutation ${mutationID} for client ${clientID} was retried after an error: ${e}`);
|
|
157
150
|
|
|
@@ -167,7 +160,7 @@ class MutatorNotFoundError extends Data.TaggedError("MutatorNotFoundError")<{
|
|
|
167
160
|
}> {}
|
|
168
161
|
|
|
169
162
|
class ServerArgsParseError extends Data.TaggedError("ServerArgsParseError")<{
|
|
170
|
-
readonly cause: Cause.Cause<
|
|
163
|
+
readonly cause: Cause.Cause<Schema.SchemaError>;
|
|
171
164
|
}> {}
|
|
172
165
|
|
|
173
166
|
const MutationUserErrorTypeId = Symbol.for(prefixId("MutationUserError"));
|
|
@@ -190,25 +183,29 @@ export const handleQuery = Effect.fn(function* <E, R1, R2>(
|
|
|
190
183
|
schema: ZeroSchema,
|
|
191
184
|
payload: TransformRequestMessage,
|
|
192
185
|
) {
|
|
193
|
-
const
|
|
186
|
+
const ctx = yield* Effect.context<R1 | R2>();
|
|
194
187
|
return yield* Effect.tryPromise({
|
|
195
188
|
try: () =>
|
|
196
189
|
handleQueryRequest(
|
|
197
190
|
(name, args) =>
|
|
198
191
|
Arr.findFirst(queries, (q) => q[QueryNameSymbol] === name).pipe(
|
|
199
|
-
Effect.
|
|
192
|
+
Effect.fromOption,
|
|
193
|
+
Effect.catchTag("NoSuchElementError", () => Effect.fail(new QueryNotFound({ name }))),
|
|
200
194
|
Effect.flatMap((query) => query[RunQuerySymbol]({ _tag: "Encoded", args })),
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
195
|
+
Effect.runSyncExitWith(ctx),
|
|
196
|
+
(exit) => {
|
|
197
|
+
if (Exit.isFailure(exit)) {
|
|
198
|
+
throw new QueryUserError<E>({ cause: exit.cause });
|
|
199
|
+
}
|
|
200
|
+
return exit.value;
|
|
201
|
+
},
|
|
205
202
|
),
|
|
206
203
|
schema,
|
|
207
204
|
payload as ReadonlyJSONValue,
|
|
208
205
|
),
|
|
209
206
|
catch: (e) =>
|
|
210
207
|
Option.liftPredicate(e, QueryUserError.is<E>).pipe(
|
|
211
|
-
Option.flatMap((e) => Cause.
|
|
208
|
+
Option.flatMap((e) => Cause.findErrorOption(e.cause)),
|
|
212
209
|
Option.getOrElse(() => new QueryRequestError({ cause: Cause.fail(e) })),
|
|
213
210
|
),
|
|
214
211
|
});
|
|
@@ -222,7 +219,7 @@ class QueryNotFound extends Data.TaggedError("QueryNotFound")<{ name: string }>
|
|
|
222
219
|
|
|
223
220
|
const QueryUserErrorTypeId = Symbol.for(prefixId("QueryUserError"));
|
|
224
221
|
class QueryUserError<E> extends Data.TaggedError("QueryUserError")<{
|
|
225
|
-
cause: Cause.Cause<E |
|
|
222
|
+
cause: Cause.Cause<E | Schema.SchemaError | QueryNotFound>;
|
|
226
223
|
}> {
|
|
227
224
|
readonly [QueryUserErrorTypeId] = QueryUserErrorTypeId;
|
|
228
225
|
static is<E>(e: unknown): e is QueryUserError<E> {
|
package/src/types/error.ts
CHANGED
|
@@ -20,11 +20,11 @@ const PushFailedBase = Schema.Struct({
|
|
|
20
20
|
message: Schema.String,
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
export const PushFailedBody = Schema.Union(
|
|
23
|
+
export const PushFailedBody = Schema.Union([
|
|
24
24
|
Schema.Struct({
|
|
25
25
|
...PushFailedBase.fields,
|
|
26
26
|
origin: Schema.Literal("server"),
|
|
27
|
-
reason: Schema.
|
|
27
|
+
reason: Schema.Literals(["database", "parse", "oooMutation", "unsupportedPushVersion", "internal"]),
|
|
28
28
|
}),
|
|
29
29
|
Schema.Struct({
|
|
30
30
|
...PushFailedBase.fields,
|
|
@@ -36,9 +36,9 @@ export const PushFailedBody = Schema.Union(
|
|
|
36
36
|
Schema.Struct({
|
|
37
37
|
...PushFailedBase.fields,
|
|
38
38
|
origin: Schema.Literal("zeroCache"),
|
|
39
|
-
reason: Schema.
|
|
39
|
+
reason: Schema.Literals(["timeout", "parse", "internal"]),
|
|
40
40
|
}),
|
|
41
|
-
);
|
|
41
|
+
]);
|
|
42
42
|
|
|
43
43
|
const TransformFailedBase = Schema.Struct({
|
|
44
44
|
kind: Schema.Literal("TransformFailed"),
|
|
@@ -50,11 +50,11 @@ const TransformFailedBase = Schema.Struct({
|
|
|
50
50
|
message: Schema.String,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
export const TransformFailedBody = Schema.Union(
|
|
53
|
+
export const TransformFailedBody = Schema.Union([
|
|
54
54
|
Schema.Struct({
|
|
55
55
|
...TransformFailedBase.fields,
|
|
56
56
|
origin: Schema.Literal("server"),
|
|
57
|
-
reason: Schema.
|
|
57
|
+
reason: Schema.Literals(["database", "parse", "internal"]),
|
|
58
58
|
}),
|
|
59
59
|
Schema.Struct({
|
|
60
60
|
...TransformFailedBase.fields,
|
|
@@ -66,6 +66,6 @@ export const TransformFailedBody = Schema.Union(
|
|
|
66
66
|
Schema.Struct({
|
|
67
67
|
...TransformFailedBase.fields,
|
|
68
68
|
origin: Schema.Literal("zeroCache"),
|
|
69
|
-
reason: Schema.
|
|
69
|
+
reason: Schema.Literals(["timeout", "parse", "internal"]),
|
|
70
70
|
}),
|
|
71
|
-
);
|
|
71
|
+
]);
|