effect-zero 0.4.2 → 0.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client-transaction.d.ts +85 -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 +130 -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 +156 -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 +287 -29
- 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 +29 -31
- package/src/client-transaction.ts +15 -11
- package/src/client.ts +19 -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 +34 -37
- package/src/server.ts +36 -37
- package/src/types/error.ts +8 -8
- package/src/types/push.ts +22 -22
- package/src/types/queries.ts +7 -7
package/src/mutators.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type AnyMutatorFns<R = any> = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
18
|
-
export type AnyMutatorSchema = Schema.
|
|
18
|
+
export type AnyMutatorSchema = Schema.Codec<any, any>;
|
|
19
19
|
export type AnyMutatorSchemas = Record<string, AnyMutatorSchema | Record<string, AnyMutatorSchema>>;
|
|
20
20
|
|
|
21
21
|
export type MutatorFns<TSchema extends AnyMutatorSchemas> = {
|
|
@@ -71,7 +71,7 @@ export function make(schema: AnyMutatorSchemas, mutators: AnyMutatorFns): AnyMut
|
|
|
71
71
|
return Rec.map(mutators, (v, name) => {
|
|
72
72
|
return Match.value([v, schema[name]]).pipe(
|
|
73
73
|
Match.when([Predicate.isFunction, Schema.isSchema], ([mutator, schema]) => makeMutator(schema, mutator)),
|
|
74
|
-
Match.when([Predicate.
|
|
74
|
+
Match.when([Predicate.isObject, Predicate.isObject], ([mutator, schema]) =>
|
|
75
75
|
Rec.map(mutator, (mutator, name) => makeMutator(Option.getOrThrow(Rec.get(schema, name)), mutator)),
|
|
76
76
|
),
|
|
77
77
|
Match.orElseAbsurd,
|
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,37 @@
|
|
|
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
|
|
12
|
+
import type * as Unify from "effect/Unify";
|
|
16
13
|
import type * as ClientTransaction from "./client-transaction.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
OutOfOrderMutationError,
|
|
20
|
-
ServerSynchronizationContext,
|
|
21
|
-
ServerTransactionInput,
|
|
22
|
-
} from "./internal/server.js";
|
|
14
|
+
import { MutationAlreadyProcessedError, OutOfOrderMutationError, ServerTransactionInput } from "./internal/server.js";
|
|
15
|
+
import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
|
|
23
16
|
import { prefixId } from "./internal/utils.js";
|
|
24
17
|
|
|
25
18
|
// Updated to: https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
type _ = NodeInspectSymbol;
|
|
21
|
+
type _Unify = Unify.typeSymbol | Unify.unifySymbol | Unify.ignoreSymbol;
|
|
22
|
+
|
|
23
|
+
// biome-ignore lint/suspicious/noExplicitAny: any client transaction (its Id literal is irrelevant here)
|
|
24
|
+
export type Context<TSchema extends ZeroSchema, TTransaction> = ReturnType<typeof make<any, TSchema, TTransaction>>;
|
|
30
25
|
|
|
31
26
|
export const make = <const Id extends string, TSchema extends ZeroSchema, TTransaction>(
|
|
32
27
|
id: Id,
|
|
33
28
|
database: ZQLDatabase<TSchema, TTransaction>,
|
|
34
|
-
clientTransaction:
|
|
29
|
+
clientTransaction: ClientTransaction.Context<TSchema>,
|
|
35
30
|
) => {
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
class Context extends Ctx.Service<
|
|
32
|
+
Context,
|
|
38
33
|
{ transaction: ZeroServerTransaction<TSchema, TTransaction>; transactionHooks: TransactionProviderHooks }
|
|
39
|
-
>()
|
|
34
|
+
>()(`${id as string}/ServerTransactionContext` as const) {}
|
|
40
35
|
|
|
41
36
|
const use = <A>(
|
|
42
37
|
fn: (
|
|
@@ -44,32 +39,33 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
44
39
|
options: { readonly signal: AbortSignal },
|
|
45
40
|
) => PromiseLike<A>,
|
|
46
41
|
) =>
|
|
47
|
-
Effect.
|
|
48
|
-
|
|
42
|
+
Effect.gen(function* () {
|
|
43
|
+
const ctx = yield* Context;
|
|
44
|
+
return yield* Effect.tryPromise({
|
|
49
45
|
try: (signal) => fn(ctx.transaction, { signal }),
|
|
50
46
|
catch: (error) => new ServerTransactionError({ cause: Cause.fail(error) }),
|
|
51
|
-
})
|
|
52
|
-
);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
53
49
|
|
|
54
50
|
const execute = Effect.fn(function* <A, E, R>(effect: Effect.Effect<A, E, R>) {
|
|
55
|
-
const
|
|
56
|
-
ServerTransactionInput | Exclude<R,
|
|
51
|
+
const ctx = yield* Effect.context<
|
|
52
|
+
ServerTransactionInput | Exclude<R, (typeof clientTransaction.Context)["Identifier"] | Context>
|
|
57
53
|
>();
|
|
58
|
-
const result = yield* Deferred.make<A, E | Effect.
|
|
54
|
+
const result = yield* Deferred.make<A, E | Effect.Error<typeof checkAndIncrementLastMutationId>>();
|
|
59
55
|
|
|
60
56
|
const transactionInput = yield* ServerTransactionInput;
|
|
61
57
|
yield* Effect.tryPromise({
|
|
62
58
|
try: (signal) =>
|
|
63
59
|
database.transaction(async (transaction, transactionHooks) => {
|
|
64
|
-
const exit = await Effect.
|
|
60
|
+
const exit = await Effect.flatMap(checkAndIncrementLastMutationId, () => effect).pipe(
|
|
65
61
|
Effect.provide([
|
|
66
|
-
Layer.succeed(
|
|
67
|
-
Layer.succeed(clientTransaction, transaction),
|
|
62
|
+
Layer.succeed(Context, { transaction, transactionHooks }),
|
|
63
|
+
Layer.succeed(clientTransaction.Context, transaction),
|
|
68
64
|
]),
|
|
69
|
-
(effect) =>
|
|
65
|
+
(effect) => Effect.runPromiseExitWith(ctx)(effect, { signal }),
|
|
70
66
|
);
|
|
71
|
-
Deferred.
|
|
72
|
-
|
|
67
|
+
Deferred.doneUnsafe(result, exit);
|
|
68
|
+
if (Exit.isFailure(exit)) {
|
|
73
69
|
// This error's purpose is to differentiate between "external" errors
|
|
74
70
|
// that originate from the user-defined mutator code and "internal" errors
|
|
75
71
|
// that originate from our own code and the Zero API.
|
|
@@ -78,7 +74,8 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
78
74
|
// are already covered by passing the Exit result to the Deferred, which is why
|
|
79
75
|
// we have the ServerTransactionUserError silenced below in the pipe.
|
|
80
76
|
throw new ServerTransactionUserError();
|
|
81
|
-
}
|
|
77
|
+
}
|
|
78
|
+
return exit.value;
|
|
82
79
|
}, transactionInput),
|
|
83
80
|
catch: (error) => {
|
|
84
81
|
if (ServerTransactionUserError.is(error)) {
|
|
@@ -90,11 +87,11 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
90
87
|
},
|
|
91
88
|
}).pipe(Effect.catchTag("ServerTransactionUserError", () => Effect.void));
|
|
92
89
|
|
|
93
|
-
return yield* result;
|
|
90
|
+
return yield* Deferred.await(result);
|
|
94
91
|
}, ServerSynchronizationContext.guard);
|
|
95
92
|
|
|
96
93
|
const checkAndIncrementLastMutationId = Effect.gen(function* () {
|
|
97
|
-
const { transactionHooks } = yield*
|
|
94
|
+
const { transactionHooks } = yield* Context;
|
|
98
95
|
const { clientID, mutationID: receivedMutationID } = yield* ServerTransactionInput;
|
|
99
96
|
|
|
100
97
|
const { lastMutationID } = yield* Effect.tryPromise({
|
|
@@ -118,7 +115,7 @@ export const make = <const Id extends string, TSchema extends ZeroSchema, TTrans
|
|
|
118
115
|
}
|
|
119
116
|
});
|
|
120
117
|
|
|
121
|
-
return
|
|
118
|
+
return { Context, use, execute };
|
|
122
119
|
};
|
|
123
120
|
|
|
124
121
|
class ServerTransactionError extends Data.TaggedError("ServerTransactionError")<{
|
package/src/server.ts
CHANGED
|
@@ -2,27 +2,22 @@ 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
|
-
|
|
23
|
-
ServerTransactionInput,
|
|
24
|
-
} from "./internal/server.js";
|
|
25
|
-
import { prefixId } from "./internal/utils.js";
|
|
17
|
+
import type * as Unify from "effect/Unify";
|
|
18
|
+
import { MutationAlreadyProcessedError, OutOfOrderMutationError, ServerTransactionInput } from "./internal/server.js";
|
|
19
|
+
import * as ServerSynchronizationContext from "./internal/server-synchronization-context.js";
|
|
20
|
+
import { normalizeArgs, prefixId } from "./internal/utils.js";
|
|
26
21
|
import * as Mutators from "./mutators.js";
|
|
27
22
|
import { type MakeQueryResult, QueryNameSymbol, RunQuerySymbol } from "./query.js";
|
|
28
23
|
import type * as ServerTransaction from "./server-transaction.js";
|
|
@@ -33,17 +28,15 @@ import type { TransformRequestMessage } from "./types/queries.js";
|
|
|
33
28
|
// https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
|
|
34
29
|
// https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/process-mutations.ts
|
|
35
30
|
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
"use"
|
|
39
|
-
>;
|
|
31
|
+
type _ = NodeInspectSymbol;
|
|
32
|
+
type _Unify = Unify.typeSymbol | Unify.unifySymbol | Unify.ignoreSymbol;
|
|
40
33
|
|
|
41
34
|
export const processPush = Effect.fn(function* <
|
|
42
35
|
TSchema extends ZeroSchema,
|
|
43
36
|
TTransaction,
|
|
44
37
|
TMutators extends Mutators.AnyMutators,
|
|
45
38
|
>(
|
|
46
|
-
transaction:
|
|
39
|
+
transaction: ServerTransaction.Context<TSchema, TTransaction>,
|
|
47
40
|
mutators: TMutators,
|
|
48
41
|
params: Types.PushParams,
|
|
49
42
|
request: Types.PushBody,
|
|
@@ -60,7 +53,7 @@ export const processPush = Effect.fn(function* <
|
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
return yield* processMutation<Mutators.ExtractMutatorsRequirements<TMutators>>(mutators, mutation).pipe(
|
|
63
|
-
Effect.
|
|
56
|
+
Effect.catch((e) => processMutationError(transaction, e)),
|
|
64
57
|
Effect.map((result) =>
|
|
65
58
|
Types.MutationResponse.make({ id: { id: mutation.id, clientID: mutation.clientID }, result }),
|
|
66
59
|
),
|
|
@@ -70,7 +63,7 @@ export const processPush = Effect.fn(function* <
|
|
|
70
63
|
clientGroupID: request.clientGroupID,
|
|
71
64
|
upstreamSchema: params.schema,
|
|
72
65
|
}),
|
|
73
|
-
Effect.provide(ServerSynchronizationContext.
|
|
66
|
+
Effect.provide(ServerSynchronizationContext.layer),
|
|
74
67
|
);
|
|
75
68
|
}),
|
|
76
69
|
),
|
|
@@ -81,7 +74,7 @@ export const processPush = Effect.fn(function* <
|
|
|
81
74
|
Stream.runCollect,
|
|
82
75
|
);
|
|
83
76
|
|
|
84
|
-
return { mutations:
|
|
77
|
+
return { mutations: responses } satisfies Types.PushResponse;
|
|
85
78
|
});
|
|
86
79
|
|
|
87
80
|
const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R>, mutation: Types.Mutation) {
|
|
@@ -93,17 +86,18 @@ const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R
|
|
|
93
86
|
Rec.get<string>(namespace),
|
|
94
87
|
Option.flatMap((mutator) =>
|
|
95
88
|
Match.value([mutator, name]).pipe(
|
|
96
|
-
Match.when([Predicate.
|
|
89
|
+
Match.when([Predicate.isObject, Predicate.isString], ([mutator, name]) => Rec.get<string>(name)(mutator)),
|
|
97
90
|
Match.when([Predicate.isFunction, Predicate.isUndefined], ([mutator]) => Option.some(mutator)),
|
|
98
91
|
Match.orElse(() => Option.none()),
|
|
99
92
|
),
|
|
100
93
|
),
|
|
101
|
-
Effect.
|
|
94
|
+
Effect.fromOption,
|
|
95
|
+
Effect.catchTag("NoSuchElementError", () => Effect.fail(new MutatorNotFoundError({ name: mutation.name }))),
|
|
102
96
|
);
|
|
103
97
|
|
|
104
|
-
const args = yield* Schema.
|
|
105
|
-
|
|
106
|
-
);
|
|
98
|
+
const args = yield* Schema.decodeUnknownEffect(mutator[Mutators.MutatorSchemaSymbol])(
|
|
99
|
+
normalizeArgs(mutation.args[0]),
|
|
100
|
+
).pipe(Effect.catchTag("SchemaError", (e) => Effect.fail(new ServerArgsParseError({ cause: Cause.fail(e) }))));
|
|
107
101
|
|
|
108
102
|
return yield* mutator(args).pipe(
|
|
109
103
|
ServerSynchronizationContext.finalize,
|
|
@@ -120,12 +114,12 @@ const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R
|
|
|
120
114
|
),
|
|
121
115
|
// Case #5 "Zero transactions then fail" / #6 "Fail before transaction"
|
|
122
116
|
// Catches all errors that are produced before the transaction is executed
|
|
123
|
-
Effect.
|
|
117
|
+
Effect.catchCause((cause) => Effect.fail(new MutationUserError({ cause }))),
|
|
124
118
|
);
|
|
125
119
|
});
|
|
126
120
|
|
|
127
121
|
const processMutationError = Effect.fn(function* <TSchema extends ZeroSchema, TTransaction>(
|
|
128
|
-
transaction:
|
|
122
|
+
transaction: ServerTransaction.Context<TSchema, TTransaction>,
|
|
129
123
|
e: unknown,
|
|
130
124
|
) {
|
|
131
125
|
const { clientID, mutationID } = yield* ServerTransactionInput;
|
|
@@ -144,14 +138,15 @@ const processMutationError = Effect.fn(function* <TSchema extends ZeroSchema, TT
|
|
|
144
138
|
|
|
145
139
|
yield* transaction
|
|
146
140
|
.execute(
|
|
147
|
-
Effect.
|
|
148
|
-
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
const { transactionHooks } = yield* transaction.Context;
|
|
143
|
+
return yield* Effect.tryPromise({
|
|
149
144
|
try: () => transactionHooks.writeMutationResult({ id: { id: mutationID, clientID }, result: appError }),
|
|
150
145
|
catch: (error) => new WriteMutationResultError({ cause: Cause.fail(error) }),
|
|
151
146
|
});
|
|
152
147
|
}),
|
|
153
148
|
)
|
|
154
|
-
.pipe(Effect.
|
|
149
|
+
.pipe(Effect.catchCause(Effect.logError));
|
|
155
150
|
|
|
156
151
|
yield* Effect.logWarning(`Mutation ${mutationID} for client ${clientID} was retried after an error: ${e}`);
|
|
157
152
|
|
|
@@ -167,7 +162,7 @@ class MutatorNotFoundError extends Data.TaggedError("MutatorNotFoundError")<{
|
|
|
167
162
|
}> {}
|
|
168
163
|
|
|
169
164
|
class ServerArgsParseError extends Data.TaggedError("ServerArgsParseError")<{
|
|
170
|
-
readonly cause: Cause.Cause<
|
|
165
|
+
readonly cause: Cause.Cause<Schema.SchemaError>;
|
|
171
166
|
}> {}
|
|
172
167
|
|
|
173
168
|
const MutationUserErrorTypeId = Symbol.for(prefixId("MutationUserError"));
|
|
@@ -190,25 +185,29 @@ export const handleQuery = Effect.fn(function* <E, R1, R2>(
|
|
|
190
185
|
schema: ZeroSchema,
|
|
191
186
|
payload: TransformRequestMessage,
|
|
192
187
|
) {
|
|
193
|
-
const
|
|
188
|
+
const ctx = yield* Effect.context<R1 | R2>();
|
|
194
189
|
return yield* Effect.tryPromise({
|
|
195
190
|
try: () =>
|
|
196
191
|
handleQueryRequest(
|
|
197
192
|
(name, args) =>
|
|
198
193
|
Arr.findFirst(queries, (q) => q[QueryNameSymbol] === name).pipe(
|
|
199
|
-
Effect.
|
|
194
|
+
Effect.fromOption,
|
|
195
|
+
Effect.catchTag("NoSuchElementError", () => Effect.fail(new QueryNotFound({ name }))),
|
|
200
196
|
Effect.flatMap((query) => query[RunQuerySymbol]({ _tag: "Encoded", args })),
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
},
|
|
205
204
|
),
|
|
206
205
|
schema,
|
|
207
206
|
payload as ReadonlyJSONValue,
|
|
208
207
|
),
|
|
209
208
|
catch: (e) =>
|
|
210
209
|
Option.liftPredicate(e, QueryUserError.is<E>).pipe(
|
|
211
|
-
Option.flatMap((e) => Cause.
|
|
210
|
+
Option.flatMap((e) => Cause.findErrorOption(e.cause)),
|
|
212
211
|
Option.getOrElse(() => new QueryRequestError({ cause: Cause.fail(e) })),
|
|
213
212
|
),
|
|
214
213
|
});
|
|
@@ -222,7 +221,7 @@ class QueryNotFound extends Data.TaggedError("QueryNotFound")<{ name: string }>
|
|
|
222
221
|
|
|
223
222
|
const QueryUserErrorTypeId = Symbol.for(prefixId("QueryUserError"));
|
|
224
223
|
class QueryUserError<E> extends Data.TaggedError("QueryUserError")<{
|
|
225
|
-
cause: Cause.Cause<E |
|
|
224
|
+
cause: Cause.Cause<E | Schema.SchemaError | QueryNotFound>;
|
|
226
225
|
}> {
|
|
227
226
|
readonly [QueryUserErrorTypeId] = QueryUserErrorTypeId;
|
|
228
227
|
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
|
+
]);
|