@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/src/keys.ts CHANGED
@@ -1,36 +1,43 @@
1
- import type { Schema } from "effect";
1
+ import { Schema } from "effect";
2
2
 
3
3
  export type Identity = string | number;
4
4
 
5
- export interface ListKey<Name extends string = string> {
5
+ export interface ListKey<TName extends string = string> {
6
6
  readonly _tag: "List";
7
- readonly collection: Name;
7
+ readonly collection: TName;
8
8
  }
9
9
 
10
- export interface ItemKey<Name extends string = string> {
10
+ export interface ItemKey<TName extends string = string> {
11
11
  readonly _tag: "Item";
12
- readonly collection: Name;
12
+ readonly collection: TName;
13
13
  readonly id: Identity;
14
14
  }
15
15
 
16
16
  export type Key = ListKey | ItemKey;
17
17
 
18
- export interface Collection<Name extends string, Id extends Identity> {
19
- readonly item: (id: Id) => ItemKey<Name>;
20
- readonly list: ListKey<Name>;
21
- readonly name: Name;
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 Name extends string, Id extends Schema.Top & { readonly Type: Identity }>(
25
- name: Name,
26
- _id: Id,
27
- ): Collection<Name, Id["Type"]> => ({
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
- const own = (key: Key): string => (key._tag === "List" ? key.collection : `${key.collection}:${key.id}`);
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<Payload extends Schema.Top | Schema.Struct.Fields> = Payload extends Schema.Struct.Fields
7
- ? Schema.Struct<Payload>
8
- : Payload;
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<Name extends string, Payload extends Schema.Top, Success extends Schema.Top, Specs extends RejectionSpecs>
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<Specs>;
36
- readonly name: Name;
37
- readonly payload: Payload;
38
- readonly Rejection: Rejections<Specs>;
39
- readonly reject: Reject<Specs>;
40
- readonly rejections: Specs;
41
- readonly success: 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<Name extends string, Payload extends Schema.Top, Success extends Schema.Top, Specs extends RejectionSpecs>
45
- extends Operation<Name, Payload, Success, Specs> {
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: Payload["Type"]) => readonly Key[];
52
+ reads: (payload: TPayload["Type"]) => readonly Key[];
48
53
  }
49
54
 
50
- export interface Command<Name extends string, Payload extends Schema.Top, Success extends Schema.Top, Specs extends RejectionSpecs>
51
- extends Operation<Name, Payload, Success, Specs> {
52
- invalidates: (payload: Payload["Type"], result: Success["Type"]) => readonly Key[];
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<Payload extends Schema.Top | Schema.Struct.Fields, Success extends Schema.Top, Specs extends RejectionSpecs> {
57
- readonly payload?: Payload;
58
- readonly rejections?: Specs & MatchingTags<Specs>;
59
- readonly success?: 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
- const payloadSchema = (payload: Loose["payload"]): Schema.Top => {
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
- const operation = <Kind extends OperationShape["kind"]>(kind: Kind, name: string, declaration: Loose) => {
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 Name extends string,
89
- Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
90
- Success extends Schema.Top = Schema.Void,
91
- const Specs extends RejectionSpecs = NoRejections,
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: Name,
94
- declaration: Declaration<Payload, Success, Specs> & {
95
- readonly reads: (payload: PayloadSchema<Payload>["Type"]) => readonly Key[];
104
+ name: TName,
105
+ declaration: Declaration<TPayload, TSuccess, TSpecs> & {
106
+ readonly reads: (payload: PayloadSchema<TPayload>["Type"]) => readonly Key[];
96
107
  },
97
- ): NoInfer<Query<Name, PayloadSchema<Payload>, Success, Specs>>;
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 Name extends string,
104
- Payload extends Schema.Top | Schema.Struct.Fields = Schema.Void,
105
- Success extends Schema.Top = Schema.Void,
106
- const Specs extends RejectionSpecs = NoRejections,
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: Name,
109
- declaration: Declaration<Payload, Success, Specs> & {
110
- readonly invalidates: (payload: PayloadSchema<Payload>["Type"], result: Success["Type"]) => readonly Key[];
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<Name, PayloadSchema<Payload>, Success, Specs>>;
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<Tag extends string, Fields extends Schema.Struct.Fields> = Schema.TaggedStruct<Tag, Fields>["Type"] & Cause.YieldableError;
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<Tag extends string, Fields extends Schema.Struct.Fields> = Schema.Class<
6
- RejectionValue<Tag, Fields>,
7
- Schema.TaggedStruct<Tag, Fields>,
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<Specs extends RejectionSpecs> = {
18
- readonly [Tag in keyof Specs & string]: Specs[Tag] extends TaggedRejection
19
- ? Specs[Tag]
20
- : Specs[Tag] extends Schema.Struct.Fields
21
- ? RejectionClass<Tag, Specs[Tag]>
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<Specs extends RejectionSpecs> = {
26
- [Tag in keyof Specs & string]: Rejections<Specs>[Tag]["Type"];
27
- }[keyof Specs & string];
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<Specs extends RejectionSpecs> = {
30
- readonly [Tag in keyof Specs & string]: Rejections<Specs>[Tag] extends new (
31
- ...args: infer Arguments
32
- ) => infer Value
33
- ? (...args: Arguments) => Effect.Effect<never, Value>
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<Specs extends RejectionSpecs> = [keyof Specs & string] extends [never]
38
+ export type RejectionUnion<TSpecs extends RejectionSpecs> = [keyof TSpecs & string] extends [never]
38
39
  ? Schema.Never
39
- : Schema.Union<readonly Rejections<Specs>[keyof Specs & string][]>;
40
+ : Schema.Union<readonly Rejections<TSpecs>[keyof TSpecs & string][]>;
40
41
 
41
- export type MatchingTags<Specs extends RejectionSpecs> = {
42
- readonly [Tag in keyof Specs]: Specs[Tag] extends TaggedRejection
43
- ? Specs[Tag]["Type"] extends { readonly _tag: Tag }
44
- ? Specs[Tag]
45
- : `The rejection class under "${Tag & string}" must have that _tag`
46
- : Specs[Tag];
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<Specs extends RejectionSpecs> {
50
- readonly error: RejectionUnion<Specs>;
51
- readonly Rejection: Rejections<Specs>;
52
- readonly reject: Reject<Specs>;
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 Specs extends RejectionSpecs>(specs: Specs): RejectionSet<Specs>;
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<Field extends string> = Readonly<{
77
- field: Schema.Literals<readonly Field[]>;
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 Fields extends Schema.Struct.Fields>(struct: { readonly fields: Fields }): FieldRejection<keyof Fields & string>;
82
- export function fieldRejection<const Fields extends Schema.Struct.Fields, const Field extends keyof Fields & string>(
83
- struct: { readonly fields: Fields },
84
- fields: readonly Field[],
85
- ): FieldRejection<Field>;
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
+ }