effect-zero 0.3.3 → 0.4.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.
Files changed (50) hide show
  1. package/README.md +23 -16
  2. package/dist/client.d.ts +1 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +1 -1
  5. package/dist/client.js.map +1 -1
  6. package/dist/internal/query-result.d.ts +2 -1
  7. package/dist/internal/query-result.d.ts.map +1 -1
  8. package/dist/internal/query-result.js.map +1 -1
  9. package/dist/query.d.ts +12 -13
  10. package/dist/query.d.ts.map +1 -1
  11. package/dist/query.js +15 -13
  12. package/dist/query.js.map +1 -1
  13. package/dist/query.test.js +4 -2
  14. package/dist/query.test.js.map +1 -1
  15. package/dist/result.d.ts +1 -1
  16. package/dist/result.d.ts.map +1 -1
  17. package/dist/server.d.ts +35 -14
  18. package/dist/server.d.ts.map +1 -1
  19. package/dist/server.js +8 -8
  20. package/dist/server.js.map +1 -1
  21. package/dist/snapshot.d.ts +3 -3
  22. package/dist/snapshot.d.ts.map +1 -1
  23. package/dist/snapshot.js +66 -8
  24. package/dist/snapshot.js.map +1 -1
  25. package/dist/types/error.d.ts +59 -0
  26. package/dist/types/error.d.ts.map +1 -0
  27. package/dist/types/error.js +58 -0
  28. package/dist/types/error.js.map +1 -0
  29. package/dist/types/push.d.ts +53 -1
  30. package/dist/types/push.d.ts.map +1 -1
  31. package/dist/types/push.js +63 -10
  32. package/dist/types/push.js.map +1 -1
  33. package/dist/types/queries.d.ts +28 -9
  34. package/dist/types/queries.d.ts.map +1 -1
  35. package/dist/types/queries.js +14 -15
  36. package/dist/types/queries.js.map +1 -1
  37. package/dist/types.test.d.ts +2 -0
  38. package/dist/types.test.d.ts.map +1 -0
  39. package/dist/types.test.js +268 -0
  40. package/dist/types.test.js.map +1 -0
  41. package/package.json +32 -26
  42. package/src/client.ts +3 -3
  43. package/src/internal/query-result.ts +2 -5
  44. package/src/query.ts +36 -34
  45. package/src/result.ts +1 -1
  46. package/src/server.ts +16 -14
  47. package/src/snapshot.ts +71 -12
  48. package/src/types/error.ts +71 -0
  49. package/src/types/push.ts +69 -10
  50. package/src/types/queries.ts +16 -16
package/src/query.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { HumanReadable, ReadonlyJSONValue, Zero, Query as ZeroQuery, Schema as ZeroSchema } from "@rocicorp/zero";
2
+ import { asQueryInternals } from "@rocicorp/zero/bindings";
2
3
  import * as Effect from "effect/Effect";
3
4
  import * as Equal from "effect/Equal";
4
5
  import * as Hash from "effect/Hash";
@@ -10,15 +11,8 @@ import * as QueryResult from "./internal/query-result.js";
10
11
  import { prefixId } from "./internal/utils.js";
11
12
  import { deepClone, getDefaultSnapshot, getSnapshot } from "./snapshot.js";
12
13
 
13
- export type Query<S extends ZeroSchema, T extends keyof S["tables"] & string, R> = ZeroQuery<S, T, R> & Equal.Equal;
14
-
15
- type QueryArgs<
16
- A extends ReadonlyJSONValue[],
17
- B extends unknown[],
18
- > = // biome-ignore lint/suspicious/noExplicitAny: handles "any query" case
19
- | { _tag: "Encoded"; args: any extends A ? any : readonly [...A] }
20
- // biome-ignore lint/suspicious/noExplicitAny: handles "any query" case
21
- | { _tag: "Decoded"; args: any extends B ? any : readonly [...B] };
14
+ // biome-ignore lint/suspicious/noConfusingVoidType: necessary to allow Schema.Void
15
+ type QueryArgs<A extends ReadonlyJSONValue | void, B> = { _tag: "Encoded"; args: A } | { _tag: "Decoded"; args: B };
22
16
 
23
17
  export const RunQuerySymbol = Symbol.for(prefixId("RunQuery"));
24
18
  export const QueryNameSymbol = Symbol.for(prefixId("QueryName"));
@@ -26,19 +20,20 @@ export const QueryNameSymbol = Symbol.for(prefixId("QueryName"));
26
20
  export const make = <
27
21
  N extends string,
28
22
  // The encoded format that is sent over the wire, must conform to JSON
