@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/CHANGELOG.md +13 -0
- package/README.md +331 -62
- package/dist/bind.d.ts +10 -5
- package/dist/bind.d.ts.map +1 -1
- package/dist/bind.js +8 -2
- package/dist/bind.js.map +1 -1
- package/dist/contract.d.ts +23 -20
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +13 -8
- package/dist/contract.js.map +1 -1
- package/dist/keys.d.ts +10 -10
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +3 -1
- package/dist/keys.js.map +1 -1
- package/dist/makeLiveServer.d.ts +20 -0
- package/dist/makeLiveServer.d.ts.map +1 -0
- package/dist/makeLiveServer.js +26 -0
- package/dist/makeLiveServer.js.map +1 -0
- package/dist/operation.d.ts +37 -25
- package/dist/operation.d.ts.map +1 -1
- package/dist/operation.js +7 -4
- package/dist/operation.js.map +1 -1
- package/dist/rejection.d.ts +27 -27
- package/dist/rejection.d.ts.map +1 -1
- package/dist/rejection.js.map +1 -1
- package/docs/north-star.md +45 -0
- package/docs/roadmap.md +26 -0
- package/package.json +56 -13
- package/src/bind.ts +31 -10
- package/src/contract.ts +59 -39
- package/src/keys.ts +15 -13
- package/src/makeLiveServer.ts +32 -0
- package/src/operation.ts +69 -42
- package/src/rejection.ts +40 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-contract",
|
|
3
|
-
"version": "0.4.
|
|
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
|
-
"
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
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
|
-
"./
|
|
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.
|
|
50
|
-
"@shivaedev/quality": "0.
|
|
51
|
-
"@types/node": "24.
|
|
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.
|
|
55
|
-
"happy-dom": "20.
|
|
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 }))\" &&
|
|
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
|
|
16
|
-
?
|
|
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:
|
|
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 {
|
|
2
|
-
import
|
|
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<
|
|
5
|
+
export type Tag<TContract extends string, TName extends string> = `${TContract}.${TName}`;
|
|
5
6
|
|
|
6
|
-
export type OperationRpc<
|
|
7
|
-
?
|
|
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<
|
|
11
|
-
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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<
|
|
27
|
-
infer
|
|
28
|
-
...infer
|
|
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
|
-
? (
|
|
40
|
+
? (THead["name"] extends TSeen ? THead["name"] : never) | Duplicated<TTail, TSeen | THead["name"]>
|
|
31
41
|
: never;
|
|
32
42
|
|
|
33
|
-
type Unique<
|
|
43
|
+
type Unique<TOperations extends readonly OperationShape[]> = [Duplicated<TOperations>] extends [never]
|
|
34
44
|
? unknown
|
|
35
|
-
: `Operation names must be unique; duplicated: ${Duplicated<
|
|
45
|
+
: `Operation names must be unique; duplicated: ${Duplicated<TOperations>}`;
|
|
36
46
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
const
|
|
65
|
-
const
|
|
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:
|
|
68
|
-
operations: {
|
|
69
|
-
|
|
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}`, {
|
|
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<
|
|
5
|
+
export interface ListKey<TName extends string = string> {
|
|
6
6
|
readonly _tag: "List";
|
|
7
|
-
readonly collection:
|
|
7
|
+
readonly collection: TName;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export interface ItemKey<
|
|
10
|
+
export interface ItemKey<TName extends string = string> {
|
|
11
11
|
readonly _tag: "Item";
|
|
12
|
-
readonly collection:
|
|
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<
|
|
24
|
-
readonly item: (id:
|
|
25
|
-
readonly list: ListKey<
|
|
26
|
-
readonly 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
|
|
30
|
-
name:
|
|
31
|
-
_id:
|
|
32
|
-
): Collection<
|
|
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
|
-
|
|
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<
|
|
7
|
-
? Schema.Struct<
|
|
8
|
-
:
|
|
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<
|
|
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<
|
|
36
|
-
readonly name:
|
|
37
|
-
readonly payload:
|
|
38
|
-
readonly Rejection: Rejections<
|
|
39
|
-
readonly reject: Reject<
|
|
40
|
-
readonly rejections:
|
|
41
|
-
readonly 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<
|
|
45
|
-
extends Operation<
|
|
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:
|
|
52
|
+
reads: (payload: TPayload["Type"]) => readonly Key[];
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
export interface
|
|
51
|
-
extends Operation<
|
|
52
|
-
|
|
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<
|
|
57
|
-
readonly payload?:
|
|
58
|
-
readonly rejections?:
|
|
59
|
-
readonly 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
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
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:
|
|
94
|
-
declaration: Declaration<
|
|
95
|
-
readonly reads: (payload: PayloadSchema<
|
|
104
|
+
name: TName,
|
|
105
|
+
declaration: Declaration<TPayload, TSuccess, TSpecs> & {
|
|
106
|
+
readonly reads: (payload: PayloadSchema<TPayload>["Type"]) => readonly Key[];
|
|
96
107
|
},
|
|
97
|
-
): NoInfer<Query<
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
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:
|
|
109
|
-
declaration: Declaration<
|
|
110
|
-
readonly invalidates: (payload: PayloadSchema<
|
|
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<
|
|
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
|
+
}
|