@shivaedev/contract 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +57 -36
- package/dist/automaticLive.d.ts +2 -2
- package/dist/automaticLive.d.ts.map +1 -1
- package/dist/automaticLive.js +4 -3
- package/dist/automaticLive.js.map +1 -1
- package/dist/bind.d.ts +11 -2
- package/dist/bind.d.ts.map +1 -1
- package/dist/bind.js +9 -0
- package/dist/bind.js.map +1 -1
- package/dist/clientOptions.d.ts +23 -0
- package/dist/clientOptions.d.ts.map +1 -0
- package/dist/clientOptions.js +2 -0
- package/dist/clientOptions.js.map +1 -0
- package/dist/contract.d.ts +4 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +6 -5
- package/dist/contract.js.map +1 -1
- package/dist/internal/bindingOwner.d.ts +1 -1
- package/dist/internal/bindingOwner.d.ts.map +1 -1
- package/dist/internal/clientOperations.d.ts.map +1 -1
- package/dist/internal/clientOperations.js +26 -7
- package/dist/internal/clientOperations.js.map +1 -1
- package/dist/internal/httpClient.d.ts +21 -0
- package/dist/internal/httpClient.d.ts.map +1 -0
- package/dist/internal/httpClient.js +13 -0
- package/dist/internal/httpClient.js.map +1 -0
- package/dist/internal/operationDeclaration.d.ts +19 -0
- package/dist/internal/operationDeclaration.d.ts.map +1 -0
- package/dist/internal/operationDeclaration.js +20 -0
- package/dist/internal/operationDeclaration.js.map +1 -0
- package/dist/internal/queryHints.d.ts +8 -0
- package/dist/internal/queryHints.d.ts.map +1 -0
- package/dist/internal/queryHints.js +34 -0
- package/dist/internal/queryHints.js.map +1 -0
- package/dist/live.d.ts +7 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +2 -2
- package/dist/live.js.map +1 -1
- package/dist/makeLiveServer.d.ts +27 -1
- package/dist/makeLiveServer.d.ts.map +1 -1
- package/dist/makeLiveServer.js +15 -8
- package/dist/makeLiveServer.js.map +1 -1
- package/dist/operation.d.ts +13 -9
- package/dist/operation.d.ts.map +1 -1
- package/dist/operation.js +5 -19
- package/dist/operation.js.map +1 -1
- package/dist/publicConfig.d.ts +9 -3
- package/dist/publicConfig.d.ts.map +1 -1
- package/dist/publicConfig.js +2 -1
- package/dist/publicConfig.js.map +1 -1
- package/dist/querySubscription.d.ts +15 -0
- package/dist/querySubscription.d.ts.map +1 -0
- package/dist/querySubscription.js +10 -0
- package/dist/querySubscription.js.map +1 -0
- package/dist/readPolicy.d.ts +11 -6
- package/dist/readPolicy.d.ts.map +1 -1
- package/dist/readPolicy.js +16 -19
- package/dist/readPolicy.js.map +1 -1
- package/dist/readPolicyErrors.d.ts +13 -0
- package/dist/readPolicyErrors.d.ts.map +1 -0
- package/dist/readPolicyErrors.js +6 -0
- package/dist/readPolicyErrors.js.map +1 -0
- package/dist/serverMetadata.d.ts +29 -11
- package/dist/serverMetadata.d.ts.map +1 -1
- package/dist/serverMetadata.js +19 -12
- package/dist/serverMetadata.js.map +1 -1
- package/dist/testing.d.ts +13 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -4
- package/dist/testing.js.map +1 -1
- package/package.json +6 -5
- package/src/automaticLive.ts +8 -3
- package/src/bind.ts +45 -3
- package/src/clientOptions.ts +35 -0
- package/src/contract.ts +11 -6
- package/src/internal/bindingOwner.ts +1 -1
- package/src/internal/clientOperations.ts +30 -11
- package/src/internal/httpClient.ts +21 -0
- package/src/internal/operationDeclaration.ts +28 -0
- package/src/internal/queryHints.ts +38 -0
- package/src/live.ts +2 -2
- package/src/makeLiveServer.ts +18 -12
- package/src/operation.ts +27 -37
- package/src/publicConfig.ts +2 -2
- package/src/querySubscription.ts +21 -0
- package/src/readPolicy.ts +23 -23
- package/src/readPolicyErrors.ts +4 -0
- package/src/serverMetadata.ts +35 -19
- package/src/testing.ts +2 -12
- package/dist/internal/makeHintPolicy.d.ts +0 -4
- package/dist/internal/makeHintPolicy.d.ts.map +0 -1
- package/dist/internal/makeHintPolicy.js +0 -25
- package/dist/internal/makeHintPolicy.js.map +0 -1
- package/dist/keys.d.ts +0 -34
- package/dist/keys.d.ts.map +0 -1
- package/dist/keys.js +0 -25
- package/dist/keys.js.map +0 -1
- package/src/internal/makeHintPolicy.ts +0 -40
- package/src/keys.ts +0 -62
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { Effect, type Schema, Stream } from "effect";
|
|
1
|
+
import { Effect, Option, type Schema, Stream } from "effect";
|
|
2
2
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
3
3
|
import type * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
4
4
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
5
5
|
import type { Rpc, RpcSchema } from "effect/rpc";
|
|
6
|
+
import { invalidationKeys, type Key, readKeys } from "@shivaedev/changes/keys.ts";
|
|
6
7
|
import type { automaticLive } from "#automaticLive.ts";
|
|
7
8
|
import type { QueryOptions } from "#bind.ts";
|
|
8
|
-
import { invalidationKeys, readKeys } from "#keys.ts";
|
|
9
9
|
import type { CommandShape, QueryShape } from "#operation.ts";
|
|
10
|
+
import { MissingReadPolicy } from "#readPolicyErrors.ts";
|
|
10
11
|
import type { BindingOwner } from "./bindingOwner.ts";
|
|
11
12
|
import { queryReactivity } from "./queryReactivity.ts";
|
|
12
13
|
|
|
13
14
|
type ErasedClient = AtomRpc.AtomRpcClient<unknown, string, Rpc.Rpc<string, Schema.Top, Schema.Top, Schema.Top>>;
|
|
14
15
|
|
|
16
|
+
const queryIds = new WeakMap<object, number>();
|
|
17
|
+
let nextQueryId = 0;
|
|
18
|
+
|
|
15
19
|
type ErasedStreamingClient = AtomRpc.AtomRpcClient<
|
|
16
20
|
unknown,
|
|
17
21
|
string,
|
|
@@ -32,15 +36,29 @@ export function boundQuery(
|
|
|
32
36
|
const reactive = queryReactivity(service.runtime);
|
|
33
37
|
return {
|
|
34
38
|
query: (payload: unknown, options: QueryOptions = {}) => {
|
|
39
|
+
if (query.readPolicy === undefined) {
|
|
40
|
+
return service.runtime.atom(
|
|
41
|
+
Effect.fail(new MissingReadPolicy({ message: `Declare a readPolicy on query ${query.name} before running it live or subscribing` })),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
35
44
|
const atom = service.query(tag, payload, options);
|
|
36
|
-
|
|
45
|
+
let id = queryIds.get(atom);
|
|
46
|
+
if (id === undefined) {
|
|
47
|
+
id = nextQueryId;
|
|
48
|
+
nextQueryId += 1;
|
|
49
|
+
queryIds.set(atom, id);
|
|
50
|
+
}
|
|
51
|
+
const local = query.reads(payload, Option.none());
|
|
52
|
+
const keys = [...local, { _tag: "List" as const, collection: `@query/${tag}/${id}` }];
|
|
53
|
+
const dependencies =
|
|
54
|
+
local.length === 0 ? [...keys, { _tag: "List" as const, collection: `@query/${tag.slice(0, tag.lastIndexOf("."))}` }] : keys;
|
|
37
55
|
const reference = { payload, query: query.name };
|
|
38
56
|
if (owner === undefined) {
|
|
39
57
|
track(undefined, reference, keys);
|
|
40
58
|
}
|
|
41
59
|
return reactive(
|
|
42
60
|
atom,
|
|
43
|
-
readKeys(
|
|
61
|
+
readKeys(dependencies, owner?.context),
|
|
44
62
|
(registry) => (owner === undefined ? () => undefined : track(registry, reference, keys)),
|
|
45
63
|
owner?.registry,
|
|
46
64
|
);
|
|
@@ -55,15 +73,16 @@ export function boundQuery(
|
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
export function boundCommand(service: ErasedClient, tag: string, command: CommandShape, owner?: BindingOwner) {
|
|
76
|
+
function invalidate(payload: unknown, result: unknown) {
|
|
77
|
+
const keys: readonly Key[] = [
|
|
78
|
+
...command.invalidates(payload, result),
|
|
79
|
+
{ _tag: "List", collection: `@query/${tag.slice(0, tag.lastIndexOf("."))}` },
|
|
80
|
+
];
|
|
81
|
+
return Reactivity.invalidate(invalidationKeys(keys, owner?.context));
|
|
82
|
+
}
|
|
58
83
|
return {
|
|
59
84
|
run: (payload: unknown) =>
|
|
60
|
-
run(
|
|
61
|
-
service,
|
|
62
|
-
service
|
|
63
|
-
.use((client) => client(tag, payload))
|
|
64
|
-
.pipe(Effect.tap((result) => Reactivity.invalidate(invalidationKeys(command.invalidates(payload, result), owner?.context)))),
|
|
65
|
-
owner,
|
|
66
|
-
),
|
|
85
|
+
run(service, service.use((client) => client(tag, payload)).pipe(Effect.tap((result) => invalidate(payload, result))), owner),
|
|
67
86
|
};
|
|
68
87
|
}
|
|
69
88
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Context, Effect, Layer } from "effect";
|
|
2
|
+
import { FetchHttpClient } from "effect/http";
|
|
3
|
+
import * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
4
|
+
import { type Rpc, RpcClient, RpcGroup, RpcSerialization } from "effect/rpc";
|
|
5
|
+
import type { ClientOptions } from "#clientOptions.ts";
|
|
6
|
+
|
|
7
|
+
export interface HttpGroup {
|
|
8
|
+
readonly annotations: Context.Context<never>;
|
|
9
|
+
readonly requests: ReadonlyMap<string, Rpc.Any>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function httpClient(name: string, source: HttpGroup, options: ClientOptions) {
|
|
13
|
+
const group = RpcGroup.make(...source.requests.values()).annotateMerge(source.annotations);
|
|
14
|
+
const protocol = RpcClient.layerProtocolHttp(options).pipe(
|
|
15
|
+
Layer.provide([options.httpClient ?? FetchHttpClient.layer, options.serialization ?? RpcSerialization.layerNdjson]),
|
|
16
|
+
);
|
|
17
|
+
const Client = AtomRpc.Service<unknown>()(`${name}/Client`, { group, protocol });
|
|
18
|
+
const client = RpcClient.make(group).pipe(Effect.provide(protocol));
|
|
19
|
+
const layer = Layer.effect(Client, RpcClient.make(group, { flatten: true })).pipe(Layer.provide(protocol));
|
|
20
|
+
return Object.assign(layer, { Client, client, runtime: Client.runtime });
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { OperationShape } from "#operation.ts";
|
|
3
|
+
import { type RejectionSpecs, rejectionSet } from "#rejection.ts";
|
|
4
|
+
|
|
5
|
+
export interface Loose {
|
|
6
|
+
readonly payload?: Schema.Top | Schema.Struct.Fields;
|
|
7
|
+
readonly rejections?: RejectionSpecs;
|
|
8
|
+
readonly success?: Schema.Top;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function payloadSchema(payload: Loose["payload"]): Schema.Top {
|
|
12
|
+
if (payload === undefined) {
|
|
13
|
+
return Schema.Void;
|
|
14
|
+
}
|
|
15
|
+
return Schema.isSchema(payload) ? payload : Schema.Struct(payload);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function operation<TKind extends OperationShape["kind"]>(kind: TKind, name: string, declaration: Loose) {
|
|
19
|
+
const { payload, success, rejections = {} } = declaration;
|
|
20
|
+
return {
|
|
21
|
+
kind,
|
|
22
|
+
name,
|
|
23
|
+
payload: payloadSchema(payload),
|
|
24
|
+
rejections,
|
|
25
|
+
success: success ?? Schema.Void,
|
|
26
|
+
...rejectionSet(rejections),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { invalidationKeys, type Key, readKeys } from "@shivaedev/changes/keys.ts";
|
|
2
|
+
import type { LiveHint } from "#live.ts";
|
|
3
|
+
|
|
4
|
+
export function localQueryKeys(keys: readonly Key[]) {
|
|
5
|
+
return keys.filter((key) => key.collection.startsWith("@query/"));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function queryHint(hint: LiveHint, tracked: readonly { readonly keys: readonly Key[] }[]): LiveHint {
|
|
9
|
+
if (hint._tag === "Resync") {
|
|
10
|
+
return hint;
|
|
11
|
+
}
|
|
12
|
+
const invalidated = invalidationKeys(hint.keys);
|
|
13
|
+
return {
|
|
14
|
+
_tag: "Changed",
|
|
15
|
+
keys: [
|
|
16
|
+
...hint.keys,
|
|
17
|
+
...tracked.flatMap(({ keys }, index) => {
|
|
18
|
+
const local = keys.filter((key) => !key.collection.startsWith("@query/"));
|
|
19
|
+
const affected =
|
|
20
|
+
hint.queryIndexes === undefined
|
|
21
|
+
? local.length === 0 || hint.keys.some((key) => key._tag === "Bucket") || readKeys(local).some((key) => invalidated.includes(key))
|
|
22
|
+
: hint.queryIndexes.includes(index);
|
|
23
|
+
return affected ? localQueryKeys(keys) : [];
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function changedHint(keys: readonly Key[], scopes?: readonly (readonly Key[])[]): LiveHint {
|
|
30
|
+
const changed = new Set(keys.map((key) => JSON.stringify(key)));
|
|
31
|
+
return {
|
|
32
|
+
_tag: "Changed",
|
|
33
|
+
keys,
|
|
34
|
+
...(scopes === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { queryIndexes: scopes.flatMap((scope, index) => (scope.some((key) => changed.has(JSON.stringify(key))) ? [index] : [])) }),
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/live.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { type Cause, Duration, Effect, Option, Schema, Stream } from "effect";
|
|
|
2
2
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
4
4
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
5
|
+
import { invalidationKeys, Key, type KeyContext, readKeys } from "@shivaedev/changes/keys.ts";
|
|
5
6
|
import { invalidateLive } from "#internal/liveInvalidation.ts";
|
|
6
|
-
import { invalidationKeys, Key, type KeyContext, readKeys } from "#keys.ts";
|
|
7
7
|
|
|
8
8
|
export const LiveHint = Schema.Union([
|
|
9
|
-
Schema.Struct({ _tag: Schema.Literal("Changed"), keys: Schema.Array(Key) }),
|
|
9
|
+
Schema.Struct({ _tag: Schema.Literal("Changed"), keys: Schema.Array(Key), queryIndexes: Schema.optional(Schema.Array(Schema.Number)) }),
|
|
10
10
|
Schema.Struct({ _tag: Schema.Literal("Resync") }),
|
|
11
11
|
]);
|
|
12
12
|
export type LiveHint = typeof LiveHint.Type;
|
package/src/makeLiveServer.ts
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { Effect, Queue, Stream } from "effect";
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { type Key, type KeyContext, readKeys } from "@shivaedev/changes/keys.ts";
|
|
4
|
+
import { changedHint } from "#internal/queryHints.ts";
|
|
5
5
|
import type { LiveHint } from "#live.ts";
|
|
6
|
+
import type { QuerySubscription } from "#querySubscription.ts";
|
|
6
7
|
|
|
7
8
|
export const makeLiveServer = Effect.gen(function* () {
|
|
8
9
|
const reactivity = yield* Reactivity.Reactivity;
|
|
9
10
|
let openStreams = 0;
|
|
10
11
|
const resyncKey = `@shivaedev/contract/resync/${crypto.randomUUID()}`;
|
|
11
|
-
function
|
|
12
|
+
function open(keys: readonly Key[], context?: KeyContext, subscription?: QuerySubscription) {
|
|
12
13
|
return Stream.unwrap(
|
|
13
14
|
Effect.gen(function* () {
|
|
14
15
|
const queue = yield* Queue.unbounded<LiveHint>();
|
|
15
|
-
const allowedKeys = yield* makeHintPolicy(keys.length, (retryKeys) =>
|
|
16
|
-
Queue.offer(queue, { _tag: "Changed", keys: retryKeys }).pipe(Effect.asVoid),
|
|
17
|
-
);
|
|
18
16
|
const pending = new Map<string, Key>();
|
|
19
17
|
let scheduled = false;
|
|
20
18
|
let resync = false;
|
|
@@ -57,12 +55,16 @@ export const makeLiveServer = Effect.gen(function* () {
|
|
|
57
55
|
}),
|
|
58
56
|
);
|
|
59
57
|
yield* Queue.offer(queue, { _tag: "Resync" });
|
|
58
|
+
function authorizeHint(hint: LiveHint) {
|
|
59
|
+
if (hint._tag === "Resync") {
|
|
60
|
+
return Effect.succeed(hint);
|
|
61
|
+
}
|
|
62
|
+
return Effect.map(subscription?.authorized ?? Effect.succeed(true), (allowed) =>
|
|
63
|
+
changedHint(allowed ? hint.keys : [], subscription?.scopes),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
60
66
|
return Stream.fromQueue(queue).pipe(
|
|
61
|
-
Stream.mapEffect(
|
|
62
|
-
hint._tag === "Resync"
|
|
63
|
-
? Effect.succeed(hint)
|
|
64
|
-
: Effect.map(allowedKeys(hint.keys), (allowed): LiveHint => ({ _tag: "Changed", keys: allowed })),
|
|
65
|
-
),
|
|
67
|
+
Stream.mapEffect(authorizeHint),
|
|
66
68
|
Stream.filter((hint) => hint._tag === "Resync" || hint.keys.length > 0),
|
|
67
69
|
);
|
|
68
70
|
}),
|
|
@@ -72,5 +74,9 @@ export const makeLiveServer = Effect.gen(function* () {
|
|
|
72
74
|
reactivity.invalidateUnsafe([resyncKey]);
|
|
73
75
|
return openStreams;
|
|
74
76
|
});
|
|
75
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
resync,
|
|
79
|
+
stream: (keys: readonly Key[], context?: KeyContext) => open(keys, context),
|
|
80
|
+
subscribe: (subscription: QuerySubscription, context?: KeyContext) => open(subscription.keys, context, subscription),
|
|
81
|
+
};
|
|
76
82
|
});
|
package/src/operation.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import type { Option, Schema } from "effect";
|
|
2
|
+
import type { Key } from "@shivaedev/changes/keys.ts";
|
|
2
3
|
import type { Bivariant } from "@shivaedev/types/Bivariant.ts";
|
|
3
|
-
import type
|
|
4
|
-
import
|
|
4
|
+
import { type Loose, operation } from "#internal/operationDeclaration.ts";
|
|
5
|
+
import type { QueryReadPolicy, Viewer } from "#readPolicy.ts";
|
|
6
|
+
import type { MatchingTags, Reject, RejectionSpecs, Rejections, RejectionUnion } from "./rejection.ts";
|
|
5
7
|
|
|
6
8
|
export type PayloadSchema<TPayload extends Schema.Top | Schema.Struct.Fields> = TPayload extends Schema.Struct.Fields
|
|
7
9
|
? Schema.Struct<TPayload>
|
|
@@ -22,7 +24,8 @@ export interface OperationShape {
|
|
|
22
24
|
|
|
23
25
|
export interface QueryShape extends OperationShape {
|
|
24
26
|
readonly kind: "query";
|
|
25
|
-
|
|
27
|
+
readonly readPolicy?: Bivariant<QueryReadPolicy<unknown>>;
|
|
28
|
+
reads: Bivariant<(payload: unknown, viewer: Option.Option<Viewer>) => readonly Key[]>;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export interface StreamShape extends OperationShape {
|
|
@@ -31,7 +34,7 @@ export interface StreamShape extends OperationShape {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface CommandShape extends OperationShape {
|
|
34
|
-
invalidates: Bivariant<(payload: unknown, result: unknown) => readonly Key[]>;
|
|
37
|
+
invalidates: Bivariant<(payload: unknown, result: unknown, viewer?: Option.Option<Viewer>) => readonly Key[]>;
|
|
35
38
|
readonly kind: "command";
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -49,7 +52,8 @@ interface Operation<TName extends string, TPayload extends Schema.Top, TSuccess
|
|
|
49
52
|
export interface Query<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
|
|
50
53
|
extends Operation<TName, TPayload, TSuccess, TSpecs> {
|
|
51
54
|
readonly kind: "query";
|
|
52
|
-
|
|
55
|
+
readonly readPolicy?: QueryReadPolicy<TPayload["Type"]>;
|
|
56
|
+
reads: (payload: TPayload["Type"], viewer: Option.Option<Viewer>) => readonly Key[];
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
export interface Streaming<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
|
|
@@ -60,7 +64,7 @@ export interface Streaming<TName extends string, TPayload extends Schema.Top, TS
|
|
|
60
64
|
|
|
61
65
|
export interface Command<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
|
|
62
66
|
extends Operation<TName, TPayload, TSuccess, TSpecs> {
|
|
63
|
-
invalidates: (payload: TPayload["Type"], result: TSuccess["Type"]) => readonly Key[];
|
|
67
|
+
invalidates: (payload: TPayload["Type"], result: TSuccess["Type"], viewer?: Option.Option<Viewer>) => readonly Key[];
|
|
64
68
|
readonly kind: "command";
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -70,31 +74,6 @@ interface Declaration<TPayload extends Schema.Top | Schema.Struct.Fields, TSucce
|
|
|
70
74
|
readonly success?: TSuccess;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
interface Loose {
|
|
74
|
-
readonly payload?: Schema.Top | Schema.Struct.Fields;
|
|
75
|
-
readonly rejections?: RejectionSpecs;
|
|
76
|
-
readonly success?: Schema.Top;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function payloadSchema(payload: Loose["payload"]): Schema.Top {
|
|
80
|
-
if (payload === undefined) {
|
|
81
|
-
return Schema.Void;
|
|
82
|
-
}
|
|
83
|
-
return Schema.isSchema(payload) ? payload : Schema.Struct(payload);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function operation<TKind extends OperationShape["kind"]>(kind: TKind, name: string, declaration: Loose) {
|
|
87
|
-
const { payload, success, rejections = {} } = declaration;
|
|
88
|
-
return {
|
|
89
|
-
kind,
|
|
90
|
-
name,
|
|
91
|
-
payload: payloadSchema(payload),
|
|
92
|
-
rejections,
|
|
93
|
-
success: success ?? Schema.Void,
|
|
94
|
-
...rejectionSet(rejections),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
77
|
export function query<
|
|
99
78
|
const TName extends string,
|
|
100
79
|
TPayload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
|
|
@@ -103,11 +82,22 @@ export function query<
|
|
|
103
82
|
>(
|
|
104
83
|
name: TName,
|
|
105
84
|
declaration: Declaration<TPayload, TSuccess, TSpecs> & {
|
|
106
|
-
readonly reads: (payload: PayloadSchema<TPayload>["Type"]) => readonly Key[];
|
|
85
|
+
readonly reads: (payload: PayloadSchema<TPayload>["Type"], viewer: Option.Option<Viewer>) => readonly Key[];
|
|
86
|
+
readonly readPolicy?: QueryReadPolicy<PayloadSchema<TPayload>["Type"]>;
|
|
107
87
|
},
|
|
108
88
|
): NoInfer<Query<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
|
|
109
|
-
export function query(
|
|
110
|
-
|
|
89
|
+
export function query(
|
|
90
|
+
name: string,
|
|
91
|
+
declaration: Loose & {
|
|
92
|
+
readonly reads: (payload: unknown, viewer: Option.Option<Viewer>) => readonly Key[];
|
|
93
|
+
readonly readPolicy?: QueryReadPolicy<unknown>;
|
|
94
|
+
},
|
|
95
|
+
): QueryShape {
|
|
96
|
+
return {
|
|
97
|
+
...operation("query", name, declaration),
|
|
98
|
+
reads: declaration.reads,
|
|
99
|
+
...(declaration.readPolicy === undefined ? {} : { readPolicy: declaration.readPolicy }),
|
|
100
|
+
};
|
|
111
101
|
}
|
|
112
102
|
|
|
113
103
|
export function command<
|
|
@@ -118,12 +108,12 @@ export function command<
|
|
|
118
108
|
>(
|
|
119
109
|
name: TName,
|
|
120
110
|
declaration: Declaration<TPayload, TSuccess, TSpecs> & {
|
|
121
|
-
readonly invalidates: (payload: PayloadSchema<TPayload>["Type"], result: TSuccess["Type"]) => readonly Key[];
|
|
111
|
+
readonly invalidates: (payload: PayloadSchema<TPayload>["Type"], result: TSuccess["Type"], viewer?: Option.Option<Viewer>) => readonly Key[];
|
|
122
112
|
},
|
|
123
113
|
): NoInfer<Command<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
|
|
124
114
|
export function command(
|
|
125
115
|
name: string,
|
|
126
|
-
declaration: Loose & { readonly invalidates: (payload: unknown, result: unknown) => readonly Key[] },
|
|
116
|
+
declaration: Loose & { readonly invalidates: (payload: unknown, result: unknown, viewer?: Option.Option<Viewer>) => readonly Key[] },
|
|
127
117
|
): CommandShape {
|
|
128
118
|
return { ...operation("command", name, declaration), invalidates: declaration.invalidates };
|
|
129
119
|
}
|
package/src/publicConfig.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Effect, type Schema } from "effect";
|
|
2
2
|
import { contract } from "#contract.ts";
|
|
3
3
|
import { query } from "#operation.ts";
|
|
4
4
|
|
|
5
5
|
export function publicConfig<T, E>(schema: Schema.Codec<T, E>) {
|
|
6
6
|
return contract("platform.publicConfig", {
|
|
7
|
-
queries: [query("get", { reads: () => [], success: schema })],
|
|
7
|
+
queries: [query("get", { readPolicy: () => Effect.succeed(true), reads: () => [], success: schema })],
|
|
8
8
|
});
|
|
9
9
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context, Effect, type Option } from "effect";
|
|
2
|
+
import type { Key } from "@shivaedev/changes/keys.ts";
|
|
3
|
+
import { checkQuery, type Viewer } from "#readPolicy.ts";
|
|
4
|
+
import type { QueryRequest } from "#serverMetadata.ts";
|
|
5
|
+
|
|
6
|
+
export interface QuerySubscription {
|
|
7
|
+
readonly authorized: Effect.Effect<boolean>;
|
|
8
|
+
readonly keys: readonly Key[];
|
|
9
|
+
readonly scopes: readonly (readonly Key[])[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const querySubscription = Effect.fnUntraced(function* (queries: readonly QueryRequest[], viewer: Option.Option<Viewer>) {
|
|
13
|
+
yield* Effect.forEach(queries, ({ query, payload }) => checkQuery(query, payload, viewer));
|
|
14
|
+
const context = Context.makeUnsafe<unknown>((yield* Effect.context<never>()).mapUnsafe);
|
|
15
|
+
const scopes = queries.map(({ query, payload }) => query.reads(payload, viewer));
|
|
16
|
+
const authorized = Effect.map(
|
|
17
|
+
Effect.forEach(queries, ({ query, payload }) => Effect.provideContext(query.readPolicy?.(payload, viewer) ?? Effect.succeed(false), context)),
|
|
18
|
+
(permissions) => permissions.every(Boolean),
|
|
19
|
+
);
|
|
20
|
+
return { authorized, keys: scopes.flat(), scopes } satisfies QuerySubscription;
|
|
21
|
+
});
|
package/src/readPolicy.ts
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { Context,
|
|
1
|
+
import { Context, Effect, Option } from "effect";
|
|
2
2
|
import type { Rpc } from "effect/rpc";
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
3
|
+
import type { QueryShape } from "#operation.ts";
|
|
4
|
+
import { MissingReadPolicy, QueryReadDenied } from "#readPolicyErrors.ts";
|
|
5
|
+
import { QueryDefinition } from "#serverMetadata.ts";
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
export interface Viewer {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
}
|
|
10
|
+
export type QueryReadPolicy<TPayload> = (payload: TPayload, viewer: Option.Option<Viewer>) => Effect.Effect<boolean, never, unknown>;
|
|
9
11
|
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
+
export const CurrentViewer = Context.Reference<Option.Option<Viewer>>("@shivaedev/contract/CurrentViewer", { defaultValue: Option.none });
|
|
13
|
+
export const checkQuery = Effect.fnUntraced(function* (query: QueryShape, payload: unknown, viewer: Option.Option<Viewer>) {
|
|
14
|
+
if (query.readPolicy === undefined) {
|
|
15
|
+
return yield* new MissingReadPolicy({ message: `Declare a readPolicy on query ${query.name} before running it live or subscribing` });
|
|
16
|
+
}
|
|
17
|
+
const context = Context.makeUnsafe<unknown>((yield* Effect.context<never>()).mapUnsafe);
|
|
18
|
+
if (!(yield* Effect.provideContext(query.readPolicy(payload, viewer), context))) {
|
|
19
|
+
return yield* new QueryReadDenied({ message: `Read not allowed for query ${query.name}` });
|
|
20
|
+
}
|
|
12
21
|
});
|
|
13
22
|
|
|
14
|
-
export const authorizeReads = Effect.fnUntraced(function* <A, E, R
|
|
23
|
+
export const authorizeReads = Effect.fnUntraced(function* <A, E, R>(
|
|
15
24
|
effect: Effect.Effect<A, E, R>,
|
|
16
25
|
request: { readonly rpc: Rpc.AnyWithProps; readonly payload: unknown },
|
|
17
|
-
|
|
18
|
-
denied: Effect.Effect<never, TDenied>,
|
|
26
|
+
viewer: Option.Option<Viewer>,
|
|
19
27
|
) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
const reads = Context.getOption(request.rpc.annotations, QueryReads);
|
|
25
|
-
if (Option.isSome(reads)) {
|
|
26
|
-
for (const key of reads.value(request.payload)) {
|
|
27
|
-
if (!(yield* canRead(key))) {
|
|
28
|
-
return yield* denied;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
28
|
+
const definition = Context.getOption(request.rpc.annotations, QueryDefinition);
|
|
29
|
+
if (Option.isSome(definition) && definition.value.definition.readPolicy !== undefined) {
|
|
30
|
+
yield* checkQuery(definition.value.definition, request.payload, viewer);
|
|
31
31
|
}
|
|
32
|
-
return yield* Effect.provideService(effect,
|
|
32
|
+
return yield* Effect.provideService(effect, CurrentViewer, viewer);
|
|
33
33
|
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class MissingReadPolicy extends Schema.TaggedError<MissingReadPolicy>()("MissingReadPolicy", { message: Schema.String }) {}
|
|
4
|
+
export class QueryReadDenied extends Schema.TaggedError<QueryReadDenied>()("QueryReadDenied", { message: Schema.String }) {}
|
package/src/serverMetadata.ts
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import { Context, Schema } from "effect";
|
|
1
|
+
import { Context, Effect, Option, Schema } from "effect";
|
|
2
2
|
import { Rpc } from "effect/rpc";
|
|
3
|
-
import type { Key } from "
|
|
3
|
+
import type { Key } from "@shivaedev/changes/keys.ts";
|
|
4
4
|
import { LiveHint } from "#live.ts";
|
|
5
5
|
import type { CommandShape, QueryShape } from "#operation.ts";
|
|
6
|
+
import type { Viewer } from "#readPolicy.ts";
|
|
7
|
+
import { MissingReadPolicy, QueryReadDenied } from "#readPolicyErrors.ts";
|
|
6
8
|
|
|
7
|
-
export class
|
|
9
|
+
export class QueryDefinition extends Context.Service<QueryDefinition, { readonly definition: QueryShape }>()("@shivaedev/contract/QueryDefinition") {}
|
|
10
|
+
export interface QueryRequest {
|
|
11
|
+
readonly payload: unknown;
|
|
12
|
+
readonly query: QueryShape;
|
|
13
|
+
}
|
|
14
|
+
export class ChangesQueries extends Context.Service<
|
|
15
|
+
ChangesQueries,
|
|
16
|
+
{
|
|
17
|
+
readonly resolve: (references: readonly QueryReference[]) => Effect.Effect<readonly QueryRequest[]>;
|
|
18
|
+
}
|
|
19
|
+
>()("@shivaedev/contract/ChangesQueries") {}
|
|
8
20
|
|
|
9
|
-
export class CommandInvalidation extends Context.Service<CommandInvalidation, CommandShape["invalidates"]>()(
|
|
21
|
+
export class CommandInvalidation extends Context.Service<CommandInvalidation, { readonly invalidates: CommandShape["invalidates"] }>()(
|
|
10
22
|
"@shivaedev/contract/CommandInvalidation",
|
|
11
23
|
) {}
|
|
12
24
|
|
|
@@ -21,20 +33,22 @@ type ReferenceSchema<TQuery extends QueryShape> = TQuery extends QueryShape
|
|
|
21
33
|
? Schema.Struct<{ query: Schema.Literal<TQuery["name"]>; payload: TQuery["payload"] }>
|
|
22
34
|
: never;
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
export function queryKeys(queries: readonly QueryShape[], requested: readonly QueryReference[]): readonly Key[] {
|
|
29
|
-
return requested.flatMap((reference) => {
|
|
30
|
-
const declared = queries.find((candidate) => candidate.name === reference.query);
|
|
31
|
-
if (declared === undefined) {
|
|
32
|
-
throw new Error(`Unknown query: ${reference.query}`);
|
|
33
|
-
}
|
|
34
|
-
return declared.reads(reference.payload);
|
|
36
|
+
function resolveQueries(queries: readonly QueryShape[], requested: readonly QueryReference[]) {
|
|
37
|
+
return Effect.forEach(requested, (reference) => {
|
|
38
|
+
const query = queries.find((candidate) => candidate.name === reference.query);
|
|
39
|
+
return query === undefined ? Effect.die(new Error(`Unknown query: ${reference.query}`)) : Effect.succeed({ payload: reference.payload, query });
|
|
35
40
|
});
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
export const queryKeys = Effect.fnUntraced(function* (
|
|
44
|
+
queries: readonly QueryShape[],
|
|
45
|
+
requested: readonly QueryReference[],
|
|
46
|
+
viewer: Option.Option<Viewer> = Option.none(),
|
|
47
|
+
): Effect.fn.Return<readonly Key[]> {
|
|
48
|
+
const resolved = yield* resolveQueries(queries, requested);
|
|
49
|
+
return resolved.flatMap(({ query, payload }) => query.reads(payload, viewer));
|
|
50
|
+
});
|
|
51
|
+
|
|
38
52
|
function referenceSchemas<const TQueries extends readonly QueryShape[]>(
|
|
39
53
|
queries: TQueries,
|
|
40
54
|
): Schema.$Array<Schema.Union<readonly ReferenceSchema<TQueries[number]>[]>>;
|
|
@@ -43,8 +57,10 @@ function referenceSchemas(queries: readonly QueryShape[]) {
|
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
export function changesRpc<const TName extends string, const TQueries extends readonly QueryShape[]>(name: TName, queries: TQueries) {
|
|
46
|
-
return Rpc.make(`${name}.$changes`, {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
return Rpc.make(`${name}.$changes`, {
|
|
61
|
+
error: Schema.Union([MissingReadPolicy, QueryReadDenied]),
|
|
62
|
+
payload: { queries: referenceSchemas(queries) },
|
|
63
|
+
stream: true,
|
|
64
|
+
success: LiveHint,
|
|
65
|
+
}).annotate(ChangesQueries, { resolve: (references) => resolveQueries(queries, references) });
|
|
50
66
|
}
|
package/src/testing.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Deferred, type Duration, Effect, Fiber, Stream, SubscriptionRef } from "effect";
|
|
2
|
-
import type { Key } from "
|
|
2
|
+
import type { Key } from "@shivaedev/changes/keys.ts";
|
|
3
3
|
import type { LiveHint } from "./live.ts";
|
|
4
4
|
|
|
5
5
|
export interface HintViewer<E> {
|
|
@@ -37,15 +37,5 @@ export const heard = Effect.fn("Live.heard")(function* <E>(
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
function namesKeys(hint: LiveHint, keys: readonly Key[]): boolean {
|
|
40
|
-
return (
|
|
41
|
-
hint._tag === "Changed"
|
|
42
|
-
&& keys.every((key) =>
|
|
43
|
-
hint.keys.some(
|
|
44
|
-
(actual) =>
|
|
45
|
-
actual._tag === key._tag
|
|
46
|
-
&& actual.collection === key.collection
|
|
47
|
-
&& (key._tag === "List" || (actual._tag === "Item" && actual.id === key.id)),
|
|
48
|
-
),
|
|
49
|
-
)
|
|
50
|
-
);
|
|
40
|
+
return hint._tag === "Changed" && keys.every((key) => hint.keys.some((actual) => JSON.stringify(actual) === JSON.stringify(key)));
|
|
51
41
|
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import type { Key } from "#keys.ts";
|
|
3
|
-
export declare const makeHintPolicy: (capacity: number, retry: (keys: readonly Key[]) => Effect.Effect<void>) => Effect.Effect<(keys: readonly Key[]) => Effect.Effect<Key[], never, never>, never, import("effect/Scope").Scope>;
|
|
4
|
-
//# sourceMappingURL=makeHintPolicy.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"makeHintPolicy.d.ts","sourceRoot":"","sources":["../../src/internal/makeHintPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,MAAM,EAAe,MAAM,QAAQ,CAAC;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAGpC,eAAO,MAAM,cAAc,mCAAgE,SAAS,GAAG,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,0BAqBlH,SAAS,GAAG,EAAE,6EAc3B,CAAC"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Cache, Data, Effect, Exit, Queue } from "effect";
|
|
2
|
-
import { ReadPolicy, ReadPolicyMemoWindow } from "#readPolicy.ts";
|
|
3
|
-
export const makeHintPolicy = Effect.fnUntraced(function* (capacity, retry) {
|
|
4
|
-
const policy = yield* ReadPolicy;
|
|
5
|
-
const window = yield* ReadPolicyMemoWindow;
|
|
6
|
-
const cache = yield* Cache.makeWith(policy, {
|
|
7
|
-
capacity: Math.max(1, capacity),
|
|
8
|
-
timeToLive: (exit) => (Exit.isSuccess(exit) ? window : 0),
|
|
9
|
-
});
|
|
10
|
-
const failed = new Map();
|
|
11
|
-
const wake = yield* Queue.unbounded();
|
|
12
|
-
yield* Queue.take(wake).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(Effect.suspend(() => {
|
|
13
|
-
const keys = [...failed.values()];
|
|
14
|
-
failed.clear();
|
|
15
|
-
return retry(keys);
|
|
16
|
-
})), Effect.forever, Effect.forkScoped);
|
|
17
|
-
return (keys) => Effect.filter(keys, (key) => Cache.get(cache, new Data.Class(key)).pipe(Effect.catch(() => Effect.gen(function* () {
|
|
18
|
-
if (failed.size === 0) {
|
|
19
|
-
yield* Queue.offer(wake, undefined);
|
|
20
|
-
}
|
|
21
|
-
failed.set(JSON.stringify(key), key);
|
|
22
|
-
return false;
|
|
23
|
-
}))));
|
|
24
|
-
});
|
|
25
|
-
//# sourceMappingURL=makeHintPolicy.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"makeHintPolicy.js","sourceRoot":"","sources":["../../src/internal/makeHintPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAE1D,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,QAAgB,EAAE,KAAoD;IAChI,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC;IACjC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,oBAAoB,CAAC;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE;QAC3C,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC;QAC/B,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;KACzD,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,GAAG,EAAe,CAAC;IACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,EAAQ,CAAC;IAC5C,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAC3B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,EACxC,MAAM,CAAC,OAAO,CACb,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;QACnB,MAAM,IAAI,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QAClC,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;IACpB,CAAC,CAAC,CACF,EACD,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,UAAU,CACjB,CAAC;IACF,OAAO,CAAC,IAAoB,EAAE,EAAE,CAC/B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAC3B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC,KAAK,CAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAC9C,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CACjB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;QACrC,OAAO,KAAK,CAAC;IACd,CAAC,CAAC,CACF,CACD,CACD,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
package/dist/keys.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
export type Identity = string | number;
|
|
3
|
-
export interface ListKey<TName extends string = string> {
|
|
4
|
-
readonly _tag: "List";
|
|
5
|
-
readonly collection: TName;
|
|
6
|
-
}
|
|
7
|
-
export interface ItemKey<TName extends string = string> {
|
|
8
|
-
readonly _tag: "Item";
|
|
9
|
-
readonly collection: TName;
|
|
10
|
-
readonly id: Identity;
|
|
11
|
-
}
|
|
12
|
-
export type Key = ListKey | ItemKey;
|
|
13
|
-
export declare const Key: Schema.Union<readonly [Schema.Struct<{
|
|
14
|
-
readonly _tag: Schema.Literal<"List">;
|
|
15
|
-
readonly collection: Schema.String;
|
|
16
|
-
}>, Schema.Struct<{
|
|
17
|
-
readonly _tag: Schema.Literal<"Item">;
|
|
18
|
-
readonly collection: Schema.String;
|
|
19
|
-
readonly id: Schema.Union<readonly [Schema.String, Schema.Number]>;
|
|
20
|
-
}>]>;
|
|
21
|
-
export interface Collection<TName extends string, TId extends Identity> {
|
|
22
|
-
readonly item: (id: TId) => ItemKey<TName>;
|
|
23
|
-
readonly list: ListKey<TName>;
|
|
24
|
-
readonly name: TName;
|
|
25
|
-
}
|
|
26
|
-
export declare function collection<const TName extends string, TId extends Schema.Top & {
|
|
27
|
-
readonly Type: Identity;
|
|
28
|
-
}>(name: TName, _id: TId): Collection<TName, TId["Type"]>;
|
|
29
|
-
export interface KeyContext {
|
|
30
|
-
readonly scope: string;
|
|
31
|
-
}
|
|
32
|
-
export declare function readKeys(keys: readonly Key[], context?: KeyContext): readonly string[];
|
|
33
|
-
export declare function invalidationKeys(keys: readonly Key[], context?: KeyContext): readonly string[];
|
|
34
|
-
//# sourceMappingURL=keys.d.ts.map
|
package/dist/keys.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,MAAM,WAAW,OAAO,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;CAC3B;AAED,MAAM,WAAW,OAAO,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;IAC3B,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;CACtB;AAED,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,OAAO,CAAC;AAEpC,eAAO,MAAM,GAAG;;;;;;;IAGd,CAAC;AAEH,MAAM,WAAW,UAAU,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,SAAS,QAAQ;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;CACrB;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,CAAC,GAAG,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,EAC1G,IAAI,EAAE,KAAK,EACX,GAAG,EAAE,GAAG,GACN,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAMhC;AAED,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAUD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,CAItF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE,CAI9F"}
|