@shivaedev/effect-contract 0.4.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-contract",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Typed query and command contracts over native Effect RPC and reactivity",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-contract#readme",
6
6
  "bugs": {
@@ -23,13 +23,55 @@
23
23
  }
24
24
  },
25
25
  "exports": {
26
- "./*.ts": {
27
- "types": "./dist/*.d.ts",
28
- "source": "./src/*.ts",
29
- "import": "./dist/*.js",
30
- "default": "./dist/*.js"
26
+ "./package.json": "./package.json",
27
+ "./bind.ts": {
28
+ "types": "./dist/bind.d.ts",
29
+ "source": "./src/bind.ts",
30
+ "import": "./dist/bind.js",
31
+ "default": "./dist/bind.js"
32
+ },
33
+ "./contract.ts": {
34
+ "types": "./dist/contract.d.ts",
35
+ "source": "./src/contract.ts",
36
+ "import": "./dist/contract.js",
37
+ "default": "./dist/contract.js"
38
+ },
39
+ "./keys.ts": {
40
+ "types": "./dist/keys.d.ts",
41
+ "source": "./src/keys.ts",
42
+ "import": "./dist/keys.js",
43
+ "default": "./dist/keys.js"
31
44
  },
32
- "./package.json": "./package.json"
45
+ "./live.ts": {
46
+ "types": "./dist/live.d.ts",
47
+ "source": "./src/live.ts",
48
+ "import": "./dist/live.js",
49
+ "default": "./dist/live.js"
50
+ },
51
+ "./makeLiveServer.ts": {
52
+ "types": "./dist/makeLiveServer.d.ts",
53
+ "source": "./src/makeLiveServer.ts",
54
+ "import": "./dist/makeLiveServer.js",
55
+ "default": "./dist/makeLiveServer.js"
56
+ },
57
+ "./operation.ts": {
58
+ "types": "./dist/operation.d.ts",
59
+ "source": "./src/operation.ts",
60
+ "import": "./dist/operation.js",
61
+ "default": "./dist/operation.js"
62
+ },
63
+ "./rejection.ts": {
64
+ "types": "./dist/rejection.d.ts",
65
+ "source": "./src/rejection.ts",
66
+ "import": "./dist/rejection.js",
67
+ "default": "./dist/rejection.js"
68
+ },
69
+ "./resume.ts": {
70
+ "types": "./dist/resume.d.ts",
71
+ "source": "./src/resume.ts",
72
+ "import": "./dist/resume.js",
73
+ "default": "./dist/resume.js"
74
+ }
33
75
  },
34
76
  "files": [
35
77
  "dist",
@@ -37,6 +79,7 @@
37
79
  "!src/**/*.test.ts",
38
80
  "!src/**/*.spec.ts",
39
81
  "!src/**/test-support",
82
+ "docs",
40
83
  "CHANGELOG.md",
41
84
  "README.md"
42
85
  ],
@@ -46,13 +89,13 @@
46
89
  "devDependencies": {
47
90
  "@effect/platform-node": "4.0.0-rc.112",
48
91
  "@effect/vitest": "4.0.0-rc.112",
49
- "@shivaedev/effect-test": "0.3.0",
50
- "@shivaedev/quality": "0.15.0",
51
- "@types/node": "24.10.1",
92
+ "@shivaedev/effect-test": "0.3.2",
93
+ "@shivaedev/quality": "0.19.0",
94
+ "@types/node": "24.19.1",
52
95
  "@typescript/native": "npm:typescript@7.0.2",
53
96
  "effect": "4.0.0-rc.112",
54
- "esbuild": "0.28.1",
55
- "happy-dom": "20.11.2",
97
+ "esbuild": "0.28.2",
98
+ "happy-dom": "20.14.5",
56
99
  "typescript": "npm:@typescript/typescript6@6.0.2",
57
100
  "vitest": "4.1.11"
58
101
  },
@@ -67,7 +110,7 @@
67
110
  "provenance": true
68
111
  },
