effect-zero 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +138 -95
  2. package/dist/client-transaction.d.ts +22 -0
  3. package/dist/client-transaction.d.ts.map +1 -0
  4. package/dist/client-transaction.js +15 -0
  5. package/dist/client-transaction.js.map +1 -0
  6. package/dist/client.d.ts +10 -38
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +23 -50
  9. package/dist/client.js.map +1 -1
  10. package/dist/internal/server.d.ts +54 -0
  11. package/dist/internal/server.d.ts.map +1 -0
  12. package/dist/internal/server.js +64 -0
  13. package/dist/internal/server.js.map +1 -0
  14. package/dist/internal/utils.d.ts.map +1 -0
  15. package/dist/internal/utils.js.map +1 -0
  16. package/dist/mutators.d.ts +18 -23
  17. package/dist/mutators.d.ts.map +1 -1
  18. package/dist/mutators.js +9 -18
  19. package/dist/mutators.js.map +1 -1
  20. package/dist/query.d.ts +30 -0
  21. package/dist/query.d.ts.map +1 -0
  22. package/dist/query.js +54 -0
  23. package/dist/query.js.map +1 -0
  24. package/dist/server-transaction.d.ts +42 -0
  25. package/dist/server-transaction.d.ts.map +1 -0
  26. package/dist/server-transaction.js +88 -0
  27. package/dist/server-transaction.js.map +1 -0
  28. package/dist/server.d.ts +111 -168
  29. package/dist/server.d.ts.map +1 -1
  30. package/dist/server.js +80 -201
  31. package/dist/server.js.map +1 -1
  32. package/dist/snapshot.d.ts.map +1 -1
  33. package/dist/snapshot.js +3 -1
  34. package/dist/snapshot.js.map +1 -1
  35. package/dist/types/common.d.ts +4 -0
  36. package/dist/types/common.d.ts.map +1 -0
  37. package/dist/types/common.js +4 -0
  38. package/dist/types/common.js.map +1 -0
  39. package/dist/{types.d.ts → types/push.d.ts} +15 -15
  40. package/dist/types/push.d.ts.map +1 -0
  41. package/dist/{types.js → types/push.js} +54 -54
  42. package/dist/types/push.js.map +1 -0
  43. package/dist/types/queries.d.ts +29 -0
  44. package/dist/types/queries.d.ts.map +1 -0
  45. package/dist/types/queries.js +42 -0
  46. package/dist/types/queries.js.map +1 -0
  47. package/package.json +46 -21
  48. package/src/client-transaction.ts +30 -0
  49. package/src/client.ts +66 -116
  50. package/src/internal/server.ts +90 -0
  51. package/src/mutators.ts +42 -44
  52. package/src/query.ts +117 -0
  53. package/src/server-transaction.ts +141 -0
  54. package/src/server.ts +174 -320
  55. package/src/snapshot.ts +0 -2
  56. package/src/types/common.ts +4 -0
  57. package/src/types/push.ts +197 -0
  58. package/src/types/queries.ts +56 -0
  59. package/dist/types.d.ts.map +0 -1
  60. package/dist/types.js.map +0 -1
  61. package/dist/utils.d.ts.map +0 -1
  62. package/dist/utils.js.map +0 -1
  63. package/src/types.ts +0 -201
  64. /package/dist/{utils.d.ts → internal/utils.d.ts} +0 -0
  65. /package/dist/{utils.js → internal/utils.js} +0 -0
  66. /package/src/{utils.ts → internal/utils.ts} +0 -0
