@shivaedev/contract 0.18.0 → 0.19.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 +33 -0
- package/README.md +168 -178
- 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 +28 -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 +32 -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
package/dist/serverMetadata.js
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
import { Context, Schema } from "effect";
|
|
1
|
+
import { Context, Effect, Option, Schema } from "effect";
|
|
2
2
|
import { Rpc } from "effect/rpc";
|
|
3
3
|
import { LiveHint } from "#live.ts";
|
|
4
|
-
|
|
4
|
+
import { MissingReadPolicy, QueryReadDenied } from "#readPolicyErrors.ts";
|
|
5
|
+
export class QueryDefinition extends Context.Service()("@shivaedev/contract/QueryDefinition") {
|
|
6
|
+
}
|
|
7
|
+
export class ChangesQueries extends Context.Service()("@shivaedev/contract/ChangesQueries") {
|
|
5
8
|
}
|
|
6
9
|
export class CommandInvalidation extends Context.Service()("@shivaedev/contract/CommandInvalidation") {
|
|
7
10
|
}
|
|
8
11
|
export class KeyScope extends Context.Service()("@shivaedev/contract/KeyScope") {
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const declared = queries.find((candidate) => candidate.name === reference.query);
|
|
15
|
-
if (declared === undefined) {
|
|
16
|
-
throw new Error(`Unknown query: ${reference.query}`);
|
|
17
|
-
}
|
|
18
|
-
return declared.reads(reference.payload);
|
|
13
|
+
function resolveQueries(queries, requested) {
|
|
14
|
+
return Effect.forEach(requested, (reference) => {
|
|
15
|
+
const query = queries.find((candidate) => candidate.name === reference.query);
|
|
16
|
+
return query === undefined ? Effect.die(new Error(`Unknown query: ${reference.query}`)) : Effect.succeed({ payload: reference.payload, query });
|
|
19
17
|
});
|
|
20
18
|
}
|
|
19
|
+
export const queryKeys = Effect.fnUntraced(function* (queries, requested, viewer = Option.none()) {
|
|
20
|
+
const resolved = yield* resolveQueries(queries, requested);
|
|
21
|
+
return resolved.flatMap(({ query, payload }) => query.reads(payload, viewer));
|
|
22
|
+
});
|
|
21
23
|
function referenceSchemas(queries) {
|
|
22
24
|
return Schema.Array(Schema.Union(queries.map((query) => Schema.Struct({ payload: query.payload, query: Schema.Literal(query.name) }))));
|
|
23
25
|
}
|
|
24
26
|
export function changesRpc(name, queries) {
|
|
25
|
-
return Rpc.make(`${name}.$changes`, {
|
|
27
|
+
return Rpc.make(`${name}.$changes`, {
|
|
28
|
+
error: Schema.Union([MissingReadPolicy, QueryReadDenied]),
|
|
29
|
+
payload: { queries: referenceSchemas(queries) },
|
|
30
|
+
stream: true,
|
|
31
|
+
success: LiveHint,
|
|
32
|
+
}).annotate(ChangesQueries, { resolve: (references) => resolveQueries(queries, references) });
|
|
26
33
|
}
|
|
27
34
|
//# sourceMappingURL=serverMetadata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverMetadata.js","sourceRoot":"","sources":["../src/serverMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"serverMetadata.js","sourceRoot":"","sources":["../src/serverMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGpC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE1E,MAAM,OAAO,eAAgB,SAAQ,OAAO,CAAC,OAAO,EAAwD,CAAC,qCAAqC,CAAC;CAAG;AAKtJ,MAAM,OAAO,cAAe,SAAQ,OAAO,CAAC,OAAO,EAKhD,CAAC,oCAAoC,CAAC;CAAG;AAE5C,MAAM,OAAO,mBAAoB,SAAQ,OAAO,CAAC,OAAO,EAA8E,CACrI,yCAAyC,CACzC;CAAG;AAEJ,MAAM,OAAO,QAAS,SAAQ,OAAO,CAAC,OAAO,EAAuB,CAAC,8BAA8B,CAAC;CAAG;AAWvG,SAAS,cAAc,CAAC,OAA8B,EAAE,SAAoC;IAC3F,OAAO,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,SAAS,EAAE,EAAE;QAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,SAAS,CAAC,KAAK,CAAC,CAAC;QAC9E,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,kBAAkB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACjJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACnD,OAA8B,EAC9B,SAAoC,EACpC,MAAM,GAA0B,MAAM,CAAC,IAAI,EAAE;IAE7C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC3D,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAC/E,CAAC,CAAC,CAAC;AAKH,SAAS,gBAAgB,CAAC,OAA8B;IACvD,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACzI,CAAC;AAED,MAAM,UAAU,UAAU,CAA2E,IAAW,EAAE,OAAiB;IAClI,OAAO,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,WAAW,EAAE;QACnC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC;QACzD,OAAO,EAAE,EAAE,OAAO,EAAE,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAC/C,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE,QAAQ;KACjB,CAAC,CAAC,QAAQ,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,CAAC,UAAU,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;AAC/F,CAAC"}
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Duration, Effect, 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
|
export interface HintViewer<E> {
|
|
5
5
|
readonly finished: Effect.Effect<void, E>;
|
|
@@ -8,6 +8,11 @@ export interface HintViewer<E> {
|
|
|
8
8
|
export declare const watchHints: <E, R>(stream: Stream.Stream<{
|
|
9
9
|
readonly _tag: "Changed";
|
|
10
10
|
readonly keys: readonly ({
|
|
11
|
+
readonly _tag: "Bucket";
|
|
12
|
+
readonly collection: string;
|
|
13
|
+
readonly field: string;
|
|
14
|
+
readonly value: string | number;
|
|
15
|
+
} | {
|
|
11
16
|
readonly _tag: "List";
|
|
12
17
|
readonly collection: string;
|
|
13
18
|
} | {
|
|
@@ -15,6 +20,7 @@ export declare const watchHints: <E, R>(stream: Stream.Stream<{
|
|
|
15
20
|
readonly collection: string;
|
|
16
21
|
readonly id: string | number;
|
|
17
22
|
})[];
|
|
23
|
+
readonly queryIndexes?: readonly number[] | undefined;
|
|
18
24
|
} | {
|
|
19
25
|
readonly _tag: "Resync";
|
|
20
26
|
}, E, R>) => Effect.Effect<{
|
|
@@ -22,6 +28,11 @@ export declare const watchHints: <E, R>(stream: Stream.Stream<{
|
|
|
22
28
|
hints: SubscriptionRef.SubscriptionRef<readonly ({
|
|
23
29
|
readonly _tag: "Changed";
|
|
24
30
|
readonly keys: readonly ({
|
|
31
|
+
readonly _tag: "Bucket";
|
|
32
|
+
readonly collection: string;
|
|
33
|
+
readonly field: string;
|
|
34
|
+
readonly value: string | number;
|
|
35
|
+
} | {
|
|
25
36
|
readonly _tag: "List";
|
|
26
37
|
readonly collection: string;
|
|
27
38
|
} | {
|
|
@@ -29,6 +40,7 @@ export declare const watchHints: <E, R>(stream: Stream.Stream<{
|
|
|
29
40
|
readonly collection: string;
|
|
30
41
|
readonly id: string | number;
|
|
31
42
|
})[];
|
|
43
|
+
readonly queryIndexes?: readonly number[] | undefined;
|
|
32
44
|
} | {
|
|
33
45
|
readonly _tag: "Resync";
|
|
34
46
|
})[]>;
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,EAAS,MAAM,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACzF,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,EAAS,MAAM,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACzF,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,4BAA4B,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,WAAW,UAAU,CAAC,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC,eAAe,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;CACrE;AAED,eAAO,MAAM,UAAU,GAA2C,CAAC,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uCAYpE,CAAC;AAEH,eAAO,MAAM,KAAK,GAAsC,CAAC;sBAG3B,QAAQ,CAAC,KAAK;kDAU1C,CAAC"}
|
package/dist/testing.js
CHANGED
|
@@ -12,9 +12,6 @@ export const heard = Effect.fn("Live.heard")(function* (viewer, keys, options =
|
|
|
12
12
|
return yield* Effect.raceFirst(matching, Effect.as(viewer.finished, false)).pipe(Effect.timeoutOrElse({ duration: options.within ?? "1 second", orElse: () => Effect.succeed(false) }));
|
|
13
13
|
});
|
|
14
14
|
function namesKeys(hint, keys) {
|
|
15
|
-
return
|
|
16
|
-
&& keys.every((key) => hint.keys.some((actual) => actual._tag === key._tag
|
|
17
|
-
&& actual.collection === key.collection
|
|
18
|
-
&& (key._tag === "List" || (actual._tag === "Item" && actual.id === key.id)))));
|
|
15
|
+
return hint._tag === "Changed" && keys.every((key) => hint.keys.some((actual) => JSON.stringify(actual) === JSON.stringify(key)));
|
|
19
16
|
}
|
|
20
17
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAiB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AASzF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,QAAQ,CAAC,EAAQ,MAAqC;IAC5G,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,IAAI,CAAsB,EAAE,CAAC,CAAC;IACnE,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAQ,CAAC;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAC/B,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,EAAE,CAC1B,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,GAAG,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CACzH,EACD,MAAM,CAAC,UAAU,CACjB,CAAC;IACF,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnC,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAA0B,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,EACrD,MAAqB,EACrB,IAAoB,EACpB,OAAO,GAAyC,EAAE;IAElD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAC1D,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EACrE,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CACf,CAAC;IACF,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAC/E,MAAM,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,IAAI,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CACrG,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,SAAS,SAAS,CAAC,IAAc,EAAE,IAAoB;IACtD,OAAO,
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAiB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AASzF,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,QAAQ,CAAC,EAAQ,MAAqC;IAC5G,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,IAAI,CAAsB,EAAE,CAAC,CAAC;IACnE,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAQ,CAAC;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAC/B,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,EAAE,CAC1B,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,GAAG,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CACzH,EACD,MAAM,CAAC,UAAU,CACjB,CAAC;IACF,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnC,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAA0B,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,EACrD,MAAqB,EACrB,IAAoB,EACpB,OAAO,GAAyC,EAAE;IAElD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAC1D,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EACrE,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CACf,CAAC;IACF,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAC/E,MAAM,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,MAAM,IAAI,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CACrG,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,SAAS,SAAS,CAAC,IAAc,EAAE,IAAoB;IACtD,OAAO,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACnI,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Typed query and command contracts over native Effect RPC and reactivity",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -44,19 +44,20 @@
|
|
|
44
44
|
"README.md"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@shivaedev/changes": "0.5.0",
|
|
47
48
|
"@shivaedev/types": "0.3.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@effect/platform-node": "4.0.5",
|
|
51
52
|
"@effect/vitest": "4.0.5",
|
|
52
|
-
"@shivaedev/quality": "0.
|
|
53
|
+
"@shivaedev/quality": "0.44.0",
|
|
53
54
|
"@shivaedev/test": "0.10.0",
|
|
54
|
-
"@shivaedev/test-sql": "0.10.
|
|
55
|
-
"@shivaedev/test-story": "0.
|
|
55
|
+
"@shivaedev/test-sql": "0.10.2",
|
|
56
|
+
"@shivaedev/test-story": "0.22.0",
|
|
56
57
|
"@types/node": "24.19.2",
|
|
57
58
|
"effect": "4.0.5",
|
|
58
59
|
"esbuild": "0.28.2",
|
|
59
|
-
"happy-dom": "21.0.
|
|
60
|
+
"happy-dom": "21.0.4",
|
|
60
61
|
"typescript": "7.0.2",
|
|
61
62
|
"vitest": "5.0.5"
|
|
62
63
|
},
|
package/src/automaticLive.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { type Duration, Effect, Stream, SubscriptionRef } from "effect";
|
|
|
2
2
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
4
4
|
import type * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
5
|
+
import { type Key, readKeys } from "@shivaedev/changes/keys.ts";
|
|
5
6
|
import type { BindingOwner } from "#internal/bindingOwner.ts";
|
|
6
|
-
import {
|
|
7
|
+
import { localQueryKeys, queryHint } from "#internal/queryHints.ts";
|
|
7
8
|
import { type LiveStatus, live } from "#live.ts";
|
|
8
9
|
import { type ResumeSource, type ResumeWindow, resumeSignal } from "#resume.ts";
|
|
9
10
|
import type { changesRpc, QueryReference } from "#serverMetadata.ts";
|
|
@@ -68,13 +69,17 @@ export function automaticLive(service: ChangesClient, name: string, owner?: Bind
|
|
|
68
69
|
return Stream.never;
|
|
69
70
|
}
|
|
70
71
|
const queries = [...current.values()].map(({ reference }) => reference);
|
|
71
|
-
return Stream.unwrap(
|
|
72
|
+
return Stream.unwrap(
|
|
73
|
+
service.use((client) =>
|
|
74
|
+
Effect.succeed(client(`${name}.$changes`, { queries }).pipe(Stream.map((hint) => queryHint(hint, [...current.values()])))),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
72
77
|
}
|
|
73
78
|
const signal = resumeSignal({ ...(typeof window === "undefined" ? {} : { window }), ...(resume === undefined ? {} : { native: resume }) });
|
|
74
79
|
const subscription = live(service.runtime, {
|
|
75
80
|
keyContext: owner?.context,
|
|
76
81
|
resume: signal,
|
|
77
|
-
resyncKeys: (registry) => [...Effect.runSync(SubscriptionRef.get(registry.get(tracked))).values()].flatMap(({ keys }) => keys),
|
|
82
|
+
resyncKeys: (registry) => [...Effect.runSync(SubscriptionRef.get(registry.get(tracked))).values()].flatMap(({ keys }) => localQueryKeys(keys)),
|
|
78
83
|
stream: Stream.unwrap(
|
|
79
84
|
Effect.map(AtomRegistry.AtomRegistry, (registry) =>
|
|
80
85
|
Stream.zipLatest(SubscriptionRef.changes(registry.get(tracked)), AtomRegistry.toStream(registry, signal)).pipe(
|
package/src/bind.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import type { Duration, Effect, Stream } from "effect";
|
|
1
|
+
import type { Duration, Effect, Layer, Stream } from "effect";
|
|
2
2
|
import type { Headers } from "effect/http";
|
|
3
3
|
import type * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
4
|
import type * as Atom from "effect/reactivity/Atom";
|
|
5
5
|
import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
6
6
|
import type * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
7
7
|
import type * as Reactivity from "effect/reactivity/Reactivity";
|
|
8
|
-
import type { Rpc } from "effect/rpc";
|
|
8
|
+
import type { Rpc, RpcGroup } from "effect/rpc";
|
|
9
9
|
import type { RpcClientError } from "effect/rpc/RpcClientError";
|
|
10
10
|
import { automaticLive, type LiveBound, LiveConnection } from "#automaticLive.ts";
|
|
11
|
+
import type { ClientOptions, MadeClient } from "#clientOptions.ts";
|
|
11
12
|
import type { Contract, Declared, Tag } from "#contract.ts";
|
|
12
13
|
import { bindingOwner } from "#internal/bindingOwner.ts";
|
|
13
14
|
import { boundCommand, boundQuery, boundStream } from "#internal/clientOperations.ts";
|
|
15
|
+
import { type HttpGroup, httpClient } from "#internal/httpClient.ts";
|
|
14
16
|
import type { CommandShape, QueryShape, StreamShape } from "#operation.ts";
|
|
17
|
+
import type { MissingReadPolicy } from "#readPolicyErrors.ts";
|
|
15
18
|
import type { ResumeSource } from "#resume.ts";
|
|
16
19
|
|
|
17
20
|
export type Failure<R extends Rpc.Any> =
|
|
@@ -31,7 +34,10 @@ export interface QueryOptions {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export interface BoundQuery<R extends Rpc.Any, TSelf> {
|
|
34
|
-
readonly query: (
|
|
37
|
+
readonly query: (
|
|
38
|
+
payload: Rpc.Payload<R>,
|
|
39
|
+
options?: QueryOptions,
|
|
40
|
+
) => Atom.Atom<AsyncResult.AsyncResult<Rpc.Success<R>, Failure<R> | MissingReadPolicy>>;
|
|
35
41
|
readonly run: (payload: Rpc.Payload<R>) => Effect.Effect<Rpc.Success<R>, RunFailure<R>, TSelf>;
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -89,6 +95,14 @@ export function bind(
|
|
|
89
95
|
service: AtomRpc.AtomRpcClient<unknown, string, Rpc.Any>,
|
|
90
96
|
options?: BindOptions,
|
|
91
97
|
): unknown {
|
|
98
|
+
return bindApi(contract, service, options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function bindApi(
|
|
102
|
+
contract: { readonly declaration: Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[], "session" | undefined> },
|
|
103
|
+
service: AtomRpc.AtomRpcClient<unknown, string, Rpc.Any>,
|
|
104
|
+
options?: BindOptions,
|
|
105
|
+
) {
|
|
92
106
|
const { name, queries, commands, streams, keyScope } = contract.declaration;
|
|
93
107
|
const owner = bindingOwner(keyScope, options?.registry);
|
|
94
108
|
const automatic = automaticLive(service, name, owner, options?.resume);
|
|
@@ -101,3 +115,31 @@ export function bind(
|
|
|
101
115
|
{ [LiveConnection]: automatic.subscription },
|
|
102
116
|
);
|
|
103
117
|
}
|
|
118
|
+
|
|
119
|
+
export function makeClient<
|
|
120
|
+
TName extends string,
|
|
121
|
+
TQueries extends readonly QueryShape[],
|
|
122
|
+
TCommands extends readonly CommandShape[],
|
|
123
|
+
TRpcs extends Rpc.Any,
|
|
124
|
+
TStreams extends readonly StreamShape[],
|
|
125
|
+
TScope extends "session" | undefined,
|
|
126
|
+
TExtra extends Rpc.Any = never,
|
|
127
|
+
>(
|
|
128
|
+
contract: Contract<TName, TQueries, TCommands, TRpcs, TStreams, TScope>,
|
|
129
|
+
options: ClientOptions & { readonly group?: RpcGroup.RpcGroup<TRpcs | TExtra> } & ("session" extends TScope
|
|
130
|
+
? { readonly registry: AtomRegistry.AtomRegistry }
|
|
131
|
+
: { readonly registry?: AtomRegistry.AtomRegistry }),
|
|
132
|
+
): MadeClient<TName, TQueries, TCommands, TRpcs, TStreams, TExtra>;
|
|
133
|
+
export function makeClient(
|
|
134
|
+
contract: HttpGroup & {
|
|
135
|
+
readonly declaration: Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[], "session" | undefined>;
|
|
136
|
+
},
|
|
137
|
+
options: ClientOptions & {
|
|
138
|
+
readonly group?: HttpGroup;
|
|
139
|
+
readonly registry?: AtomRegistry.AtomRegistry;
|
|
140
|
+
},
|
|
141
|
+
): Layer.Layer<never> {
|
|
142
|
+
const connection = httpClient(contract.declaration.name, options.group ?? contract, options);
|
|
143
|
+
const api = bindApi(contract, connection.Client, options);
|
|
144
|
+
return Object.assign(connection, { api });
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Context, Effect, Layer, Scope } from "effect";
|
|
2
|
+
import type { HttpClient } from "effect/http/HttpClient";
|
|
3
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
4
|
+
import type * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
5
|
+
import type { Rpc, RpcClient, RpcSerialization } from "effect/rpc";
|
|
6
|
+
import type { RpcClientError } from "effect/rpc/RpcClientError";
|
|
7
|
+
import type { LiveBound } from "#automaticLive.ts";
|
|
8
|
+
import type { Bound } from "#bind.ts";
|
|
9
|
+
import type { CommandShape, QueryShape, StreamShape } from "#operation.ts";
|
|
10
|
+
|
|
11
|
+
export interface ClientOptions {
|
|
12
|
+
readonly httpClient?: Layer.Layer<HttpClient>;
|
|
13
|
+
readonly serialization?: Layer.Layer<RpcSerialization.RpcSerialization>;
|
|
14
|
+
readonly transformClient?: NonNullable<Parameters<typeof RpcClient.layerProtocolHttp>[0]["transformClient"]>;
|
|
15
|
+
readonly url: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ClientService<TName extends string, TRpcs extends Rpc.Any> = Context.ServiceClass.Shape<
|
|
19
|
+
`${TName}/Client`,
|
|
20
|
+
RpcClient.RpcClient.Flat<TRpcs, RpcClientError>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
export type MadeClient<
|
|
24
|
+
TName extends string,
|
|
25
|
+
TQueries extends readonly QueryShape[],
|
|
26
|
+
TCommands extends readonly CommandShape[],
|
|
27
|
+
TRpcs extends Rpc.Any,
|
|
28
|
+
TStreams extends readonly StreamShape[],
|
|
29
|
+
TExtra extends Rpc.Any,
|
|
30
|
+
> = Layer.Layer<ClientService<TName, TRpcs | TExtra>> & {
|
|
31
|
+
readonly api: Bound<TName, TQueries, TCommands, TRpcs, ClientService<TName, TRpcs | TExtra>, TStreams> & LiveBound;
|
|
32
|
+
readonly Client: AtomRpc.AtomRpcClient<ClientService<TName, TRpcs | TExtra>, `${TName}/Client`, TRpcs | TExtra>;
|
|
33
|
+
readonly client: Effect.Effect<RpcClient.RpcClient<TRpcs | TExtra, RpcClientError>, never, Scope.Scope | Rpc.MiddlewareClient<TRpcs | TExtra>>;
|
|
34
|
+
readonly runtime: Atom.AtomRuntime<ClientService<TName, TRpcs | TExtra>>;
|
|
35
|
+
};
|
package/src/contract.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { Context,
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
2
|
import { Rpc, RpcGroup, type RpcMiddleware, type RpcSchema } from "effect/rpc";
|
|
3
3
|
import type { CommandShape, OperationShape, QueryShape, StreamShape } from "#operation.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { QueryReadDenied } from "#readPolicyErrors.ts";
|
|
5
|
+
import { CommandInvalidation, changesRpc, KeyScope, QueryDefinition } from "#serverMetadata.ts";
|
|
5
6
|
|
|
6
7
|
export type Tag<TContract extends string, TName extends string> = `${TContract}.${TName}`;
|
|
7
8
|
|
|
9
|
+
type OperationError<TOperation extends OperationShape> = TOperation extends QueryShape
|
|
10
|
+
? Schema.Union<readonly [TOperation["error"], typeof QueryReadDenied]>
|
|
11
|
+
: TOperation["error"];
|
|
12
|
+
|
|
8
13
|
export type OperationRpc<TContract extends string, TOperation> = TOperation extends OperationShape
|
|
9
14
|
? TOperation extends StreamShape
|
|
10
15
|
? Rpc.Rpc<Tag<TContract, TOperation["name"]>, TOperation["payload"], RpcSchema.Stream<TOperation["success"], TOperation["error"]>, Schema.Never>
|
|
11
|
-
: Rpc.Rpc<Tag<TContract, TOperation["name"]>, TOperation["payload"], TOperation["success"], TOperation
|
|
16
|
+
: Rpc.Rpc<Tag<TContract, TOperation["name"]>, TOperation["payload"], TOperation["success"], OperationError<TOperation>>
|
|
12
17
|
: never;
|
|
13
18
|
|
|
14
19
|
export interface Declared<
|
|
@@ -61,9 +66,9 @@ function assertUnique(operations: readonly OperationShape[]) {
|
|
|
61
66
|
function annotations(operation: QueryShape | CommandShape | StreamShape, keyScope: "session" | undefined) {
|
|
62
67
|
const scope = keyScope === undefined ? Context.empty() : Context.make(KeyScope, keyScope);
|
|
63
68
|
if (operation.kind === "command") {
|
|
64
|
-
return Context.add(scope, CommandInvalidation, operation.invalidates);
|
|
69
|
+
return Context.add(scope, CommandInvalidation, { invalidates: operation.invalidates });
|
|
65
70
|
}
|
|
66
|
-
return operation.kind === "query" ? Context.add(scope,
|
|
71
|
+
return operation.kind === "query" ? Context.add(scope, QueryDefinition, { definition: operation }) : scope;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
type AnyDeclared = Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[], "session" | undefined>;
|
|
@@ -122,7 +127,7 @@ export function contract(
|
|
|
122
127
|
assertUnique([...queries, ...commands, ...streams]);
|
|
123
128
|
const rpcs = [...queries, ...commands, ...streams].map((operation) =>
|
|
124
129
|
Rpc.make(`${name}.${operation.name}`, {
|
|
125
|
-
error: operation.error,
|
|
130
|
+
error: operation.kind === "query" ? Schema.Union([operation.error, QueryReadDenied]) : operation.error,
|
|
126
131
|
payload: operation.payload,
|
|
127
132
|
stream: operation.kind === "stream",
|
|
128
133
|
success: operation.success,
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { Effect, type Schema, Stream } from "effect";
|
|
1
|
+
import { Effect, MutableHashMap, 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
|
+
// 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
|
+
|
|
15
20
|
type ErasedStreamingClient = AtomRpc.AtomRpcClient<
|
|
16
21
|
unknown,
|
|
17
22
|
string,
|
|
@@ -32,15 +37,30 @@ export function boundQuery(
|
|
|
32
37
|
const reactive = queryReactivity(service.runtime);
|
|
33
38
|
return {
|
|
34
39
|
query: (payload: unknown, options: QueryOptions = {}) => {
|
|
40
|
+
if (query.readPolicy === undefined) {
|
|
41
|
+
return service.runtime.atom(
|
|
42
|
+
Effect.fail(new MissingReadPolicy({ message: `Declare a readPolicy on query ${query.name} before running it live or subscribing` })),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
35
45
|
const atom = service.query(tag, payload, options);
|
|
36
|
-
const
|
|
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);
|
|
52
|
+
}
|
|
53
|
+
const local = query.reads(payload, Option.none());
|
|
54
|
+
const keys = [...local, { _tag: "List" as const, collection: `@query/${tag}/${id}` }];
|
|
55
|
+
const dependencies =
|
|
56
|
+
local.length === 0 ? [...keys, { _tag: "List" as const, collection: `@query/${tag.slice(0, tag.lastIndexOf("."))}` }] : keys;
|
|
37
57
|
const reference = { payload, query: query.name };
|
|
38
58
|
if (owner === undefined) {
|
|
39
59
|
track(undefined, reference, keys);
|
|
40
60
|
}
|
|
41
61
|
return reactive(
|
|
42
62
|
atom,
|
|
43
|
-
readKeys(
|
|
63
|
+
readKeys(dependencies, owner?.context),
|
|
44
64
|
(registry) => (owner === undefined ? () => undefined : track(registry, reference, keys)),
|
|
45
65
|
owner?.registry,
|
|
46
66
|
);
|
|
@@ -55,15 +75,16 @@ export function boundQuery(
|
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
export function boundCommand(service: ErasedClient, tag: string, command: CommandShape, owner?: BindingOwner) {
|
|
78
|
+
function invalidate(payload: unknown, result: unknown) {
|
|
79
|
+
const keys: readonly Key[] = [
|
|
80
|
+
...command.invalidates(payload, result),
|
|
81
|
+
{ _tag: "List", collection: `@query/${tag.slice(0, tag.lastIndexOf("."))}` },
|
|
82
|
+
];
|
|
83
|
+
return Reactivity.invalidate(invalidationKeys(keys, owner?.context));
|
|
84
|
+
}
|
|
58
85
|
return {
|
|
59
86
|
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
|
-
),
|
|
87
|
+
run(service, service.use((client) => client(tag, payload)).pipe(Effect.tap((result) => invalidate(payload, result))), owner),
|
|
67
88
|
};
|
|
68
89
|
}
|
|
69
90
|
|
|
@@ -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
|
});
|