@shivaedev/contract 0.19.1 → 0.20.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 +13 -0
- package/README.md +23 -23
- package/dist/automaticLive.d.ts +8 -7
- package/dist/automaticLive.d.ts.map +1 -1
- package/dist/automaticLive.js +101 -51
- package/dist/automaticLive.js.map +1 -1
- package/dist/bind.d.ts +1 -0
- package/dist/bind.d.ts.map +1 -1
- package/dist/bind.js +4 -4
- package/dist/bind.js.map +1 -1
- package/dist/clientOptions.d.ts +6 -5
- package/dist/clientOptions.d.ts.map +1 -1
- package/dist/internal/clientOperations.d.ts +2 -2
- package/dist/internal/clientOperations.d.ts.map +1 -1
- package/dist/internal/clientOperations.js +10 -16
- package/dist/internal/clientOperations.js.map +1 -1
- package/dist/internal/queryReactivity.d.ts +1 -1
- package/dist/internal/queryReactivity.d.ts.map +1 -1
- package/dist/internal/queryReactivity.js +1 -2
- package/dist/internal/queryReactivity.js.map +1 -1
- package/dist/internal/queryResult.d.ts +3 -0
- package/dist/internal/queryResult.d.ts.map +1 -0
- package/dist/internal/queryResult.js +19 -0
- package/dist/internal/queryResult.js.map +1 -0
- package/dist/internal/{httpClient.d.ts → rpcClient.d.ts} +6 -4
- package/dist/internal/rpcClient.d.ts.map +1 -0
- package/dist/internal/rpcClient.js +31 -0
- package/dist/internal/rpcClient.js.map +1 -0
- package/dist/live.d.ts +1 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +14 -1
- package/dist/live.js.map +1 -1
- package/dist/makeLiveServer.js +6 -4
- package/dist/makeLiveServer.js.map +1 -1
- package/package.json +3 -3
- package/src/automaticLive.ts +113 -70
- package/src/bind.ts +7 -6
- package/src/clientOptions.ts +6 -5
- package/src/internal/clientOperations.ts +14 -27
- package/src/internal/queryReactivity.ts +1 -7
- package/src/internal/queryResult.ts +19 -0
- package/src/internal/rpcClient.ts +53 -0
- package/src/live.ts +14 -1
- package/src/makeLiveServer.ts +6 -6
- package/dist/internal/httpClient.d.ts.map +0 -1
- package/dist/internal/httpClient.js +0 -13
- package/dist/internal/httpClient.js.map +0 -1
- package/src/internal/httpClient.ts +0 -21
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect,
|
|
1
|
+
import { Effect, Option, 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";
|
|
@@ -13,10 +13,6 @@ import { queryReactivity } from "./queryReactivity.ts";
|
|
|
13
13
|
|
|
14
14
|
type ErasedClient = AtomRpc.AtomRpcClient<unknown, string, Rpc.Rpc<string, Schema.Top, Schema.Top, Schema.Top>>;
|
|
15
15
|
|
|
16
|
-
// AtomRpc holds query atoms weakly, so an id per atom object would change after a collection and make live tracking resubscribe.
|
|
17
|
-
const queryIds = MutableHashMap.empty<{ readonly payload: unknown; readonly tag: string }, number>();
|
|
18
|
-
let nextQueryId = 0;
|
|
19
|
-
|
|
20
16
|
type ErasedStreamingClient = AtomRpc.AtomRpcClient<
|
|
21
17
|
unknown,
|
|
22
18
|
string,
|
|
@@ -27,14 +23,9 @@ function run<A, E>(service: ErasedClient, effect: Effect.Effect<A, E, unknown>,
|
|
|
27
23
|
return owner === undefined ? effect : Effect.suspend(() => AtomRegistry.getResult(owner.registry, service.runtime.atom(effect)));
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
export function boundQuery(
|
|
31
|
-
service: ErasedClient,
|
|
32
|
-
tag: string,
|
|
33
|
-
query: QueryShape,
|
|
34
|
-
track: ReturnType<typeof automaticLive>["track"],
|
|
35
|
-
owner?: BindingOwner,
|
|
36
|
-
) {
|
|
26
|
+
export function boundQuery(service: ErasedClient, tag: string, query: QueryShape, automatic: ReturnType<typeof automaticLive>, owner?: BindingOwner) {
|
|
37
27
|
const reactive = queryReactivity(service.runtime);
|
|
28
|
+
const cached = new WeakMap<ReturnType<typeof service.query>, ReturnType<typeof service.query>>();
|
|
38
29
|
return {
|
|
39
30
|
query: (payload: unknown, options: QueryOptions = {}) => {
|
|
40
31
|
if (query.readPolicy === undefined) {
|
|
@@ -43,27 +34,23 @@ export function boundQuery(
|
|
|
43
34
|
);
|
|
44
35
|
}
|
|
45
36
|
const atom = service.query(tag, payload, options);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
id = nextQueryId;
|
|
50
|
-
nextQueryId += 1;
|
|
51
|
-
MutableHashMap.set(queryIds, identity, id);
|
|
37
|
+
const existing = cached.get(atom);
|
|
38
|
+
if (existing !== undefined) {
|
|
39
|
+
return existing;
|
|
52
40
|
}
|
|
53
41
|
const local = query.reads(payload, Option.none());
|
|
54
|
-
const keys = [...local, { _tag: "List" as const, collection: `@query/${tag}/${
|
|
42
|
+
const keys = [...local, { _tag: "List" as const, collection: `@query/${tag}/${crypto.randomUUID()}` }];
|
|
55
43
|
const dependencies =
|
|
56
44
|
local.length === 0 ? [...keys, { _tag: "List" as const, collection: `@query/${tag.slice(0, tag.lastIndexOf("."))}` }] : keys;
|
|
57
45
|
const reference = { payload, query: query.name };
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
readKeys(dependencies, owner?.context),
|
|
64
|
-
(registry) => (owner === undefined ? () => undefined : track(registry, reference, keys)),
|
|
65
|
-
owner?.registry,
|
|
46
|
+
const result = automatic.query(
|
|
47
|
+
reactive(atom, readKeys(dependencies, owner?.context), owner?.registry),
|
|
48
|
+
reference,
|
|
49
|
+
keys,
|
|
50
|
+
Schema.toEquivalence(query.success),
|
|
66
51
|
);
|
|
52
|
+
cached.set(atom, result);
|
|
53
|
+
return result;
|
|
67
54
|
},
|
|
68
55
|
run: (payload: unknown) =>
|
|
69
56
|
run(
|
|
@@ -10,12 +10,7 @@ export function queryReactivity<R, E>(runtime: Atom.AtomRuntime<R, E>) {
|
|
|
10
10
|
const service = runtime.atom(Effect.service(Reactivity.Reactivity));
|
|
11
11
|
type Query = Atom.Atom<AsyncResult.AsyncResult<unknown, unknown>>;
|
|
12
12
|
const cached = new WeakMap<Query, Query>();
|
|
13
|
-
return (
|
|
14
|
-
atom: Query,
|
|
15
|
-
keys: readonly string[],
|
|
16
|
-
track: (registry: AtomRegistry.AtomRegistry) => () => void,
|
|
17
|
-
owner?: AtomRegistry.AtomRegistry,
|
|
18
|
-
): Query => {
|
|
13
|
+
return (atom: Query, keys: readonly string[], owner?: AtomRegistry.AtomRegistry): Query => {
|
|
19
14
|
const existing = cached.get(atom);
|
|
20
15
|
if (existing !== undefined) {
|
|
21
16
|
return existing;
|
|
@@ -35,7 +30,6 @@ export function queryReactivity<R, E>(runtime: Atom.AtomRuntime<R, E>) {
|
|
|
35
30
|
return get(source);
|
|
36
31
|
}
|
|
37
32
|
const reactivity = context.value;
|
|
38
|
-
get.addFinalizer(track(get.registry));
|
|
39
33
|
let pending: (() => boolean) | undefined;
|
|
40
34
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
41
35
|
function clear() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
2
|
+
|
|
3
|
+
export function queryResult<A, E>(equivalent: (left: A, right: A) => boolean, publish: (value: AsyncResult.AsyncResult<A, E>) => void) {
|
|
4
|
+
let settled: AsyncResult.Success<A, E> | undefined;
|
|
5
|
+
return (value: AsyncResult.AsyncResult<A, E>) => {
|
|
6
|
+
if (!AsyncResult.isSuccess(value)) {
|
|
7
|
+
publish(value);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (settled !== undefined && equivalent(settled.value, value.value)) {
|
|
11
|
+
publish(value.waiting ? AsyncResult.waiting(settled) : settled);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!value.waiting) {
|
|
15
|
+
settled = value;
|
|
16
|
+
}
|
|
17
|
+
publish(value);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Context, Effect, Layer } from "effect";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
4
|
+
import { type Rpc, RpcClient, RpcGroup, RpcSerialization } from "effect/rpc";
|
|
5
|
+
import * as Socket from "effect/socket/Socket";
|
|
6
|
+
import type { ClientOptions } from "#clientOptions.ts";
|
|
7
|
+
|
|
8
|
+
export interface RpcSource {
|
|
9
|
+
readonly annotations: Context.Context<never>;
|
|
10
|
+
readonly requests: ReadonlyMap<string, Rpc.Any>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function rpcClient(name: string, source: RpcSource, options: ClientOptions) {
|
|
14
|
+
const group = RpcGroup.make(...source.requests.values()).annotateMerge(source.annotations);
|
|
15
|
+
function sharedProtocol(identity?: string) {
|
|
16
|
+
return socketProtocol({
|
|
17
|
+
headers: options.headers,
|
|
18
|
+
identity,
|
|
19
|
+
url: options.url.replace(/^http/u, "ws"),
|
|
20
|
+
webSocketConstructor: options.webSocketConstructor,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const protocol = options.protocol ?? sharedProtocol();
|
|
24
|
+
const Client = AtomRpc.Service<unknown>()(`${name}/Client`, {
|
|
25
|
+
group,
|
|
26
|
+
protocol: options.protocol ?? ((get) => sharedProtocol(get(socketIdentity))),
|
|
27
|
+
});
|
|
28
|
+
const client = Effect.flatMap(Layer.build(protocol), (context) => RpcClient.make(group).pipe(Effect.provideContext(context)));
|
|
29
|
+
const layer = Layer.effect(Client, RpcClient.make(group, { flatten: true })).pipe(Layer.provide(protocol));
|
|
30
|
+
return Object.assign(layer, { Client, client, runtime: Client.runtime });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const socketIdentity = Atom.make<string | undefined>(undefined);
|
|
34
|
+
|
|
35
|
+
const socketMemoMap = Layer.makeMemoMapUnsafe();
|
|
36
|
+
const socketProtocol = Atom.family(
|
|
37
|
+
(options: {
|
|
38
|
+
readonly url: string;
|
|
39
|
+
readonly headers: ClientOptions["headers"];
|
|
40
|
+
readonly identity: string | undefined;
|
|
41
|
+
readonly webSocketConstructor: ClientOptions["webSocketConstructor"];
|
|
42
|
+
}) => {
|
|
43
|
+
const protocol = RpcClient.layerProtocolSocket().pipe(
|
|
44
|
+
Layer.provide(
|
|
45
|
+
Socket.layerWebSocket(options.url, { headers: options.headers }).pipe(
|
|
46
|
+
Layer.provide(options.webSocketConstructor ?? Socket.layerWebSocketConstructorGlobal),
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
Layer.provide(RpcSerialization.layerNdjson),
|
|
50
|
+
);
|
|
51
|
+
return Layer.effectContext(Effect.flatMap(Effect.scope, (scope) => Layer.buildWithMemoMap(protocol, socketMemoMap, scope)));
|
|
52
|
+
},
|
|
53
|
+
);
|
package/src/live.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface LiveStatus<E> {
|
|
|
18
18
|
}
|
|
19
19
|
export interface LiveOptions<E, R> {
|
|
20
20
|
readonly coalesceWindow?: Duration.Input;
|
|
21
|
+
readonly initialResync?: boolean;
|
|
21
22
|
readonly keyContext?: KeyContext | undefined;
|
|
22
23
|
readonly resume?: Atom.Atom<unknown>;
|
|
23
24
|
readonly resyncKeys: readonly Key[] | ((registry: AtomRegistry.AtomRegistry) => readonly Key[]);
|
|
@@ -50,6 +51,9 @@ export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeE
|
|
|
50
51
|
get.addFinalizer(() => {
|
|
51
52
|
active = false;
|
|
52
53
|
clear();
|
|
54
|
+
if (get.registry.getNodes().has(status)) {
|
|
55
|
+
update({ connection: "connecting", failure: Option.none(), needsResync: true, pending: 0 });
|
|
56
|
+
}
|
|
53
57
|
});
|
|
54
58
|
function changed(changedKeys: readonly Key[]) {
|
|
55
59
|
const window = get.once(coalesceWindow);
|
|
@@ -93,6 +97,14 @@ export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeE
|
|
|
93
97
|
get.once(options.resume);
|
|
94
98
|
get.subscribe(options.resume, resync);
|
|
95
99
|
}
|
|
100
|
+
let connected = false;
|
|
101
|
+
function reconcile() {
|
|
102
|
+
if (connected || options.initialResync !== false) {
|
|
103
|
+
resync();
|
|
104
|
+
} else {
|
|
105
|
+
update({ needsResync: false });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
96
108
|
const attempt = Effect.suspend(() => {
|
|
97
109
|
let isFirst = true;
|
|
98
110
|
function observe(hint: LiveHint) {
|
|
@@ -100,10 +112,11 @@ export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeE
|
|
|
100
112
|
if (get.once(paused)) {
|
|
101
113
|
update({ needsResync: true, pending: Math.min(MAX_PENDING, get.once(status).pending + 1) });
|
|
102
114
|
} else if (isFirst || hint._tag === "Resync") {
|
|
103
|
-
|
|
115
|
+
reconcile();
|
|
104
116
|
} else {
|
|
105
117
|
changed(hint.keys);
|
|
106
118
|
}
|
|
119
|
+
connected = true;
|
|
107
120
|
isFirst = false;
|
|
108
121
|
}
|
|
109
122
|
return options.stream.pipe(
|
package/src/makeLiveServer.ts
CHANGED
|
@@ -56,12 +56,12 @@ export const makeLiveServer = Effect.gen(function* () {
|
|
|
56
56
|
);
|
|
57
57
|
yield* Queue.offer(queue, { _tag: "Resync" });
|
|
58
58
|
function authorizeHint(hint: LiveHint) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
59
|
+
return Effect.map(subscription?.authorized ?? Effect.succeed(true), (allowed) => {
|
|
60
|
+
if (!allowed) {
|
|
61
|
+
return changedHint([], subscription?.scopes);
|
|
62
|
+
}
|
|
63
|
+
return hint._tag === "Resync" ? hint : changedHint(hint.keys, subscription?.scopes);
|
|
64
|
+
});
|
|
65
65
|
}
|
|
66
66
|
return Stream.fromQueue(queue).pipe(
|
|
67
67
|
Stream.mapEffect(authorizeHint),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"httpClient.d.ts","sourceRoot":"","sources":["../../src/internal/httpClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAErD,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,KAAK,GAAG,EAAyC,MAAM,YAAY,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;CAChD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa;;;;;;;;;;;EASjF"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Effect, Layer } from "effect";
|
|
2
|
-
import { FetchHttpClient } from "effect/http";
|
|
3
|
-
import * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
4
|
-
import { RpcClient, RpcGroup, RpcSerialization } from "effect/rpc";
|
|
5
|
-
export function httpClient(name, source, options) {
|
|
6
|
-
const group = RpcGroup.make(...source.requests.values()).annotateMerge(source.annotations);
|
|
7
|
-
const protocol = RpcClient.layerProtocolHttp(options).pipe(Layer.provide([options.httpClient ?? FetchHttpClient.layer, options.serialization ?? RpcSerialization.layerNdjson]));
|
|
8
|
-
const Client = AtomRpc.Service()(`${name}/Client`, { group, protocol });
|
|
9
|
-
const client = RpcClient.make(group).pipe(Effect.provide(protocol));
|
|
10
|
-
const layer = Layer.effect(Client, RpcClient.make(group, { flatten: true })).pipe(Layer.provide(protocol));
|
|
11
|
-
return Object.assign(layer, { Client, client, runtime: Client.runtime });
|
|
12
|
-
}
|
|
13
|
-
//# sourceMappingURL=httpClient.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"httpClient.js","sourceRoot":"","sources":["../../src/internal/httpClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAY,SAAS,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAQ7E,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,MAAiB,EAAE,OAAsB;IACjF,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC3F,MAAM,QAAQ,GAAG,SAAS,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,IAAI,CACzD,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,UAAU,IAAI,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,aAAa,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAC,CACnH,CAAC;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,EAAW,CAAC,GAAG,IAAI,SAAS,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IACjF,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC3G,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;AAC1E,CAAC"}
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|