package/src/client.ts CHANGED
@@ -1,8 +1,5 @@
1
- import { Atom } from "@effect-atom/atom";
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 Stream from "effect/Stream";
16
- import * as Subscribable from "effect/Subscribable";
17
- import * as SubscriptionRef from "effect/SubscriptionRef";
18
- import {
19
- type AnyMutator,
20
- type AnyMutators,
21
- type ExtractMutatorsRequirements,
22
- MutatorArgsSchemaSym,
23
- } from "./mutators.js";
24
- import { deepClone, getDefaultSnapshot, getSnapshot } from "./snapshot.js";
25
- import { prefixId } from "./utils.js";
26
-
27
- // Updated to: https://github.com/rocicorp/mono/blob/2e18f2e1d084c530ebd9bd7fef9bb848e607cc19/packages/zero-pg/src/push-processor.ts
28
-
29
- // Necessary workaround for TS declaration generation
30
- export interface ZeroClientTransaction {
31
- readonly _tag: unique symbol;
32
- }
33
-
34
- export const makeClient = <S extends ZeroSchema>() => {
35
- // TODO: Maybe prefix this / suffix this with a tag passed in to `make`?
36
- const ZeroClientTransaction = Context.Tag(prefixId("ZeroClientTransaction"))<ZeroClientTransaction, Transaction<S>>();
37
-
38
- const use = <A>(fn: (transaction: Transaction<S>, options: { readonly signal: AbortSignal }) => PromiseLike<A>) =>
39
- Effect.flatMap(ZeroClientTransaction, (transaction) =>
40
- Effect.promise((signal) => fn(transaction, { signal })),
41
- ).pipe(
42
- Effect.tapErrorCause((cause) => Effect.logError(cause)),
43
- Effect.sandbox,
44
- Effect.annotateLogs({ module: prefixId("ZeroClient.Transaction"), method: "use" }),
45
- Effect.mapError((cause) => new ZeroClientTransactionError({ cause })),
46
- );
47
-
48
- const unwrapMutators = Effect.fn(function* <T extends AnyMutators>(mutators: T) {
49
- const runtime = yield* Effect.runtime<Exclude<ExtractMutatorsRequirements<T>, ZeroClientTransaction>>();
50
-
51
- function unwrapMutator<E>(mutator: AnyMutator<ExtractMutatorsRequirements<T>, E>) {
52
- return async (tx: Transaction<S>, args: unknown) => {
53
- const exit = await Schema.decode(mutator[MutatorArgsSchemaSym])(args).pipe(
54
- Effect.catchTag("ParseError", (e) => new ZeroClientArgsParseError({ cause: Cause.fail(e) })),
55
- Effect.flatMap(mutator),
56
- Effect.provideService(ZeroClientTransaction, tx),
57
- Runtime.runPromiseExit(runtime),
58
- );
59
- return Exit.getOrElse(exit, (c) => {
60
- // Extract underlying error bypassing FiberFailure
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
- return {
107
- unwrapMutators,
108
- querySub,
109
- queryAtom,
110
- Transaction: Object.assign(ZeroClientTransaction, { use }),
111
- };
112
- };
113
-
114
- type UnwrapMutator<S extends ZeroSchema, T extends AnyMutator> = Parameters<T> extends []
115
- ? (transaction: Transaction<S>) => Promise<void>
116
- : (transaction: Transaction<S>, args: Schema.Schema.Encoded<T[typeof MutatorArgsSchemaSym]>) => Promise<void>;
117
-
118
- export type UnwrapMutators<S extends ZeroSchema, T extends AnyMutators> = {
119
- [A in keyof T]: T[A] extends AnyMutator
120
- ? UnwrapMutator<S, T[A]>
121
- : { [B in keyof T[A]]: T[A][B] extends AnyMutator ? UnwrapMutator<S, T[A][B]> : never };
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 ZeroClientTransactionError extends Data.TaggedError("ZeroClientTransactionError")<{
125
- cause: Cause.Cause<unknown>;
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 AnyMutatorDef<R = any, TArgs extends any[] = any[]> = (...args: TArgs) => Effect.Effect<any, any, R>;
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 AnyMutatorDefs<R = any> = {
14
- [K: string]: AnyMutatorDef<R> | { [K: string]: AnyMutatorDef<R> };
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 AnyMutatorSchemaArgs = Schema.Schema<any, any, never>;
19
- export type AnyMutatorSchema = Record<string, AnyMutatorSchemaArgs | Record<string, AnyMutatorSchemaArgs>>;
18
+ export type AnyMutatorSchema = Schema.Schema<any, any, never>;
19
+ export type AnyMutatorSchemas = Record<string, AnyMutatorSchema | Record<string, AnyMutatorSchema>>;
20
20
 
21
- export type MutatorDefs<TSchema extends AnyMutatorSchema> = {
22
- [K in keyof TSchema]: TSchema[K] extends AnyMutatorSchemaArgs
23
- ? AnyMutatorDef<unknown, [Schema.Schema.Type<TSchema[K]>]>
24
- : TSchema[K] extends infer TSchema extends Record<string, AnyMutatorSchemaArgs>
25
- ? { [K in keyof TSchema]: AnyMutatorDef<unknown, [Schema.Schema.Type<TSchema[K]>]> }
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 MutatorArgsSchemaSym = Symbol.for(prefixId("MutatorArgsSchema"));
29
+ export const MutatorSchemaSymbol = Symbol.for(prefixId("MutatorSchema"));
30
30
 
31
31
  export type Mutator<
32
- TFunc extends AnyMutatorDef = AnyMutatorDef,
33
- TSchema extends AnyMutatorSchemaArgs = AnyMutatorSchemaArgs,
32
+ TFunc extends AnyMutatorFn = AnyMutatorFn,
33
+ TSchema extends AnyMutatorSchema = AnyMutatorSchema,
34
34
  > = TFunc & {
35
- [MutatorArgsSchemaSym]: TSchema;
35
+ [MutatorSchemaSymbol]: TSchema;
36
36
  };
37
37
 
38
- export type Mutators<TSchema extends AnyMutatorSchema, TDefs extends AnyMutatorDefs> = {
39
- [K in keyof TDefs & keyof TSchema]: TDefs[K] extends AnyMutatorDef
40
- ? TSchema[K] extends AnyMutatorSchemaArgs
41
- ? Mutator<TDefs[K], TSchema[K]>
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, AnyMutatorSchemaArgs>
44
- ? TDefs[K] extends infer TDefs extends Record<string, AnyMutatorDef>
45
- ? { [K in keyof TDefs & keyof TSchema]: Mutator<TDefs[K], TSchema[K]> }
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<T extends AnyMutators> = T extends AnyMutators<infer R> ? R : never;
56
+ export type ExtractMutatorsRequirements<TMutators extends AnyMutators> = TMutators extends AnyMutators<infer R>
57
+ ? R
58
+ : never;
57
59
 
58
- export class MutatorSchema<T extends AnyMutatorSchema> {
59
- private constructor(public schema: T) {}
60
+ export const schema = <TSchema extends AnyMutatorSchemas>(schema: TSchema) => schema;
60
61
 
61
- static make<T extends AnyMutatorSchema>(schema: T) {
62
- return new MutatorSchema(schema);
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
- // Associates mutator with its args schema
66
- private makeMutator(argsSchema: AnyMutatorSchemaArgs, fn: AnyMutatorDef): AnyMutator {
67
- return Object.assign(fn, { [MutatorArgsSchemaSym]: argsSchema });
68
- }
69
-
70
- makeMutators<TDefs extends MutatorDefs<T>>(mutators: TDefs): Mutators<T, TDefs>;
71
- makeMutators(mutators: AnyMutatorDefs): AnyMutators {
72
- return Rec.map(mutators, (v, name) => {
73
- return Match.value([v, this.schema[name]]).pipe(
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
+ });