effect-app 4.0.1 → 4.0.2
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 +7 -0
- package/dist/waitingGate.d.ts +38 -0
- package/dist/waitingGate.d.ts.map +1 -0
- package/dist/waitingGate.js +63 -0
- package/dist/withToast.d.ts.map +1 -1
- package/dist/withToast.js +42 -3
- package/package.json +2 -2
- package/src/waitingGate.ts +82 -0
- package/src/withToast.ts +51 -5
- package/test/dist/waitingGate.test.d.ts.map +1 -0
- package/test/waitingGate.test.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @effect-app/prelude
|
|
2
2
|
|
|
3
|
+
## 4.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 76911ed: Add `suspendWaiting` so a command can park on a dialog without the spinner or the waiting toast. The button stays disabled. Both come back when the effect finishes and the rest of the command runs.
|
|
8
|
+
- 7a99d1b: Upgrade Effect packages to npm `4.0.2`.
|
|
9
|
+
|
|
3
10
|
## 4.0.1
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as Context from "./Context.js";
|
|
2
|
+
import * as Effect from "./Effect.js";
|
|
3
|
+
/**
|
|
4
|
+
* Notified with the suspension depth after each change. `0` means the command
|
|
5
|
+
* is doing work. A positive depth means it is parked on a caller.
|
|
6
|
+
*/
|
|
7
|
+
export type WaitingGateController = (depth: number) => Effect.Effect<void>;
|
|
8
|
+
export interface WaitingGateShape {
|
|
9
|
+
/** `false` for the default gate. Suspension then changes nothing. */
|
|
10
|
+
readonly active: boolean;
|
|
11
|
+
readonly depth: number;
|
|
12
|
+
readonly add: (delta: number) => Effect.Effect<void>;
|
|
13
|
+
/** Stack a controller. The returned function restores the previous one. */
|
|
14
|
+
readonly bind: (controller: WaitingGateController) => () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Per-command gate for {@link suspendWaiting}. Commands provide an active gate.
|
|
18
|
+
* The default is inactive, so the helper is a no-op outside a command.
|
|
19
|
+
*/
|
|
20
|
+
export declare const WaitingGate: Context.Reference<WaitingGateShape>;
|
|
21
|
+
/** Build the gate one command owns. `onDepth` runs synchronously inside `add`. */
|
|
22
|
+
export declare const makeWaitingGate: (onDepth?: (depth: number) => void) => WaitingGateShape;
|
|
23
|
+
/**
|
|
24
|
+
* Run `self` while command chrome is quiet.
|
|
25
|
+
*
|
|
26
|
+
* The spinner and the waiting toast pause for `self` and resume when it
|
|
27
|
+
* finishes, including when it is interrupted. The command stays blocked for
|
|
28
|
+
* the whole run, so the button cannot start again.
|
|
29
|
+
*
|
|
30
|
+
* Outside a command the default gate is inactive and `self` runs unchanged.
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* const decision = yield* suspendWaiting(ask(pickupDate))
|
|
34
|
+
* yield* close(orderId)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const suspendWaiting: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
38
|
+
//# sourceMappingURL=waitingGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waitingGate.d.ts","sourceRoot":"","sources":["../src/waitingGate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AAErC;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACpD,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,CAAC,UAAU,EAAE,qBAAqB,KAAK,MAAM,IAAI,CAAA;CACjE;AASD;;;GAGG;AACH,eAAO,MAAM,WAAW,qCAEtB,CAAA;AAEF,kFAAkF;AAClF,eAAO,MAAM,eAAe,aAAc,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KAAG,gBAwBnE,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAOzF,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as Context from "./Context.js";
|
|
2
|
+
import * as Effect from "./Effect.js";
|
|
3
|
+
const inactiveWaitingGate = {
|
|
4
|
+
active: false,
|
|
5
|
+
depth: 0,
|
|
6
|
+
add: () => Effect.void,
|
|
7
|
+
bind: () => () => { }
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Per-command gate for {@link suspendWaiting}. Commands provide an active gate.
|
|
11
|
+
* The default is inactive, so the helper is a no-op outside a command.
|
|
12
|
+
*/
|
|
13
|
+
export const WaitingGate = Context.Reference("effect-app/WaitingGate", {
|
|
14
|
+
defaultValue: () => inactiveWaitingGate
|
|
15
|
+
});
|
|
16
|
+
/** Build the gate one command owns. `onDepth` runs synchronously inside `add`. */
|
|
17
|
+
export const makeWaitingGate = (onDepth) => {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let controller;
|
|
20
|
+
return {
|
|
21
|
+
active: true,
|
|
22
|
+
get depth() {
|
|
23
|
+
return depth;
|
|
24
|
+
},
|
|
25
|
+
add: (delta) => Effect.uninterruptible(Effect.gen(function* () {
|
|
26
|
+
depth += delta;
|
|
27
|
+
if (depth < 0)
|
|
28
|
+
depth = 0;
|
|
29
|
+
const next = depth;
|
|
30
|
+
onDepth?.(next);
|
|
31
|
+
if (controller)
|
|
32
|
+
yield* controller(next);
|
|
33
|
+
})),
|
|
34
|
+
bind: (next) => {
|
|
35
|
+
const previous = controller;
|
|
36
|
+
controller = next;
|
|
37
|
+
return () => {
|
|
38
|
+
if (controller === next)
|
|
39
|
+
controller = previous;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Run `self` while command chrome is quiet.
|
|
46
|
+
*
|
|
47
|
+
* The spinner and the waiting toast pause for `self` and resume when it
|
|
48
|
+
* finishes, including when it is interrupted. The command stays blocked for
|
|
49
|
+
* the whole run, so the button cannot start again.
|
|
50
|
+
*
|
|
51
|
+
* Outside a command the default gate is inactive and `self` runs unchanged.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const decision = yield* suspendWaiting(ask(pickupDate))
|
|
55
|
+
* yield* close(orderId)
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export const suspendWaiting = (self) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
59
|
+
const gate = yield* WaitingGate;
|
|
60
|
+
yield* gate.add(1);
|
|
61
|
+
return yield* Effect.ensuring(restore(self), gate.add(-1));
|
|
62
|
+
}));
|
|
63
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2FpdGluZ0dhdGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvd2FpdGluZ0dhdGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUE7QUFDdkMsT0FBTyxLQUFLLE1BQU0sTUFBTSxhQUFhLENBQUE7QUFpQnJDLE1BQU0sbUJBQW1CLEdBQXFCO0lBQzVDLE1BQU0sRUFBRSxLQUFLO0lBQ2IsS0FBSyxFQUFFLENBQUM7SUFDUixHQUFHLEVBQUUsR0FBRyxFQUFFLENBQUMsTUFBTSxDQUFDLElBQUk7SUFDdEIsSUFBSSxFQUFFLEdBQUcsRUFBRSxDQUFDLEdBQUcsRUFBRSxHQUFFLENBQUM7Q0FDckIsQ0FBQTtBQUVEOzs7R0FHRztBQUNILE1BQU0sQ0FBQyxNQUFNLFdBQVcsR0FBRyxPQUFPLENBQUMsU0FBUyxDQUFtQix3QkFBd0IsRUFBRTtJQUN2RixZQUFZLEVBQUUsR0FBRyxFQUFFLENBQUMsbUJBQW1CO0NBQ3hDLENBQUMsQ0FBQTtBQUVGLGtGQUFrRjtBQUNsRixNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsQ0FBQyxPQUFpQyxFQUFvQixFQUFFO0lBQ3JGLElBQUksS0FBSyxHQUFHLENBQUMsQ0FBQTtJQUNiLElBQUksVUFBNkMsQ0FBQTtJQUNqRCxPQUFPO1FBQ0wsTUFBTSxFQUFFLElBQUk7UUFDWixJQUFJLEtBQUs7WUFDUCxPQUFPLEtBQUssQ0FBQTtRQUNkLENBQUM7UUFDRCxHQUFHLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUNiLE1BQU0sQ0FBQyxlQUFlLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUM7WUFDekMsS0FBSyxJQUFJLEtBQUssQ0FBQTtZQUNkLElBQUksS0FBSyxHQUFHLENBQUM7Z0JBQUUsS0FBSyxHQUFHLENBQUMsQ0FBQTtZQUN4QixNQUFNLElBQUksR0FBRyxLQUFLLENBQUE7WUFDbEIsT0FBTyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUE7WUFDZixJQUFJLFVBQVU7Z0JBQUUsS0FBSyxDQUFDLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFBO1FBQ3pDLENBQUMsQ0FBQyxDQUFDO1FBQ0wsSUFBSSxFQUFFLENBQUMsSUFBSSxFQUFFLEVBQUU7WUFDYixNQUFNLFFBQVEsR0FBRyxVQUFVLENBQUE7WUFDM0IsVUFBVSxHQUFHLElBQUksQ0FBQTtZQUNqQixPQUFPLEdBQUcsRUFBRTtnQkFDVixJQUFJLFVBQVUsS0FBSyxJQUFJO29CQUFFLFVBQVUsR0FBRyxRQUFRLENBQUE7WUFDaEQsQ0FBQyxDQUFBO1FBQ0gsQ0FBQztLQUNGLENBQUE7QUFDSCxDQUFDLENBQUE7QUFFRDs7Ozs7Ozs7Ozs7OztHQWFHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLENBQVUsSUFBNEIsRUFBMEIsRUFBRSxDQUM5RixNQUFNLENBQUMsbUJBQW1CLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUNyQyxNQUFNLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQztJQUNsQixNQUFNLElBQUksR0FBRyxLQUFLLENBQUMsQ0FBQyxXQUFXLENBQUE7SUFDL0IsS0FBSyxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQTtJQUNsQixPQUFPLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFBO0FBQzVELENBQUMsQ0FBQyxDQUNILENBQUEifQ==
|
package/dist/withToast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"withToast.d.ts","sourceRoot":"","sources":["../src/withToast.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAA;AAE1C,OAAO,EAAkB,KAAK,EAAgB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"withToast.d.ts","sourceRoot":"","sources":["../src/withToast.ts"],"names":[],"mappings":"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"}
|
package/dist/withToast.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as Layer from "./Layer.js";
|
|
|
6
6
|
import * as S from "./Schema.js";
|
|
7
7
|
import { CurrentToastId, Toast } from "./toast.js";
|
|
8
8
|
import { wrapEffect } from "./utils.js";
|
|
9
|
+
import { WaitingGate } from "./waitingGate.js";
|
|
9
10
|
// @effect-diagnostics-next-line missingEffectServiceDependency:off
|
|
10
11
|
export class WithToast extends Context.Service()("WithToast", {
|
|
11
12
|
make: Effect.gen(function* () {
|
|
@@ -22,8 +23,46 @@ export class WithToast extends Context.Service()("WithToast", {
|
|
|
22
23
|
const toastId = t === null
|
|
23
24
|
? stableToastId
|
|
24
25
|
: stableToastId ?? `wait-${Math.random().toString(36).slice(2)}`;
|
|
25
|
-
const
|
|
26
|
-
|
|
26
|
+
const gate = yield* WaitingGate;
|
|
27
|
+
let waitingFiber;
|
|
28
|
+
let toastShown = false;
|
|
29
|
+
let unbind = () => { };
|
|
30
|
+
const armWaiting = () => Effect.gen(function* () {
|
|
31
|
+
if (waitingFiber !== undefined || t === null || toastId === undefined)
|
|
32
|
+
return;
|
|
33
|
+
waitingFiber = yield* Effect.forkChild(Effect.sleep("1 seconds").pipe(Effect.andThen(Effect.uninterruptible(Effect.gen(function* () {
|
|
34
|
+
toastShown = true;
|
|
35
|
+
yield* toast.info(t, { id: toastId, timeout: Infinity, ...meta });
|
|
36
|
+
}))), Effect.asVoid));
|
|
37
|
+
});
|
|
38
|
+
const disarmWaiting = () => Effect.gen(function* () {
|
|
39
|
+
const fiber = waitingFiber;
|
|
40
|
+
waitingFiber = undefined;
|
|
41
|
+
if (fiber)
|
|
42
|
+
yield* Fiber.interrupt(fiber);
|
|
43
|
+
if (toastShown && toastId !== undefined) {
|
|
44
|
+
toastShown = false;
|
|
45
|
+
yield* toast.dismiss(toastId);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// An active gate pauses the toast while `suspendWaiting` is parked and
|
|
49
|
+
// starts a fresh one-second delay when work resumes. The inactive
|
|
50
|
+
// default keeps the original fork.
|
|
51
|
+
if (t !== null && gate.active) {
|
|
52
|
+
unbind = gate.bind((depth) => (depth === 0 ? armWaiting() : disarmWaiting()));
|
|
53
|
+
if (gate.depth === 0)
|
|
54
|
+
yield* armWaiting();
|
|
55
|
+
}
|
|
56
|
+
else if (t !== null) {
|
|
57
|
+
waitingFiber = yield* Effect.forkChild(Effect.sleep("1 seconds").pipe(Effect.andThen(toast.info(t, { id: toastId, timeout: Infinity, ...meta })), Effect.asVoid));
|
|
58
|
+
}
|
|
59
|
+
const interruptWaiting = Effect.uninterruptible(Effect.gen(function* () {
|
|
60
|
+
unbind();
|
|
61
|
+
const fiber = waitingFiber;
|
|
62
|
+
waitingFiber = undefined;
|
|
63
|
+
if (fiber)
|
|
64
|
+
yield* Fiber.interrupt(fiber);
|
|
65
|
+
}));
|
|
27
66
|
return yield* self.pipe(Effect.tap(Effect.fnUntraced(function* (a) {
|
|
28
67
|
yield* interruptWaiting;
|
|
29
68
|
const t = yield* wrapEffect(options.onSuccess)(a, ...args);
|
|
@@ -61,4 +100,4 @@ export class WithToast extends Context.Service()("WithToast", {
|
|
|
61
100
|
static Default = this.DefaultWithoutDependencies;
|
|
62
101
|
static handle = (options) => (self, ...args) => this.use((_) => _(options)(self, ...args));
|
|
63
102
|
}
|
|
64
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
103
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid2l0aFRvYXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL3dpdGhUb2FzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssS0FBSyxNQUFNLGNBQWMsQ0FBQTtBQUNyQyxPQUFPLEtBQUssS0FBSyxNQUFNLGNBQWMsQ0FBQTtBQUNyQyxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQTtBQUN2QyxPQUFPLEtBQUssTUFBTSxNQUFNLGFBQWEsQ0FBQTtBQUNyQyxPQUFPLEtBQUssS0FBSyxNQUFNLFlBQVksQ0FBQTtBQUVuQyxPQUFPLEtBQUssQ0FBQyxNQUFNLGFBQWEsQ0FBQTtBQUNoQyxPQUFPLEVBQUUsY0FBYyxFQUFFLEtBQUssRUFBZ0IsTUFBTSxZQUFZLENBQUE7QUFDaEUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLFlBQVksQ0FBQTtBQUN2QyxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sa0JBQWtCLENBQUE7QUFrQzlDLG1FQUFtRTtBQUNuRSxNQUFNLE9BQU8sU0FBVSxTQUFRLE9BQU8sQ0FBQyxPQUFPLEVBQWEsQ0FBQyxXQUFXLEVBQUU7SUFDdkUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUMsUUFBUSxDQUFDO1FBQ3hCLE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxDQUFDLEtBQUssQ0FBQTtRQUMxQixPQUFPLENBQ0wsT0FBbUQsRUFDbkQsRUFBRSxDQUNGLE1BQU0sQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLEVBQUMsSUFBNEIsRUFBRSxHQUFHLElBQVU7WUFDckUsTUFBTSxXQUFXLEdBQUcsT0FBTyxDQUFDLE9BQU8sSUFBSSxLQUFLLENBQUE7WUFFNUMsTUFBTSxhQUFhLEdBQUcsT0FBTyxPQUFPLENBQUMsYUFBYSxLQUFLLFVBQVU7Z0JBQy9ELENBQUMsQ0FBQyxPQUFPLENBQUMsYUFBYSxDQUFDLEdBQUcsSUFBSSxDQUFDO2dCQUNoQyxDQUFDLENBQUMsT0FBTyxDQUFDLGFBQWEsQ0FBQTtZQUV6QixNQUFNLFNBQVMsR0FBVyxLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsV0FBVyxDQUFDLElBQUksQ0FDdEQsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxFQUNsQyxNQUFNLENBQUMsYUFBYSxDQUFDLEdBQUcsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FDOUMsQ0FBQTtZQUNELE1BQU0sT0FBTyxHQUFHLE9BQU8sQ0FBQyxPQUFPLENBQUE7WUFDL0IsTUFBTSxJQUFJLEdBQUcsRUFBRSxHQUFHLENBQUMsT0FBTyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsU0FBUyxFQUFFLENBQUE7WUFFekUsTUFBTSxDQUFDLEdBQUcsS0FBSyxDQUFDLENBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFBO1lBQ3ZELE1BQU0sT0FBTyxHQUF3QixDQUFDLEtBQUssSUFBSTtnQkFDN0MsQ0FBQyxDQUFDLGFBQWE7Z0JBQ2YsQ0FBQyxDQUFDLGFBQWEsSUFBSSxRQUFRLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUE7WUFFbEUsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLENBQUMsV0FBVyxDQUFBO1lBQy9CLElBQUksWUFBMkMsQ0FBQTtZQUMvQyxJQUFJLFVBQVUsR0FBRyxLQUFLLENBQUE7WUFDdEIsSUFBSSxNQUFNLEdBQUcsR0FBRyxFQUFFLEdBQUUsQ0FBQyxDQUFBO1lBRXJCLE1BQU0sVUFBVSxHQUFHLEdBQUcsRUFBRSxDQUN0QixNQUFNLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQztnQkFDbEIsSUFBSSxZQUFZLEtBQUssU0FBUyxJQUFJLENBQUMsS0FBSyxJQUFJLElBQUksT0FBTyxLQUFLLFNBQVM7b0JBQUUsT0FBTTtnQkFDN0UsWUFBWSxHQUFHLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQ3BDLE1BQU0sQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLENBQUMsSUFBSSxDQUM1QixNQUFNLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxlQUFlLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUM7b0JBQ3hELFVBQVUsR0FBRyxJQUFJLENBQUE7b0JBQ2pCLEtBQUssQ0FBQyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxFQUFFLEVBQUUsRUFBRSxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLEdBQUcsSUFBSSxFQUFFLENBQUMsQ0FBQTtnQkFDbkUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUNKLE1BQU0sQ0FBQyxNQUFNLENBQ2QsQ0FDRixDQUFBO1lBQ0gsQ0FBQyxDQUFDLENBQUE7WUFFSixNQUFNLGFBQWEsR0FBRyxHQUFHLEVBQUUsQ0FDekIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUM7Z0JBQ2xCLE1BQU0sS0FBSyxHQUFHLFlBQVksQ0FBQTtnQkFDMUIsWUFBWSxHQUFHLFNBQVMsQ0FBQTtnQkFDeEIsSUFBSSxLQUFLO29CQUFFLEtBQUssQ0FBQyxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUE7Z0JBQ3hDLElBQUksVUFBVSxJQUFJLE9BQU8sS0FBSyxTQUFTLEVBQUUsQ0FBQztvQkFDeEMsVUFBVSxHQUFHLEtBQUssQ0FBQTtvQkFDbEIsS0FBSyxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsQ0FBQTtnQkFDL0IsQ0FBQztZQUNILENBQUMsQ0FBQyxDQUFBO1lBRUosdUVBQXVFO1lBQ3ZFLGtFQUFrRTtZQUNsRSxtQ0FBbUM7WUFDbkMsSUFBSSxDQUFDLEtBQUssSUFBSSxJQUFJLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDOUIsTUFBTSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsS0FBSyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQyxDQUFDLGFBQWEsRUFBRSxDQUFDLENBQUMsQ0FBQTtnQkFDN0UsSUFBSSxJQUFJLENBQUMsS0FBSyxLQUFLLENBQUM7b0JBQUUsS0FBSyxDQUFDLENBQUMsVUFBVSxFQUFFLENBQUE7WUFDM0MsQ0FBQztpQkFBTSxJQUFJLENBQUMsS0FBSyxJQUFJLEVBQUUsQ0FBQztnQkFDdEIsWUFBWSxHQUFHLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQ3BDLE1BQU0sQ0FBQyxLQUFLLENBQUMsV0FBVyxDQUFDLENBQUMsSUFBSSxDQUM1QixNQUFNLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxFQUFFLEVBQUUsRUFBRSxFQUFFLE9BQVEsRUFBRSxPQUFPLEVBQUUsUUFBUSxFQUFFLEdBQUcsSUFBSSxFQUFFLENBQUMsQ0FBQyxFQUMzRSxNQUFNLENBQUMsTUFBTSxDQUNkLENBQ0YsQ0FBQTtZQUNILENBQUM7WUFFRCxNQUFNLGdCQUFnQixHQUFHLE1BQU0sQ0FBQyxlQUFlLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUM7Z0JBQ2xFLE1BQU0sRUFBRSxDQUFBO2dCQUNSLE1BQU0sS0FBSyxHQUFHLFlBQVksQ0FBQTtnQkFDMUIsWUFBWSxHQUFHLFNBQVMsQ0FBQTtnQkFDeEIsSUFBSSxLQUFLO29CQUFFLEtBQUssQ0FBQyxDQUFDLEtBQUssQ0FBQyxTQUFTLENBQUMsS0FBSyxDQUFDLENBQUE7WUFDMUMsQ0FBQyxDQUFDLENBQUMsQ0FBQTtZQUVILE9BQU8sS0FBSyxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksQ0FDckIsTUFBTSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxFQUFDLENBQUM7Z0JBQ3RDLEtBQUssQ0FBQyxDQUFDLGdCQUFnQixDQUFBO2dCQUN2QixNQUFNLENBQUMsR0FBRyxLQUFLLENBQUMsQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLElBQUksQ0FBQyxDQUFBO2dCQUMxRCxJQUFJLENBQUMsS0FBSyxJQUFJLEVBQUUsQ0FBQztvQkFDZixPQUFNO2dCQUNSLENBQUM7Z0JBQ0QsS0FBSyxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FDbEIsQ0FBQyxFQUNELE9BQU8sS0FBSyxTQUFTO29CQUNuQixDQUFDLENBQUMsRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLE9BQU8sRUFBRSxXQUFXLEVBQUUsR0FBRyxJQUFJLEVBQUU7b0JBQ2hELENBQUMsQ0FBQyxFQUFFLE9BQU8sRUFBRSxXQUFXLEVBQUUsR0FBRyxJQUFJLEVBQUUsQ0FDdEMsQ0FBQTtZQUNILENBQUMsQ0FBQyxDQUFDLEVBQ0gsTUFBTSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxFQUFDLEtBQUs7Z0JBQy9DLEtBQUssQ0FBQyxDQUFDLGdCQUFnQixDQUFBO2dCQUN2QixLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUNwQixrQ0FBa0MsR0FBRyxLQUFLLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUN4RCxLQUFLLENBQUMsaUJBQWlCLENBQUMsS0FBSyxDQUFDLEVBQzlCLEtBQUssQ0FDTixDQUFBO2dCQUVELElBQUksS0FBSyxDQUFDLGlCQUFpQixDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7b0JBQ25DLElBQUksT0FBTzt3QkFBRSxLQUFLLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFBO29CQUMxQyxPQUFNO2dCQUNSLENBQUM7Z0JBRUQsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLFlBQVksS0FBSyxLQUFLO29CQUM3QyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQzlCLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLFlBQVksSUFBSSxDQUFDLE9BQU8sV0FBVyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUMsRUFDdEUsTUFBTSxDQUFDLGFBQWEsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FDL0I7b0JBQ0QsQ0FBQyxDQUFDLEVBQUUsQ0FBQTtnQkFFTixNQUFNLENBQUMsR0FBRyxLQUFLLENBQUMsQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxlQUFlLENBQUMsS0FBSyxDQUFDLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQTtnQkFDckYsTUFBTSxJQUFJLEdBQUcsRUFBRSxPQUFPLEVBQUUsV0FBVyxHQUFHLENBQUMsRUFBRSxHQUFHLElBQUksRUFBRSxDQUFBO2dCQUVsRCxJQUFJLE9BQU8sQ0FBQyxLQUFLLFFBQVEsRUFBRSxDQUFDO29CQUMxQixNQUFNLE9BQU8sR0FBRyxDQUFDLENBQUMsT0FBTyxHQUFHLFFBQVEsQ0FBQTtvQkFDcEMsT0FBTyxDQUFDLENBQUMsS0FBSyxLQUFLLE1BQU07d0JBQ3ZCLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLE9BQU8sRUFBRSxPQUFPLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLEdBQUcsSUFBSSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO3dCQUN4RixDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxPQUFPLEVBQUUsT0FBTyxLQUFLLFNBQVMsQ0FBQyxDQUFDLENBQUMsRUFBRSxHQUFHLElBQUksRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFBO2dCQUMxRixDQUFDO2dCQUNELEtBQUssQ0FBQyxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFHLFFBQVEsRUFBRSxPQUFPLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLEdBQUcsSUFBSSxFQUFFLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUE7WUFDM0YsQ0FBQyxFQUFFLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQyxFQUMzQixPQUFPLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsY0FBYyxDQUFDLGNBQWMsRUFBRSxjQUFjLENBQUMsRUFBRSxDQUFDLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUN6RyxDQUFBO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDTixDQUFDLENBQUM7Q0FDSCxDQUFDO0lBQ0EsTUFBTSxDQUFVLDBCQUEwQixHQUFHLEtBQUssQ0FBQyxNQUFNLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQTtJQUMxRSxNQUFNLENBQVUsT0FBTyxHQUFHLElBQUksQ0FBQywwQkFBMEIsQ0FBQTtJQUV6RCxNQUFNLENBQVUsTUFBTSxHQUFHLENBQ3ZCLE9BQW1ELEVBQ3VELEVBQUUsQ0FDOUcsQ0FBQyxJQUFJLEVBQUUsR0FBRyxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBa0MsT0FBTyxDQUFDLENBQUMsSUFBSSxFQUFFLEdBQUcsSUFBSSxDQUFDLENBQUMsQ0FBQTtDQUMvRiJ9
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-app",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"vitest": "^4.1.7"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"effect": "^4.0.
|
|
28
|
+
"effect": "^4.0.2"
|
|
29
29
|
},
|
|
30
30
|
"typesVersions": {
|
|
31
31
|
"*": {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Context from "./Context.ts"
|
|
2
|
+
import * as Effect from "./Effect.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Notified with the suspension depth after each change. `0` means the command
|
|
6
|
+
* is doing work. A positive depth means it is parked on a caller.
|
|
7
|
+
*/
|
|
8
|
+
export type WaitingGateController = (depth: number) => Effect.Effect<void>
|
|
9
|
+
|
|
10
|
+
export interface WaitingGateShape {
|
|
11
|
+
/** `false` for the default gate. Suspension then changes nothing. */
|
|
12
|
+
readonly active: boolean
|
|
13
|
+
readonly depth: number
|
|
14
|
+
readonly add: (delta: number) => Effect.Effect<void>
|
|
15
|
+
/** Stack a controller. The returned function restores the previous one. */
|
|
16
|
+
readonly bind: (controller: WaitingGateController) => () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const inactiveWaitingGate: WaitingGateShape = {
|
|
20
|
+
active: false,
|
|
21
|
+
depth: 0,
|
|
22
|
+
add: () => Effect.void,
|
|
23
|
+
bind: () => () => {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-command gate for {@link suspendWaiting}. Commands provide an active gate.
|
|
28
|
+
* The default is inactive, so the helper is a no-op outside a command.
|
|
29
|
+
*/
|
|
30
|
+
export const WaitingGate = Context.Reference<WaitingGateShape>("effect-app/WaitingGate", {
|
|
31
|
+
defaultValue: () => inactiveWaitingGate
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/** Build the gate one command owns. `onDepth` runs synchronously inside `add`. */
|
|
35
|
+
export const makeWaitingGate = (onDepth?: (depth: number) => void): WaitingGateShape => {
|
|
36
|
+
let depth = 0
|
|
37
|
+
let controller: WaitingGateController | undefined
|
|
38
|
+
return {
|
|
39
|
+
active: true,
|
|
40
|
+
get depth() {
|
|
41
|
+
return depth
|
|
42
|
+
},
|
|
43
|
+
add: (delta) =>
|
|
44
|
+
Effect.uninterruptible(Effect.gen(function*() {
|
|
45
|
+
depth += delta
|
|
46
|
+
if (depth < 0) depth = 0
|
|
47
|
+
const next = depth
|
|
48
|
+
onDepth?.(next)
|
|
49
|
+
if (controller) yield* controller(next)
|
|
50
|
+
})),
|
|
51
|
+
bind: (next) => {
|
|
52
|
+
const previous = controller
|
|
53
|
+
controller = next
|
|
54
|
+
return () => {
|
|
55
|
+
if (controller === next) controller = previous
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run `self` while command chrome is quiet.
|
|
63
|
+
*
|
|
64
|
+
* The spinner and the waiting toast pause for `self` and resume when it
|
|
65
|
+
* finishes, including when it is interrupted. The command stays blocked for
|
|
66
|
+
* the whole run, so the button cannot start again.
|
|
67
|
+
*
|
|
68
|
+
* Outside a command the default gate is inactive and `self` runs unchanged.
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* const decision = yield* suspendWaiting(ask(pickupDate))
|
|
72
|
+
* yield* close(orderId)
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const suspendWaiting = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
76
|
+
Effect.uninterruptibleMask((restore) =>
|
|
77
|
+
Effect.gen(function*() {
|
|
78
|
+
const gate = yield* WaitingGate
|
|
79
|
+
yield* gate.add(1)
|
|
80
|
+
return yield* Effect.ensuring(restore(self), gate.add(-1))
|
|
81
|
+
})
|
|
82
|
+
)
|
package/src/withToast.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type * as Option from "./Option.ts"
|
|
|
7
7
|
import * as S from "./Schema.ts"
|
|
8
8
|
import { CurrentToastId, Toast, type ToastId } from "./toast.ts"
|
|
9
9
|
import { wrapEffect } from "./utils.ts"
|
|
10
|
+
import { WaitingGate } from "./waitingGate.ts"
|
|
10
11
|
|
|
11
12
|
export interface ToastOptions<A, E, Args extends ReadonlyArray<unknown>, WaiR, SucR, ErrR> {
|
|
12
13
|
stableToastId?: undefined | string | ((...args: Args) => string | undefined)
|
|
@@ -66,12 +67,57 @@ export class WithToast extends Context.Service<WithToast>()("WithToast", {
|
|
|
66
67
|
? stableToastId
|
|
67
68
|
: stableToastId ?? `wait-${Math.random().toString(36).slice(2)}`
|
|
68
69
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const gate = yield* WaitingGate
|
|
71
|
+
let waitingFiber: Fiber.Fiber<void> | undefined
|
|
72
|
+
let toastShown = false
|
|
73
|
+
let unbind = () => {}
|
|
74
|
+
|
|
75
|
+
const armWaiting = () =>
|
|
76
|
+
Effect.gen(function*() {
|
|
77
|
+
if (waitingFiber !== undefined || t === null || toastId === undefined) return
|
|
78
|
+
waitingFiber = yield* Effect.forkChild(
|
|
79
|
+
Effect.sleep("1 seconds").pipe(
|
|
80
|
+
Effect.andThen(Effect.uninterruptible(Effect.gen(function*() {
|
|
81
|
+
toastShown = true
|
|
82
|
+
yield* toast.info(t, { id: toastId, timeout: Infinity, ...meta })
|
|
83
|
+
}))),
|
|
84
|
+
Effect.asVoid
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const disarmWaiting = () =>
|
|
90
|
+
Effect.gen(function*() {
|
|
91
|
+
const fiber = waitingFiber
|
|
92
|
+
waitingFiber = undefined
|
|
93
|
+
if (fiber) yield* Fiber.interrupt(fiber)
|
|
94
|
+
if (toastShown && toastId !== undefined) {
|
|
95
|
+
toastShown = false
|
|
96
|
+
yield* toast.dismiss(toastId)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// An active gate pauses the toast while `suspendWaiting` is parked and
|
|
101
|
+
// starts a fresh one-second delay when work resumes. The inactive
|
|
102
|
+
// default keeps the original fork.
|
|
103
|
+
if (t !== null && gate.active) {
|
|
104
|
+
unbind = gate.bind((depth) => (depth === 0 ? armWaiting() : disarmWaiting()))
|
|
105
|
+
if (gate.depth === 0) yield* armWaiting()
|
|
106
|
+
} else if (t !== null) {
|
|
107
|
+
waitingFiber = yield* Effect.forkChild(
|
|
108
|
+
Effect.sleep("1 seconds").pipe(
|
|
109
|
+
Effect.andThen(toast.info(t, { id: toastId!, timeout: Infinity, ...meta })),
|
|
110
|
+
Effect.asVoid
|
|
111
|
+
)
|
|
72
112
|
)
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const interruptWaiting = Effect.uninterruptible(Effect.gen(function*() {
|
|
116
|
+
unbind()
|
|
117
|
+
const fiber = waitingFiber
|
|
118
|
+
waitingFiber = undefined
|
|
119
|
+
if (fiber) yield* Fiber.interrupt(fiber)
|
|
120
|
+
}))
|
|
75
121
|
|
|
76
122
|
return yield* self.pipe(
|
|
77
123
|
Effect.tap(Effect.fnUntraced(function*(a) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"waitingGate.test.d.ts","sourceRoot":"","sources":["../waitingGate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as Deferred from "effect/Deferred"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Fiber from "effect/Fiber"
|
|
4
|
+
import { describe, expect, test } from "vitest"
|
|
5
|
+
import { makeWaitingGate, suspendWaiting, WaitingGate } from "../src/waitingGate.js"
|
|
6
|
+
|
|
7
|
+
const run = <A, E>(self: Effect.Effect<A, E, never>) => Effect.runPromise(self)
|
|
8
|
+
|
|
9
|
+
describe("suspendWaiting", () => {
|
|
10
|
+
test("brackets depth and tells the controller", async () => {
|
|
11
|
+
const depths: Array<number> = []
|
|
12
|
+
const gate = makeWaitingGate((depth) => depths.push(depth))
|
|
13
|
+
gate.bind((depth) => Effect.sync(() => depths.push(depth)))
|
|
14
|
+
|
|
15
|
+
await run(
|
|
16
|
+
suspendWaiting(Effect.sync(() => {
|
|
17
|
+
expect(gate.depth).toBe(1)
|
|
18
|
+
}))
|
|
19
|
+
.pipe(Effect.provideService(WaitingGate, gate))
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(gate.depth).toBe(0)
|
|
23
|
+
expect(depths).toEqual([1, 1, 0, 0])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("nests", async () => {
|
|
27
|
+
const gate = makeWaitingGate()
|
|
28
|
+
await run(
|
|
29
|
+
suspendWaiting(suspendWaiting(Effect.sync(() => {
|
|
30
|
+
expect(gate.depth).toBe(2)
|
|
31
|
+
})))
|
|
32
|
+
.pipe(Effect.provideService(WaitingGate, gate))
|
|
33
|
+
)
|
|
34
|
+
expect(gate.depth).toBe(0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("restores depth when the effect is interrupted", async () => {
|
|
38
|
+
const gate = makeWaitingGate()
|
|
39
|
+
await run(Effect.gen(function*() {
|
|
40
|
+
const entered = yield* Deferred.make<void>()
|
|
41
|
+
const fiber = yield* suspendWaiting(Effect.gen(function*() {
|
|
42
|
+
yield* Deferred.succeed(entered, undefined)
|
|
43
|
+
return yield* Effect.never
|
|
44
|
+
}))
|
|
45
|
+
.pipe(
|
|
46
|
+
Effect.provideService(WaitingGate, gate),
|
|
47
|
+
Effect.forkChild
|
|
48
|
+
)
|
|
49
|
+
yield* Deferred.await(entered)
|
|
50
|
+
expect(gate.depth).toBe(1)
|
|
51
|
+
yield* Fiber.interrupt(fiber)
|
|
52
|
+
expect(gate.depth).toBe(0)
|
|
53
|
+
}))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("does nothing on the default gate", async () => {
|
|
57
|
+
await run(Effect.gen(function*() {
|
|
58
|
+
const gate = yield* WaitingGate
|
|
59
|
+
expect(gate.active).toBe(false)
|
|
60
|
+
yield* suspendWaiting(Effect.void)
|
|
61
|
+
expect(gate.depth).toBe(0)
|
|
62
|
+
}))
|
|
63
|
+
})
|
|
64
|
+
})
|