effect-zero 0.1.1 → 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.
- package/README.md +138 -95
- package/dist/client-transaction.d.ts +22 -0
- package/dist/client-transaction.d.ts.map +1 -0
- package/dist/client-transaction.js +15 -0
- package/dist/client-transaction.js.map +1 -0
- package/dist/client.d.ts +10 -38
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +23 -50
- package/dist/client.js.map +1 -1
- package/dist/internal/server.d.ts +54 -0
- package/dist/internal/server.d.ts.map +1 -0
- package/dist/internal/server.js +64 -0
- package/dist/internal/server.js.map +1 -0
- package/dist/internal/utils.d.ts.map +1 -0
- package/dist/internal/utils.js.map +1 -0
- package/dist/mutators.d.ts +18 -23
- package/dist/mutators.d.ts.map +1 -1
- package/dist/mutators.js +9 -18
- package/dist/mutators.js.map +1 -1
- package/dist/query.d.ts +30 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +54 -0
- package/dist/query.js.map +1 -0
- package/dist/server-transaction.d.ts +42 -0
- package/dist/server-transaction.d.ts.map +1 -0
- package/dist/server-transaction.js +88 -0
- package/dist/server-transaction.js.map +1 -0
- package/dist/server.d.ts +111 -168
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +80 -201
- package/dist/server.js.map +1 -1
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +3 -1
- package/dist/snapshot.js.map +1 -1
- package/dist/types/common.d.ts +4 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +4 -0
- package/dist/types/common.js.map +1 -0
- package/dist/{types.d.ts → types/push.d.ts} +15 -15
- package/dist/types/push.d.ts.map +1 -0
- package/dist/{types.js → types/push.js} +54 -54
- package/dist/types/push.js.map +1 -0
- package/dist/types/queries.d.ts +29 -0
- package/dist/types/queries.d.ts.map +1 -0
- package/dist/types/queries.js +42 -0
- package/dist/types/queries.js.map +1 -0
- package/package.json +45 -21
- package/src/client-transaction.ts +30 -0
- package/src/client.ts +66 -116
- package/src/internal/server.ts +90 -0
- package/src/mutators.ts +42 -44
- package/src/query.ts +117 -0
- package/src/server-transaction.ts +141 -0
- package/src/server.ts +174 -320
- package/src/snapshot.ts +0 -2
- package/src/types/common.ts +4 -0
- package/src/types/push.ts +197 -0
- package/src/types/queries.ts +56 -0
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/types.ts +0 -201
- /package/dist/{utils.d.ts → internal/utils.d.ts} +0 -0
- /package/dist/{utils.js → internal/utils.js} +0 -0
- /package/src/{utils.ts → internal/utils.ts} +0 -0
package/src/client.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { HumanReadable, Query, ReadonlyJSONValue, Transaction, Schema as ZeroSchema } from "@rocicorp/zero";
|
|
3
|
-
import type { QueryResult } from "@rocicorp/zero/react";
|
|
1
|
+
import { Zero, type ZeroOptions, type Schema as ZeroSchema, type Transaction as ZeroTransaction } from "@rocicorp/zero";
|
|
4
2
|
import * as Cause from "effect/Cause";
|
|
5
|
-
import * as Context from "effect/Context";
|
|
6
3
|
import * as Data from "effect/Data";
|
|
7
4
|
import * as Effect from "effect/Effect";
|
|
8
5
|
import * as Exit from "effect/Exit";
|
|
@@ -12,119 +9,72 @@ import * as Predicate from "effect/Predicate";
|
|
|
12
9
|
import * as Rec from "effect/Record";
|
|
13
10
|
import * as Runtime from "effect/Runtime";
|
|
14
11
|
import * as Schema from "effect/Schema";
|
|
15
|
-
import * as
|
|
16
|
-
import * as
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw Cause.squash(c);
|
|
62
|
-
});
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return Rec.map(mutators, (v) =>
|
|
67
|
-
Match.value(v).pipe(Match.when(Predicate.isFunction, unwrapMutator), Match.orElse(Rec.map(unwrapMutator))),
|
|
68
|
-
) as UnwrapMutators<S, T>;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const querySub = Effect.fn(function* <T extends keyof S["tables"] & string, R>(query: Query<S, T, R>) {
|
|
72
|
-
const view = yield* Effect.acquireRelease(
|
|
73
|
-
Effect.sync(() => query.materialize()),
|
|
74
|
-
(view) => Effect.sync(() => view.destroy()),
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const subscriptionRef = yield* SubscriptionRef.make<QueryResult<R>>(getDefaultSnapshot(query.format.singular));
|
|
78
|
-
|
|
79
|
-
yield* Stream.asyncEffect<Parameters<Parameters<(typeof view)["addListener"]>[0]>>((emit) =>
|
|
80
|
-
Effect.sync(() => view.addListener((...args) => emit.single(args))),
|
|
81
|
-
).pipe(
|
|
82
|
-
Stream.mapEffect(([data, resultType]) =>
|
|
83
|
-
Effect.sync(() => {
|
|
84
|
-
// logic here borrowed from: https://github.com/rocicorp/mono/blob/288b00ec94f5a9ae6e988513423af25c281dbb2a/packages/zero-react/src/use-query.tsx#L295
|
|
85
|
-
const cloned = data === undefined ? data : (deepClone(data as ReadonlyJSONValue) as HumanReadable<R>);
|
|
86
|
-
return getSnapshot<R>(query.format.singular, cloned, resultType);
|
|
87
|
-
}),
|
|
88
|
-
),
|
|
89
|
-
Stream.runForEach((snapshot) => SubscriptionRef.set(subscriptionRef, snapshot)),
|
|
90
|
-
Effect.forkScoped,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
return subscriptionRef.pipe(
|
|
94
|
-
Subscribable.map(([data, { type: status }]) => ({
|
|
95
|
-
data,
|
|
96
|
-
status,
|
|
97
|
-
})),
|
|
98
|
-
);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/** Create an Atom for the query */
|
|
102
|
-
const queryAtom = Atom.family(<T extends keyof S["tables"] & string, R>(query: Query<S, T, R>) =>
|
|
103
|
-
Atom.subscribable(querySub(query)).pipe(Atom.mapResult((res) => res.data)),
|
|
12
|
+
import type * as ClientTransaction from "./client-transaction.js";
|
|
13
|
+
import * as Mutators from "./mutators.js";
|
|
14
|
+
|
|
15
|
+
type ClientTransactionContext<TSchema extends ZeroSchema> = Omit<
|
|
16
|
+
ReturnType<typeof ClientTransaction.make<string, TSchema>>,
|
|
17
|
+
"use"
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export const make = Effect.fn(function* <TSchema extends ZeroSchema, TMutators extends Mutators.AnyMutators>(
|
|
21
|
+
transaction: ClientTransactionContext<TSchema>,
|
|
22
|
+
mutators: TMutators,
|
|
23
|
+
options: Omit<ZeroOptions<TSchema>, "schema" | "mutators">,
|
|
24
|
+
) {
|
|
25
|
+
const runtime =
|
|
26
|
+
yield* Effect.runtime<
|
|
27
|
+
Exclude<Mutators.ExtractMutatorsRequirements<TMutators>, ClientTransaction.ClientTransaction>
|
|
28
|
+
>();
|
|
29
|
+
|
|
30
|
+
function unwrapMutator<E>(mutator: Mutators.AnyMutator<Mutators.ExtractMutatorsRequirements<TMutators>, E>) {
|
|
31
|
+
return async (tx: ZeroTransaction<TSchema>, args: unknown) => {
|
|
32
|
+
const exit = await Schema.decode(mutator[Mutators.MutatorSchemaSymbol])(args).pipe(
|
|
33
|
+
Effect.catchTag("ParseError", (e) => new ClientArgsParseError({ cause: Cause.fail(e) })),
|
|
34
|
+
Effect.flatMap(mutator),
|
|
35
|
+
Effect.provideService(transaction, tx),
|
|
36
|
+
Runtime.runPromiseExit(runtime),
|
|
37
|
+
);
|
|
38
|
+
return Exit.getOrElse(exit, (c) => {
|
|
39
|
+
// Extract underlying error bypassing FiberFailure
|
|
40
|
+
throw Cause.squash(c);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const unwrappedMutators = Rec.map(mutators, (v) =>
|
|
46
|
+
Match.value(v).pipe(Match.when(Predicate.isFunction, unwrapMutator), Match.orElse(Rec.map(unwrapMutator))),
|
|
47
|
+
) as UnwrapMutators<TSchema, TMutators>;
|
|
48
|
+
|
|
49
|
+
return yield* Effect.acquireRelease(
|
|
50
|
+
Effect.sync(() => {
|
|
51
|
+
return new Zero({
|
|
52
|
+
...options,
|
|
53
|
+
schema: transaction.schema,
|
|
54
|
+
mutators: unwrappedMutators,
|
|
55
|
+
});
|
|
56
|
+
}),
|
|
57
|
+
(zero) => Effect.promise(() => zero.close()),
|
|
104
58
|
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
type
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
type UnwrapMutator<TSchema extends ZeroSchema, TMutators extends Mutators.AnyMutator> = Parameters<TMutators> extends []
|
|
62
|
+
? (transaction: ZeroTransaction<TSchema>) => Promise<void>
|
|
63
|
+
: (
|
|
64
|
+
transaction: ZeroTransaction<TSchema>,
|
|
65
|
+
args: Schema.Schema.Encoded<TMutators[typeof Mutators.MutatorSchemaSymbol]>,
|
|
66
|
+
) => Promise<void>;
|
|
67
|
+
|
|
68
|
+
type UnwrapMutators<TSchema extends ZeroSchema, TMutators extends Mutators.AnyMutators> = {
|
|
69
|
+
[A in keyof TMutators]: TMutators[A] extends Mutators.AnyMutator
|
|
70
|
+
? UnwrapMutator<TSchema, TMutators[A]>
|
|
71
|
+
: {
|
|
72
|
+
[B in keyof TMutators[A]]: TMutators[A][B] extends Mutators.AnyMutator
|
|
73
|
+
? UnwrapMutator<TSchema, TMutators[A][B]>
|
|
74
|
+
: never;
|
|
75
|
+
};
|
|
122
76
|
} & {};
|
|
123
77
|
|
|
124
|
-
export class
|
|
125
|
-
cause: Cause.Cause<
|
|
126
|
-
}> {}
|
|
127
|
-
|
|
128
|
-
export class ZeroClientArgsParseError extends Data.TaggedError("ZeroClientArgsParseError")<{
|
|
129
|
-
cause: Cause.Cause<ParseResult.ParseError>;
|
|
78
|
+
export class ClientArgsParseError extends Data.TaggedError("ClientArgsParseError")<{
|
|
79
|
+
readonly cause: Cause.Cause<ParseResult.ParseError>;
|
|
130
80
|
}> {}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { TransactionProviderInput } from "@rocicorp/zero/server";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Data from "effect/Data";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Predicate from "effect/Predicate";
|
|
6
|
+
import * as SynchronizedRef from "effect/SynchronizedRef";
|
|
7
|
+
import { prefixId } from "./utils.js";
|
|
8
|
+
|
|
9
|
+
export class ServerTransactionInput extends Context.Tag(prefixId("ServerTransactionInput"))<
|
|
10
|
+
ServerTransactionInput,
|
|
11
|
+
TransactionProviderInput
|
|
12
|
+
>() {}
|
|
13
|
+
|
|
14
|
+
export class ServerSynchronizationContext extends Effect.Service<ServerSynchronizationContext>()(
|
|
15
|
+
prefixId("ServerSynchronizationContext"),
|
|
16
|
+
{
|
|
17
|
+
effect: Effect.gen(function* () {
|
|
18
|
+
return {
|
|
19
|
+
wasTransactionExecuted: yield* SynchronizedRef.make(false),
|
|
20
|
+
};
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
) {
|
|
24
|
+
// Ensures that only one transaction is executed at a time and checks that another transaction wasn't already executed.
|
|
25
|
+
static guard = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
26
|
+
Effect.flatMap(this, (ctx) =>
|
|
27
|
+
SynchronizedRef.modifyEffect(
|
|
28
|
+
ctx.wasTransactionExecuted,
|
|
29
|
+
Effect.fn(function* (wasTransactionExecuted) {
|
|
30
|
+
if (wasTransactionExecuted) {
|
|
31
|
+
return yield* new MultipleTransactionsError();
|
|
32
|
+
}
|
|
33
|
+
const result = yield* effect;
|
|
34
|
+
return [result, true];
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
static finalize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
40
|
+
Effect.gen(this, function* () {
|
|
41
|
+
const wasTransactionExecuted = yield* this.pipe(Effect.flatMap((ctx) => ctx.wasTransactionExecuted));
|
|
42
|
+
|
|
43
|
+
return yield* effect.pipe(
|
|
44
|
+
// Case #2 "One transaction then fail"
|
|
45
|
+
// If the transaction was executed successfully, swallow the error and just log it, otherwise re-throw it
|
|
46
|
+
Effect.catchAllCause((e) => {
|
|
47
|
+
if (wasTransactionExecuted) {
|
|
48
|
+
return Effect.logError("Error occurred after transaction execution completed", e);
|
|
49
|
+
}
|
|
50
|
+
return Effect.failCause(e);
|
|
51
|
+
}),
|
|
52
|
+
// Case #4 "Zero transactions then succeed"
|
|
53
|
+
// Check that the transaction was executed during the mutation
|
|
54
|
+
Effect.tap(!wasTransactionExecuted ? new NoTransactionError() : Effect.void),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class NoTransactionError extends Data.TaggedError("NoTransactionError") {}
|
|
60
|
+
export class MultipleTransactionsError extends Data.TaggedError("MultipleTransactionsError") {}
|
|
61
|
+
|
|
62
|
+
const OutOfOrderMutationErrorTypeId = Symbol.for(prefixId("OutOfOrderMutationError"));
|
|
63
|
+
export class OutOfOrderMutationError extends Data.TaggedError("OutOfOrderMutationError")<{
|
|
64
|
+
readonly clientID: string;
|
|
65
|
+
readonly receivedMutationID: number;
|
|
66
|
+
readonly lastMutationID: number | bigint;
|
|
67
|
+
}> {
|
|
68
|
+
readonly [OutOfOrderMutationErrorTypeId] = OutOfOrderMutationErrorTypeId;
|
|
69
|
+
override get message() {
|
|
70
|
+
return `Client ${this.clientID} sent mutation ID ${this.receivedMutationID} but expected ${this.lastMutationID}`;
|
|
71
|
+
}
|
|
72
|
+
static is(e: unknown): e is OutOfOrderMutationError {
|
|
73
|
+
return Predicate.hasProperty(e, OutOfOrderMutationErrorTypeId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const MutationAlreadyProcessedErrorTypeId = Symbol.for(prefixId("MutationAlreadyProcessedError"));
|
|
78
|
+
export class MutationAlreadyProcessedError extends Data.TaggedError("MutationAlreadyProcessedError")<{
|
|
79
|
+
readonly clientID: string;
|
|
80
|
+
readonly received: number;
|
|
81
|
+
readonly actual: number | bigint;
|
|
82
|
+
}> {
|
|
83
|
+
readonly [MutationAlreadyProcessedErrorTypeId] = MutationAlreadyProcessedErrorTypeId;
|
|
84
|
+
override get message() {
|
|
85
|
+
return `Ignoring mutation from ${this.clientID} with ID ${this.received} as it was already processed. Expected: ${this.actual}`;
|
|
86
|
+
}
|
|
87
|
+
static is(e: unknown): e is MutationAlreadyProcessedError {
|
|
88
|
+
return Predicate.hasProperty(e, MutationAlreadyProcessedErrorTypeId);
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/mutators.ts
CHANGED
|
@@ -4,45 +4,45 @@ import * as Option from "effect/Option";
|
|
|
4
4
|
import * as Predicate from "effect/Predicate";
|
|
5
5
|
import * as Rec from "effect/Record";
|
|
6
6
|
import * as Schema from "effect/Schema";
|
|
7
|
-
import { prefixId } from "./utils.js";
|
|
7
|
+
import { prefixId } from "./internal/utils.js";
|
|
8
8
|
|
|
9
9
|
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
10
|
-
export type
|
|
10
|
+
export type AnyMutatorFn<R = any, TArgs extends any[] = any[]> = (...args: TArgs) => Effect.Effect<any, any, R>;
|
|
11
11
|
|
|
12
12
|
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
13
|
-
export type
|
|
14
|
-
[K: string]:
|
|
13
|
+
export type AnyMutatorFns<R = any> = {
|
|
14
|
+
[K: string]: AnyMutatorFn<R> | { [K: string]: AnyMutatorFn<R> };
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
18
|
-
export type
|
|
19
|
-
export type
|
|
18
|
+
export type AnyMutatorSchema = Schema.Schema<any, any, never>;
|
|
19
|
+
export type AnyMutatorSchemas = Record<string, AnyMutatorSchema | Record<string, AnyMutatorSchema>>;
|
|
20
20
|
|
|
21
|
-
export type
|
|
22
|
-
[K in keyof TSchema]: TSchema[K] extends
|
|
23
|
-
?
|
|
24
|
-
: TSchema[K] extends infer TSchema extends Record<string,
|
|
25
|
-
? { [K in keyof TSchema]:
|
|
21
|
+
export type MutatorFns<TSchema extends AnyMutatorSchemas> = {
|
|
22
|
+
[K in keyof TSchema]: TSchema[K] extends AnyMutatorSchema
|
|
23
|
+
? AnyMutatorFn<unknown, [Schema.Schema.Type<TSchema[K]>]>
|
|
24
|
+
: TSchema[K] extends infer TSchema extends Record<string, AnyMutatorSchema>
|
|
25
|
+
? { [K in keyof TSchema]: AnyMutatorFn<unknown, [Schema.Schema.Type<TSchema[K]>]> }
|
|
26
26
|
: never;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
export const
|
|
29
|
+
export const MutatorSchemaSymbol = Symbol.for(prefixId("MutatorSchema"));
|
|
30
30
|
|
|
31
31
|
export type Mutator<
|
|
32
|
-
TFunc extends
|
|
33
|
-
TSchema extends
|
|
32
|
+
TFunc extends AnyMutatorFn = AnyMutatorFn,
|
|
33
|
+
TSchema extends AnyMutatorSchema = AnyMutatorSchema,
|
|
34
34
|
> = TFunc & {
|
|
35
|
-
[
|
|
35
|
+
[MutatorSchemaSymbol]: TSchema;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export type Mutators<TSchema extends
|
|
39
|
-
[K in keyof
|
|
40
|
-
? TSchema[K] extends
|
|
41
|
-
? Mutator<
|
|
38
|
+
export type Mutators<TSchema extends AnyMutatorSchemas, TFns extends AnyMutatorFns> = {
|
|
39
|
+
[K in keyof TFns & keyof TSchema]: TFns[K] extends AnyMutatorFn
|
|
40
|
+
? TSchema[K] extends AnyMutatorSchema
|
|
41
|
+
? Mutator<TFns[K], TSchema[K]>
|
|
42
42
|
: never
|
|
43
|
-
: TSchema[K] extends infer TSchema extends Record<string,
|
|
44
|
-
?
|
|
45
|
-
? { [K in keyof
|
|
43
|
+
: TSchema[K] extends infer TSchema extends Record<string, AnyMutatorSchema>
|
|
44
|
+
? TFns[K] extends infer TFns extends Record<string, AnyMutatorFn>
|
|
45
|
+
? { [K in keyof TFns & keyof TSchema]: Mutator<TFns[K], TSchema[K]> }
|
|
46
46
|
: never
|
|
47
47
|
: never;
|
|
48
48
|
} & {};
|
|
@@ -53,30 +53,28 @@ export type AnyMutator<R = any, E = any> = Mutator<(...args: any[]) => Effect.Ef
|
|
|
53
53
|
// biome-ignore lint/suspicious/noExplicitAny: upper bound to allow everything
|
|
54
54
|
export type AnyMutators<R = any> = Record<string, AnyMutator<R> | Record<string, AnyMutator<R>>>;
|
|
55
55
|
|
|
56
|
-
export type ExtractMutatorsRequirements<
|
|
56
|
+
export type ExtractMutatorsRequirements<TMutators extends AnyMutators> = TMutators extends AnyMutators<infer R>
|
|
57
|
+
? R
|
|
58
|
+
: never;
|
|
57
59
|
|
|
58
|
-
export
|
|
59
|
-
private constructor(public schema: T) {}
|
|
60
|
+
export const schema = <TSchema extends AnyMutatorSchemas>(schema: TSchema) => schema;
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
export function make<TSchema extends AnyMutatorSchemas, TFns extends MutatorFns<TSchema>>(
|
|
63
|
+
schema: TSchema,
|
|
64
|
+
mutators: TFns,
|
|
65
|
+
): Mutators<TSchema, TFns>;
|
|
66
|
+
export function make(schema: AnyMutatorSchemas, mutators: AnyMutatorFns): AnyMutators {
|
|
67
|
+
function makeMutator(schema: AnyMutatorSchema, fn: AnyMutatorFn) {
|
|
68
|
+
return Object.assign(fn, { [MutatorSchemaSymbol]: schema });
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Match.when([Predicate.isFunction, Schema.isSchema], ([mutator, schema]) => this.makeMutator(schema, mutator)),
|
|
75
|
-
Match.when([Predicate.isRecord, Predicate.isRecord], ([mutator, schema]) =>
|
|
76
|
-
Rec.map(mutator, (mutator, name) => this.makeMutator(Option.getOrThrow(Rec.get(schema, name)), mutator)),
|
|
77
|
-
),
|
|
78
|
-
Match.orElseAbsurd,
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
71
|
+
return Rec.map(mutators, (v, name) => {
|
|
72
|
+
return Match.value([v, schema[name]]).pipe(
|
|
73
|
+
Match.when([Predicate.isFunction, Schema.isSchema], ([mutator, schema]) => makeMutator(schema, mutator)),
|
|
74
|
+
Match.when([Predicate.isRecord, Predicate.isRecord], ([mutator, schema]) =>
|
|
75
|
+
Rec.map(mutator, (mutator, name) => makeMutator(Option.getOrThrow(Rec.get(schema, name)), mutator)),
|
|
76
|
+
),
|
|
77
|
+
Match.orElseAbsurd,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
82
80
|
}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { HumanReadable, ReadonlyJSONValue, Zero, Query as ZeroQuery, Schema as ZeroSchema } from "@rocicorp/zero";
|
|
2
|
+
import type { QueryResult as ZeroQueryResult } from "@rocicorp/zero/react";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Equal from "effect/Equal";
|
|
5
|
+
import * as Hash from "effect/Hash";
|
|
6
|
+
import * as Match from "effect/Match";
|
|
7
|
+
import * as Schema from "effect/Schema";
|
|
8
|
+
import * as Stream from "effect/Stream";
|
|
9
|
+
import * as Subscribable from "effect/Subscribable";
|
|
10
|
+
import * as SubscriptionRef from "effect/SubscriptionRef";
|
|
11
|
+
import { prefixId } from "./internal/utils.js";
|
|
12
|
+
import { deepClone, getDefaultSnapshot, getSnapshot } from "./snapshot.js";
|
|
13
|
+
|
|
14
|
+
export type Query<S extends ZeroSchema, T extends keyof S["tables"] & string, R> = ZeroQuery<S, T, R> & Equal.Equal;
|
|
15
|
+
|
|
16
|
+
type QueryArgs<
|
|
17
|
+
A extends ReadonlyJSONValue[],
|
|
18
|
+
B extends unknown[],
|
|
19
|
+
> = // biome-ignore lint/suspicious/noExplicitAny: handles "any query" case
|
|
20
|
+
| { _tag: "Encoded"; args: any extends A ? any : readonly [...A] }
|
|
21
|
+
// biome-ignore lint/suspicious/noExplicitAny: handles "any query" case
|
|
22
|
+
| { _tag: "Decoded"; args: any extends B ? any : readonly [...B] };
|
|
23
|
+
|
|
24
|
+
export const RunQuerySymbol = Symbol.for(prefixId("RunQuery"));
|
|
25
|
+
export const QueryNameSymbol = Symbol.for(prefixId("QueryName"));
|
|
26
|
+
|
|
27
|
+
export const make = <
|
|
28
|
+
N extends string,
|
|
29
|
+
// The encoded format that is sent over the wire, must conform to JSON
|
|
30
|
+
A extends ReadonlyJSONValue[],
|
|
31
|
+
// The decoded format, can be anything
|
|
32
|
+
B extends unknown[],
|
|
33
|
+
S extends ZeroSchema,
|
|
34
|
+
T extends keyof S["tables"] & string,
|
|
35
|
+
R,
|
|
36
|
+
E,
|
|
37
|
+
R1,
|
|
38
|
+
R2,
|
|
39
|
+
>(options: {
|
|
40
|
+
name: N;
|
|
41
|
+
payload: Schema.Schema<readonly [...B], readonly [...A], R1>;
|
|
42
|
+
query: (...args: NoInfer<B>) => Effect.Effect<ZeroQuery<S, T, R>, E, R2>;
|
|
43
|
+
}) => {
|
|
44
|
+
const runQuery = Effect.fn(function* (args: QueryArgs<A, B>) {
|
|
45
|
+
const { encoded, decoded } = yield* Match.valueTags(args, {
|
|
46
|
+
Encoded: ({ args: encoded }) =>
|
|
47
|
+
Effect.map(Schema.decode(options.payload)(encoded), (decoded) => ({ encoded, decoded })),
|
|
48
|
+
Decoded: ({ args: decoded }) =>
|
|
49
|
+
Effect.map(Schema.encode(options.payload)(decoded), (encoded) => ({ encoded, decoded })),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return yield* options.query(...decoded).pipe(
|
|
53
|
+
Effect.map((rawQuery) => {
|
|
54
|
+
const query = rawQuery.nameAndArgs(
|
|
55
|
+
options.name,
|
|
56
|
+
// We pass the encoded `args` to the `nameAndArgs` method, as that is the wire format which
|
|
57
|
+
// is sent to the server.
|
|
58
|
+
encoded,
|
|
59
|
+
) as Query<S, T, R>;
|
|
60
|
+
query[Hash.symbol] = function () {
|
|
61
|
+
return Hash.hash(this.hash());
|
|
62
|
+
};
|
|
63
|
+
query[Equal.symbol] = function (that) {
|
|
64
|
+
if (Hash.isHash(that)) {
|
|
65
|
+
return Equal.equals(this[Hash.symbol](), that[Hash.symbol]());
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
return query;
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return Object.assign((...args: B) => runQuery({ _tag: "Decoded", args }), {
|
|
75
|
+
[QueryNameSymbol]: options.name,
|
|
76
|
+
[RunQuerySymbol]: runQuery,
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: accept any query
|
|
81
|
+
export type MakeQueryResult<E = any, R1 = any, R2 = any> = ReturnType<
|
|
82
|
+
// biome-ignore lint/suspicious/noExplicitAny: accept any query
|
|
83
|
+
typeof make<string, any, any, ZeroSchema, string, Record<string, any> | undefined, E, R1, R2>
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
export const subscribe = Effect.fn(function* <S extends ZeroSchema, T extends keyof S["tables"] & string, R>(
|
|
87
|
+
zero: Zero<S>,
|
|
88
|
+
query: ZeroQuery<S, T, R> | Query<S, T, R>,
|
|
89
|
+
) {
|
|
90
|
+
const view = yield* Effect.acquireRelease(
|
|
91
|
+
Effect.sync(() => zero.materialize(query)),
|
|
92
|
+
(view) => Effect.sync(() => view.destroy()),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const subscriptionRef = yield* SubscriptionRef.make<ZeroQueryResult<R>>(getDefaultSnapshot(query.format.singular));
|
|
96
|
+
|
|
97
|
+
yield* Stream.asyncEffect<Parameters<Parameters<(typeof view)["addListener"]>[0]>>((emit) =>
|
|
98
|
+
Effect.sync(() => view.addListener((...args) => emit.single(args))),
|
|
99
|
+
).pipe(
|
|
100
|
+
Stream.mapEffect(([data, resultType]) =>
|
|
101
|
+
Effect.sync(() => {
|
|
102
|
+
// logic here borrowed from: https://github.com/rocicorp/mono/blob/288b00ec94f5a9ae6e988513423af25c281dbb2a/packages/zero-react/src/use-query.tsx#L295
|
|
103
|
+
const cloned = (data === undefined ? data : deepClone(data as ReadonlyJSONValue)) as HumanReadable<R>;
|
|
104
|
+
return getSnapshot<R>(query.format.singular, cloned, resultType);
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
Stream.runForEach((snapshot) => SubscriptionRef.set(subscriptionRef, snapshot)),
|
|
108
|
+
Effect.forkScoped,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return subscriptionRef.pipe(
|
|
112
|
+
Subscribable.map(([data, { type: status }]) => ({
|
|
113
|
+
data,
|
|
114
|
+
status,
|
|
115
|
+
})),
|
|
116
|
+
);
|
|
117
|
+
});
|