@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/src/keys.ts
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Schema } from "effect";
|
|
2
2
|
|
|
3
3
|
export type Identity = string | number;
|
|
4
4
|
|
|
5
|
-
export interface ListKey<
|
|
5
|
+
export interface ListKey<TName extends string = string> {
|
|
6
6
|
readonly _tag: "List";
|
|
7
|
-
readonly collection:
|
|
7
|
+
readonly collection: TName;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export interface ItemKey<
|
|
10
|
+
export interface ItemKey<TName extends string = string> {
|
|
11
11
|
readonly _tag: "Item";
|
|
12
|
-
readonly collection:
|
|
12
|
+
readonly collection: TName;
|
|
13
13
|
readonly id: Identity;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export type Key = ListKey | ItemKey;
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
export const Key = Schema.Union([
|
|
19
|
+
Schema.Struct({ _tag: Schema.Literal("List"), collection: Schema.String }),
|
|
20
|
+
Schema.Struct({ _tag: Schema.Literal("Item"), collection: Schema.String, id: Schema.Union([Schema.String, Schema.Number]) }),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export interface Collection<TName extends string, TId extends Identity> {
|
|
24
|
+
readonly item: (id: TId) => ItemKey<TName>;
|
|
25
|
+
readonly list: ListKey<TName>;
|
|
26
|
+
readonly name: TName;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
export const collection = <const
|
|
25
|
-
name:
|
|
26
|
-
_id:
|
|
27
|
-
): Collection<
|
|
29
|
+
export const collection = <const TName extends string, TId extends Schema.Top & { readonly Type: Identity }>(
|
|
30
|
+
name: TName,
|
|
31
|
+
_id: TId,
|
|
32
|
+
): Collection<TName, TId["Type"]> => ({
|
|
28
33
|
item: (id) => ({ _tag: "Item", collection: name, id }),
|
|
29
34
|
list: { _tag: "List", collection: name },
|
|
30
35
|
name,
|
|
31
36
|
});
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
function own(key: Key): string {
|
|
39
|
+
return key._tag === "List" ? key.collection : `${key.collection}:${key.id}`;
|
|
40
|
+
}
|
|
34
41
|
|
|
35
42
|
export const readKeys = (keys: readonly Key[]): readonly string[] => [...new Set(keys.map(own))];
|
|
36
43
|
|
package/src/live.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type Cause, type Duration, Effect, Option, Schema, Stream } from "effect";
|
|
2
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
3
|
+
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
4
|
+
import { invalidationKeys, Key, readKeys } from "./keys.ts";
|
|
5
|
+
|
|
6
|
+
export const LiveHint = Schema.Union([
|
|
7
|
+
Schema.Struct({ _tag: Schema.Literal("Changed"), keys: Schema.Array(Key) }),
|
|
8
|
+
Schema.Struct({ _tag: Schema.Literal("Resync") }),
|
|
9
|
+
]);
|
|
10
|
+
export type LiveHint = typeof LiveHint.Type;
|
|
11
|
+
export interface LiveStatus<E> {
|
|
12
|
+
readonly connection: "connecting" | "live" | "reconnecting";
|
|
13
|
+
readonly failure: Option.Option<Cause.Cause<E>>;
|
|
14
|
+
readonly needsResync: boolean;
|
|
15
|
+
readonly pending: number;
|
|
16
|
+
}
|
|
17
|
+
export interface LiveOptions<E, R> {
|
|
18
|
+
readonly resume?: Atom.Atom<unknown>;
|
|
19
|
+
readonly resyncKeys: readonly Key[];
|
|
20
|
+
readonly retryDelay?: Duration.Input;
|
|
21
|
+
readonly stream: Stream.Stream<LiveHint, E, R>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MAX_PENDING = 256;
|
|
25
|
+
|
|
26
|
+
export function live<R, E, TRuntimeError>(runtime: Atom.AtomRuntime<R, TRuntimeError>, options: LiveOptions<E, R>) {
|
|
27
|
+
const paused = Atom.make(false);
|
|
28
|
+
const status = Atom.make<LiveStatus<E>>({ connection: "connecting", failure: Option.none(), needsResync: true, pending: 0 });
|
|
29
|
+
const connection = runtime
|
|
30
|
+
.atom((get) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
get.mount(status);
|
|
33
|
+
const reactivity = yield* Reactivity.Reactivity;
|
|
34
|
+
function update(value: Partial<LiveStatus<E>>) {
|
|
35
|
+
get.set(status, { ...get.once(status), ...value });
|
|
36
|
+
}
|
|
37
|
+
function resync() {
|
|
38
|
+
if (get.once(paused)) {
|
|
39
|
+
update({ needsResync: true });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
reactivity.invalidateUnsafe(readKeys(options.resyncKeys));
|
|
43
|
+
update({ needsResync: false, pending: 0 });
|
|
44
|
+
}
|
|
45
|
+
get.subscribe(paused, (value) => {
|
|
46
|
+
if (!value) {
|
|
47
|
+
resync();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (options.resume !== undefined) {
|
|
51
|
+
// A subscription alone does not initialize a derived signal's dependencies.
|
|
52
|
+
get.once(options.resume);
|
|
53
|
+
get.subscribe(options.resume, resync);
|
|
54
|
+
}
|
|
55
|
+
const attempt = Effect.suspend(() => {
|
|
56
|
+
let isFirst = true;
|
|
57
|
+
function observe(hint: LiveHint) {
|
|
58
|
+
update({ connection: "live", failure: Option.none() });
|
|
59
|
+
if (get.once(paused)) {
|
|
60
|
+
update({ needsResync: true, pending: Math.min(MAX_PENDING, get.once(status).pending + 1) });
|
|
61
|
+
} else if (isFirst || hint._tag === "Resync") {
|
|
62
|
+
resync();
|
|
63
|
+
} else {
|
|
64
|
+
reactivity.invalidateUnsafe(invalidationKeys(hint.keys));
|
|
65
|
+
}
|
|
66
|
+
isFirst = false;
|
|
67
|
+
}
|
|
68
|
+
return options.stream.pipe(
|
|
69
|
+
Stream.runForEach((hint) => Effect.sync(() => observe(hint))),
|
|
70
|
+
Effect.scoped,
|
|
71
|
+
Effect.catchCause((failure) => Effect.sync(() => update({ failure: Option.some(failure) }))),
|
|
72
|
+
Effect.andThen(Effect.sync(() => update({ connection: "reconnecting", needsResync: true }))),
|
|
73
|
+
Effect.andThen(Effect.sleep(options.retryDelay ?? "1 second")),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
yield* Effect.forever(attempt);
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
.pipe(Atom.setIdleTTL(0));
|
|
80
|
+
return { connection, paused, status };
|
|
81
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
|
+
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
3
|
+
import { type Key, readKeys } from "./keys.ts";
|
|
4
|
+
import type { LiveHint } from "./live.ts";
|
|
5
|
+
|
|
6
|
+
export const makeLiveServer = Effect.gen(function* () {
|
|
7
|
+
const reactivity = yield* Reactivity.Reactivity;
|
|
8
|
+
const resyncKey = `@shivaedev/effect-contract/resync/${crypto.randomUUID()}`;
|
|
9
|
+
let generation = 0;
|
|
10
|
+
function stream(keys: readonly Key[]) {
|
|
11
|
+
return Stream.unwrap(
|
|
12
|
+
Effect.sync(() => {
|
|
13
|
+
let seen = -1;
|
|
14
|
+
return reactivity.stream(
|
|
15
|
+
[...readKeys(keys), resyncKey],
|
|
16
|
+
Effect.sync((): LiveHint => {
|
|
17
|
+
if (seen !== generation) {
|
|
18
|
+
seen = generation;
|
|
19
|
+
return { _tag: "Resync" };
|
|
20
|
+
}
|
|
21
|
+
return { _tag: "Changed", keys };
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const resync = Effect.sync(() => {
|
|
28
|
+
generation += 1;
|
|
29
|
+
reactivity.invalidateUnsafe([resyncKey]);
|
|
30
|
+
});
|
|
31
|
+
return { resync, stream };
|
|
32
|
+
});
|
package/src/operation.ts
CHANGED
|
@@ -3,15 +3,15 @@ import type { Bivariant } from "@shivaedev/types/bivariant.ts";
|
|
|
3
3
|
import type { Key } from "./keys.ts";
|
|
4
4
|
import { type MatchingTags, type Reject, type RejectionSpecs, type Rejections, type RejectionUnion, rejectionSet } from "./rejection.ts";
|
|
5
5
|
|
|
6
|
-
export type PayloadSchema<
|
|
7
|
-
? Schema.Struct<
|
|
8
|
-
:
|
|
6
|
+
export type PayloadSchema<TPayload extends Schema.Top | Schema.Struct.Fields> = TPayload extends Schema.Struct.Fields
|
|
7
|
+
? Schema.Struct<TPayload>
|
|
8
|
+
: TPayload;
|
|
9
9
|
|
|
10
10
|
type NoRejections = Record<never, never>;
|
|
11
11
|
|
|
12
12
|
export interface OperationShape {
|
|
13
13
|
readonly error: Schema.Top;
|
|
14
|
-
readonly kind: "query" | "command";
|
|
14
|
+
readonly kind: "query" | "command" | "stream";
|
|
15
15
|
readonly name: string;
|
|
16
16
|
readonly payload: Schema.Top;
|
|
17
17
|
readonly Rejection: object;
|
|
@@ -25,38 +25,49 @@ export interface QueryShape extends OperationShape {
|
|
|
25
25
|
reads: Bivariant<(payload: unknown) => readonly Key[]>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface StreamShape extends OperationShape {
|
|
29
|
+
readonly kind: "stream";
|
|
30
|
+
reads: Bivariant<(payload: unknown) => readonly Key[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export interface CommandShape extends OperationShape {
|
|
29
34
|
invalidates: Bivariant<(payload: unknown, result: unknown) => readonly Key[]>;
|
|
30
35
|
readonly kind: "command";
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
interface Operation<
|
|
38
|
+
interface Operation<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
|
|
34
39
|
extends OperationShape {
|
|
35
|
-
readonly error: RejectionUnion<
|
|
36
|
-
readonly name:
|
|
37
|
-
readonly payload:
|
|
38
|
-
readonly Rejection: Rejections<
|
|
39
|
-
readonly reject: Reject<
|
|
40
|
-
readonly rejections:
|
|
41
|
-
readonly success:
|
|
40
|
+
readonly error: RejectionUnion<TSpecs>;
|
|
41
|
+
readonly name: TName;
|
|
42
|
+
readonly payload: TPayload;
|
|
43
|
+
readonly Rejection: Rejections<TSpecs>;
|
|
44
|
+
readonly reject: Reject<TSpecs>;
|
|
45
|
+
readonly rejections: TSpecs;
|
|
46
|
+
readonly success: TSuccess;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
export interface Query<
|
|
45
|
-
extends Operation<
|
|
49
|
+
export interface Query<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
|
|
50
|
+
extends Operation<TName, TPayload, TSuccess, TSpecs> {
|
|
46
51
|
readonly kind: "query";
|
|
47
|
-
reads: (payload:
|
|
52
|
+
reads: (payload: TPayload["Type"]) => readonly Key[];
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
export interface
|
|
51
|
-
extends Operation<
|
|
52
|
-
|
|
55
|
+
export interface Streaming<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
|
|
56
|
+
extends Operation<TName, TPayload, TSuccess, TSpecs> {
|
|
57
|
+
readonly kind: "stream";
|
|
58
|
+
reads: (payload: TPayload["Type"]) => readonly Key[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Command<TName extends string, TPayload extends Schema.Top, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs>
|
|
62
|
+
extends Operation<TName, TPayload, TSuccess, TSpecs> {
|
|
63
|
+
invalidates: (payload: TPayload["Type"], result: TSuccess["Type"]) => readonly Key[];
|
|
53
64
|
readonly kind: "command";
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
interface Declaration<
|
|
57
|
-
readonly payload?:
|
|
58
|
-
readonly rejections?:
|
|
59
|
-
readonly success?:
|
|
67
|
+
interface Declaration<TPayload extends Schema.Top | Schema.Struct.Fields, TSuccess extends Schema.Top, TSpecs extends RejectionSpecs> {
|
|
68
|
+
readonly payload?: TPayload;
|
|
69
|
+
readonly rejections?: TSpecs & MatchingTags<TSpecs>;
|
|
70
|
+
readonly success?: TSuccess;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
interface Loose {
|
|
@@ -65,14 +76,14 @@ interface Loose {
|
|
|
65
76
|
readonly success?: Schema.Top;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
function payloadSchema(payload: Loose["payload"]): Schema.Top {
|
|
69
80
|
if (payload === undefined) {
|
|
70
81
|
return Schema.Void;
|
|
71
82
|
}
|
|
72
83
|
return Schema.isSchema(payload) ? payload : Schema.Struct(payload);
|
|
73
|
-
}
|
|
84
|
+
}
|
|
74
85
|
|
|
75
|
-
|
|
86
|
+
function operation<TKind extends OperationShape["kind"]>(kind: TKind, name: string, declaration: Loose) {
|
|
76
87
|
const { payload, success, rejections = {} } = declaration;
|
|
77
88
|
return {
|
|
78
89
|
kind,
|
|
@@ -82,37 +93,53 @@ const operation = <Kind extends OperationShape["kind"]>(kind: Kind, name: string
|
|
|
82
93
|
success: success ?? Schema.Void,
|
|
83
94
|
...rejectionSet(rejections),
|
|
84
95
|
};
|
|
85
|
-
}
|
|
96
|
+
}
|
|
86
97
|
|
|
87
98
|
export function query<
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
99
|
+
const TName extends string,
|
|
100
|
+
TPayload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
|
|
101
|
+
TSuccess extends Schema.Top = Schema.Void,
|
|
102
|
+
const TSpecs extends RejectionSpecs = NoRejections,
|
|
92
103
|
>(
|
|
93
|
-
name:
|
|
94
|
-
declaration: Declaration<
|
|
95
|
-
readonly reads: (payload: PayloadSchema<
|
|
104
|
+
name: TName,
|
|
105
|
+
declaration: Declaration<TPayload, TSuccess, TSpecs> & {
|
|
106
|
+
readonly reads: (payload: PayloadSchema<TPayload>["Type"]) => readonly Key[];
|
|
96
107
|
},
|
|
97
|
-
): NoInfer<Query<
|
|
108
|
+
): NoInfer<Query<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
|
|
98
109
|
export function query(name: string, declaration: Loose & { readonly reads: (payload: unknown) => readonly Key[] }): QueryShape {
|
|
99
110
|
return { ...operation("query", name, declaration), reads: declaration.reads };
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
export function command<
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
114
|
+
const TName extends string,
|
|
115
|
+
TPayload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
|
|
116
|
+
TSuccess extends Schema.Top = Schema.Void,
|
|
117
|
+
const TSpecs extends RejectionSpecs = NoRejections,
|
|
107
118
|
>(
|
|
108
|
-
name:
|
|
109
|
-
declaration: Declaration<
|
|
110
|
-
readonly invalidates: (payload: PayloadSchema<
|
|
119
|
+
name: TName,
|
|
120
|
+
declaration: Declaration<TPayload, TSuccess, TSpecs> & {
|
|
121
|
+
readonly invalidates: (payload: PayloadSchema<TPayload>["Type"], result: TSuccess["Type"]) => readonly Key[];
|
|
111
122
|
},
|
|
112
|
-
): NoInfer<Command<
|
|
123
|
+
): NoInfer<Command<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
|
|
113
124
|
export function command(
|
|
114
125
|
name: string,
|
|
115
126
|
declaration: Loose & { readonly invalidates: (payload: unknown, result: unknown) => readonly Key[] },
|
|
116
127
|
): CommandShape {
|
|
117
128
|
return { ...operation("command", name, declaration), invalidates: declaration.invalidates };
|
|
118
129
|
}
|
|
130
|
+
|
|
131
|
+
export function stream<
|
|
132
|
+
const TName extends string,
|
|
133
|
+
TPayload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
|
|
134
|
+
TSuccess extends Schema.Top = Schema.Void,
|
|
135
|
+
const TSpecs extends RejectionSpecs = NoRejections,
|
|
136
|
+
>(
|
|
137
|
+
name: TName,
|
|
138
|
+
declaration: Declaration<TPayload, TSuccess, TSpecs> & {
|
|
139
|
+
readonly success: TSuccess;
|
|
140
|
+
readonly reads: (payload: PayloadSchema<TPayload>["Type"]) => readonly Key[];
|
|
141
|
+
},
|
|
142
|
+
): NoInfer<Streaming<TName, PayloadSchema<TPayload>, TSuccess, TSpecs>>;
|
|
143
|
+
export function stream(name: string, declaration: Loose & { readonly reads: (payload: unknown) => readonly Key[] }): StreamShape {
|
|
144
|
+
return { ...operation("stream", name, declaration), reads: declaration.reads };
|
|
145
|
+
}
|
package/src/rejection.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type Cause, Effect, Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
export type RejectionValue<
|
|
3
|
+
export type RejectionValue<TTag extends string, TFields extends Schema.Struct.Fields> = Schema.TaggedStruct<TTag, TFields>["Type"] &
|
|
4
|
+
Cause.YieldableError;
|
|
4
5
|
|
|
5
|
-
export type RejectionClass<
|
|
6
|
-
RejectionValue<
|
|
7
|
-
Schema.TaggedStruct<
|
|
6
|
+
export type RejectionClass<TTag extends string, TFields extends Schema.Struct.Fields> = Schema.Class<
|
|
7
|
+
RejectionValue<TTag, TFields>,
|
|
8
|
+
Schema.TaggedStruct<TTag, TFields>,
|
|
8
9
|
Cause.YieldableError
|
|
9
10
|
>;
|
|
10
11
|
|
|
@@ -14,42 +15,42 @@ export interface RejectionSpecs {
|
|
|
14
15
|
readonly [tag: string]: Schema.Struct.Fields | TaggedRejection;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export type Rejections<
|
|
18
|
-
readonly [
|
|
19
|
-
?
|
|
20
|
-
:
|
|
21
|
-
? RejectionClass<
|
|
18
|
+
export type Rejections<TSpecs extends RejectionSpecs> = {
|
|
19
|
+
readonly [TTag in keyof TSpecs & string]: TSpecs[TTag] extends TaggedRejection
|
|
20
|
+
? TSpecs[TTag]
|
|
21
|
+
: TSpecs[TTag] extends Schema.Struct.Fields
|
|
22
|
+
? RejectionClass<TTag, TSpecs[TTag]>
|
|
22
23
|
: never;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
export type RejectedBy<
|
|
26
|
-
[
|
|
27
|
-
}[keyof
|
|
26
|
+
export type RejectedBy<TSpecs extends RejectionSpecs> = {
|
|
27
|
+
[TTag in keyof TSpecs & string]: Rejections<TSpecs>[TTag]["Type"];
|
|
28
|
+
}[keyof TSpecs & string];
|
|
28
29
|
|
|
29
|
-
export type Reject<
|
|
30
|
-
readonly [
|
|
31
|
-
...args: infer
|
|
32
|
-
) => infer
|
|
33
|
-
? (...args:
|
|
30
|
+
export type Reject<TSpecs extends RejectionSpecs> = {
|
|
31
|
+
readonly [TTag in keyof TSpecs & string]: Rejections<TSpecs>[TTag] extends new (
|
|
32
|
+
...args: infer TArguments
|
|
33
|
+
) => infer TValue
|
|
34
|
+
? (...args: TArguments) => Effect.Effect<never, TValue>
|
|
34
35
|
: never;
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
export type RejectionUnion<
|
|
38
|
+
export type RejectionUnion<TSpecs extends RejectionSpecs> = [keyof TSpecs & string] extends [never]
|
|
38
39
|
? Schema.Never
|
|
39
|
-
: Schema.Union<readonly Rejections<
|
|
40
|
+
: Schema.Union<readonly Rejections<TSpecs>[keyof TSpecs & string][]>;
|
|
40
41
|
|
|
41
|
-
export type MatchingTags<
|
|
42
|
-
readonly [
|
|
43
|
-
?
|
|
44
|
-
?
|
|
45
|
-
: `The rejection class under "${
|
|
46
|
-
:
|
|
42
|
+
export type MatchingTags<TSpecs extends RejectionSpecs> = {
|
|
43
|
+
readonly [TTag in keyof TSpecs]: TSpecs[TTag] extends TaggedRejection
|
|
44
|
+
? TSpecs[TTag]["Type"] extends { readonly _tag: TTag }
|
|
45
|
+
? TSpecs[TTag]
|
|
46
|
+
: `The rejection class under "${TTag & string}" must have that _tag`
|
|
47
|
+
: TSpecs[TTag];
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
export interface RejectionSet<
|
|
50
|
-
readonly error: RejectionUnion<
|
|
51
|
-
readonly Rejection: Rejections<
|
|
52
|
-
readonly reject: Reject<
|
|
50
|
+
export interface RejectionSet<TSpecs extends RejectionSpecs> {
|
|
51
|
+
readonly error: RejectionUnion<TSpecs>;
|
|
52
|
+
readonly Rejection: Rejections<TSpecs>;
|
|
53
|
+
readonly reject: Reject<TSpecs>;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
interface LooseRejectionSet {
|
|
@@ -58,7 +59,7 @@ interface LooseRejectionSet {
|
|
|
58
59
|
readonly reject: { readonly [tag: string]: (...args: readonly unknown[]) => Effect.Effect<never, unknown> };
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export function rejectionSet<const
|
|
62
|
+
export function rejectionSet<const TSpecs extends RejectionSpecs>(specs: TSpecs): RejectionSet<TSpecs>;
|
|
62
63
|
export function rejectionSet(specs: RejectionSpecs): LooseRejectionSet {
|
|
63
64
|
const classes = Object.entries(specs).map(([tag, spec]): readonly [string, TaggedRejection] => [
|
|
64
65
|
tag,
|
|
@@ -73,16 +74,18 @@ export function rejectionSet(specs: RejectionSpecs): LooseRejectionSet {
|
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
export type FieldRejection<
|
|
77
|
-
field: Schema.Literals<readonly
|
|
77
|
+
export type FieldRejection<TField extends string> = Readonly<{
|
|
78
|
+
field: Schema.Literals<readonly TField[]>;
|
|
78
79
|
message: Schema.String;
|
|
79
80
|
}>;
|
|
80
81
|
|
|
81
|
-
export function fieldRejection<const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
export function fieldRejection<const TFields extends Schema.Struct.Fields>(struct: {
|
|
83
|
+
readonly fields: TFields;
|
|
84
|
+
}): FieldRejection<keyof TFields & string>;
|
|
85
|
+
export function fieldRejection<const TFields extends Schema.Struct.Fields, const TField extends keyof TFields & string>(
|
|
86
|
+
struct: { readonly fields: TFields },
|
|
87
|
+
fields: readonly TField[],
|
|
88
|
+
): FieldRejection<TField>;
|
|
86
89
|
export function fieldRejection(struct: { readonly fields: Schema.Struct.Fields }, fields?: readonly string[]): FieldRejection<string> {
|
|
87
90
|
return { field: Schema.Literals(fields ?? Object.keys(struct.fields)), message: Schema.String };
|
|
88
91
|
}
|
package/src/resume.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as Atom from "effect/unstable/reactivity/Atom";
|
|
2
|
+
|
|
3
|
+
export type ResumeSource = (resume: () => void) => () => void;
|
|
4
|
+
|
|
5
|
+
type ResumeEvent = "online" | "visibilitychange";
|
|
6
|
+
|
|
7
|
+
export interface ResumeWindow {
|
|
8
|
+
readonly addEventListener: (type: ResumeEvent, listener: () => void) => void;
|
|
9
|
+
readonly document: { readonly visibilityState: string };
|
|
10
|
+
readonly removeEventListener: (type: ResumeEvent, listener: () => void) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ResumeOptions {
|
|
14
|
+
readonly native?: ResumeSource;
|
|
15
|
+
readonly window?: ResumeWindow;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function whileVisible(target: ResumeWindow, type: ResumeEvent): ResumeSource {
|
|
19
|
+
return (resume) => {
|
|
20
|
+
function visible() {
|
|
21
|
+
if (target.document.visibilityState === "visible") {
|
|
22
|
+
resume();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
target.addEventListener(type, visible);
|
|
26
|
+
return () => target.removeEventListener(type, visible);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resumeSignal({ window, native }: ResumeOptions): Atom.Atom<number> {
|
|
31
|
+
const browser = window === undefined ? [] : [whileVisible(window, "visibilitychange"), whileVisible(window, "online")];
|
|
32
|
+
const sources = native === undefined ? browser : [...browser, native];
|
|
33
|
+
return Atom.readable((get) => {
|
|
34
|
+
let count = 0;
|
|
35
|
+
function resume() {
|
|
36
|
+
count += 1;
|
|
37
|
+
get.setSelf(count);
|
|
38
|
+
}
|
|
39
|
+
for (const source of sources) {
|
|
40
|
+
get.addFinalizer(source(resume));
|
|
41
|
+
}
|
|
42
|
+
return count;
|
|
43
|
+
});
|
|
44
|
+
}
|