69
112
  "scripts": {
70
- "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc6 --project tsconfig.build.json",
113
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
71
114
  "ready": "pnpm typecheck && pnpm test && pnpm build",
72
115
  "test": "vitest run",
73
116
  "typecheck": "tsc --noEmit"
package/src/bind.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  import type { Duration, Schema } from "effect";
2
- import { Effect } from "effect";
2
+ import { Effect, Stream } from "effect";
3
3
  import type { Headers } from "effect/unstable/http";
4
4
  import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
5
  import type * as Atom from "effect/unstable/reactivity/Atom";
6
6
  import type * as AtomRpc from "effect/unstable/reactivity/AtomRpc";
7
7
  import * as Reactivity from "effect/unstable/reactivity/Reactivity";
8
- import type { Rpc } from "effect/unstable/rpc";
8
+ import type { Rpc, RpcSchema } from "effect/unstable/rpc";
9
9
  import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
10
10
  import type { Contract, Declared, Tag } from "./contract.ts";
11
11
  import { invalidationKeys, type Key, readKeys } from "./keys.ts";
12
- import type { CommandShape, QueryShape } from "./operation.ts";
12
+ import type { CommandShape, QueryShape, StreamShape } from "./operation.ts";
13
13
 
14
14
  export type Failure<R extends Rpc.Any> =
15
- R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer TError, infer TMiddleware, infer _Requires>
16
- ? TError["Type"] | TMiddleware["error"]["Type"] | RpcClientError
15
+ R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer _Error, infer TMiddleware, infer _Requires>
16
+ ? Rpc.ErrorExit<R> | TMiddleware["error"]["Type"] | RpcClientError
17
17
  : never;
18
18
 
19
19
  export type RunFailure<R extends Rpc.Any> =
@@ -36,20 +36,33 @@ export interface BoundCommand<R extends Rpc.Any, TSelf> {
36
36
  readonly run: (payload: Rpc.Payload<R>) => Effect.Effect<Rpc.Success<R>, RunFailure<R>, TSelf | Reactivity.Reactivity>;
37
37
  }
38
38
 
39
+ export interface BoundStream<R extends Rpc.Any, TSelf> {
40
+ readonly run: (payload: Rpc.Payload<R>) => Stream.Stream<Rpc.SuccessChunk<R>, RunFailure<R>, TSelf | Rpc.ServicesClient<R>>;
41
+ }
42
+
39
43
  export type Bound<
40
44
  TName extends string,
41
45
  TQueries extends readonly QueryShape[],
42
46
  TCommands extends readonly CommandShape[],
43
47
  TRpcs extends Rpc.Any,
44
48
  TSelf,
49
+ TStreams extends readonly StreamShape[] = readonly [],
45
50
  > = {
46
51
  readonly [TQuery in TQueries[number] as TQuery["name"]]: BoundQuery<Rpc.ExtractTag<TRpcs, Tag<TName, TQuery["name"]>>, TSelf>;
47
52
  } & {
48
53
  readonly [TCommand in TCommands[number] as TCommand["name"]]: BoundCommand<Rpc.ExtractTag<TRpcs, Tag<TName, TCommand["name"]>>, TSelf>;
54
+ } & {
55
+ readonly [TStream in TStreams[number] as TStream["name"]]: BoundStream<Rpc.ExtractTag<TRpcs, Tag<TName, TStream["name"]>>, TSelf>;
49
56
  };
50
57
 
51
58
  type ErasedClient = AtomRpc.AtomRpcClient<unknown, string, Rpc.Rpc<string, Schema.Top, Schema.Top, Schema.Top>>;
52
59
 
60
+ type ErasedStreamingClient = AtomRpc.AtomRpcClient<
61
+ unknown,
62
+ string,
63
+ Rpc.Rpc<string, Schema.Top, RpcSchema.Stream<Schema.Top, Schema.Top>, Schema.Never>
64
+ >;
65
+
53
66
  type MatchingClient<TRpcs extends Rpc.Any, TClientRpcs extends Rpc.Any> = [TRpcs] extends [Extract<TClientRpcs, { readonly _tag: TRpcs["_tag"] }>]
54
67
  ? [Extract<TClientRpcs, { readonly _tag: TRpcs["_tag"] }>] extends [TRpcs]
55
68
  ? unknown
@@ -77,6 +90,12 @@ function boundCommand(service: ErasedClient, tag: string, command: CommandShape)
77
90
  };
