@shivaedev/effect-contract 0.3.0 → 0.4.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 +25 -0
- package/README.md +332 -55
- package/dist/bind.d.ts +23 -12
- package/dist/bind.d.ts.map +1 -1
- package/dist/bind.js +19 -9
- package/dist/bind.js.map +1 -1
- package/dist/contract.d.ts +23 -20
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +13 -8
- package/dist/contract.js.map +1 -1
- package/dist/keys.d.ts +19 -11
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +8 -1
- package/dist/keys.js.map +1 -1
- package/dist/live.d.ts +35 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +60 -0
- package/dist/live.js.map +1 -0
- package/dist/makeLiveServer.d.ts +20 -0
- package/dist/makeLiveServer.d.ts.map +1 -0
- package/dist/makeLiveServer.js +26 -0
- package/dist/makeLiveServer.js.map +1 -0
- package/dist/operation.d.ts +37 -25
- package/dist/operation.d.ts.map +1 -1
- package/dist/operation.js +7 -4
- package/dist/operation.js.map +1 -1
- package/dist/rejection.d.ts +27 -27
- package/dist/rejection.d.ts.map +1 -1
- package/dist/rejection.js.map +1 -1
- package/dist/resume.d.ts +17 -0
- package/dist/resume.d.ts.map +1 -0
- package/dist/resume.js +28 -0
- package/dist/resume.js.map +1 -0
- package/docs/north-star.md +45 -0
- package/docs/roadmap.md +26 -0
- package/package.json +62 -12
- package/src/bind.ts +71 -36
- package/src/contract.ts +59 -39
- package/src/keys.ts +21 -14
- package/src/live.ts +81 -0
- package/src/makeLiveServer.ts +32 -0
- package/src/operation.ts +69 -42
- package/src/rejection.ts +40 -37
- package/src/resume.ts +44 -0
package/dist/rejection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rejection.d.ts","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEpD,MAAM,MAAM,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"rejection.d.ts","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"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"}
|
package/dist/rejection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rejection.js","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"rejection.js","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AA8DpD,MAAM,UAAU,YAAY,CAAC,KAAqB;IACjD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAsC,EAAE,CAAC;QAC9F,GAAG;QACH,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,EAAwB,CAAC,GAAG,EAAE,IAAI,CAAC;KACpF,CAAC,CAAC;IACH,OAAO;QACN,KAAK,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;QAC9F,SAAS,EAAE,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC;QACtC,MAAM,EAAE,MAAM,CAAC,WAAW,CACzB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,IAAwB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAC1H;KACD,CAAC;AACH,CAAC;AAcD,MAAM,UAAU,cAAc,CAAC,MAAiD,EAAE,MAA0B;IAC3G,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;AACjG,CAAC"}
|
package/dist/resume.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
2
|
+
export type ResumeSource = (resume: () => void) => () => void;
|
|
3
|
+
type ResumeEvent = "online" | "visibilitychange";
|
|
4
|
+
export interface ResumeWindow {
|
|
5
|
+
readonly addEventListener: (type: ResumeEvent, listener: () => void) => void;
|
|
6
|
+
readonly document: {
|
|
7
|
+
readonly visibilityState: string;
|
|
8
|
+
};
|
|
9
|
+
readonly removeEventListener: (type: ResumeEvent, listener: () => void) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface ResumeOptions {
|
|
12
|
+
readonly native?: ResumeSource;
|
|
13
|
+
readonly window?: ResumeWindow;
|
|
14
|
+
}
|
|
15
|
+
export declare function resumeSignal({ window, native }: ResumeOptions): Atom.Atom<number>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=resume.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume.d.ts","sourceRoot":"","sources":["../src/resume.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAExD,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;AAE9D,KAAK,WAAW,GAAG,QAAQ,GAAG,kBAAkB,CAAC;AAEjD,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAC7E,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,QAAQ,CAAC,mBAAmB,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAChF;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAC/B;AAcD,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAcjF"}
|
package/dist/resume.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
2
|
+
function whileVisible(target, type) {
|
|
3
|
+
return (resume) => {
|
|
4
|
+
function visible() {
|
|
5
|
+
if (target.document.visibilityState === "visible") {
|
|
6
|
+
resume();
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
target.addEventListener(type, visible);
|
|
10
|
+
return () => target.removeEventListener(type, visible);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function resumeSignal({ window, native }) {
|
|
14
|
+
const browser = window === undefined ? [] : [whileVisible(window, "visibilitychange"), whileVisible(window, "online")];
|
|
15
|
+
const sources = native === undefined ? browser : [...browser, native];
|
|
16
|
+
return Atom.readable((get) => {
|
|
17
|
+
let count = 0;
|
|
18
|
+
function resume() {
|
|
19
|
+
count += 1;
|
|
20
|
+
get.setSelf(count);
|
|
21
|
+
}
|
|
22
|
+
for (const source of sources) {
|
|
23
|
+
get.addFinalizer(source(resume));
|
|
24
|
+
}
|
|
25
|
+
return count;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=resume.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resume.js","sourceRoot":"","sources":["../src/resume.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,iCAAiC,CAAC;AAiBxD,SAAS,YAAY,CAAC,MAAoB,EAAE,IAAiB;IAC5D,OAAO,CAAC,MAAM,EAAE,EAAE;QACjB,SAAS,OAAO;YACf,IAAI,MAAM,CAAC,QAAQ,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;gBACnD,MAAM,EAAE,CAAC;YACV,CAAC;QACF,CAAC;QACD,MAAM,CAAC,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACvC,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAiB;IAC7D,MAAM,OAAO,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAAE,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IACvH,MAAM,OAAO,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC;IACtE,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,EAAE;QAC5B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,SAAS,MAAM;YACd,KAAK,IAAI,CAAC,CAAC;YACX,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC9B,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAClC,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"type": "module",
|
|
17
17
|
"imports": {
|
|
18
|
-
"#test/*": "./test/*",
|
|
18
|
+
"#test/*": "./src/test-support/*",
|
|
19
19
|
"#*.ts": {
|
|
20
20
|
"source": "./src/*.ts",
|
|
21
21
|
"types": "./dist/*.d.ts",
|
|
@@ -23,17 +23,63 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
"./bind.ts": {
|
|
28
|
+
"types": "./dist/bind.d.ts",
|
|
29
|
+
"source": "./src/bind.ts",
|
|
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"
|
|
31
44
|
},
|
|
32
|
-
"./
|
|
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"
|
|
68
|
+
},
|
|
69
|
+
"./resume.ts": {
|
|
70
|
+
"types": "./dist/resume.d.ts",
|
|
71
|
+
"source": "./src/resume.ts",
|
|
72
|
+
"import": "./dist/resume.js",
|
|
73
|
+
"default": "./dist/resume.js"
|
|
74
|
+
}
|
|
33
75
|
},
|
|
34
76
|
"files": [
|
|
35
77
|
"dist",
|
|
36
78
|
"src",
|
|
79
|
+
"!src/**/*.test.ts",
|
|
80
|
+
"!src/**/*.spec.ts",
|
|
81
|
+
"!src/**/test-support",
|
|
82
|
+
"docs",
|
|
37
83
|
"CHANGELOG.md",
|
|
38
84
|
"README.md"
|
|
39
85
|
],
|
|
@@ -41,13 +87,17 @@
|
|
|
41
87
|
"@shivaedev/types": "0.2.0"
|
|
42
88
|
},
|
|
43
89
|
"devDependencies": {
|
|
90
|
+
"@effect/platform-node": "4.0.0-rc.112",
|
|
44
91
|
"@effect/vitest": "4.0.0-rc.112",
|
|
45
|
-
"@shivaedev/
|
|
46
|
-
"@
|
|
92
|
+
"@shivaedev/effect-test": "0.3.2",
|
|
93
|
+
"@shivaedev/quality": "0.19.0",
|
|
94
|
+
"@types/node": "24.19.1",
|
|
47
95
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
48
96
|
"effect": "4.0.0-rc.112",
|
|
97
|
+
"esbuild": "0.28.2",
|
|
98
|
+
"happy-dom": "20.14.5",
|
|
49
99
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
50
|
-
"vitest": "4.1.
|
|
100
|
+
"vitest": "4.1.11"
|
|
51
101
|
},
|
|
52
102
|
"peerDependencies": {
|
|
53
103
|
"effect": "4.0.0-rc.112"
|
|
@@ -60,7 +110,7 @@
|
|
|
60
110
|
"provenance": true
|
|
61
111
|
},
|
|
62
112
|
"scripts": {
|
|
63
|
-
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" &&
|
|
113
|
+
"build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
|
|
64
114
|
"ready": "pnpm typecheck && pnpm test && pnpm build",
|
|
65
115
|
"test": "vitest run",
|
|
66
116
|
"typecheck": "tsc --noEmit"
|
package/src/bind.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import type { Duration, Schema } from "effect";
|
|
2
|
-
import { Effect } from "effect";
|
|
2
|
+
import { Effect, Stream } from "effect";
|
|
3
3
|
import type { Headers } from "effect/unstable/http";
|
|
4
4
|
import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
5
5
|
import type * as Atom from "effect/unstable/reactivity/Atom";
|
|
6
6
|
import type * as AtomRpc from "effect/unstable/reactivity/AtomRpc";
|
|
7
7
|
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
8
|
-
import type { Rpc } from "effect/unstable/rpc";
|
|
8
|
+
import type { Rpc, RpcSchema } from "effect/unstable/rpc";
|
|
9
9
|
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";
|
|
10
10
|
import type { Contract, Declared, Tag } from "./contract.ts";
|
|
11
11
|
import { invalidationKeys, type Key, readKeys } from "./keys.ts";
|
|
12
|
-
import type { CommandShape, QueryShape } from "./operation.ts";
|
|
12
|
+
import type { CommandShape, QueryShape, StreamShape } from "./operation.ts";
|
|
13
13
|
|
|
14
14
|
export type Failure<R extends Rpc.Any> =
|
|
15
|
-
R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer
|
|
16
|
-
?
|
|
15
|
+
R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer _Error, infer TMiddleware, infer _Requires>
|
|
16
|
+
? Rpc.ErrorExit<R> | TMiddleware["error"]["Type"] | RpcClientError
|
|
17
17
|
: never;
|
|
18
18
|
|
|
19
19
|
export type RunFailure<R extends Rpc.Any> =
|
|
20
|
-
R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer _Error, infer
|
|
21
|
-
? Failure<R> |
|
|
20
|
+
R extends Rpc.Rpc<infer _Tag, infer _Payload, infer _Success, infer _Error, infer TMiddleware, infer _Requires>
|
|
21
|
+
? Failure<R> | TMiddleware["~ClientError"]
|
|
22
22
|
: never;
|
|
23
23
|
|
|
24
24
|
export interface QueryOptions {
|
|
@@ -27,61 +27,96 @@ export interface QueryOptions {
|
|
|
27
27
|
readonly timeToLive?: Duration.Input;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export interface BoundQuery<R extends Rpc.Any,
|
|
30
|
+
export interface BoundQuery<R extends Rpc.Any, TSelf> {
|
|
31
31
|
readonly query: (payload: Rpc.Payload<R>, options?: QueryOptions) => Atom.Atom<AsyncResult.AsyncResult<Rpc.Success<R>, Failure<R>>>;
|
|
32
|
-
readonly run: (payload: Rpc.Payload<R>) => Effect.Effect<Rpc.Success<R>, RunFailure<R>,
|
|
32
|
+
readonly run: (payload: Rpc.Payload<R>) => Effect.Effect<Rpc.Success<R>, RunFailure<R>, TSelf>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export interface BoundCommand<R extends Rpc.Any,
|
|
36
|
-
readonly run: (payload: Rpc.Payload<R>) => Effect.Effect<Rpc.Success<R>, RunFailure<R>,
|
|
35
|
+
export interface BoundCommand<R extends Rpc.Any, TSelf> {
|
|
36
|
+
readonly run: (payload: Rpc.Payload<R>) => Effect.Effect<Rpc.Success<R>, RunFailure<R>, TSelf | Reactivity.Reactivity>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BoundStream<R extends Rpc.Any, TSelf> {
|
|
40
|
+
readonly run: (payload: Rpc.Payload<R>) => Stream.Stream<Rpc.SuccessChunk<R>, RunFailure<R>, TSelf | Rpc.ServicesClient<R>>;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export type Bound<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
TName extends string,
|
|
45
|
+
TQueries extends readonly QueryShape[],
|
|
46
|
+
TCommands extends readonly CommandShape[],
|
|
47
|
+
TRpcs extends Rpc.Any,
|
|
48
|
+
TSelf,
|
|
49
|
+
TStreams extends readonly StreamShape[] = readonly [],
|
|
45
50
|
> = {
|
|
46
|
-
readonly [
|
|
51
|
+
readonly [TQuery in TQueries[number] as TQuery["name"]]: BoundQuery<Rpc.ExtractTag<TRpcs, Tag<TName, TQuery["name"]>>, TSelf>;
|
|
47
52
|
} & {
|
|
48
|
-
readonly [
|
|
53
|
+
readonly [TCommand in TCommands[number] as TCommand["name"]]: BoundCommand<Rpc.ExtractTag<TRpcs, Tag<TName, TCommand["name"]>>, TSelf>;
|
|
54
|
+
} & {
|
|
55
|
+
readonly [TStream in TStreams[number] as TStream["name"]]: BoundStream<Rpc.ExtractTag<TRpcs, Tag<TName, TStream["name"]>>, TSelf>;
|
|
49
56
|
};
|
|
50
57
|
|
|
51
58
|
type ErasedClient = AtomRpc.AtomRpcClient<unknown, string, Rpc.Rpc<string, Schema.Top, Schema.Top, Schema.Top>>;
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
type ErasedStreamingClient = AtomRpc.AtomRpcClient<
|
|
61
|
+
unknown,
|
|
62
|
+
string,
|
|
63
|
+
Rpc.Rpc<string, Schema.Top, RpcSchema.Stream<Schema.Top, Schema.Top>, Schema.Never>
|
|
64
|
+
>;
|
|
65
|
+
|
|
66
|
+
type MatchingClient<TRpcs extends Rpc.Any, TClientRpcs extends Rpc.Any> = [TRpcs] extends [Extract<TClientRpcs, { readonly _tag: TRpcs["_tag"] }>]
|
|
67
|
+
? [Extract<TClientRpcs, { readonly _tag: TRpcs["_tag"] }>] extends [TRpcs]
|
|
68
|
+
? unknown
|
|
69
|
+
: "Client must preserve the contract RPC types"
|
|
70
|
+
: "Client must implement the contract RPCs";
|
|
71
|
+
|
|
72
|
+
const invalidateAfter = Effect.fn("EffectContract.invalidateAfter")(function* (keys: readonly Key[]) {
|
|
54
73
|
yield* Reactivity.invalidate(invalidationKeys(keys));
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
function boundQuery(service: ErasedClient, tag: string, query: QueryShape) {
|
|
77
|
+
function reads(payload: unknown) {
|
|
78
|
+
return readKeys(query.reads(payload));
|
|
79
|
+
}
|
|
59
80
|
return {
|
|
60
81
|
query: (payload: unknown, options: QueryOptions = {}) => service.query(tag, payload, { ...options, reactivityKeys: reads(payload) }),
|
|
61
82
|
run: (payload: unknown) => service.use((client) => client(tag, payload)),
|
|
62
83
|
};
|
|
63
|
-
}
|
|
84
|
+
}
|
|
64
85
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
function boundCommand(service: ErasedClient, tag: string, command: CommandShape) {
|
|
87
|
+
return {
|
|
88
|
+
run: (payload: unknown) =>
|
|
89
|
+
service.use((client) => client(tag, payload)).pipe(Effect.tap((result) => invalidateAfter(command.invalidates(payload, result)))),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function boundStream(service: ErasedStreamingClient, tag: string) {
|
|
94
|
+
return {
|
|
95
|
+
run: (payload: unknown) => Stream.unwrap(service.use((client) => Effect.succeed(client(tag, payload)))),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
69
98
|
|
|
70
99
|
export function bind<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
100
|
+
TName extends string,
|
|
101
|
+
TQueries extends readonly QueryShape[],
|
|
102
|
+
TCommands extends readonly CommandShape[],
|
|
103
|
+
TRpcs extends Rpc.Any,
|
|
104
|
+
TSelf,
|
|
105
|
+
TId extends string,
|
|
106
|
+
TClientRpcs extends Rpc.Any,
|
|
107
|
+
TStreams extends readonly StreamShape[],
|
|
108
|
+
>(
|
|
109
|
+
contract: Contract<TName, TQueries, TCommands, TRpcs, TStreams>,
|
|
110
|
+
service: AtomRpc.AtomRpcClient<TSelf, TId, TClientRpcs> & MatchingClient<NoInfer<TRpcs>, TClientRpcs>,
|
|
111
|
+
): Bound<TName, TQueries, TCommands, TRpcs, TSelf, TStreams>;
|
|
78
112
|
export function bind(
|
|
79
|
-
contract: { readonly declaration: Declared<string, readonly QueryShape[], readonly CommandShape[]> },
|
|
80
|
-
service:
|
|
113
|
+
contract: { readonly declaration: Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[]> },
|
|
114
|
+
service: AtomRpc.AtomRpcClient<unknown, string, Rpc.Any>,
|
|
81
115
|
): unknown {
|
|
82
|
-
const { name, queries, commands } = contract.declaration;
|
|
116
|
+
const { name, queries, commands, streams } = contract.declaration;
|
|
83
117
|
return Object.fromEntries([
|
|
84
118
|
...queries.map((query) => [query.name, boundQuery(service, `${name}.${query.name}`, query)] as const),
|
|
85
119
|
...commands.map((command) => [command.name, boundCommand(service, `${name}.${command.name}`, command)] as const),
|
|
120
|
+
...streams.map((stream) => [stream.name, boundStream(service, `${name}.${stream.name}`)] as const),
|
|
86
121
|
]);
|
|
87
122
|
}
|
package/src/contract.ts
CHANGED
|
@@ -1,47 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { Schema } from "effect";
|
|
2
|
+
import { Rpc, RpcGroup, type RpcMiddleware, type RpcSchema } from "effect/unstable/rpc";
|
|
3
|
+
import type { CommandShape, OperationShape, QueryShape, StreamShape } from "./operation.ts";
|
|
3
4
|
|
|
4
|
-
export type Tag<
|
|
5
|
+
export type Tag<TContract extends string, TName extends string> = `${TContract}.${TName}`;
|
|
5
6
|
|
|
6
|
-
export type OperationRpc<
|
|
7
|
-
?
|
|
7
|
+
export type OperationRpc<TContract extends string, TOperation> = TOperation extends OperationShape
|
|
8
|
+
? TOperation extends StreamShape
|
|
9
|
+
? Rpc.Rpc<Tag<TContract, TOperation["name"]>, TOperation["payload"], RpcSchema.Stream<TOperation["success"], TOperation["error"]>, Schema.Never>
|
|
10
|
+
: Rpc.Rpc<Tag<TContract, TOperation["name"]>, TOperation["payload"], TOperation["success"], TOperation["error"]>
|
|
8
11
|
: never;
|
|
9
12
|
|
|
10
|
-
export interface Declared<
|
|
11
|
-
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
13
|
+
export interface Declared<
|
|
14
|
+
TName extends string,
|
|
15
|
+
TQueries extends readonly QueryShape[],
|
|
16
|
+
TCommands extends readonly CommandShape[],
|
|
17
|
+
TStreams extends readonly StreamShape[] = readonly [],
|
|
18
|
+
> {
|
|
19
|
+
readonly commands: TCommands;
|
|
20
|
+
readonly name: TName;
|
|
21
|
+
readonly queries: TQueries;
|
|
22
|
+
readonly streams: TStreams;
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
export interface Contract<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
TName extends string,
|
|
27
|
+
TQueries extends readonly QueryShape[],
|
|
28
|
+
TCommands extends readonly CommandShape[],
|
|
29
|
+
TRpcs extends Rpc.Any = OperationRpc<TName, TQueries[number] | TCommands[number]>,
|
|
30
|
+
TStreams extends readonly StreamShape[] = readonly [],
|
|
31
|
+
> extends RpcGroup.RpcGroup<TRpcs> {
|
|
32
|
+
readonly declaration: Declared<TName, TQueries, TCommands, TStreams>;
|
|
33
|
+
middleware: <TM extends RpcMiddleware.AnyService>(middleware: TM) => Contract<TName, TQueries, TCommands, Rpc.AddMiddleware<TRpcs, TM>, TStreams>;
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
type Duplicated<
|
|
27
|
-
infer
|
|
28
|
-
...infer
|
|
36
|
+
type Duplicated<TOperations extends readonly OperationShape[], TSeen extends string = never> = TOperations extends readonly [
|
|
37
|
+
infer THead extends OperationShape,
|
|
38
|
+
...infer TTail extends readonly OperationShape[],
|
|
29
39
|
]
|
|
30
|
-
? (
|
|
40
|
+
? (THead["name"] extends TSeen ? THead["name"] : never) | Duplicated<TTail, TSeen | THead["name"]>
|
|
31
41
|
: never;
|
|
32
42
|
|
|
33
|
-
type Unique<
|
|
43
|
+
type Unique<TOperations extends readonly OperationShape[]> = [Duplicated<TOperations>] extends [never]
|
|
34
44
|
? unknown
|
|
35
|
-
: `Operation names must be unique; duplicated: ${Duplicated<
|
|
45
|
+
: `Operation names must be unique; duplicated: ${Duplicated<TOperations>}`;
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
function assertUnique(operations: readonly OperationShape[]) {
|
|
38
48
|
const duplicated = operations.map(({ name }) => name).filter((name, index, names) => names.indexOf(name) !== index);
|
|
39
49
|
if (duplicated.length > 0) {
|
|
40
50
|
throw new Error(`Operation names must be unique; duplicated: ${[...new Set(duplicated)].join(", ")}`);
|
|
41
51
|
}
|
|
42
|
-
}
|
|
52
|
+
}
|
|
43
53
|
|
|
44
|
-
type AnyDeclared = Declared<string, readonly QueryShape[], readonly CommandShape[]>;
|
|
54
|
+
type AnyDeclared = Declared<string, readonly QueryShape[], readonly CommandShape[], readonly StreamShape[]>;
|
|
45
55
|
|
|
46
56
|
interface Group {
|
|
47
57
|
middleware: (middleware: RpcMiddleware.AnyService) => Group;
|
|
@@ -51,30 +61,40 @@ interface DeclaredGroup extends Group {
|
|
|
51
61
|
readonly declaration: AnyDeclared;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
function withDeclaration(group: Group, declaration: AnyDeclared): DeclaredGroup {
|
|
55
65
|
const nativeMiddleware = group.middleware.bind(group);
|
|
56
66
|
return Object.assign(group, {
|
|
57
67
|
declaration,
|
|
58
68
|
middleware: (middleware: RpcMiddleware.AnyService) => withDeclaration(nativeMiddleware(middleware), declaration),
|
|
59
69
|
});
|
|
60
|
-
}
|
|
70
|
+
}
|
|
61
71
|
|
|
62
72
|
export function contract<
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
73
|
+
const TName extends string,
|
|
74
|
+
const TQueries extends readonly QueryShape[] = readonly [],
|
|
75
|
+
const TCommands extends readonly CommandShape[] = readonly [],
|
|
76
|
+
const TStreams extends readonly StreamShape[] = readonly [],
|
|
66
77
|
>(
|
|
67
|
-
name:
|
|
68
|
-
operations: {
|
|
69
|
-
|
|
78
|
+
name: TName,
|
|
79
|
+
operations: {
|
|
80
|
+
readonly queries?: TQueries & Unique<TQueries>;
|
|
81
|
+
readonly commands?: TCommands & Unique<[...TQueries, ...TCommands]>;
|
|
82
|
+
readonly streams?: TStreams & Unique<[...TQueries, ...TCommands, ...TStreams]>;
|
|
83
|
+
},
|
|
84
|
+
): Contract<TName, TQueries, TCommands, OperationRpc<TName, TQueries[number] | TCommands[number] | TStreams[number]>, TStreams>;
|
|
70
85
|
export function contract(
|
|
71
86
|
name: string,
|
|
72
|
-
operations: { readonly queries?: readonly QueryShape[]; readonly commands?: readonly CommandShape[] },
|
|
87
|
+
operations: { readonly queries?: readonly QueryShape[]; readonly commands?: readonly CommandShape[]; readonly streams?: readonly StreamShape[] },
|
|
73
88
|
): DeclaredGroup {
|
|
74
|
-
const { queries = [], commands = [] } = operations;
|
|
75
|
-
assertUnique([...queries, ...commands]);
|
|
76
|
-
const rpcs = [...queries, ...commands].map((operation) =>
|
|
77
|
-
Rpc.make(`${name}.${operation.name}`, {
|
|
89
|
+
const { queries = [], commands = [], streams = [] } = operations;
|
|
90
|
+
assertUnique([...queries, ...commands, ...streams]);
|
|
91
|
+
const rpcs = [...queries, ...commands, ...streams].map((operation) =>
|
|
92
|
+
Rpc.make(`${name}.${operation.name}`, {
|
|
93
|
+
error: operation.error,
|
|
94
|
+
payload: operation.payload,
|
|
95
|
+
stream: operation.kind === "stream",
|
|
96
|
+
success: operation.success,
|
|
97
|
+
}),
|
|
78
98
|
);
|
|
79
|
-
return withDeclaration(RpcGroup.make(...rpcs), { commands, name, queries });
|
|
99
|
+
return withDeclaration(RpcGroup.make(...rpcs), { commands, name, queries, streams });
|
|
80
100
|
}
|