@shivaedev/effect-contract 0.7.0 → 0.9.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 +27 -0
- package/README.md +45 -26
- package/dist/automaticLive.d.ts +3 -0
- package/dist/automaticLive.d.ts.map +1 -1
- package/dist/automaticLive.js.map +1 -1
- package/dist/bind.d.ts.map +1 -1
- package/dist/bind.js +3 -1
- package/dist/bind.js.map +1 -1
- package/dist/contract.d.ts +3 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/internal/liveInvalidation.d.ts +12 -0
- package/dist/internal/liveInvalidation.d.ts.map +1 -0
- package/dist/internal/liveInvalidation.js +14 -0
- package/dist/internal/liveInvalidation.js.map +1 -0
- package/dist/internal/makeHintPolicy.d.ts +4 -0
- package/dist/internal/makeHintPolicy.d.ts.map +1 -0
- package/dist/internal/makeHintPolicy.js +25 -0
- package/dist/internal/makeHintPolicy.js.map +1 -0
- package/dist/internal/queryReactivity.d.ts +4 -0
- package/dist/internal/queryReactivity.d.ts.map +1 -0
- package/dist/internal/queryReactivity.js +92 -0
- package/dist/internal/queryReactivity.js.map +1 -0
- package/dist/live.d.ts +3 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +38 -3
- package/dist/live.js.map +1 -1
- package/dist/makeLiveServer.d.ts.map +1 -1
- package/dist/makeLiveServer.js +3 -3
- package/dist/makeLiveServer.js.map +1 -1
- package/dist/queryCoalesceWindow.d.ts +4 -0
- package/dist/queryCoalesceWindow.d.ts.map +1 -0
- package/dist/queryCoalesceWindow.js +11 -0
- package/dist/queryCoalesceWindow.js.map +1 -0
- package/dist/readPolicy.d.ts +5 -4
- package/dist/readPolicy.d.ts.map +1 -1
- package/dist/readPolicy.js +3 -0
- package/dist/readPolicy.js.map +1 -1
- package/package.json +5 -3
- package/src/automaticLive.ts +6 -2
- package/src/bind.ts +3 -1
- package/src/contract.ts +8 -2
- package/src/internal/liveInvalidation.ts +21 -0
- package/src/internal/makeHintPolicy.ts +40 -0
- package/src/internal/queryReactivity.ts +99 -0
- package/src/live.ts +39 -3
- package/src/makeLiveServer.ts +5 -3
- package/src/queryCoalesceWindow.ts +13 -0
- package/src/readPolicy.ts +8 -4
package/dist/live.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Effect, Option, Schema, Stream } from "effect";
|
|
1
|
+
import { Duration, Effect, Option, Schema, Stream } from "effect";
|
|
2
2
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
|
+
import { invalidateLive } from "#internal/liveInvalidation.ts";
|
|
4
5
|
import { invalidationKeys, Key, readKeys } from "./keys.js";
|
|
5
6
|
export const LiveHint = Schema.Union([
|
|
6
7
|
Schema.Struct({ _tag: Schema.Literal("Changed"), keys: Schema.Array(Key) }),
|
|
@@ -8,6 +9,7 @@ export const LiveHint = Schema.Union([
|
|
|
8
9
|
]);
|
|
9
10
|
const MAX_PENDING = 256;
|
|
10
11
|
export function live(runtime, options) {
|
|
12
|
+
const coalesceWindow = Atom.keepAlive(Atom.make(options.coalesceWindow ?? "20 millis"));
|
|
11
13
|
const paused = Atom.make(false);
|
|
12
14
|
const status = Atom.make({ connection: "connecting", failure: Option.none(), needsResync: true, pending: 0 });
|
|
13
15
|
const connection = runtime
|
|
@@ -17,7 +19,40 @@ export function live(runtime, options) {
|
|
|
17
19
|
function update(value) {
|
|
18
20
|
get.set(status, { ...get.once(status), ...value });
|
|
19
21
|
}
|
|
22
|
+
const pending = new Set();
|
|
23
|
+
let timer;
|
|
24
|
+
function clear() {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
timer = undefined;
|
|
27
|
+
pending.clear();
|
|
28
|
+
}
|
|
29
|
+
let active = true;
|
|
30
|
+
get.addFinalizer(() => {
|
|
31
|
+
active = false;
|
|
32
|
+
clear();
|
|
33
|
+
});
|
|
34
|
+
function changed(changedKeys) {
|
|
35
|
+
const window = get.once(coalesceWindow);
|
|
36
|
+
const keys = invalidationKeys(changedKeys);
|
|
37
|
+
invalidateLive(reactivity, keys, () => active && !get.once(paused), window);
|
|
38
|
+
const delay = Duration.toMillis(Duration.fromInputUnsafe(window));
|
|
39
|
+
if (delay === 0) {
|
|
40
|
+
invalidateLive(reactivity, keys, () => active && !get.once(paused));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const key of invalidationKeys(changedKeys)) {
|
|
44
|
+
pending.add(key);
|
|
45
|
+
}
|
|
46
|
+
timer ??= setTimeout(() => {
|
|
47
|
+
const queued = [...pending];
|
|
48
|
+
clear();
|
|
49
|
+
if (!get.once(paused)) {
|
|
50
|
+
invalidateLive(reactivity, queued, () => active && !get.once(paused));
|
|
51
|
+
}
|
|
52
|
+
}, delay);
|
|
53
|
+
}
|
|
20
54
|
function resync() {
|
|
55
|
+
clear();
|
|
21
56
|
if (get.once(paused)) {
|
|
22
57
|
update({ needsResync: true });
|
|
23
58
|
return;
|
|
@@ -48,7 +83,7 @@ export function live(runtime, options) {
|
|
|
48
83
|
resync();
|
|
49
84
|
}
|
|
50
85
|
else {
|
|
51
|
-
|
|
86
|
+
changed(hint.keys);
|
|
52
87
|
}
|
|
53
88
|
isFirst = false;
|
|
54
89
|
}
|
|
@@ -57,6 +92,6 @@ export function live(runtime, options) {
|
|
|
57
92
|
yield* Effect.forever(attempt);
|
|
58
93
|
}))
|
|
59
94
|
.pipe(Atom.setIdleTTL(0));
|
|
60
|
-
return { connection, paused, status };
|
|
95
|
+
return { coalesceWindow, connection, paused, status };
|
|
61
96
|
}
|
|
62
97
|
//# sourceMappingURL=live.js.map
|
package/dist/live.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live.js","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"live.js","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"makeLiveServer.d.ts","sourceRoot":"","sources":["../src/makeLiveServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"makeLiveServer.d.ts","sourceRoot":"","sources":["../src/makeLiveServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,KAAK,UAAU,MAAM,8BAA8B,CAAC;AAE3D,OAAO,EAAE,KAAK,GAAG,EAAY,MAAM,WAAW,CAAC;AAG/C,eAAO,MAAM,cAAc;;mBAGJ,SAAS,GAAG,EAAE;;;;;;;;;;;;;gCA8DnC,CAAC"}
|
package/dist/makeLiveServer.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Effect, Queue, Stream } from "effect";
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import { makeHintPolicy } from "#internal/makeHintPolicy.ts";
|
|
3
4
|
import { readKeys } from "./keys.js";
|
|
4
|
-
import { ReadPolicy } from "./readPolicy.js";
|
|
5
5
|
export const makeLiveServer = Effect.gen(function* () {
|
|
6
6
|
const reactivity = yield* Reactivity.Reactivity;
|
|
7
7
|
const resyncKey = `@shivaedev/effect-contract/resync/${crypto.randomUUID()}`;
|
|
8
8
|
function stream(keys) {
|
|
9
9
|
return Stream.unwrap(Effect.gen(function* () {
|
|
10
|
-
const canRead = yield* ReadPolicy;
|
|
11
10
|
const queue = yield* Queue.unbounded();
|
|
11
|
+
const allowedKeys = yield* makeHintPolicy(keys.length, (retryKeys) => Queue.offer(queue, { _tag: "Changed", keys: retryKeys }).pipe(Effect.asVoid));
|
|
12
12
|
const pending = new Map();
|
|
13
13
|
let scheduled = false;
|
|
14
14
|
let resync = false;
|
|
@@ -47,7 +47,7 @@ export const makeLiveServer = Effect.gen(function* () {
|
|
|
47
47
|
yield* Queue.offer(queue, { _tag: "Resync" });
|
|
48
48
|
return Stream.fromQueue(queue).pipe(Stream.mapEffect((hint) => hint._tag === "Resync"
|
|
49
49
|
? Effect.succeed(hint)
|
|
50
|
-
: Effect.map(
|
|
50
|
+
: Effect.map(allowedKeys(hint.keys), (allowed) => ({ _tag: "Changed", keys: allowed }))), Stream.filter((hint) => hint._tag === "Resync" || hint.keys.length > 0));
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
53
|
const resync = Effect.sync(() => {
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
1
|
+
{"version":3,"file":"makeLiveServer.js","sourceRoot":"","sources":["../src/makeLiveServer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryCoalesceWindow.d.ts","sourceRoot":"","sources":["../src/queryCoalesceWindow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAI/C,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAC,CAOrG"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
2
|
+
const windows = new WeakMap();
|
|
3
|
+
export function queryCoalesceWindow(query) {
|
|
4
|
+
let window = windows.get(query);
|
|
5
|
+
if (window === undefined) {
|
|
6
|
+
window = Atom.keepAlive(Atom.make(undefined));
|
|
7
|
+
windows.set(query, window);
|
|
8
|
+
}
|
|
9
|
+
return window;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=queryCoalesceWindow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryCoalesceWindow.js","sourceRoot":"","sources":["../src/queryCoalesceWindow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAE/C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAiE,CAAC;AAE7F,MAAM,UAAU,mBAAmB,CAAI,KAAmB;IACzD,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAA6B,SAAS,CAAC,CAAC,CAAC;QAC1E,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC"}
|
package/dist/readPolicy.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Context, Effect } from "effect";
|
|
1
|
+
import { Context, type Duration, Effect } from "effect";
|
|
2
2
|
import type { Rpc } from "effect/rpc";
|
|
3
3
|
import type { Key } from "./keys.ts";
|
|
4
|
-
export declare const ReadPolicy: Context.Reference<(key: Key) => Effect.Effect<boolean>>;
|
|
5
|
-
export declare const
|
|
4
|
+
export declare const ReadPolicy: Context.Reference<(key: Key) => Effect.Effect<boolean, unknown>>;
|
|
5
|
+
export declare const ReadPolicyMemoWindow: Context.Reference<Duration.Input>;
|
|
6
|
+
export declare const authorizeReads: <A, E, R, TPolicyServices, TDenied, TPolicyError>(effect: Effect.Effect<A, E, R>, request: {
|
|
6
7
|
readonly rpc: Rpc.AnyWithProps;
|
|
7
8
|
readonly payload: unknown;
|
|
8
|
-
}, policy: (key: Key) => Effect.Effect<boolean,
|
|
9
|
+
}, policy: (key: Key) => Effect.Effect<boolean, TPolicyError, TPolicyServices>, denied: Effect.Effect<never, TDenied, never>) => Effect.Effect<A, E | TDenied | TPolicyError, TPolicyServices | Exclude<R, never>>;
|
|
9
10
|
//# sourceMappingURL=readPolicy.d.ts.map
|
package/dist/readPolicy.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"readPolicy.d.ts","sourceRoot":"","sources":["../src/readPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAChE,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,EAAE,OAAO,CAAC,CAEvF,CAAC;AAEH,eAAO,MAAM,oBAAoB,mCAE/B,CAAC;AAEH,eAAO,MAAM,cAAc,GAAgC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,eAAe,EAAE,OAAO,EAAE,YAAY;kBAEhF,GAAG,CAAC,YAAY;sBAAoB,OAAO;iBACtD,GAAG,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,eAAe,CAAC,oIAgB1E,CAAC"}
|
package/dist/readPolicy.js
CHANGED
|
@@ -3,6 +3,9 @@ import { QueryReads } from "./serverMetadata.js";
|
|
|
3
3
|
export const ReadPolicy = Context.Reference("@shivaedev/effect-contract/ReadPolicy", {
|
|
4
4
|
defaultValue: () => () => Effect.succeed(true),
|
|
5
5
|
});
|
|
6
|
+
export const ReadPolicyMemoWindow = Context.Reference("@shivaedev/effect-contract/ReadPolicyMemoWindow", {
|
|
7
|
+
defaultValue: () => "1 second",
|
|
8
|
+
});
|
|
6
9
|
export const authorizeReads = Effect.fnUntraced(function* (effect, request, policy, denied) {
|
|
7
10
|
const context = yield* Effect.context();
|
|
8
11
|
function canRead(key) {
|
package/dist/readPolicy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readPolicy.js","sourceRoot":"","sources":["../src/readPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"readPolicy.js","sourceRoot":"","sources":["../src/readPolicy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAiB,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhE,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAgD,uCAAuC,EAAE;IACnI,YAAY,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;CAC9C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAiB,iDAAiD,EAAE;IACxH,YAAY,EAAE,GAAG,EAAE,CAAC,UAAU;CAC9B,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACxD,MAA8B,EAC9B,OAAsE,EACtE,MAA2E,EAC3E,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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"types": "./dist/*.d.ts",
|
|
29
29
|
"default": "./dist/*.js"
|
|
30
30
|
},
|
|
31
|
+
"./internal/*": null,
|
|
31
32
|
"./package.json": "./package.json"
|
|
32
33
|
},
|
|
33
34
|
"files": [
|
|
@@ -46,8 +47,9 @@
|
|
|
46
47
|
"@effect/platform-node": "4.0.4",
|
|
47
48
|
"@effect/vitest": "4.0.4",
|
|
48
49
|
"@shivaedev/effect-test": "0.7.1",
|
|
49
|
-
"@shivaedev/
|
|
50
|
-
"@shivaedev/
|
|
50
|
+
"@shivaedev/effect-test-sql": "0.8.1",
|
|
51
|
+
"@shivaedev/quality": "0.31.0",
|
|
52
|
+
"@shivaedev/test-story": "0.17.0",
|
|
51
53
|
"@types/node": "24.19.2",
|
|
52
54
|
"effect": "4.0.4",
|
|
53
55
|
"esbuild": "0.28.2",
|
package/src/automaticLive.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Stream, SubscriptionRef } from "effect";
|
|
1
|
+
import { type Duration, Effect, Stream, SubscriptionRef } from "effect";
|
|
2
2
|
import type * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import type * as AtomRpc from "effect/reactivity/AtomRpc";
|
|
4
4
|
import { type Key, readKeys } from "./keys.ts";
|
|
@@ -10,7 +10,11 @@ declare const window: ResumeWindow | undefined;
|
|
|
10
10
|
|
|
11
11
|
export const LiveConnection = Symbol.for("@shivaedev/effect-contract/LiveConnection");
|
|
12
12
|
export interface LiveBound {
|
|
13
|
-
readonly [LiveConnection]: {
|
|
13
|
+
readonly [LiveConnection]: {
|
|
14
|
+
readonly coalesceWindow: Atom.Writable<Duration.Input>;
|
|
15
|
+
readonly connection: Atom.Atom<unknown>;
|
|
16
|
+
readonly status: Atom.Atom<LiveStatus<unknown>>;
|
|
17
|
+
};
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export type ChangesClient = AtomRpc.AtomRpcClient<unknown, string, ReturnType<typeof changesRpc>>;
|
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 { queryReactivity } from "#internal/queryReactivity.ts";
|
|
10
11
|
import { automaticLive, type LiveBound, LiveConnection } from "./automaticLive.ts";
|
|
11
12
|
import type { Contract, Declared, Tag } from "./contract.ts";
|
|
12
13
|
import { invalidationKeys, type Key, readKeys } from "./keys.ts";
|
|
@@ -75,13 +76,14 @@ const invalidateAfter = Effect.fn("EffectContract.invalidateAfter")(function* (k
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
function boundQuery(service: ErasedClient, tag: string, query: QueryShape, track: (keys: readonly Key[]) => void) {
|
|
79
|
+
const reactive = queryReactivity(service.runtime);
|
|
78
80
|
function reads(payload: unknown) {
|
|
79
81
|
return readKeys(query.reads(payload));
|
|
80
82
|
}
|
|
81
83
|
return {
|
|
82
84
|
query: (payload: unknown, options: QueryOptions = {}) => {
|
|
83
85
|
track(query.reads(payload));
|
|
84
|
-
return service.query(tag, payload,
|
|
86
|
+
return reactive(service.query(tag, payload, options), reads(payload));
|
|
85
87
|
},
|
|
86
88
|
run: (payload: unknown) => service.use((client) => client(tag, payload)),
|
|
87
89
|
};
|
package/src/contract.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface Contract<
|
|
|
27
27
|
TName extends string,
|
|
28
28
|
TQueries extends readonly QueryShape[],
|
|
29
29
|
TCommands extends readonly CommandShape[],
|
|
30
|
-
TRpcs extends Rpc.Any = OperationRpc<TName, TQueries[number] | TCommands[number]
|
|
30
|
+
TRpcs extends Rpc.Any = OperationRpc<TName, TQueries[number] | TCommands[number]> | ReturnType<typeof changesRpc<TName>>,
|
|
31
31
|
TStreams extends readonly StreamShape[] = readonly [],
|
|
32
32
|
> extends RpcGroup.RpcGroup<TRpcs> {
|
|
33
33
|
readonly declaration: Declared<TName, TQueries, TCommands, TStreams>;
|
|
@@ -89,7 +89,13 @@ export function contract<
|
|
|
89
89
|
readonly commands?: TCommands & Unique<[...TQueries, ...TCommands]>;
|
|
90
90
|
readonly streams?: TStreams & Unique<[...TQueries, ...TCommands, ...TStreams]>;
|
|
91
91
|
},
|
|
92
|
-
): Contract<
|
|
92
|
+
): Contract<
|
|
93
|
+
TName,
|
|
94
|
+
TQueries,
|
|
95
|
+
TCommands,
|
|
96
|
+
OperationRpc<TName, TQueries[number] | TCommands[number] | TStreams[number]> | ReturnType<typeof changesRpc<TName>>,
|
|
97
|
+
TStreams
|
|
98
|
+
>;
|
|
93
99
|
export function contract(
|
|
94
100
|
name: string,
|
|
95
101
|
operations: { readonly queries?: readonly QueryShape[]; readonly commands?: readonly CommandShape[]; readonly streams?: readonly StreamShape[] },
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Duration } from "effect";
|
|
2
|
+
import type * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
|
|
4
|
+
interface Batch {
|
|
5
|
+
readonly active: () => boolean;
|
|
6
|
+
readonly seen: Set<() => void>;
|
|
7
|
+
readonly window: Duration.Input | undefined;
|
|
8
|
+
}
|
|
9
|
+
export const liveBatches = new WeakMap<Reactivity.Reactivity, Batch>();
|
|
10
|
+
export function liveKeys(keys: readonly string[]) {
|
|
11
|
+
return keys.map((key) => `@shivaedev/live:${key}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function invalidateLive(reactivity: Reactivity.Reactivity, keys: readonly string[], active: () => boolean, window?: Duration.Input) {
|
|
15
|
+
liveBatches.set(reactivity, { active, seen: new Set(), window });
|
|
16
|
+
try {
|
|
17
|
+
reactivity.invalidateUnsafe(window === undefined ? keys : liveKeys(keys));
|
|
18
|
+
} finally {
|
|
19
|
+
liveBatches.delete(reactivity);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Cache, Data, Effect, Exit, Queue } from "effect";
|
|
2
|
+
import type { Key } from "#keys.ts";
|
|
3
|
+
import { ReadPolicy, ReadPolicyMemoWindow } from "#readPolicy.ts";
|
|
4
|
+
|
|
5
|
+
export const makeHintPolicy = Effect.fnUntraced(function* (capacity: number, retry: (keys: readonly Key[]) => Effect.Effect<void>) {
|
|
6
|
+
const policy = yield* ReadPolicy;
|
|
7
|
+
const window = yield* ReadPolicyMemoWindow;
|
|
8
|
+
const cache = yield* Cache.makeWith(policy, {
|
|
9
|
+
capacity: Math.max(1, capacity),
|
|
10
|
+
timeToLive: (exit) => (Exit.isSuccess(exit) ? window : 0),
|
|
11
|
+
});
|
|
12
|
+
const failed = new Map<string, Key>();
|
|
13
|
+
const wake = yield* Queue.unbounded<void>();
|
|
14
|
+
yield* Queue.take(wake).pipe(
|
|
15
|
+
Effect.andThen(Effect.sleep("1 second")),
|
|
16
|
+
Effect.andThen(
|
|
17
|
+
Effect.suspend(() => {
|
|
18
|
+
const keys = [...failed.values()];
|
|
19
|
+
failed.clear();
|
|
20
|
+
return retry(keys);
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
Effect.forever,
|
|
24
|
+
Effect.forkScoped,
|
|
25
|
+
);
|
|
26
|
+
return (keys: readonly Key[]) =>
|
|
27
|
+
Effect.filter(keys, (key) =>
|
|
28
|
+
Cache.get(cache, new Data.Class<Key>(key)).pipe(
|
|
29
|
+
Effect.catch(() =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
if (failed.size === 0) {
|
|
32
|
+
yield* Queue.offer(wake, undefined);
|
|
33
|
+
}
|
|
34
|
+
failed.set(JSON.stringify(key), key);
|
|
35
|
+
return false;
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Duration, Effect } from "effect";
|
|
2
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
3
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
4
|
+
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
5
|
+
import { queryCoalesceWindow } from "#queryCoalesceWindow.ts";
|
|
6
|
+
import { liveBatches, liveKeys } from "./liveInvalidation.ts";
|
|
7
|
+
|
|
8
|
+
export function queryReactivity<R, E>(runtime: Atom.AtomRuntime<R, E>) {
|
|
9
|
+
const service = runtime.atom(Effect.service(Reactivity.Reactivity));
|
|
10
|
+
type Query = Atom.Atom<AsyncResult.AsyncResult<unknown, unknown>>;
|
|
11
|
+
const cached = new WeakMap<Query, Query>();
|
|
12
|
+
return (atom: Query, keys: readonly string[]): Query => {
|
|
13
|
+
const existing = cached.get(atom);
|
|
14
|
+
if (existing !== undefined) {
|
|
15
|
+
return existing;
|
|
16
|
+
}
|
|
17
|
+
const source = Atom.setIdleTTL(
|
|
18
|
+
Atom.readable((get) => atom.read(get), atom.refresh),
|
|
19
|
+
0,
|
|
20
|
+
);
|
|
21
|
+
let wrapped = Atom.transform(
|
|
22
|
+
source,
|
|
23
|
+
(get) => {
|
|
24
|
+
const context = get(service);
|
|
25
|
+
if (!AsyncResult.isSuccess(context)) {
|
|
26
|
+
return get(source);
|
|
27
|
+
}
|
|
28
|
+
const reactivity = context.value;
|
|
29
|
+
let pending: (() => boolean) | undefined;
|
|
30
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
31
|
+
function clear() {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
timer = undefined;
|
|
34
|
+
pending = undefined;
|
|
35
|
+
}
|
|
36
|
+
function refresh(active: () => boolean) {
|
|
37
|
+
if (!active()) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (get.once(source).waiting) {
|
|
41
|
+
pending = active;
|
|
42
|
+
} else {
|
|
43
|
+
pending = undefined;
|
|
44
|
+
get.refresh(source);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function changed() {
|
|
48
|
+
const batch = liveBatches.get(reactivity);
|
|
49
|
+
if (batch === undefined || batch.seen.has(changed)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
batch.seen.add(changed);
|
|
53
|
+
const delay = Duration.toMillis(Duration.fromInputUnsafe(get.once(queryCoalesceWindow(wrapped)) ?? batch.window ?? 0));
|
|
54
|
+
if (delay === 0) {
|
|
55
|
+
clear();
|
|
56
|
+
get.refresh(source);
|
|
57
|
+
} else {
|
|
58
|
+
timer ??= setTimeout(() => {
|
|
59
|
+
timer = undefined;
|
|
60
|
+
refresh(batch.active);
|
|
61
|
+
}, delay);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
get.addFinalizer(clear);
|
|
65
|
+
get.addFinalizer(reactivity.registerUnsafe(liveKeys(keys), changed));
|
|
66
|
+
get.addFinalizer(
|
|
67
|
+
reactivity.registerUnsafe(keys, () => {
|
|
68
|
+
if (!liveBatches.has(reactivity)) {
|
|
69
|
+
clear();
|
|
70
|
+
get.refresh(source);
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
get.subscribe(source, (value) => {
|
|
75
|
+
get.setSelf(value);
|
|
76
|
+
if (pending !== undefined && !value.waiting) {
|
|
77
|
+
const active = pending;
|
|
78
|
+
pending = undefined;
|
|
79
|
+
if (active()) {
|
|
80
|
+
get.refresh(source);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return get.once(source);
|
|
85
|
+
},
|
|
86
|
+
{ initialValueTarget: source },
|
|
87
|
+
);
|
|
88
|
+
if (atom.keepAlive) {
|
|
89
|
+
wrapped = Atom.keepAlive(wrapped);
|
|
90
|
+
} else if (atom.idleTTL !== undefined) {
|
|
91
|
+
wrapped = Atom.setIdleTTL(wrapped, atom.idleTTL);
|
|
92
|
+
}
|
|
93
|
+
if (Atom.isSerializable(atom)) {
|
|
94
|
+
Object.assign(wrapped, { [Atom.SerializableTypeId]: atom[Atom.SerializableTypeId] });
|
|
95
|
+
}
|
|
96
|
+
cached.set(atom, wrapped);
|
|
97
|
+
return wrapped;
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/live.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type Cause,
|
|
1
|
+
import { type Cause, Duration, Effect, Option, Schema, Stream } from "effect";
|
|
2
2
|
import * as Atom from "effect/reactivity/Atom";
|
|
3
3
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
4
|
+
import { invalidateLive } from "#internal/liveInvalidation.ts";
|
|
4
5
|
import { invalidationKeys, Key, readKeys } from "./keys.ts";
|
|
5
6
|
|
|
6
7
|
export const LiveHint = Schema.Union([
|
|
@@ -15,6 +16,7 @@ export interface LiveStatus<E> {
|
|
|
15
16
|
readonly pending: number;
|
|
16
17
|
}
|
|
17
18
|
export interface LiveOptions<E, R> {
|
|
19
|
+
readonly coalesceWindow?: Duration.Input;
|
|
18
20
|
readonly resume?: Atom.Atom<unknown>;
|
|
19
21
|
readonly resyncKeys: readonly Key[] | (() => readonly Key[]);
|
|
20
22
|
readonly retryDelay?: Duration.Input;
|
|
@@ -24,6 +26,7 @@ export interface LiveOptions<E, R> {
|
|
|
24
26
|
const MAX_PENDING = 256;
|
|
25
27
|
|
|
26
28
|
export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeError>, options: LiveOptions<E, R>) {
|
|
29
|
+
const coalesceWindow = Atom.keepAlive(Atom.make<Duration.Input>(options.coalesceWindow ?? "20 millis"));
|
|
27
30
|
const paused = Atom.make(false);
|
|
28
31
|
const status = Atom.make<LiveStatus<E>>({ connection: "connecting", failure: Option.none(), needsResync: true, pending: 0 });
|
|
29
32
|
const connection = runtime
|
|
@@ -34,7 +37,40 @@ export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeE
|
|
|
34
37
|
function update(value: Partial<LiveStatus<E>>) {
|
|
35
38
|
get.set(status, { ...get.once(status), ...value });
|
|
36
39
|
}
|
|
40
|
+
const pending = new Set<string>();
|
|
41
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
42
|
+
function clear() {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
timer = undefined;
|
|
45
|
+
pending.clear();
|
|
46
|
+
}
|
|
47
|
+
let active = true;
|
|
48
|
+
get.addFinalizer(() => {
|
|
49
|
+
active = false;
|
|
50
|
+
clear();
|
|
51
|
+
});
|
|
52
|
+
function changed(changedKeys: readonly Key[]) {
|
|
53
|
+
const window = get.once(coalesceWindow);
|
|
54
|
+
const keys = invalidationKeys(changedKeys);
|
|
55
|
+
invalidateLive(reactivity, keys, () => active && !get.once(paused), window);
|
|
56
|
+
const delay = Duration.toMillis(Duration.fromInputUnsafe(window));
|
|
57
|
+
if (delay === 0) {
|
|
58
|
+
invalidateLive(reactivity, keys, () => active && !get.once(paused));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const key of invalidationKeys(changedKeys)) {
|
|
62
|
+
pending.add(key);
|
|
63
|
+
}
|
|
64
|
+
timer ??= setTimeout(() => {
|
|
65
|
+
const queued = [...pending];
|
|
66
|
+
clear();
|
|
67
|
+
if (!get.once(paused)) {
|
|
68
|
+
invalidateLive(reactivity, queued, () => active && !get.once(paused));
|
|
69
|
+
}
|
|
70
|
+
}, delay);
|
|
71
|
+
}
|
|
37
72
|
function resync() {
|
|
73
|
+
clear();
|
|
38
74
|
if (get.once(paused)) {
|
|
39
75
|
update({ needsResync: true });
|
|
40
76
|
return;
|
|
@@ -63,7 +99,7 @@ export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeE
|
|
|
63
99
|
} else if (isFirst || hint._tag === "Resync") {
|
|
64
100
|
resync();
|
|
65
101
|
} else {
|
|
66
|
-
|
|
102
|
+
changed(hint.keys);
|
|
67
103
|
}
|
|
68
104
|
isFirst = false;
|
|
69
105
|
}
|
|
@@ -79,5 +115,5 @@ export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeE
|
|
|
79
115
|
}),
|
|
80
116
|
)
|
|
81
117
|
.pipe(Atom.setIdleTTL(0));
|
|
82
|
-
return { connection, paused, status };
|
|
118
|
+
return { coalesceWindow, connection, paused, status };
|
|
83
119
|
}
|
package/src/makeLiveServer.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Effect, Queue, Stream } from "effect";
|
|
2
2
|
import * as Reactivity from "effect/reactivity/Reactivity";
|
|
3
|
+
import { makeHintPolicy } from "#internal/makeHintPolicy.ts";
|
|
3
4
|
import { type Key, readKeys } from "./keys.ts";
|
|
4
5
|
import type { LiveHint } from "./live.ts";
|
|
5
|
-
import { ReadPolicy } from "./readPolicy.ts";
|
|
6
6
|
|
|
7
7
|
export const makeLiveServer = Effect.gen(function* () {
|
|
8
8
|
const reactivity = yield* Reactivity.Reactivity;
|
|
@@ -10,8 +10,10 @@ export const makeLiveServer = Effect.gen(function* () {
|
|
|
10
10
|
function stream(keys: readonly Key[]) {
|
|
11
11
|
return Stream.unwrap(
|
|
12
12
|
Effect.gen(function* () {
|
|
13
|
-
const canRead = yield* ReadPolicy;
|
|
14
13
|
const queue = yield* Queue.unbounded<LiveHint>();
|
|
14
|
+
const allowedKeys = yield* makeHintPolicy(keys.length, (retryKeys) =>
|
|
15
|
+
Queue.offer(queue, { _tag: "Changed", keys: retryKeys }).pipe(Effect.asVoid),
|
|
16
|
+
);
|
|
15
17
|
const pending = new Map<string, Key>();
|
|
16
18
|
let scheduled = false;
|
|
17
19
|
let resync = false;
|
|
@@ -56,7 +58,7 @@ export const makeLiveServer = Effect.gen(function* () {
|
|
|
56
58
|
Stream.mapEffect((hint) =>
|
|
57
59
|
hint._tag === "Resync"
|
|
58
60
|
? Effect.succeed(hint)
|
|
59
|
-
: Effect.map(
|
|
61
|
+
: Effect.map(allowedKeys(hint.keys), (allowed): LiveHint => ({ _tag: "Changed", keys: allowed })),
|
|
60
62
|
),
|
|
61
63
|
Stream.filter((hint) => hint._tag === "Resync" || hint.keys.length > 0),
|
|
62
64
|
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Duration } from "effect";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
|
|
4
|
+
const windows = new WeakMap<Atom.Atom<unknown>, Atom.Writable<Duration.Input | undefined>>();
|
|
5
|
+
|
|
6
|
+
export function queryCoalesceWindow<A>(query: Atom.Atom<A>): Atom.Writable<Duration.Input | undefined> {
|
|
7
|
+
let window = windows.get(query);
|
|
8
|
+
if (window === undefined) {
|
|
9
|
+
window = Atom.keepAlive(Atom.make<Duration.Input | undefined>(undefined));
|
|
10
|
+
windows.set(query, window);
|
|
11
|
+
}
|
|
12
|
+
return window;
|
|
13
|
+
}
|
package/src/readPolicy.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { Context, Effect, Option } from "effect";
|
|
1
|
+
import { Context, type Duration, Effect, Option } from "effect";
|
|
2
2
|
import type { Rpc } from "effect/rpc";
|
|
3
3
|
import type { Key } from "./keys.ts";
|
|
4
4
|
import { QueryReads } from "./serverMetadata.ts";
|
|
5
5
|
|
|
6
|
-
export const ReadPolicy = Context.Reference<(key: Key) => Effect.Effect<boolean>>("@shivaedev/effect-contract/ReadPolicy", {
|
|
6
|
+
export const ReadPolicy = Context.Reference<(key: Key) => Effect.Effect<boolean, unknown>>("@shivaedev/effect-contract/ReadPolicy", {
|
|
7
7
|
defaultValue: () => () => Effect.succeed(true),
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const ReadPolicyMemoWindow = Context.Reference<Duration.Input>("@shivaedev/effect-contract/ReadPolicyMemoWindow", {
|
|
11
|
+
defaultValue: () => "1 second",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const authorizeReads = Effect.fnUntraced(function* <A, E, R, TPolicyServices, TDenied, TPolicyError>(
|
|
11
15
|
effect: Effect.Effect<A, E, R>,
|
|
12
16
|
request: { readonly rpc: Rpc.AnyWithProps; readonly payload: unknown },
|
|
13
|
-
policy: (key: Key) => Effect.Effect<boolean,
|
|
17
|
+
policy: (key: Key) => Effect.Effect<boolean, TPolicyError, TPolicyServices>,
|
|
14
18
|
denied: Effect.Effect<never, TDenied>,
|
|
15
19
|
) {
|
|
16
20
|
const context = yield* Effect.context<TPolicyServices>();
|