@shivaedev/effect-contract 0.5.1 → 0.7.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 +29 -0
- package/README.md +43 -32
- package/dist/automaticLive.d.ts +22 -0
- package/dist/automaticLive.d.ts.map +1 -0
- package/dist/automaticLive.js +29 -0
- package/dist/automaticLive.js.map +1 -0
- package/dist/bind.d.ts +2 -1
- package/dist/bind.d.ts.map +1 -1
- package/dist/bind.js +10 -5
- package/dist/bind.js.map +1 -1
- package/dist/contract.d.ts +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +10 -2
- package/dist/contract.js.map +1 -1
- package/dist/keys.d.ts +4 -4
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +13 -9
- package/dist/keys.js.map +1 -1
- package/dist/live.d.ts +1 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +3 -1
- package/dist/live.js.map +1 -1
- package/dist/makeLiveServer.d.ts.map +1 -1
- package/dist/makeLiveServer.js +42 -10
- package/dist/makeLiveServer.js.map +1 -1
- package/dist/readPolicy.d.ts +9 -0
- package/dist/readPolicy.d.ts.map +1 -0
- package/dist/readPolicy.js +21 -0
- package/dist/readPolicy.js.map +1 -0
- package/dist/serverMetadata.d.ts +33 -0
- package/dist/serverMetadata.d.ts.map +1 -0
- package/dist/serverMetadata.js +12 -0
- package/dist/serverMetadata.js.map +1 -0
- package/package.json +11 -55
- package/src/automaticLive.ts +44 -0
- package/src/bind.ts +16 -8
- package/src/contract.ts +11 -3
- package/src/keys.ts +14 -10
- package/src/live.ts +4 -2
- package/src/makeLiveServer.ts +50 -12
- package/src/readPolicy.ts +29 -0
- package/src/serverMetadata.ts +15 -0
- package/docs/north-star.md +0 -45
- package/docs/roadmap.md +0 -26
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"makeLiveServer.js","sourceRoot":"","sources":["../src/makeLiveServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"makeLiveServer.js","sourceRoot":"","sources":["../src/makeLiveServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAY,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IACjD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IAChD,MAAM,SAAS,GAAG,qCAAqC,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC;IAC7E,SAAS,MAAM,CAAC,IAAoB;QACnC,OAAO,MAAM,CAAC,MAAM,CACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YACnB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC;YAClC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,EAAY,CAAC;YACjD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAe,CAAC;YACvC,IAAI,SAAS,GAAG,KAAK,CAAC;YACtB,IAAI,MAAM,GAAG,KAAK,CAAC;YACnB,IAAI,MAAM,GAAG,IAAI,CAAC;YAClB,SAAS,KAAK;gBACb,IAAI,SAAS,EAAE,CAAC;oBACf,OAAO;gBACR,CAAC;gBACD,SAAS,GAAG,IAAI,CAAC;gBACjB,cAAc,CAAC,GAAG,EAAE;oBACnB,SAAS,GAAG,KAAK,CAAC;oBAClB,IAAI,MAAM,EAAE,CAAC;wBACZ,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;oBAC1G,CAAC;oBACD,OAAO,CAAC,KAAK,EAAE,CAAC;oBAChB,MAAM,GAAG,KAAK,CAAC;gBAChB,CAAC,CAAC,CAAC;YACJ,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC/B,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC5B,OAAO,UAAU,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE;oBAC1C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,CAAC;oBAC/B,KAAK,EAAE,CAAC;gBACT,CAAC,CAAC,CAAC;YACJ,CAAC,CAAC,CAAC;YACH,MAAM,CAAC,IAAI,CACV,UAAU,CAAC,cAAc,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE;gBAC3C,MAAM,GAAG,IAAI,CAAC;gBACd,KAAK,EAAE,CAAC;YACT,CAAC,CAAC,CACF,CAAC;YACF,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAC/B,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;gBAChB,MAAM,GAAG,KAAK,CAAC;gBACf,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;oBAC3B,IAAI,EAAE,CAAC;gBACR,CAAC;YACF,CAAC,CAAC,CACF,CAAC;YACF,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC9C,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,IAAI,CAClC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CACzB,IAAI,CAAC,IAAI,KAAK,QAAQ;gBACrB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;gBACtB,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAC7G,EACD,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CACvE,CAAC;QACH,CAAC,CAAC,CACF,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;QAC/B,UAAU,CAAC,gBAAgB,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context, Effect } from "effect";
|
|
2
|
+
import type { Rpc } from "effect/rpc";
|
|
3
|
+
import type { Key } from "./keys.ts";
|
|
4
|
+
export declare const ReadPolicy: Context.Reference<(key: Key) => Effect.Effect<boolean>>;
|
|
5
|
+
export declare const authorizeReads: <A, E, R, TPolicyServices, TDenied>(effect: Effect.Effect<A, E, R>, request: {
|
|
6
|
+
readonly rpc: Rpc.AnyWithProps;
|
|
7
|
+
readonly payload: unknown;
|
|
8
|
+
}, policy: (key: Key) => Effect.Effect<boolean, never, TPolicyServices>, denied: Effect.Effect<never, TDenied, never>) => Effect.Effect<A, E | TDenied, TPolicyServices | Exclude<R, never>>;
|
|
9
|
+
//# sourceMappingURL=readPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readPolicy.d.ts","sourceRoot":"","sources":["../src/readPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAGrC,eAAO,MAAM,UAAU,0BAA2B,GAAG,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAE9E,CAAC;AAEH,eAAO,MAAM,cAAc,GAAgC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,eAAe,EAAE,OAAO;kBAElE,GAAG,CAAC,YAAY;sBAAoB,OAAO;iBACtD,GAAG,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,eAAe,CAAC,qHAgBnE,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context, Effect, Option } from "effect";
|
|
2
|
+
import { QueryReads } from "./serverMetadata.js";
|
|
3
|
+
export const ReadPolicy = Context.Reference("@shivaedev/effect-contract/ReadPolicy", {
|
|
4
|
+
defaultValue: () => () => Effect.succeed(true),
|
|
5
|
+
});
|
|
6
|
+
export const authorizeReads = Effect.fnUntraced(function* (effect, request, policy, denied) {
|
|
7
|
+
const context = yield* Effect.context();
|
|
8
|
+
function canRead(key) {
|
|
9
|
+
return Effect.provideContext(policy(key), context);
|
|
10
|
+
}
|
|
11
|
+
const reads = Context.getOption(request.rpc.annotations, QueryReads);
|
|
12
|
+
if (Option.isSome(reads)) {
|
|
13
|
+
for (const key of reads.value(request.payload)) {
|
|
14
|
+
if (!(yield* canRead(key))) {
|
|
15
|
+
return yield* denied;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return yield* Effect.provideService(effect, ReadPolicy, canRead);
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=readPolicy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readPolicy.js","sourceRoot":"","sources":["../src/readPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGjD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAuC,uCAAuC,EAAE;IAC1H,YAAY,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;CAC9C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACxD,MAA8B,EAC9B,OAAsE,EACtE,MAAoE,EACpE,MAAqC;IAErC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAmB,CAAC;IACzD,SAAS,OAAO,CAAC,GAAQ;QACxB,OAAO,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACrE,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC5B,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC;YACtB,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
import { Rpc } from "effect/rpc";
|
|
3
|
+
import { Key } from "./keys.ts";
|
|
4
|
+
declare const QueryReads_base: Context.ServiceClass<QueryReads, "@shivaedev/effect-contract/QueryReads", import("@shivaedev/types/bivariant.ts").Bivariant<(payload: unknown) => readonly Key[]>>;
|
|
5
|
+
export declare class QueryReads extends QueryReads_base {
|
|
6
|
+
}
|
|
7
|
+
declare const CommandInvalidation_base: Context.ServiceClass<CommandInvalidation, "@shivaedev/effect-contract/CommandInvalidation", import("@shivaedev/types/bivariant.ts").Bivariant<(payload: unknown, result: unknown) => readonly Key[]>>;
|
|
8
|
+
export declare class CommandInvalidation extends CommandInvalidation_base {
|
|
9
|
+
}
|
|
10
|
+
export declare function changesRpc<const TName extends string>(name: TName): Rpc.Rpc<`${TName}.$changes`, Schema.Struct<{
|
|
11
|
+
keys: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
12
|
+
readonly _tag: Schema.Literal<"List">;
|
|
13
|
+
readonly collection: Schema.String;
|
|
14
|
+
}>, Schema.Struct<{
|
|
15
|
+
readonly _tag: Schema.Literal<"Item">;
|
|
16
|
+
readonly collection: Schema.String;
|
|
17
|
+
readonly id: Schema.Union<readonly [Schema.String, Schema.Number]>;
|
|
18
|
+
}>]>>;
|
|
19
|
+
}>, import("effect/rpc/RpcSchema").Stream<Schema.Union<readonly [Schema.Struct<{
|
|
20
|
+
readonly _tag: Schema.Literal<"Changed">;
|
|
21
|
+
readonly keys: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
|
|
22
|
+
readonly _tag: Schema.Literal<"List">;
|
|
23
|
+
readonly collection: Schema.String;
|
|
24
|
+
}>, Schema.Struct<{
|
|
25
|
+
readonly _tag: Schema.Literal<"Item">;
|
|
26
|
+
readonly collection: Schema.String;
|
|
27
|
+
readonly id: Schema.Union<readonly [Schema.String, Schema.Number]>;
|
|
28
|
+
}>]>>;
|
|
29
|
+
}>, Schema.Struct<{
|
|
30
|
+
readonly _tag: Schema.Literal<"Resync">;
|
|
31
|
+
}>]>, Schema.Never>, Schema.Never, never, never>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=serverMetadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverMetadata.d.ts","sourceRoot":"","sources":["../src/serverMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;;AAIhC,qBAAa,UAAW,SAAQ,eAA2F;CAAG;;AAE9H,qBAAa,mBAAoB,SAAQ,wBAExC;CAAG;AAEJ,wBAAgB,UAAU,CAAC,KAAK,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,KAAK;;;;;;;;;;;;;;;;;;;;;iDAEjE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
import { Rpc } from "effect/rpc";
|
|
3
|
+
import { Key } from "./keys.js";
|
|
4
|
+
import { LiveHint } from "./live.js";
|
|
5
|
+
export class QueryReads extends Context.Service()("@shivaedev/effect-contract/QueryReads") {
|
|
6
|
+
}
|
|
7
|
+
export class CommandInvalidation extends Context.Service()("@shivaedev/effect-contract/CommandInvalidation") {
|
|
8
|
+
}
|
|
9
|
+
export function changesRpc(name) {
|
|
10
|
+
return Rpc.make(`${name}.$changes`, { payload: { keys: Schema.Array(Key) }, stream: true, success: LiveHint });
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=serverMetadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverMetadata.js","sourceRoot":"","sources":["../src/serverMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGrC,MAAM,OAAO,UAAW,SAAQ,OAAO,CAAC,OAAO,EAAmC,CAAC,uCAAuC,CAAC;CAAG;AAE9H,MAAM,OAAO,mBAAoB,SAAQ,OAAO,CAAC,OAAO,EAAoD,CAC3G,gDAAgD,CAChD;CAAG;AAEJ,MAAM,UAAU,UAAU,CAA6B,IAAW;IACjE,OAAO,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;AAChH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Typed query and command contracts over native Effect RPC and reactivity",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-contract#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -23,55 +23,12 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"types": "./dist
|
|
29
|
-
"
|
|
30
|
-
"import": "./dist/bind.js",
|
|
31
|
-
"default": "./dist/bind.js"
|
|
32
|
-
},
|
|
33
|
-
"./contract.ts": {
|
|
34
|
-
"types": "./dist/contract.d.ts",
|
|
35
|
-
"source": "./src/contract.ts",
|
|
36
|
-
"import": "./dist/contract.js",
|
|
37
|
-
"default": "./dist/contract.js"
|
|
38
|
-
},
|
|
39
|
-
"./keys.ts": {
|
|
40
|
-
"types": "./dist/keys.d.ts",
|
|
41
|
-
"source": "./src/keys.ts",
|
|
42
|
-
"import": "./dist/keys.js",
|
|
43
|
-
"default": "./dist/keys.js"
|
|
44
|
-
},
|
|
45
|
-
"./live.ts": {
|
|
46
|
-
"types": "./dist/live.d.ts",
|
|
47
|
-
"source": "./src/live.ts",
|
|
48
|
-
"import": "./dist/live.js",
|
|
49
|
-
"default": "./dist/live.js"
|
|
50
|
-
},
|
|
51
|
-
"./makeLiveServer.ts": {
|
|
52
|
-
"types": "./dist/makeLiveServer.d.ts",
|
|
53
|
-
"source": "./src/makeLiveServer.ts",
|
|
54
|
-
"import": "./dist/makeLiveServer.js",
|
|
55
|
-
"default": "./dist/makeLiveServer.js"
|
|
56
|
-
},
|
|
57
|
-
"./operation.ts": {
|
|
58
|
-
"types": "./dist/operation.d.ts",
|
|
59
|
-
"source": "./src/operation.ts",
|
|
60
|
-
"import": "./dist/operation.js",
|
|
61
|
-
"default": "./dist/operation.js"
|
|
62
|
-
},
|
|
63
|
-
"./rejection.ts": {
|
|
64
|
-
"types": "./dist/rejection.d.ts",
|
|
65
|
-
"source": "./src/rejection.ts",
|
|
66
|
-
"import": "./dist/rejection.js",
|
|
67
|
-
"default": "./dist/rejection.js"
|
|
26
|
+
"./*.ts": {
|
|
27
|
+
"source": "./src/*.ts",
|
|
28
|
+
"types": "./dist/*.d.ts",
|
|
29
|
+
"default": "./dist/*.js"
|
|
68
30
|
},
|
|
69
|
-
"./
|
|
70
|
-
"types": "./dist/resume.d.ts",
|
|
71
|
-
"source": "./src/resume.ts",
|
|
72
|
-
"import": "./dist/resume.js",
|
|
73
|
-
"default": "./dist/resume.js"
|
|
74
|
-
}
|
|
31
|
+
"./package.json": "./package.json"
|
|
75
32
|
},
|
|
76
33
|
"files": [
|
|
77
34
|
"dist",
|
|
@@ -79,7 +36,6 @@
|
|
|
79
36
|
"!src/**/*.test.ts",
|
|
80
37
|
"!src/**/*.spec.ts",
|
|
81
38
|
"!src/**/test-support",
|
|
82
|
-
"docs",
|
|
83
39
|
"CHANGELOG.md",
|
|
84
40
|
"README.md"
|
|
85
41
|
],
|
|
@@ -89,14 +45,14 @@
|
|
|
89
45
|
"devDependencies": {
|
|
90
46
|
"@effect/platform-node": "4.0.4",
|
|
91
47
|
"@effect/vitest": "4.0.4",
|
|
92
|
-
"@shivaedev/effect-test": "0.
|
|
93
|
-
"@shivaedev/quality": "0.
|
|
94
|
-
"@
|
|
95
|
-
"@
|
|
48
|
+
"@shivaedev/effect-test": "0.7.1",
|
|
49
|
+
"@shivaedev/quality": "0.29.1",
|
|
50
|
+
"@shivaedev/test-story": "0.15.0",
|
|
51
|
+
"@types/node": "24.19.2",
|
|
96
52
|
"effect": "4.0.4",
|
|
97
53
|
"esbuild": "0.28.2",
|
|
98
54
|
"happy-dom": "20.14.6",
|
|
99
|
-
"typescript": "
|
|
55
|
+
"typescript": "7.0.2",
|
|
100
56
|
"vitest": "5.0.3"
|
|
101
57
|
},
|
|
102
58
|
"peerDependencies": {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Effect, Stream, SubscriptionRef } from "effect";
|
|
2
|
+
import type * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import type * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
4
|
+
import { type Key, readKeys } from "./keys.ts";
|
|
5
|
+
import { type LiveStatus, live } from "./live.ts";
|
|
6
|
+
import { type ResumeWindow, resumeSignal } from "./resume.ts";
|
|
7
|
+
import type { changesRpc } from "./serverMetadata.ts";
|
|
8
|
+
|
|
9
|
+
declare const window: ResumeWindow | undefined;
|
|
10
|
+
|
|
11
|
+
export const LiveConnection = Symbol.for("@shivaedev/effect-contract/LiveConnection");
|
|
12
|
+
export interface LiveBound {
|
|
13
|
+
readonly [LiveConnection]: { readonly connection: Atom.Atom<unknown>; readonly status: Atom.Atom<LiveStatus<unknown>> };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ChangesClient = AtomRpc.AtomRpcClient<unknown, string, ReturnType<typeof changesRpc>>;
|
|
17
|
+
|
|
18
|
+
export function automaticLive(service: ChangesClient, name: string) {
|
|
19
|
+
const keys = Effect.runSync(SubscriptionRef.make<readonly Key[]>([]));
|
|
20
|
+
const known = new Set<string>();
|
|
21
|
+
function track(reads: readonly Key[]) {
|
|
22
|
+
const fresh = reads.filter((key) => {
|
|
23
|
+
const id = readKeys([key])[0] ?? "";
|
|
24
|
+
if (known.has(id)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
known.add(id);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
if (fresh.length > 0) {
|
|
31
|
+
Effect.runSync(SubscriptionRef.update(keys, (current) => [...current, ...fresh]));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const subscription = live(service.runtime, {
|
|
35
|
+
resume: resumeSignal(typeof window === "undefined" ? {} : { window }),
|
|
36
|
+
resyncKeys: () => Effect.runSync(SubscriptionRef.get(keys)),
|
|
37
|
+
stream: SubscriptionRef.changes(keys).pipe(
|
|
38
|
+
Stream.switchMap((current) =>
|
|
39
|
+
current.length === 0 ? Stream.never : Stream.unwrap(service.use((client) => Effect.succeed(client(`${name}.$changes`, { keys: current })))),
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
});
|
|
43
|
+
return { subscription, track };
|
|
44
|
+
}
|
package/src/bind.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
|
7
7
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
8
8
|
import type { Rpc, RpcSchema } from "effect/rpc";
|
|
9
9
|
import type { RpcClientError } from "effect/rpc/RpcClientError";
|
|
10
|
+
import { automaticLive, type LiveBound, LiveConnection } from "./automaticLive.ts";
|
|
10
11
|
import type { Contract, Declared, Tag } from "./contract.ts";
|
|
11
12
|
import { invalidationKeys, type Key, readKeys } from "./keys.ts";
|
|
12
13
|
import type { CommandShape, QueryShape, StreamShape } from "./operation.ts";
|
|
@@ -73,12 +74,15 @@ const invalidateAfter = Effect.fn("EffectContract.invalidateAfter")(function* (k
|
|
|
73
74
|
yield* Reactivity.invalidate(invalidationKeys(keys));
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
function boundQuery(service: ErasedClient, tag: string, query: QueryShape) {
|
|
77
|
+
function boundQuery(service: ErasedClient, tag: string, query: QueryShape, track: (keys: readonly Key[]) => void) {
|
|
77
78
|
function reads(payload: unknown) {
|
|
78
79
|
return readKeys(query.reads(payload));
|
|
79
80
|
}
|
|
80
81
|
return {
|
|
81
|
-
query: (payload: unknown, options: QueryOptions = {}) =>
|
|
82
|
+
query: (payload: unknown, options: QueryOptions = {}) => {
|
|
83
|
+
track(query.reads(payload));
|
|
84
|
+
return service.query(tag, payload, { ...options, reactivityKeys: reads(payload) });
|
|
85
|
+
},
|
|
82
86
|
run: (payload: unknown) => service.use((client) => client(tag, payload)),
|
|
83
87
|
};
|
|
84
88
|
}
|
|
@@ -108,15 +112,19 @@ export function bind<
|
|
|
108
112
|
>(
|
|
109
113
|
contract: Contract<TName, TQueries, TCommands, TRpcs, TStreams>,
|
|
110
114
|
service: AtomRpc.AtomRpcClient<TSelf, TId, TClientRpcs> & MatchingClient<NoInfer<TRpcs>, TClientRpcs>,
|
|
111
|
-
): Bound<TName, TQueries, TCommands, TRpcs, TSelf, TStreams
|
|
115
|
+
): Bound<TName, TQueries, TCommands, TRpcs, TSelf, TStreams> & LiveBound;
|
|
112
116
|
export function bind(
|
|
113
117
|
contract: { readonly declaration: Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[]> },
|
|
114
118
|
service: AtomRpc.AtomRpcClient<unknown, string, Rpc.Any>,
|
|
115
119
|
): unknown {
|
|
116
120
|
const { name, queries, commands, streams } = contract.declaration;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
const automatic = automaticLive(service, name);
|
|
122
|
+
return Object.assign(
|
|
123
|
+
Object.fromEntries([
|
|
124
|
+
...queries.map((query) => [query.name, boundQuery(service, `${name}.${query.name}`, query, automatic.track)] as const),
|
|
125
|
+
...commands.map((command) => [command.name, boundCommand(service, `${name}.${command.name}`, command)] as const),
|
|
126
|
+
...streams.map((stream) => [stream.name, boundStream(service, `${name}.${stream.name}`)] as const),
|
|
127
|
+
]),
|
|
128
|
+
{ [LiveConnection]: automatic.subscription },
|
|
129
|
+
);
|
|
122
130
|
}
|
package/src/contract.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Context, type 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 { CommandInvalidation, changesRpc, QueryReads } from "./serverMetadata.ts";
|
|
4
5
|
|
|
5
6
|
export type Tag<TContract extends string, TName extends string> = `${TContract}.${TName}`;
|
|
6
7
|
|
|
@@ -51,6 +52,13 @@ function assertUnique(operations: readonly OperationShape[]) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function annotations(operation: QueryShape | CommandShape | StreamShape) {
|
|
56
|
+
if (operation.kind === "command") {
|
|
57
|
+
return Context.make(CommandInvalidation, operation.invalidates);
|
|
58
|
+
}
|
|
59
|
+
return operation.kind === "query" ? Context.make(QueryReads, operation.reads) : Context.empty();
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
type AnyDeclared = Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[]>;
|
|
55
63
|
|
|
56
64
|
interface Group {
|
|
@@ -94,7 +102,7 @@ export function contract(
|
|
|
94
102
|
payload: operation.payload,
|
|
95
103
|
stream: operation.kind === "stream",
|
|
96
104
|
success: operation.success,
|
|
97
|
-
}),
|
|
105
|
+
}).annotateMerge(annotations(operation)),
|
|
98
106
|
);
|
|
99
|
-
return withDeclaration(RpcGroup.make(...rpcs), { commands, name, queries, streams });
|
|
107
|
+
return withDeclaration(RpcGroup.make(...rpcs, changesRpc(name)), { commands, name, queries, streams });
|
|
100
108
|
}
|
package/src/keys.ts
CHANGED
|
@@ -26,21 +26,25 @@ export interface Collection<TName extends string, TId extends Identity> {
|
|
|
26
26
|
readonly name: TName;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export
|
|
29
|
+
export function collection<const TName extends string, TId extends Schema.Top & { readonly Type: Identity }>(
|
|
30
30
|
name: TName,
|
|
31
31
|
_id: TId,
|
|
32
|
-
): Collection<TName, TId["Type"]>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
): Collection<TName, TId["Type"]> {
|
|
33
|
+
return {
|
|
34
|
+
item: (id) => ({ _tag: "Item", collection: name, id }),
|
|
35
|
+
list: { _tag: "List", collection: name },
|
|
36
|
+
name,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
37
39
|
|
|
38
40
|
function own(key: Key): string {
|
|
39
41
|
return key._tag === "List" ? key.collection : `${key.collection}:${key.id}`;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
export
|
|
44
|
+
export function readKeys(keys: readonly Key[]): readonly string[] {
|
|
45
|
+
return [...new Set(keys.map(own))];
|
|
46
|
+
}
|
|
43
47
|
|
|
44
|
-
export
|
|
45
|
-
...new Set(keys.flatMap((key) => (key._tag === "List" ? [key.collection] : [own(key), key.collection])))
|
|
46
|
-
|
|
48
|
+
export function invalidationKeys(keys: readonly Key[]): readonly string[] {
|
|
49
|
+
return [...new Set(keys.flatMap((key) => (key._tag === "List" ? [key.collection] : [own(key), key.collection])))];
|
|
50
|
+
}
|
package/src/live.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface LiveStatus<E> {
|
|
|
16
16
|
}
|
|
17
17
|
export interface LiveOptions<E, R> {
|
|
18
18
|
readonly resume?: Atom.Atom<unknown>;
|
|
19
|
-
readonly resyncKeys: readonly Key[];
|
|
19
|
+
readonly resyncKeys: readonly Key[] | (() => readonly Key[]);
|
|
20
20
|
readonly retryDelay?: Duration.Input;
|
|
21
21
|
readonly stream: Stream.Stream<LiveHint, E, R>;
|
|
22
22
|
}
|
|
@@ -39,9 +39,11 @@ export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeE
|
|
|
39
39
|
update({ needsResync: true });
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
reactivity.invalidateUnsafe(readKeys(options.resyncKeys));
|
|
42
|
+
reactivity.invalidateUnsafe(readKeys(typeof options.resyncKeys === "function" ? options.resyncKeys() : options.resyncKeys));
|
|
43
43
|
update({ needsResync: false, pending: 0 });
|
|
44
44
|
}
|
|
45
|
+
// A subscription alone does not build the flag, so its first read would otherwise announce a change.
|
|
46
|
+
get.once(paused);
|
|
45
47
|
get.subscribe(paused, (value) => {
|
|
46
48
|
if (!value) {
|
|
47
49
|
resync();
|
package/src/makeLiveServer.ts
CHANGED
|
@@ -1,31 +1,69 @@
|
|
|
1
|
-
import { Effect, Stream } from "effect";
|
|
1
|
+
import { Effect, Queue, Stream } from "effect";
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
3
|
import { type Key, readKeys } from "./keys.ts";
|
|
4
4
|
import type { LiveHint } from "./live.ts";
|
|
5
|
+
import { ReadPolicy } from "./readPolicy.ts";
|
|
5
6
|
|
|
6
7
|
export const makeLiveServer = Effect.gen(function* () {
|
|
7
8
|
const reactivity = yield* Reactivity.Reactivity;
|
|
8
9
|
const resyncKey = `@shivaedev/effect-contract/resync/${crypto.randomUUID()}`;
|
|
9
|
-
let generation = 0;
|
|
10
10
|
function stream(keys: readonly Key[]) {
|
|
11
11
|
return Stream.unwrap(
|
|
12
|
-
Effect.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const canRead = yield* ReadPolicy;
|
|
14
|
+
const queue = yield* Queue.unbounded<LiveHint>();
|
|
15
|
+
const pending = new Map<string, Key>();
|
|
16
|
+
let scheduled = false;
|
|
17
|
+
let resync = false;
|
|
18
|
+
let active = true;
|
|
19
|
+
function flush() {
|
|
20
|
+
if (scheduled) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
scheduled = true;
|
|
24
|
+
queueMicrotask(() => {
|
|
25
|
+
scheduled = false;
|
|
26
|
+
if (active) {
|
|
27
|
+
Queue.offerUnsafe(queue, resync ? { _tag: "Resync" } : { _tag: "Changed", keys: [...pending.values()] });
|
|
20
28
|
}
|
|
21
|
-
|
|
29
|
+
pending.clear();
|
|
30
|
+
resync = false;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const cancel = keys.map((key) => {
|
|
34
|
+
const own = readKeys([key]);
|
|
35
|
+
return reactivity.registerUnsafe(own, () => {
|
|
36
|
+
pending.set(own[0] ?? "", key);
|
|
37
|
+
flush();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
cancel.push(
|
|
41
|
+
reactivity.registerUnsafe([resyncKey], () => {
|
|
42
|
+
resync = true;
|
|
43
|
+
flush();
|
|
22
44
|
}),
|
|
23
45
|
);
|
|
46
|
+
yield* Effect.addFinalizer(() =>
|
|
47
|
+
Effect.sync(() => {
|
|
48
|
+
active = false;
|
|
49
|
+
for (const stop of cancel) {
|
|
50
|
+
stop();
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
yield* Queue.offer(queue, { _tag: "Resync" });
|
|
55
|
+
return Stream.fromQueue(queue).pipe(
|
|
56
|
+
Stream.mapEffect((hint) =>
|
|
57
|
+
hint._tag === "Resync"
|
|
58
|
+
? Effect.succeed(hint)
|
|
59
|
+
: Effect.map(Effect.filter(hint.keys, canRead), (allowed): LiveHint => ({ _tag: "Changed", keys: allowed })),
|
|
60
|
+
),
|
|
61
|
+
Stream.filter((hint) => hint._tag === "Resync" || hint.keys.length > 0),
|
|
62
|
+
);
|
|
24
63
|
}),
|
|
25
64
|
);
|
|
26
65
|
}
|
|
27
66
|
const resync = Effect.sync(() => {
|
|
28
|
-
generation += 1;
|
|
29
67
|
reactivity.invalidateUnsafe([resyncKey]);
|
|
30
68
|
});
|
|
31
69
|
return { resync, stream };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Context, Effect, Option } from "effect";
|
|
2
|
+
import type { Rpc } from "effect/rpc";
|
|
3
|
+
import type { Key } from "./keys.ts";
|
|
4
|
+
import { QueryReads } from "./serverMetadata.ts";
|
|
5
|
+
|
|
6
|
+
export const ReadPolicy = Context.Reference<(key: Key) => Effect.Effect<boolean>>("@shivaedev/effect-contract/ReadPolicy", {
|
|
7
|
+
defaultValue: () => () => Effect.succeed(true),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const authorizeReads = Effect.fnUntraced(function* <A, E, R, TPolicyServices, TDenied>(
|
|
11
|
+
effect: Effect.Effect<A, E, R>,
|
|
12
|
+
request: { readonly rpc: Rpc.AnyWithProps; readonly payload: unknown },
|
|
13
|
+
policy: (key: Key) => Effect.Effect<boolean, never, TPolicyServices>,
|
|
14
|
+
denied: Effect.Effect<never, TDenied>,
|
|
15
|
+
) {
|
|
16
|
+
const context = yield* Effect.context<TPolicyServices>();
|
|
17
|
+
function canRead(key: Key) {
|
|
18
|
+
return Effect.provideContext(policy(key), context);
|
|
19
|
+
}
|
|
20
|
+
const reads = Context.getOption(request.rpc.annotations, QueryReads);
|
|
21
|
+
if (Option.isSome(reads)) {
|
|
22
|
+
for (const key of reads.value(request.payload)) {
|
|
23
|
+
if (!(yield* canRead(key))) {
|
|
24
|
+
return yield* denied;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return yield* Effect.provideService(effect, ReadPolicy, canRead);
|
|
29
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
import { Rpc } from "effect/rpc";
|
|
3
|
+
import { Key } from "./keys.ts";
|
|
4
|
+
import { LiveHint } from "./live.ts";
|
|
5
|
+
import type { CommandShape, QueryShape } from "./operation.ts";
|
|
6
|
+
|
|
7
|
+
export class QueryReads extends Context.Service<QueryReads, QueryShape["reads"]>()("@shivaedev/effect-contract/QueryReads") {}
|
|
8
|
+
|
|
9
|
+
export class CommandInvalidation extends Context.Service<CommandInvalidation, CommandShape["invalidates"]>()(
|
|
10
|
+
"@shivaedev/effect-contract/CommandInvalidation",
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
export function changesRpc<const TName extends string>(name: TName) {
|
|
14
|
+
return Rpc.make(`${name}.$changes`, { payload: { keys: Schema.Array(Key) }, stream: true, success: LiveHint });
|
|
15
|
+
}
|
package/docs/north-star.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# North star
|
|
2
|
-
|
|
3
|
-
## The problem
|
|
4
|
-
|
|
5
|
-
A feature crosses several boundaries. A service knows its input, result and expected failures. An RPC operation repeats them. A client needs the same types, then separately needs to know which queries a successful write should refresh. When those descriptions live apart, they drift: a failure becomes an untyped message, a newly created item does not refresh, or a broad invalidation replaces unrelated views.
|
|
6
|
-
|
|
7
|
-
Effect already provides the underlying pieces: Schema, typed Effects, Rpc, RpcGroup, middleware, AtomRpc, Reactivity and Streams. The missing piece is a small declaration that ties the feature's boundary and read/write dependencies together. Replacing those native pieces would introduce another model to learn and another owner for errors, interruption and resource lifetime.
|
|
8
|
-
|
|
9
|
-
## The ideal
|
|
10
|
-
|
|
11
|
-
A feature declares its boundary once. Its payload and success come from Schema. Its expected failures are tagged rejection classes that handlers and clients share. Queries name the dependencies they read. Commands name the dependencies their successful results change, including IDs returned by a create operation.
|
|
12
|
-
|
|
13
|
-
That declaration is an ordinary native RPC group. Implementing, securing, hosting and calling it uses the APIs an Effect engineer already knows. The client binding adds only the mechanical work: register declared read keys on native query atoms and invalidate declared change keys after a successful call.
|
|
14
|
-
|
|
15
|
-
Freshness has one vocabulary. An item change invalidates that item and its collection list. A reconciliation request uses an explicit read scope, rather than guessing that a list key covers all item queries. Optional live hints and resume signals request those same reads again. They do not hold replacement data; the successful query remains authoritative.
|
|
16
|
-
|
|
17
|
-
## What good looks like
|
|
18
|
-
|
|
19
|
-
- A reader finds the input, result, rejection and dependency rule together, and an agent can infer the types without repeating them at call sites.
|
|
20
|
-
- A domain service can raise a rejection class that several operations reuse. Generated field rejections name the form fields they describe.
|
|
21
|
-
- An ordinary query/command feature needs no subscription. A feature needing live freshness composes a native Stream with the same registry and keys.
|
|
22
|
-
- A successful command refreshes affected views. A rejected command leaves those reads alone. A reconnect reconciles the caller's explicit scope, including changes missed while disconnected.
|
|
23
|
-
- Native atoms retain query state and own stale-request interruption. The helper never becomes another cache or a parallel resource owner.
|
|
24
|
-
- Browser code can import declarations, bindings and resume signals without bringing in React or server implementation packages.
|
|
25
|
-
- Type tests, native client tests and real HTTP tests make different claims. Each claim stops at the boundary its evidence actually exercises.
|
|
26
|
-
|
|
27
|
-
## Trade-offs
|
|
28
|
-
|
|
29
|
-
- **Native composition over a shorter host setup.** Applications choose their native middleware, server host, serialization and AtomRpc service. The package removes repeated declarations without concealing those decisions.
|
|
30
|
-
- **Precise dependency keys over convenient broad refresh.** A feature must name its reads and changes. Reconciliation must name a common scope when it needs one. Incorrect dependency discovery is an application error the library cannot infer away.
|
|
31
|
-
- **Ordinary calls over mandatory streaming.** Live invalidation is optional. No query waits for a journal sequence, subscription cursor or change acknowledgment to return its domain value.
|
|
32
|
-
- **Bounded observations over replay.** Paused live coordination records a bounded observation count and asks for current values on resume. It does not retain an unbounded list of events or promise recovery of each source change.
|
|
33
|
-
- **Typed expected failures over transport-owned business wording.** Rejection schemas describe domain failures. Authentication policy, user-facing text and error presentation remain with the application and neighbouring packages.
|
|
34
|
-
- **Consumer evidence over speculative helpers.** Add an abstraction after a consuming feature demonstrates repeated work. Preserve the native route for cases the helper does not cover.
|
|
35
|
-
|
|
36
|
-
## What it deliberately leaves out
|
|
37
|
-
|
|
38
|
-
- Service implementations, repositories, SQL transactions and commit-bound publication. These belong to `effect-service`, `effect-sql` and `effect-changes`.
|
|
39
|
-
- Authentication policy, session resolution and request identity implementation. The shared server boundary belongs to `platform`; the application's host supplies its policy.
|
|
40
|
-
- React hooks, rendering, editable drafts and form submission state. These belong to `effect-react` and `effect-form`.
|
|
41
|
-
- A second client cache, a custom RPC transport or a required mutation atom.
|
|
42
|
-
- An event journal, replay cursor, authoritative data stream, offline writes or cross-process delivery policy.
|
|
43
|
-
- Claims of SSR, authenticated live delivery, physical browser lifecycle or native-device behavior based only on a local library fixture.
|
|
44
|
-
|
|
45
|
-
Package status belongs in [roadmap.md](roadmap.md). Framework composition and deployment decisions belong in the [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md).
|
package/docs/roadmap.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Roadmap
|
|
2
|
-
|
|
3
|
-
This file owns effect-contract's implementation status and package questions. The [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md) owns application composition, adoption and deployment validation; it links here instead of repeating these package milestones.
|
|
4
|
-
|
|
5
|
-
## Built
|
|
6
|
-
|
|
7
|
-
- [x] `query`, `command` and `contract`: Schema payloads/results, namespaced native RPC groups, inline declarations, typed defaults, native handler layers and middleware. Compiler fixtures and native client round-trips cover the declared types and failures.
|
|
8
|
-
- [x] `stream(name, { payload?, success, rejections?, reads })` declarations in `contract(..., { streams })` map to native RPC with `stream: true`. Bound `run` returns a native Stream. Compiler checks cover payloads, elements, errors, duplicate names and unary-client mismatch; real HTTP tests cover delivery, declared rejection, completion and client interruption releasing the server subscription. Work Board uses this path for its live subscription.
|
|
9
|
-
- [x] Shared tagged rejection classes, generated operation-specific rejection classes and typed `reject` helpers. Field rejections derive and optionally narrow a struct's field names.
|
|
10
|
-
- [x] Unique operation names checked in literal declarations and at construction when arrays conceal them.
|
|
11
|
-
- [x] Collection item/list keys with native read hashing, deduplication and item-to-list mutation invalidation. Native Reactivity tests distinguish affected and unrelated items.
|
|
12
|
-
- [x] Native AtomRpc binding with query atoms and per-call Effects. Successful commands invalidate their declared keys, including keys derived from results; rejected commands do not invalidate. Client type checks cover matching, larger, incomplete and incompatible RPC groups.
|
|
13
|
-
- [x] Optional `LiveHint`/`Key` schemas and native live coordination: precise refresh, explicit first/reconnected scope reconciliation, a paused count capped at 256, typed stream failures and registry teardown. The framework guide owns the composed HTTP fixture evidence.
|
|
14
|
-
- [x] `makeLiveServer` turns committed Reactivity keys into scoped hint streams and resyncs every active read scope. [Server evidence](https://github.com/ShivaeDev/platform/blob/main/packages/effect-contract/src/makeLiveServer.test.ts).
|
|
15
|
-
- [x] Shared browser/native `resumeSignal`. Synthetic event tests cover visible versus hidden browser events, injected native callbacks, native SWR composition and listener disposal. The installed consumer checks an otherwise unobserved source's initialization and cleanup through `live`.
|
|
16
|
-
- [x] Installed-package runtime and type fixtures for contracts, keys, rejection fields and live pause/resume composition.
|
|
17
|
-
|
|
18
|
-
## Next
|
|
19
|
-
|
|
20
|
-
No additional declaration or live helper is selected beyond the server helper used by the reference order handlers. A concrete consuming feature should establish the next repeated task before this package adds an abstraction. Host/proxy behavior, authenticated subscriptions, SSR and physical browser/mobile lifecycle remain framework or application validation work.
|
|
21
|
-
|
|
22
|
-
## Open questions
|
|
23
|
-
|
|
24
|
-
- Should query freshness, including live coordination and resume, remain this package's long-term scope? The APIs live here and public package consumers use them; the original declaration boundary does not by itself decide every future lifecycle helper.
|
|
25
|
-
- Is the exported lower-level `rejectionSet` intended for application authors as well as operation construction? It is public and the operation tests exercise its implementation; direct standalone use has no dedicated regression fixture.
|
|
26
|
-
- Which concrete consumer requirement should select the next package helper? There is no package-specific committed API backlog beyond the existing declarations, binding and freshness composition.
|