78
91
  }
79
92
 
93
+ function boundStream(service: ErasedStreamingClient, tag: string) {
94
+ return {
95
+ run: (payload: unknown) => Stream.unwrap(service.use((client) => Effect.succeed(client(tag, payload)))),
96
+ };
97
+ }
98
+
80
99
  export function bind<
81
100
  TName extends string,
82
101
  TQueries extends readonly QueryShape[],
@@ -85,17 +104,19 @@ export function bind<
85
104
  TSelf,
86
105
  TId extends string,
87
106
  TClientRpcs extends Rpc.Any,
107
+ TStreams extends readonly StreamShape[],
88
108
  >(
89
- contract: Contract<TName, TQueries, TCommands, TRpcs>,
109
+ contract: Contract<TName, TQueries, TCommands, TRpcs, TStreams>,
90
110
  service: AtomRpc.AtomRpcClient<TSelf, TId, TClientRpcs> & MatchingClient<NoInfer<TRpcs>, TClientRpcs>,
91
- ): Bound<TName, TQueries, TCommands, TRpcs, TSelf>;
111
+ ): Bound<TName, TQueries, TCommands, TRpcs, TSelf, TStreams>;
92
112
  export function bind(
93
- contract: { readonly declaration: Declared<string, readonly QueryShape[], readonly CommandShape[]> },
94
- service: ErasedClient,
113
+ contract: { readonly declaration: Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[]> },
114
+ service: AtomRpc.AtomRpcClient<unknown, string, Rpc.Any>,
95
115
  ): unknown {
96
- const { name, queries, commands } = contract.declaration;
116
+ const { name, queries, commands, streams } = contract.declaration;
97
117
  return Object.fromEntries([
98
118
  ...queries.map((query) => [query.name, boundQuery(service, `${name}.${query.name}`, query)] as const),
99
119
  ...commands.map((command) => [command.name, boundCommand(service, `${name}.${command.name}`, command)] as const),
120
+ ...streams.map((stream) => [stream.name, boundStream(service, `${name}.${stream.name}`)] as const),
100
121
  ]);
101
122
  }
package/src/contract.ts CHANGED
@@ -1,47 +1,57 @@
1
- import { Rpc, RpcGroup, type RpcMiddleware } from "effect/unstable/rpc";
2
- import type { CommandShape, OperationShape, QueryShape } from "./operation.ts";
1
+ import type { Schema } from "effect";
2
+ import { Rpc, RpcGroup, type RpcMiddleware, type RpcSchema } from "effect/unstable/rpc";
3
+ import type { CommandShape, OperationShape, QueryShape, StreamShape } from "./operation.ts";
3
4
 
4
- export type Tag<Contract extends string, Name extends string> = `${Contract}.${Name}`;
5
+ export type Tag<TContract extends string, TName extends string> = `${TContract}.${TName}`;
5
6
 
6
- export type OperationRpc<Contract extends string, Operation> = Operation extends OperationShape
7
- ? Rpc.Rpc<Tag<Contract, Operation["name"]>, Operation["payload"], Operation["success"], Operation["error"]>
7
+ export type OperationRpc<TContract extends string, TOperation> = TOperation extends OperationShape
8
+ ? TOperation extends StreamShape
9
+ ? Rpc.Rpc<Tag<TContract, TOperation["name"]>, TOperation["payload"], RpcSchema.Stream<TOperation["success"], TOperation["error"]>, Schema.Never>
10
+ : Rpc.Rpc<Tag<TContract, TOperation["name"]>, TOperation["payload"], TOperation["success"], TOperation["error"]>
8
11
  : never;
9
12
 