29
- A extends ReadonlyJSONValue[],
23
+ // biome-ignore lint/suspicious/noConfusingVoidType: necessary to allow Schema.Void
24
+ A extends ReadonlyJSONValue | void,
30
25
  // The decoded format, can be anything
31
- B extends unknown[],
32
- S extends ZeroSchema,
26
+ B,
33
27
  T extends keyof S["tables"] & string,
28
+ S extends ZeroSchema,
34
29
  R,
35
30
  E,
36
31
  R1,
37
32
  R2,
38
33
  >(options: {
39
34
  name: N;
40
- payload: Schema.Schema<readonly [...B], readonly [...A], R1>;
41
- query: (...args: NoInfer<B>) => Effect.Effect<ZeroQuery<S, T, R>, E, R2>;
35
+ payload: Schema.Schema<B, A, R1>;
36
+ query: (args: NoInfer<B>) => Effect.Effect<ZeroQuery<T, S, R>, E, R2>;
42
37
  }) => {
43
38
  const runQuery = Effect.fn(function* (args: QueryArgs<A, B>) {
44
39
  const { encoded, decoded } = yield* Match.valueTags(args, {
@@ -48,14 +43,14 @@ export const make = <
48
43
  Effect.map(Schema.encode(options.payload)(decoded), (encoded) => ({ encoded, decoded })),
49
44
  });
50
45
 
51
- return yield* options.query(...decoded).pipe(
46
+ return yield* options.query(decoded).pipe(
52
47
  Effect.map((rawQuery) => {
53
- const query = rawQuery.nameAndArgs(
48
+ const query = asQueryInternals(rawQuery).nameAndArgs(
54
49
  options.name,
55
50
  // We pass the encoded `args` to the `nameAndArgs` method, as that is the wire format which
56
51
  // is sent to the server.
57
- encoded,
58
- ) as Query<S, T, R>;
52
+ encoded !== undefined ? [encoded] : [],
53
+ ) as ZeroQuery<T, S, R> & Equal.Equal;
59
54
  // Adapted from https://github.com/rocicorp/mono/blob/17171f975e61f7ec93c61569da7bda1d962ac962/packages/zero-protocol/src/query-hash.ts#L17
60
55
  query[Hash.symbol] = () => Hash.string(`${options.name}:${JSON.stringify(encoded)}`);
61
56
  query[Equal.symbol] = function (that) {
@@ -70,7 +65,7 @@ export const make = <
70
65
  });
71
66
 
72
67
  // We return a callable object for ease of use on the frontend.
73
- return Object.assign((...args: B) => runQuery({ _tag: "Decoded", args }), {
68
+ return Object.assign((args: B) => runQuery({ _tag: "Decoded", args }), {
74
69
  [QueryNameSymbol]: options.name,
75
70
  // Internally, we include the full `runQuery` function for use in library's server logic.
76
71
  [RunQuerySymbol]: runQuery,
@@ -80,13 +75,12 @@ export const make = <
80
75
  // biome-ignore lint/suspicious/noExplicitAny: accept any query
81
76
  export type MakeQueryResult<E = any, R1 = any, R2 = any> = ReturnType<
82
77
  // biome-ignore lint/suspicious/noExplicitAny: accept any query
83
- typeof make<string, any, any, ZeroSchema, string, Record<string, any> | undefined, E, R1, R2>
78
+ typeof make<string, any, any, string, ZeroSchema, Record<string, any> | undefined, E, R1, R2>
84
79
  >;
85
80
 
86
- export const stream = <S extends ZeroSchema, T extends keyof S["tables"] & string, R>(
81
+ export const stream = <T extends keyof S["tables"] & string, S extends ZeroSchema, R>(
87
82
  zero: Zero<S>,
88
- // TODO: Look into why this is needed, instead of just `ZeroQuery<S, T, R>`
89
- query: ZeroQuery<S, T, R> | Query<S, T, R>,
83
+ query: ZeroQuery<T, S, R>,
90
84
  ) =>
91
85
  Effect.gen(function* () {
92
86
  const view = yield* Effect.acquireRelease(
@@ -97,28 +91,36 @@ export const stream = <S extends ZeroSchema, T extends keyof S["tables"] & strin
97
91
  return Stream.asyncEffect<Parameters<Parameters<(typeof view)["addListener"]>[0]>>((emit) =>
98
92
  Effect.sync(() => view.addListener((...args) => emit.single(args))),
99
93
  ).pipe(
100
- Stream.mapEffect(([data, resultType]) =>
94
+ Stream.mapEffect(([data, resultType, error]) =>
101
95
  Effect.sync(() => {
102
- // logic here borrowed from: https://github.com/rocicorp/mono/blob/288b00ec94f5a9ae6e988513423af25c281dbb2a/packages/zero-react/src/use-query.tsx#L295
103
- // TODO: Look into why cast needs to be applied to whole ternary here, unlike source.
104
- const cloned = (data === undefined ? data : deepClone(data as ReadonlyJSONValue)) as HumanReadable<R>;
105
- return getSnapshot<R>(query.format.singular, cloned, resultType);
96
+ // logic here borrowed from: https://github.com/rocicorp/mono/blob/8e0f600fb3a9185facf60cfd4971d260b266690e/packages/zero-react/src/use-query.tsx#L543
97
+ const cloned = data === undefined ? data : (deepClone(data as ReadonlyJSONValue) as HumanReadable<R>);
98
+ return getSnapshot<R>(
99
+ asQueryInternals(query).format.singular,
100
+ cloned,
101
+ resultType,
102
+ // TODO: Look into this. It seems like this is difficult but not impossible to model in the Effect paradigm.
103
+ // Likely need some kind of stateful piece between the publisher and and subscriber, allowing the subscriber to swap out
104
+ // the publisher when this function is called.
105
+ () => Effect.dieMessage("retry not available in `effect-zero`"),
106
+ error,
107
+ );
106
108
  }),
107
109
  ),
108
110
  Stream.map(QueryResult.make),
109
111
  );
110
112
  }).pipe(Stream.unwrapScoped);
111
113
 
112
- export const initialValue = <S extends ZeroSchema, T extends keyof S["tables"] & string, R>(
113
- query: ZeroQuery<S, T, R> | Query<S, T, R>,
114
- ) => QueryResult.make<R>(getDefaultSnapshot(query.format.singular));
114
+ export const initialValue = <T extends keyof S["tables"] & string, S extends ZeroSchema, R>(
115
+ query: ZeroQuery<T, S, R>,
116
+ ) => QueryResult.make<R>(getDefaultSnapshot(asQueryInternals(query).format.singular));
115
117
 
116
- export const subscribable = <S extends ZeroSchema, T extends keyof S["tables"] & string, R>(
118
+ export const subscribable = <T extends keyof S["tables"] & string, S extends ZeroSchema, R>(
117
119
  zero: Zero<S>,
118
- query: ZeroQuery<S, T, R> | Query<S, T, R>,
120
+ query: ZeroQuery<T, S, R>,
119
121
  ) => {
120
122
  return Subscribable.make({
121
- get: Effect.promise(() => zero.run(query as ZeroQuery<S, T, R>, { type: "unknown" })).pipe(
123
+ get: Effect.promise(() => zero.run(query, { type: "unknown" })).pipe(
122
124
  Effect.map((value) => ({ _tag: "Partial", value }) satisfies QueryResult.QueryResult.Partial<R>),
123
125
  ),
124
126
  changes: stream(zero, query),
package/src/result.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Result as AtomResult } from "@effect-atom/atom";
2
- import type { HumanReadable } from "@rocicorp/zero/react";
2
+ import type { HumanReadable } from "@rocicorp/zero";
3
3
  import * as Cause from "effect/Cause";
4
4
  import * as Data from "effect/Data";
5
5
  import * as Fn from "effect/Function";
package/src/server.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ReadonlyJSONValue, Schema as ZeroSchema } from "@rocicorp/zero";
2
- import { handleGetQueriesRequest } from "@rocicorp/zero/server";
2
+ import { handleQueryRequest } from "@rocicorp/zero/server";
3
3
  import * as Arr from "effect/Array";
4
4
  import * as Cause from "effect/Cause";
5
5
  import * as Chunk from "effect/Chunk";
@@ -29,6 +29,10 @@ import type * as ServerTransaction from "./server-transaction.js";
29
29
  import * as Types from "./types/push.js";
30
30
  import type { TransformRequestMessage } from "./types/queries.js";
31
31
 
32
+ // Updated to:
33
+ // https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/push-processor.ts
34
+ // https://github.com/rocicorp/mono/blob/3082c9fa061891067b4bd7dc9fe74f798270d8d7/packages/zero-server/src/process-mutations.ts
35
+
32
36
  type ServerTransactionContext<TSchema extends ZeroSchema, TTransaction> = Omit<
33
37
  ReturnType<typeof ServerTransaction.make<string, TSchema, TTransaction>>,
34
38
  "use"
@@ -54,6 +58,7 @@ export const processPush = Effect.fn(function* <
54
58
  if (mutation.type !== "custom") {
55
59
  return yield* new CustomMutationExpectedError();
56
60
  }
61
+
57
62
  return yield* processMutation<Mutators.ExtractMutatorsRequirements<TMutators>>(mutators, mutation).pipe(
58
63
  Effect.catchAll((e) => processMutationError(transaction, e)),
59
64
  Effect.map((result) =>
@@ -65,6 +70,7 @@ export const processPush = Effect.fn(function* <
65
70
  clientGroupID: request.clientGroupID,
66
71
  upstreamSchema: params.schema,
67
72
  }),
73
+ Effect.provide(ServerSynchronizationContext.Default),
68
74
  );
69
75
  }),
70
76
  ),
@@ -76,7 +82,7 @@ export const processPush = Effect.fn(function* <
76
82
  );
77
83
 
78
84
  return { mutations: Chunk.toArray(responses) } satisfies Types.PushResponse;
79
- }, Effect.provide(ServerSynchronizationContext.Default));
85
+ });
80
86
 
81
87
  const processMutation = Effect.fn(function* <R>(mutators: Mutators.AnyMutators<R>, mutation: Types.Mutation) {
82
88
  // Support both "namespace|name" and "namespace.name" formats, and single-segment names.
@@ -179,7 +185,7 @@ class MutationUserError extends Data.TaggedError("MutationUserError")<{
179
185
  }
180
186
  }
181
187
 
182
- export const handleGetQueries = Effect.fn(function* <E, R1, R2>(
188
+ export const handleQuery = Effect.fn(function* <E, R1, R2>(
183
189
  queries: MakeQueryResult<E, R1, R2>[],
184
190
  schema: ZeroSchema,
185
191
  payload: TransformRequestMessage,
@@ -187,19 +193,15 @@ export const handleGetQueries = Effect.fn(function* <E, R1, R2>(
187
193
  const runtime = yield* Effect.runtime<R1 | R2>();
188
194
  return yield* Effect.tryPromise({
189
195
  try: () =>
190
- handleGetQueriesRequest(
196
+ handleQueryRequest(
191
197
  (name, args) =>
192
198
  Arr.findFirst(queries, (q) => q[QueryNameSymbol] === name).pipe(
193
199
  Effect.catchTag("NoSuchElementException", () => new QueryNotFound({ name })),
194
200
  Effect.flatMap((query) => query[RunQuerySymbol]({ _tag: "Encoded", args })),
195
- Effect.map((query) => ({ query })),
196
- (effect) => Runtime.runPromiseExit(runtime, effect),
197
- (exit) =>
198
- exit.then(
199
- Exit.getOrElse((cause) => {
200
- throw new QueryUserError<E>({ cause });
201
- }),
202
- ),
201
+ (effect) => Runtime.runSyncExit(runtime, effect),
202
+ Exit.getOrElse((cause) => {
203
+ throw new QueryUserError<E>({ cause });
204
+ }),
203
205
  ),
204
206
  schema,
205
207
  payload as ReadonlyJSONValue,
@@ -207,7 +209,7 @@ export const handleGetQueries = Effect.fn(function* <E, R1, R2>(
207
209
  catch: (e) =>
208
210
  Option.liftPredicate(e, QueryUserError.is<E>).pipe(
209
211
  Option.flatMap((e) => Cause.failureOption(e.cause)),
210
- Option.getOrElse(() => new GetQueriesError({ cause: Cause.fail(e) })),
212
+ Option.getOrElse(() => new QueryRequestError({ cause: Cause.fail(e) })),
211
213
  ),
212
214
  });
213
215
  });
@@ -228,4 +230,4 @@ class QueryUserError<E> extends Data.TaggedError("QueryUserError")<{
228
230
  }
229
231
  }
230
232
 
231
- class GetQueriesError extends Data.TaggedError("GetQueriesError")<{ cause: Cause.Cause<unknown> }> {}
233
+ class QueryRequestError extends Data.TaggedError("QueryRequestError")<{ cause: Cause.Cause<unknown> }> {}
package/src/snapshot.ts CHANGED
@@ -1,19 +1,22 @@
1
1
  // Code borrowed from:
2
- // https://github.com/rocicorp/mono/blob/30f209f2946b4cf2cd2dee459849351498f11308/packages/zero-react/src/use-query.tsx
3
- // https://github.com/rocicorp/mono/blob/30f209f2946b4cf2cd2dee459849351498f11308/packages/shared/src/deep-clone.ts#L4
2
+ // https://github.com/rocicorp/mono/blob/8e0f600fb3a9185facf60cfd4971d260b266690e/packages/zero-react/src/use-query.tsx
3
+ // https://github.com/rocicorp/mono/blob/8e0f600fb3a9185facf60cfd4971d260b266690e/packages/shared/src/deep-clone.ts
4
4
 
5
- import type { JSONValue, ReadonlyJSONValue } from "@rocicorp/zero";
6
- import type { HumanReadable, QueryResult } from "@rocicorp/zero/react";
5
+ import type { ErroredQuery, HumanReadable, JSONValue, QueryErrorDetails, ReadonlyJSONValue, ResultType } from "@rocicorp/zero";
6
+ import type { QueryResult } from "@rocicorp/zero/react";
7
7
 
8
8
  const emptyArray: unknown[] = [];
9
9
 
10
10
  const resultTypeUnknown = { type: "unknown" } as const;
11
11
  const resultTypeComplete = { type: "complete" } as const;
12
+ const resultTypeError = {type: 'error'} as const;
12
13
 
13
14
  const emptySnapshotSingularUnknown = [undefined, resultTypeUnknown] as const;
14
15
  const emptySnapshotSingularComplete = [undefined, resultTypeComplete] as const;
16
+ const emptySnapshotSingularErrorUnknown = [undefined, resultTypeError] as const;
15
17
  const emptySnapshotPluralUnknown = [emptyArray, resultTypeUnknown] as const;
16
18
  const emptySnapshotPluralComplete = [emptyArray, resultTypeComplete] as const;
19
+ const emptySnapshotErrorUnknown = [emptyArray, resultTypeError] as const;
17
20
 
18
21
  export function getDefaultSnapshot<TReturn>(singular: boolean): QueryResult<TReturn> {
19
22
  return (singular ? emptySnapshotSingularUnknown : emptySnapshotPluralUnknown) as QueryResult<TReturn>;
@@ -26,21 +29,77 @@ export function getDefaultSnapshot<TReturn>(singular: boolean): QueryResult<TRet
26
29
  export function getSnapshot<TReturn>(
27
30
  singular: boolean,
28
31
  data: HumanReadable<TReturn>,
29
- resultType: string,
32
+ resultType: ResultType,
33
+ retryFn: () => void,
34
+ error?: ErroredQuery,
30
35
  ): QueryResult<TReturn> {
31
36
  if (singular && data === undefined) {
32
- return (resultType === "complete"
33
- ? emptySnapshotSingularComplete
34
- : emptySnapshotSingularUnknown) as unknown as QueryResult<TReturn>;
37
+ switch (resultType) {
38
+ case 'error':
39
+ if (error) {
40
+ return [
41
+ undefined,
42
+ makeError(retryFn, error),
43
+ ] as unknown as QueryResult<TReturn>;
44
+ }
45
+ return emptySnapshotSingularErrorUnknown as unknown as QueryResult<TReturn>;
46
+ case 'complete':
47
+ return emptySnapshotSingularComplete as unknown as QueryResult<TReturn>;
48
+ case 'unknown':
49
+ return emptySnapshotSingularUnknown as unknown as QueryResult<TReturn>;
50
+ }
35
51
  }
36
52
 
37
53
  if (!singular && (data as unknown[]).length === 0) {
38
- return (
39
- resultType === "complete" ? emptySnapshotPluralComplete : emptySnapshotPluralUnknown
40
- ) as QueryResult<TReturn>;
54
+ switch (resultType) {
55
+ case 'error':
56
+ if (error) {
57
+ return [
58
+ emptyArray,
59
+ makeError(retryFn, error),
60
+ ] as unknown as QueryResult<TReturn>;
61
+ }
62
+ return emptySnapshotErrorUnknown as unknown as QueryResult<TReturn>;
63
+ case 'complete':
64
+ return emptySnapshotPluralComplete as unknown as QueryResult<TReturn>;
65
+ case 'unknown':
66
+ return emptySnapshotPluralUnknown as unknown as QueryResult<TReturn>;
67
+ }
41
68
  }
42
69
 
43
- return [data, resultType === "complete" ? resultTypeComplete : resultTypeUnknown];
70
+ switch (resultType) {
71
+ case 'error':
72
+ if (error) {
73
+ return [data, makeError(retryFn, error)];
74
+ }
75
+ return [
76
+ data,
77
+ makeError(retryFn, {
78
+ error: 'app',
79
+ id: 'unknown',
80
+ name: 'unknown',
81
+ message: 'An unknown error occurred',
82
+ }),
83
+ ];
84
+ case 'complete':
85
+ return [data, resultTypeComplete];
86
+ case 'unknown':
87
+ return [data, resultTypeUnknown];
88
+ }
89
+ }
90
+
91
+ function makeError(retry: () => void, error: ErroredQuery): QueryErrorDetails {
92
+ const message = error.message ?? 'An unknown error occurred';
93
+ return {
94
+ type: 'error',
95
+ retry,
96
+ refetch: retry,
97
+ error: {
98
+ type: error.error,
99
+ message,
100
+ ...(error.details ? {details: error.details} : {}),
101
+ },
102
+ };
44
103
  }
45
104
 
46
105
  export function deepClone(value: ReadonlyJSONValue): JSONValue {
@@ -0,0 +1,71 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { JsonSchema } from "./common.js";
3
+
4
+ // The code below was converted to Effect Schema from:
5
+ // https://github.com/rocicorp/mono/blob/8e0f600fb3a9185facf60cfd4971d260b266690e/packages/zero-protocol/src/error.ts
6
+
7
+ const MutationId = Schema.Struct({
8
+ id: Schema.Number,
9
+ clientID: Schema.String,
10
+ });
11
+
12
+ const PushFailedBase = Schema.Struct({
13
+ kind: Schema.Literal("PushFailed"),
14
+ details: Schema.optional(JsonSchema),
15
+ /**
16
+ * The mutationIDs of the mutations that failed to process.
17
+ * This can be a subset of the mutationIDs in the request.
18
+ */
19
+ mutationIDs: Schema.Array(MutationId),
20
+ message: Schema.String,
21
+ });
22
+
23
+ export const PushFailedBody = Schema.Union(
24
+ Schema.Struct({
25
+ ...PushFailedBase.fields,
26
+ origin: Schema.Literal("server"),
27
+ reason: Schema.Literal("database", "parse", "oooMutation", "unsupportedPushVersion", "internal"),
28
+ }),
29
+ Schema.Struct({
30
+ ...PushFailedBase.fields,
31
+ origin: Schema.Literal("zeroCache"),
32
+ reason: Schema.Literal("http"),
33
+ status: Schema.Number,
34
+ bodyPreview: Schema.optional(Schema.String),
35
+ }),
36
+ Schema.Struct({
37
+ ...PushFailedBase.fields,
38
+ origin: Schema.Literal("zeroCache"),
39
+ reason: Schema.Literal("timeout", "parse", "internal"),
40
+ }),
41
+ );
42
+
43
+ const TransformFailedBase = Schema.Struct({
44
+ kind: Schema.Literal("TransformFailed"),
45
+ details: Schema.optional(JsonSchema),
46
+ /**
47
+ * The queryIDs of the queries that failed to transform.
48
+ */
49
+ queryIDs: Schema.Array(Schema.String),
50
+ message: Schema.String,
51
+ });
52
+
53
+ export const TransformFailedBody = Schema.Union(
54
+ Schema.Struct({
55
+ ...TransformFailedBase.fields,
56
+ origin: Schema.Literal("server"),
57
+ reason: Schema.Literal("database", "parse", "internal"),
58
+ }),
59
+ Schema.Struct({
60
+ ...TransformFailedBase.fields,
61
+ origin: Schema.Literal("zeroCache"),
62
+ reason: Schema.Literal("http"),
63
+ status: Schema.Number,
64
+ bodyPreview: Schema.optional(Schema.String),
65
+ }),
66
+ Schema.Struct({
67
+ ...TransformFailedBase.fields,
68
+ origin: Schema.Literal("zeroCache"),
69
+ reason: Schema.Literal("timeout", "parse", "internal"),
70
+ }),
71
+ );
package/src/types/push.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as Schema from "effect/Schema";
2
2
  import { JsonSchema } from "./common.js";
3
+ import { PushFailedBody } from "./error.js";
3
4
 
4
5
  // The code below was converted to Effect Schema from:
5
- // https://github.com/rocicorp/mono/blob/30f209f2946b4cf2cd2dee459849351498f11308/packages/zero-protocol/src/push.ts
6
+ // https://github.com/rocicorp/mono/blob/8e0f600fb3a9185facf60cfd4971d260b266690e/packages/zero-protocol/src/push.ts
6
7
 
7
8
  const PrimaryKey = Schema.Array(Schema.String);
8
9
 
@@ -15,6 +16,32 @@ const Row = Schema.Record({ key: Schema.String, value: Value });
15
16
 
16
17
  const CRUD_MUTATION_NAME = "_zero_crud";
17
18
 
19
+ // biome-ignore lint/correctness/noUnusedVariables: borrowed code
20
+ const CLEANUP_RESULTS_MUTATION_NAME = "_zero_cleanupResults";
21
+
22
+ // biome-ignore lint/correctness/noUnusedVariables: borrowed code
23
+ const CleanupResultsArg = Schema.Union(
24
+ // Legacy format (no type field) - treat as single
25
+ Schema.Struct({
26
+ clientGroupID: Schema.String,
27
+ clientID: Schema.String,
28
+ upToMutationID: Schema.Number,
29
+ }),
30
+ // Explicit single: delete up to a specific mutation ID for one client
31
+ Schema.Struct({
32
+ type: Schema.Literal("single"),
33
+ clientGroupID: Schema.String,
34
+ clientID: Schema.String,
35
+ upToMutationID: Schema.Number,
36
+ }),
37
+ // Bulk: delete all mutations for multiple clients
38
+ Schema.Struct({
39
+ type: Schema.Literal("bulk"),
40
+ clientGroupID: Schema.String,
41
+ clientIDs: Schema.NonEmptyArray(Schema.String),
42
+ }),
43
+ );
44
+
18
45
  /**
19
46
  * Inserts if entity with id does not already exist.
20
47
  */
@@ -97,6 +124,11 @@ export const PushBody = Schema.Struct({
97
124
  schemaVersion: Schema.optional(Schema.Number),
98
125
  timestamp: Schema.Number,
99
126
  requestID: Schema.String,
127
+ /**
128
+ * @deprecated auth is managed at client-group scope via connect/updateAuth
129
+ * and should not be included in push messages.
130
+ */
131
+ auth: Schema.optional(Schema.String),
100
132
  });
101
133
  export type PushBody = typeof PushBody.Type;
102
134
 
@@ -111,13 +143,18 @@ const MutationId = Schema.Struct({
111
143
  export const AppError = Schema.Struct({
112
144
  error: Schema.Literal("app"),
113
145
  // The user can return any additional data here
146
+ message: Schema.optional(Schema.String),
114
147
  details: Schema.optional(JsonSchema),
115
148
  });
116
149
 
117
150
  export type AppError = typeof AppError.Type;
118
151
 
119
152
  export const ZeroError = Schema.Struct({
120
- error: Schema.Union(Schema.Literal("oooMutation"), Schema.Literal("alreadyProcessed")),
153
+ error: Schema.Union(
154
+ /** @deprecated push oooMutation errors are now represented as ['error', { ... }] messages */
155
+ Schema.Literal("oooMutation"),
156
+ Schema.Literal("alreadyProcessed"),
157
+ ),
121
158
  details: Schema.optional(JsonSchema),
122
159
  });
123
160
 
@@ -147,42 +184,64 @@ const PushOk = Schema.Struct({
147
184
  mutations: Schema.Array(MutationResponse),
148
185
  });
149
186
 
187
+ /**
188
+ * @deprecated push errors are now represented as ['error', { ... }] messages
189
+ */
150
190
  const UnsupportedPushVersion = Schema.Struct({
191
+ /** @deprecated */
151
192
  error: Schema.Literal("unsupportedPushVersion"),
152
- // optional for backwards compatibility
153
- // This field is included so the client knows which mutations
154
- // were not processed by the server.
193
+ /** @deprecated */
155
194
  mutationIDs: Schema.optional(Schema.Array(MutationId)),
156
195
  });
157
196
 
197
+ /**
198
+ * @deprecated push errors are now represented as ['error', { ... }] messages
199
+ */
158
200
  const UnsupportedSchemaVersion = Schema.Struct({
201
+ /** @deprecated */
159
202
  error: Schema.Literal("unsupportedSchemaVersion"),
160
- // optional for backwards compatibility
161
- // This field is included so the client knows which mutations
162
- // were not processed by the server.
203
+ /** @deprecated */
163
204
  mutationIDs: Schema.optional(Schema.Array(MutationId)),
164
205
  });
165
206
 
207
+ /**
208
+ * @deprecated push http errors are now represented as ['error', { ... }] messages
209
+ */
166
210
  const HttpError = Schema.Struct({
211
+ /** @deprecated */
167
212
  error: Schema.Literal("http"),
213
+ /** @deprecated */
168
214
  status: Schema.Number,
215
+ /** @deprecated */
169
216
  details: Schema.String,
217
+ /** @deprecated */
170
218
  mutationIDs: Schema.optional(Schema.Array(MutationId)),
171
219
  });
172
220
 
221
+ /**
222
+ * @deprecated push zero errors are now represented as ['error', { ... }] messages
223
+ */
173
224
  const ZeroPusherError = Schema.Struct({
225
+ /** @deprecated */
174
226
  error: Schema.Literal("zeroPusher"),
227
+ /** @deprecated */
175
228
  details: Schema.String,
229
+ /** @deprecated */
176
230
  mutationIDs: Schema.optional(Schema.Array(MutationId)),
177
231
  });
178
232
 
233
+ /**
234
+ * @deprecated push errors are now represented as ['error', { ... }] messages
235
+ */
179
236
  const PushError = Schema.Union(UnsupportedPushVersion, UnsupportedSchemaVersion, HttpError, ZeroPusherError);
180
237
 
181
- export const PushResponse = Schema.Union(PushOk, PushError);
238
+ const PushResponseBody = Schema.Union(PushOk, PushError);
239
+
240
+ export const PushResponse = Schema.Union(PushResponseBody, PushFailedBody);
182
241
  export type PushResponse = typeof PushResponse.Type;
183
242
 
184
243
  // biome-ignore lint/correctness/noUnusedVariables: borrowed code
185
- const PushResponseMessage = Schema.Tuple(Schema.Literal("pushResponse"), PushResponse);
244
+ const PushResponseMessage = Schema.Tuple(Schema.Literal("pushResponse"), PushResponseBody);
186
245
 
187
246
  // biome-ignore lint/correctness/noUnusedVariables: borrowed code
188
247
  const AckMutationResponsesMessage = Schema.Tuple(Schema.Literal("ackMutationResponses"), MutationId);
@@ -1,8 +1,9 @@
1
1
  import * as Schema from "effect/Schema";
2
2
  import { JsonSchema } from "./common.js";
3
+ import { TransformFailedBody } from "./error.js";
3
4
 
4
5
  // The code below was converted to Effect Schema from:
5
- // https://github.com/rocicorp/mono/blob/9fe2b3cb770bad269ecaf12296c64a0739c23e4e/packages/zero-protocol/src/custom-queries.ts#L14
6
+ // https://github.com/rocicorp/mono/blob/8e0f600fb3a9185facf60cfd4971d260b266690e/packages/zero-protocol/src/custom-queries.ts
6
7
 
7
8
  /** TODO: Defined as unknown for now */
8
9
  const Ast = Schema.Unknown;
@@ -21,29 +22,24 @@ const TransformedQuery = Schema.Struct({
21
22
  ast: Ast,
22
23
  });
23
24
 
24
- const AppQueryError = Schema.Struct({
25
+ const AppErroredQuery = Schema.Struct({
25
26
  error: Schema.Literal("app"),
26
27
  id: Schema.String,
27
28
  name: Schema.String,
28
- details: JsonSchema,
29
+ // optional for backwards compatibility
30
+ message: Schema.optional(Schema.String),
31
+ details: Schema.optional(JsonSchema),
29
32
  });
30
33
 
31
- const ErrorSchema = Schema.Struct({
32
- error: Schema.Literal("zero"),
34
+ const ParseErroredQuery = Schema.Struct({
35
+ error: Schema.Literal("parse"),
33
36
  id: Schema.String,
34
37
  name: Schema.String,
35
- details: JsonSchema,
38
+ message: Schema.String,
39
+ details: Schema.optional(JsonSchema),
36
40
  });
37
41
 
38
- const HttpQueryError = Schema.Struct({
39
- error: Schema.Literal("http"),
40
- id: Schema.String,
41
- name: Schema.String,
42
- status: Schema.Number,
43
- details: JsonSchema,
44
- });
45
-
46
- const ErroredQuery = Schema.Union(AppQueryError, HttpQueryError, ErrorSchema);
42
+ const ErroredQuery = Schema.Union(AppErroredQuery, ParseErroredQuery);
47
43
 
48
44
  const TransformResponseBody = Schema.Array(Schema.Union(TransformedQuery, ErroredQuery));
49
45
 
@@ -53,4 +49,8 @@ export type TransformRequestMessage = typeof TransformRequestMessage.Type;
53
49
  // biome-ignore lint/correctness/noUnusedVariables: borrowed code
54
50
  const TransformErrorMessage = Schema.Tuple(Schema.Literal("transformError"), Schema.Array(ErroredQuery));
55
51
 
56
- export const TransformResponseMessage = Schema.Tuple(Schema.Literal("transformed"), TransformResponseBody);
52
+ const TransformFailedMessage = Schema.Tuple(Schema.Literal("transformFailed"), TransformFailedBody);
53
+
54
+ const TransformOkMessage = Schema.Tuple(Schema.Literal("transformed"), TransformResponseBody);
55
+
56
+ export const TransformResponseMessage = Schema.Union(TransformOkMessage, TransformFailedMessage);