@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +23 -23
  3. package/dist/automaticLive.d.ts +8 -7
  4. package/dist/automaticLive.d.ts.map +1 -1
  5. package/dist/automaticLive.js +101 -51
  6. package/dist/automaticLive.js.map +1 -1
  7. package/dist/bind.d.ts +1 -0
  8. package/dist/bind.d.ts.map +1 -1
  9. package/dist/bind.js +4 -4
  10. package/dist/bind.js.map +1 -1
  11. package/dist/clientOptions.d.ts +6 -5
  12. package/dist/clientOptions.d.ts.map +1 -1
  13. package/dist/internal/clientOperations.d.ts +2 -2
  14. package/dist/internal/clientOperations.d.ts.map +1 -1
  15. package/dist/internal/clientOperations.js +10 -16
  16. package/dist/internal/clientOperations.js.map +1 -1
  17. package/dist/internal/queryReactivity.d.ts +1 -1
  18. package/dist/internal/queryReactivity.d.ts.map +1 -1
  19. package/dist/internal/queryReactivity.js +1 -2
  20. package/dist/internal/queryReactivity.js.map +1 -1
  21. package/dist/internal/queryResult.d.ts +3 -0
  22. package/dist/internal/queryResult.d.ts.map +1 -0
  23. package/dist/internal/queryResult.js +19 -0
  24. package/dist/internal/queryResult.js.map +1 -0
  25. package/dist/internal/{httpClient.d.ts → rpcClient.d.ts} +6 -4
  26. package/dist/internal/rpcClient.d.ts.map +1 -0
  27. package/dist/internal/rpcClient.js +31 -0
  28. package/dist/internal/rpcClient.js.map +1 -0
  29. package/dist/live.d.ts +1 -0
  30. package/dist/live.d.ts.map +1 -1
  31. package/dist/live.js +14 -1
  32. package/dist/live.js.map +1 -1
  33. package/dist/makeLiveServer.js +6 -4
  34. package/dist/makeLiveServer.js.map +1 -1
  35. package/package.json +3 -3
  36. package/src/automaticLive.ts +113 -70
  37. package/src/bind.ts +7 -6
  38. package/src/clientOptions.ts +6 -5
  39. package/src/internal/clientOperations.ts +14 -27
  40. package/src/internal/queryReactivity.ts +1 -7
  41. package/src/internal/queryResult.ts +19 -0
  42. package/src/internal/rpcClient.ts +53 -0
  43. package/src/live.ts +14 -1
  44. package/src/makeLiveServer.ts +6 -6
  45. package/dist/internal/httpClient.d.ts.map +0 -1
  46. package/dist/internal/httpClient.js +0 -13
  47. package/dist/internal/httpClient.js.map +0 -1
  48. package/src/internal/httpClient.ts +0 -21
@@ -1,4 +1,4 @@
1
- import { Effect, MutableHashMap, Option, type Schema, Stream } from "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 identity = { payload, tag };
47
- let id = Option.getOrUndefined(MutableHashMap.get(queryIds, identity));
48
- if (id === undefined) {
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}/${id}` }];
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
- if (owner === undefined) {
59
- track(undefined, reference, keys);
60
- }
61
- return reactive(
62
- atom,
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
- resync();
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(
@@ -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
- 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
- );
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
- }