10
- export interface Declared<Name extends string, Queries extends readonly QueryShape[], Commands extends readonly CommandShape[]> {
11
- readonly commands: Commands;
12
- readonly name: Name;
13
- readonly queries: Queries;
13
+ export interface Declared<
14
+ TName extends string,
15
+ TQueries extends readonly QueryShape[],
16
+ TCommands extends readonly CommandShape[],
17
+ TStreams extends readonly StreamShape[] = readonly [],
18
+ > {
19
+ readonly commands: TCommands;
20
+ readonly name: TName;
21
+ readonly queries: TQueries;
22
+ readonly streams: TStreams;
14
23
  }
15
24
 
16
25
  export interface Contract<
17
- Name extends string,
18
- Queries extends readonly QueryShape[],
19
- Commands extends readonly CommandShape[],
20
- Rpcs extends Rpc.Any = OperationRpc<Name, Queries[number] | Commands[number]>,
21
- > extends RpcGroup.RpcGroup<Rpcs> {
22
- readonly declaration: Declared<Name, Queries, Commands>;
23
- middleware: <M extends RpcMiddleware.AnyService>(middleware: M) => Contract<Name, Queries, Commands, Rpc.AddMiddleware<Rpcs, M>>;
26
+ TName extends string,
27
+ TQueries extends readonly QueryShape[],
28
+ TCommands extends readonly CommandShape[],
29
+ TRpcs extends Rpc.Any = OperationRpc<TName, TQueries[number] | TCommands[number]>,
30
+ TStreams extends readonly StreamShape[] = readonly [],
31
+ > extends RpcGroup.RpcGroup<TRpcs> {
32
+ readonly declaration: Declared<TName, TQueries, TCommands, TStreams>;
33
+ middleware: <TM extends RpcMiddleware.AnyService>(middleware: TM) => Contract<TName, TQueries, TCommands, Rpc.AddMiddleware<TRpcs, TM>, TStreams>;
24
34
  }
25
35
 
26
- type Duplicated<Operations extends readonly OperationShape[], Seen extends string = never> = Operations extends readonly [
27
- infer Head extends OperationShape,
28
- ...infer Tail extends readonly OperationShape[],
36
+ type Duplicated<TOperations extends readonly OperationShape[], TSeen extends string = never> = TOperations extends readonly [
37
+ infer THead extends OperationShape,
38
+ ...infer TTail extends readonly OperationShape[],
29
39
  ]
30
- ? (Head["name"] extends Seen ? Head["name"] : never) | Duplicated<Tail, Seen | Head["name"]>
40
+ ? (THead["name"] extends TSeen ? THead["name"] : never) | Duplicated<TTail, TSeen | THead["name"]>
31
41
  : never;
32
42
 
33
- type Unique<Operations extends readonly OperationShape[]> = [Duplicated<Operations>] extends [never]
43
+ type Unique<TOperations extends readonly OperationShape[]> = [Duplicated<TOperations>] extends [never]
34
44
  ? unknown
35
- : `Operation names must be unique; duplicated: ${Duplicated<Operations>}`;
45
+ : `Operation names must be unique; duplicated: ${Duplicated<TOperations>}`;
36
46
 
37
- const assertUnique = (operations: readonly OperationShape[]) => {
47
+ function assertUnique(operations: readonly OperationShape[]) {
38
48
  const duplicated = operations.map(({ name }) => name).filter((name, index, names) => names.indexOf(name) !== index);
39
49
  if (duplicated.length > 0) {
40
50
  throw new Error(`Operation names must be unique; duplicated: ${[...new Set(duplicated)].join(", ")}`);
41
51
  }
42
- };
52
+ }
43
53
 
44
- type AnyDeclared = Declared<string, readonly QueryShape[], readonly CommandShape[]>;
54
+ type AnyDeclared = Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[]>;
45
55
 
46
56
  interface Group {
47
57
  middleware: (middleware: RpcMiddleware.AnyService) => Group;
@@ -51,30 +61,40 @@ interface DeclaredGroup extends Group {
51
61
  readonly declaration: AnyDeclared;
52
62
  }
53
63
 
54
- const withDeclaration = (group: Group, declaration: AnyDeclared): DeclaredGroup => {
64
+ function withDeclaration(group: Group, declaration: AnyDeclared): DeclaredGroup {
55
65
  const nativeMiddleware = group.middleware.bind(group);
56
66
  return Object.assign(group, {
57
67
  declaration,
58
68
  middleware: (middleware: RpcMiddleware.AnyService) => withDeclaration(nativeMiddleware(middleware), declaration),
59
69
  });
60
- };
70
+ }
61
71
 
62
72
  export function contract<
63
- const Name extends string,
64
- const Queries extends readonly QueryShape[] = readonly [],
65
- const Commands extends readonly CommandShape[] = readonly [],
73
+ const TName extends string,
74
+ const TQueries extends readonly QueryShape[] = readonly [],
75
+ const TCommands extends readonly CommandShape[] = readonly [],
76
+ const TStreams extends readonly StreamShape[] = readonly [],
66
77
  >(
67
- name: Name,
68
- operations: { readonly queries?: Queries & Unique<Queries>; readonly commands?: Commands & Unique<[...Queries, ...Commands]> },
69
- ): Contract<Name, Queries, Commands>;
78
+ name: TName,
79
+ operations: {
80
+ readonly queries?: TQueries & Unique<TQueries>;
81
+ readonly commands?: TCommands & Unique<[...TQueries, ...TCommands]>;
82
+ readonly streams?: TStreams & Unique<[...TQueries, ...TCommands, ...TStreams]>;
83
+ },
84
+ ): Contract<TName, TQueries, TCommands, OperationRpc<TName, TQueries[number] | TCommands[number] | TStreams[number]>, TStreams>;
70
85
  export function contract(
71
86
  name: string,
72
- operations: { readonly queries?: readonly QueryShape[]; readonly commands?: readonly CommandShape[] },
87
+ operations: { readonly queries?: readonly QueryShape[]; readonly commands?: readonly CommandShape[]; readonly streams?: readonly StreamShape[] },
73
88
  ): DeclaredGroup {
74
- const { queries = [], commands = [] } = operations;
75
- assertUnique([...queries, ...commands]);
76
- const rpcs = [...queries, ...commands].map((operation) =>
77
- Rpc.make(`${name}.${operation.name}`, { error: operation.error, payload: operation.payload, success: operation.success }),
89
+ const { queries = [], commands = [], streams = [] } = operations;
90
+ assertUnique([...queries, ...commands, ...streams]);
91
+ const rpcs = [...queries, ...commands, ...streams].map((operation) =>
92
+ Rpc.make(`${name}.${operation.name}`, {
93
+ error: operation.error,
94
+ payload: operation.payload,
95
+ stream: operation.kind === "stream",
96
+ success: operation.success,
97
+ }),
78
98
  );
79
- return withDeclaration(RpcGroup.make(...rpcs), { commands, name, queries });
99
+ return withDeclaration(RpcGroup.make(...rpcs), { commands, name, queries, streams });
80
100
  }
package/src/keys.ts CHANGED
@@ -2,14 +2,14 @@ import { Schema } from "effect";
2
2
 
3
3
  export type Identity = string | number;
4
4
 
5
- export interface ListKey<Name extends string = string> {
5
+ export interface ListKey<TName extends string = string> {
6
6
  readonly _tag: "List";
7
- readonly collection: Name;
7
+ readonly collection: TName;
8
8
  }
9
9
 
10
- export interface ItemKey<Name extends string = string> {
10
+ export interface ItemKey<TName extends string = string> {
11
11
  readonly _tag: "Item";
12
- readonly collection: Name;
12
+ readonly collection: TName;
13
13
  readonly id: Identity;
14
14
  }
15
15
 
@@ -20,22 +20,24 @@ export const Key = Schema.Union([
20
20
  Schema.Struct({ _tag: Schema.Literal("Item"), collection: Schema.String, id: Schema.Union([Schema.String, Schema.Number]) }),
21
21
  ]);
22
22
 
23
- export interface Collection<Name extends string, Id extends Identity> {
24
- readonly item: (id: Id) => ItemKey<Name>;
25
- readonly list: ListKey<Name>;
26
- readonly name: Name;
23
+ export interface Collection<TName extends string, TId extends Identity> {
24
+ readonly item: (id: TId) => ItemKey<TName>;
25
+ readonly list: ListKey<TName>;
26
+ readonly name: TName;
27
27
  }
28
28
 
29
- export const collection = <const Name extends string, Id extends Schema.Top & { readonly Type: Identity }>(
30
- name: Name,
31
- _id: Id,
32
- ): Collection<Name, Id["Type"]> => ({
29
+ export const collection = <const TName extends string, TId extends Schema.Top & { readonly Type: Identity }>(
30
+ name: TName,
31
+ _id: TId,
32
+ ): Collection<TName, TId["Type"]> => ({
33
33
  item: (id) => ({ _tag: "Item", collection: name, id }),
34
34
  list: { _tag: "List", collection: name },
35
35
  name,
36
36
  });
37
37
 
38
- const own = (key: Key): string => (key._tag === "List" ? key.collection : `${key.collection}:${key.id}`);
38
+ function own(key: Key): string {
39
+ return key._tag === "List" ? key.collection : `${key.collection}:${key.id}`;
40
+ }
39
41
 
40
42
  export const readKeys = (keys: readonly Key[]): readonly string[] => [...new Set(keys.map(own))];
41
43
 
@@ -0,0 +1,32 @@
1
+ import { Effect, Stream } from "effect";
2
+ import * as Reactivity from "effect/unstable/reactivity/Reactivity";
3
+ import { type Key, readKeys } from "./keys.ts";
4
+ import type { LiveHint } from "./live.ts";
5
+
6
+ export const makeLiveServer = Effect.gen(function* () {
7
+ const reactivity = yield* Reactivity.Reactivity;
8
+ const resyncKey = `@shivaedev/effect-contract/resync/${crypto.randomUUID()}`;
9
+ let generation = 0;
10
+ function stream(keys: readonly Key[]) {
11
+ return Stream.unwrap(
12
+ Effect.sync(() => {
13
+ let seen = -1;
14
+ return reactivity.stream(
15
+ [...readKeys(keys), resyncKey],
16
+ Effect.sync((): LiveHint => {
17
+ if (seen !== generation) {
18
+ seen = generation;
19
+ return { _tag: "Resync" };
20
+ }
21
+ return { _tag: "Changed", keys };
22
+ }),
23
+ );
24
+ }),
25
+ );
26
+ }
27
+ const resync = Effect.sync(() => {
28
+ generation += 1;
29
+ reactivity.invalidateUnsafe([resyncKey]);
30
+ });
31
+ return { resync, stream };
32
+ });
package/src/operation.ts CHANGED
@@ -3,15 +3,15 @@ import type { Bivariant } from "@shivaedev/types/bivariant.ts";
3
3
  import type { Key } from "./keys.ts";
4
4
  import { type MatchingTags, type Reject, type RejectionSpecs, type Rejections, type RejectionUnion, rejectionSet } from "./rejection.ts";
5
5
 
6
- export type PayloadSchema<Payload extends Schema.Top | Schema.Struct.Fields> = Payload extends Schema.Struct.Fields
7
- ? Schema.Struct<Payload>
8
- : Payload;
6
+ export type PayloadSchema<TPayload extends Schema.Top | Schema.Struct.Fields> = TPayload extends Schema.Struct.Fields
7
+ ? Schema.Struct<TPayload>
8
+ : TPayload;
9
9
 
10
10
  type NoRejections = Record<never, never>;
11
11
 
12
12
  export interface OperationShape {
13
13
  readonly error: Schema.Top;
14
- readonly kind: "query" | "command";
14
+ readonly kind: "query" | "command" | "stream";
15
15
  readonly name: string;
16
16
  readonly payload: Schema.Top;
17
17
  readonly Rejection: object;
@@ -25,38 +25,49 @@ export interface QueryShape extends OperationShape {
25
25
  reads: Bivariant<(payload: unknown) => readonly Key[]>;
26
26
  }
27
27
 
28
+ export interface StreamShape extends OperationShape {
29
+ readonly kind: "stream";
30
+ reads: Bivariant<(payload: unknown) => readonly Key[]>;
31
+ }
32
+
28
33
  export interface CommandShape extends OperationShape {
29
34
  invalidates: Bivariant<(payload: unknown, result: unknown) => readonly Key[]>;
30
35
  readonly kind: "command";
31
36
  }
32
37
 
33
- interface Operation<Name extends string, Payload extends Schema.Top, Success extends Schema.Top, Specs extends RejectionSpecs>
38
+ interface Operation<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
34
39
  extends OperationShape {
35
- readonly error: RejectionUnion<Specs>;
36
- readonly name: Name;
37
- readonly payload: Payload;
38
- readonly Rejection: Rejections<Specs>;
39
- readonly reject: Reject<Specs>;
40
- readonly rejections: Specs;
41
- readonly success: Success;
40
+ readonly error: RejectionUnion<TSpecs>;
41
+ readonly name: TName;
42
+ readonly payload: TPayload;
43
+ readonly Rejection: Rejections<TSpecs>;
44
+ readonly reject: Reject<TSpecs>;
45
+ readonly rejections: TSpecs;
46
+ readonly success: TSuccess;
42
47
  }
43
48
 
44
- export interface Query<Name extends string, Payload extends Schema.Top, Success extends Schema.Top, Specs extends RejectionSpecs>
45
- extends Operation<Name, Payload, Success, Specs> {
49
+ export interface Query<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
50
+ extends Operation<TName, TPayload, TSuccess, TSpecs> {
46
51
  readonly kind: "query";
47
- reads: (payload: Payload["Type"]) => readonly Key[];
52
+ reads: (payload: TPayload["Type"]) => readonly Key[];
48
53
  }
49
54
 
50
- export interface Command<Name extends string, Payload extends Schema.Top, Success extends Schema.Top, Specs extends RejectionSpecs>
51
- extends Operation<Name, Payload, Success, Specs> {
52
- invalidates: (payload: Payload["Type"], result: Success["Type"]) => readonly Key[];
55
+ export interface Streaming<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
56
+ extends Operation<TName, TPayload, TSuccess, TSpecs> {
57
+ readonly kind: "stream";
58
+ reads: (payload: TPayload["Type"]) => readonly Key[];
59
+ }
60
+
61
+ export interface Command<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
62
+ extends Operation<TName, TPayload, TSuccess, TSpecs> {
63
+ invalidates: (payload: TPayload["Type"], result: TSuccess["Type"]) => readonly Key[];
53
64
  readonly kind: "command";
54
65
  }
55
66
 
56
- interface Declaration<Payload extends Schema.Top | Schema.Struct.Fields, Success extends Schema.Top, Specs extends RejectionSpecs> {
57
- readonly payload?: Payload;
58
- readonly rejections?: Specs & MatchingTags<Specs>;
59
- readonly success?: Success;
67
+ interface Declaration<TPayload extends Schema.Top | Schema.Struct.Fields, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs> {
68
+ readonly payload?: TPayload;
69
+ readonly rejections?: TSpecs & MatchingTags<TSpecs>;
70
+ readonly success?: TSuccess;
60
71
  }
61
72
 
62
73
  interface Loose {
@@ -65,14 +76,14 @@ interface Loose {
65
76
  readonly success?: Schema.Top;
66
77
  }
67
78
 
68
- const payloadSchema = (payload: Loose["payload"]): Schema.Top => {
79
+ function payloadSchema(payload: Loose["payload"]): Schema.Top {
69
80
  if (payload === undefined) {
70
81
  return Schema.Void;
71
82
  }
72
83
  return Schema.isSchema(payload) ? payload : Schema.Struct(payload);
73
- };
84
+ }
74
85
 
75
- const operation = <Kind extends OperationShape["kind"]>(kind: Kind, name: string, declaration: Loose) => {
86
+ function operation<TKind extends OperationShape["kind"]>(kind: TKind, name: string, declaration: Loose) {
76
87
  const { payload, success, rejections = {} } = declaration;
77
88
  return {
78
89
  kind,
@@ -82,37 +93,53 @@ const operation = <Kind extends OperationShape["kind"]>(kind: Kind, name: string
82
93
  success: success ?? Schema.Void,
83
94
  ...rejectionSet(rejections),
84
95
  };
85
- };
96
+ }
86
97
 
87
98
  export function query<
88
- const Name extends string,
89
- Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
90
- Success extends Schema.Top = Schema.Void,
91
- const Specs extends RejectionSpecs = NoRejections,
99
+ const TName extends string,
100
+ TPayload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
101
+ TSuccess extends Schema.Top = Schema.Void,
102
+ const TSpecs extends RejectionSpecs = NoRejections,
92
103
  >(
93
- name: Name,
94
- declaration: Declaration<Payload, Success, Specs> & {
95
- readonly reads: (payload: PayloadSchema<Payload>["Type"]) => readonly Key[];
104
+ name: TName,
105
+ declaration: Declaration<TPayload, TSuccess, TSpecs> & {
106
+ readonly reads: (payload: PayloadSchema<TPayload>["Type"]) => readonly Key[];
96
107
  },
97
- ): NoInfer<Query<Name, PayloadSchema<Payload>, Success, Specs>>;
108
+ ): NoInfer<Query<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
98
109
  export function query(name: string, declaration: Loose & { readonly reads: (payload: unknown) => readonly Key[] }): QueryShape {
99
110
  return { ...operation("query", name, declaration), reads: declaration.reads };
100
111
  }
101
112
 
102
113
  export function command<
103
- const Name extends string,
104
- Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
105
- Success extends Schema.Top = Schema.Void,
106
- const Specs extends RejectionSpecs = NoRejections,
114
+ const TName extends string,
115
+ TPayload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
116
+ TSuccess extends Schema.Top = Schema.Void,
117
+ const TSpecs extends RejectionSpecs = NoRejections,
107
118
  >(
108
- name: Name,
109
- declaration: Declaration<Payload, Success, Specs> & {
110
- readonly invalidates: (payload: PayloadSchema<Payload>["Type"], result: Success["Type"]) => readonly Key[];
119
+ name: TName,
120
+ declaration: Declaration<TPayload, TSuccess, TSpecs> & {
121
+ readonly invalidates: (payload: PayloadSchema<TPayload>["Type"], result: TSuccess["Type"]) => readonly Key[];
111
122
  },
112
- ): NoInfer<Command<Name, PayloadSchema<Payload>, Success, Specs>>;
123
+ ): NoInfer<Command<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
113
124
  export function command(
114
125
  name: string,
115
126
  declaration: Loose & { readonly invalidates: (payload: unknown, result: unknown) => readonly Key[] },
116
127
  ): CommandShape {
117
128
  return { ...operation("command", name, declaration), invalidates: declaration.invalidates };
118
129
  }
130
+
131
+ export function stream<
132
+ const TName extends string,
133
+ TPayload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
134
+ TSuccess extends Schema.Top = Schema.Void,
135
+ const TSpecs extends RejectionSpecs = NoRejections,
136
+ >(
137
+ name: TName,
138
+ declaration: Declaration<TPayload, TSuccess, TSpecs> & {
139
+ readonly success: TSuccess;
140
+ readonly reads: (payload: PayloadSchema<TPayload>["Type"]) => readonly Key[];
141
+ },
142
+ ): NoInfer<Streaming<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
143
+ export function stream(name: string, declaration: Loose & { readonly reads: (payload: unknown) => readonly Key[] }): StreamShape {
144
+ return { ...operation("stream", name, declaration), reads: declaration.reads };
145
+ }